Repository navigation
feat(beacon): sync committee support in the node and the validator client - #668
MegaRedHand wants to merge 13 commits into
Conversation
…eposit_contract and POST /eth/v1/validator/duties/sync/{epoch} from ethlambda beacon, three of the four Beacon API endpoints validator clients call that were missing. fork returns the state's own Fork through the same state_id resolution as the other state endpoints, so a state root is the same 404. deposit_contract returns the Config's deposit chain id and contract address. duties/sync reads the head state's current_sync_committee for an epoch in the head's own sync committee period and next_sync_committee for the next one, matches each requested validator by pubkey and returns every seat it holds (the committee is drawn with replacement), leaves out validators with no seat, and answers 400 for an unknown index or any other period and 503 while syncing. An earlier period is refused rather than answered from a historical state, recorded in docs/spec_deviations.md. compute_sync_committee_period is added to the altair helpers as validator.md defines it.
The Beacon API's sync committee endpoints take SyncCommitteeMessage and SignedContributionAndProof bodies and answer with contributions, so the node and the validator client both need these containers in both directions. SYNC_SUBCOMMITTEE_SIZE names the subnet width the gossip seen caches, the pool and the validator client all index by.
Sync committee gossip and block packing both need the committee a message belongs to, its signing roots and somewhere to hold validated messages. The helpers key the committee by the message's slot and the domain by the fork schedule, so a head that lags across a period or fork boundary does not reject honest messages. The pool keeps messages per position and the best contribution per subcommittee, and builds the block's aggregate.
…committees The validator client had no way to ask for, sign or publish sync committee work. This adds the pieces the duty service builds on: the six BeaconNodeApi calls (HTTP, failover and mock), the three sync signing roots, the sync message and contribution deadlines (read from the node's spec, with gloas variants), and period-keyed sync duties. An optimistic head is reported as BeaconNodeSyncing so failover tries the next node instead of signing over an unvalidated head, and subscriptions go to every node because a node that never joined a subnet cannot pool the messages a contribution is folded from.
A client holding sync committee seats now signs the head root at the sync message deadline, aggregates for the subnets its selection proof picks at the contribution deadline, and re-sends its subnet subscriptions every epoch (the node forgets them on restart), adding the next period's once its boundary is four epochs away. The sync work runs beside the attester and PTC work under tokio::join!, each half bounded by the slot, because gloas moves the sync deadline before the attestation and an ordered loop would hold it back. A subnet whose contribution cannot be fetched or does not match the request is skipped without stopping the others.
The sync_committee_{subnet_id} and contribution topics were relayed to
nobody: contributions were decoded and then ignored. The rules follow the
altair p2p-interface, split into cheap and stateful halves like the other
topics, and the committee is read by the message's slot so a lagging head
does not reject honest messages. The spec's gossip vectors for both topics
move from the ignored list into the runner.
… node Block production always carried the empty sync aggregate, forfeiting the committee's rewards. Both proposal paths now take a sync aggregate as an input, and verified_sync_aggregate returns it only when it passes exactly the check process_sync_aggregate will apply, so a stale or foreign vote costs rewards and never the block. The RpcToP2P protocol gains the three sync committee calls, the p2p actor handles them through a stub module the p2p stage fills in, and one shared pool is created at startup for p2p and the Beacon API. The spec deviations this work introduces are recorded.
The node half of sync committees (helpers, gossip rules, pool, sync aggregates in produced blocks, RpcToP2P stubs) joins the validator client half already on the branch.
…regate A validator client on a sync committee needs somewhere to send its messages and contributions, and a way to ask for the subnets to be joined. Add the four beacon-APIs endpoints (pool/sync_committees, sync_committee_contribution, contribution_and_proofs, sync_committee_subscriptions). Submissions run the same checks as gossip before they are published, and are pooled here because gossip never echoes a node its own messages. Block production now takes the sync aggregate pooled for (slot - 1, parent_root) instead of always the empty one. It is verified against the block's own pre-state, so a stale or wrong-root candidate costs the rewards and never the block, and the "retry without operations" fallbacks drop it too.
Sync committee messages and contributions had no consumer, so the node relayed none of them and a validator client could not take part in a sync committee through it. The contribution topic is always validated; the four message subnets are joined only on a validator client's request, since validating all of them permanently would cost every follower up to a committee's worth of BLS verifications per slot. Joined subnets live on the wire so MetaData `syncnets` advertises exactly them, follow every held digest across fork boundaries, and are left by the 12 s sweep once their until_epoch passes. Stateful checks draw from their own permit pool, since their burst coincides with the attestations'. Accepted gossip is pooled for block production and never reaches the chain actor.
Sync committee gossip (on-demand subnets, the contribution topic, seen caches, syncnets in MetaData) joins the endpoints and pooled block aggregates, so the node both serves and relays sync committee work.
… router The unit tests check each sync committee endpoint and the client's service against a mock; neither shows the two ends agree on the wire. These tests run the real HttpBeaconNode and SyncCommitteeService against the real router over a socket: duties, subscriptions, messages (gossiped on exactly their seats' subnets), contributions that verify, and then the next slot's block, whose sync aggregate must be full and pass process_sync_aggregate. One test per fork family (fulu and gloas). The fulu harness pins the clock and roots the store's head at the state's header root, since a committee signs the head and the block builder checks the aggregate against the root the state's block_roots hold. Also documents the sync committee wire and validator duties in CLAUDE.md.
🤖 Codex Code ReviewI didn’t spot a consensus-critical bug in the sync-committee flow overall; the design is careful about re-validating pooled aggregates before block packing. I do see one correctness/perf issue worth fixing.
Everything else I checked looks sensible:
Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🤖 Claude Code ReviewReview: sync committee support (PR 668)I read the core of the PR: the gossip rules, the pool, the p2p subnet management, the Beacon API handlers and The design is sound and the rules follow the altair p2p spec closely. The departures are deliberate and documented. Findings are minor. Correctness
Performance and robustness
Idioms and nits
Verdict: looks good. I'd address the lock-held-during-BLS point before merge, and the rest is optional. Automated review by Claude (Anthropic) · sonnet · custom prompt |
🤖 Kimi Code ReviewI'll review this PR implementing sync committee support in ethlambda. This is a substantial feature addition spanning gossip validation, pooling, block production, validator client duties, and Beacon API endpoints. High-Level AssessmentThe PR implements Altair sync committee functionality (messages, contributions, aggregation) with proper integration into gossip validation, block production, and validator client duties. The architecture follows existing patterns in the codebase (attestation pool, gossip split into cheap/stateful checks, permit pools). Critical Issues1. Race condition in
|
| Category | Count | Notes |
|---|---|---|
| Critical | 0 | No critical security bugs found |
| Security | 0 | All signature verification paths are correct |
| Correctness | 0 | Spec deviations are documented and intentional |
| Performance | 1 | Could use RwLock for pool, but follows existing pattern |
| Style/Rust | 2 | Minor test code issues |
| Documentation | 0 | Clear and comprehensive |
Recommendations (Non-Blocking)
-
Add
debug_assert!forpositionbounds inSyncCommitteePool::contribution(line 185) to catch any invariant violations in testing. -
Consider
RwLockinstead ofMutexforSharedSyncCommitteePoolif read contention becomes measurable. -
The
unwrap_or(true)incontributionat line 185 could beexpectsince it should be unreachable — or document whytrue(skip) is the safe fallback. -
Test
verified_sync_aggregatewith mismatched bits/signature — add a test where bits claim more participants than the signature covers, to ensure it returns empty aggregate.
Overall Assessment
Approve with minor suggestions. The PR is well-structured, thoroughly tested, correctly implements the spec with documented deviations, and follows the codebase's architectural patterns. The sync committee integration is complete from gossip through block production to validator client duties.
Automated review by Kimi (Moonshot AI) · kimi-k2.6 · custom prompt
…633-636-638-gloas-live Both the sync committee (#668) and the builder market sides are carried through every shared surface: P2P::spawn, P2PServer, BeaconApiHandles, RecordingNetwork, the Validated enum and its matches, the gossip reason enums, the spec gossip runner (no bid, preferences or sync handler is ignored) and the docs. Sync and builder gossip each validate on their own permit pool. Non-obvious resolutions: - GloasBidBlockInputs gains sync_aggregate and assemble_gloas_block_on_bid uses it. assemble_on_bid verifies the pooled aggregate for (slot - 1, parent_root) against the block's pre-state and falls back to the empty aggregate on every retry path, as the self-build does. - produce() runs the local build and the execution client version lookup concurrently, each skipped when no engine is configured, and the bid selection reads the pooled sync candidate. - Test fixtures follow tmp: no attestation pool extension, the sync pool and OwnVersion layers in the builder market app, BuiltGloasPayload and Prepared initializers with the builder market fields, and the ActiveBalanceCache argument of gloas process_block. - New test: a block built on a bid carries the pooled sync aggregate. Known failure: gossip::execution_payload_bid::tests:: a_bid_across_an_epoch_uses_and_caches_the_checkpoint_state fails. Its scene has finalized_checkpoint.epoch 1 at slot 32, and tmp's get_finality_delay now errors when the finalized epoch is past the previous epoch, so advancing the scene to epoch 2 returns StateUnavailable. The fixture state is unreachable on a real chain; left unchanged pending a decision.
Motivation
Neither
ethlambda beaconnorethlambda validatortook part in sync committees. Other clients' validator clients on our node got a 404 for every sync duty (seen on the fulu->gloas devnet behind #667), our validator client had no sync duties at all, and every block we produced carried an empty sync aggregate, so every sync committee member lost its reward in our slots.Stacked on #662 and merges
feat/beacon-api-missing-endpoints(#642) forPOST /eth/v1/validator/duties/sync/{epoch}; the #642 commits disappear from this diff once it lands.Beacon node
sync_committee_{subnet_id}(joined on demand fromsync_committee_subscriptions, untiluntil_epoch, carried across fork digests, advertised in MetaDatasyncnets) andsync_committee_contribution_and_proof(already subscribed, now validated instead of ignored). Every altair rule, with seen caches and their own validation permit pool so the 1/3-slot burst does not starve attestations.SharedSyncCommitteePoolfilled by gossip and the API: messages by(slot, root, subcommittee)position, best contribution per key.(N-1, parent_root), the best contribution per subcommittee extended with pooled messages at uncovered positions, verified exactly asprocess_sync_aggregatechecks it (else the empty aggregate). Fulu and gloas; every retry-without-operations path drops it too.POST /eth/v1/beacon/pool/sync_committees,GET /eth/v1/validator/sync_committee_contribution,POST /eth/v1/validator/contribution_and_proofs,POST /eth/v1/validator/sync_committee_subscriptions(JSON, beacon-APIs shapes, IndexedErrorMessage on partial failure).Validator client
Sync duties per period (current and next), subnet subscriptions re-sent every epoch, a message per validator at
SYNC_MESSAGE_DUE_BPS(_GLOASfrom gloas) over the head root (skipped when the head is optimistic), selection proofs and contributions atCONTRIBUTION_DUE_BPS, run concurrently with attestations. Newethlambda_validator_sync_*metrics.Deviations (in
docs/spec_deviations.md)slot + 1's period) against the cached head state, and the domain from the fork schedule, so an honest message is not refused while the head lags a period or fork boundary.syncnets(ethrex's discovery server cannot replace the served record at runtime).(slot - 1, parent_root), contributions extended with direct messages.Validation
gossip_sync_committee_messageandgossip_sync_committee_contribution_and_proof, mainnet: 46 passed (fulu and gloas, includingvalid_at_period_boundary). Minimal not run locally.validator_client_tests.rs, fulu and gloas): the real validator client against the real router covers all 512 seats on all 4 subnets, contributions with every bit, and the next block carries a full sync aggregate thatprocess_sync_aggregateaccepts.--lib: p2p 318, rpc 195, state-transition 501, validator 299; clippy and fmt clean.