Skip to content

feat(eip8198): derive the base fee change from block spacing (EIPs#12552) - #14984

Draft
Marchhill wants to merge 6 commits into
feature/eip-8198from
feat/eip8198-derived-base-fee
Draft

Marchhill wants to merge 6 commits into
feature/eip-8198from
feat/eip8198-derived-base-fee

Conversation

@Marchhill

Copy link
Copy Markdown
Contributor

Changes

Implements ethereum/EIPs#12552 at 57884154, stacked on #14606. The max base-fee change is now derived from the spacing of a block and its parent instead of being a fixed 5/48.

  • Rule (with EIP-8198 on): delta = target_fee_gas_delta * s // (D * 12 * k), where s = header.timestamp - parent.timestamp, k = header.slot_number - parent.slot_number and D = BaseFeeMaxChangeDenominator (8). It multiplies before dividing.
    • This gives 1/8 at 12 s slots, 5/48 at 10 s, 1/12 at 8 s and 1/16 at 6 s, and missed slots don't change it.
    • s is capped at D * 12 * k, so the change is never more than the whole base fee. The cap comes from execution-specs#3873; the EIP text doesn't have it.
  • Validation: under EIP-8198 the slot number must be greater than the parent's. This is checked before the base fee, as in execution-specs#3873.
  • Fallback: a parent without a slot number, or invalid spacing, uses the EIP-1559 rule. Header validation rejects invalid spacing anyway.
  • Calculator: IBaseFeeCalculator.Calculate now takes the child's timestamp and slot number. Validation, block production, the tx source, simulate, trace, Clique, Xdc, Optimism and t8n pass the real header.
  • Predictions: eth_baseFee, eth_fillTransaction, the eth_feeHistory head estimate, the txpool's NextBaseFee, the inclusion-list builder and the prewarmer assume the next block lands one slot after the parent, using the slot schedule (eip8198SlotDurationMs / Blocks.SecondsPerSlot).
    • The base-fee and min-gas-price tx filters only see the parent, so they assume one 12 s slot. That is the 1/8 rule.
  • Removed: BaseFeeMaxChangeNumerator, BaseFeeMaxChange.ScaledToSlot and Eip8198Constants.DesignSlotDurationMs. EIP-8198 no longer overrides a configured denominator; it scales it.
  • Blob parameters: Bogota no longer hard-codes its blob parameters (12/18/12018519); it inherits Amsterdam's (14/21/11684671). This follows execution-specs#3871, and it is a separate commit. Devnets set their values through blobSchedule, and the fixtures through config.blobSchedule.

No behaviour changes before EIP-8198 activates.

Fixture status: merge after new fixtures

tests-heze-devnet@v2.0.0 was filled with 5/48 and puts every block at slot 0, so most Bogota block tests fail here with SlotNumberNotAfterParent. The remaining failures are base-fee mismatches.

Fixtures filled from execution-specs#3873 (c768e6c5) for eip8198_quick_slots and eip7843_slotnum pass:

  • 155/155 block tests;
  • 34/34 state tests.

The engine variants from that branch use engine_newPayloadV5 for Bogota, but Bogota here is V6 because it includes FOCIL, so they can't be run as is.

Heze v2, Bogota and its transitions:

Suite Base #14606 This PR Failure reasons
blockchain_tests 24,469/24,469 1,994/24,469 22,253 slot not after parent, 220 base fee mismatch, 2 base_fee_wrong_rule_across_fork[fork_block_old_rule] (now valid at 12 s)
blockchain_tests_engine all pass (#14606) 926/26,719 25,571 slot not after parent, 220 base fee mismatch, 2 fork_block_old_rule
state_tests 16,496/16,496

The PR has to wait for Heze fixtures filled with execution-specs#3873, which uses increasing slot numbers and the derived rule.

Types of changes

  • New feature (a non-breaking change that adds functionality)
  • Breaking change (EIP-8198 base fee rule)

Testing

Requires testing

  • Yes

If yes, did you write tests?

  • Yes

Unit tests cover 12/10/8/6 s slots, missed slots, slot not increasing, a parent without a slot number, spacing above 96 s, overflow of 256 and 64 bits, pre-fork forks, activation, and the RPC prediction paths.

Implements ethereum/EIPs#12552: the maximum change is
seconds_elapsed / (8 * 12 * slots_elapsed) from the header timestamps and
EIP-7843 slot numbers, capped at the whole base fee as in
execution-specs#3873. Under EIP-8198 the slot number must increase.
Predictions place the next block in the slot after the parent.
ethereum/EIPs#12552 drops the provisional blob values; a slot duration
change brings its own EIP-7892 blobSchedule entry (execution-specs#3871).
Bogota now inherits Amsterdam's target 14, maximum 21 and update fraction
11684671.
@Marchhill

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @Marchhill's task in 2m 24s —— View job


Review of PR #14984: EIP-8198 base fee derived from block spacing

  • Gather context (diffed the two PR commits HEAD~2..HEAD; the merge base with origin/feature/eip-8198 wasn't in the shallow clone, but the stat matches the PR at 52 files, +474/−297)
  • Review the core base-fee rule and validation
  • Review the callers and prediction paths
  • Review the tests and comments
  • Post the summary

Verdict

I found no correctness problems. The one blocker is the one you already listed in the description: the Heze/Bogota fixtures need to be refilled from execution-specs#3873 before this can merge.

The core rule (BaseFeeCalculator.cs)

  • MaxChange is exact. It splits X = q·d + r, so ⌊X·n/d⌋ = q·n + ⌊r·n/d⌋.
    • Because n is capped at d, q·n ≤ X, so the first term can't overflow.
    • r·n < d². With d = 8·12·k, this stays far below 2²⁵⁶ even for k = 2⁶⁴.
    • The 256-bit and 64-bit overflow tests cover the boundaries.
  • The 12 s reference gives the old 1/8 rule. CalculateForReferenceSlot gives n/d = 12/96, and ⌊X·12/96⌋ = ⌊X/8⌋, so it is bit-identical to EIP-1559.
  • The fallback in TryGetSlotSpacing is safe. It returns false for a missing slot number, a slot that isn't after the parent's, or a timestamp that isn't after the parent's, so it never divides by zero or wraps. parent.SlotNumber + 1 overflowing to 0 also falls back correctly.
  • The slot ordering check comes before the base-fee check. ValidateSlotNumberAfterParent runs before Validate1559, matching the execution-specs order, and orphaned headers skip it.
  • Production sets the slot before the base fee. BlockProducerBase sets SlotNumber from the payload attributes in the header initializer, before computing BaseFeePerGas, so built blocks use the derived rule.
  • Simulate stays consistent. It feeds the override Time into both GetSlotNumber and the calculator.
  • Unaffected consensus paths: Optimism and Xdc only pass the new arguments through. The AuRa processing-time _txFilter doesn't include BaseFeeTxFilter or MinGasPriceTxFilter, so nothing consensus-relevant depends on the 12 s reference.

Findings

Severity Finding
High (acknowledged) Fixtures: tests-heze-devnet@v2.0.0 puts every block at slot 0 and uses 5/48. With this PR, about 22k Bogota block tests and about 25k engine tests fail with SlotNumberNotAfterParent or a base-fee mismatch. The description already says to merge only after Heze fixtures are refilled from execution-specs#3873, so this is gating, not a code defect.
Low (acknowledged) Tx filters: BaseFeeTxFilter and MinGasPriceTxFilter now price a 12 s slot. At shorter slots they overstate a rising base fee, so the producer skips some transactions the block could include. Before this change the result here was exact. This doesn't affect consensus. Details are in the inline comment.
Low Comment: ChainHeadInfoProvider.cs:132, // Under EIP-8198 the next block is assumed to land in the next slot., repeats CalculateForNextSlot's name and doc (inline).

Minor notes (no action needed)

  • The predictions in FeeHistoryOracle and InclusionListBuilder still pick the spec from the parent's timestamp. So the first block after Bogota activates is estimated with the 1/8 rule. The remarks already document this.
  • T8nValidator now reads slotNumber and parentSlotNumber for the base fee. As far as I can see, the env's slotNumber isn't applied to the header the tool executes. That was already the case for EIP-7843 and is outside this PR.
  • On performance, validation adds one UInt256.Mod per header when EIP-8198 is active, which is negligible. The SlotSchedule instances are built lazily and cached once.

Not done

  • I didn't build the code or run the tests. The fetch and deepen needed for a proper merge-base diff required approval, so I reviewed by static reading only.

🤖 AI agent (Claude Code / Opus 5.5) on behalf of @Marchhill

CurrentBaseFee = header.BaseFeePerGas;
IReleaseSpec childSpec = SpecProvider.GetSpec(header.Number + 1, header.Timestamp);
NextBaseFee = childSpec.IsEip1559Enabled ? BaseFeeCalculator.Calculate(header, childSpec) : UInt256.Zero;
// Under EIP-8198 the next block is assumed to land in the next slot.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low (comment quality): this repeats what CalculateForNextSlot already says in its name and XML doc. Consider dropping it.

🤖 AI agent (Claude Code / Opus 5.5) on behalf of @Marchhill

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in baf4ed0.

}

UInt256 baseFee = BaseFeeCalculator.Calculate(parentHeader, spec);
UInt256 baseFee = BaseFeeCalculator.CalculateForReferenceSlot(parentHeader, spec);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low (acknowledged in the PR description): before this change, the parent-only base fee here was exact. Under EIP-8198 on a chain with slots other than 12 s, the 12 s reference overstates a rising base fee. At 10 s slots that is 1/8 instead of 5/48, so the producer's TxFilterPipeline skips transactions whose MaxFeePerGas falls between the two, even though the block could include them. This doesn't affect consensus, because the AuRa processing filter doesn't use it and is off post-merge. It does cost some inclusion when fees are rising.

If you want to fix it later, TxPoolTxSource already has targetBlock and the exact baseFee. Passing the predicted base fee through, or pricing for the next slot as the other prediction paths do, would make it exact again. It's fine to leave it as a follow-up.

🤖 AI agent (Claude Code / Opus 5.5) on behalf of @Marchhill

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in baf4ed0: the producer pipelines pass the slot schedule, so both filters price the slot after the parent. Without a schedule they keep the 12 s reference.

The base fee and min gas price filters now take the slot schedule from the
producer's tx source factories, so under EIP-8198 they price the slot after
the parent rather than a 12 s reference slot. Drop a comment that repeated
CalculateForNextSlot.
@Marchhill

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @Marchhill's task in 46s —— View job


Re-review of PR #14984 (after baf4ed0)

  • Gather context: the only new commit since the last review is baf4ed0 (next-slot pricing for the tx filters)
  • Review baf4ed0
  • Re-check the earlier findings
  • Post the summary

Verdict

baf4ed0 resolves both Low findings from the last review, and I found no new correctness problems. The only thing still blocking merge is the one you already listed in the description: the Heze/Bogota fixtures need to be refilled from execution-specs#3873.

What baf4ed0 changes

  • Tx filters now price the next slot. BaseFeeTxFilter and MinGasPriceTxFilter take an optional SlotSchedule and call the new CalculateForNextSlot(parent, SlotSchedule?, spec) overload.
    • On a regular slot grid, s / k is the slot length whichever slot the block lands in, so pricing "one slot after the parent" is exact even when slots were missed.
    • At 10 s slots the producer's filter result is exact again, and transactions priced between the 5/48 and 1/8 base fees are no longer skipped.
  • The callers are wired. TxPoolTxSourceFactory, NethDevBlockProducerTxSourceFactory and XdcTxPoolTxSourceFactory build the schedule with specProvider.GetSlotSchedule(blocksConfig.SecondsPerSlot), the same source as the other prediction paths. This happens once per Create(), not per transaction.
  • Older forks are unchanged. Before EIP-8198, Calculate ignores the timestamp and slot, so slots only matters once the fork is active. The new test's Amsterdam case checks this.
  • The null fallback keeps the old behaviour. Callers that don't pass a schedule still get CalculateForReferenceSlot, so the AuRa factory and the txpool-admission MinGasPriceTxFilter from TxFilterBuilders behave as before.
  • The redundant comment in ChainHeadInfoProvider has been removed.

Findings

Severity Finding
High (acknowledged) Fixtures: tests-heze-devnet@v2.0.0 puts every block at slot 0 and uses 5/48, so about 22k Bogota block tests and about 25k engine tests fail. The description already makes the refilled fixtures from execution-specs#3873 a merge gate.
Low AuRa tx filters still use the 12 s reference. AuRaTxPoolTxSourceFactory.cs:75 and :95 still call the parameterless .WithBaseFeeFilter(), and the AuRa producer's min-gas-price filter is built by TxFilterBuilders.CreateStandardMinGasPriceTxFilter without a schedule. If a Gnosis-style chain (5 s slots) ever enables EIP-8198, its producer will overstate a rising base fee as before. Nothing schedules that today, so it's fine to leave as a follow-up.
Low Test assertions: Eip8198TxFilterTests uses Assert.That(x.Equals(y), Is.True), so a failure doesn't show the actual result. Is.EqualTo would (inline).

Earlier findings

  • Low: the tx filters overstate the base fee at non-12 s slots is fixed by baf4ed0.
  • Low: the redundant ChainHeadInfoProvider comment is removed.

Not done

  • I didn't build the code or run the tests; this re-review is static reading only.

🤖 AI agent (Claude Code / Opus 5.5) on behalf of @Marchhill

Transaction tx = Tx(FeeBetweenTheRules);
using (Assert.EnterMultipleScope())
{
Assert.That(new BaseFeeTxFilter(new SlotSchedule(10)).IsAllowed(tx, Parent(), Spec).Equals(AcceptTxResult.Accepted), Is.True, "10 s slots");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low (test style): Assert.That(x.Equals(y), Is.True, ...) only reports Expected: True But was: False when it fails. Is.EqualTo shows the actual AcceptTxResult, which includes the rejection reason. The same applies to lines 46–48 and 60–61.

Suggested change
Assert.That(new BaseFeeTxFilter(new SlotSchedule(10)).IsAllowed(tx, Parent(), Spec).Equals(AcceptTxResult.Accepted), Is.True, "10 s slots");
Assert.That(new BaseFeeTxFilter(new SlotSchedule(10)).IsAllowed(tx, Parent(), Spec), Is.EqualTo(AcceptTxResult.Accepted), "10 s slots");

🤖 AI agent (Claude Code / Opus 5.5) on behalf of @Marchhill

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6aa8081; also wired the AuRa producer filters to the slot schedule.

The AuRa producer's base fee and min gas price filters take the slot
schedule as the standard producer's do. The filter tests assert the
result with Is.EqualTo.
@Marchhill

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @Marchhill's task in 28s —— View job


Re-review of PR #14984 (after 6aa8081)

  • Gather context: the only new commit since the last review is 6aa8081, which wires the AuRa producer filters to the slot schedule and switches the test assertions to Is.EqualTo
  • Review 6aa8081
  • Re-check the earlier findings
  • Post the summary

Verdict

6aa8081 fixes both Low findings from the last review, and I found nothing new. The only remaining merge gate is the one in the PR description: the Heze/Bogota fixtures need to be refilled from execution-specs#3873.

What 6aa8081 changes

  • AuRa base-fee filter. Both .WithBaseFeeFilter() calls in AuRaTxPoolTxSourceFactory now pass specProvider.GetSlotSchedule(blocksConfig.SecondsPerSlot). This matches TxPoolTxSourceFactory and XdcTxPoolTxSourceFactory. The factory already received specProvider and blocksConfig.
  • AuRa min-gas-price filter. CreateBaseAuRaTxFilter(IDictionaryContractDataStore?) now builds MinGasPriceTxFilter with the schedule.
    • The new comment says this path is producer-only, and that is correct. Its only caller is CreateAuRaTxFilterForProducer.
    • The processing and txpool paths (AuRaPlugin.cs:240 and AuRaBlockProducerEnvFactory.cs:71) go through the other CreateBaseAuRaTxFilter(ITxFilter) overload, which doesn't build a MinGasPriceTxFilter. So nothing consensus-relevant changes.
  • TxFilterBuilders.CreateStandardMinGasPriceTxFilter. It gains an optional SlotSchedule? slots = null. AuRa is the only caller, so the default keeps any external caller on the 12 s reference.
  • Test assertions. Eip8198TxFilterTests now uses Is.EqualTo(AcceptTxResult.…). AcceptTxResult implements IEquatable by Id, so the comparison is correct, and a failure now shows the actual result.
  • Older forks are unchanged. Before EIP-8198 the schedule is ignored, and the Amsterdam test case covers this.

Findings

Severity Finding
High (acknowledged) Fixtures: tests-heze-devnet@v2.0.0 puts every block at slot 0 and uses 5/48. About 22k Bogota block tests and about 25k engine tests fail with SlotNumberNotAfterParent or a base-fee mismatch. The description already says to merge only after the fixtures are refilled from execution-specs#3873. This is a merge gate, not a code defect.

Earlier findings

  • Low: the AuRa tx filters still use the 12 s reference: fixed in 6aa8081.
  • Low: Assert.That(x.Equals(y), Is.True) in Eip8198TxFilterTests: fixed in 6aa8081.
  • Low: the tx filters overstate the base fee at slot lengths other than 12 s: fixed in baf4ed0.
  • Low: the redundant ChainHeadInfoProvider comment: removed in baf4ed0.

Not done

  • I didn't build the code or run the tests. This re-review is static reading only.

🤖 AI agent (Claude Code / Opus 5.5) on behalf of @Marchhill

@github-actions

Copy link
Copy Markdown
Contributor

Stateless guest cost

⚠️ Baseline does not match this pull request's base; deltas are suppressed.

Restored from d48833af88ec, not this pull request's base (e2309ed354d1).

Only absolute measurements are shown.

block steps prover cost
11957686 355,805,763 39,170,309,648
11962410 216,956,956 23,691,247,427
11962415 251,791,548 27,788,026,470
11962427 218,531,855 23,803,870,975
11962485 221,200,455 24,326,127,770
25526356 94,105,146 15,562,861,107
25532382 100,140,154 17,443,170,299
25532442 130,081,157 19,963,276,732
25532454 120,966,563 19,821,250,619
25532553 111,702,224 17,900,094,072
25532642 111,117,895 18,164,754,991

f082b928fc65 (merge commit) · 11 pinned blocks · ziskemu is deterministic, so these figures are exact and any non-zero delta is real.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants