-
Notifications
You must be signed in to change notification settings - Fork 61
fix: use head state's justified checkpoint as attestation source #501
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,115 @@ | ||||
| """ | ||||
| Tests for attestation source consistency between voting and block building. | ||||
|
|
||||
| Regression test for a bug where produce_attestation_data used store.latest_justified | ||||
| as the attestation source, while build_block filtered attestations against | ||||
| post_state.latest_justified. When store.latest_justified diverged from the head | ||||
| state's latest_justified (caused by processing a non-head-chain block that crosses | ||||
| the 2/3 supermajority threshold), all attestations would be filtered out during | ||||
| block building, producing blocks with 0 attestations. | ||||
|
|
||||
| The fix aligns produce_attestation_data with the original 3sf-mini design: the | ||||
| attestation source comes from the head block's post-state, not from the store-wide | ||||
| max. | ||||
|
|
||||
| See: ethereum/research 3sf-mini/p2p.py — Staker.vote() uses | ||||
| self.post_states[self.head].latest_justified_hash, not a store-wide value. | ||||
| """ | ||||
|
|
||||
| from __future__ import annotations | ||||
|
|
||||
| from lean_spec.subspecs.containers.checkpoint import Checkpoint | ||||
| from lean_spec.subspecs.containers.slot import Slot | ||||
| from lean_spec.types import Bytes32 | ||||
|
||||
| from lean_spec.types import Bytes32 |
Copilot
AI
Mar 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TEST_VALIDATOR_ID is imported but never used in this test module; with ruff enabled (F401), this will fail linting. Please remove the unused import.
| TEST_VALIDATOR_ID, |
Copilot
AI
Mar 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test’s “build_block initial filter” simulation uses head_state.latest_justified, but State.build_block() actually filters attestations against post_state.latest_justified after processing the candidate block. At genesis-parent (slot 1), process_block_header can update the justified checkpoint root even for an empty block, so post_state.latest_justified is not guaranteed to equal head_state.latest_justified. To avoid a false sense of coverage, compute the filter the same way as build_block does (derive post_state from an empty candidate block) and assert the produced att_data.source matches that value; also consider asserting store.validate_attestation(Attestation(...)) passes for the produced data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
head_state.latest_justifiedas the attestation source can produce an invalid source root at genesis: the anchorStateis stored withlatest_justified.root == Bytes32.zero()(seeState.generate_genesis), whileStore.validate_attestation()requiresdata.source.root in self.blocks. With this change,produce_attestation_data()at genesis would emit a source checkpoint whose root is not inself.blocks, causing gossip validation (and aggregation/block building paths) to fail. Consider anchoring the stored head state’s checkpoints to the anchor root inStore.from_anchor(update the storedstate.latest_justified/latest_finalizedroots), or otherwise ensure the returnedsource.rootalways refers to a known block root (e.g., fallback whenhead_state.latest_justified.rootis unknown).