diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index a78ae266e5..da9037a180 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1638,6 +1638,53 @@ impl BeaconChain { Ok((duties, dependent_root, execution_status)) } + /// Returns the inclusion list duties for the given validator indices. + /// + /// The returned `Vec` will have the same length as `validator_indices`, any + /// non-existing/inactive validators will have `None` values. + pub fn validator_inclusion_list_duties( + &self, + validator_indices: &[u64], + epoch: Epoch, + head_block_root: Hash256, + ) -> Result<(Vec>, Hash256), Error> { + // NOTE: we likely need some additional logic to handle cases where the head block root is + // from some prior epoch. + let head_block = self + .canonical_head + .fork_choice_read_lock() + .get_block(&head_block_root) + .ok_or(Error::MissingBeaconBlock(head_block_root))?; + + // NOTE: here we reuse the attestation shuffling IDs. + let shuffling_id = BlockShufflingIds { + current: head_block.current_epoch_shuffling_id.clone(), + next: head_block.next_epoch_shuffling_id.clone(), + previous: None, + block_root: head_block.root, + } + .id_for_epoch(epoch) + .ok_or_else(|| Error::InvalidShufflingId { + shuffling_epoch: epoch, + head_block_epoch: head_block.slot.epoch(T::EthSpec::slots_per_epoch()), + })?; + let dependent_root = shuffling_id.shuffling_decision_block; + + let head_beacon_state = self.get_state(&head_block.root, Some(head_block.slot))?; + let Some(head_beacon_state) = head_beacon_state else { + return Err(Error::MissingBeaconState(head_block.root)); + }; + let duties = validator_indices + .iter() + .map(|&validator_index| { + head_beacon_state + .get_inclusion_list_duties(validator_index as usize, epoch, &self.spec) + .map_err(Error::InclusionListDutiesError) + }) + .collect::, _>>()?; + Ok((duties, dependent_root)) + } + pub fn get_aggregated_attestation( &self, attestation: AttestationRef, diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 2a8fd4cd01..c3e31c23da 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -132,6 +132,7 @@ pub enum BeaconChainError { shuffling_epoch: Epoch, }, SyncDutiesError(BeaconStateError), + InclusionListDutiesError(BeaconStateError), InconsistentForwardsIter { request_slot: Slot, slot: Slot, diff --git a/beacon_node/http_api/src/inclusion_list_duties.rs b/beacon_node/http_api/src/inclusion_list_duties.rs new file mode 100644 index 0000000000..01cb4f7309 --- /dev/null +++ b/beacon_node/http_api/src/inclusion_list_duties.rs @@ -0,0 +1,97 @@ +use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes}; +use eth2::types::{self as api_types}; +use slot_clock::SlotClock; +use types::{Epoch, EthSpec, Hash256, InclusionListDuty}; + +/// The struct that is returned to the requesting HTTP client. +type ApiDuties = api_types::DutiesResponse>; + +/// Handles a request from the HTTP API for inclusion list duties. +pub fn inclusion_list_duties( + request_epoch: Epoch, + request_indices: &[u64], + chain: &BeaconChain, +) -> Result { + let current_epoch = chain + .epoch() + .map_err(warp_utils::reject::beacon_chain_error)?; + + // Determine what the current epoch would be if we fast-forward our system clock by + // `MAXIMUM_GOSSIP_CLOCK_DISPARITY`. + // + // Most of the time, `tolerant_current_epoch` will be equal to `current_epoch`. However, during + // the first `MAXIMUM_GOSSIP_CLOCK_DISPARITY` duration of the epoch `tolerant_current_epoch` + // will equal `current_epoch + 1` + let tolerant_current_epoch = chain + .slot_clock + .now_with_future_tolerance(chain.spec.maximum_gossip_clock_disparity()) + .ok_or_else(|| warp_utils::reject::custom_server_error("unable to read slot clock".into()))? + .epoch(T::EthSpec::slots_per_epoch()); + + if request_epoch == current_epoch + || request_epoch == current_epoch + 1 + || request_epoch == tolerant_current_epoch + 1 + { + let head_block_root = chain.canonical_head.cached_head().head_block_root(); + let (duties, dependent_root) = chain + .validator_inclusion_list_duties(request_indices, request_epoch, head_block_root) + .map_err(warp_utils::reject::beacon_chain_error)?; + convert_to_api_response(duties, request_indices, dependent_root, chain) + } else if request_epoch > current_epoch + 1 { + Err(warp_utils::reject::custom_bad_request(format!( + "request epoch {} is more than one epoch past the current epoch {}", + request_epoch, current_epoch + ))) + } else { + // request_epoch < current_epoch + // + // TODO: support historical inclusion list duties requests + Err(warp_utils::reject::custom_bad_request(format!( + "request epoch {} is earlier than the current epoch {}", + request_epoch, current_epoch + ))) + } +} + +/// Convert the internal representation of attester duties into the format returned to the HTTP +/// client. +fn convert_to_api_response( + duties: Vec>, + indices: &[u64], + dependent_root: Hash256, + chain: &BeaconChain, +) -> Result { + // Protect against an inconsistent slot clock. + if duties.len() != indices.len() { + return Err(warp_utils::reject::custom_server_error(format!( + "duties length {} does not match indices length {}", + duties.len(), + indices.len() + ))); + } + + let usize_indices = indices.iter().map(|i| *i as usize).collect::>(); + let index_to_pubkey_map = chain + .validator_pubkey_bytes_many(&usize_indices) + .map_err(warp_utils::reject::beacon_chain_error)?; + + let data = duties + .into_iter() + .zip(indices) + .filter_map(|(duty_opt, &validator_index)| { + let duty = duty_opt?; + Some(api_types::InclusionListDutyData { + validator_index, + pubkey: *index_to_pubkey_map.get(&(validator_index as usize))?, + slot: duty.slot, + }) + }) + .collect::>(); + + // TODO: account for optimistic execution + Ok(api_types::DutiesResponse { + dependent_root, + execution_optimistic: None, + data, + }) +} diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index fe05f55a01..cc3b01db87 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -13,6 +13,7 @@ mod block_rewards; mod build_block_contents; mod builder_states; mod database; +mod inclusion_list_duties; mod light_client; mod metrics; mod produce_block; @@ -257,6 +258,7 @@ pub fn prometheus_metrics() -> warp::filters::log::Log( }, ); + // POST validator/duties/inclusion_list/{epoch} + let post_validator_duties_inclusion_list = eth_v1 + .and(warp::path("validator")) + .and(warp::path("duties")) + .and(warp::path("inclusion_list")) + .and(warp::path::param::().or_else(|_| async { + Err(warp_utils::reject::custom_bad_request( + "Invalid epoch".to_string(), + )) + })) + .and(warp::path::end()) + .and(not_while_syncing_filter.clone()) + .and(warp_utils::json::json()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .then( + |epoch: Epoch, + not_synced_filter: Result<(), Rejection>, + indices: api_types::ValidatorIndexData, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P0, move || { + not_synced_filter?; + inclusion_list_duties::inclusion_list_duties(epoch, &indices.0, &chain) + }) + }, + ); + // GET validator/sync_committee_contribution let get_validator_sync_committee_contribution = eth_v1 .and(warp::path("validator")) @@ -4743,6 +4773,7 @@ pub fn serve( .uor(post_beacon_rewards_sync_committee) .uor(post_validator_duties_attester) .uor(post_validator_duties_sync) + .uor(post_validator_duties_inclusion_list) .uor(post_validator_aggregate_and_proofs) .uor(post_validator_contribution_and_proofs) .uor(post_validator_beacon_committee_subscriptions) diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index c187399ebd..f1b2216336 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -740,6 +740,14 @@ pub struct ProposerData { pub slot: Slot, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct InclusionListDutyData { + pub pubkey: PublicKeyBytes, + #[serde(with = "serde_utils::quoted_u64")] + pub validator_index: u64, + pub slot: Slot, +} + #[derive(Clone, Deserialize)] pub struct ValidatorBlocksQuery { pub randao_reveal: SignatureBytes, diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index fe5509b0a6..b035a39b06 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -812,7 +812,7 @@ impl BeaconState { /// Returns the inclusion list committee for the given `slot` in the current or next epoch. /// /// Spec v0.12.1 - pub fn get_inclusion_list_commitee( + pub fn get_inclusion_list_committee( &self, slot: Slot, spec: &ChainSpec, @@ -1721,6 +1721,21 @@ impl BeaconState { Ok(cache.get_attestation_duties(validator_index)) } + pub fn get_inclusion_list_duties( + &self, + validator_index: usize, + epoch: Epoch, + spec: &ChainSpec, + ) -> Result, Error> { + for slot in epoch.slot_iter(E::slots_per_epoch()) { + let committee = self.get_inclusion_list_committee(slot, spec)?; + if committee.contains(&validator_index) { + return Ok(Some(InclusionListDuty { slot })); + } + } + Ok(None) + } + /// Compute the total active balance cache from scratch. /// /// This method should rarely be invoked because single-pass epoch processing keeps the total diff --git a/consensus/types/src/inclusion_list_duty.rs b/consensus/types/src/inclusion_list_duty.rs new file mode 100644 index 0000000000..bf578b4623 --- /dev/null +++ b/consensus/types/src/inclusion_list_duty.rs @@ -0,0 +1,8 @@ +use crate::*; +use serde::{Deserialize, Serialize}; + +#[derive(arbitrary::Arbitrary, Debug, PartialEq, Clone, Copy, Default, Serialize, Deserialize)] +pub struct InclusionListDuty { + /// The slot during which the validator must produce an inclusion list. + pub slot: Slot, +} diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index 6a3aebe724..8a1eab3d75 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -49,6 +49,7 @@ pub mod graffiti; pub mod historical_batch; pub mod historical_summary; pub mod inclusion_list; +pub mod inclusion_list_duty; pub mod indexed_attestation; pub mod light_client_bootstrap; pub mod light_client_finality_update; @@ -180,6 +181,7 @@ pub use crate::fork_versioned_response::{ForkVersionDeserialize, ForkVersionedRe pub use crate::graffiti::{Graffiti, GRAFFITI_BYTES_LEN}; pub use crate::historical_batch::HistoricalBatch; pub use crate::inclusion_list::{InclusionList, SignedInclusionList}; +pub use crate::inclusion_list_duty::InclusionListDuty; pub use crate::indexed_attestation::{ IndexedAttestation, IndexedAttestationBase, IndexedAttestationElectra, IndexedAttestationRef, };