feat(rpc): serve validator/liveness - #643
pablodeymo wants to merge 2 commits into
Conversation
…the endpoint a validator client's doppelganger protection calls before signing. A validator is live in an epoch if the head state credits it for that epoch (a non-zero participation byte, covering what blocks have already included) or if this node observed it act in that epoch, which covers what no block has included yet. The observations live in a new ObservedLiveness on the Store, one bitset per epoch for the newest three, because three places write them and all three already hold a Store clone: P2P when it accepts a gossip aggregate (its aggregator and every attester its signature verified) or subnet attestation, the chain actor when it imports a beacon block (its proposer, so range-synced and self-published blocks count), and the RPC for attestations and aggregates submitted through pool/attestations and aggregate_and_proofs, since gossip never delivers a node its own messages. The endpoint answers the store clock's previous, current and next epoch and is a 400 for any other epoch or an unknown index, and a 503 while syncing.
🤖 Codex Code ReviewLooks good overall. The change is narrowly scoped, thread-safe, and doesn’t touch fork choice, state transition, justification/finalization, XMSS, or SSZ logic.
Minor note:
I don’t see correctness, security, or performance regressions in this PR. Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🤖 Claude Code ReviewReview of PR 643: Beacon API
|
🤖 Kimi Code ReviewI'll review this PR which implements the
|
| Severity | Count | Description |
|---|---|---|
| Critical | 0 | None found |
| High | 0 | None found |
| Medium | 0 | None found |
| Low/Info | 2 | Minor clarity improvements possible |
The PR is well-implemented and correct. The liveness tracking properly covers all paths (gossip aggregates, gossip attestations, block proposers, API submissions) with appropriate epoch windowing and memory bounds. The endpoint correctly combines on-chain participation with off-chain observation for doppelganger protection.
Optional suggestions:
-
File:
crates/storage/src/liveness.rs, line 78: Add explicitvalidator < MAX_TRACKED_INDEXcheck inis_livefor symmetry withrecord_all, or document the implicitfalsereturn. -
File:
crates/net/rpc/src/beacon/validator.rs, line 178: Considerepoch.saturating_add(1) < currentfor absolute safety, though not practically needed.
Automated review by Kimi (Moonshot AI) · kimi-k2.6 · custom prompt
🗒️ Description / Motivation
POST /eth/v1/validator/liveness/{epoch}is what a validator client's doppelganger protection calls before it signs anything: "did anyone see these validators act in epoch E?" If the node answers 404:This is the last of the four missing endpoints; the other three are in #642, which this PR is stacked on.
The Beacon API leaves the answer to the node's own view ("network, chain or API"), and clients differ:
is_liveWhat Changed
crates/storage/src/liveness.rs(new):ObservedLiveness, one bitset per epoch for the newest three.1 << 24, so one epoch is at most 2 MiB. Every writer passes a validated index anyway; mainnet is ~2.4M validators, about 300 KB.crates/storage/src/store.rs: anobserved_livenessfield and accessor, shared by everyStoreclone likecommittee_cache, because the set has three writers and a reader that all already hold aStore.crates/net/p2p/src/beacon/verdict.rs: onAccept, an aggregate marks its aggregator and every attester its signature verified; a subnet attestation marks its attester.crates/blockchain/src/lib.rs: a successfully imported beacon block marks its proposer. This is done at import, not at gossip acceptance, so range-synced blocks and blocks published through this node's API count too.crates/net/rpc/src/beacon/pool.rs: submissions throughpool/attestationsandaggregate_and_proofs, since gossip never delivers a node its own messages. The aggregate path now keeps the attesting indices thatstateful_checksalready returned.crates/net/rpc/src/beacon/validator.rs:post_liveness/liveness.docs/rpc.md: the route and the liveness semantics.Correctness / Behavior Guarantees
Accepted gossip and validated API submissions are recorded; anIgnore,RejectorOverloadedobject marks nobody.current_epoch_participation) and the one before (previous_epoch_participation). Before altair there are no flags, so only observations count.false, because a doppelganger check at an epoch boundary can land on it; Lighthouse accepts it too.false). An index outside the registry is a client bug, better reported than hidden.Mutexonce per accepted gossip object, to set bits. That includes every accepted attestation on the backbone subnets, which don't touch the attestation pool: roughly a couple of thousand short locks a slot on mainnet, against a reader that only runs when a validator client asks.Tests Added / Run
ObservedLiveness:a_recorded_validator_is_live_in_that_epoch_onlyrecord_all_sets_every_indexepochs_older_than_the_window_are_prunedan_epoch_below_the_window_is_not_recordedan_index_past_the_guard_is_ignoredobserved_liveness_is_shared_across_store_clonesa_participation_flag_makes_a_validator_live(current and previous epoch flags)an_observed_validator_is_live_without_a_flagthe_next_epoch_is_answered_and_nobody_is_live_in_itepochs_outside_the_window_are_a_400an_unknown_validator_is_a_400a_syncing_node_answers_503an_accepted_aggregate_marks_its_aggregator_and_attesters_livean_accepted_subnet_attestation_marks_its_attester_livean_object_that_was_not_accepted_marks_nobody_livea_published_attestation_marks_its_attester_livea_refused_attestation_marks_nobody_livea_published_aggregate_marks_its_aggregator_and_attesters_live: the aggregate is built on one node and submitted to a fresh one, so only the aggregate itself can have marked its attesters.--enable-doppelganger-protection. Its validators should come online after the doppelganger wait instead of staying muted.Related Issues / PRs
fork,deposit_contract,duties/sync); retarget tobeacon-chain-integrationonce feat(rpc): serve states/{id}/fork, config/deposit_contract and validator/duties/sync #642 merges.✅ Verification Checklist
make fmt— cleanmake lint(clippy with-D warnings) — cleanmake test(test-consensusplustest-node, atrelease-fast) — all passing (1966 passed, 0 failed, 26 ignored)