From 6fd288eabd44e4a4df230a4e4f727b0d87d1558c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Tue, 6 Oct 2026 00:19:38 -0300 Subject: [PATCH] fix(rpc): version the attester slashings pool by the wall clock GET /eth/v2/beacon/pool/attester_slashings named the fork of the head slot in `version` and Eth-Consensus-Version. The Beacon API means the active consensus version, so at a fork boundary whose block is late or missing the head lags the clock and a validator client decoded the list as the previous fork. The version now comes from the wall-clock epoch. The pooled slashings are electra-shaped, and their JSON is identical in every fork (only the SSZ bound of the attesting indices differs), so no conversion is needed. The other pool GETs carry no version, and the attestation pool reads it from the request slot already. --- crates/net/rpc/src/beacon/operations.rs | 38 ++++++++++++++++++++++--- docs/rpc.md | 2 +- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/crates/net/rpc/src/beacon/operations.rs b/crates/net/rpc/src/beacon/operations.rs index 103bf003..9c9b3b1f 100644 --- a/crates/net/rpc/src/beacon/operations.rs +++ b/crates/net/rpc/src/beacon/operations.rs @@ -73,13 +73,21 @@ async fn get_proposer_slashings(State(store): State) -> Response { crate::json_response(serde_json::json!({ "data": data })) } +/// The pool's attester slashings, versioned by the fork of the wall clock's +/// current epoch, as the Beacon API's "active consensus version" means. The +/// head's fork lags the clock at a fork boundary whose block is late or +/// missing, and a validator client would then decode the list as the wrong +/// fork's container. +/// +/// The pool holds electra-shaped slashings. Their JSON is the same for every +/// fork (the containers differ only in the SSZ list bound of the attesting +/// indices), so no conversion is needed for the version to be honest. async fn get_attester_slashings(State(store): State) -> Response { - let Some((head_slot, _root)) = store.beacon_head() else { - return ApiError::Internal("no head block").into_response(); - }; let fork = store .config() - .fork_at_epoch(compute_epoch_at_slot(head_slot)); + .fork_at_epoch(compute_epoch_at_slot(crate::beacon::node::wall_slot( + &store, + ))); let data = store.operation_pool().attester_slashings(); let response = crate::json_response(serde_json::json!({ "version": fork.as_str(), @@ -534,6 +542,28 @@ mod tests { assert_eq!(json["data"], serde_json::to_value([&slashing]).unwrap()); } + /// The head is before the fork boundary and the wall clock after it: the + /// version is the clock's fork, not the head's. + #[tokio::test] + async fn the_attester_slashings_version_follows_the_wall_clock_not_the_head() { + let mut fixture = fixture(); + let state = with_signing_validators_at(ForkName::Fulu, 64); + let config = fixture.store.config(); + let head_fork = config.fork_at_epoch(compute_epoch_at_slot(state.slot())); + let wall_epoch = compute_epoch_at_slot(crate::beacon::node::wall_slot(&fixture.store)); + let wall_fork = config.fork_at_epoch(wall_epoch); + assert_ne!( + head_fork, wall_fork, + "the head must sit before a fork boundary the clock is past" + ); + fixture.store = beacon_store_at(state).0; + + let (status, headers, json) = get(&fixture, "/eth/v2/beacon/pool/attester_slashings").await; + assert_eq!(status, StatusCode::OK); + assert_eq!(headers["eth-consensus-version"], wall_fork.as_str()); + assert_eq!(json["version"], wall_fork.as_str()); + } + #[tokio::test] async fn a_mixed_bls_change_batch_pools_the_valid_one_and_reports_the_other() { let fixture = fixture(); diff --git a/docs/rpc.md b/docs/rpc.md index b9d26580..6568cb76 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -242,7 +242,7 @@ surface rather than sitting beside it; a `/lean/v0` path on a beacon node is a | `GET` | `/eth/v1/validator/attestation_data` | JSON | What to attest to at `slot` | | `POST` | `/eth/v2/beacon/pool/attestations` | *(status only)* | Validate and gossip `SingleAttestation`s | | `GET`, `POST` | `/eth/v1/beacon/pool/proposer_slashings` | JSON | The operation pool's `ProposerSlashing`s; validate, pool and gossip one | -| `GET`, `POST` | `/eth/v2/beacon/pool/attester_slashings` | JSON | The pool's `AttesterSlashing`s (GET carries `Eth-Consensus-Version`); validate, pool and gossip one | +| `GET`, `POST` | `/eth/v2/beacon/pool/attester_slashings` | JSON | The pool's `AttesterSlashing`s (GET carries `Eth-Consensus-Version`, the fork of the wall clock's current epoch); validate, pool and gossip one | | `GET`, `POST` | `/eth/v1/beacon/pool/voluntary_exits` | JSON | The pool's `SignedVoluntaryExit`s; validate, pool and gossip one | | `GET`, `POST` | `/eth/v1/beacon/pool/bls_to_execution_changes` | JSON | The pool's `SignedBLSToExecutionChange`s; POST takes an array | | `POST` | `/eth/v1/validator/beacon_committee_subscriptions` | *(status only)* | Aggregators' entries join their committee's subnet |