fix: gate builder bid lookup on shouldBuildOnFull in produceBlockV4 - #9442
Conversation
When the PTC majority votes the parent's payload was unavailable or untimely, shouldBuildOnFull returns false and the self-build path in produceBlockBody correctly reorgs to EMPTY. The builder bid path bypassed this check: produceBlockV4 unconditionally queried the bid pool with parentBlock.executionPayloadBlockHash (the FULL hash when FULL remains canonical head by weight), then preferred any fulfilled builder result over the engine result. Net effect: the validator would publish a builder block extending the late/unavailable FULL payload instead of executing the spec-mandated reorg. Compute shouldBuildOnFull before the pool lookup and, when reorging, query the pool with the EMPTY variant's executionPayloadBlockHash (grandparent EL hash) so only EMPTY-reorg-targeting bids can match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request updates the validator API to conditionally select the parent block hash for builder bids based on whether the node is building on a full block. However, querying the fork choice for the empty variant of the parent block is likely to return null during block production. It is recommended to directly use the parent block's parent block hash instead, which is more efficient and avoids this potential bug.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
…ckHash Use parentBlock.parentBlockHash directly instead of looking up the EMPTY variant in the protoArray. For any variant of a gloas parent block, both fields hold the same value (the bid's parent_block_hash), because forkChoice.onBlock sets executionPayloadBlockHash = parentBlockHash = bid.parentBlockHash for the PENDING/EMPTY variants, and only the FULL variant overrides executionPayloadBlockHash to the delivered payload hash. Pre-gloas parents (at the fork transition) have parentBlockHash = null, which getBestBid already handles by returning null. The explicit null guard is therefore unnecessary. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Performance Report🚀🚀 Significant benchmark improvement detected
Full benchmark results
|
nflaig
left a comment
There was a problem hiding this comment.
lgtm, I am fine with merging that as is, functionally, it should achieve what we want
| parentBlock.executionPayloadBlockHash, | ||
| parentBlockRootHex | ||
| ); | ||
| const isBuildingOnFull = chain.forkChoice.shouldBuildOnFull(parentBlock, slot); |
There was a problem hiding this comment.
have you looked into deduplicating the shouldBuildOnFull call? now we call it twice, this might require more refactoring though, but ideally we just wanna call it once
| parentBlockRootHex | ||
| ); | ||
| const isBuildingOnFull = chain.forkChoice.shouldBuildOnFull(parentBlock, slot); | ||
| const bidParentBlockHash = isBuildingOnFull ? parentBlock.executionPayloadBlockHash : parentBlock.parentBlockHash; |
There was a problem hiding this comment.
I find our naming here confusing, why do we use parentBlockHash per spec, but also executionPayloadBlockHash instead of blockHash?
we can choose either, but would be good if we can be consistent, or is there a good reason for this?
|
going to merge this, we can look into further refactoring separately |
|
🎉 This PR is included in v1.44.0 🎉 |
See discussion from https://github.com/ChainSafe/lodestar/pull/9431/changes#r3330285562