Glamsterdam - Engine API Changes#11081
Conversation
|
@claude review please |
|
Claude finished @svlachakis's task in 4m 44s —— View job PR Review — Glamsterdam Engine API Changes
SummaryThe implementation is correct overall — the two spec changes from execution-apis PRs #770 (ancestor reorg depth limit) and #760 (zero-hash preservation) are handled properly. Tests cover the key boundary cases. A few issues are worth addressing before merging. Issues1. Misleading method name
|
| Spec requirement | Implementation | Verdict |
|---|---|---|
| Ancestor ≤32 blocks → MUST reorg | > AncestorReorgDepthLimit (strict) → NOT skipped at depth 32 |
✓ |
| Ancestor >32 blocks → MAY skip | > 32 → skipped |
✓ |
| Non-canonical branch → always reorg | IsMainChain guard in IsOnMainChainBehindHead short-circuits |
✓ |
| Unprocessed block check → no depth limit | Uses IsOnMainChainBehindOrEqualHead (not depth-limited) |
✓ |
| Zero safe/finalized → preserve last known | ResolveZeroHash(hash, _blockTree.FinalizedHash/SafeHash) in both ForkChoiceUpdated call sites |
✓ |
| Zero with no prior → stays zero | knownHash ?? Keccak.Zero |
✓ |
| Non-zero unknown finalized → -38002 | Pre-existing IsInconsistent check unmodified |
✓ |
| Taiko always reorgs (no depth limit) | TaikoForkchoiceUpdatedHandler.IsOnMainChainBehindHead always returns true |
✓ |
The boundary arithmetic is correct: headNumber - block.Number > 32 evaluates to false at depth 32 (triggers reorg) and true at depth 33 (allows skip). Both ForkChoiceUpdated call sites (ApplyForkchoiceUpdate L289 and StartBuildingPayload L339) are updated consistently — the double write is idempotent and pre-existing.
Non-blocking notes
- The
ResolveZeroHashhelper is clean and easy to follow. - Taiko's override design (virtual
IsOnMainChainBehindHead) is a good, minimal extension point. - The Taiko test at L128 (
IsOnMainChainBehindHead_Override_PreventsAncestorReorgSkip) manually mocksIBlockTree— this is a pre-existing pattern in that file but worth noting for future cleanups.
|
@claude review |
|
Claude finished @LukaszRozmej's task in 3m 59s —— View job PR Review — Glamsterdam Engine API Changes
SummaryThe implementation is correct. Both spec changes — the 32-block ancestor reorg depth limit (execution-apis #770) and zero-hash preservation (execution-apis #760) — are implemented accurately. Previous review feedback was well applied: method renamed, tests parameterized, and payload-path test added. A few things remain worth noting. Correctness Verification
Issues1. Confusing return semantics on the virtual method
The virtual method is named The callsite amplifies this: Consider renaming to reflect the actual boolean meaning — e.g. 2.
|
|
@claude review again please |
|
Claude finished @svlachakis's task in 4m 37s —— View job PR Review — Glamsterdam Engine API Changes
SummaryAll issues raised in the two previous review rounds have been addressed. The implementation is correct and clean. One minor inconsistency remains that is worth fixing before merging. Addressed From Previous Reviews
Remaining IssueSource inconsistency in
|
| Spec requirement | Implementation | Verdict |
|---|---|---|
| Ancestor ≤32 blocks → MUST reorg | > AncestorReorgDepthLimit (strict) — depth 32 is not skipped |
✓ |
| Ancestor >32 blocks → MAY skip | > 32 → ShouldProceedWithReorg returns false |
✓ |
| Non-canonical branch → always reorg | IsMainChain guard in IsAncestorOnMainChainBeyondReorgDepthLimit |
✓ |
| Unprocessed block → no depth limit | Line 170 uses IsOnMainChainBehindOrEqualHead directly |
✓ |
| Zero safe/finalized → preserve last known | ResolveZeroHash(hash, _blockTree.FinalizedHash/SafeHash) in both ForkChoiceUpdated call sites |
✓ |
| Zero with no prior → stays zero | knownHash ?? Keccak.Zero |
✓ |
| Non-zero unknown finalized → -38002 | IsInconsistent check; zero-hash path correctly bypassed |
✓ |
| Taiko always reorgs | Override always returns true |
✓ |
Non-Blocking Notes
EnsureNewHeadis called beforeShouldProceedWithReorgat line 243 vs 250. For skip-eligible ancestors (>32 blocks),TryGetBranchtraverses the tree but the result is discarded. No correctness issue — pure read, no state mutation — just a minor wasted traversal.AncestorReorgDepthLimit = 32as a public constant onBlockTreeExtensionsis the right call for testability and Taiko reuse.- The
ShouldProceedWithReorgnaming and[NotNullWhen(false)]annotation make the virtual contract clear for future overriders.
|
@claude review again |
|
Claude finished @svlachakis's task in 7m 10s —— View job PR Review — Glamsterdam Engine API Changes (latest round, post spec-update #786)
SummaryAll previous issues are resolved. The spec compliance for execution-apis #760, #770, and #786 is correct. However, this round introduced a self-inconsistency between two changes made in the same PR: Critical Issue
|
| Spec requirement (PR #786) | Implementation | Verdict |
|---|---|---|
| MAY skip when head is valid ancestor of latest known finalized | newHeadHeader.Number <= knownFinalizedHeader.Number && IsMainChain(newHeadHeader) |
✓ |
| MUST return -38006 when reorg depth > client limit | reorgDepth > _maxReorgDepth → TooDeepReorg |
✓ |
| Client limit tied to pruning state | _maxReorgDepth = pruningConfig.PruningBoundary (default 64) |
✓ |
-38006 code added to MergeErrorCodes |
TooDeepReorg = -38006 |
✓ |
| Finalized header not found → safe fallback | Logs warning, falls through to depth check | ✓ |
| Zero safe/finalized → preserve last known (PR #760) | ResolveZeroHash in both ApplyForkchoiceUpdate call sites |
✓ |
| Non-zero unknown finalized → -38002 | Pre-existing ValidateBlockHash / finalizationErrorMsg path |
✓ |
| Taiko always reorgs | ShouldProceedWithReorg override always returns true |
✓ |
Test Coverage Verification
| Spec requirement | Test | Status |
|---|---|---|
| Ancestor within PruningBoundary → MUST reorg | WhenHeadIsCanonicalAncestorWithinPruningBoundary_ReorgsToAncestor [TestCase(3), TestCase(33)] |
✓ |
| Ancestor of finalized → MAY skip | WhenHeadIsAncestorOfFinalizedBlock_SkipsUpdate |
|
| Reorg depth > PruningBoundary → -38006 | WhenReorgDepthExceedsPruningBoundary_ReturnsTooDeepReorg |
✓ |
| Different branch → always reorg | WhenHeadIsOnDifferentBranch_ReorgsRegardlessOfDepth |
✓ |
| Zero safe/finalized → no -38002 | WhenZeroFinalizedAndSafeHash_ReturnsValidWithoutError |
✓ |
| Non-zero unknown finalized → -38002 | WhenNonZeroUnknownFinalizedHash_ReturnsInvalidForkchoiceState |
✓ |
| Zero finalized preserves last known | WhenZeroFinalizedHash_PreservesKnownFinalizedHash + _WithPayloadAttributes |
✓ |
| Taiko bypasses -38006 | ShouldProceedWithReorg_Override_BypassesTooDeepReorgError |
✓ |
FindMainChainAncestorNumber Correctness
- Main-chain block: returns
block.Numberdirectly → depth =head.Number - block.Number(exact) - Side-chain block: walks parents to find common ancestor → conservative depth (correct for state availability)
- Null parent (pruned ancestry): returns 0 (genesis) → maximizes reported depth (safe fallback)
Non-blocking Notes
BaseEngineModuleTests.cschange is a correct design decision in isolation (helpers should not silently imply finality semantics). The bug is that the new test wasn't updated to match._maxReorgDepthcomment referencing execution-apis/pull/786 is good.[NotNullWhen(false)]onShouldProceedWithReorgcorrectly documents the out-parameter contract.<=innewHeadHeader.Number <= knownFinalizedHeader.Numbercorrectly includes FCU to the finalized block itself (equivalent to an ancestor).
|
|
||
| // Set head to b34 and explicitly finalize it | ||
| (await rpc.engine_forkchoiceUpdatedV1(new ForkchoiceStateV1(b34Hash, b34Hash, b34Hash))).Data.PayloadStatus.Status.Should().Be(PayloadStatus.Valid); | ||
| chain.BlockTree.HeadHash.Should().Be(b34Hash, "precondition: head is at H=34"); |
There was a problem hiding this comment.
Critical — broken test: precondition and main assertion will both fail
ProduceBranchV1(setHead: true) was changed in this same PR (BaseEngineModuleTests.cs) to use Keccak.Zero for finalized/safe:
// BaseEngineModuleTests.cs — after this PR's change:
ForkchoiceStateV1 forkchoiceStateV1 = new(newHead, Keccak.Zero, Keccak.Zero);So after ProduceBranchV1(34, setHead: true), chain.BlockTree.FinalizedHash is null/Keccak.Zero, not b34Hash. This assertion will throw:
chain.BlockTree.FinalizedHash.Should().Be(b34Hash, "precondition: b34 is finalized after setHead:true branch");
And even if the precondition were skipped, ShouldProceedWithReorg with zero finalized would fall through to the depth check: reorgDepth = 34 - 1 = 33 < PruningBoundary(64) → reorg proceeds → head changes to b1 → the assertion at line 616 (HeadHash.Should().Be(b34Hash)) also fails.
The fix is to explicitly finalize b34 before testing the MAY-skip rule:
// After ProduceBranchV1, explicitly set finalized to b34:
await rpc.engine_forkchoiceUpdatedV1(new ForkchoiceStateV1(b34Hash, b34Hash, b34Hash));
chain.BlockTree.FinalizedHash.Should().Be(b34Hash, "precondition: b34 is finalized");Also update the stale comment at line 597–600 which says "ProduceBranchV1 with setHead:true finalizes each block via FCU" — this is no longer true after the BaseEngineModuleTests.cs change.
| ForkchoiceStateV1 forkchoiceStateV1 = new(newHead, newHead, newHead); | ||
| // Use Keccak.Zero for finalized/safe: ProduceBranchV1 is a chain-building helper, | ||
| // not a finality-setting one. Tests that need finalized must set it explicitly. | ||
| ForkchoiceStateV1 forkchoiceStateV1 = new(newHead, Keccak.Zero, Keccak.Zero); |
There was a problem hiding this comment.
High — silent breaking change to all setHead: true callers
Changing the FCU from new(newHead, newHead, newHead) to new(newHead, Keccak.Zero, Keccak.Zero) is a reasonable design decision (helpers shouldn't imply semantics they don't own), but it is a breaking change for any caller that relied on finalized being set.
The only explicit regression is the new WhenHeadIsAncestorOfFinalizedBlock_SkipsUpdate test in this same PR, which has an inconsistency: it calls ProduceBranchV1(setHead: true) and then asserts chain.BlockTree.FinalizedHash.Should().Be(b34Hash). With this change that assertion will fail.
Before landing, verify there are no other pre-existing callers in EngineModuleTests.Synchronization.cs (which has 4+ setHead: true usages) that rely on the finalized hash being advanced. A quick grep for FinalizedHash assertions near ProduceBranchV1(..., true) would confirm.
|
@claude review again |
|
Claude finished @svlachakis's task in 7m 15s —— View job PR Review — Glamsterdam Engine API Changes (post spec-update #786, latest round)
SummaryAll issues from the five previous review rounds have been resolved. The implementation correctly handles all three spec changes, DI wiring is sound, and tests are accurate and well-named. Resolved From Previous Reviews
Spec Compliance Verification
Test Coverage Verification
DI Wiring Verification
|
|
@claude review again, very deeply based on the spec. |
|
Claude finished @svlachakis's task in 9m 44s —— View job PR Review — Glamsterdam Engine API Changes (Deep Spec Review)
SummaryAll issues from prior review rounds have been resolved. After a deep spec-level re-read against execution-apis PRs #760, #770, and #786, the implementation is correct and spec-compliant. A few new low-severity issues are noted below; none block merging. Spec Compliance — Full VerificationExecution-apis PR #786 — FCU ancestor reorg depth limit
Execution-apis PR #760 — Zero-hash finalized/safe preservation
Taiko-specific
Boundary Arithmetic Verification
New FindingsLow —
|
| Spec requirement | Status |
|---|---|
| MAY skip → ancestor of finalized | ✓ Correct |
| MUST -38006 → depth > pruning boundary | ✓ Correct |
Boundary = PruningConfig.PruningBoundary (default 64) |
✓ Correct |
| Zero safe/finalized → preserve last known (PR #760) | ✓ Both call sites |
| Non-zero unknown → -38002 | ✓ Unchanged |
| Taiko bypass | ✓ Override always returns true |
| All 8 tests correct and well-named | ✓ |
The implementation is correct. All previous Critical/High/Medium findings have been resolved. Remaining issues are Low severity, previously acknowledged.
|
@claude review again, not all these comments are needed |
|
Claude finished @svlachakis's task in 3m 15s —— View job PR Review — Glamsterdam Engine API Changes
SummaryAll Critical, High, and Medium issues from the previous review rounds have been resolved. The implementation is correct and spec-compliant. Only one Low-severity item remains outstanding. Spec Compliance
Test Coverage
Remaining IssueLow —
|
| private readonly IPoSSwitcher _poSSwitcher = poSSwitcher ?? throw new ArgumentNullException(nameof(poSSwitcher)); | ||
| private readonly ILogger _logger = logManager.GetClassLogger<ForkchoiceUpdatedHandler>(); | ||
| private readonly bool _simulateBlockProduction = mergeConfig.SimulateBlockProduction; | ||
| // Spec point 6: implementation-specific limit for -38006. Matches pruning boundary |
There was a problem hiding this comment.
unclear comment, probably can be removed
| } | ||
|
|
||
| protected virtual bool IsOnMainChainBehindHead(BlockHeader newHeadHeader, ForkchoiceStateV1 forkchoiceState, | ||
| // Spec point 2: MAY skip if headBlockHash is a VALID ancestor of the latest known finalized block. |
| { | ||
| if (_logger.IsInfo) _logger.Info($"Valid. ForkChoiceUpdated ignored - already in canonical chain."); | ||
| errorResult = ForkchoiceUpdatedV1Result.Valid(null, forkchoiceState.HeadBlockHash); | ||
| BlockHeader? knownFinalizedHeader = _blockTree.FindHeader(knownFinalizedHash, BlockTreeLookupOptions.DoNotCreateLevelIfMissing); |
There was a problem hiding this comment.
use FindFinalizedHeader?
| } | ||
| } | ||
|
|
||
| long reorgDepth = (_blockTree.Head?.Number ?? 0) - FindMainChainAncestorNumber(newHeadHeader); |
There was a problem hiding this comment.
return false straight away if head is null
|
|
||
| _blockTree.ForkChoiceUpdated(forkchoiceState.FinalizedBlockHash, forkchoiceState.SafeBlockHash); | ||
| _blockTree.ForkChoiceUpdated( | ||
| ResolveZeroHash(forkchoiceState.FinalizedBlockHash, _blockTree.FinalizedHash), |
There was a problem hiding this comment.
add comment to explain why this is needed
| _manualBlockFinalizationManager.MarkFinalized(newHeadHeader, finalizedHeader!); | ||
| } | ||
|
|
||
| Hash256 resolvedFinalizedHash = ResolveZeroHash(finalizedBlockHash, _blockTree.FinalizedHash); |
There was a problem hiding this comment.
add comment to explain why this is needed
| BlockHeader? knownFinalizedHeader = _blockTree.FindHeader(knownFinalizedHash, BlockTreeLookupOptions.DoNotCreateLevelIfMissing); | ||
| if (knownFinalizedHeader is null) | ||
| { | ||
| if (_logger.IsWarn) _logger.Warn($"Known finalized hash {knownFinalizedHash} has no header — cannot check spec point 2 ancestry. Falling back to depth limit."); |
| // block this is the common ancestor's number — used to compute the true reorg depth. | ||
| // Returns 0 (genesis) when the parent chain cannot be fully walked, which is the most | ||
| // conservative fallback (maximises the reported reorg depth). | ||
| private long FindMainChainAncestorNumber(BlockHeader newHeadHeader) |
There was a problem hiding this comment.
maybe just have a function to calculate reorg depth directly
| { | ||
| /// <summary> | ||
| /// Returns <c>true</c> when <paramref name="header"/> belongs to the canonical chain | ||
| /// and is at or behind the current head — i.e. an unprocessed FCU to it can be safely |
|
closing in favour of #11388 |
Fixes Closes Resolves
Should get merged after #11354
Changes
Implement Engine API FCU spec updates
Implements two Engine API spec changes: the FCU ancestor reorg depth limit and the zero-hash finalized/safe block behavior.
FCU ancestor reorg depth limit
Previously, FCU to any canonical ancestor was silently skipped. The new spec narrows this: a skip is only allowed when the ancestor is more than 32 blocks behind the current head. At ≤32 blocks, the client MUST execute the reorg.
BlockTreeExtensions:
IsOnMainChainBehindHeadnow requiresheadNumber - blockNumber > 32(wasblockNumber < headNumber). The depth limit only applies to canonical ancestors — theIsMainChainguard ensures blocks on different branches always trigger a full reorg regardless of depth.ForkchoiceUpdatedHandler: The handler callsIsOnMainChainBehindHeadin two distinct places with different semantics. The unprocessed-block shortcut (line 170 — "is this block already in the canonical chain?") must not have a depth limit, so it was changed to callIsOnMainChainBehindOrEqualHeaddirectly. The post-EnsureNewHeadcheck (line 249 — "should we skip a reorg?") retains the virtual method with the 32-block limit.Zero hash behavior (execution-apis PR #760)
When
finalizedBlockHashorsafeBlockHashis zero, the client must use the latest known value rather than overwriting the stored hash with zero. BothForkChoiceUpdatedcall sites now resolve zero hashes viaResolveZeroHashbefore persisting to DB. Zero with no known previous value stays zero.The -38002 condition (step 6) already correctly required a non-zero hash via IsInconsistent.
Tests
7 new tests in
EngineModuleTests.V1.cscovering all spec branches: within/beyond/at the depth limit, different branch, zero hashes no-error, non-zero unknown hash error, and zero-hash preservation of known finalized state.Test coverage — all cases:
Types of changes
What types of changes does your code introduce?
Testing
Requires testing
If yes, did you write tests?