fix(blockchain): source attestations from the head state's justified#447
fix(blockchain): source attestations from the head state's justified#447MegaRedHand wants to merge 4 commits into
Conversation
Validators sourced their vote's `source` checkpoint from the store's global `latest_justified`, which is a highest-slot-wins maximum. When a minority fork justifies a slot the head chain never saw, that maximum latches onto an off-head sibling. Voting with an off-chain source makes every head-chain target fail `is_valid_vote` (the target no longer descends from the source), so the head can never re-justify and block production stalls. Source from the head state's `latest_justified` instead, which always lies on the head's own chain. The target walk-back is fed the same head-state justified so the emitted source and target stay mutually consistent. The store's `latest_justified` is left untouched; only vote production changes. `get_attestation_target` (used by the forkchoice spec-test harness) keeps reading the store checkpoints, so spec behavior is unchanged. This mirrors the head-state-driven treatment finalized already received in #437, but applied to vote production rather than the stored checkpoint. It is a deliberate divergence from leanSpec, which sources from `store.latest_justified` (leanSpec PR #595). Claude-Session: https://claude.ai/code/session_01783X9yFrkXfA7uY1cyL8E4
🤖 Kimi Code ReviewOverall Assessment: This is a critical liveness fix for fork choice safety. The change correctly prevents attestation production from stalling when the store's global justified checkpoint latches onto a minority fork. Issues and Suggestions: 1. Panic Risk in State Retrieval (Line 703-705) let source = store
.get_state(&head_root)
.expect("head state exists")
.latest_justified;While the head state should always exist in a well-formed store, using 2. Performance Consideration (Line 703) 3. Documentation Quality Security Implications: Recommendation: Accept, but consider replacing Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt |
🤖 Codex Code ReviewNo findings. The change in crates/blockchain/src/store.rs looks internally consistent: Residual gap: the new test only asserts I could not run the targeted tests in this environment: Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🤖 Claude Code ReviewNow I have a complete picture. Here is the review: Review:
|
The raw genesis state carries a zero-root justified placeholder that the state transition rebases to the real genesis root only on the first block. Sourcing votes from the head state (previous commit) therefore emitted a zero-root source for votes cast on the genesis head, which `validate_attestation_data` rejects as `UnknownSourceBlock`. Normalize the placeholder to the head root in `produce_attestation_data`, matching leanSpec PR #1166, which lands this exact source change plus the same genesis handling. This also confirms the change is now the spec direction rather than a divergence. Claude-Session: https://claude.ai/code/session_01783X9yFrkXfA7uY1cyL8E4
Summary
Validators sourced their vote's
sourcecheckpoint from the store's globallatest_justified, a highest-slot-wins maximum. When a minority fork justifies a slot the head chain never extended, that maximum latches onto an off-head sibling (always an ancestor of the head, but possibly at a higher slot than the head chain's own state has justified). Sourcing a vote from that higher slot pins every target above the canonical chain's justification ladder, so the canonical chain cannot finalize until it climbs past whatever slot the minority fork justified, and block production stalls (the "did not converge" stall).This sources the vote's
sourcefrom the head state'slatest_justifiedinstead, which always reflects what the head chain itself justified, so source and target stay on the same ladder and finalization can advance.Matches leanSpec PR #1166
This is the same change leanSpec is landing in PR #1166, including its genesis handling, so it aligns ethlambda with the spec direction (the prior
store.latest_justifiedsourcing came from leanSpec #595).What changed
produce_attestation_data(vote production only):sourcestore.latest_justified()(global max)head_state.latest_justified(head chain's own)store.latest_justified()latest_justifiedcheckpoint is left untouched.get_attestation_target(store)is unchanged and still reads the store checkpoints. It is what the forkchoice spec-test harness uses to validateattestationTargetSlot, so spec-test behavior is unchanged.validate_attestation_datawould reject it withUnknownSourceBlock.Testing
produce_attestation_data_sources_from_head_state_not_store: with the store's global justified latched onto a higher off-head sibling, the produced source follows the head state's on-chain justified.produce_attestation_data_normalizes_genesis_placeholder_source: a genesis-head vote rebases the zero-root placeholder to the genesis root and passesvalidate_attestation_data.cargo test -p ethlambda-blockchain --lib— 42 passed.cargo fmt --all -- --check,cargo clippy -p ethlambda-blockchain --all-targets -- -D warnings— clean.get_attestation_target, the only attestation path the spec harness exercises. CI will confirm against the full vectors.https://claude.ai/code/session_01783X9yFrkXfA7uY1cyL8E4