fix(aggregation): resolve signers from the head state's validator registry - #420
Merged
mananuf merged 4 commits intoSep 1, 2026
Merged
Conversation
…istry A session looked up a stored state per attestation target and dropped the group when that state was absent. On devnet-5 that was every group, every slot: an aggregator holding 753 signatures produced nothing for 355 consecutive slots while its head tracked the chain. The lookup was never necessary. The registry is written once in generate_genesis and never by the state transition, so every state on the chain carries the same validators and the head state — which SnapshotInputs already requires — resolves the same signers. Using it removes the failure mode rather than reporting it. This also drops a store read per data root from the snapshot and the per-target state map it filled. The missing_target_state skip reason goes with it; the condition it named can no longer occur. Refs #418
The lean_attestation_aggregate_coverage_* gauges were registered but never set, so gean published no series for them and could not appear on the shared devnet Aggregation Coverage panels at all. That is half the reason an aggregator producing nothing went unnoticed: the panel an operator checks first is blank for gean whether or not it is working. Emits the same sections and labels the other clients use, so the numbers line up on one panel: timely new payloads captured before the tick promotes them late new payloads for the same round arriving after block votes for the round carried by the canonical head block combined union of the three agg_start_new what the session is about to work from, at interval 2 proposal_combined what our own proposal covers plus the block-vs-timely symmetric difference. Subnet split is validator_id % committee_count, matching p2p.SubnetID. The four post-block sections record a genuine all-zero reading: an empty slot is real information and should read as a dip, not as a gauge holding its last value. The diff gauges instead keep their previous value until a block has reported the round, since before that the comparison is undefined rather than empty. Pure observability; nothing here feeds fork choice or the transition. Refs #418
mananuf
changed the base branch from
fix/aggregation-skip-visibility
to
main
September 1, 2026 16:25
mananuf
changed the base branch from
main
to
fix/aggregation-skip-visibility
September 1, 2026 16:26
A single-aggregator devnet leaves the payload buffers empty whenever the emitters read them, so the sections there are legitimately zero and prove nothing. These feed known votes for a round straight into the reporter and assert the block/timely split and the symmetric difference, plus the empty-round and no-head-state paths. Refs #418
feat(metrics): report attestation-aggregate coverage
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Step 2 of #418: remove the failure mode that step 1 made visible.
Stacked on #419 — review that first; this PR's diff is against it.
The problem
aggregateFromSnapshotlooked up a stored state per attestation target and dropped the group when that state was absent:On devnet-5 (2026-08-31, 14:35–15:04 UTC) that was every group, every slot.
gean_7produced nothing in 355 of 356 slots while healthy (Behind: 0–3) and holding 753 signatures, each session finishing in 14–90 µs. It held 262 states against a head of slot 399, and the votes it was aggregating carried target slot 342.Why the lookup was never needed
Every use of that state was
state.Validators[...]— resolving attestation pubkeys, nothing else.The validator registry is written once, in
generate_genesis, and never by the state transition. I checked the pinned spec (lstar/state_transition.py):validators=appears only in genesis construction, and gean'sinternal/statetransitionnever assigns to.Validatorseither.So every state on the chain carries the same registry, and the head state resolves exactly the same signers.
SnapshotInputsalready requires a head state and returns nil without one, so it is always available — unlike an arbitrary target's state.What changes
snap.headState.Validators.missing_target_stateskip reason is removed: the condition it named can no longer occur.Selection, ordering, budget and the resulting aggregates are otherwise unchanged.
How ethlambda does it
crates/blockchain/src/aggregation.rsbuilds oneProjectedStatefrom the head state and validates candidates against that chain view. It has never needed a per-target stored state; this brings gean to the same footing.Risk
The change rests on the registry being genesis-fixed. If a future spec adds validator set changes, resolving against the head would be wrong for a target under a different registry — the reasoning is recorded in a comment at the call site so it surfaces during that work.
Testing
make test— all 25 packages green;make lint,go vetclean;-raceclean onaggregation,node,store.aggregateTestSnapshotbuilt a snapshot with no head state and votes targeting slot 0 — neither of whichSnapshotInputscan produce. They now carry a head state and a target slot matching the vote slot.