Describe the bug
The proposer-boost dependent-root gate added in #9565 compares the dependent root of the incoming block against the cached this.head:
https://github.com/ChainSafe/lodestar/blob/6e4cd4e2c8dc4d1b4bfc3e35337030cf7d708060/packages/fork-choice/src/forkChoice/forkChoice.ts#L719-L724
The spec computes the head fresh inside on_block, right before inserting the block:
# Compute head before applying the block
head = get_head(store)
...
update_proposer_boost_root(store, head.root, block_root)
https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.12/specs/phase0/fork-choice.md#on_block
These diverge when the tick preceding the block import crosses an epoch boundary: on_tick's pull-up can update the justified checkpoint, which changes filter_block_tree and therefore the head. Lodestar's cached this.head still points at the pre-tick branch, the dependent-root comparison is made against the wrong chain, and the boost is denied for a block the spec would boost.
Impact
The first timely block after an epoch-boundary justification change can lose its proposer boost, weakening reorg resistance for that slot. Narrow window, but it is exactly the epoch-boundary slot where fork choice is most contested.
Reproduction
Found by the fork-choice compliance suite (consensus-specs model-generated vectors, integration in progress): case block_tree_test_16_201284350_1 fails identically on every pre-gloas fork (altair→fulu, 1471/1472 each) at a check step expecting proposer_boost_root set after a block delivered at the exact start of an epoch.
Instrumented trace at the failing import: isTimely=true, proposerBoostRoot=null, but sameDepRoot=false with this.head still on the pre-tick branch. Forcing a fresh head in the gate makes the fork pass 1472/1472.
Candidate fix
Use a freshly computed head in the gate (e.g. this.updateHead() instead of this.head), which also matches the spec's ordering of vote processing relative to on_block. Needs a perf sanity-check (one findHead per block import) and should be sequenced with:
Describe the bug
The proposer-boost dependent-root gate added in #9565 compares the dependent root of the incoming block against the cached
this.head:https://github.com/ChainSafe/lodestar/blob/6e4cd4e2c8dc4d1b4bfc3e35337030cf7d708060/packages/fork-choice/src/forkChoice/forkChoice.ts#L719-L724
The spec computes the head fresh inside
on_block, right before inserting the block:https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.12/specs/phase0/fork-choice.md#on_block
These diverge when the tick preceding the block import crosses an epoch boundary:
on_tick's pull-up can update the justified checkpoint, which changesfilter_block_treeand therefore the head. Lodestar's cachedthis.headstill points at the pre-tick branch, the dependent-root comparison is made against the wrong chain, and the boost is denied for a block the spec would boost.Impact
The first timely block after an epoch-boundary justification change can lose its proposer boost, weakening reorg resistance for that slot. Narrow window, but it is exactly the epoch-boundary slot where fork choice is most contested.
Reproduction
Found by the fork-choice compliance suite (consensus-specs model-generated vectors, integration in progress): case
block_tree_test_16_201284350_1fails identically on every pre-gloas fork (altair→fulu, 1471/1472 each) at a check step expectingproposer_boost_rootset after a block delivered at the exact start of an epoch.Instrumented trace at the failing import:
isTimely=true,proposerBoostRoot=null, butsameDepRoot=falsewiththis.headstill on the pre-tick branch. Forcing a fresh head in the gate makes the fork pass 1472/1472.Candidate fix
Use a freshly computed head in the gate (e.g.
this.updateHead()instead ofthis.head), which also matches the spec's ordering of vote processing relative toon_block. Needs a perf sanity-check (onefindHeadper block import) and should be sequenced with: