fix(rpc): refuse attestation data for an unvalidated head - #636
MegaRedHand wants to merge 6 commits into
Conversation
A validator client signs whatever attestation_data returns and never sees the execution status behind it. optimistic-sync.md forbids an optimistic validator to attest, and the Beacon API puts the refusal on the node: "A 503 error must be returned if the block identified by the response beacon_block_root is optimistic". We answered 200 instead, so a node tracking a head its execution client had not validated still handed out votes for it. attestation_data and aggregate_attestation now answer 503 while the block they name is optimistic. A 503 is also what makes a validator client, ours included, fail over to its next beacon node. The node endpoints that report the same status now follow the spec's wording as well: /node/syncing's is_optimistic is about the head rather than any optimistic root in the store, which failed our validator client's duty refresh over a branch fork choice did not pick, and /node/health answers 206 on an optimistic head, not only while syncing.
🤖 Kimi Code ReviewI'll review this PR which implements optimistic sync safety guards for the Beacon API. Let me analyze each file.
|
| Severity | Item | Location |
|---|---|---|
| Minor | Redundant store.clone() in test closure |
node.rs:183 |
| Question | Store mutability pattern — &mut self methods on what appears to be a handle |
validator_client_tests.rs:261 |
The PR is correct and well-tested. The semantic change from "any optimistic root" to "head is optimistic" is the right fix per the Beacon API specification. The 503 refusals on attestation_data and aggregate_attestation properly prevent validators from signing unvalidated blocks, with good failover behavior for validator clients.
One architectural note: The require_validated helper is clean and reusable. Consider if get_block and other validator endpoints that return block roots should also use it (future work, not this PR).
Automated review by Kimi (Moonshot AI) · kimi-k2.6 · custom prompt
🤖 Codex Code Review
Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🤖 Claude Code ReviewReview of PR 636: refuse validator answers for an optimistic headThe change is correct and well scoped. I found no blocking issues. What it does
Observations (non-blocking)
Tests
Docs
Overall: LGTM. Item 1 is the only thing I'd consider before merging. Automated review by Claude (Anthropic) · sonnet · custom prompt |
The merge of main into beacon-chain-integration placed `mod banner;` after `mod beacon;`, which `cargo fmt --check` rejects, so Lint fails on every PR against this branch.
A node run without --execution-endpoint imports every block as NotRequired, so no block is ever marked optimistic and the optimistic check on attestation_data always passes. It then answers with a head whose payload nobody validated, which is what optimistic-sync.md forbids a validator to attest to. Refuse with 503 instead, as block production already does for its own reason.
An aggregator signs over the block its pooled votes name, and on a node run without an execution client nothing ever validated that block's payload. Every block imports as NotRequired there, so the optimistic check on aggregate_attestation always passes. Refuse with 503, the same way attestation_data now does, through one shared helper.
The Beacon API marks committee_index optional and deprecated, and tells gloas clients to omit it. We required it, so a client that follows the deprecation got a 400 from the query extractor and missed its attestation. The value was already ignored, since electra moved the committee out of AttestationData.
POST /eth/v1/beacon/states/{state_id}/validators allows either filter to
be null to mean no filtering. serde's default covers only an absent
field, so an explicit null failed the whole body with a 400, and a
validator client sending one could not resolve its indices.
Why
We had a report of the node importing blocks optimistically while still answering
attestation_data. A validator client signs whatever that endpoint returns and never sees the execution status behind it, so the refusal has to come from the node:specs/bellatrix/optimistic-sync.md, "Validator assignments": an optimistic validator MUST NOT attest, propose, or take part in sync committees.attestation_dataandaggregate_attestation: "A 503 error must be returned if the block identified by the responsebeacon_block_rootis optimistic".Lighthouse, Prysm and Teku all refuse
attestation_datain this case. Prysm and Teku answer 503; Lighthouse answers 500.Changes
GET /eth/v1/validator/attestation_dataGET /eth/v2/validator/aggregate_attestationbeacon_block_rootis optimistic, whether or not it is the headNotRequired, so no block is ever optimistic and the optimistic check alone would always passGET /eth/v1/validator/attestation_data, nocommittee_indexPOST /eth/v1/beacon/states/{state_id}/validators,idsorstatusesset tonullGET /eth/v1/node/syncing,is_optimistictrueif any root in the store is optimistictrueif the head is (spec: "optimistically tracking head")GET /eth/v1/node/healthThe optimistic 503 is temporary. A
VALIDanswer toforkchoiceUpdated, which the chain actor sends at least once a slot, clears the root.The
is_optimisticchange also matters forethlambda validator. Its per-epoch duty refresh fails while the flag is set: it keeps the previous schedule and reports the node unavailable. Before this change, an optimistic block on a side branch was enough to trigger that.Still open
Found in an audit of every route this node serves, against beacon-APIs and consensus-specs. None is fixed here: each needs a decision or is a change of its own.
ethlambda validator's failover assumes the node answers 503 while syncingSyncStatusControllerreports synced during a beacon catch-up, so it cannot drive this. The fix compares the head slot with the wall clock, and the allowed lag needs choosingexecution_optimisticisfalseon every block, header, state and duty response, andel_offlineisfalsePOST /eth/v2/beacon/blocksrefuses JSON bodies with a plain-text 415SignedBlockContents. Not yet checked which clients publish JSONconsensus_block_valueas"0"skip_randao_verificationPOST /eth/v2/beacon/blocksignoresbroadcast_validation, and answers 400 to blocks carrying blobsconsensusandconsensus_and_equivocationcallers get weaker checks than they asked for. A blob block gets a 400 rather than a retryable 503pool/attestations,aggregate_and_proofs) accept JSON onlyattestation_dataanswers 400 for a slot before the head block's slotGET /eth/v1/validator/duties/proposer/{epoch}orGET /eth/v1/node/versioncanonical: falsefinalizedis slot-basedfinalized: trueAccept. Axum's default rejections return 415, 422 or a plain-text 400 rather than a{code, message}body./node/healthignoressyncing_statusNot a deviation: block production has no explicit optimistic check, and needs none. The Engine API returns no
payloadIdunless the head isVALID, so an optimistic head already gets a 503.Also in this PR
style: sort the binary's module declarations. The main merge intobeacon-chain-integrationleftmod banner;aftermod beacon;inbin/ethlambda/src/main.rs, whichcargo fmt --checkrejects. Without that fix, Lint fails on every PR against this branch.Test plan
cargo test -p ethlambda-rpc -p ethlambda-storage --profile release-fast --libcargo clippy --locked -p ethlambda-rpc -p ethlambda-storage --all-targets -- -D warningscargo fmt --all -- --checkattestation_dataanswers 503 on an optimistic head, then 200 once it is validated.aggregate_attestationdoes the same for the voted block.attestation_dataanswers withoutcommittee_index, and rejects a malformed one.validatorsreads anullfilter as no filter./node/syncingand/node/healthignore an optimistic side branch and react to an optimistic head.ethlambda validator's HTTP client reads the new 503 as the retryableBeaconNodeSyncing.