diff --git a/Cargo.lock b/Cargo.lock index 9b7a594eb59..6e7ededbd6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7312,6 +7312,7 @@ dependencies = [ "futures", "hex", "hyper", + "itertools 0.10.1", "lazy_static", "libc", "libsecp256k1", diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index b8c3f44358f..5c42a515cc9 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -43,7 +43,7 @@ use crate::validator_pubkey_cache::ValidatorPubkeyCache; use crate::BeaconForkChoiceStore; use crate::BeaconSnapshot; use crate::{metrics, BeaconChainError}; -use eth2::types::{EventKind, SseBlock, SseChainReorg, SseFinalizedCheckpoint, SseHead}; +use eth2::types::{EventKind, SseBlock, SseChainReorg, SseFinalizedCheckpoint, SseHead, SyncDuty}; use fork_choice::ForkChoice; use futures::channel::mpsc::Sender; use itertools::process_results; @@ -1081,6 +1081,29 @@ impl BeaconChain { Ok(pubkey_cache.get_index(pubkey)) } + /// Return the validator indices of all public keys fetched from an iterator. + /// + /// If any public key doesn't belong to a known validator then an error will be returned. + /// We could consider relaxing this by returning `Vec>` in future. + pub fn validator_indices<'a>( + &self, + validator_pubkeys: impl Iterator, + ) -> Result, Error> { + let pubkey_cache = self + .validator_pubkey_cache + .try_read_for(VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT) + .ok_or(Error::ValidatorPubkeyCacheLockTimeout)?; + + validator_pubkeys + .map(|pubkey| { + pubkey_cache + .get_index(pubkey) + .map(|id| id as u64) + .ok_or(Error::ValidatorPubkeyUnknown(*pubkey)) + }) + .collect() + } + /// Returns the validator pubkey (if any) for the given validator index. /// /// ## Notes @@ -1214,6 +1237,16 @@ impl BeaconChain { .get_by_slot_and_root(slot, attestation_data_root) } + /// Return an aggregated `SyncCommitteeContribution` matching the given `root`. + pub fn get_aggregated_sync_committee_contribution( + &self, + sync_contribution_data: &SyncContributionData, + ) -> Option> { + self.naive_sync_aggregation_pool + .read() + .get(sync_contribution_data) + } + /// Produce an unaggregated `Attestation` that is valid for the given `slot` and `index`. /// /// The produced `Attestation` will not be valid until it has been signed by exactly one @@ -1882,6 +1915,19 @@ impl BeaconChain { Ok(()) } + /// Attempt to obtain sync committee duties from the head. + pub fn sync_committee_duties_from_head( + &self, + epoch: Epoch, + validator_indices: &[u64], + ) -> Result>, Error> { + self.with_head(move |head| { + head.beacon_state + .get_sync_committee_duties(epoch, validator_indices, &self.spec) + .map_err(Error::SyncDutiesError) + }) + } + /// Attempt to verify and import a chain of blocks to `self`. /// /// The provided blocks _must_ each reference the previous block via `block.parent_root` (i.e., @@ -2624,6 +2670,22 @@ impl BeaconChain { let proposer_index = state.get_beacon_proposer_index(state.slot(), &self.spec)? as u64; let voluntary_exits = self.op_pool.get_voluntary_exits(&state, &self.spec).into(); + // Closure to fetch a sync aggregate in cases where it is required. + let get_sync_aggregate = || -> Result, BlockProductionError> { + Ok(self + .op_pool + .get_sync_aggregate(&state) + .map_err(BlockProductionError::OpPoolError)? + .unwrap_or_else(|| { + warn!( + self.log, + "Producing block with no sync contributions"; + "slot" => state.slot(), + ); + SyncAggregate::new() + })) + }; + let inner_block = match state { BeaconState::Base(_) => BeaconBlock::Base(BeaconBlockBase { slot, @@ -2641,24 +2703,26 @@ impl BeaconChain { voluntary_exits, }, }), - BeaconState::Altair(_) => BeaconBlock::Altair(BeaconBlockAltair { - slot, - proposer_index, - parent_root, - state_root: Hash256::zero(), - body: BeaconBlockBodyAltair { - randao_reveal, - eth1_data, - graffiti, - proposer_slashings: proposer_slashings.into(), - attester_slashings: attester_slashings.into(), - attestations, - deposits, - voluntary_exits, - // FIXME(altair): put a sync aggregate from the pool here (once implemented) - sync_aggregate: SyncAggregate::new(), - }, - }), + BeaconState::Altair(_) => { + let sync_aggregate = get_sync_aggregate()?; + BeaconBlock::Altair(BeaconBlockAltair { + slot, + proposer_index, + parent_root, + state_root: Hash256::zero(), + body: BeaconBlockBodyAltair { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings: proposer_slashings.into(), + attester_slashings: attester_slashings.into(), + attestations, + deposits, + voluntary_exits, + sync_aggregate, + }, + }) + } }; let block = SignedBeaconBlock::from_block( diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index f484b194549..543f4222269 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -87,6 +87,7 @@ pub enum BeaconChainError { DuplicateValidatorPublicKey, ValidatorPubkeyCacheFileError(String), ValidatorIndexUnknown(usize), + ValidatorPubkeyUnknown(PublicKeyBytes), OpPoolError(OpPoolError), NaiveAggregationError(NaiveAggregationError), ObservedAttestationsError(ObservedAttestationsError), @@ -120,6 +121,7 @@ pub enum BeaconChainError { state_epoch: Epoch, shuffling_epoch: Epoch, }, + SyncDutiesError(BeaconStateError), InconsistentForwardsIter { request_slot: Slot, slot: Slot, diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index e34205c700d..67a53b90497 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -151,7 +151,7 @@ pub fn test_spec() -> ChainSpec { pub struct BeaconChainHarness { pub validator_keypairs: Vec, - pub chain: BeaconChain, + pub chain: Arc>, pub spec: ChainSpec, pub data_dir: TempDir, pub shutdown_receiver: Receiver, @@ -229,6 +229,29 @@ impl BeaconChainHarness> { target_aggregators_per_committee: u64, store_config: StoreConfig, chain_config: ChainConfig, + ) -> Self { + Self::new_with_mutator( + eth_spec_instance, + spec, + validator_keypairs, + target_aggregators_per_committee, + store_config, + chain_config, + |x| x, + ) + } + + /// Apply a function to beacon chain builder before building. + pub fn new_with_mutator( + eth_spec_instance: E, + spec: Option, + validator_keypairs: Vec, + target_aggregators_per_committee: u64, + store_config: StoreConfig, + chain_config: ChainConfig, + mutator: impl FnOnce( + BeaconChainBuilder>, + ) -> BeaconChainBuilder>, ) -> Self { let data_dir = tempdir().expect("should create temporary data_dir"); let mut spec = spec.unwrap_or_else(test_spec::); @@ -240,7 +263,7 @@ impl BeaconChainHarness> { let log = test_logger(); let store = HotColdDB::open_ephemeral(store_config, spec.clone(), log.clone()).unwrap(); - let chain = BeaconChainBuilder::new(eth_spec_instance) + let builder = BeaconChainBuilder::new(eth_spec_instance) .logger(log.clone()) .custom_spec(spec.clone()) .store(Arc::new(store)) @@ -260,13 +283,13 @@ impl BeaconChainHarness> { log.clone(), 1, ))) - .monitor_validators(true, vec![], log) - .build() - .expect("should build"); + .monitor_validators(true, vec![], log); + + let chain = mutator(builder).build().expect("should build"); Self { spec: chain.spec.clone(), - chain, + chain: Arc::new(chain), validator_keypairs, data_dir, shutdown_receiver, @@ -311,7 +334,7 @@ impl BeaconChainHarness> { Self { spec: chain.spec.clone(), - chain, + chain: Arc::new(chain), validator_keypairs, data_dir, shutdown_receiver, @@ -353,7 +376,7 @@ impl BeaconChainHarness> { Self { spec: chain.spec.clone(), - chain, + chain: Arc::new(chain), validator_keypairs, data_dir, shutdown_receiver, diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 446b19195ab..ccdb98a2310 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -4,7 +4,7 @@ extern crate lazy_static; use beacon_chain::test_utils::{ - AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, + test_logger, AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, }; use beacon_chain::{BeaconSnapshot, BlockError, ChainConfig, ChainSegmentResult}; use slasher::{Config as SlasherConfig, Slasher}; @@ -830,17 +830,25 @@ fn block_gossip_verification() { #[test] fn verify_block_for_gossip_slashing_detection() { - let mut harness = get_harness(VALIDATOR_COUNT); - let slasher_dir = tempdir().unwrap(); let slasher = Arc::new( Slasher::open( SlasherConfig::new(slasher_dir.path().into()).for_testing(), - harness.logger().clone(), + test_logger(), ) .unwrap(), ); - harness.chain.slasher = Some(slasher.clone()); + + let harness = BeaconChainHarness::new_with_mutator( + MainnetEthSpec, + None, + KEYPAIRS.to_vec(), + 1 << 32, + StoreConfig::default(), + ChainConfig::default(), + |builder| builder.slasher(slasher.clone()), + ); + harness.advance_slot(); let state = harness.get_current_state(); let (block1, _) = harness.make_block(state.clone(), Slot::new(1)); diff --git a/beacon_node/http_api/Cargo.toml b/beacon_node/http_api/Cargo.toml index 0f288cfea0c..2b87b6f3398 100644 --- a/beacon_node/http_api/Cargo.toml +++ b/beacon_node/http_api/Cargo.toml @@ -3,6 +3,7 @@ name = "http_api" version = "0.1.0" authors = ["Paul Hauner "] edition = "2018" +autotests = false # using a single test binary compiles faster [dependencies] warp = { git = "https://github.com/paulhauner/warp ", branch = "cors-wildcard" } @@ -35,3 +36,7 @@ store = { path = "../store" } environment = { path = "../../lighthouse/environment" } tree_hash = "0.1.1" sensitive_url = { path = "../../common/sensitive_url" } + +[[test]] +name = "bn_http_api_tests" +path = "tests/main.rs" diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index a652484e728..32a10ed71d8 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -10,7 +10,9 @@ mod block_id; mod metrics; mod proposer_duties; mod state_id; +mod sync_committees; mod validator_inclusion; +mod version; use beacon_chain::{ attestation_verification::SignatureVerifiedAttestation, @@ -20,7 +22,7 @@ use beacon_chain::{ WhenSlotSkipped, }; use block_id::BlockId; -use eth2::types::{self as api_types, ValidatorId}; +use eth2::types::{self as api_types, EndpointVersion, ValidatorId}; use eth2_libp2p::{types::SyncState, EnrExt, NetworkGlobals, PeerId, PubsubMessage}; use lighthouse_version::version_with_platform; use network::NetworkMessage; @@ -37,10 +39,12 @@ use std::sync::Arc; use tokio::sync::mpsc::UnboundedSender; use tokio_stream::{wrappers::BroadcastStream, StreamExt}; use types::{ - Attestation, AttesterSlashing, CommitteeCache, ConfigAndPreset, Epoch, EthSpec, - ProposerSlashing, RelativeEpoch, SignedAggregateAndProof, SignedBeaconBlock, - SignedVoluntaryExit, Slot, + Attestation, AttesterSlashing, BeaconStateError, CommitteeCache, ConfigAndPreset, Epoch, + EthSpec, ProposerSlashing, RelativeEpoch, SignedAggregateAndProof, SignedBeaconBlock, + SignedContributionAndProof, SignedVoluntaryExit, Slot, SyncCommitteeMessage, + SyncContributionData, }; +use version::{fork_versioned_response, unsupported_version_rejection, V1}; use warp::http::StatusCode; use warp::sse::Event; use warp::Reply; @@ -48,7 +52,6 @@ use warp::{http::Response, Filter}; use warp_utils::task::{blocking_json_task, blocking_task}; const API_PREFIX: &str = "eth"; -const API_VERSION: &str = "v1"; /// If the node is within this many epochs from the head, we declare it to be synced regardless of /// the network sync state. @@ -152,7 +155,7 @@ pub fn prometheus_metrics() -> warp::filters::log::Log Option<&'static str> { - if info.path() == format!("/{}/{}/{}", API_PREFIX, API_VERSION, s) { + if info.path() == format!("/{}/{}", API_PREFIX, s) { Some(s) } else { None @@ -160,30 +163,30 @@ pub fn prometheus_metrics() -> warp::filters::log::Log Option<&'static str> { - if info - .path() - .starts_with(&format!("/{}/{}/{}", API_PREFIX, API_VERSION, s)) - { + if info.path().starts_with(&format!("/{}/{}", API_PREFIX, s)) { Some(s) } else { None } }; - equals("beacon/blocks") - .or_else(|| starts_with("validator/duties/attester")) - .or_else(|| starts_with("validator/duties/proposer")) - .or_else(|| starts_with("validator/attestation_data")) - .or_else(|| starts_with("validator/blocks")) - .or_else(|| starts_with("validator/aggregate_attestation")) - .or_else(|| starts_with("validator/aggregate_and_proofs")) - .or_else(|| starts_with("validator/beacon_committee_subscriptions")) - .or_else(|| starts_with("beacon/")) - .or_else(|| starts_with("config/")) - .or_else(|| starts_with("debug/")) - .or_else(|| starts_with("events/")) - .or_else(|| starts_with("node/")) - .or_else(|| starts_with("validator/")) + // First line covers `POST /v1/beacon/blocks` only + equals("v1/beacon/blocks") + .or_else(|| starts_with("v1/validator/duties/attester")) + .or_else(|| starts_with("v1/validator/duties/proposer")) + .or_else(|| starts_with("v1/validator/attestation_data")) + .or_else(|| starts_with("v1/validator/blocks")) + .or_else(|| starts_with("v2/validator/blocks")) + .or_else(|| starts_with("v1/validator/aggregate_attestation")) + .or_else(|| starts_with("v1/validator/aggregate_and_proofs")) + .or_else(|| starts_with("v1/validator/beacon_committee_subscriptions")) + .or_else(|| starts_with("v1/beacon/")) + .or_else(|| starts_with("v2/beacon/")) + .or_else(|| starts_with("v1/config/")) + .or_else(|| starts_with("v1/debug/")) + .or_else(|| starts_with("v1/events/")) + .or_else(|| starts_with("v1/node/")) + .or_else(|| starts_with("v1/validator/")) .unwrap_or("other") }; @@ -239,7 +242,29 @@ pub fn serve( )); } - let eth1_v1 = warp::path(API_PREFIX).and(warp::path(API_VERSION)); + // Create a filter that extracts the endpoint version. + let any_version = warp::path(API_PREFIX).and(warp::path::param::().or_else( + |_| async move { + Err(warp_utils::reject::custom_bad_request( + "Invalid version identifier".to_string(), + )) + }, + )); + + // Filter that enforces a single endpoint version and then discards the `EndpointVersion`. + let single_version = |reqd: EndpointVersion| { + any_version + .and_then(move |version| async move { + if version == reqd { + Ok(()) + } else { + Err(unsupported_version_rejection(version)) + } + }) + .untuple_one() + }; + + let eth1_v1 = single_version(V1); // Create a `warp` filter that provides access to the network globals. let inner_network_globals = ctx.network_globals.clone(); @@ -659,6 +684,61 @@ pub fn serve( }, ); + // GET beacon/states/{state_id}/sync_committees?epoch + let get_beacon_state_sync_committees = beacon_states_path + .clone() + .and(warp::path("sync_committees")) + .and(warp::query::()) + .and(warp::path::end()) + .and_then( + |state_id: StateId, + chain: Arc>, + query: api_types::SyncCommitteesQuery| { + blocking_json_task(move || { + let sync_committee = state_id.map_state(&chain, |state| { + let current_epoch = state.current_epoch(); + let epoch = query.epoch.unwrap_or(current_epoch); + state + .get_built_sync_committee(epoch, &chain.spec) + .map(|committee| committee.clone()) + .map_err(|e| match e { + BeaconStateError::SyncCommitteeNotKnown { .. } => { + warp_utils::reject::custom_bad_request(format!( + "state at epoch {} has no sync committee for epoch {}", + current_epoch, epoch + )) + } + BeaconStateError::IncorrectStateVariant => { + warp_utils::reject::custom_bad_request(format!( + "state at epoch {} is not activated for Altair", + current_epoch, + )) + } + e => warp_utils::reject::beacon_state_error(e), + }) + })?; + + let validators = chain + .validator_indices(sync_committee.pubkeys.iter()) + .map_err(warp_utils::reject::beacon_chain_error)?; + + let validator_aggregates = validators + .chunks_exact(T::EthSpec::sync_subcommittee_size()) + .map(|indices| api_types::SyncSubcommittee { + indices: indices.to_vec(), + }) + .collect(); + + let response = api_types::SyncCommitteeByValidatorIndices { + validators, + validator_aggregates, + }; + + Ok(api_types::GenericResponse::from(response)) + }) + }, + ); + // GET beacon/headers // // Note: this endpoint only returns information about blocks in the canonical chain. Given that @@ -875,23 +955,32 @@ pub fn serve( }, ); - let beacon_blocks_path = eth1_v1 + let block_id_or_err = warp::path::param::().or_else(|_| async { + Err(warp_utils::reject::custom_bad_request( + "Invalid block ID".to_string(), + )) + }); + + let beacon_blocks_path_v1 = eth1_v1 .and(warp::path("beacon")) .and(warp::path("blocks")) - .and(warp::path::param::().or_else(|_| async { - Err(warp_utils::reject::custom_bad_request( - "Invalid block ID".to_string(), - )) - })) + .and(block_id_or_err) + .and(chain_filter.clone()); + + let beacon_blocks_path_any = any_version + .and(warp::path("beacon")) + .and(warp::path("blocks")) + .and(block_id_or_err) .and(chain_filter.clone()); // GET beacon/blocks/{block_id} - let get_beacon_block = beacon_blocks_path + let get_beacon_block = beacon_blocks_path_any .clone() .and(warp::path::end()) .and(warp::header::optional::("accept")) .and_then( - |block_id: BlockId, + |endpoint_version: EndpointVersion, + block_id: BlockId, chain: Arc>, accept_header: Option| { blocking_task(move || { @@ -907,17 +996,18 @@ pub fn serve( e )) }), - _ => Ok( - warp::reply::json(&api_types::GenericResponseRef::from(&block)) - .into_response(), - ), + _ => { + let fork_name = block.fork_name(&chain.spec).ok(); + fork_versioned_response(endpoint_version, fork_name, block) + .map(|res| warp::reply::json(&res).into_response()) + } } }) }, ); // GET beacon/blocks/{block_id}/root - let get_beacon_block_root = beacon_blocks_path + let get_beacon_block_root = beacon_blocks_path_v1 .clone() .and(warp::path("root")) .and(warp::path::end()) @@ -931,7 +1021,7 @@ pub fn serve( }); // GET beacon/blocks/{block_id}/attestations - let get_beacon_block_attestations = beacon_blocks_path + let get_beacon_block_attestations = beacon_blocks_path_v1 .clone() .and(warp::path("attestations")) .and(warp::path::end()) @@ -1250,6 +1340,28 @@ pub fn serve( }) }); + // POST beacon/pool/sync_committees + let post_beacon_pool_sync_committees = beacon_pool_path + .clone() + .and(warp::path("sync_committees")) + .and(warp::path::end()) + .and(warp::body::json()) + .and(network_tx_filter.clone()) + .and(log_filter.clone()) + .and_then( + |chain: Arc>, + signatures: Vec, + network_tx: UnboundedSender>, + log: Logger| { + blocking_json_task(move || { + sync_committees::process_sync_committee_signatures( + signatures, network_tx, &chain, log, + )?; + Ok(api_types::GenericResponse::from(())) + }) + }, + ); + /* * config/fork_schedule */ @@ -1307,7 +1419,7 @@ pub fn serve( */ // GET debug/beacon/states/{state_id} - let get_debug_beacon_states = eth1_v1 + let get_debug_beacon_states = any_version .and(warp::path("debug")) .and(warp::path("beacon")) .and(warp::path("states")) @@ -1320,7 +1432,8 @@ pub fn serve( .and(warp::header::optional::("accept")) .and(chain_filter.clone()) .and_then( - |state_id: StateId, + |endpoint_version: EndpointVersion, + state_id: StateId, accept_header: Option, chain: Arc>| { blocking_task(move || match accept_header { @@ -1338,10 +1451,9 @@ pub fn serve( }) } _ => state_id.map_state(&chain, |state| { - Ok( - warp::reply::json(&api_types::GenericResponseRef::from(&state)) - .into_response(), - ) + let fork_name = state.fork_name(&chain.spec).ok(); + let res = fork_versioned_response(endpoint_version, fork_name, &state)?; + Ok(warp::reply::json(&res).into_response()) }), }) }, @@ -1659,7 +1771,7 @@ pub fn serve( }); // GET validator/blocks/{slot} - let get_validator_blocks = eth1_v1 + let get_validator_blocks = any_version .and(warp::path("validator")) .and(warp::path("blocks")) .and(warp::path::param::().or_else(|_| async { @@ -1672,7 +1784,10 @@ pub fn serve( .and(warp::query::()) .and(chain_filter.clone()) .and_then( - |slot: Slot, query: api_types::ValidatorBlocksQuery, chain: Arc>| { + |endpoint_version: EndpointVersion, + slot: Slot, + query: api_types::ValidatorBlocksQuery, + chain: Arc>| { blocking_json_task(move || { let randao_reveal = (&query.randao_reveal).try_into().map_err(|e| { warp_utils::reject::custom_bad_request(format!( @@ -1681,11 +1796,11 @@ pub fn serve( )) })?; - chain + let (block, _) = chain .produce_block(randao_reveal, slot, query.graffiti.map(Into::into)) - .map(|block_and_state| block_and_state.0) - .map(api_types::GenericResponse::from) - .map_err(warp_utils::reject::block_production_error) + .map_err(warp_utils::reject::block_production_error)?; + let fork_name = block.to_ref().fork_name(&chain.spec).ok(); + fork_versioned_response(endpoint_version, fork_name, block) }) }, ); @@ -1770,12 +1885,57 @@ pub fn serve( }, ); + // POST validator/duties/sync + let post_validator_duties_sync = eth1_v1 + .and(warp::path("validator")) + .and(warp::path("duties")) + .and(warp::path("sync")) + .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::body::json()) + .and(chain_filter.clone()) + .and_then( + |epoch: Epoch, indices: api_types::ValidatorIndexData, chain: Arc>| { + blocking_json_task(move || { + sync_committees::sync_committee_duties(epoch, &indices.0, &chain) + }) + }, + ); + + // GET validator/sync_committee_contribution + let get_validator_sync_committee_contribution = eth1_v1 + .and(warp::path("validator")) + .and(warp::path("sync_committee_contribution")) + .and(warp::path::end()) + .and(warp::query::()) + .and(not_while_syncing_filter.clone()) + .and(chain_filter.clone()) + .and_then( + |sync_committee_data: SyncContributionData, chain: Arc>| { + blocking_json_task(move || { + chain + .get_aggregated_sync_committee_contribution(&sync_committee_data) + .map(api_types::GenericResponse::from) + .ok_or_else(|| { + warp_utils::reject::custom_not_found( + "no matching sync contribution found".to_string(), + ) + }) + }) + }, + ); + // POST validator/aggregate_and_proofs let post_validator_aggregate_and_proofs = eth1_v1 .and(warp::path("validator")) .and(warp::path("aggregate_and_proofs")) .and(warp::path::end()) - .and(not_while_syncing_filter) + .and(not_while_syncing_filter.clone()) .and(chain_filter.clone()) .and(warp::body::json()) .and(network_tx_filter.clone()) @@ -1871,13 +2031,39 @@ pub fn serve( }, ); + let post_validator_contribution_and_proofs = eth1_v1 + .and(warp::path("validator")) + .and(warp::path("contribution_and_proofs")) + .and(warp::path::end()) + .and(not_while_syncing_filter) + .and(chain_filter.clone()) + .and(warp::body::json()) + .and(network_tx_filter.clone()) + .and(log_filter.clone()) + .and_then( + |chain: Arc>, + contributions: Vec>, + network_tx: UnboundedSender>, + log: Logger| { + blocking_json_task(move || { + sync_committees::process_signed_contribution_and_proofs( + contributions, + network_tx, + &chain, + log, + )?; + Ok(api_types::GenericResponse::from(())) + }) + }, + ); + // POST validator/beacon_committee_subscriptions let post_validator_beacon_committee_subscriptions = eth1_v1 .and(warp::path("validator")) .and(warp::path("beacon_committee_subscriptions")) .and(warp::path::end()) .and(warp::body::json()) - .and(network_tx_filter) + .and(network_tx_filter.clone()) .and(chain_filter.clone()) .and_then( |subscriptions: Vec, @@ -1911,6 +2097,38 @@ pub fn serve( }, ); + // POST validator/sync_committee_subscriptions + let post_validator_sync_committee_subscriptions = eth1_v1 + .and(warp::path("validator")) + .and(warp::path("sync_committee_subscriptions")) + .and(warp::path::end()) + .and(warp::body::json()) + .and(network_tx_filter) + .and(chain_filter.clone()) + .and_then( + |subscriptions: Vec, + network_tx: UnboundedSender>, + chain: Arc>| { + blocking_json_task(move || { + for subscription in subscriptions { + chain + .validator_monitor + .write() + .auto_register_local_validator(subscription.validator_index); + + publish_network_message( + &network_tx, + NetworkMessage::SyncCommitteeSubscribe { + subscriptions: vec![subscription], + }, + )?; + } + + Ok(()) + }) + }, + ); + // POST lighthouse/liveness let post_lighthouse_liveness = warp::path("lighthouse") .and(warp::path("liveness")) @@ -2248,6 +2466,7 @@ pub fn serve( .or(get_beacon_state_validators.boxed()) .or(get_beacon_state_validators_id.boxed()) .or(get_beacon_state_committees.boxed()) + .or(get_beacon_state_sync_committees.boxed()) .or(get_beacon_headers.boxed()) .or(get_beacon_headers_block_id.boxed()) .or(get_beacon_block.boxed()) @@ -2273,6 +2492,7 @@ pub fn serve( .or(get_validator_blocks.boxed()) .or(get_validator_attestation_data.boxed()) .or(get_validator_aggregate_attestation.boxed()) + .or(get_validator_sync_committee_contribution.boxed()) .or(get_lighthouse_health.boxed()) .or(get_lighthouse_syncing.boxed()) .or(get_lighthouse_peers.boxed()) @@ -2294,10 +2514,14 @@ pub fn serve( .or(post_beacon_pool_attester_slashings.boxed()) .or(post_beacon_pool_proposer_slashings.boxed()) .or(post_beacon_pool_voluntary_exits.boxed()) + .or(post_beacon_pool_sync_committees.boxed()) .or(post_validator_duties_attester.boxed()) + .or(post_validator_duties_sync.boxed()) .or(post_validator_aggregate_and_proofs.boxed()) - .or(post_lighthouse_liveness.boxed()) - .or(post_validator_beacon_committee_subscriptions.boxed()), + .or(post_validator_contribution_and_proofs.boxed()) + .or(post_validator_beacon_committee_subscriptions.boxed()) + .or(post_validator_sync_committee_subscriptions.boxed()) + .or(post_lighthouse_liveness.boxed()), )) .recover(warp_utils::reject::handle_rejection) .with(slog_logging(log.clone())) diff --git a/beacon_node/http_api/src/sync_committees.rs b/beacon_node/http_api/src/sync_committees.rs new file mode 100644 index 00000000000..68f54a8d460 --- /dev/null +++ b/beacon_node/http_api/src/sync_committees.rs @@ -0,0 +1,294 @@ +//! Handlers for sync committee endpoints. + +use crate::publish_pubsub_message; +use beacon_chain::sync_committee_verification::{ + Error as SyncVerificationError, VerifiedSyncCommitteeMessage, +}; +use beacon_chain::{ + BeaconChain, BeaconChainError, BeaconChainTypes, StateSkipConfig, + MAXIMUM_GOSSIP_CLOCK_DISPARITY, +}; +use eth2::types::{self as api_types}; +use eth2_libp2p::PubsubMessage; +use network::NetworkMessage; +use slog::{error, warn, Logger}; +use slot_clock::SlotClock; +use std::cmp::max; +use std::collections::HashMap; +use tokio::sync::mpsc::UnboundedSender; +use types::{ + slot_data::SlotData, BeaconStateError, Epoch, EthSpec, SignedContributionAndProof, + SyncCommitteeMessage, SyncDuty, SyncSubnetId, +}; + +/// The struct that is returned to the requesting HTTP client. +type SyncDuties = api_types::GenericResponse>; + +/// Handles a request from the HTTP API for sync committee duties. +pub fn sync_committee_duties( + request_epoch: Epoch, + request_indices: &[u64], + chain: &BeaconChain, +) -> Result { + let altair_fork_epoch = if let Some(altair_fork_epoch) = chain.spec.altair_fork_epoch { + altair_fork_epoch + } else { + // Empty response for networks with Altair disabled. + return Ok(convert_to_response(vec![])); + }; + + // Try using the head's sync committees to satisfy the request. This should be sufficient for + // the vast majority of requests. Rather than checking if we think the request will succeed in a + // way prone to data races, we attempt the request immediately and check the error code. + match chain.sync_committee_duties_from_head(request_epoch, request_indices) { + Ok(duties) => return Ok(convert_to_response(duties)), + Err(BeaconChainError::SyncDutiesError(BeaconStateError::SyncCommitteeNotKnown { + .. + })) + | Err(BeaconChainError::SyncDutiesError(BeaconStateError::IncorrectStateVariant)) => (), + Err(e) => return Err(warp_utils::reject::beacon_chain_error(e)), + } + + let duties = duties_from_state_load(request_epoch, request_indices, altair_fork_epoch, chain) + .map_err(|e| match e { + BeaconChainError::SyncDutiesError(BeaconStateError::SyncCommitteeNotKnown { + current_epoch, + .. + }) => warp_utils::reject::custom_bad_request(format!( + "invalid epoch: {}, current epoch: {}", + request_epoch, current_epoch + )), + e => warp_utils::reject::beacon_chain_error(e), + })?; + Ok(convert_to_response(duties)) +} + +/// Slow path for duties: load a state and use it to compute the duties. +fn duties_from_state_load( + request_epoch: Epoch, + request_indices: &[u64], + altair_fork_epoch: Epoch, + chain: &BeaconChain, +) -> Result>, BeaconChainError> { + // 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 last `MAXIMUM_GOSSIP_CLOCK_DISPARITY` duration of the epoch `tolerant_current_epoch` + // will equal `current_epoch + 1` + let current_epoch = chain.epoch()?; + let tolerant_current_epoch = chain + .slot_clock + .now_with_future_tolerance(MAXIMUM_GOSSIP_CLOCK_DISPARITY) + .ok_or(BeaconChainError::UnableToReadSlot)? + .epoch(T::EthSpec::slots_per_epoch()); + + let max_sync_committee_period = tolerant_current_epoch.sync_committee_period(&chain.spec)? + 1; + let sync_committee_period = request_epoch.sync_committee_period(&chain.spec)?; + + if tolerant_current_epoch < altair_fork_epoch { + // Empty response if the epoch is pre-Altair. + Ok(vec![]) + } else if sync_committee_period <= max_sync_committee_period { + // Load the state at the start of the *previous* sync committee period. + // This is sufficient for historical duties, and efficient in the case where the head + // is lagging the current epoch and we need duties for the next period (because we only + // have to transition the head to start of the current period). + // + // We also need to ensure that the load slot is after the Altair fork. + let load_slot = max( + chain.spec.epochs_per_sync_committee_period * sync_committee_period.saturating_sub(1), + altair_fork_epoch, + ) + .start_slot(T::EthSpec::slots_per_epoch()); + + let state = chain.state_at_slot(load_slot, StateSkipConfig::WithoutStateRoots)?; + + state + .get_sync_committee_duties(request_epoch, request_indices, &chain.spec) + .map_err(BeaconChainError::SyncDutiesError) + } else { + Err(BeaconChainError::SyncDutiesError( + BeaconStateError::SyncCommitteeNotKnown { + current_epoch, + epoch: request_epoch, + }, + )) + } +} + +fn convert_to_response(duties: Vec>) -> SyncDuties { + api_types::GenericResponse::from(duties.into_iter().flatten().collect::>()) +} + +/// Receive sync committee duties, storing them in the pools & broadcasting them. +pub fn process_sync_committee_signatures( + sync_committee_signatures: Vec, + network_tx: UnboundedSender>, + chain: &BeaconChain, + log: Logger, +) -> Result<(), warp::reject::Rejection> { + let mut failures = vec![]; + + for (i, sync_committee_signature) in sync_committee_signatures.iter().enumerate() { + let subnet_positions = match get_subnet_positions_for_sync_committee_message( + sync_committee_signature, + chain, + ) { + Ok(positions) => positions, + Err(e) => { + error!( + log, + "Unable to compute subnet positions for sync message"; + "error" => ?e, + "slot" => sync_committee_signature.slot, + ); + failures.push(api_types::Failure::new(i, format!("Verification: {:?}", e))); + continue; + } + }; + + // Verify and publish on all relevant subnets. + // + // The number of assigned subnets on any practical network should be ~1, so the apparent + // inefficiency of verifying multiple times is not a real inefficiency. + let mut verified_for_pool = None; + for subnet_id in subnet_positions.keys().copied() { + match VerifiedSyncCommitteeMessage::verify( + sync_committee_signature.clone(), + subnet_id, + chain, + ) { + Ok(verified) => { + publish_pubsub_message( + &network_tx, + PubsubMessage::SyncCommitteeMessage(Box::new(( + subnet_id, + verified.sync_message().clone(), + ))), + )?; + + verified_for_pool = Some(verified); + } + Err(e) => { + error!( + log, + "Failure verifying sync committee signature for gossip"; + "error" => ?e, + "request_index" => i, + "slot" => sync_committee_signature.slot, + "validator_index" => sync_committee_signature.validator_index, + ); + failures.push(api_types::Failure::new(i, format!("Verification: {:?}", e))); + } + } + } + + if let Some(verified) = verified_for_pool { + if let Err(e) = chain.add_to_naive_sync_aggregation_pool(verified) { + error!( + log, + "Unable to add sync committee signature to pool"; + "error" => ?e, + "slot" => sync_committee_signature.slot, + "validator_index" => sync_committee_signature.validator_index, + ); + } + } + } + + if failures.is_empty() { + Ok(()) + } else { + Err(warp_utils::reject::indexed_bad_request( + "error processing sync committee signatures".to_string(), + failures, + )) + } +} + +/// Get the set of all subnet assignments for a `SyncCommitteeMessage`. +pub fn get_subnet_positions_for_sync_committee_message( + sync_message: &SyncCommitteeMessage, + chain: &BeaconChain, +) -> Result>, SyncVerificationError> { + let pubkey = chain + .validator_pubkey_bytes(sync_message.validator_index as usize)? + .ok_or(SyncVerificationError::UnknownValidatorIndex( + sync_message.validator_index as usize, + ))?; + let sync_committee = chain.sync_committee_at_next_slot(sync_message.get_slot())?; + Ok(sync_committee.subcommittee_positions_for_public_key(&pubkey)?) +} + +/// Receive signed contributions and proofs, storing them in the op pool and broadcasting. +pub fn process_signed_contribution_and_proofs( + signed_contribution_and_proofs: Vec>, + network_tx: UnboundedSender>, + chain: &BeaconChain, + log: Logger, +) -> Result<(), warp::reject::Rejection> { + let mut verified_contributions = Vec::with_capacity(signed_contribution_and_proofs.len()); + let mut failures = vec![]; + + // Verify contributions & broadcast to the network. + for (index, contribution) in signed_contribution_and_proofs.into_iter().enumerate() { + let aggregator_index = contribution.message.aggregator_index; + let subcommittee_index = contribution.message.contribution.subcommittee_index; + let contribution_slot = contribution.message.contribution.slot; + + match chain.verify_sync_contribution_for_gossip(contribution) { + Ok(verified_contribution) => { + publish_pubsub_message( + &network_tx, + PubsubMessage::SignedContributionAndProof(Box::new( + verified_contribution.aggregate().clone(), + )), + )?; + + // FIXME(altair): notify validator monitor + verified_contributions.push((index, verified_contribution)); + } + // If we already know the contribution, don't broadcast it or attempt to + // further verify it. Return success. + Err(SyncVerificationError::SyncContributionAlreadyKnown(_)) => continue, + Err(e) => { + error!( + log, + "Failure verifying signed contribution and proof"; + "error" => ?e, + "request_index" => index, + "aggregator_index" => aggregator_index, + "subcommittee_index" => subcommittee_index, + "contribution_slot" => contribution_slot, + ); + failures.push(api_types::Failure::new( + index, + format!("Verification: {:?}", e), + )); + } + } + } + + // Add to the block inclusion pool. + for (index, verified_contribution) in verified_contributions { + if let Err(e) = chain.add_contribution_to_block_inclusion_pool(verified_contribution) { + warn!( + log, + "Could not add verified sync contribution to the inclusion pool"; + "error" => ?e, + "request_index" => index, + ); + failures.push(api_types::Failure::new(index, format!("Op pool: {:?}", e))); + } + } + + if !failures.is_empty() { + Err(warp_utils::reject::indexed_bad_request( + "error processing contribution and proofs".to_string(), + failures, + )) + } else { + Ok(()) + } +} diff --git a/beacon_node/http_api/src/version.rs b/beacon_node/http_api/src/version.rs new file mode 100644 index 00000000000..db891727ef7 --- /dev/null +++ b/beacon_node/http_api/src/version.rs @@ -0,0 +1,28 @@ +use crate::api_types::{EndpointVersion, ForkVersionedResponse}; +use serde::Serialize; +use types::ForkName; + +pub const V1: EndpointVersion = EndpointVersion(1); +pub const V2: EndpointVersion = EndpointVersion(2); + +pub fn fork_versioned_response( + endpoint_version: EndpointVersion, + fork_name: Option, + data: T, +) -> Result, warp::reject::Rejection> { + let fork_name = if endpoint_version == V1 { + None + } else if endpoint_version == V2 { + fork_name + } else { + return Err(unsupported_version_rejection(endpoint_version)); + }; + Ok(ForkVersionedResponse { + version: fork_name, + data, + }) +} + +pub fn unsupported_version_rejection(version: EndpointVersion) -> warp::reject::Rejection { + warp_utils::reject::custom_bad_request(format!("Unsupported endpoint version: {}", version)) +} diff --git a/beacon_node/http_api/tests/common.rs b/beacon_node/http_api/tests/common.rs new file mode 100644 index 00000000000..bca7bbd62d5 --- /dev/null +++ b/beacon_node/http_api/tests/common.rs @@ -0,0 +1,142 @@ +use beacon_chain::{ + test_utils::{BeaconChainHarness, EphemeralHarnessType}, + BeaconChain, BeaconChainTypes, +}; +use eth2::{BeaconNodeHttpClient, Timeouts}; +use eth2_libp2p::{ + discv5::enr::{CombinedKey, EnrBuilder}, + rpc::methods::{MetaData, MetaDataV2}, + types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield, SyncState}, + Enr, NetworkGlobals, PeerId, +}; +use http_api::{Config, Context}; +use network::NetworkMessage; +use sensitive_url::SensitiveUrl; +use slog::Logger; +use std::future::Future; +use std::net::{Ipv4Addr, SocketAddr}; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::{mpsc, oneshot}; +use types::{test_utils::generate_deterministic_keypairs, ChainSpec, EthSpec}; + +pub const TCP_PORT: u16 = 42; +pub const UDP_PORT: u16 = 42; +pub const SEQ_NUMBER: u64 = 0; +pub const EXTERNAL_ADDR: &str = "/ip4/0.0.0.0/tcp/9000"; + +/// HTTP API tester that allows interaction with the underlying beacon chain harness. +pub struct InteractiveTester { + pub harness: BeaconChainHarness>, + pub client: BeaconNodeHttpClient, + pub network_rx: mpsc::UnboundedReceiver>, + _server_shutdown: oneshot::Sender<()>, +} + +/// The result of calling `create_api_server`. +/// +/// Glue-type between `tests::ApiTester` and `InteractiveTester`. +pub struct ApiServer> { + pub server: SFut, + pub listening_socket: SocketAddr, + pub shutdown_tx: oneshot::Sender<()>, + pub network_rx: tokio::sync::mpsc::UnboundedReceiver>, + pub local_enr: Enr, + pub external_peer_id: PeerId, +} + +impl InteractiveTester { + pub fn new(spec: Option, validator_count: usize) -> Self { + let harness = BeaconChainHarness::new( + E::default(), + spec, + generate_deterministic_keypairs(validator_count), + ); + + let ApiServer { + server, + listening_socket, + shutdown_tx: _server_shutdown, + network_rx, + .. + } = create_api_server(harness.chain.clone(), harness.logger().clone()); + + tokio::spawn(server); + + let client = BeaconNodeHttpClient::new( + SensitiveUrl::parse(&format!( + "http://{}:{}", + listening_socket.ip(), + listening_socket.port() + )) + .unwrap(), + Timeouts::set_all(Duration::from_secs(1)), + ); + + Self { + harness, + client, + network_rx, + _server_shutdown, + } + } +} + +pub fn create_api_server( + chain: Arc>, + log: Logger, +) -> ApiServer> { + let (network_tx, network_rx) = mpsc::unbounded_channel(); + + // Default metadata + let meta_data = MetaData::V2(MetaDataV2 { + seq_number: SEQ_NUMBER, + attnets: EnrAttestationBitfield::::default(), + syncnets: EnrSyncCommitteeBitfield::::default(), + }); + let enr_key = CombinedKey::generate_secp256k1(); + let enr = EnrBuilder::new("v4").build(&enr_key).unwrap(); + let network_globals = + NetworkGlobals::new(enr.clone(), TCP_PORT, UDP_PORT, meta_data, vec![], &log); + + let peer_id = PeerId::random(); + network_globals + .peers + .write() + .connect_ingoing(&peer_id, EXTERNAL_ADDR.parse().unwrap(), None); + + *network_globals.sync_state.write() = SyncState::Synced; + + let eth1_service = eth1::Service::new(eth1::Config::default(), log.clone(), chain.spec.clone()); + + let context = Arc::new(Context { + config: Config { + enabled: true, + listen_addr: Ipv4Addr::new(127, 0, 0, 1), + listen_port: 0, + allow_origin: None, + serve_legacy_spec: true, + }, + chain: Some(chain.clone()), + network_tx: Some(network_tx), + network_globals: Some(Arc::new(network_globals)), + eth1_service: Some(eth1_service), + log, + }); + let ctx = context.clone(); + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + let server_shutdown = async { + // It's not really interesting why this triggered, just that it happened. + let _ = shutdown_rx.await; + }; + let (listening_socket, server) = http_api::serve(ctx, server_shutdown).unwrap(); + + ApiServer { + server, + listening_socket, + shutdown_tx, + network_rx, + local_enr: enr, + external_peer_id: peer_id, + } +} diff --git a/beacon_node/http_api/tests/fork_tests.rs b/beacon_node/http_api/tests/fork_tests.rs new file mode 100644 index 00000000000..9286b60af87 --- /dev/null +++ b/beacon_node/http_api/tests/fork_tests.rs @@ -0,0 +1,305 @@ +//! Tests for API behaviour across fork boundaries. +use crate::common::*; +use beacon_chain::{test_utils::RelativeSyncCommittee, StateSkipConfig}; +use eth2::types::{StateId, SyncSubcommittee}; +use types::{ChainSpec, Epoch, EthSpec, MinimalEthSpec, Slot}; + +type E = MinimalEthSpec; + +fn altair_spec(altair_fork_epoch: Epoch) -> ChainSpec { + let mut spec = E::default_spec(); + spec.altair_fork_epoch = Some(altair_fork_epoch); + spec +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn sync_committee_duties_across_fork() { + let validator_count = E::sync_committee_size(); + let fork_epoch = Epoch::new(8); + let spec = altair_spec(fork_epoch); + let tester = InteractiveTester::::new(Some(spec.clone()), validator_count); + let harness = &tester.harness; + let client = &tester.client; + + let all_validators = harness.get_all_validators(); + let all_validators_u64 = all_validators.iter().map(|x| *x as u64).collect::>(); + + assert_eq!(harness.get_current_slot(), 0); + + // Prior to the fork the endpoint should return an empty vec. + let early_duties = client + .post_validator_duties_sync(fork_epoch - 1, &all_validators_u64) + .await + .unwrap() + .data; + assert!(early_duties.is_empty()); + + // If there's a skip slot at the fork slot, the endpoint should return duties, even + // though the head state hasn't transitioned yet. + let fork_slot = fork_epoch.start_slot(E::slots_per_epoch()); + let (genesis_state, genesis_state_root) = harness.get_current_state_and_root(); + let (_, state) = harness + .add_attested_block_at_slot( + fork_slot - 1, + genesis_state, + genesis_state_root, + &all_validators, + ) + .unwrap(); + + harness.advance_slot(); + assert_eq!(harness.get_current_slot(), fork_slot); + + let sync_duties = client + .post_validator_duties_sync(fork_epoch, &all_validators_u64) + .await + .unwrap() + .data; + assert_eq!(sync_duties.len(), E::sync_committee_size()); + + // After applying a block at the fork slot the duties should remain unchanged. + let state_root = state.canonical_root(); + harness + .add_attested_block_at_slot(fork_slot, state, state_root, &all_validators) + .unwrap(); + + assert_eq!( + client + .post_validator_duties_sync(fork_epoch, &all_validators_u64) + .await + .unwrap() + .data, + sync_duties + ); + + // Sync duties should also be available for the next period. + let current_period = fork_epoch.sync_committee_period(&spec).unwrap(); + let next_period_epoch = spec.epochs_per_sync_committee_period * (current_period + 1); + + let next_period_duties = client + .post_validator_duties_sync(next_period_epoch, &all_validators_u64) + .await + .unwrap() + .data; + assert_eq!(next_period_duties.len(), E::sync_committee_size()); + + // Sync duties should *not* be available for the period after the next period. + // We expect a 400 (bad request) response. + let next_next_period_epoch = spec.epochs_per_sync_committee_period * (current_period + 2); + assert_eq!( + client + .post_validator_duties_sync(next_next_period_epoch, &all_validators_u64) + .await + .unwrap_err() + .status() + .unwrap(), + 400 + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn attestations_across_fork_with_skip_slots() { + let validator_count = E::sync_committee_size(); + let fork_epoch = Epoch::new(8); + let spec = altair_spec(fork_epoch); + let tester = InteractiveTester::::new(Some(spec.clone()), validator_count); + let harness = &tester.harness; + let client = &tester.client; + + let all_validators = harness.get_all_validators(); + + let fork_slot = fork_epoch.start_slot(E::slots_per_epoch()); + let fork_state = harness + .chain + .state_at_slot(fork_slot, StateSkipConfig::WithStateRoots) + .unwrap(); + + harness.set_current_slot(fork_slot); + + let attestations = harness.make_attestations( + &all_validators, + &fork_state, + fork_state.canonical_root(), + (*fork_state.get_block_root(fork_slot - 1).unwrap()).into(), + fork_slot, + ); + + let unaggregated_attestations = attestations + .iter() + .flat_map(|(atts, _)| atts.iter().map(|(att, _)| att.clone())) + .collect::>(); + + assert!(!unaggregated_attestations.is_empty()); + client + .post_beacon_pool_attestations(&unaggregated_attestations) + .await + .unwrap(); + + let signed_aggregates = attestations + .into_iter() + .filter_map(|(_, op_aggregate)| op_aggregate) + .collect::>(); + assert!(!signed_aggregates.is_empty()); + + client + .post_validator_aggregate_and_proof(&signed_aggregates) + .await + .unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn sync_contributions_across_fork_with_skip_slots() { + let validator_count = E::sync_committee_size(); + let fork_epoch = Epoch::new(8); + let spec = altair_spec(fork_epoch); + let tester = InteractiveTester::::new(Some(spec.clone()), validator_count); + let harness = &tester.harness; + let client = &tester.client; + + let fork_slot = fork_epoch.start_slot(E::slots_per_epoch()); + let fork_state = harness + .chain + .state_at_slot(fork_slot, StateSkipConfig::WithStateRoots) + .unwrap(); + + harness.set_current_slot(fork_slot); + + let sync_messages = harness.make_sync_contributions( + &fork_state, + *fork_state.get_block_root(fork_slot - 1).unwrap(), + fork_slot, + RelativeSyncCommittee::Current, + ); + + let sync_committee_messages = sync_messages + .iter() + .flat_map(|(messages, _)| messages.iter().map(|(message, _subnet)| message.clone())) + .collect::>(); + assert!(!sync_committee_messages.is_empty()); + + client + .post_beacon_pool_sync_committee_signatures(&sync_committee_messages) + .await + .unwrap(); + + let signed_contributions = sync_messages + .into_iter() + .filter_map(|(_, op_aggregate)| op_aggregate) + .collect::>(); + assert!(!signed_contributions.is_empty()); + + client + .post_validator_contribution_and_proofs(&signed_contributions) + .await + .unwrap(); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn sync_committee_indices_across_fork() { + let validator_count = E::sync_committee_size(); + let fork_epoch = Epoch::new(8); + let spec = altair_spec(fork_epoch); + let tester = InteractiveTester::::new(Some(spec.clone()), validator_count); + let harness = &tester.harness; + let client = &tester.client; + + let all_validators = harness.get_all_validators(); + + // Flatten subcommittees into a single vec. + let flatten = |subcommittees: &[SyncSubcommittee]| -> Vec { + subcommittees + .iter() + .flat_map(|sub| sub.indices.iter().copied()) + .collect() + }; + + // Prior to the fork the `sync_committees` endpoint should return a 400 error. + assert_eq!( + client + .get_beacon_states_sync_committees(StateId::Slot(Slot::new(0)), None) + .await + .unwrap_err() + .status() + .unwrap(), + 400 + ); + assert_eq!( + client + .get_beacon_states_sync_committees(StateId::Head, Some(Epoch::new(0))) + .await + .unwrap_err() + .status() + .unwrap(), + 400 + ); + + // If there's a skip slot at the fork slot, the endpoint will return a 400 until a block is + // applied. + let fork_slot = fork_epoch.start_slot(E::slots_per_epoch()); + let (genesis_state, genesis_state_root) = harness.get_current_state_and_root(); + let (_, state) = harness + .add_attested_block_at_slot( + fork_slot - 1, + genesis_state, + genesis_state_root, + &all_validators, + ) + .unwrap(); + + harness.advance_slot(); + assert_eq!(harness.get_current_slot(), fork_slot); + + // Using the head state must fail. + assert_eq!( + client + .get_beacon_states_sync_committees(StateId::Head, Some(fork_epoch)) + .await + .unwrap_err() + .status() + .unwrap(), + 400 + ); + + // In theory we could do a state advance and make this work, but to keep things simple I've + // avoided doing that for now. + assert_eq!( + client + .get_beacon_states_sync_committees(StateId::Slot(fork_slot), None) + .await + .unwrap_err() + .status() + .unwrap(), + 400 + ); + + // Once the head is updated it should be useable for requests, including in the next sync + // committee period. + let state_root = state.canonical_root(); + harness + .add_attested_block_at_slot(fork_slot + 1, state, state_root, &all_validators) + .unwrap(); + + let current_period = fork_epoch.sync_committee_period(&spec).unwrap(); + let next_period_epoch = spec.epochs_per_sync_committee_period * (current_period + 1); + assert!(next_period_epoch > fork_epoch); + + for epoch in [ + None, + Some(fork_epoch), + Some(fork_epoch + 1), + Some(next_period_epoch), + Some(next_period_epoch + 1), + ] { + let committee = client + .get_beacon_states_sync_committees(StateId::Head, epoch) + .await + .unwrap() + .data; + assert_eq!(committee.validators.len(), E::sync_committee_size()); + + assert_eq!( + committee.validators, + flatten(&committee.validator_aggregates) + ); + } +} diff --git a/beacon_node/http_api/tests/main.rs b/beacon_node/http_api/tests/main.rs new file mode 100644 index 00000000000..d10725a0264 --- /dev/null +++ b/beacon_node/http_api/tests/main.rs @@ -0,0 +1,6 @@ +#![cfg(not(debug_assertions))] // Tests are too slow in debug. +#![recursion_limit = "256"] + +pub mod common; +pub mod fork_tests; +pub mod tests; diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index c8908b77c66..21e52b92bbf 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -1,6 +1,4 @@ -#![cfg(not(debug_assertions))] // Tests are too slow in debug. -#![recursion_limit = "256"] - +use crate::common::{create_api_server, ApiServer}; use beacon_chain::{ test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType}, BeaconChain, StateSkipConfig, WhenSlotSkipped, MAXIMUM_GOSSIP_CLOCK_DISPARITY, @@ -9,21 +7,14 @@ use environment::null_logger; use eth2::Error; use eth2::StatusCode; use eth2::{types::*, BeaconNodeHttpClient, Timeouts}; -use eth2_libp2p::discv5::enr::{CombinedKey, EnrBuilder}; -use eth2_libp2p::{ - rpc::methods::{MetaData, MetaDataV2}, - types::{EnrAttestationBitfield, EnrSyncCommitteeBitfield, SyncState}, - Enr, EnrExt, NetworkGlobals, PeerId, -}; +use eth2_libp2p::{Enr, EnrExt, PeerId}; use futures::stream::{Stream, StreamExt}; use futures::FutureExt; -use http_api::{Config, Context}; use network::NetworkMessage; use sensitive_url::SensitiveUrl; use slot_clock::SlotClock; use state_processing::per_slot_processing; use std::convert::TryInto; -use std::net::Ipv4Addr; use std::sync::Arc; use tokio::sync::{mpsc, oneshot}; use tokio::time::Duration; @@ -41,9 +32,6 @@ const VALIDATOR_COUNT: usize = SLOTS_PER_EPOCH as usize; const CHAIN_LENGTH: u64 = SLOTS_PER_EPOCH * 5 - 1; // Make `next_block` an epoch transition const JUSTIFIED_EPOCH: u64 = 4; const FINALIZED_EPOCH: u64 = 3; -const TCP_PORT: u16 = 42; -const UDP_PORT: u16 = 42; -const SEQ_NUMBER: u64 = 0; const EXTERNAL_ADDR: &str = "/ip4/0.0.0.0/tcp/9000"; /// Skipping the slots around the epoch boundary allows us to check that we're obtaining states @@ -74,9 +62,13 @@ struct ApiTester { impl ApiTester { pub fn new() -> Self { - let mut harness = BeaconChainHarness::new( + // This allows for testing voluntary exits without building out a massive chain. + let mut spec = E::default_spec(); + spec.shard_committee_period = 2; + + let harness = BeaconChainHarness::new( MainnetEthSpec, - None, + Some(spec), generate_deterministic_keypairs(VALIDATOR_COUNT), ); @@ -134,13 +126,7 @@ impl ApiTester { let proposer_slashing = harness.make_proposer_slashing(2); let voluntary_exit = harness.make_voluntary_exit(3, harness.chain.epoch().unwrap()); - // Changing this *after* the chain has been initialized is a bit cheeky, but it shouldn't - // cause issue. - // - // This allows for testing voluntary exits without building out a massive chain. - harness.chain.spec.shard_committee_period = 2; - - let chain = Arc::new(harness.chain); + let chain = harness.chain.clone(); assert_eq!( chain.head_info().unwrap().finalized_checkpoint.epoch, @@ -157,56 +143,18 @@ impl ApiTester { "precondition: justification" ); - let (network_tx, network_rx) = mpsc::unbounded_channel(); - let log = null_logger().unwrap(); - // Default metadata - let meta_data = MetaData::V2(MetaDataV2 { - seq_number: SEQ_NUMBER, - attnets: EnrAttestationBitfield::::default(), - syncnets: EnrSyncCommitteeBitfield::::default(), - }); - let enr_key = CombinedKey::generate_secp256k1(); - let enr = EnrBuilder::new("v4").build(&enr_key).unwrap(); - let enr_clone = enr.clone(); - let network_globals = NetworkGlobals::new(enr, TCP_PORT, UDP_PORT, meta_data, vec![], &log); - - let peer_id = PeerId::random(); - network_globals.peers.write().connect_ingoing( - &peer_id, - EXTERNAL_ADDR.parse().unwrap(), - None, - ); - - *network_globals.sync_state.write() = SyncState::Synced; - - let eth1_service = - eth1::Service::new(eth1::Config::default(), log.clone(), chain.spec.clone()); - - let context = Arc::new(Context { - config: Config { - enabled: true, - listen_addr: Ipv4Addr::new(127, 0, 0, 1), - listen_port: 0, - allow_origin: None, - serve_legacy_spec: true, - }, - chain: Some(chain.clone()), - network_tx: Some(network_tx), - network_globals: Some(Arc::new(network_globals)), - eth1_service: Some(eth1_service), - log, - }); - let ctx = context.clone(); - let (shutdown_tx, shutdown_rx) = oneshot::channel(); - let server_shutdown = async { - // It's not really interesting why this triggered, just that it happened. - let _ = shutdown_rx.await; - }; - let (listening_socket, server) = http_api::serve(ctx, server_shutdown).unwrap(); + let ApiServer { + server, + listening_socket, + shutdown_tx, + network_rx, + local_enr, + external_peer_id, + } = create_api_server(chain.clone(), log); - tokio::spawn(async { server.await }); + tokio::spawn(server); let client = BeaconNodeHttpClient::new( SensitiveUrl::parse(&format!( @@ -230,8 +178,8 @@ impl ApiTester { _server_shutdown: shutdown_tx, validator_keypairs: harness.validator_keypairs, network_rx, - local_enr: enr_clone, - external_peer_id: peer_id, + local_enr, + external_peer_id, } } @@ -271,58 +219,20 @@ impl ApiTester { let proposer_slashing = harness.make_proposer_slashing(2); let voluntary_exit = harness.make_voluntary_exit(3, harness.chain.epoch().unwrap()); - let chain = Arc::new(harness.chain); - - let (network_tx, network_rx) = mpsc::unbounded_channel(); + let chain = harness.chain.clone(); let log = null_logger().unwrap(); - // Default metadata - let meta_data = MetaData::V2(MetaDataV2 { - seq_number: SEQ_NUMBER, - attnets: EnrAttestationBitfield::::default(), - syncnets: EnrSyncCommitteeBitfield::::default(), - }); - let enr_key = CombinedKey::generate_secp256k1(); - let enr = EnrBuilder::new("v4").build(&enr_key).unwrap(); - let enr_clone = enr.clone(); - let network_globals = NetworkGlobals::new(enr, TCP_PORT, UDP_PORT, meta_data, vec![], &log); - - let peer_id = PeerId::random(); - network_globals.peers.write().connect_ingoing( - &peer_id, - EXTERNAL_ADDR.parse().unwrap(), - None, - ); - - *network_globals.sync_state.write() = SyncState::Synced; - - let eth1_service = - eth1::Service::new(eth1::Config::default(), log.clone(), chain.spec.clone()); - - let context = Arc::new(Context { - config: Config { - enabled: true, - listen_addr: Ipv4Addr::new(127, 0, 0, 1), - listen_port: 0, - allow_origin: None, - serve_legacy_spec: true, - }, - chain: Some(chain.clone()), - network_tx: Some(network_tx), - network_globals: Some(Arc::new(network_globals)), - eth1_service: Some(eth1_service), - log, - }); - let ctx = context.clone(); - let (shutdown_tx, shutdown_rx) = oneshot::channel(); - let server_shutdown = async { - // It's not really interesting why this triggered, just that it happened. - let _ = shutdown_rx.await; - }; - let (listening_socket, server) = http_api::serve(ctx, server_shutdown).unwrap(); + let ApiServer { + server, + listening_socket, + shutdown_tx, + network_rx, + local_enr, + external_peer_id, + } = create_api_server(chain.clone(), log); - tokio::spawn(async { server.await }); + tokio::spawn(server); let client = BeaconNodeHttpClient::new( SensitiveUrl::parse(&format!( @@ -346,8 +256,8 @@ impl ApiTester { _server_shutdown: shutdown_tx, validator_keypairs: harness.validator_keypairs, network_rx, - local_enr: enr_clone, - external_peer_id: peer_id, + local_enr, + external_peer_id, } } @@ -1011,13 +921,18 @@ impl ApiTester { } } - let json_result = self - .client - .get_beacon_blocks(block_id) - .await - .unwrap() - .map(|res| res.data); - assert_eq!(json_result, expected, "{:?}", block_id); + let json_result = self.client.get_beacon_blocks(block_id).await.unwrap(); + + if let (Some(json), Some(expected)) = (&json_result, &expected) { + assert_eq!(json.data, *expected, "{:?}", block_id); + assert_eq!( + json.version, + Some(expected.fork_name(&self.chain.spec).unwrap()) + ); + } else { + assert_eq!(json_result, None); + assert_eq!(expected, None); + } let ssz_result = self .client @@ -1025,6 +940,16 @@ impl ApiTester { .await .unwrap(); assert_eq!(ssz_result, expected, "{:?}", block_id); + + // Check that the legacy v1 API still works but doesn't return a version field. + let v1_result = self.client.get_beacon_blocks_v1(block_id).await.unwrap(); + if let (Some(v1_result), Some(expected)) = (&v1_result, &expected) { + assert_eq!(v1_result.version, None); + assert_eq!(v1_result.data, *expected); + } else { + assert_eq!(v1_result, None); + assert_eq!(expected, None); + } } self @@ -1443,23 +1368,44 @@ impl ApiTester { pub async fn test_get_debug_beacon_states(self) -> Self { for state_id in self.interesting_state_ids() { + let result_json = self.client.get_debug_beacon_states(state_id).await.unwrap(); + + let mut expected = self.get_state(state_id); + expected.as_mut().map(|state| state.drop_all_caches()); + + if let (Some(json), Some(expected)) = (&result_json, &expected) { + assert_eq!(json.data, *expected, "{:?}", state_id); + assert_eq!( + json.version, + Some(expected.fork_name(&self.chain.spec).unwrap()) + ); + } else { + assert_eq!(result_json, None); + assert_eq!(expected, None); + } + + // Check SSZ API. let result_ssz = self .client .get_debug_beacon_states_ssz(state_id, &self.chain.spec) .await .unwrap(); - let result_json = self + assert_eq!(result_ssz, expected, "{:?}", state_id); + + // Check legacy v1 API. + let result_v1 = self .client - .get_debug_beacon_states(state_id) + .get_debug_beacon_states_v1(state_id) .await - .unwrap() - .map(|res| res.data); - - let mut expected = self.get_state(state_id); - expected.as_mut().map(|state| state.drop_all_caches()); + .unwrap(); - assert_eq!(result_ssz, expected, "{:?}", state_id); - assert_eq!(result_json, expected, "{:?}", state_id); + if let (Some(json), Some(expected)) = (&result_v1, &expected) { + assert_eq!(json.version, None); + assert_eq!(json.data, *expected, "{:?}", state_id); + } else { + assert_eq!(result_v1, None); + assert_eq!(expected, None); + } } self diff --git a/beacon_node/network/src/beacon_processor/tests.rs b/beacon_node/network/src/beacon_processor/tests.rs index 30cc1724276..0f491527b27 100644 --- a/beacon_node/network/src/beacon_processor/tests.rs +++ b/beacon_node/network/src/beacon_processor/tests.rs @@ -23,8 +23,8 @@ use std::time::Duration; use tokio::runtime::Runtime; use tokio::sync::mpsc; use types::{ - test_utils::generate_deterministic_keypairs, Attestation, AttesterSlashing, MainnetEthSpec, - ProposerSlashing, SignedBeaconBlock, SignedVoluntaryExit, SubnetId, + test_utils::generate_deterministic_keypairs, Attestation, AttesterSlashing, EthSpec, + MainnetEthSpec, ProposerSlashing, SignedBeaconBlock, SignedVoluntaryExit, SubnetId, }; type E = MainnetEthSpec; @@ -71,9 +71,13 @@ impl Drop for TestRig { impl TestRig { pub fn new(chain_length: u64) -> Self { - let mut harness = BeaconChainHarness::new( + // This allows for testing voluntary exits without building out a massive chain. + let mut spec = E::default_spec(); + spec.shard_committee_period = 2; + + let harness = BeaconChainHarness::new( MainnetEthSpec, - None, + Some(spec), generate_deterministic_keypairs(VALIDATOR_COUNT), ); @@ -151,13 +155,7 @@ impl TestRig { let proposer_slashing = harness.make_proposer_slashing(2); let voluntary_exit = harness.make_voluntary_exit(3, harness.chain.epoch().unwrap()); - // Changing this *after* the chain has been initialized is a bit cheeky, but it shouldn't - // cause issue. - // - // This allows for testing voluntary exits without building out a massive chain. - harness.chain.spec.shard_committee_period = 2; - - let chain = Arc::new(harness.chain); + let chain = harness.chain; let (network_tx, _network_rx) = mpsc::unbounded_channel(); diff --git a/beacon_node/network/src/beacon_processor/worker/mod.rs b/beacon_node/network/src/beacon_processor/worker/mod.rs index ec2cce0ee6f..58fec22d5f5 100644 --- a/beacon_node/network/src/beacon_processor/worker/mod.rs +++ b/beacon_node/network/src/beacon_processor/worker/mod.rs @@ -27,8 +27,8 @@ impl Worker { /// Creates a log if there is an internal error. fn send_sync_message(&self, message: SyncMessage) { self.sync_tx.send(message).unwrap_or_else(|e| { - debug!(self.log, "Could not send message to the sync service, likely shutdown"; - "error" => %e) + debug!(self.log, "Could not send message to the sync service"; + "error" => %e) }); } diff --git a/beacon_node/network/src/service/tests.rs b/beacon_node/network/src/service/tests.rs index 02be935c1fc..db61d2a88cf 100644 --- a/beacon_node/network/src/service/tests.rs +++ b/beacon_node/network/src/service/tests.rs @@ -35,15 +35,13 @@ mod tests { fn test_dht_persistence() { let log = get_logger(false); - let beacon_chain = Arc::new( - BeaconChainHarness::new_with_store_config( - MinimalEthSpec, - None, - generate_deterministic_keypairs(8), - StoreConfig::default(), - ) - .chain, - ); + let beacon_chain = BeaconChainHarness::new_with_store_config( + MinimalEthSpec, + None, + generate_deterministic_keypairs(8), + StoreConfig::default(), + ) + .chain; let store = beacon_chain.store.clone(); diff --git a/boot_node/src/server.rs b/boot_node/src/server.rs index ed563504b0b..3cdd8789a3e 100644 --- a/boot_node/src/server.rs +++ b/boot_node/src/server.rs @@ -40,13 +40,22 @@ pub async fn run(config: BootNodeConfig, log: slog::Logger) { }; // construct the discv5 server - let mut discv5 = Discv5::new(config.local_enr, config.local_key, discv5_config).unwrap(); + let mut discv5 = + Discv5::new(config.local_enr.clone(), config.local_key, discv5_config).unwrap(); // If there are any bootnodes add them to the routing table for enr in config.boot_nodes { - info!(log, "Adding bootnode"; "address" => format!("{:?}", enr.udp_socket()), "peer_id" => enr.peer_id().to_string(), "node_id" => enr.node_id().to_string()); - if let Err(e) = discv5.add_enr(enr) { - slog::warn!(log, "Failed adding ENR"; "error" => e.to_string()); + info!( + log, + "Adding bootnode"; + "address" => ?enr.udp_socket(), + "peer_id" => enr.peer_id().to_string(), + "node_id" => enr.node_id().to_string() + ); + if enr != config.local_enr { + if let Err(e) = discv5.add_enr(enr) { + slog::warn!(log, "Failed adding ENR"; "error" => e.to_string()); + } } } diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 08b4bcaead8..528360cf41c 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -12,7 +12,7 @@ pub mod lighthouse; pub mod lighthouse_vc; pub mod types; -use self::types::*; +use self::types::{Error as ResponseError, *}; use eth2_libp2p::PeerId; use futures::Stream; use futures_util::StreamExt; @@ -26,6 +26,9 @@ use std::fmt; use std::iter::Iterator; use std::time::Duration; +pub const V1: EndpointVersion = EndpointVersion(1); +pub const V2: EndpointVersion = EndpointVersion(2); + #[derive(Debug)] pub enum Error { /// The `reqwest` client raised an error. @@ -86,6 +89,7 @@ pub struct Timeouts { pub liveness: Duration, pub proposal: Duration, pub proposer_duties: Duration, + pub sync_duties: Duration, } impl Timeouts { @@ -96,6 +100,7 @@ impl Timeouts { liveness: timeout, proposal: timeout, proposer_duties: timeout, + sync_duties: timeout, } } } @@ -142,14 +147,14 @@ impl BeaconNodeHttpClient { } } - /// Return the path with the standard `/eth1/v1` prefix applied. - fn eth_path(&self) -> Result { + /// Return the path with the standard `/eth/vX` prefix applied. + fn eth_path(&self, version: EndpointVersion) -> Result { let mut path = self.server.full.clone(); path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("eth") - .push("v1"); + .push(&version.to_string()); Ok(path) } @@ -315,7 +320,7 @@ impl BeaconNodeHttpClient { /// /// May return a `404` if beacon chain genesis has not yet occurred. pub async fn get_beacon_genesis(&self) -> Result, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -332,7 +337,7 @@ impl BeaconNodeHttpClient { &self, state_id: StateId, ) -> Result>, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -351,7 +356,7 @@ impl BeaconNodeHttpClient { &self, state_id: StateId, ) -> Result>, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -370,7 +375,7 @@ impl BeaconNodeHttpClient { &self, state_id: StateId, ) -> Result>, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -390,7 +395,7 @@ impl BeaconNodeHttpClient { state_id: StateId, ids: Option<&[ValidatorId]>, ) -> Result>>, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -420,7 +425,7 @@ impl BeaconNodeHttpClient { ids: Option<&[ValidatorId]>, statuses: Option<&[ValidatorStatus]>, ) -> Result>>, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -460,7 +465,7 @@ impl BeaconNodeHttpClient { index: Option, epoch: Option, ) -> Result>>, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -487,6 +492,29 @@ impl BeaconNodeHttpClient { self.get_opt(path).await } + /// `GET beacon/states/{state_id}/sync_committees?epoch` + pub async fn get_beacon_states_sync_committees( + &self, + state_id: StateId, + epoch: Option, + ) -> Result, Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("states") + .push(&state_id.to_string()) + .push("sync_committees"); + + if let Some(epoch) = epoch { + path.query_pairs_mut() + .append_pair("epoch", &epoch.to_string()); + } + + self.get(path).await + } + /// `GET beacon/states/{state_id}/validators/{validator_id}` /// /// Returns `Ok(None)` on a 404 error. @@ -495,7 +523,7 @@ impl BeaconNodeHttpClient { state_id: StateId, validator_id: &ValidatorId, ) -> Result>, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -516,7 +544,7 @@ impl BeaconNodeHttpClient { slot: Option, parent_root: Option, ) -> Result>>, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -543,7 +571,7 @@ impl BeaconNodeHttpClient { &self, block_id: BlockId, ) -> Result>, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -561,7 +589,7 @@ impl BeaconNodeHttpClient { &self, block: &SignedBeaconBlock, ) -> Result<(), Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -574,14 +602,32 @@ impl BeaconNodeHttpClient { Ok(()) } - /// `GET beacon/blocks` + /// `GET v2/beacon/blocks` /// /// Returns `Ok(None)` on a 404 error. pub async fn get_beacon_blocks( &self, block_id: BlockId, - ) -> Result>>, Error> { - let mut path = self.eth_path()?; + ) -> Result>>, Error> { + let mut path = self.eth_path(V2)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("blocks") + .push(&block_id.to_string()); + + self.get_opt(path).await + } + + /// `GET v1/beacon/blocks` (LEGACY) + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn get_beacon_blocks_v1( + &self, + block_id: BlockId, + ) -> Result>>, Error> { + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -600,7 +646,7 @@ impl BeaconNodeHttpClient { block_id: BlockId, spec: &ChainSpec, ) -> Result>, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V2)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -621,7 +667,7 @@ impl BeaconNodeHttpClient { &self, block_id: BlockId, ) -> Result>, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -640,7 +686,7 @@ impl BeaconNodeHttpClient { &self, block_id: BlockId, ) -> Result>>>, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -657,7 +703,7 @@ impl BeaconNodeHttpClient { &self, attestations: &[Attestation], ) -> Result<(), Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -665,15 +711,8 @@ impl BeaconNodeHttpClient { .push("pool") .push("attestations"); - let response = self - .client - .post(path) - .timeout(self.timeouts.attestation) - .json(attestations) - .send() - .await - .map_err(Error::Reqwest)?; - ok_or_indexed_error(response).await?; + self.post_with_timeout(path, &attestations, self.timeouts.attestation) + .await?; Ok(()) } @@ -684,7 +723,7 @@ impl BeaconNodeHttpClient { slot: Option, committee_index: Option, ) -> Result>>, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -710,7 +749,7 @@ impl BeaconNodeHttpClient { &self, slashing: &AttesterSlashing, ) -> Result<(), Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -727,7 +766,7 @@ impl BeaconNodeHttpClient { pub async fn get_beacon_pool_attester_slashings( &self, ) -> Result>>, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -743,7 +782,7 @@ impl BeaconNodeHttpClient { &self, slashing: &ProposerSlashing, ) -> Result<(), Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -760,7 +799,7 @@ impl BeaconNodeHttpClient { pub async fn get_beacon_pool_proposer_slashings( &self, ) -> Result>, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -776,7 +815,7 @@ impl BeaconNodeHttpClient { &self, exit: &SignedVoluntaryExit, ) -> Result<(), Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -793,7 +832,7 @@ impl BeaconNodeHttpClient { pub async fn get_beacon_pool_voluntary_exits( &self, ) -> Result>, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -804,9 +843,44 @@ impl BeaconNodeHttpClient { self.get(path).await } + /// `POST beacon/pool/sync_committees` + pub async fn post_beacon_pool_sync_committee_signatures( + &self, + signatures: &[SyncCommitteeMessage], + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("pool") + .push("sync_committees"); + + self.post(path, &signatures).await?; + + Ok(()) + } + + /// `POST validator/contribution_and_proofs` + pub async fn post_validator_contribution_and_proofs( + &self, + signed_contributions: &[SignedContributionAndProof], + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("contribution_and_proofs"); + + self.post(path, &signed_contributions).await?; + + Ok(()) + } + /// `GET config/fork_schedule` pub async fn get_config_fork_schedule(&self) -> Result>, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -818,7 +892,7 @@ impl BeaconNodeHttpClient { /// `GET config/spec` pub async fn get_config_spec(&self) -> Result, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -832,7 +906,7 @@ impl BeaconNodeHttpClient { pub async fn get_config_deposit_contract( &self, ) -> Result, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -844,7 +918,7 @@ impl BeaconNodeHttpClient { /// `GET node/version` pub async fn get_node_version(&self) -> Result, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -856,7 +930,7 @@ impl BeaconNodeHttpClient { /// `GET node/identity` pub async fn get_node_identity(&self) -> Result, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -868,7 +942,7 @@ impl BeaconNodeHttpClient { /// `GET node/syncing` pub async fn get_node_syncing(&self) -> Result, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -880,7 +954,7 @@ impl BeaconNodeHttpClient { /// `GET node/health` pub async fn get_node_health(&self) -> Result { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -906,7 +980,7 @@ impl BeaconNodeHttpClient { &self, peer_id: PeerId, ) -> Result, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -923,7 +997,7 @@ impl BeaconNodeHttpClient { states: Option<&[PeerState]>, directions: Option<&[PeerDirection]>, ) -> Result { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -953,7 +1027,7 @@ impl BeaconNodeHttpClient { /// `GET node/peer_count` pub async fn get_node_peer_count(&self) -> Result, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -963,12 +1037,29 @@ impl BeaconNodeHttpClient { self.get(path).await } - /// `GET debug/beacon/states/{state_id}` + /// `GET v2/debug/beacon/states/{state_id}` pub async fn get_debug_beacon_states( &self, state_id: StateId, - ) -> Result>>, Error> { - let mut path = self.eth_path()?; + ) -> Result>>, Error> { + let mut path = self.eth_path(V2)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("debug") + .push("beacon") + .push("states") + .push(&state_id.to_string()); + + self.get_opt(path).await + } + + /// `GET v1/debug/beacon/states/{state_id}` (LEGACY) + pub async fn get_debug_beacon_states_v1( + &self, + state_id: StateId, + ) -> Result>>, Error> { + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -987,7 +1078,7 @@ impl BeaconNodeHttpClient { state_id: StateId, spec: &ChainSpec, ) -> Result>, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -1006,7 +1097,7 @@ impl BeaconNodeHttpClient { pub async fn get_debug_beacon_heads( &self, ) -> Result>, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -1022,7 +1113,7 @@ impl BeaconNodeHttpClient { &self, epoch: Epoch, ) -> Result>, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -1035,14 +1126,14 @@ impl BeaconNodeHttpClient { .await } - /// `GET validator/blocks/{slot}` + /// `GET v2/validator/blocks/{slot}` pub async fn get_validator_blocks( &self, slot: Slot, randao_reveal: &SignatureBytes, graffiti: Option<&Graffiti>, - ) -> Result>, Error> { - let mut path = self.eth_path()?; + ) -> Result>, Error> { + let mut path = self.eth_path(V2)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -1067,7 +1158,7 @@ impl BeaconNodeHttpClient { slot: Slot, committee_index: CommitteeIndex, ) -> Result, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -1087,7 +1178,7 @@ impl BeaconNodeHttpClient { slot: Slot, attestation_data_root: Hash256, ) -> Result>>, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -1105,6 +1196,32 @@ impl BeaconNodeHttpClient { .await } + /// `GET validator/sync_committee_contribution` + pub async fn get_validator_sync_committee_contribution( + &self, + sync_committee_data: &SyncContributionData, + ) -> Result>>, Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("sync_committee_contribution"); + + path.query_pairs_mut() + .append_pair("slot", &sync_committee_data.slot.to_string()) + .append_pair( + "beacon_block_root", + &format!("{:?}", sync_committee_data.beacon_block_root), + ) + .append_pair( + "subcommittee_index", + &sync_committee_data.subcommittee_index.to_string(), + ); + + self.get_opt(path).await + } + /// `POST lighthouse/liveness` pub async fn post_lighthouse_liveness( &self, @@ -1135,7 +1252,7 @@ impl BeaconNodeHttpClient { epoch: Epoch, indices: &[u64], ) -> Result>, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -1153,22 +1270,15 @@ impl BeaconNodeHttpClient { &self, aggregates: &[SignedAggregateAndProof], ) -> Result<(), Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("validator") .push("aggregate_and_proofs"); - let response = self - .client - .post(path) - .timeout(self.timeouts.attestation) - .json(aggregates) - .send() - .await - .map_err(Error::Reqwest)?; - ok_or_indexed_error(response).await?; + self.post_with_timeout(path, &aggregates, self.timeouts.attestation) + .await?; Ok(()) } @@ -1178,7 +1288,7 @@ impl BeaconNodeHttpClient { &self, subscriptions: &[BeaconCommitteeSubscription], ) -> Result<(), Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? @@ -1190,12 +1300,29 @@ impl BeaconNodeHttpClient { Ok(()) } + /// `POST validator/sync_committee_subscriptions` + pub async fn post_validator_sync_committee_subscriptions( + &self, + subscriptions: &[SyncCommitteeSubscription], + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("sync_committee_subscriptions"); + + self.post(path, &subscriptions).await?; + + Ok(()) + } + /// `GET events?topics` pub async fn get_events( &self, topic: &[EventTopic], ) -> Result, Error>>, Error> { - let mut path = self.eth_path()?; + let mut path = self.eth_path(V1)?; path.path_segments_mut() .map_err(|()| Error::InvalidUrl(self.server.clone()))? .push("events"); @@ -1219,31 +1346,39 @@ impl BeaconNodeHttpClient { Err(e) => Err(Error::Reqwest(e)), })) } -} -/// Returns `Ok(response)` if the response is a `200 OK` response. Otherwise, creates an -/// appropriate error message. -async fn ok_or_error(response: Response) -> Result { - let status = response.status(); + /// `POST validator/duties/sync/{epoch}` + pub async fn post_validator_duties_sync( + &self, + epoch: Epoch, + indices: &[u64], + ) -> Result>, Error> { + let mut path = self.eth_path(V1)?; - if status == StatusCode::OK { - Ok(response) - } else if let Ok(message) = response.json().await { - Err(Error::ServerMessage(message)) - } else { - Err(Error::StatusCode(status)) + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("duties") + .push("sync") + .push(&epoch.to_string()); + + self.post_with_timeout_and_response(path, &indices, self.timeouts.sync_duties) + .await } } /// Returns `Ok(response)` if the response is a `200 OK` response. Otherwise, creates an -/// appropriate indexed error message. -async fn ok_or_indexed_error(response: Response) -> Result { +/// appropriate error message. +async fn ok_or_error(response: Response) -> Result { let status = response.status(); if status == StatusCode::OK { Ok(response) } else if let Ok(message) = response.json().await { - Err(Error::ServerIndexedMessage(message)) + match message { + ResponseError::Message(message) => Err(Error::ServerMessage(message)), + ResponseError::Indexed(indexed) => Err(Error::ServerIndexedMessage(indexed)), + } } else { Err(Error::StatusCode(status)) } diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index 6b43e785757..9a9f09ad66f 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -10,6 +10,14 @@ use std::fmt; use std::str::{from_utf8, FromStr}; pub use types::*; +/// An API error serializable to JSON. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Error { + Indexed(IndexedErrorMessage), + Message(ErrorMessage), +} + /// An API error serializable to JSON. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ErrorMessage { @@ -43,6 +51,30 @@ impl Failure { } } +/// The version of a single API endpoint, e.g. the `v1` in `/eth/v1/beacon/blocks`. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct EndpointVersion(pub u64); + +impl FromStr for EndpointVersion { + type Err = (); + + fn from_str(s: &str) -> Result { + if let Some(version_str) = s.strip_prefix('v') { + u64::from_str(version_str) + .map(EndpointVersion) + .map_err(|_| ()) + } else { + Err(()) + } + } +} + +impl std::fmt::Display for EndpointVersion { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(fmt, "v{}", self.0) + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct GenesisData { #[serde(with = "serde_utils::quoted_u64")] @@ -179,6 +211,14 @@ impl<'a, T: Serialize> From<&'a T> for GenericResponseRef<'a, T> { } } +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +// #[serde(bound = "T: Serialize + serde::de::DeserializeOwned")] +pub struct ForkVersionedResponse { + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, + pub data: T, +} + #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct RootData { pub root: Hash256, @@ -378,6 +418,11 @@ pub struct CommitteesQuery { pub epoch: Option, } +#[derive(Serialize, Deserialize)] +pub struct SyncCommitteesQuery { + pub epoch: Option, +} + #[derive(Serialize, Deserialize)] pub struct AttestationPoolQuery { pub slot: Option, @@ -399,6 +444,20 @@ pub struct CommitteeData { pub validators: Vec, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SyncCommitteeByValidatorIndices { + #[serde(with = "serde_utils::quoted_u64_vec")] + pub validators: Vec, + pub validator_aggregates: Vec, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct SyncSubcommittee { + #[serde(with = "serde_utils::quoted_u64_vec")] + pub indices: Vec, +} + #[derive(Serialize, Deserialize)] pub struct HeadersQuery { pub slot: Option, diff --git a/common/rest_types/Cargo.toml b/common/rest_types/Cargo.toml deleted file mode 100644 index 3d4c70c1c33..00000000000 --- a/common/rest_types/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "rest_types" -version = "0.2.0" -authors = ["Sigma Prime "] -edition = "2018" - -[dependencies] -types = { path = "../../consensus/types" } -eth2_ssz_derive = "0.1.0" -eth2_ssz = "0.1.2" -eth2_hashing = "0.1.0" -tree_hash = "0.1.0" -state_processing = { path = "../../consensus/state_processing" } -bls = { path = "../../crypto/bls" } -serde = { version = "1.0.110", features = ["derive"] } -rayon = "1.3.0" -hyper = "0.14.4" -tokio = { version = "1.1.0", features = ["sync"] } -environment = { path = "../../lighthouse/environment" } -store = { path = "../../beacon_node/store" } -beacon_chain = { path = "../../beacon_node/beacon_chain" } -serde_json = "1.0.52" -serde_yaml = "0.8.11" - -[target.'cfg(target_os = "linux")'.dependencies] -psutil = "3.1.0" -procinfo = "0.4.2" diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index 6f3d18d2802..cf420e01aa9 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -50,6 +50,7 @@ superstruct = "0.2.0" serde_json = "1.0.58" criterion = "0.3.3" beacon_chain = { path = "../../beacon_node/beacon_chain" } +eth2_interop_keypairs = { path = "../../common/eth2_interop_keypairs" } [features] default = ["sqlite", "legacy-arith"] diff --git a/consensus/types/src/beacon_block.rs b/consensus/types/src/beacon_block.rs index 0de0683f2d2..27bed6f4973 100644 --- a/consensus/types/src/beacon_block.rs +++ b/consensus/types/src/beacon_block.rs @@ -149,6 +149,27 @@ impl BeaconBlock { } impl<'a, T: EthSpec> BeaconBlockRef<'a, T> { + /// Returns the name of the fork pertaining to `self`. + /// + /// Will return an `Err` if `self` has been instantiated to a variant conflicting with the fork + /// dictated by `self.slot()`. + pub fn fork_name(&self, spec: &ChainSpec) -> Result { + let fork_at_slot = spec.fork_name_at_slot::(self.slot()); + let object_fork = match self { + BeaconBlockRef::Base { .. } => ForkName::Base, + BeaconBlockRef::Altair { .. } => ForkName::Altair, + }; + + if fork_at_slot == object_fork { + Ok(object_fork) + } else { + Err(InconsistentFork { + fork_at_slot, + object_fork, + }) + } + } + /// Convenience accessor for the `body` as a `BeaconBlockBodyRef`. pub fn body(&self) -> BeaconBlockBodyRef<'a, T> { match self { diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 54437969ca9..e62d53a1a2e 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -837,6 +837,32 @@ impl BeaconState { }) } + /// Get the sync committee duties for a list of validator indices. + /// + /// Will return a `SyncCommitteeNotKnown` error if the `epoch` is out of bounds with respect + /// to the current or next sync committee periods. + pub fn get_sync_committee_duties( + &self, + epoch: Epoch, + validator_indices: &[u64], + spec: &ChainSpec, + ) -> Result>, Error> { + let sync_committee = self.get_built_sync_committee(epoch, spec)?; + + validator_indices + .iter() + .map(|&validator_index| { + let pubkey = self.get_validator(validator_index as usize)?.pubkey; + + Ok(SyncDuty::from_sync_committee( + validator_index, + pubkey, + sync_committee, + )) + }) + .collect() + } + /// Get the canonical root of the `latest_block_header`, filling in its state root if necessary. /// /// It needs filling in on all slots where there isn't a skip. diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index 043007decdf..14320f871e4 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -685,6 +685,8 @@ where #[cfg(test)] mod tests { use super::*; + use itertools::Itertools; + use safe_arith::SafeArith; #[test] fn test_mainnet_spec_can_be_constructed() { @@ -745,6 +747,33 @@ mod tests { } } } + + // Test that `next_fork_epoch` is consistent with the other functions. + #[test] + fn next_fork_epoch_consistency() { + type E = MainnetEthSpec; + let spec = ChainSpec::mainnet(); + + let mut last_fork_slot = Slot::new(0); + + for (_, fork) in ForkName::list_all().into_iter().tuple_windows() { + if let Some(fork_epoch) = spec.fork_epoch(fork) { + last_fork_slot = fork_epoch.start_slot(E::slots_per_epoch()); + + // Fork is activated at non-zero epoch: check that `next_fork_epoch` returns + // the correct result. + if let Ok(prior_slot) = last_fork_slot.safe_sub(1) { + let (next_fork, next_fork_epoch) = + spec.next_fork_epoch::(prior_slot).unwrap(); + assert_eq!(fork, next_fork); + assert_eq!(spec.fork_epoch(fork).unwrap(), next_fork_epoch); + } + } else { + // Fork is not activated, check that `next_fork_epoch` returns `None`. + assert_eq!(spec.next_fork_epoch::(last_fork_slot), None); + } + } + } } #[cfg(test)] diff --git a/consensus/types/src/fork_name.rs b/consensus/types/src/fork_name.rs index 4941073b67b..a464a0ed9ad 100644 --- a/consensus/types/src/fork_name.rs +++ b/consensus/types/src/fork_name.rs @@ -1,6 +1,12 @@ use crate::{ChainSpec, Epoch}; +use serde_derive::{Deserialize, Serialize}; +use std::convert::TryFrom; +use std::fmt::{self, Display, Formatter}; +use std::str::FromStr; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(try_from = "String")] +#[serde(into = "String")] pub enum ForkName { Base, Altair, @@ -48,7 +54,7 @@ impl ForkName { } } -impl std::str::FromStr for ForkName { +impl FromStr for ForkName { type Err = (); fn from_str(fork_name: &str) -> Result { @@ -60,6 +66,29 @@ impl std::str::FromStr for ForkName { } } +impl Display for ForkName { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { + match self { + ForkName::Base => "phase0".fmt(f), + ForkName::Altair => "altair".fmt(f), + } + } +} + +impl From for String { + fn from(fork: ForkName) -> String { + fork.to_string() + } +} + +impl TryFrom for ForkName { + type Error = String; + + fn try_from(s: String) -> Result { + Self::from_str(&s).map_err(|()| format!("Invalid fork name: {}", s)) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct InconsistentFork { pub fork_at_slot: ForkName, diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index b8876ccf802..403544f007c 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -56,6 +56,7 @@ pub mod signed_contribution_and_proof; pub mod signed_voluntary_exit; pub mod signing_data; pub mod sync_committee_subscription; +pub mod sync_duty; pub mod validator; pub mod validator_subscription; pub mod voluntary_exit; @@ -137,9 +138,10 @@ pub use crate::subnet_id::SubnetId; pub use crate::sync_aggregate::SyncAggregate; pub use crate::sync_aggregator_selection_data::SyncAggregatorSelectionData; pub use crate::sync_committee::SyncCommittee; -pub use crate::sync_committee_contribution::SyncCommitteeContribution; +pub use crate::sync_committee_contribution::{SyncCommitteeContribution, SyncContributionData}; pub use crate::sync_committee_message::SyncCommitteeMessage; pub use crate::sync_committee_subscription::SyncCommitteeSubscription; +pub use crate::sync_duty::SyncDuty; pub use crate::sync_selection_proof::SyncSelectionProof; pub use crate::sync_subnet_id::SyncSubnetId; pub use crate::validator::Validator; diff --git a/consensus/types/src/signed_beacon_block.rs b/consensus/types/src/signed_beacon_block.rs index 1a9e93b8e2d..14c1ffb8cfe 100644 --- a/consensus/types/src/signed_beacon_block.rs +++ b/consensus/types/src/signed_beacon_block.rs @@ -71,20 +71,7 @@ impl SignedBeaconBlock { /// Will return an `Err` if `self` has been instantiated to a variant conflicting with the fork /// dictated by `self.slot()`. pub fn fork_name(&self, spec: &ChainSpec) -> Result { - let fork_at_slot = spec.fork_name_at_slot::(self.slot()); - let object_fork = match self { - SignedBeaconBlock::Base { .. } => ForkName::Base, - SignedBeaconBlock::Altair { .. } => ForkName::Altair, - }; - - if fork_at_slot == object_fork { - Ok(object_fork) - } else { - Err(InconsistentFork { - fork_at_slot, - object_fork, - }) - } + self.message().fork_name(spec) } /// SSZ decode. diff --git a/consensus/types/src/sync_committee_contribution.rs b/consensus/types/src/sync_committee_contribution.rs index a2934090be6..c8fce78a8bb 100644 --- a/consensus/types/src/sync_committee_contribution.rs +++ b/consensus/types/src/sync_committee_contribution.rs @@ -77,9 +77,9 @@ impl SignedRoot for Hash256 {} /// This is not in the spec, but useful for determining uniqueness of sync committee contributions #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)] pub struct SyncContributionData { - slot: Slot, - beacon_block_root: Hash256, - subcommittee_index: u64, + pub slot: Slot, + pub beacon_block_root: Hash256, + pub subcommittee_index: u64, } impl SyncContributionData { diff --git a/consensus/types/src/sync_duty.rs b/consensus/types/src/sync_duty.rs new file mode 100644 index 00000000000..e3ffe62bfd1 --- /dev/null +++ b/consensus/types/src/sync_duty.rs @@ -0,0 +1,83 @@ +use crate::{EthSpec, SyncCommittee, SyncSubnetId}; +use bls::PublicKeyBytes; +use safe_arith::ArithError; +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashSet; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct SyncDuty { + pub pubkey: PublicKeyBytes, + #[serde(with = "serde_utils::quoted_u64")] + pub validator_index: u64, + #[serde(with = "serde_utils::quoted_u64_vec")] + pub validator_sync_committee_indices: Vec, +} + +impl SyncDuty { + /// Create a new `SyncDuty` from the list of validator indices in a sync committee. + pub fn from_sync_committee_indices( + validator_index: u64, + pubkey: PublicKeyBytes, + sync_committee_indices: &[usize], + ) -> Option { + // Positions of the `validator_index` within the committee. + let validator_sync_committee_indices = sync_committee_indices + .iter() + .enumerate() + .filter_map(|(i, &v)| { + if validator_index == v as u64 { + Some(i as u64) + } else { + None + } + }) + .collect(); + Self::new(validator_index, pubkey, validator_sync_committee_indices) + } + + /// Create a new `SyncDuty` from a `SyncCommittee`, which contains the pubkeys but not the + /// indices. + pub fn from_sync_committee( + validator_index: u64, + pubkey: PublicKeyBytes, + sync_committee: &SyncCommittee, + ) -> Option { + let validator_sync_committee_indices = sync_committee + .pubkeys + .iter() + .enumerate() + .filter_map(|(i, committee_pubkey)| { + if &pubkey == committee_pubkey { + Some(i as u64) + } else { + None + } + }) + .collect(); + Self::new(validator_index, pubkey, validator_sync_committee_indices) + } + + /// Create a duty if the `validator_sync_committee_indices` is non-empty. + fn new( + validator_index: u64, + pubkey: PublicKeyBytes, + validator_sync_committee_indices: Vec, + ) -> Option { + if !validator_sync_committee_indices.is_empty() { + Some(SyncDuty { + pubkey, + validator_index, + validator_sync_committee_indices, + }) + } else { + None + } + } + + /// Get the set of subnet IDs for this duty. + pub fn subnet_ids(&self) -> Result, ArithError> { + SyncSubnetId::compute_subnets_for_sync_committee::( + &self.validator_sync_committee_indices, + ) + } +} diff --git a/lcli/src/main.rs b/lcli/src/main.rs index adc0e041026..7a7b04d77f1 100644 --- a/lcli/src/main.rs +++ b/lcli/src/main.rs @@ -41,7 +41,8 @@ fn main() { .takes_value(true) .required(true) .possible_values(&["minimal", "mainnet"]) - .default_value("mainnet"), + .default_value("mainnet") + .global(true), ) .arg( Arg::with_name("testnet-dir") diff --git a/scripts/local_testnet/beacon_node.sh b/scripts/local_testnet/beacon_node.sh index bb75da8d3a9..5c286e31978 100755 --- a/scripts/local_testnet/beacon_node.sh +++ b/scripts/local_testnet/beacon_node.sh @@ -20,4 +20,5 @@ exec lighthouse \ --enr-udp-port $2 \ --enr-tcp-port $2 \ --port $2 \ - --http-port $3 + --http-port $3 \ + --target-peers $((NODE_COUNT - 1)) diff --git a/scripts/local_testnet/setup.sh b/scripts/local_testnet/setup.sh index a171fb1b085..edc19e02426 100755 --- a/scripts/local_testnet/setup.sh +++ b/scripts/local_testnet/setup.sh @@ -9,6 +9,8 @@ # $VALIDATOR_COUNT and $NODE_COUNT variables. # +set -o nounset -o errexit -o pipefail + source ./vars.env lcli \ @@ -20,10 +22,9 @@ lcli \ NOW=`date +%s` GENESIS_TIME=`expr $NOW + $GENESIS_DELAY` - lcli \ - --spec mainnet \ new-testnet \ + --spec $SPEC_PRESET \ --deposit-contract-address $DEPOSIT_CONTRACT_ADDRESS \ --testnet-dir $TESTNET_DIR \ --min-genesis-active-validator-count $GENESIS_VALIDATOR_COUNT \ @@ -50,8 +51,8 @@ echo Validators generated with keystore passwords at $DATADIR. echo "Building genesis state... (this might take a while)" lcli \ - --spec mainnet \ interop-genesis \ + --spec $SPEC_PRESET \ --genesis-time $GENESIS_TIME \ --testnet-dir $TESTNET_DIR \ $GENESIS_VALIDATOR_COUNT diff --git a/scripts/local_testnet/vars.env b/scripts/local_testnet/vars.env index 5c2ed22bd67..293a46d58d9 100644 --- a/scripts/local_testnet/vars.env +++ b/scripts/local_testnet/vars.env @@ -29,6 +29,9 @@ NETWORK_ID=4242 # Hard fork configuration ALTAIR_FORK_EPOCH=18446744073709551615 +# Spec version (mainnet or minimal) +SPEC_PRESET=mainnet + # Seconds per Eth2 slot SECONDS_PER_SLOT=3 diff --git a/scripts/tests/vars.env b/scripts/tests/vars.env index 7e11393035b..43dad52e229 100644 --- a/scripts/tests/vars.env +++ b/scripts/tests/vars.env @@ -35,5 +35,8 @@ SECONDS_PER_SLOT=3 # Seconds per Eth1 block SECONDS_PER_ETH1_BLOCK=1 +# Spec preset +SPEC_PRESET=mainnet + # Enable doppelganger detection VC_ARGS=" --enable-doppelganger-protection " diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index 801a0954b03..0e259340212 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -64,6 +64,7 @@ scrypt = { version = "0.5.0", default-features = false } lighthouse_metrics = { path = "../common/lighthouse_metrics" } lazy_static = "1.4.0" fallback = { path = "../common/fallback" } +itertools = "0.10.0" monitoring_api = { path = "../common/monitoring_api" } sensitive_url = { path = "../common/sensitive_url" } task_executor = { path = "../common/task_executor" } diff --git a/validator_client/src/attestation_service.rs b/validator_client/src/attestation_service.rs index 50f127db5bf..841a12574be 100644 --- a/validator_client/src/attestation_service.rs +++ b/validator_client/src/attestation_service.rs @@ -18,7 +18,7 @@ use types::{ }; /// Builds an `AttestationService`. -pub struct AttestationServiceBuilder { +pub struct AttestationServiceBuilder { duties_service: Option>>, validator_store: Option>>, slot_clock: Option, diff --git a/validator_client/src/duties_service.rs b/validator_client/src/duties_service.rs index 1a667d1cb29..6e5e5a546d0 100644 --- a/validator_client/src/duties_service.rs +++ b/validator_client/src/duties_service.rs @@ -6,6 +6,8 @@ //! The `DutiesService` is also responsible for sending events to the `BlockService` which trigger //! block production. +mod sync; + use crate::beacon_node_fallback::{BeaconNodeFallback, RequireSynced}; use crate::{ block_service::BlockServiceNotification, @@ -20,6 +22,8 @@ use slog::{debug, error, info, warn, Logger}; use slot_clock::SlotClock; use std::collections::{HashMap, HashSet}; use std::sync::Arc; +use sync::poll_sync_committee_duties; +use sync::SyncDutiesMap; use tokio::{sync::mpsc::Sender, time::sleep}; use types::{ChainSpec, Epoch, EthSpec, Hash256, PublicKeyBytes, SelectionProof, Slot}; @@ -40,6 +44,14 @@ pub enum Error { FailedToDownloadAttesters(String), FailedToProduceSelectionProof(ValidatorStoreError), InvalidModulo(ArithError), + Arith(ArithError), + SyncDutiesNotFound(u64), +} + +impl From for Error { + fn from(e: ArithError) -> Self { + Self::Arith(e) + } } /// Neatly joins the server-generated `AttesterData` with the locally-generated `selection_proof`. @@ -94,6 +106,8 @@ pub struct DutiesService { /// Maps an epoch to all *local* proposers in this epoch. Notably, this does not contain /// proposals for any validators which are not registered locally. pub proposers: RwLock, + /// Map from validator index to sync committee duties. + pub sync_duties: SyncDutiesMap, /// Provides the canonical list of locally-managed validators. pub validator_store: Arc>, /// Tracks the current slot. @@ -302,6 +316,37 @@ pub fn start_update_service( }, "duties_service_attesters", ); + + // Spawn the task which keeps track of local sync committee duties. + let duties_service = core_duties_service.clone(); + let log = core_duties_service.context.log().clone(); + core_duties_service.context.executor.spawn( + async move { + loop { + if let Err(e) = poll_sync_committee_duties(&duties_service).await { + error!( + log, + "Failed to poll sync committee duties"; + "error" => ?e + ); + } + + // Wait until the next slot before polling again. + // This doesn't mean that the beacon node will get polled every slot + // as the sync duties service will return early if it deems it already has + // enough information. + if let Some(duration) = duties_service.slot_clock.duration_to_next_slot() { + sleep(duration).await; + } else { + // Just sleep for one slot if we are unable to read the system clock, this gives + // us an opportunity for the clock to eventually come good. + sleep(duties_service.slot_clock.slot_duration()).await; + continue; + } + } + }, + "duties_service_sync_committee", + ); } /// Iterate through all the voting pubkeys in the `ValidatorStore` and attempt to learn any unknown diff --git a/validator_client/src/duties_service/sync.rs b/validator_client/src/duties_service/sync.rs new file mode 100644 index 00000000000..3608c17cbc1 --- /dev/null +++ b/validator_client/src/duties_service/sync.rs @@ -0,0 +1,599 @@ +use crate::{ + doppelganger_service::DoppelgangerStatus, + duties_service::{DutiesService, Error}, +}; +use itertools::Itertools; +use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; +use slog::{crit, debug, info, warn}; +use slot_clock::SlotClock; +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; +use types::{ + ChainSpec, Epoch, EthSpec, PublicKeyBytes, Slot, SyncDuty, SyncSelectionProof, SyncSubnetId, +}; + +/// Number of epochs in advance to compute selection proofs. +pub const AGGREGATION_PRE_COMPUTE_EPOCHS: u64 = 2; + +/// Top-level data-structure containing sync duty information. +/// +/// This data is structured as a series of nested `HashMap`s wrapped in `RwLock`s. Fine-grained +/// locking is used to provide maximum concurrency for the different services reading and writing. +/// +/// Deadlocks are prevented by: +/// +/// 1. Hierarchical locking. It is impossible to lock an inner lock (e.g. `validators`) without +/// first locking its parent. +/// 2. One-at-a-time locking. For the innermost locks on the aggregator duties, all of the functions +/// in this file take care to only lock one validator at a time. We never hold a lock while +/// trying to obtain another one (hence no lock ordering issues). +pub struct SyncDutiesMap { + /// Map from sync committee period to duties for members of that sync committee. + committees: RwLock>, +} + +/// Duties for a single sync committee period. +#[derive(Default)] +pub struct CommitteeDuties { + /// Map from validator index to validator duties. + /// + /// A `None` value indicates that the validator index is known *not* to be a member of the sync + /// committee, while a `Some` indicates a known member. An absent value indicates that the + /// validator index was not part of the set of local validators when the duties were fetched. + /// This allows us to track changes to the set of local validators. + validators: RwLock>>, +} + +/// Duties for a single validator. +pub struct ValidatorDuties { + /// The sync duty: including validator sync committee indices & pubkey. + duty: SyncDuty, + /// The aggregator duties: cached selection proofs for upcoming epochs. + aggregation_duties: AggregatorDuties, +} + +/// Aggregator duties for a single validator. +pub struct AggregatorDuties { + /// The epoch up to which aggregation proofs have already been computed (inclusive). + pre_compute_epoch: RwLock>, + /// Map from slot & subnet ID to proof that this validator is an aggregator. + /// + /// The slot is the slot at which the signed contribution and proof should be broadcast, + /// which is 1 less than the slot for which the `duty` was computed. + proofs: RwLock>, +} + +/// Duties for multiple validators, for a single slot. +/// +/// This type is returned to the sync service. +pub struct SlotDuties { + /// List of duties for all sync committee members at this slot. + /// + /// Note: this is intentionally NOT split by subnet so that we only sign + /// one `SyncCommitteeMessage` per validator (recall a validator may be part of multiple + /// subnets). + pub duties: Vec, + /// Map from subnet ID to validator index and selection proof of each aggregator. + pub aggregators: HashMap>, +} + +impl Default for SyncDutiesMap { + fn default() -> Self { + Self { + committees: RwLock::new(HashMap::new()), + } + } +} + +impl SyncDutiesMap { + /// Check if duties are already known for all of the given validators for `committee_period`. + fn all_duties_known(&self, committee_period: u64, validator_indices: &[u64]) -> bool { + self.committees + .read() + .get(&committee_period) + .map_or(false, |committee_duties| { + let validator_duties = committee_duties.validators.read(); + validator_indices + .iter() + .all(|index| validator_duties.contains_key(index)) + }) + } + + /// Prepare for pre-computation of selection proofs for `committee_period`. + /// + /// Return the epoch up to which proofs should be pre-computed, as well as a vec of + /// `(previous_pre_compute_epoch, sync_duty)` pairs for all validators which need to have proofs + /// computed. See `fill_in_aggregation_proofs` for the actual calculation. + fn prepare_for_aggregator_pre_compute( + &self, + committee_period: u64, + current_epoch: Epoch, + spec: &ChainSpec, + ) -> (Epoch, Vec<(Epoch, SyncDuty)>) { + let default_start_epoch = + std::cmp::max(current_epoch, first_epoch_of_period(committee_period, spec)); + let pre_compute_epoch = std::cmp::min( + current_epoch + AGGREGATION_PRE_COMPUTE_EPOCHS, + last_epoch_of_period(committee_period, spec), + ); + + let pre_compute_duties = self.committees.read().get(&committee_period).map_or_else( + Vec::new, + |committee_duties| { + let validator_duties = committee_duties.validators.read(); + validator_duties + .values() + .filter_map(|maybe_duty| { + let duty = maybe_duty.as_ref()?; + let old_pre_compute_epoch = duty + .aggregation_duties + .pre_compute_epoch + .write() + .replace(pre_compute_epoch); + + match old_pre_compute_epoch { + // No proofs pre-computed previously, compute all from the start of + // the period or the current epoch (whichever is later). + None => Some((default_start_epoch, duty.duty.clone())), + // Proofs computed up to `prev`, start from the subsequent epoch. + Some(prev) if prev < pre_compute_epoch => { + Some((prev + 1, duty.duty.clone())) + } + // Proofs already known, no need to compute. + _ => None, + } + }) + .collect() + }, + ); + (pre_compute_epoch, pre_compute_duties) + } + + fn get_or_create_committee_duties<'a, 'b>( + &'a self, + committee_period: u64, + validator_indices: impl IntoIterator, + ) -> MappedRwLockReadGuard<'a, CommitteeDuties> { + let mut committees_writer = self.committees.write(); + + committees_writer + .entry(committee_period) + .or_insert_with(CommitteeDuties::default) + .init(validator_indices); + + // Return shared reference + RwLockReadGuard::map( + RwLockWriteGuard::downgrade(committees_writer), + |committees_reader| &committees_reader[&committee_period], + ) + } + + /// Get duties for all validators for the given `wall_clock_slot`. + /// + /// This is the entry-point for the sync committee service. + pub fn get_duties_for_slot( + &self, + wall_clock_slot: Slot, + spec: &ChainSpec, + ) -> Option { + // Sync duties lag their assigned slot by 1 + let duty_slot = wall_clock_slot + 1; + + let sync_committee_period = duty_slot + .epoch(E::slots_per_epoch()) + .sync_committee_period(spec) + .ok()?; + + let committees_reader = self.committees.read(); + let committee_duties = committees_reader.get(&sync_committee_period)?; + + let mut duties = vec![]; + let mut aggregators = HashMap::new(); + + committee_duties + .validators + .read() + .values() + // Filter out non-members & failed subnet IDs. + .filter_map(|opt_duties| { + let duty = opt_duties.as_ref()?; + let subnet_ids = duty.duty.subnet_ids::().ok()?; + Some((duty, subnet_ids)) + }) + // Add duties for members to the vec of all duties, and aggregators to the + // aggregators map. + .for_each(|(validator_duty, subnet_ids)| { + duties.push(validator_duty.duty.clone()); + + let proofs = validator_duty.aggregation_duties.proofs.read(); + + for subnet_id in subnet_ids { + if let Some(proof) = proofs.get(&(wall_clock_slot, subnet_id)) { + aggregators.entry(subnet_id).or_insert_with(Vec::new).push(( + validator_duty.duty.validator_index, + validator_duty.duty.pubkey, + proof.clone(), + )); + } + } + }); + + Some(SlotDuties { + duties, + aggregators, + }) + } + + /// Prune duties for past sync committee periods from the map. + fn prune(&self, current_sync_committee_period: u64) { + self.committees + .write() + .retain(|period, _| *period >= current_sync_committee_period) + } +} + +impl CommitteeDuties { + fn init<'b>(&mut self, validator_indices: impl IntoIterator) { + validator_indices.into_iter().for_each(|validator_index| { + self.validators + .get_mut() + .entry(*validator_index) + .or_insert(None); + }) + } +} + +impl ValidatorDuties { + fn new(duty: SyncDuty) -> Self { + Self { + duty, + aggregation_duties: AggregatorDuties { + pre_compute_epoch: RwLock::new(None), + proofs: RwLock::new(HashMap::new()), + }, + } + } +} + +/// Number of epochs to wait from the start of the period before actually fetching duties. +fn epoch_offset(spec: &ChainSpec) -> u64 { + spec.epochs_per_sync_committee_period.as_u64() / 2 +} + +fn first_epoch_of_period(sync_committee_period: u64, spec: &ChainSpec) -> Epoch { + spec.epochs_per_sync_committee_period * sync_committee_period +} + +fn last_epoch_of_period(sync_committee_period: u64, spec: &ChainSpec) -> Epoch { + first_epoch_of_period(sync_committee_period + 1, spec) - 1 +} + +pub async fn poll_sync_committee_duties( + duties_service: &Arc>, +) -> Result<(), Error> { + let sync_duties = &duties_service.sync_duties; + let spec = &duties_service.spec; + let current_epoch = duties_service + .slot_clock + .now() + .ok_or(Error::UnableToReadSlotClock)? + .epoch(E::slots_per_epoch()); + + // If the Altair fork is yet to be activated, do not attempt to poll for duties. + if spec + .altair_fork_epoch + .map_or(true, |altair_epoch| current_epoch < altair_epoch) + { + return Ok(()); + } + + let current_sync_committee_period = current_epoch.sync_committee_period(spec)?; + let next_sync_committee_period = current_sync_committee_period + 1; + + // Collect *all* pubkeys, even those undergoing doppelganger protection. + // + // Sync committee messages are not slashable and are currently excluded from doppelganger + // protection. + let local_pubkeys: HashSet<_> = duties_service + .validator_store + .voting_pubkeys(DoppelgangerStatus::ignored); + + let local_indices = { + let mut local_indices = Vec::with_capacity(local_pubkeys.len()); + + let vals_ref = duties_service.validator_store.initialized_validators(); + let vals = vals_ref.read(); + for &pubkey in &local_pubkeys { + if let Some(validator_index) = vals.get_index(&pubkey) { + local_indices.push(validator_index) + } + } + local_indices + }; + + // If duties aren't known for the current period, poll for them. + if !sync_duties.all_duties_known(current_sync_committee_period, &local_indices) { + poll_sync_committee_duties_for_period( + duties_service, + &local_indices, + current_sync_committee_period, + ) + .await?; + + // Prune previous duties (we avoid doing this too often as it locks the whole map). + sync_duties.prune(current_sync_committee_period); + } + + // Pre-compute aggregator selection proofs for the current period. + let (current_pre_compute_epoch, new_pre_compute_duties) = sync_duties + .prepare_for_aggregator_pre_compute(current_sync_committee_period, current_epoch, spec); + + if !new_pre_compute_duties.is_empty() { + let sub_duties_service = duties_service.clone(); + duties_service.context.executor.spawn_blocking( + move || { + fill_in_aggregation_proofs( + sub_duties_service, + &new_pre_compute_duties, + current_sync_committee_period, + current_epoch, + current_pre_compute_epoch, + ) + }, + "duties_service_sync_selection_proofs", + ); + } + + // If we're past the point in the current period where we should determine duties for the next + // period and they are not yet known, then poll. + if current_epoch.as_u64() % spec.epochs_per_sync_committee_period.as_u64() >= epoch_offset(spec) + && !sync_duties.all_duties_known(next_sync_committee_period, &local_indices) + { + poll_sync_committee_duties_for_period( + duties_service, + &local_indices, + next_sync_committee_period, + ) + .await?; + + // Prune (this is the main code path for updating duties, so we should almost always hit + // this prune). + sync_duties.prune(current_sync_committee_period); + } + + // Pre-compute aggregator selection proofs for the next period. + if (current_epoch + AGGREGATION_PRE_COMPUTE_EPOCHS).sync_committee_period(spec)? + == next_sync_committee_period + { + let (pre_compute_epoch, new_pre_compute_duties) = sync_duties + .prepare_for_aggregator_pre_compute(next_sync_committee_period, current_epoch, spec); + + if !new_pre_compute_duties.is_empty() { + let sub_duties_service = duties_service.clone(); + duties_service.context.executor.spawn_blocking( + move || { + fill_in_aggregation_proofs( + sub_duties_service, + &new_pre_compute_duties, + next_sync_committee_period, + current_epoch, + pre_compute_epoch, + ) + }, + "duties_service_sync_selection_proofs", + ); + } + } + + Ok(()) +} + +pub async fn poll_sync_committee_duties_for_period( + duties_service: &Arc>, + local_indices: &[u64], + sync_committee_period: u64, +) -> Result<(), Error> { + let spec = &duties_service.spec; + let log = duties_service.context.log(); + + debug!( + log, + "Fetching sync committee duties"; + "sync_committee_period" => sync_committee_period, + "num_validators" => local_indices.len(), + ); + + let period_start_epoch = spec.epochs_per_sync_committee_period * sync_committee_period; + + let duties_response = duties_service + .beacon_nodes + .first_success(duties_service.require_synced, |beacon_node| async move { + beacon_node + .post_validator_duties_sync(period_start_epoch, local_indices) + .await + }) + .await; + + let duties = match duties_response { + Ok(res) => res.data, + Err(e) => { + warn!( + log, + "Failed to download sync committee duties"; + "sync_committee_period" => sync_committee_period, + "error" => %e, + ); + return Ok(()); + } + }; + + debug!(log, "Fetched sync duties from BN"; "count" => duties.len()); + + // Add duties to map. + let committee_duties = duties_service + .sync_duties + .get_or_create_committee_duties(sync_committee_period, local_indices); + + let mut validator_writer = committee_duties.validators.write(); + for duty in duties { + let validator_duties = validator_writer + .get_mut(&duty.validator_index) + .ok_or(Error::SyncDutiesNotFound(duty.validator_index))?; + + let updated = validator_duties.as_ref().map_or(true, |existing_duties| { + let updated_due_to_reorg = existing_duties.duty.validator_sync_committee_indices + != duty.validator_sync_committee_indices; + if updated_due_to_reorg { + warn!( + log, + "Sync committee duties changed"; + "message" => "this could be due to a really long re-org, or a bug" + ); + } + updated_due_to_reorg + }); + + if updated { + info!( + log, + "Validator in sync committee"; + "validator_index" => duty.validator_index, + "sync_committee_period" => sync_committee_period, + ); + + *validator_duties = Some(ValidatorDuties::new(duty)); + } + } + + Ok(()) +} + +pub fn fill_in_aggregation_proofs( + duties_service: Arc>, + pre_compute_duties: &[(Epoch, SyncDuty)], + sync_committee_period: u64, + current_epoch: Epoch, + pre_compute_epoch: Epoch, +) { + let log = duties_service.context.log(); + + debug!( + log, + "Calculating sync selection proofs"; + "period" => sync_committee_period, + "current_epoch" => current_epoch, + "pre_compute_epoch" => pre_compute_epoch + ); + + // Generate selection proofs for each validator at each slot, one epoch at a time. + for epoch in (current_epoch.as_u64()..=pre_compute_epoch.as_u64()).map(Epoch::new) { + // Generate proofs. + let validator_proofs: Vec<(u64, Vec<_>)> = pre_compute_duties + .iter() + .filter_map(|(validator_start_epoch, duty)| { + // Proofs are already known at this epoch for this validator. + if epoch < *validator_start_epoch { + return None; + } + + let subnet_ids = duty + .subnet_ids::() + .map_err(|e| { + crit!( + log, + "Arithmetic error computing subnet IDs"; + "error" => ?e, + ); + }) + .ok()?; + + let proofs = epoch + .slot_iter(E::slots_per_epoch()) + .cartesian_product(&subnet_ids) + .filter_map(|(duty_slot, &subnet_id)| { + // Construct proof for prior slot. + let slot = duty_slot - 1; + + let proof = duties_service + .validator_store + .produce_sync_selection_proof(&duty.pubkey, slot, subnet_id) + .map_err(|_| { + warn!( + log, + "Pubkey missing when signing selection proof"; + "pubkey" => ?duty.pubkey, + "slot" => slot, + ); + }) + .ok()?; + + let is_aggregator = proof + .is_aggregator::() + .map_err(|e| { + warn!( + log, + "Error determining is_aggregator"; + "pubkey" => ?duty.pubkey, + "slot" => slot, + "error" => ?e, + ); + }) + .ok()?; + + if is_aggregator { + debug!( + log, + "Validator is sync aggregator"; + "validator_index" => duty.validator_index, + "slot" => slot, + "subnet_id" => %subnet_id, + ); + Some(((slot, subnet_id), proof)) + } else { + None + } + }) + .collect(); + + Some((duty.validator_index, proofs)) + }) + .collect(); + + // Add to global storage (we add regularly so the proofs can be used ASAP). + let sync_map = duties_service.sync_duties.committees.read(); + let committee_duties = if let Some(duties) = sync_map.get(&sync_committee_period) { + duties + } else { + debug!( + log, + "Missing sync duties"; + "period" => sync_committee_period, + ); + continue; + }; + let validators = committee_duties.validators.read(); + let num_validators_updated = validator_proofs.len(); + + for (validator_index, proofs) in validator_proofs { + if let Some(Some(duty)) = validators.get(&validator_index) { + duty.aggregation_duties.proofs.write().extend(proofs); + } else { + debug!( + log, + "Missing sync duty to update"; + "validator_index" => validator_index, + "period" => sync_committee_period, + ); + } + } + + if num_validators_updated > 0 { + debug!( + log, + "Finished computing sync selection proofs"; + "epoch" => epoch, + "updated_validators" => num_validators_updated, + ); + } + } +} diff --git a/validator_client/src/fork_service.rs b/validator_client/src/fork_service.rs deleted file mode 100644 index f5d39e397c9..00000000000 --- a/validator_client/src/fork_service.rs +++ /dev/null @@ -1,222 +0,0 @@ -use crate::beacon_node_fallback::{BeaconNodeFallback, RequireSynced}; -use crate::http_metrics::metrics; -use environment::RuntimeContext; -use eth2::types::StateId; -use parking_lot::RwLock; -use slog::{debug, trace}; -use slog::{error, Logger}; -use slot_clock::SlotClock; -use std::ops::Deref; -use std::sync::Arc; -use tokio::time::{sleep, Duration}; -use types::{EthSpec, Fork}; - -/// Delay this period of time after the slot starts. This allows the node to process the new slot. -const TIME_DELAY_FROM_SLOT: Duration = Duration::from_millis(80); - -/// Builds a `ForkService`. -pub struct ForkServiceBuilder { - fork: Option, - slot_clock: Option, - beacon_nodes: Option>>, - log: Option, -} - -impl ForkServiceBuilder { - pub fn new() -> Self { - Self { - fork: None, - slot_clock: None, - beacon_nodes: None, - log: None, - } - } - - pub fn fork(mut self, fork: Fork) -> Self { - self.fork = Some(fork); - self - } - - pub fn slot_clock(mut self, slot_clock: T) -> Self { - self.slot_clock = Some(slot_clock); - self - } - - pub fn beacon_nodes(mut self, beacon_nodes: Arc>) -> Self { - self.beacon_nodes = Some(beacon_nodes); - self - } - - pub fn log(mut self, log: Logger) -> Self { - self.log = Some(log); - self - } - - pub fn build(self) -> Result, String> { - Ok(ForkService { - inner: Arc::new(Inner { - fork: RwLock::new(self.fork.ok_or("Cannot build ForkService without fork")?), - slot_clock: self - .slot_clock - .ok_or("Cannot build ForkService without slot_clock")?, - beacon_nodes: self - .beacon_nodes - .ok_or("Cannot build ForkService without beacon_node")?, - log: self - .log - .ok_or("Cannot build ForkService without logger")? - .clone(), - }), - }) - } -} - -#[cfg(test)] -#[allow(dead_code)] -impl ForkServiceBuilder { - pub fn testing_only(spec: types::ChainSpec, log: Logger) -> Self { - use crate::beacon_node_fallback::CandidateBeaconNode; - - let slot_clock = slot_clock::TestingSlotClock::new( - types::Slot::new(0), - std::time::Duration::from_secs(42), - std::time::Duration::from_secs(42), - ); - let candidates = vec![CandidateBeaconNode::new(eth2::BeaconNodeHttpClient::new( - sensitive_url::SensitiveUrl::parse("http://127.0.0.1").unwrap(), - eth2::Timeouts::set_all(Duration::from_secs(12)), - ))]; - let mut beacon_nodes = BeaconNodeFallback::new(candidates, spec, log.clone()); - beacon_nodes.set_slot_clock(slot_clock); - - Self { - fork: Some(types::Fork::default()), - slot_clock: Some(slot_clock::TestingSlotClock::new( - types::Slot::new(0), - std::time::Duration::from_secs(42), - std::time::Duration::from_secs(42), - )), - beacon_nodes: Some(Arc::new(beacon_nodes)), - log: Some(log), - } - } -} - -/// Helper to minimise `Arc` usage. -pub struct Inner { - fork: RwLock, - beacon_nodes: Arc>, - log: Logger, - slot_clock: T, -} - -/// Attempts to download the `Fork` struct from the beacon node at the start of each epoch. -pub struct ForkService { - inner: Arc>, -} - -impl Clone for ForkService { - fn clone(&self) -> Self { - Self { - inner: self.inner.clone(), - } - } -} - -impl Deref for ForkService { - type Target = Inner; - - fn deref(&self) -> &Self::Target { - self.inner.deref() - } -} - -impl ForkService { - /// Returns the last fork downloaded from the beacon node, if any. - pub fn fork(&self) -> Fork { - *self.fork.read() - } - - /// Returns the slot clock. - pub fn slot_clock(&self) -> T { - self.slot_clock.clone() - } - - /// Starts the service that periodically polls for the `Fork`. - pub fn start_update_service(self, context: &RuntimeContext) -> Result<(), String> { - // Run an immediate update before starting the updater service. - context - .executor - .spawn_ignoring_error(self.clone().do_update(), "fork service update"); - - let executor = context.executor.clone(); - let log = context.log().clone(); - let spec = E::default_spec(); - - let interval_fut = async move { - loop { - // Run this poll before the wait, this should hopefully download the fork before the - // other services need them. - self.clone().do_update().await.ok(); - - if let Some(duration_to_next_epoch) = - self.slot_clock.duration_to_next_epoch(E::slots_per_epoch()) - { - sleep(duration_to_next_epoch + TIME_DELAY_FROM_SLOT).await; - } else { - error!(log, "Failed to read slot clock"); - // If we can't read the slot clock, just wait another slot. - sleep(Duration::from_secs(spec.seconds_per_slot)).await; - continue; - } - } - }; - - executor.spawn(interval_fut, "fork_service"); - - Ok(()) - } - - /// Attempts to download the `Fork` from the server. - async fn do_update(self) -> Result<(), ()> { - let _timer = - metrics::start_timer_vec(&metrics::FORK_SERVICE_TIMES, &[metrics::FULL_UPDATE]); - - let log = &self.log; - let fork = self - .inner - .beacon_nodes - .first_success(RequireSynced::No, |beacon_node| async move { - beacon_node - .get_beacon_states_fork(StateId::Head) - .await - .map_err(|e| { - trace!( - log, - "Fork update failed"; - "error" => format!("Error retrieving fork: {:?}", e) - ) - })? - .ok_or_else(|| { - trace!( - log, - "Fork update failed"; - "error" => "The beacon head fork is unknown" - ) - }) - .map(|result| result.data) - }) - .await - .map_err(|_| ())?; - - if *(self.fork.read()) != fork { - *(self.fork.write()) = fork; - } - - debug!(self.log, "Fork update success"); - - // Returning an error will stop the interval. This is not desired, a single failure - // should not stop all future attempts. - Ok(()) - } -} diff --git a/validator_client/src/http_api/tests.rs b/validator_client/src/http_api/tests.rs index fd8b2b9e730..a3cda8c6601 100644 --- a/validator_client/src/http_api/tests.rs +++ b/validator_client/src/http_api/tests.rs @@ -4,7 +4,7 @@ use crate::doppelganger_service::DoppelgangerService; use crate::{ http_api::{ApiSecret, Config as HttpConfig, Context}, - Config, ForkServiceBuilder, InitializedValidators, ValidatorDefinitions, ValidatorStore, + Config, InitializedValidators, ValidatorDefinitions, ValidatorStore, }; use account_utils::{ eth2_wallet::WalletBuilder, mnemonic_from_phrase, random_mnemonic, random_password, @@ -17,10 +17,11 @@ use eth2_keystore::KeystoreBuilder; use parking_lot::RwLock; use sensitive_url::SensitiveUrl; use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; -use slot_clock::TestingSlotClock; +use slot_clock::{SlotClock, TestingSlotClock}; use std::marker::PhantomData; use std::net::Ipv4Addr; use std::sync::Arc; +use std::time::Duration; use tempfile::{tempdir, TempDir}; use tokio::runtime::Runtime; use tokio::sync::oneshot; @@ -73,20 +74,19 @@ impl ApiTester { let spec = E::default_spec(); - let fork_service = ForkServiceBuilder::testing_only(spec.clone(), log.clone()) - .build() - .unwrap(); - let slashing_db_path = config.validator_dir.join(SLASHING_PROTECTION_FILENAME); let slashing_protection = SlashingDatabase::open_or_create(&slashing_db_path).unwrap(); - let validator_store: ValidatorStore = ValidatorStore::new( + let slot_clock = + TestingSlotClock::new(Slot::new(0), Duration::from_secs(0), Duration::from_secs(1)); + + let validator_store = ValidatorStore::<_, E>::new( initialized_validators, slashing_protection, Hash256::repeat_byte(42), spec, - fork_service.clone(), Some(Arc::new(DoppelgangerService::new(log.clone()))), + slot_clock, log.clone(), ); @@ -96,7 +96,7 @@ impl ApiTester { let initialized_validators = validator_store.initialized_validators(); - let context: Arc> = Arc::new(Context { + let context = Arc::new(Context { runtime, api_secret, validator_dir: Some(validator_dir.path().into()), diff --git a/validator_client/src/http_metrics/metrics.rs b/validator_client/src/http_metrics/metrics.rs index 120e6c2c7e6..db8eb821241 100644 --- a/validator_client/src/http_metrics/metrics.rs +++ b/validator_client/src/http_metrics/metrics.rs @@ -67,16 +67,26 @@ lazy_static::lazy_static! { "Total count of attempted SelectionProof signings", &["status"] ); + pub static ref SIGNED_SYNC_COMMITTEE_MESSAGES_TOTAL: Result = try_create_int_counter_vec( + "vc_signed_sync_committee_messages_total", + "Total count of attempted SyncCommitteeMessage signings", + &["status"] + ); + pub static ref SIGNED_SYNC_COMMITTEE_CONTRIBUTIONS_TOTAL: Result = try_create_int_counter_vec( + "vc_signed_sync_committee_contributions_total", + "Total count of attempted ContributionAndProof signings", + &["status"] + ); + pub static ref SIGNED_SYNC_SELECTION_PROOFS_TOTAL: Result = try_create_int_counter_vec( + "vc_signed_sync_selection_proofs_total", + "Total count of attempted SyncSelectionProof signings", + &["status"] + ); pub static ref DUTIES_SERVICE_TIMES: Result = try_create_histogram_vec( "vc_duties_service_task_times_seconds", "Duration to perform duties service tasks", &["task"] ); - pub static ref FORK_SERVICE_TIMES: Result = try_create_histogram_vec( - "vc_fork_service_task_times_seconds", - "Duration to perform fork service tasks", - &["task"] - ); pub static ref ATTESTATION_SERVICE_TIMES: Result = try_create_histogram_vec( "vc_attestation_service_task_times_seconds", "Duration to perform attestation service tasks", diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index be9a27db7b9..bfdc183d436 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -5,12 +5,12 @@ mod check_synced; mod cli; mod config; mod duties_service; -mod fork_service; mod graffiti_file; mod http_metrics; mod initialized_validators; mod key_cache; mod notifier; +mod sync_committee_service; mod validator_store; mod doppelganger_service; @@ -31,9 +31,7 @@ use block_service::{BlockService, BlockServiceBuilder}; use clap::ArgMatches; use duties_service::DutiesService; use environment::RuntimeContext; -use eth2::types::StateId; use eth2::{reqwest::ClientBuilder, BeaconNodeHttpClient, StatusCode, Timeouts}; -use fork_service::{ForkService, ForkServiceBuilder}; use http_api::ApiSecret; use initialized_validators::InitializedValidators; use notifier::spawn_notifier; @@ -46,11 +44,12 @@ use std::marker::PhantomData; use std::net::SocketAddr; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; +use sync_committee_service::SyncCommitteeService; use tokio::{ sync::mpsc, time::{sleep, Duration}, }; -use types::{EthSpec, Fork, Hash256}; +use types::{EthSpec, Hash256}; use validator_store::ValidatorStore; /// The interval between attempts to contact the beacon node during startup. @@ -66,6 +65,7 @@ const HTTP_ATTESTER_DUTIES_TIMEOUT_QUOTIENT: u32 = 4; const HTTP_LIVENESS_TIMEOUT_QUOTIENT: u32 = 4; const HTTP_PROPOSAL_TIMEOUT_QUOTIENT: u32 = 2; const HTTP_PROPOSER_DUTIES_TIMEOUT_QUOTIENT: u32 = 4; +const HTTP_SYNC_DUTIES_TIMEOUT_QUOTIENT: u32 = 4; const DOPPELGANGER_SERVICE_NAME: &str = "doppelganger"; @@ -73,9 +73,9 @@ const DOPPELGANGER_SERVICE_NAME: &str = "doppelganger"; pub struct ProductionValidatorClient { context: RuntimeContext, duties_service: Arc>, - fork_service: ForkService, block_service: BlockService, attestation_service: AttestationService, + sync_committee_service: SyncCommitteeService, doppelganger_service: Option>, validator_store: Arc>, http_api_listen_addr: Option, @@ -263,6 +263,7 @@ impl ProductionValidatorClient { liveness: slot_duration / HTTP_LIVENESS_TIMEOUT_QUOTIENT, proposal: slot_duration / HTTP_PROPOSAL_TIMEOUT_QUOTIENT, proposer_duties: slot_duration / HTTP_PROPOSER_DUTIES_TIMEOUT_QUOTIENT, + sync_duties: slot_duration / HTTP_SYNC_DUTIES_TIMEOUT_QUOTIENT, } } else { Timeouts::set_all(slot_duration) @@ -293,7 +294,7 @@ impl ProductionValidatorClient { BeaconNodeFallback::new(candidates, context.eth2_config.spec.clone(), log.clone()); // Perform some potentially long-running initialization tasks. - let (genesis_time, genesis_validators_root, fork) = tokio::select! { + let (genesis_time, genesis_validators_root) = tokio::select! { tuple = init_from_beacon_node(&beacon_nodes, &context) => tuple?, () = context.executor.exit() => return Err("Shutting down".to_string()) }; @@ -313,13 +314,6 @@ impl ProductionValidatorClient { let beacon_nodes = Arc::new(beacon_nodes); start_fallback_updater_service(context.clone(), beacon_nodes.clone())?; - let fork_service = ForkServiceBuilder::new() - .fork(fork) - .slot_clock(slot_clock.clone()) - .beacon_nodes(beacon_nodes.clone()) - .log(log.clone()) - .build()?; - let doppelganger_service = if config.enable_doppelganger_protection { Some(Arc::new(DoppelgangerService::new( context @@ -331,16 +325,15 @@ impl ProductionValidatorClient { None }; - let validator_store: Arc> = - Arc::new(ValidatorStore::new( - validators, - slashing_protection, - genesis_validators_root, - context.eth2_config.spec.clone(), - fork_service.clone(), - doppelganger_service.clone(), - log.clone(), - )); + let validator_store = Arc::new(ValidatorStore::new( + validators, + slashing_protection, + genesis_validators_root, + context.eth2_config.spec.clone(), + doppelganger_service.clone(), + slot_clock.clone(), + log.clone(), + )); info!( log, @@ -359,6 +352,7 @@ impl ProductionValidatorClient { let duties_service = Arc::new(DutiesService { attesters: <_>::default(), proposers: <_>::default(), + sync_duties: <_>::default(), slot_clock: slot_clock.clone(), beacon_nodes: beacon_nodes.clone(), validator_store: validator_store.clone(), @@ -394,6 +388,14 @@ impl ProductionValidatorClient { .runtime_context(context.service_context("attestation".into())) .build()?; + let sync_committee_service = SyncCommitteeService::new( + duties_service.clone(), + validator_store.clone(), + slot_clock, + beacon_nodes.clone(), + context.service_context("sync_committee".into()), + ); + // Wait until genesis has occured. // // It seems most sensible to move this into the `start_service` function, but I'm caution @@ -406,9 +408,9 @@ impl ProductionValidatorClient { Ok(Self { context, duties_service, - fork_service, block_service, attestation_service, + sync_committee_service, doppelganger_service, validator_store, config, @@ -427,11 +429,6 @@ impl ProductionValidatorClient { duties_service::start_update_service(self.duties_service.clone(), block_service_tx); - self.fork_service - .clone() - .start_update_service(&self.context) - .map_err(|e| format!("Unable to start fork service: {}", e))?; - self.block_service .clone() .start_update_service(block_service_rx) @@ -442,6 +439,11 @@ impl ProductionValidatorClient { .start_update_service(&self.context.eth2_config.spec) .map_err(|e| format!("Unable to start attestation service: {}", e))?; + self.sync_committee_service + .clone() + .start_update_service(&self.context.eth2_config.spec) + .map_err(|e| format!("Unable to start sync committee service: {}", e))?; + if let Some(doppelganger_service) = self.doppelganger_service.clone() { DoppelgangerService::start_update_service( doppelganger_service, @@ -461,7 +463,7 @@ impl ProductionValidatorClient { let api_secret = ApiSecret::create_or_open(&self.config.validator_dir)?; self.http_api_listen_addr = if self.config.http_api.enabled { - let ctx: Arc> = Arc::new(http_api::Context { + let ctx = Arc::new(http_api::Context { runtime: self.context.executor.runtime(), api_secret, validator_store: Some(self.validator_store.clone()), @@ -495,7 +497,7 @@ impl ProductionValidatorClient { async fn init_from_beacon_node( beacon_nodes: &BeaconNodeFallback, context: &RuntimeContext, -) -> Result<(u64, Hash256, Fork), String> { +) -> Result<(u64, Hash256), String> { loop { beacon_nodes.update_unready_candidates().await; let num_available = beacon_nodes.num_available().await; @@ -554,33 +556,7 @@ async fn init_from_beacon_node( sleep(RETRY_DELAY).await; }; - let fork = loop { - match beacon_nodes - .first_success(RequireSynced::No, |node| async move { - node.get_beacon_states_fork(StateId::Head).await - }) - .await - { - Ok(Some(fork)) => break fork.data, - Ok(None) => { - info!( - context.log(), - "Failed to get fork, state not found"; - ); - } - Err(errors) => { - error!( - context.log(), - "Failed to get fork"; - "error" => %errors - ); - } - } - - sleep(RETRY_DELAY).await; - }; - - Ok((genesis.genesis_time, genesis.genesis_validators_root, fork)) + Ok((genesis.genesis_time, genesis.genesis_validators_root)) } async fn wait_for_genesis( diff --git a/validator_client/src/sync_committee_service.rs b/validator_client/src/sync_committee_service.rs new file mode 100644 index 00000000000..33110ed2e82 --- /dev/null +++ b/validator_client/src/sync_committee_service.rs @@ -0,0 +1,537 @@ +use crate::beacon_node_fallback::{BeaconNodeFallback, RequireSynced}; +use crate::{duties_service::DutiesService, validator_store::ValidatorStore}; +use environment::RuntimeContext; +use eth2::types::BlockId; +use futures::future::FutureExt; +use slog::{crit, debug, error, info, trace, warn}; +use slot_clock::SlotClock; +use std::collections::HashMap; +use std::ops::Deref; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use tokio::time::{sleep, sleep_until, Duration, Instant}; +use types::{ + ChainSpec, EthSpec, Hash256, PublicKeyBytes, Slot, SyncCommitteeSubscription, + SyncContributionData, SyncDuty, SyncSelectionProof, SyncSubnetId, +}; + +pub const SUBSCRIPTION_LOOKAHEAD_EPOCHS: u64 = 4; + +pub struct SyncCommitteeService { + inner: Arc>, +} + +impl Clone for SyncCommitteeService { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl Deref for SyncCommitteeService { + type Target = Inner; + + fn deref(&self) -> &Self::Target { + self.inner.deref() + } +} + +pub struct Inner { + duties_service: Arc>, + validator_store: Arc>, + slot_clock: T, + beacon_nodes: Arc>, + context: RuntimeContext, + /// Boolean to track whether the service has posted subscriptions to the BN at least once. + /// + /// This acts as a latch that fires once upon start-up, and then never again. + first_subscription_done: AtomicBool, +} + +impl SyncCommitteeService { + pub fn new( + duties_service: Arc>, + validator_store: Arc>, + slot_clock: T, + beacon_nodes: Arc>, + context: RuntimeContext, + ) -> Self { + Self { + inner: Arc::new(Inner { + duties_service, + validator_store, + slot_clock, + beacon_nodes, + context, + first_subscription_done: AtomicBool::new(false), + }), + } + } + + /// Check if the Altair fork has been activated and therefore sync duties should be performed. + /// + /// Slot clock errors are mapped to `false`. + fn altair_fork_activated(&self) -> bool { + self.duties_service + .spec + .altair_fork_epoch + .and_then(|fork_epoch| { + let current_epoch = self.slot_clock.now()?.epoch(E::slots_per_epoch()); + Some(current_epoch >= fork_epoch) + }) + .unwrap_or(false) + } + + pub fn start_update_service(self, spec: &ChainSpec) -> Result<(), String> { + let log = self.context.log().clone(); + let slot_duration = Duration::from_secs(spec.seconds_per_slot); + let duration_to_next_slot = self + .slot_clock + .duration_to_next_slot() + .ok_or("Unable to determine duration to next slot")?; + + info!( + log, + "Sync committee service started"; + "next_update_millis" => duration_to_next_slot.as_millis() + ); + + let executor = self.context.executor.clone(); + + let interval_fut = async move { + loop { + if let Some(duration_to_next_slot) = self.slot_clock.duration_to_next_slot() { + // Wait for contribution broadcast interval 1/3 of the way through the slot. + let log = self.context.log(); + sleep(duration_to_next_slot + slot_duration / 3).await; + + // Do nothing if the Altair fork has not yet occurred. + if !self.altair_fork_activated() { + continue; + } + + if let Err(e) = self.spawn_contribution_tasks(slot_duration).await { + crit!( + log, + "Failed to spawn sync contribution tasks"; + "error" => e + ) + } else { + trace!( + log, + "Spawned sync contribution tasks"; + ) + } + + // Do subscriptions for future slots/epochs. + self.spawn_subscription_tasks(); + } else { + error!(log, "Failed to read slot clock"); + // If we can't read the slot clock, just wait another slot. + sleep(slot_duration).await; + } + } + }; + + executor.spawn(interval_fut, "sync_committee_service"); + Ok(()) + } + + async fn spawn_contribution_tasks(&self, slot_duration: Duration) -> Result<(), String> { + let log = self.context.log().clone(); + let slot = self.slot_clock.now().ok_or("Failed to read slot clock")?; + let duration_to_next_slot = self + .slot_clock + .duration_to_next_slot() + .ok_or("Unable to determine duration to next slot")?; + + // If a validator needs to publish a sync aggregate, they must do so at 2/3 + // through the slot. This delay triggers at this time + let aggregate_production_instant = Instant::now() + + duration_to_next_slot + .checked_sub(slot_duration / 3) + .unwrap_or_else(|| Duration::from_secs(0)); + + let slot_duties = self + .duties_service + .sync_duties + .get_duties_for_slot::(slot, &self.duties_service.spec) + .ok_or_else(|| format!("Error fetching duties for slot {}", slot))?; + + if slot_duties.duties.is_empty() { + debug!( + log, + "No local validators in current sync committee"; + "slot" => slot, + ); + return Ok(()); + } + + // Fetch block root for `SyncCommitteeContribution`. + let block_root = self + .beacon_nodes + .first_success(RequireSynced::Yes, |beacon_node| async move { + beacon_node.get_beacon_blocks_root(BlockId::Head).await + }) + .await + .map_err(|e| e.to_string())? + .ok_or_else(|| format!("No block root found for slot {}", slot))? + .data + .root; + + // Spawn one task to publish all of the sync committee signatures. + let validator_duties = slot_duties.duties; + self.inner.context.executor.spawn( + self.clone() + .publish_sync_committee_signatures(slot, block_root, validator_duties) + .map(|_| ()), + "sync_committee_signature_publish", + ); + + let aggregators = slot_duties.aggregators; + self.inner.context.executor.spawn( + self.clone() + .publish_sync_committee_aggregates( + slot, + block_root, + aggregators, + aggregate_production_instant, + ) + .map(|_| ()), + "sync_committee_aggregate_publish", + ); + + Ok(()) + } + + /// Publish sync committee signatures. + async fn publish_sync_committee_signatures( + self, + slot: Slot, + beacon_block_root: Hash256, + validator_duties: Vec, + ) -> Result<(), ()> { + let log = self.context.log().clone(); + + let committee_signatures = validator_duties + .iter() + .filter_map(|duty| { + self.validator_store + .produce_sync_committee_signature( + slot, + beacon_block_root, + duty.validator_index, + &duty.pubkey, + ) + .map_err(|e| { + crit!( + log, + "Failed to sign sync committee signature"; + "validator_index" => duty.validator_index, + "slot" => slot, + "error" => ?e, + ); + }) + .ok() + }) + .collect::>(); + + let signatures_slice = &committee_signatures; + + self.beacon_nodes + .first_success(RequireSynced::No, |beacon_node| async move { + beacon_node + .post_beacon_pool_sync_committee_signatures(signatures_slice) + .await + }) + .await + .map_err(|e| { + error!( + log, + "Unable to publish sync committee messages"; + "slot" => slot, + "error" => %e, + ); + })?; + + info!( + log, + "Successfully published sync committee messages"; + "count" => committee_signatures.len(), + "head_block" => ?beacon_block_root, + "slot" => slot, + ); + + Ok(()) + } + + async fn publish_sync_committee_aggregates( + self, + slot: Slot, + beacon_block_root: Hash256, + aggregators: HashMap>, + aggregate_instant: Instant, + ) { + for (subnet_id, subnet_aggregators) in aggregators { + let service = self.clone(); + self.inner.context.executor.spawn( + service + .publish_sync_committee_aggregate_for_subnet( + slot, + beacon_block_root, + subnet_id, + subnet_aggregators, + aggregate_instant, + ) + .map(|_| ()), + "sync_committee_aggregate_publish_subnet", + ); + } + } + + async fn publish_sync_committee_aggregate_for_subnet( + self, + slot: Slot, + beacon_block_root: Hash256, + subnet_id: SyncSubnetId, + subnet_aggregators: Vec<(u64, PublicKeyBytes, SyncSelectionProof)>, + aggregate_instant: Instant, + ) -> Result<(), ()> { + sleep_until(aggregate_instant).await; + + let log = self.context.log(); + + let contribution = self + .beacon_nodes + .first_success(RequireSynced::No, |beacon_node| async move { + let sync_contribution_data = SyncContributionData { + slot, + beacon_block_root, + subcommittee_index: subnet_id.into(), + }; + + beacon_node + .get_validator_sync_committee_contribution::(&sync_contribution_data) + .await + }) + .await + .map_err(|e| { + crit!( + log, + "Failed to produce sync contribution"; + "slot" => slot, + "beacon_block_root" => ?beacon_block_root, + "error" => %e, + ) + })? + .ok_or_else(|| { + crit!( + log, + "No aggregate contribution found"; + "slot" => slot, + "beacon_block_root" => ?beacon_block_root, + ); + })? + .data; + + // Make `SignedContributionAndProof`s + let signed_contributions = subnet_aggregators + .into_iter() + .filter_map(|(aggregator_index, aggregator_pk, selection_proof)| { + self.validator_store + .produce_signed_contribution_and_proof( + aggregator_index, + &aggregator_pk, + contribution.clone(), + selection_proof, + ) + .map_err(|e| { + crit!( + log, + "Unable to sign sync committee contribution"; + "slot" => slot, + "error" => ?e, + ); + }) + .ok() + }) + .collect::>(); + + // Publish to the beacon node. + let signed_contributions_slice = &signed_contributions; + self.beacon_nodes + .first_success(RequireSynced::No, |beacon_node| async move { + beacon_node + .post_validator_contribution_and_proofs(signed_contributions_slice) + .await + }) + .await + .map_err(|e| { + error!( + log, + "Unable to publish signed contributions and proofs"; + "slot" => slot, + "error" => %e, + ); + })?; + + info!( + log, + "Successfully published sync contributions"; + "subnet" => %subnet_id, + "beacon_block_root" => %beacon_block_root, + "num_signers" => contribution.aggregation_bits.num_set_bits(), + "slot" => slot, + ); + + Ok(()) + } + + fn spawn_subscription_tasks(&self) { + let service = self.clone(); + let log = self.context.log().clone(); + self.inner.context.executor.spawn( + async move { + service.publish_subscriptions().await.unwrap_or_else(|e| { + error!( + log, + "Error publishing subscriptions"; + "error" => ?e, + ) + }); + }, + "sync_committee_subscription_publish", + ); + } + + async fn publish_subscriptions(self) -> Result<(), String> { + let log = self.context.log().clone(); + let spec = &self.duties_service.spec; + let slot = self.slot_clock.now().ok_or("Failed to read slot clock")?; + + let mut duty_slots = vec![]; + let mut all_succeeded = true; + + // At the start of every epoch during the current period, re-post the subscriptions + // to the beacon node. This covers the case where the BN has forgotten the subscriptions + // due to a restart, or where the VC has switched to a fallback BN. + let current_period = sync_period_of_slot::(slot, spec)?; + + if !self.first_subscription_done.load(Ordering::Relaxed) + || slot.as_u64() % E::slots_per_epoch() == 0 + { + duty_slots.push((slot, current_period)); + } + + // Near the end of the current period, push subscriptions for the next period to the + // beacon node. We aggressively push every slot in the lead-up, as this is the main way + // that we want to ensure that the BN is subscribed (well in advance). + let lookahead_slot = slot + SUBSCRIPTION_LOOKAHEAD_EPOCHS * E::slots_per_epoch(); + + let lookahead_period = sync_period_of_slot::(lookahead_slot, spec)?; + + if lookahead_period > current_period { + duty_slots.push((lookahead_slot, lookahead_period)); + } + + if duty_slots.is_empty() { + return Ok(()); + } + + // Collect subscriptions. + let mut subscriptions = vec![]; + + for (duty_slot, sync_committee_period) in duty_slots { + debug!( + log, + "Fetching subscription duties"; + "duty_slot" => duty_slot, + "current_slot" => slot, + ); + match self + .duties_service + .sync_duties + .get_duties_for_slot::(duty_slot, spec) + { + Some(duties) => subscriptions.extend(subscriptions_from_sync_duties( + duties.duties, + sync_committee_period, + spec, + )), + None => { + warn!( + log, + "Missing duties for subscription"; + "slot" => duty_slot, + ); + all_succeeded = false; + } + } + } + + // Post subscriptions to BN. + debug!( + log, + "Posting sync subscriptions to BN"; + "count" => subscriptions.len(), + ); + let subscriptions_slice = &subscriptions; + + for subscription in subscriptions_slice { + debug!( + log, + "Subscription"; + "validator_index" => subscription.validator_index, + "validator_sync_committee_indices" => ?subscription.sync_committee_indices, + "until_epoch" => subscription.until_epoch, + ); + } + + if let Err(e) = self + .beacon_nodes + .first_success(RequireSynced::No, |beacon_node| async move { + beacon_node + .post_validator_sync_committee_subscriptions(subscriptions_slice) + .await + }) + .await + { + error!( + log, + "Unable to post sync committee subscriptions"; + "slot" => slot, + "error" => %e, + ); + all_succeeded = false; + } + + // Disable first-subscription latch once all duties have succeeded once. + if all_succeeded { + self.first_subscription_done.store(true, Ordering::Relaxed); + } + + Ok(()) + } +} + +fn sync_period_of_slot(slot: Slot, spec: &ChainSpec) -> Result { + slot.epoch(E::slots_per_epoch()) + .sync_committee_period(spec) + .map_err(|e| format!("Error computing sync period: {:?}", e)) +} + +fn subscriptions_from_sync_duties( + duties: Vec, + sync_committee_period: u64, + spec: &ChainSpec, +) -> impl Iterator { + let until_epoch = spec.epochs_per_sync_committee_period * (sync_committee_period + 1); + duties + .into_iter() + .map(move |duty| SyncCommitteeSubscription { + validator_index: duty.validator_index, + sync_committee_indices: duty.validator_sync_committee_indices, + until_epoch, + }) +} diff --git a/validator_client/src/validator_store.rs b/validator_client/src/validator_store.rs index 54cef8b6783..7100ee35232 100644 --- a/validator_client/src/validator_store.rs +++ b/validator_client/src/validator_store.rs @@ -1,5 +1,5 @@ use crate::{ - doppelganger_service::DoppelgangerService, fork_service::ForkService, http_metrics::metrics, + doppelganger_service::DoppelgangerService, http_metrics::metrics, initialized_validators::InitializedValidators, }; use account_utils::{validator_definitions::ValidatorDefinition, ZeroizeString}; @@ -8,12 +8,15 @@ use slashing_protection::{NotSafe, Safe, SlashingDatabase}; use slog::{crit, error, info, warn, Logger}; use slot_clock::SlotClock; use std::iter::FromIterator; +use std::marker::PhantomData; use std::path::Path; use std::sync::Arc; use types::{ attestation::Error as AttestationError, graffiti::GraffitiString, Attestation, BeaconBlock, ChainSpec, Domain, Epoch, EthSpec, Fork, Graffiti, Hash256, Keypair, PublicKeyBytes, - SelectionProof, Signature, SignedAggregateAndProof, SignedBeaconBlock, SignedRoot, Slot, + SelectionProof, Signature, SignedAggregateAndProof, SignedBeaconBlock, + SignedContributionAndProof, SignedRoot, Slot, SyncCommitteeContribution, SyncCommitteeMessage, + SyncSelectionProof, SyncSubnetId, }; use validator_dir::ValidatorDir; @@ -69,8 +72,8 @@ pub struct ValidatorStore { spec: Arc, log: Logger, doppelganger_service: Option>, - fork_service: ForkService, slot_clock: T, + _phantom: PhantomData, } impl ValidatorStore { @@ -79,8 +82,8 @@ impl ValidatorStore { slashing_protection: SlashingDatabase, genesis_validators_root: Hash256, spec: ChainSpec, - fork_service: ForkService, doppelganger_service: Option>, + slot_clock: T, log: Logger, ) -> Self { Self { @@ -89,10 +92,10 @@ impl ValidatorStore { slashing_protection_last_prune: Arc::new(Mutex::new(Epoch::new(0))), genesis_validators_root, spec: Arc::new(spec), - log: log.clone(), + log, doppelganger_service, - slot_clock: fork_service.slot_clock(), - fork_service, + slot_clock, + _phantom: PhantomData, } } @@ -253,8 +256,8 @@ impl ValidatorStore { self.validators.read().num_enabled() } - fn fork(&self) -> Fork { - self.fork_service.fork() + fn fork(&self, epoch: Epoch) -> Fork { + self.spec.fork_at_epoch(epoch) } /// Runs `func`, providing it access to the `Keypair` corresponding to `validator_pubkey`. @@ -301,7 +304,7 @@ impl ValidatorStore { let domain = self.spec.get_domain( epoch, Domain::Randao, - &self.fork(), + &self.fork(epoch), self.genesis_validators_root, ); let message = epoch.signing_root(domain); @@ -334,7 +337,7 @@ impl ValidatorStore { } // Check for slashing conditions. - let fork = self.fork(); + let fork = self.fork(block.epoch()); let domain = self.spec.get_domain( block.epoch(), Domain::BeaconProposer, @@ -403,7 +406,7 @@ impl ValidatorStore { } // Checking for slashing conditions. - let fork = self.fork(); + let fork = self.fork(attestation.data.target.epoch); let domain = self.spec.get_domain( attestation.data.target.epoch, @@ -486,8 +489,7 @@ impl ValidatorStore { aggregate: Attestation, selection_proof: SelectionProof, ) -> Result, Error> { - // Take the fork early to avoid lock interleaving. - let fork = self.fork(); + let fork = self.fork(aggregate.data.target.epoch); let proof = self.with_validator_keypair(validator_pubkey, move |keypair| { SignedAggregateAndProof::from_aggregate( @@ -513,9 +515,6 @@ impl ValidatorStore { validator_pubkey: PublicKeyBytes, slot: Slot, ) -> Result { - // Take the fork early to avoid lock interleaving. - let fork = self.fork(); - // Bypass the `with_validator_keypair` function. // // This is because we don't care about doppelganger protection when it comes to selection @@ -531,7 +530,7 @@ impl ValidatorStore { let proof = SelectionProof::new::( slot, &keypair.sk, - &fork, + &self.fork(slot.epoch(E::slots_per_epoch())), self.genesis_validators_root, &self.spec, ); @@ -541,6 +540,93 @@ impl ValidatorStore { Ok(proof) } + /// Produce a `SyncSelectionProof` for `slot` signed by the secret key of `validator_pubkey`. + pub fn produce_sync_selection_proof( + &self, + validator_pubkey: &PublicKeyBytes, + slot: Slot, + subnet_id: SyncSubnetId, + ) -> Result { + // Bypass `with_validator_keypair`: sync committee messages are not slashable. + let validators = self.validators.read(); + let voting_keypair = validators + .voting_keypair(validator_pubkey) + .ok_or(Error::UnknownPubkey(*validator_pubkey))?; + + metrics::inc_counter_vec( + &metrics::SIGNED_SYNC_SELECTION_PROOFS_TOTAL, + &[metrics::SUCCESS], + ); + + Ok(SyncSelectionProof::new::( + slot, + subnet_id.into(), + &voting_keypair.sk, + &self.fork(slot.epoch(E::slots_per_epoch())), + self.genesis_validators_root, + &self.spec, + )) + } + + pub fn produce_sync_committee_signature( + &self, + slot: Slot, + beacon_block_root: Hash256, + validator_index: u64, + validator_pubkey: &PublicKeyBytes, + ) -> Result { + // Bypass `with_validator_keypair`: sync committee messages are not slashable. + let validators = self.validators.read(); + let voting_keypair = validators + .voting_keypair(validator_pubkey) + .ok_or(Error::UnknownPubkey(*validator_pubkey))?; + + metrics::inc_counter_vec( + &metrics::SIGNED_SYNC_COMMITTEE_MESSAGES_TOTAL, + &[metrics::SUCCESS], + ); + + Ok(SyncCommitteeMessage::new::( + slot, + beacon_block_root, + validator_index, + &voting_keypair.sk, + &self.fork(slot.epoch(E::slots_per_epoch())), + self.genesis_validators_root, + &self.spec, + )) + } + + pub fn produce_signed_contribution_and_proof( + &self, + aggregator_index: u64, + aggregator_pubkey: &PublicKeyBytes, + contribution: SyncCommitteeContribution, + selection_proof: SyncSelectionProof, + ) -> Result, Error> { + // Bypass `with_validator_keypair`: sync committee messages are not slashable. + let validators = self.validators.read(); + let voting_keypair = validators + .voting_keypair(aggregator_pubkey) + .ok_or(Error::UnknownPubkey(*aggregator_pubkey))?; + let fork = self.fork(contribution.slot.epoch(E::slots_per_epoch())); + + metrics::inc_counter_vec( + &metrics::SIGNED_SYNC_COMMITTEE_CONTRIBUTIONS_TOTAL, + &[metrics::SUCCESS], + ); + + Ok(SignedContributionAndProof::from_aggregate( + aggregator_index, + contribution, + Some(selection_proof), + &voting_keypair.sk, + &fork, + self.genesis_validators_root, + &self.spec, + )) + } + /// Prune the slashing protection database so that it remains performant. /// /// This function will only do actual pruning periodically, so it should usually be