Skip to content

docs: update roadmap progress - #19

Merged
MegaRedHand merged 1 commit into
mainfrom
update-roadmap
Jan 13, 2026
Merged

MegaRedHand merged 1 commit into
mainfrom
update-roadmap

Conversation

@MegaRedHand

Copy link
Copy Markdown
Collaborator

No description provided.

@MegaRedHand
MegaRedHand merged commit c6ea2cd into main Jan 13, 2026
@MegaRedHand
MegaRedHand deleted the update-roadmap branch January 13, 2026 14:38
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant