Repository navigation
docs: update roadmap progress - #19
Merged
Merged
Conversation
tcoratger
pushed a commit
to tcoratger/ethlambda
that referenced
this pull request
Oct 30, 2026
…o attestation subnets (lambdaclass#19) * feat(beacon): the rules for aggregates and attestation subnets Neither is wired to anything yet: this is the pure half, so the gossip handler and the chain actor that follow have something to call. `subnets` is `das`'s counterpart for attestations. A beacon node owes the network `SUBNETS_PER_NODE` long-lived subscriptions chosen from its own node id, because phase 0 has no shard committees and so nothing else gives the attestation subnets a stable membership; a follower holds them for the mesh's sake, not for its own head. Two byte-order traps are what make this worth its own module with fixtures: the prefix is the leading bits of the *big-endian* node id, while the permutation seed is hashed over the *little-endian* period, and either one alone yields a set that agrees with no other client. The prefix width is derived rather than read off `Config`, although lambdaclass#15 gave `ATTESTATION_SUBNET_PREFIX_BITS` a typed home. The specification defines it as a derivation over `ATTESTATION_SUBNET_COUNT`, so deriving it cannot disagree with the count it is taken over, whereas a configuration that narrows the count and omits the prefix key would fall back to mainnet's 6 and shuffle over a space its own count does not match. A test pins the two together for the configurations that do carry both. `aggregate` holds the `beacon_aggregate_and_proof` conditions that need a state. It is deliberately not the whole validator: the conditions needing nothing but a clock belong in the p2p handler, and the two seen-set gates belong in the chain actor, because the specification marks an aggregate seen only *after* its signatures verify. Recording it earlier is a one-message censorship attack, since a garbage aggregate claiming some `(epoch, aggregator)` pair would drop that aggregator's genuine one. Lighthouse splits it the same way, reading its observed-sets early and writing them in `verify_late_checks`. Committees resolve against the aggregate's own target checkpoint state, not `store.block_states[get_head(store).root]` as the pseudocode has it. That is stricter, since the head may sit on a branch the aggregate does not vote for while the target is an ancestor of the attested block by `validate_on_attestation`'s own consistency check, and it means one state serves both these conditions and the `on_attestation` that follows. `SignedAggregateAndProof` moves down from `ethlambda-p2p`, which re-exports it: the gossip path no longer ends at the decode, so the type has to sit where both consumers can reach it. * feat(beacon): apply gossip aggregates to fork choice, and backbone two subnets `beacon_aggregate_and_proof` was subscribed, decoded and dropped, so fork choice learned its votes only from block bodies. It now reaches `on_gossip_aggregate`, and the ENR stops claiming this node serves no attestation subnet when the specification asks every node to serve two. The path is p2p -> `new_beacon_aggregate` -> chain actor, and each layer keeps the checks it can actually answer. p2p runs the conditions needing nothing but a clock, so a stale or malformed aggregate never costs a mailbox slot. The actor runs the rest, because the committee-shaped conditions need a state and the two seen-set gates need the verification verdict: the specification marks an aggregate seen only after its signatures verify, and a set written earlier is a one-message censorship attack, since a garbage aggregate claiming some `(epoch, aggregator)` pair would drop that aggregator's real one for the epoch. The deferral queue is load-bearing, not an optimization. `validate_on_attestation` requires `get_current_slot(store) >= data.slot + 1` and aggregates are published two thirds of the way through the slot they vote for, so every one of them arrives a slot too early. Without the queue this topic would apply approximately nothing. It drains once per beacon tick, between the clock advancing and the head being recomputed, so the votes it releases are in fork choice before the head that tick reports is chosen. The superset-of-bits gate is what makes the volume affordable rather than any amortization: a committee's `TARGET_AGGREGATORS_PER_COMMITTEE` aggregators mostly converge on the same votes, and once the running union covers a later aggregate it is dropped for a hash and two lookups instead of three signature verifications. Per-aggregate costs the gates do not remove are left in place deliberately, one `block_index()` scan and one `EpochCommittees` build each, until measurement says they matter. Which is what the four new metrics are for: decode in p2p, the mailbox hop, the processing span and the end-to-end time. Without them a cost regression here surfaces as an unexplained head lag. The subnet backbone is separate work sharing this branch. Every beacon node should hold `SUBNETS_PER_NODE` long-lived subscriptions chosen from its node id so the attestation subnets have a stable mesh for validators to publish into; what arrives on them is relayed but never applied to fork choice, which is what a lighthouse node with no validators does too. Signatures are not verified there, and that is the one place this stops short of lighthouse on purpose: the relay has already happened by the time a handler sees the payload, since this node runs gossipsub without `validate_messages()`, and nothing downstream reads the verdict. Peer scoring is what should turn it into a real verifier. `SignedAggregateAndProof` moves again, from the state-transition crate to `ethlambda-types`. `ethlambda-network-api` has to name it and depends only on that crate, deliberately, so the container lives there and the one conversion needing fork choice's own `Attestation` stays behind as a `From`. * docs(beacon): describe the aggregate path and the subnet backbone `beacon_wire.md` opened by saying this node "does not read the aggregate topic, so fork choice learns its votes from block bodies", which is now the opposite of what it does. The new section says where each of `validate_beacon_aggregate_and_proof_gossip`'s conditions runs and why that split is forced rather than chosen: the seen-set gates need the verification verdict, so recording on arrival would let one garbage aggregate censor an aggregator's real one for the epoch. Two departures are written down as departures, with their reasoning, so neither reads later as an oversight. Committees resolve against the target checkpoint state rather than the head's, which is stricter. And the subnet backbone is subscribed and relayed but neither applied to fork choice nor signature-verified — the first matching a lighthouse follower, the second stopping short of one, because this node runs gossipsub without `validate_messages()` so the relay has already happened and no verdict is read. `metrics.md` gains the six aggregate metrics with what to watch and why. `mailbox_wait` gets the emphasis: it is the failure mode this path introduces and the only one no other timing can show, since aggregates queueing behind block imports arrive too late to move the head while every per-aggregate timing still looks healthy. * docs(beacon): name the flag that would make subnet verification worth paying for The subnet handler and `beacon_wire.md` both said turning peer scoring on is what should make it verify signatures. That names the wrong trigger: `validate_messages()` is what holds propagation until a verdict is reported back, so it is what gives a verdict somewhere to go. Acting on the old wording would mean enabling scoring and finding nothing had changed. Also says what does *not* differ. Lighthouse sets `validate_messages()` and pairs it with `ValidationMode::Anonymous`, the same mode this node uses, so the mode is not the distinction between the two clients; that one flag is. Worth writing down, since `ValidationMode::Anonymous` is the line a reader of this node's swarm setup actually sees and could easily read as "validation is off". * test(beacon): pin the per-slot aggregate bound per preset MAX_AGGREGATES_PER_SLOT derives from MAX_COMMITTEES_PER_SLOT, which the minimal preset shrinks, so asserting the mainnet answer failed both minimal CI jobs. Assert each preset's own value rather than the definition, which would restate the arithmetic instead of checking it. * refactor(beacon): move the attestation subnet math to the p2p crate Every function in subnets.rs is a pure function of a node id, a slot and Config; none of them touches a BeaconState. What they describe is which topic a message goes on, so they belong with the wire code that consumes them rather than with the state transition. The two primitives they do need, the swap-or-not shuffle and SHA-256, are imported. das.rs is the same shape for columns and stays put: the networking fixture suite has handlers for get_custody_groups and compute_columns_for_custody_group, and that runner lives in the state transition crate. aggregate.rs stays for a different reason, that is_aggregator and the gossip conditions both take a state. * fix(beacon): decode subnet attestations as SingleAttestation from electra on From electra, beacon_attestation_{subnet_id} carries a SingleAttestation, not an Attestation. The decoder chose the fork from a slot read four bytes in, where phase0's Attestation keeps it. On a SingleAttestation those bytes are part of the committee and attester indices, and whichever fork they landed on, the bytes were then decoded as a container they are not. Every subnet vote on mainnet was counted as decode_failed. Relaying was unaffected, since gossipsub forwards a message before the handler sees it. The payload cannot name its own fork on this topic, because electra moved the slot. The fork now comes from the topic instead, the way p2p-interface.md types every topic by its fork digest: BeaconWire carries the fork its digest was computed at, and decode_attestation takes that fork rather than the config. * perf(beacon): resolve gossip aggregate committees through the committee cache The committee cache made EpochCommittees::new shuffle the whole active set up front, which only pays off when callers share the result. The aggregate path still had two uncached get_beacon_committee lookups (is_aggregator and the committee-size check), so every aggregate paid two full shuffles of ~2.4M validators. On the eth-3 mainnet follower that was ~956 ms per applied aggregate, up from 65 ms, which saturated the chain actor and stopped the head. Both lookups now go through the actor's cache, the same one verified_attesting_indices already uses.
tcoratger
pushed a commit
to tcoratger/ethlambda
that referenced
this pull request
Oct 30, 2026
…relaying them (lambdaclass#47) * refactor(beacon): keep the committee cache in the Store, shared by both actors Validating aggregates and subnet attestations in p2p needs the same shufflings the chain actor derives, and a shuffle of the mainnet active set is too expensive to run twice per epoch. The actor owned its CommitteeCache as a &mut value, so nothing else could read it. The cache now lives in the Store, which both actors already share, and locks internally: the entry list is locked only to find or insert an entry, never while a shuffle runs, and each entry is a OnceLock so concurrent misses on one shuffling run one derivation and the rest wait for it. Every method takes &self, so the STF and fork choice take &CommitteeCache where they took &mut. storage cannot name state_transition's types, so the pieces split by what they need: EpochCommittees, a plain data type, moves to ethlambda-types; the container, keyed by an opaque ShufflingKey, moves to ethlambda-storage; deriving a shuffling and its key stays in state_transition, behind CommitteeCacheExt so call sites keep calling .committees(state, epoch). No behavior changes. * fix(p2p): refuse a 64-bit attestation subnet prefix The guard accepted a prefix of exactly u64::BITS, and the shift that follows it overflows there: a panic in debug, a wrap to 1 in release. No shipped config reaches it, but a custom --network config can. * feat(beacon): gossip rules for aggregates and subnet attestations beacon_aggregate_and_proof and beacon_attestation_{subnet_id} get the same treatment lambdaclass#38 gave blocks and columns: gossip::aggregate and gossip::attestation hold every condition of the spec's validators, split into cheap checks (message, clock, seen caches) and stateful checks. Three deliberate departures from the spec's text, each documented at the module: - The state is the voted block's cached post-state, not the head's. It is the attested chain's own state, so the shuffling is the one the attesters were assigned even on a fork whose deciding block differs from the head's, and its block_roots answer both ancestry checks without a LiveChain scan. - Pubkey-only signatures run before any committee is derived. A state that cannot key its shuffling derives one uncached, which is a full shuffle of the active set; a forged message must not reach that step. - A voted block with no cached post-state is IGNORE, not the spec's REJECT, for the reason blocks and columns already give: without a bad-block cache, a block that failed is indistinguishable from one still importing. The seen caches follow the spec's semantics rather than a running union: an aggregate is ignored only when one already-accepted aggregate covers its bits, so aggregates the spec says to propagate are not dropped. Both are LRU-bounded, never pruned by finality. The v1.7.0-beta.1 vectors for both topics now run on both presets. The two reject_block_failed_validation cases are skipped for the reason above. The fixture stamp now names its handler list, so a tree extracted before a handler was added re-extracts. * feat(p2p): validate aggregates and subnet attestations before relaying them Since lambdaclass#38 a beacon message is relayed only once p2p reports a verdict. Aggregates were validated in the chain actor instead, after a one-slot deferral, so they were never relayed, and subnet attestations got no verdict at all. Both now run through the verdict path with the rules from the previous commit. Only an aggregate gossip accepted reaches the actor, carrying the attesting indices whose signature p2p verified. The actor no longer derives committees or checks signatures: apply_verified_aggregate runs fork choice's own conditions and records the votes. This closes both blocking review findings on lambdaclass#19. The deferral queue can no longer be filled with unverified aggregates, and no aggregate rebuilds committees on the actor. The actor's applied-bits gate is pruned by the store clock rather than by finality, and a drain scans the block index once, only when something is ready. Aggregates and subnet attestations get a permit pool of their own, so a per-slot burst of them can never make a block or column answer Overloaded. An aggregate is forwarded only on Accept, never on Overloaded, since the actor no longer judges it. Subnet attestations are relayed but never forwarded, as before. The seen-attestation capacity is sized from the per-subnet bound, times this node's backbone subnets.
tcoratger
pushed a commit
to tcoratger/ethlambda
that referenced
this pull request
Oct 30, 2026
Brings in lambdaclass#48, lambdaclass#19, lambdaclass#47 and lambdaclass#49. Both conflicts were tests added on both sides of gossip/mod.rs and gossip/test_support.rs: kept all of them and merged the test_support module doc to name every module it serves.
tcoratger
pushed a commit
to tcoratger/ethlambda
that referenced
this pull request
Oct 30, 2026
…justifications Picks up beacon-chain-integration (lambdaclass#48, lambdaclass#19, lambdaclass#47, lambdaclass#49) through the base branch. Clean merge.
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.
No description provided.