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 |