Repository navigation
fix(beacon): check vote signatures under the fork schedule's domain - #673
MegaRedHand wants to merge 1 commit into
Conversation
Gossip and the Beacon API check a vote against a state that was never advanced to the vote's epoch: gossip reads the voted block's post-state, the pool endpoint the head's. `get_domain` takes the fork version from `state.fork`, so while a fork's first slots are empty that state is still the previous fork's, and every correctly signed vote of the new fork fails its signature check: as a gossip REJECT, which also penalizes the peer that relayed it, and as "invalid signature" to a validator client. Seen on a fulu-to-gloas transition devnet: with the first two gloas slots empty, every ethlambda node refused every client's attestations for them, and a node whose head stayed pre-fork through an execution-client outage refused all of the fork's first epoch. Read the version from the config's schedule at the target epoch instead, for subnet attestations, the aggregate's three signatures, and `POST /eth/v2/beacon/pool/attestations`. The state transition keeps `get_domain`: it advances the state to the slot first, where the two agree.
🤖 Claude Code ReviewReview: PR 673, check vote signatures under the fork schedule's domainOverall: The fix is correct and well scoped. I read the diff only and did not build or run the tests. Why the approach is sound
Details that look right
Minor, non-blocking notes
No bugs or security issues found. LGTM. Automated review by Claude (Anthropic) · sonnet · custom prompt |
🤖 Kimi Code ReviewI'll review this PR which fixes fork-boundary attestation signature verification by using the config's fork schedule instead of the state's current fork when determining signing domains. Overall AssessmentThis is a well-structured bugfix for a legitimate consensus issue: when a fork's first slots are empty, attestations are verified against a state that hasn't been advanced to the new fork, causing signature verification failures. The fix introduces Issues Found1. Critical:
|
| Priority | Item | File | Line |
|---|---|---|---|
| Medium | Verify no other is_valid_indexed_attestation call sites need fixing |
attestation.rs, electra.rs |
78, 275 |
| Medium | Address non-deterministic wall_epoch in test |
pool.rs |
438 |
| Low | Remove unused get_domain import |
validator_client_tests.rs |
17 |
| Low | Pass config to scheduled_domain |
validator_client_tests.rs |
248 |
| Low | Add assertion/comment on config/state fork consistency | accessors.rs |
427 |
| Low | Document target.root == beacon_block_root in test | attestation.rs |
453 |
The core fix is correct and well-justified. The main risk is ensuring all call sites are updated and that the state transition paths don't accidentally use the wrong variant.
Automated review by Kimi (Moonshot AI) · kimi-k2.6 · custom prompt
🤖 Codex Code ReviewLooks good overall. The core change fixes a real consensus bug: gossip signature checks now use the fork schedule for
Small maintainability nit:
If you want, I can also do a second pass specifically on the new tests around fork-boundary gossip behavior. Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
…3-64-633-636-638-gloas-live The branch is cut from beacon-chain-integration 17af7c3, so it also brings bci's squash of #658. tmp already holds that content through its own #658 merges, so the storage conflicts take tmp's side and the merge changes nothing there. Beyond the conflict markers: - gloas's is_valid_indexed_attestation gets the same _with_domain split, and the gloas arm of gossip::aggregate::stateful_checks uses it, so a gloas aggregate for a pre-gloas block verifies too. - pool.rs keeps tmp's test scaffolding (fixture_at, attestation_for). Its signing helpers sign under the schedule, and the fork-boundary tests run fulu to gloas, the transition the devnet failure was seen on. - lib.rs keeps tmp's beacon_store_with_config, which has the same signature as the branch's.
Problem
Gossip and the Beacon API verify vote signatures against a state that was never advanced to the vote's epoch:
beacon_attestation_{subnet}gossip (gossip::attestation::stateful_checks)beacon_aggregate_and_proofgossip andPOST /eth/v2/validator/aggregate_and_proofs(gossip::aggregate::stateful_checks)POST /eth/v2/beacon/pool/attestations(pool::validate)get_domaintakes the fork version fromstate.fork. If a fork's first slots are empty, that state is still the previous fork's, so a correctly signed vote of the new fork fails its signature check. Gossip REJECTs it, which also penalizes the peer that relayed it, and a validator client gets "invalid signature". This lasts until a block of the new fork is imported.Seen on a fulu-to-gloas transition devnet:
Fix
get_domain_from_schedule(config, state, domain_type, epoch)reads the fork version from the config's schedule atepoch. It is used for:is_valid_indexed_attestation_with_domain(phase0 and electra) is the existing check with the domain passed in.is_valid_indexed_attestationis now a thin wrapper over it, so the state transition is unchanged.get_domain, since it advances the state to the slot first and there the two agree. Operation gossip (slashings) keeps it too, since the spec checks those against the head state as-is.Tests
the_schedule_names_the_new_fork_before_the_state_reaches_it: the two readings agree on the old fork's epochs and differ at the new fork's first.a_vote_across_a_fork_boundary_verifies_under_the_new_forks_version(gossip),an_attestation_in_a_forks_first_empty_slot_is_publishedandan_aggregate_in_a_forks_first_empty_slot_is_published(API): an electra head, a fulu vote. Each fails without the fix:BadSignature, or a 400.fork.cargo test --workspace --profile release-fast --lib --binspasses, as do the 96 gossip spec vectors (--features beacon-spec-tests -- gossip/) andmake lint.