Skip to content

feat(eip8198): Quick Slots - 5/48 base fee change and fork-aware slot schedule - #14606

Draft
Marchhill wants to merge 42 commits into
masterfrom
feature/eip-8198
Draft

Marchhill wants to merge 42 commits into
masterfrom
feature/eip-8198

Conversation

@Marchhill

@Marchhill Marchhill commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Changes

  • Includes feat(sync): track executed BAL catch-up blocks #14899: BAL catch-up fallback metrics and the TestBlockchain registration fix.

  • Enable EIP-8198 Quick Slots in Bogota, including Geth bogotaTime and Parity fork labels.

  • Change maximum base-fee movement from 1/8 to 5/48 at activation. The fraction is spec data (IEip1559Spec.BaseFeeMaxChangeNumerator/Denominator), derived from IsEip8198Enabled in DerivedSpecValues as BaseFeeMaxChange.ScaledToSlot(10_000), which keeps the 1/8-per-12 s rate per second. While EIP-8198 is active it overrides a chainspec eip1559BaseFeeMaxChangeDenominator.

  • Make slot scheduling fork-aware for RPC predictions, simulation, prewarming, payload pacing, and EraE. Fix EraE timestamp units. GetSlotSchedule starts a segment at each fork activation that changes the slot length.

  • Add an optional eip8198SlotDurationMs (chainspec params or Geth config, a whole number of seconds in ms): the slot length from EIP-8198, matching the CL's SLOT_DURATION_MS_EIP8198. Unset keeps Blocks.SecondsPerSlot, so a genesis with only bogotaTime (Heze devnet v2, 12 s) loads and needs nothing else. It only moves slot timing; the base fee fraction is 5/48 either way.

    "eip8198SlotDurationMs": 10000
  • Use the canonical child's base fee for historical eth_feeHistory results.

  • Run tests-heze-devnet@v2.0.0 in CI, including Bogota transitions, and load its blob schedule (target 12, maximum 18).

Types of changes

  • Bugfix (a non-breaking change that fixes an issue)
  • New feature (a non-breaking change that adds functionality)

Testing

Requires testing

  • Yes

If yes, did you write tests?

  • Yes

All 67,785 Heze v2 fixtures pass locally at 86e9b9386e, plus 135 FOCIL cases with parallel execution and batch reads. Specs: 625 passed. Regression coverage includes bogotaTime: 1792416379, slot scheduling, RPCs, EraE, and payload pacing.

Documentation

Requires documentation update

  • No

Requires explanation in Release Notes

  • No

Remarks

Includes the FOCIL fix from #14916. Follows execution-specs#3703 and consensus-specs#5710. Mainnet's chainspec will set eip8198SlotDurationMs: 10000 when Hegota is scheduled.

Gate a new IsEip8198Enabled flag behind Eip8198TransitionTimestamp; no
named fork enables it. When active, the EIP-1559 base fee update uses
target_fee_gas_delta * 5 / 48 instead of / BaseFeeMaxChangeDenominator,
per execution-specs eips/bogota/eip-8198. The flag lives on IEip1559Spec
because the base fee calculator only sees that interface.

eth_baseFee, eth_feeHistory and eth_fillTransaction looked up the next
block's 1559 spec by block number only, which ignores timestamp forks;
use the parent timestamp as ChainHeadInfoProvider does.
@claude

claude Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @Marchhill's task in 1m 9s —— View job


Review of PR #14606: EIP-8198 (Quick Slots), max base fee change of 5/48

  • Gather context and diff
  • Review consensus logic (base fee calculator, spec flag plumbing)
  • Review RPC changes
  • Review tests
  • Post findings

Verdict: I found no Critical, High or Medium issues. There are two Low notes. I did not build the code or run the tests here.

What I checked

  • Calculator (BaseFeeCalculator.cs): matches the reference formula target_fee_gas_delta * 5 // 48. The multiply happens before the divide, and the existing max(…, 1) floor on increases is kept. With the flag off the code computes x / BaseFeeMaxChangeDenominator, the same as before. This runs once per header, so the extra multiply is negligible.

  • Flag plumbing: the flag is set on all of these:

    • ReleaseSpec
    • ReleaseSpecDecorator
    • OverridableEip1559Spec
    • OverridableReleaseSpec
    • the chainspec param, loader and provider

    Eip8198TransitionTimestamp ends in TransitionTimestamp, so AddTransitions picks it up automatically. Without that, the fork would never get its own release spec.

  • Callers that compute a base fee: consensus and block production get their spec from the header or from (number, timestamp):

    • HeaderValidator
    • BlockProducerBase
    • TxPoolTxSource
    • ChainHeadInfoProvider
    • InclusionListBuilder
    • MempoolStatePrewarmer
    • StartupPipelineWarmer
    • TraceRpcModule

    The three RPC call sites changed here were the only ones looking up the spec by block number alone. The remaining GetSpecFor1559 callers only read IsEip1559Enabled, which is fine.

  • Tests:

    • The execution-specs vectors are included.
    • A BigInteger reference formula is checked against a 7×7 matrix of base fee × gas used, with the flag on and off.
    • Header validation is tested at the exact value and at ±1 wei.
    • The chainspec transition is tested, and so is the RPC regression for timestamp forks.

    Tests are parameterised where they should be.

Findings

Low: simplify MaxChange (inline), BaseFeeCalculator.cs:71-74

  • The helper receives both the denominator and isEip8198, so the fork check is split across two places.
  • Choosing maxChangeNumerator (5 or 1) next to maxChangeDenominator turns both branches into delta * num / den, and the helper can be removed.

Low / informational: fork-boundary prediction in the RPC, EthRpcModule.cs:153, :539 and FeeHistoryOracle.cs:139

  • These calls use the parent's timestamp to stand in for the next block's timestamp, the same as ChainHeadInfoProvider.
  • For the one block where parent.Timestamp < forkTs ≤ child.Timestamp, eth_baseFee and eth_fillTransaction will predict with the 1/8 rule. This is unavoidable for the head, because the next timestamp is unknown, so no change is needed.
  • eth_feeHistory over historical ranges could read the real child header. That would be extra work for one block per fork, so this is only a note.

Comment quality is good. The added comments cite the EIP or explain why the flag sits on IEip1559Spec, and none just restate the code.

@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Stateless guest cost

block steps Δ steps prover cost Δ cost
11957686 357,875,471 -0.001% 39,424,295,195 -0.001%
11962410 218,029,660 -0.001% 23,812,390,961 -0.001%
11962415 253,366,691 -0.000% 27,970,480,877 -0.000%
11962427 219,696,781 +0.000% 23,936,593,555 +0.000%
11962485 222,561,735 +0.000% 24,487,880,657 +0.000%
25526356 96,873,102 +0.000% 15,884,262,025 -0.000%
25532382 103,704,758 +0.000% 17,851,758,624 +0.000%
25532442 132,326,170 -0.002% 20,238,008,733 -0.001%
25532454 124,375,070 +0.000% 20,216,787,667 +0.000%
25532553 114,751,679 -0.002% 18,252,257,191 -0.001%
25532642 114,263,795 -0.001% 18,519,994,652 -0.000%
all 11 blocks 1,957,824,912 -0.001% 250,594,710,137 -0.001%
Variable cost by bucket, summed over shared blocks (excludes BASE)
bucket before after Δ
MAIN 133,132,930,892 133,132,094,016 -0.001%
OPCODES 29,911,209,312 29,910,890,067 -0.001%
PRECOMPILES 56,647,271,747 56,647,270,889 -0.000%
MEMORY 27,744,175,677 27,744,047,101 -0.000%

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

Compared against 90792c0cdcd4.

private static UInt256 MaxChange(in UInt256 targetFeeGasDelta, in UInt256 denominator, bool isEip8198) =>
isEip8198
? targetFeeGasDelta * Eip8198Constants.BaseFeeMaxChangeNumerator / denominator
: targetFeeGasDelta / denominator;

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 (simplification): MaxChange takes both the denominator and the isEip8198 flag, so the fork check happens in two places. If you choose the numerator next to the denominator (UInt256 maxChangeNumerator = isEip8198 ? Eip8198Constants.BaseFeeMaxChangeNumerator : UInt256.One;), both branches become targetFeeGasDelta * maxChangeNumerator / maxChangeDenominator and the helper can go. Multiplying by 1 gives the same result as before, so pre-fork behaviour is unchanged. The extra UInt256 multiply costs nothing measurable here.

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.

Done in e9b3caf: a numerator (5 or 1) now sits beside the denominator, both branches compute delta * num / den, and MaxChange is gone. Flag-off results are unchanged (×1), and the vectors and the 7×7 matrix still pass.

Pick a numerator (5 or 1) beside the denominator so both branches compute
delta * num / den, and drop the MaxChange helper. Flag-off results are
unchanged.

eth_feeHistory's next base fee for the newest block now takes the spec
from the canonical child's timestamp when the child exists, so a block
just before a timestamp fork reports the post-fork rule. The head keeps
the parent-timestamp estimate.
@Marchhill

Copy link
Copy Markdown
Contributor Author

@claude review

Fork-boundary Low: eth_feeHistory now picks the spec from the canonical child's timestamp when that child exists (boundary test added). For the head (eth_baseFee, eth_fillTransaction) the next timestamp is unknown, so it keeps the parent-timestamp estimate, the same one ChainHeadInfoProvider uses.

@claude

claude Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @Marchhill's task in 1m 8s —— View job


Re-review of PR #14606 (after e9b3caf)

  • Gather the diff (shallow clone, so I diffed against da6397c)
  • Re-check the MaxChange simplification
  • Review the eth_feeHistory child-timestamp change and its boundary test
  • Post findings

Verdict: I found no Critical, High or Medium issues, and one new Low. Both earlier findings are resolved. I did not build the code or run the tests here.

Earlier findings

  • MaxChange simplification: resolved. BaseFeeCalculator.cs:30-56 now computes delta * num / den, with num set to 5 or 1.
    • The zero-denominator guard now checks the chosen maxChangeDenominator, which is correct. With the flag on it is 48, so a misconfigured denominator of 0 can no longer turn off base-fee updates after the fork.
    • With the flag off the result is exactly the old one, because the code multiplies by 1.
  • Fork-boundary prediction: resolved for eth_feeHistory. GetNextBaseFee (FeeHistoryOracle.cs:242-253) uses the canonical child's spec when the child exists.
    • The child.ParentHash != info.BlockHash check protects against a reorg between the cached entry and the lookup.
    • RequireCanonical stops a sibling block from being used.
    • For latest (the usual query), the lookup at height+1 returns null, so the extra cost is one missed lookup per request. That is negligible.
    • The cache still stores the parent-timestamp estimate, which is correct for the head case.
    • I agree with keeping the parent-timestamp estimate for eth_baseFee and eth_fillTransaction, since the next timestamp is unknown at the head.
  • Boundary test: the 3 cases cover after-fork, before-fork as head, and before-fork with a child after the fork. I checked the values by hand:
    • 1e9 × 15M/15M × 5/48 = 104,166,666, giving 1,104,166,666 after the fork and 1,125,000,000 with the old 1/8 rule.
    • 0x41d03f0a and 0x356554f6 match 1e9 ± 104,166,666.

New finding

Low: simplify GetNextBaseFee (inline), FeeHistoryOracle.cs:242-253

  • Once the canonical child is found, its BaseFeePerGas is the next base fee, because HeaderValidator checks it against the calculator.
  • Returning that value drops the second FindHeader call by hash, the recalculation and the null fallback.
  • The <summary> explains the reason for the method instead of its contract. That part belongs in <remarks>, and the inline suggestion moves it there.

Comment quality

The added comments explain the reason or cite the EIP; the only issue is the <summary> placement above.

Comment on lines +242 to +253
/// <summary>
/// The cached estimate picks the next block's spec from the block's own timestamp, so it is one block
/// off at a timestamp fork. When the canonical child already exists, its timestamp picks the spec.
/// </summary>
private UInt256 GetNextBaseFee(in BlockFeeHistorySearchInfo info)
{
BlockHeader? child = _blockTree.FindHeader(info.BlockNumber + 1, BlockTreeLookupOptions.RequireCanonical | BlockTreeLookupOptions.TotalDifficultyNotNeeded);
if (child is null || child.ParentHash != info.BlockHash) return info.BaseFeePerGasEst;

BlockHeader? header = _blockTree.FindHeader(info.BlockHash, BlockTreeLookupOptions.TotalDifficultyNotNeeded, info.BlockNumber);
return header is null ? info.BaseFeePerGasEst : BaseFeeCalculator.Calculate(header, _specProvider.GetSpec(child));
}

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 (simplification): when the canonical child exists and child.ParentHash == info.BlockHash, the next base fee is already known: it is child.BaseFeePerGas. HeaderValidator requires that value to equal BaseFeeCalculator.Calculate(parent, GetSpec(child)). Before 1559, both are 0, because the calculator returns the parent's base fee.

Returning child.BaseFeePerGas would:

  • remove the second FindHeader(info.BlockHash, …) lookup and the recalculation
  • remove the null-header fallback

You would still need BlockHash for the parent-hash check. On chains whose validator doesn't enforce the calculator (Taiko, for example), it would also report the actual next base fee instead of an estimate.

Suggested change
/// <summary>
/// The cached estimate picks the next block's spec from the block's own timestamp, so it is one block
/// off at a timestamp fork. When the canonical child already exists, its timestamp picks the spec.
/// </summary>
private UInt256 GetNextBaseFee(in BlockFeeHistorySearchInfo info)
{
BlockHeader? child = _blockTree.FindHeader(info.BlockNumber + 1, BlockTreeLookupOptions.RequireCanonical | BlockTreeLookupOptions.TotalDifficultyNotNeeded);
if (child is null || child.ParentHash != info.BlockHash) return info.BaseFeePerGasEst;
BlockHeader? header = _blockTree.FindHeader(info.BlockHash, BlockTreeLookupOptions.TotalDifficultyNotNeeded, info.BlockNumber);
return header is null ? info.BaseFeePerGasEst : BaseFeeCalculator.Calculate(header, _specProvider.GetSpec(child));
}
/// <summary>
/// Returns the base fee of the block after <paramref name="info"/>: the canonical child's actual value when
/// the child exists, otherwise the estimate.
/// </summary>
/// <remarks>
/// The estimate picks the next block's spec from the block's own timestamp, so at a timestamp fork it is one block off.
/// </remarks>
private UInt256 GetNextBaseFee(in BlockFeeHistorySearchInfo info)
{
BlockHeader? child = _blockTree.FindHeader(info.BlockNumber + 1, BlockTreeLookupOptions.RequireCanonical | BlockTreeLookupOptions.TotalDifficultyNotNeeded);
return child is not null && child.ParentHash == info.BlockHash ? child.BaseFeePerGas : info.BaseFeePerGasEst;
}

The with_child_after_fork test would then need .WithBaseFeePerGas(1_104_166_666) on child, and it could drop the FindHeader(block.Hash!, …) stub.

Separately, the current <summary> explains the reason for the method rather than what it returns. Per AGENTS.md that explanation belongs in <remarks>; the suggestion above splits it that way.

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.

Applied in 58bff53: when the canonical child exists, GetNextBaseFee returns its BaseFeePerGas, the summary/remarks are split as suggested, and the test sets the child's base fee and drops the hash stub.

@Marchhill

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

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


Re-review of PR #14606 (after 58bff53)

  • Gather the diff (shallow clone, so I diffed against da6397c)
  • Check that the GetNextBaseFee simplification was applied correctly
  • Check the test update and look for leftover code
  • Post findings

Verdict: I found no Critical, High, Medium or Low issues, and all earlier findings are resolved. I did not build the code or run the tests here.

GetNextBaseFee simplification: resolved

FeeHistoryOracle.cs:242-253

  • When the canonical child exists and child.ParentHash == info.BlockHash: it returns child.BaseFeePerGas. Otherwise it returns the estimate.
  • Removed code: the second FindHeader lookup by hash, the recalculation and the null-header fallback are all gone.
  • Docs: <summary> now states what the method returns, and the reason moved to <remarks>. This matches AGENTS.md.
  • Behaviour on historical ranges: eth_feeHistory now reports the base fee actually stored in the child. On Ethereum, HeaderValidator requires that value to equal the calculator's result, so nothing changes. On chains whose base fee rules differ, it is more accurate.
  • Cost: for latest, it is still a single missed canonical lookup per request.

Other checks

  • Estimate cached for the head: it uses GetSpec(b.Number + 1, b.Timestamp). GetSpecFor1559(n) was just GetSpec(n, null), so the only difference is that the timestamp is now passed in.
  • Test (EthRpcModuleTests.FeeHistory.cs):
    • with_child_after_fork now sets the child's base fee to 1_104_166_666.
    • The FindHeader(block.Hash!, …) stub is gone.
    • The comment above the test cases was updated to describe the new behaviour.
    • No other JsonRpc test stubs FindHeader by number in a way this lookup could pick up.
  • Leftovers: _specProvider is still used, and no stale usings or helpers remain.

The changes since the last review are small, and every comment either explains a reason or cites the EIP.

Validator passed StartingBlockTimestamp, in seconds, to SlotTime.GetSlot,
which takes ms, so HistoricalRoots and HistoricalSummaries verification
threw "before genesis".
SlotSchedule maps timestamps to slots and steps slots the way the CL does
under EIP-8198 (consensus-specs#5710): the configured slot length from
beacon genesis, then 10 s from the first boundary at or after the
EIP-8198 activation. ISpecProvider.GetEip8198Timestamp finds that
activation from the transition list; without it the schedule is the
single configured length.
…hedule

- eth_simulate EIP-7843 slot numbers come from the piecewise beacon grid.
- eth_simulate and debug_traceCallMany default timestamps advance one
  scheduled slot per block, so 10 s from the EIP-8198 fork.
- eth_baseFee and eth_fillTransaction take the next block's spec one
  slot after the head instead of at the head's timestamp.
- The eth_estimateGas funded run predicts the next block one scheduled
  slot on.
@Marchhill Marchhill changed the title feat(eip8198): lower the max base fee change to 5/48 (Quick Slots) feat(eip8198): Quick Slots - 5/48 base fee change and fork-aware slot schedule Oct 13, 2026
@Marchhill

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @Marchhill's task in 1m 27s —— View job


Re-review of PR #14606 (after bb41bb8)

  • Gather the diff (da6397c..HEAD). Since the last review, the PR added the fork-aware slot schedule and the EraE ms fix.
  • Review SlotSchedule and GetEip8198Timestamp
  • Review the call sites (simulate, RPC, EraE, prewarmer, payload pacing)
  • Review DI wiring and API surface
  • Post findings

Verdict: I found no Critical, High or Medium issues. There is one Low (performance) note. All earlier findings stay resolved. I did not build the code or run the tests here.

What I checked

  • SlotSchedule arithmetic (SlotSchedule.cs). I traced each method on both sides of the fork:
    • Fork position: _forkSlot = ceil((fork − genesis) / sps) puts the fork on the first boundary at or after activation, the same way as the CL's compute_slot_at_time_ms. A fork at or before genesis clamps to slot 0.
    • AddSlots: pre-fork steps go up to the fork boundary, and every step after that is 10 s.
    • AddSlotsUntil: when it moves past the fork, steps > stepsToFork implies target > atFork. The post-fork CeilDiv therefore always gets a positive delta and no underflow can occur.
    • TryGetSlot / GetSlotTimestamp: they are inverses of each other, and IsSlotStart relies on that.
  • GetEip8198Timestamp: chainspec timestamp transitions always appear in TransitionActivations, and a fork enabled at genesis is caught by GenesisSpec. With no fork scheduled, the schedule has _hasFork = false, so it computes exactly what the old ts + n·sps code did.
  • MempoolStatePrewarmer.PredictNextTimestamp:
    • Before the fork, AddSlotsUntil(parent, now − grace) gives the same result as the old parent + max(1, ceil((now − grace − parent)/sps))·sps.
    • The now > grace ? … : 0 guard keeps the old behaviour for clocks before the grace period.
  • EraE:
    • Validator used to pass seconds into SlotTime.GetSlot, which takes ms. That bug is fixed and has a regression test.
    • The writer keeps 12 s as its pre-fork length.
    • Both now throw an Era*Exception when the timestamp is before beacon genesis, instead of returning a nonsense slot. That can't happen for post-merge blocks on real chains.
  • Payload pacing:
    • The PayloadPreparationService DI constructor gains an optional ISpecProvider. Autofac resolves it, because the TimeSpan constructor can't be satisfied.
    • Optimism passes its provider through.
    • Taiko and the test subclasses use the TimeSpan constructor and keep their fixed slot length (_slots is null).
  • eth_baseFee / eth_fillTransaction: the next-block spec now uses head.Timestamp + one slot. This fixes the one-block-late fork boundary that the first review flagged.
  • IsEip8198Enabled on IEip1559Spec: the only implementers are IReleaseSpec (via ReleaseSpec, ReleaseSpecDecorator and OverridableReleaseSpec) and OverridableEip1559Spec. All of them are covered, so no plugin implementer is left behind.
  • Comments: the new XML docs and inline comments state contracts, cite consensus-specs or explain the reasons. None just restate the code.

Finding

Low (performance): the slot schedule is rebuilt on every request (inline), EthRpcModule.cs:164

  • Cost per build: GetSlotSchedule runs about 20 GetSpec lookups on mainnet and allocates once, to compute a value fixed at startup.
  • Where it is rebuilt:
    • per eth_baseFee and eth_fillTransaction call
    • per eth_estimateGas funded run (BlockchainBridge.cs:316)
    • per post-7843 simulated block (SimulateBridgeHelper.cs:362)
  • Fix: cache it in a field, as SimulateTxExecutor, the prewarmer and EraE already do. Keep it per call only where the spec provider can be overridden per request.

Fix this →


/// <summary>The spec of the block after <paramref name="head"/>, assumed to land in the next slot.</summary>
private IReleaseSpec GetNextBlockSpec(BlockHeader head) =>
_specProvider.GetSpec(head.Number + 1, _specProvider.GetSlotSchedule(_secondsPerSlot).GetNextSlotTimestamp(head.Timestamp));

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 (performance): GetSlotSchedule builds a new SlotSchedule on each call. Each build runs GetEip8198Timestamp, which loops over every TransitionActivations entry and calls GetSpec for each one. On mainnet that is about 20 spec lookups and one allocation, all to get a value fixed at startup.

Here it runs once per eth_baseFee and eth_fillTransaction call. It also runs per request in:

  • BlockchainBridge.CreateFundedRunContext, which is on the eth_estimateGas path
  • SimulateBridgeHelper.GetSlotNumber, once per simulated block after EIP-7843

That isn't a correctness problem, but it costs time on hot RPC paths for no benefit. Building the schedule once per instance, as SimulateTxExecutor, MempoolStatePrewarmer and EraE already do, removes the cost. For example, a private readonly SlotSchedule _slots = specProvider.GetSlotSchedule(secondsPerSlot ...) field here, and similar fields in BlockchainBridge and SimulateBridgeHelper.

The simulate helper may receive an overridden spec provider. If that is the case, keep it per call there, or cache it per provider instance.

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.

Done in 7bc644c. EthRpcModule and BlockchainBridge build the schedule once, lazily. SimulateBridgeHelper caches it per spec provider, so a request that brings an override provider rebuilds it once per request rather than once per block.

@Marchhill

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

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


Re-review of PR #14606 (after 7bc644c)

  • Gather the diff since the last review (bb41bb8..7bc644c)
  • Review the slot-schedule caching in EthRpcModule, BlockchainBridge and SimulateBridgeHelper
  • Re-check the full PR for regressions
  • Post findings

Verdict: I found no Critical, High or Medium issues, and one new Low (docs). The performance finding from the last review is resolved. I did not build the code or run the tests here.

Slot-schedule caching: resolved

  • EthRpcModule.GetNextBlockSpec: _slots ??= … builds the schedule lazily, once per module instance.
    • Lazy is the right choice over a field initializer: the module may be built per pooled instance, and most instances never serve eth_baseFee or eth_fillTransaction.
    • Two threads racing on ??= can only build two identical schedules. That is harmless, because SlotSchedule is sealed and has only readonly fields.
  • BlockchainBridge.CreateFundedRunContext: uses the same lazy pattern on the bridge's own specProvider, which no request can override, so caching it is safe.
  • SimulateBridgeHelper.GetSlotNumber: caches per provider in a SlotScheduleCache(ISpecProvider, SlotSchedule) record, compared with ReferenceEquals.
    • The helper may be shared by concurrent requests that bring different override providers. Each request reads _slots into a local once and compares it with its own provider, so it can never use a schedule built for another provider. Writing a record reference is atomic, so there is no tearing.
    • The worst case is a rebuild when requests with different providers interleave, and the result is still correct.
    • The // Rebuilt only when a request brings a different spec provider. comment states the invariant, so it should stay.

New finding

Low (docs): the doc comment moved onto the field (inline), BlockchainBridge.cs:314

  • private SlotSchedule? _slots; sits between the existing <summary> and CreateFundedRunContext, so the summary now documents the field instead of the method.
  • Fix: move the field above the doc block, or next to the class's other fields.

Fix this →

Earlier findings

Everything from earlier reviews is still resolved:

  • the MaxChange simplification
  • the eth_feeHistory canonical-child base fee
  • the eth_baseFee and eth_fillTransaction next-slot spec
  • the per-request schedule rebuild

/// A failure without standard text names the gas the next block's rules fund, run with the blob fee cap
/// filled from the next block's blob base fee when the request left it out.
/// </summary>
private SlotSchedule? _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 (docs): this field sits between the existing <summary> (lines 310-313) and CreateFundedRunContext. As a result, the XML doc now attaches to _slots, and the method has no docs. IntelliSense will show the funded-run text on the field.

Fix: move private SlotSchedule? _slots; above the /// <summary> block, or next to the class's other fields.

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.

Moved the field above the doc block in 2cb2b3c.

@Marchhill

Copy link
Copy Markdown
Contributor Author

@claude review

1 similar comment
@Marchhill

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

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


Re-review of PR #14606 (after 2cb2b3c)

  • Get the diff since the last review (7bc644c..2cb2b3c)
  • Check that the doc-placement fix is correct
  • Re-check SlotSchedule for regressions
  • Post findings

Verdict: I found no Critical, High, Medium or Low issues, and every earlier finding is resolved. I did not build the code or run the tests here.

Doc placement: resolved

In BlockchainBridge.cs:310, private SlotSchedule? _slots; now comes before the /// <summary> block. The funded-run summary documents CreateFundedRunContext again. The commit makes no other changes.

Earlier findings

All are still resolved:

  • the MaxChange simplification (delta * num / den)
  • the eth_feeHistory next base fee, which now comes from the canonical child
  • the eth_baseFee and eth_fillTransaction next-block spec, now taken one slot after the head
  • the slot schedule, now cached instead of rebuilt per request (lazily in EthRpcModule and BlockchainBridge, and per spec provider in SimulateBridgeHelper)
  • the doc placement in BlockchainBridge

Spot-check

I re-read SlotSchedule.cs and found nothing new:

  • AddSlots and AddSlotsUntil switch to 10 s steps cleanly once they cross the fork.
  • TryGetSlot and GetSlotTimestamp are still inverses of each other.
  • With no fork scheduled, the class computes exactly the old ts + n·sps.

…ionMs is set

EIP-8198 networks such as Heze devnet v2 keep 12 s slots, so without an
explicit eip8198SlotDurationMs the slot length after the fork stays
Blocks.SecondsPerSlot. DesignSlotDurationMs remains only the base fee
design constant: 5/48 applies whatever the slot length.
@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 42s —— View job


Re-review of PR #14606 (after 0071ad6)

  • Get the diff since the last review (be8ff89..0071ad6: ccbdfc7, 0071ad6)
  • Review the slot-length fallback change and GetSlotSchedule
  • Check the test harnesses that relied on the old 10 s fallback
  • Post findings

Verdict: I found no Critical, High, Medium or Low issues, plus one test-comment nit. Every earlier finding is still resolved. I did not build the code or run the tests here.

Slot length falls back to Blocks.SecondsPerSlot (0071ad6)

  • DerivedSpecValues.SlotDurationMs: now returns eip8198SlotDurationMs under EIP-8198 and null otherwise. Without a configured value, GetSlotSchedule sees null == current at the fork, adds no segment and builds the single-segment grid, which is exactly ts + n·sps. With a value set, the fork starts a segment, as before.
  • Base fee is unaffected: DesignSlotDurationMs is now used only by ScaledToSlot, so the fraction is still 5/48 whatever the slot length. The <remarks> on the constant now say this.
  • Harnesses that relied on the old 10 s default: every test that expects 10 s after the fork now sets Eip8198SlotDurationMs = 10_000, including SlotSchedule, EraE writer and validator, simulate, fee history, gas price and Hive simulate. Engine, RPC and fixture harnesses set 12 000. MempoolStatePrewarmerTests builds SlotSchedule directly with an explicit (124, 10_000) entry, so it doesn't depend on the provider fallback. I found no test that still expects 10 s without setting the value.
  • Docs: ISpecProvider, IReleaseSpec, ChainParameters and both JSON models now say that null keeps Blocks.SecondsPerSlot. No reference to "EIP-8198's 10 s" as the fallback is left.
  • OverridableReleaseSpec comment: it explains why the wrapped value falls back to 8 and how to keep another value. That covers the nit from the last review.

Heze devnet genesis test (ccbdfc7)

  • It loads a real-shaped Heze genesis with bogotaTime alone and checks the following at bogotaTime − 1 and at bogotaTime:
    • FOCIL and EIP-8198 flags
    • 1/8 → 5/48
    • blob target and max (21 → 12/18), and the update fraction
    • 12 s slots after the fork
  • This is the regression test for the behaviour that 0071ad6 restores.

Nit (test-only, not blocking)

In GethGenesisLoaderTests.cs the comment above Bogota_activates_eip8198_at_the_configured_timestamp says the fraction holds "whatever slot length the chain overrides it with". With no 10 s default anymore, nothing is overridden. "…whatever slot length the chain sets" would match the new contract.

Behaviour to be aware of

If a chainspec schedules EIP-8198 but leaves out eip8198SlotDurationMs when its CL runs shorter slots, the node quietly keeps 12 s timing. Prewarm and payload pacing would then be off, but block validity would not change. The Remarks already say mainnet will set the value, so no change is needed.

Still open from earlier reviews (optional, not blocking)

  • Lazy parent scope in BranchProcessor.cs:165-166: an optional performance note, unchanged.
  • OP/Taiko calculators that override only the denominator: a heads-up only.

Earlier findings

All are still resolved:

  • the MaxChange simplification, now derived spec data
  • the eth_feeHistory next base fee, which comes from the canonical child
  • the eth_baseFee and eth_fillTransaction next-block spec, taken one slot after the head
  • the slot schedule, cached instead of rebuilt per request
  • the doc placement in BlockchainBridge
  • the slot duration for blockchain-test spec providers
  • FOCIL blocks no longer forced onto sequential BAL application
  • IsEip8198Enabled no longer on IEip1559Spec

@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 19s —— View job


Re-review of PR #14606 (after aecf2a0)

  • Get the diff since the last review (0071ad6..aecf2a0)
  • Check the test-comment change
  • Post findings

Verdict: I found no Critical, High, Medium or Low issues. The nit from the last review is resolved, and every earlier finding is still resolved. I did not build the code or run the tests here.

Test comment wording (aecf2a0): resolved

  • The only new commit changes one comment in GethGenesisLoaderTests.cs:851. It now reads "…whatever slot length the chain sets".
  • That fits the current contract: eip8198SlotDurationMs is optional, and when it is unset the slot length stays at Blocks.SecondsPerSlot. Nothing overrides a default.
  • The commit changes no code.

Still open from earlier reviews (optional, not blocking)

  • Lazy parent scope in BranchProcessor.cs:165-166: an optional performance note, unchanged.
  • OP/Taiko calculators that override only the denominator: a heads-up only. It matters only if one of those stacks ever schedules EIP-8198.

Earlier findings

All are still resolved:

  • the MaxChange simplification, now derived spec data
  • the eth_feeHistory next base fee, which comes from the canonical child
  • the eth_baseFee and eth_fillTransaction next-block spec, taken one slot after the head
  • the slot schedule, cached instead of rebuilt per request
  • the doc placement in BlockchainBridge
  • the slot duration for blockchain-test spec providers
  • FOCIL blocks no longer forced onto sequential BAL application
  • IsEip8198Enabled no longer on IEip1559Spec
  • the OverridableReleaseSpec fallback comment and the Bogota test comment wording

@Marchhill

Copy link
Copy Markdown
Contributor Author

Merged master and applied the reviewed lazy parent-scope and generated-BAL fixes. Specs 723 and focused consensus 87 tests pass; Heze v2 remains unfiltered.

@claude review

@claude

claude Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

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


Re-review of PR #14606 (after 58d6a19)

  • Get the diff since the last review (aecf2a0..58d6a19: 58d6a19, plus the 4f1d6c8 merge of master)
  • Review the lazy parent scope and generated-BAL fixes (58d6a19)
  • Check how the master merge resolved conflicts. I couldn't do this: this run doesn't allow git show --cc.
  • Post findings

Verdict: No Critical, High, Medium or Low issues. The optional lazy-parent-scope note from earlier reviews is resolved. I didn't build the code or run the tests here.

Lazy parent scope (58d6a19): resolved

  • The scope now opens only when needed. It used to open before ProcessOne. Now the readParent callback opens it the first time something reads it, through parentScope ??= OpenInclusionListParentScope(preBlockBaseBlock), and a finally closes it (BranchProcessor.cs:203-218).
  • Blocks that never read the parent don't open it. InclusionListCommittedState.Parent calls the callback at most once. It only does so when an IL sender's field changes at the finalization index and there is no earlier change in the block, or for a storage slot in the same situation. The full-block and all-included shortcuts never get that far, so no snapshot-bundle lease is held while the block runs or commits.
  • The parent can still be read after the block runs. For i > 0, preBlockBaseBlock is the previous block of the branch, which hasn't been written to the database yet. It is still readable when the scope opens later, because:
    • worldStateCloser still holds the branch.
    • The scope opens before PreCommitBlock and before stateProvider.Reset().
    • Inclusion_list_uses_each_parent_inside_an_uncommitted_branch still checks false, true, true on both the flat and trie backends.
  • Errors are reported the same way as before. A missing parent still throws StateNotRetainedException, and missing data still throws InvalidOperationException. Neither marks the block invalid.

Generated BAL fallback (58d6a19)

  • What changed: if a block has neither a decoded nor an encoded BAL, the list now comes from GeneratedBlockAccessList, by encoding it and decoding it again. Before, readList returned null here, which set Available = false and made the IL check throw.
  • Using the generated list is safe. With no BAL supplied, there is nothing to validate it against, and the generated list is this node's own record of the block's execution.
  • The cost is small. The encode and decode run inside readList, which only runs the first time an account is read. Most blocks bring their own BAL and never reach this branch. The shortcut paths skip it too.
  • The new test checks both changes. Inclusion_list_uses_generated_bal_without_reading_unchanged_parent gives a block only an empty generated BAL and checks that:
    • the verdict is false (censorship is detected);
    • CreateResettableWorldState is never called, so the parent scope stays closed when no field changes at the finalization index.

Still open from earlier reviews (optional, not blocking)

  • OP/Taiko calculators that override only the denominator: a heads-up only. It matters only if one of those stacks ever schedules EIP-8198.

Earlier findings

All still resolved:

  • the MaxChange simplification, now derived spec data
  • eth_feeHistory takes the next base fee from the canonical child
  • eth_baseFee and eth_fillTransaction use the spec one slot after the head
  • the slot schedule is cached instead of rebuilt per request
  • the doc placement in BlockchainBridge
  • the slot duration for blockchain-test spec providers
  • FOCIL blocks are no longer forced onto sequential BAL application
  • IsEip8198Enabled is no longer on IEip1559Spec
  • the OverridableReleaseSpec fallback comment and the Bogota test comment wording
  • the lazy parent scope in BranchProcessor

@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 32s —— View job


Re-review of PR #14606 (after 9c52da8)

  • Get the diff since the last review (58d6a19..9c52da8)
  • Review the new mainnet and Sepolia genesis tests
  • Post findings

Verdict: No Critical, High, Medium or Low issues. There are two optional test-only nits. Every earlier finding is still resolved. I didn't build the code or run the tests here.

Mainnet and Sepolia genesis tests (9c52da8)

This commit only adds tests, in GethGenesisLoaderTests.cs:931-966.

  • Mainnet_and_sepolia_genesis_leave_eip8198_off loads the bundled Chains/foundation.json and Chains/sepolia.json. Both are Geth-format files with a config block. On the final spec it checks that:

    • Eip8198TransitionTimestamp is null and IsEip8198Enabled is false
    • the base fee fraction is still 1/8
    • Eip8198SlotDurationMs is null
    • slots stay at 12 s at LastScheduledForkTimestamp

    This guards the shipped networks against Bogota accidentally being scheduled.

  • Mainnet_and_sepolia_genesis_load_an_eip8198_slot_duration adds config.eip8198SlotDurationMs = 10000 to each real genesis. It checks that the loader accepts the value, that the provider exposes it, and that the slot length doesn't change while EIP-8198 is unscheduled. This is the setup the PR Remarks describe for when Hegota is scheduled.

  • File paths: ChainFile uses the same WorkDirectory/../../../../Chains path as LoadHoodiChainSpec, so it resolves the same way in CI.

  • Why the name is fully qualified: Nethermind.Core.Specs.SpecProviderExtensions.LastScheduledForkTimestamp is written out in full because Nethermind.Core.Extensions also has a SpecProviderExtensions class. Without the full name, the reference would be ambiguous.

Nits (test-only, optional)

  • Comment wording (GethGenesisLoaderTests.cs:962): // EIP-8198 is not scheduled, so the slot length stays the configured one. In this test the configured value is the 10 000 ms that was just set, while the assertion expects the 12 s passed to GetSlotSchedule. Wording such as "…so the slot length stays secondsPerSlot" would avoid the confusion.
  • Duplicate path helper: LoadHoodiChainSpec (line 81) could reuse the new ChainFile("hoodi.json"). AGENTS.md says to keep unrelated code unchanged, so this is fine to leave as it is.

Still open from earlier reviews (optional, not blocking)

  • OP/Taiko calculators that override only the denominator: a heads-up only. It matters only if one of those stacks ever schedules EIP-8198.

Earlier findings

All still resolved:

  • the MaxChange simplification, now derived spec data
  • eth_feeHistory takes the next base fee from the canonical child
  • eth_baseFee and eth_fillTransaction use the spec one slot after the head
  • the slot schedule is cached instead of rebuilt per request
  • the doc placement in BlockchainBridge
  • the slot duration for blockchain-test spec providers
  • FOCIL blocks are no longer forced onto sequential BAL application
  • IsEip8198Enabled is no longer on IEip1559Spec
  • the OverridableReleaseSpec fallback comment and the Bogota test comment wording
  • the lazy parent scope in BranchProcessor

@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 14s —— View job


Re-review of PR #14606 (after a63b992)

  • Get the diff since the last review (9c52da8..a63b992)
  • Check the test-comment change
  • Post findings

Verdict: I found no Critical, High, Medium or Low issues. The comment-wording nit from the last review is fixed, and every earlier finding is still resolved. I didn't build the code or run the tests here.

Test comment wording (a63b992): resolved

  • This commit changes one line in GethGenesisLoaderTests.cs:963. The comment now reads // EIP-8198 is not scheduled, so the slot length stays Blocks.SecondsPerSlot.
  • The assertion passes 12 to GetSlotSchedule and expects 12 s, even though the genesis sets 10 000 ms, so the comment now says what the test actually checks.
  • No code changed.

Still open from earlier reviews (optional, not blocking)

  • OP/Taiko calculators that override only the denominator: a heads-up only. It matters only if one of those stacks ever schedules EIP-8198.
  • Duplicate path helper: LoadHoodiChainSpec could use ChainFile("hoodi.json"). It's fine to leave this as it is, because AGENTS.md says to keep unrelated code unchanged.

Earlier findings

All still resolved:

  • the MaxChange simplification, now derived spec data
  • eth_feeHistory takes the next base fee from the canonical child
  • eth_baseFee and eth_fillTransaction use the spec one slot after the head
  • the slot schedule is cached instead of rebuilt per request
  • the doc placement in BlockchainBridge
  • the slot duration for blockchain-test spec providers
  • FOCIL blocks are no longer forced onto sequential BAL application
  • IsEip8198Enabled is no longer on IEip1559Spec
  • the OverridableReleaseSpec fallback comment and the Bogota test comment wording
  • the lazy parent scope in BranchProcessor
  • the comment wording in the mainnet and Sepolia slot-length test

@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 39s —— View job


Re-review of PR #14606 (after b9ca4ed)

  • Get the diff since the last review (a63b992..b9ca4ed, a merge of master only)
  • Check master changes in files the PR also touches
  • Post findings

Verdict: I found no Critical, High, Medium or Low issues, and every earlier finding is still resolved. I didn't build the code or run the tests here. This run isn't allowed to use git show --remerge-diff, so I checked the merge by diffing a63b992..b9ca4ed in the files that both sides change.

Merge of master (b9ca4ed)

No new PR commits. Five files that the PR touches also changed on master. The PR's changes in each one survived the merge:

  • FeeHistoryOracle.cs: master now loads rewards lazily. BlockFeeHistorySearchInfo was reordered and rewards moved into a StrongBox<BlockRewards?>.
    • BlockHash is still a field, just in a new position. So GetNextBaseFee (:253-257) still checks child.ParentHash == info.BlockHash and returns the canonical child's BaseFeePerGas.
    • The cached estimate still uses GetSpec(b.Number + 1, b.Timestamp) (from this PR). The rewards change doesn't affect the next-base-fee slot.
  • EthRpcModule.cs: master now estimates gas price and priority fee in eth_fillTransaction only when they are needed. BaseFee = BaseFeeCalculator.Calculate(head, GetNextBlockSpec(head)) is unchanged, so the spec is still the one for the slot after the head.
  • NewPayloadHandler.cs: master now starts sender recovery earlier and makes it cancellable. That code is separate from the PR's IL stored-recheck path (InclusionListCommittedState).
  • BlockProcessorTests.cs and E2ESyncTests.cs: the master changes are about releasing parallel-validation parent readers and about BAL state reconstruction. Neither touches the PR's TransactionsExecuted or IL changes.

Still open from earlier reviews (optional, not blocking)

  • OP/Taiko calculators that override only the denominator: a heads-up only. It matters only if one of those stacks ever schedules EIP-8198.
  • Duplicate path helper: LoadHoodiChainSpec could use ChainFile("hoodi.json"). Leaving it is fine, since AGENTS.md says to keep unrelated code unchanged.

Earlier findings

All still resolved:

  • the MaxChange simplification, now derived spec data
  • eth_feeHistory takes the next base fee from the canonical child
  • eth_baseFee and eth_fillTransaction use the spec one slot after the head
  • the slot schedule is cached instead of rebuilt per request
  • the doc placement in BlockchainBridge
  • the slot duration for blockchain-test spec providers
  • FOCIL blocks are no longer forced onto sequential BAL application
  • IsEip8198Enabled is no longer on IEip1559Spec
  • the OverridableReleaseSpec fallback comment and the Bogota test comment wording
  • the lazy parent scope in BranchProcessor
  • the comment wording in the mainnet and Sepolia slot-length test

Marchhill and others added 2 commits October 21, 2026 11:06
* feat(sync): count BAL catch-up blocks that execute because of the engine API or missing finality

Blocks delivered through engine_newPayload, and catch-up blocks the consensus
client has not reported finalized yet, now show up in
nethermind_bal_catch_up_fallbacks under NewPayload and NotFinalized. Only
blocks older than two epochs count, so following the head adds nothing.

The decorator now delegates when it is nested under another instance, so a
pipeline that loads the module twice no longer counts fallbacks twice. The
config description documents when the catch-up applies.

* feat(sync): log each block reconstructed from its access list at debug level

* fix(test): build TestBlockchain's container once, and drop the nesting guard it needed

TestBlockchain configured its container twice, so every module, decorator
and IMainProcessingModule was registered twice and the BAL catch-up
decorator was nested in the engine tests. Production registers it once, so
the [ThreadStatic] guard against nesting goes.

Rename the EngineApi reason, since forkchoiceUpdated reprocessing also uses
the engine path, and shorten the config description. The engine test now
checks the chain's own log, as the counters are shared with parallel tests.

* docs(sync): shorten BAL catch-up guidance

* fix(sync): follow slot schedule for BAL catch-up age

---------

Co-authored-by: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com>
@Marchhill

Copy link
Copy Markdown
Contributor Author

@claude review

@claude

claude Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @Marchhill's task in 1m 10s —— View job


Re-review of PR #14606 (after a3e4a73)

  • Get the diff since the last review (b9ca4ed..a3e4a73: 312fa8f, which is feat(sync): track executed BAL catch-up blocks #14899 squashed onto this branch, then the c7388f0 merge of master)
  • Review the BAL catch-up tracking changes (FinalizedBlockAccessListProcessor, FinalizedBlockAccessListPolicy, TestBlockchain)
  • Check the merge
  • Post findings

Verdict: I found no Critical, High or Medium issues. There is one Low, about the PR description. Every earlier finding is still resolved. I didn't build the code or run the tests here.

BAL catch-up tracking (312fa8f)

  • Engine API blocks: execution unchanged, only counting added.
    • ProcessingOptions.EthereumMerge already included DoNotUpdateHead, which was in excluded, so engine API payloads already skipped reconstruction before this change.
    • MarkAsProcessed is used only as part of EthereumMerge, so moving the check to MarkAsProcessed doesn't send any other path to execution.
    • The new branch only counts. It calls policy.Evaluate only when IsCatchingUp is true, meaning the block is more than 64 slots old. A node following the head therefore never takes the policy lock on engine_newPayload.
  • Paths that don't come from the engine API:
    • Producing a block (DoNotUpdateHead without MarkAsProcessed) still goes straight to inner and is never counted.
    • Queued sync blocks are evaluated once. NotFinalized is counted only once the block is past the catch-up age.
    • The needsGasDimensions check still runs before any list is used.
  • FinalizedBlockAccessListPolicy.Evaluate: it maps each early return false to a reason. CanReconstruct is now Evaluate(...) == Eligible, so its behaviour is unchanged.
  • Catch-up age follows the slot schedule:
    • IsCatchingUp uses _slots.AddSlots(header.Timestamp, 64), so the age is correct across an EIP-8198 slot-length change.
    • I checked Catch_up_age_follows_slot_duration_changes by hand. For example, from 2008: 16 × 12 s up to the fork at 2200, then 48 × 6 s, gives 2488.
    • The schedule is built once per decorator in the constructor, and GetSlotSchedule can no longer throw now that the duration is optional.
    • The decorator is only registered when the feature is on, which the new enabled: null case checks.
  • DI: the new ITimestamper, IBlocksConfig and ISpecProvider constructor parameters can be resolved through AddDecorator. EthereumRunnerTests passes them explicitly.
  • TestBlockchain: removing the second ConfigureContainer call fixes every module being registered twice. This affects every TestBlockchain-based test, so CI is the real check, but the change is correct.
  • Counters are static, so the tests avoid sharing them: the metric tests are in a [NonParallelizable] fixture, and the engine test checks the chain's own log instead of the counters. Both choices are correct, and the comments explaining them should stay.

Finding

Low (PR description): the PR now also carries #14899, the BAL catch-up fallback metrics.

  • That includes:
    • two new BalCatchUpFallbackReason values (EngineApi, NotFinalized)
    • the new BalCatchUpEligibility enum
    • a reworded ReconstructFinalizedStateFromBlockAccessLists config description
    • the TestBlockchain fix
  • The Changes section doesn't mention any of this. Only the slot-schedule-aware catch-up age is related to EIP-8198.
  • One line under Changes or Remarks, such as "Includes feat(sync): track executed BAL catch-up blocks #14899", would tell reviewers and release notes why sync and metrics files changed.

Merge of master (c7388f0)

master brought in the BAL catch-up benchmark. The only file it shares with this PR is BalCatchUpBenchmark.cs, where the merge result moves TimingBlockProcessor.TransactionsExecuted to Action<Block>. That matches the IBlockProcessor signature this PR changed, so the benchmark project should still compile.

Still open from earlier reviews (optional, not blocking)

  • OP/Taiko calculators that override only the denominator: a heads-up only. It matters only if one of those stacks ever schedules EIP-8198.
  • Duplicate path helper: LoadHoodiChainSpec could use ChainFile("hoodi.json"). It's fine to leave as is.

Earlier findings

All still resolved:

  • the MaxChange simplification, now derived spec data
  • eth_feeHistory takes the next base fee from the canonical child
  • eth_baseFee and eth_fillTransaction use the spec one slot after the head
  • the slot schedule is cached instead of rebuilt per request
  • the doc placement in BlockchainBridge
  • the slot duration for blockchain-test spec providers
  • FOCIL blocks are no longer forced onto sequential BAL application
  • IsEip8198Enabled is no longer on IEip1559Spec
  • the lazy parent scope in BranchProcessor
  • the test comment wording

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.

1 participant