diff --git a/Cargo.lock b/Cargo.lock index 6b60c58c24..fe28c0c961 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3876,7 +3876,9 @@ dependencies = [ "eth2_wallet", "ethereum_hashing", "ethereum_ssz", + "execution_layer", "genesis", + "hex", "int_to_bytes", "lighthouse_network", "lighthouse_version", @@ -8438,6 +8440,7 @@ dependencies = [ "slashing_protection", "slog", "slot_clock", + "strum", "sysinfo", "system_health", "task_executor", diff --git a/Makefile b/Makefile index e3d889bfea..19988fc2c6 100644 --- a/Makefile +++ b/Makefile @@ -215,7 +215,8 @@ lint: -A clippy::upper-case-acronyms \ -A clippy::vec-init-then-push \ -A clippy::question-mark \ - -A clippy::uninlined-format-args + -A clippy::uninlined-format-args \ + -A clippy::enum_variant_names # Lints the code using Clippy and automatically fix some simple compiler warnings. lint-fix: diff --git a/account_manager/src/validator/slashing_protection.rs b/account_manager/src/validator/slashing_protection.rs index c6d81275a5..0a98a452b8 100644 --- a/account_manager/src/validator/slashing_protection.rs +++ b/account_manager/src/validator/slashing_protection.rs @@ -16,7 +16,6 @@ pub const EXPORT_CMD: &str = "export"; pub const IMPORT_FILE_ARG: &str = "IMPORT-FILE"; pub const EXPORT_FILE_ARG: &str = "EXPORT-FILE"; -pub const MINIFY_FLAG: &str = "minify"; pub const PUBKEYS_FLAG: &str = "pubkeys"; pub fn cli_app<'a, 'b>() -> App<'a, 'b> { @@ -31,16 +30,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .value_name("FILE") .help("The slashing protection interchange file to import (.json)"), ) - .arg( - Arg::with_name(MINIFY_FLAG) - .long(MINIFY_FLAG) - .takes_value(true) - .possible_values(&["false", "true"]) - .help( - "Deprecated: Lighthouse no longer requires minification on import \ - because it always minifies", - ), - ), ) .subcommand( App::new(EXPORT_CMD) @@ -61,17 +50,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { comma-separated. All known keys will be exported if omitted", ), ) - .arg( - Arg::with_name(MINIFY_FLAG) - .long(MINIFY_FLAG) - .takes_value(true) - .default_value("false") - .possible_values(&["false", "true"]) - .help( - "Minify the output file. This will make it smaller and faster to \ - import, but not faster to generate.", - ), - ), ) } @@ -92,7 +70,6 @@ pub fn cli_run( match matches.subcommand() { (IMPORT_CMD, Some(matches)) => { let import_filename: PathBuf = clap_utils::parse_required(matches, IMPORT_FILE_ARG)?; - let minify: Option = clap_utils::parse_optional(matches, MINIFY_FLAG)?; let import_file = File::open(&import_filename).map_err(|e| { format!( "Unable to open import file at {}: {:?}", @@ -102,23 +79,10 @@ pub fn cli_run( })?; eprint!("Loading JSON file into memory & deserializing"); - let mut interchange = Interchange::from_json_reader(&import_file) + let interchange = Interchange::from_json_reader(&import_file) .map_err(|e| format!("Error parsing file for import: {:?}", e))?; eprintln!(" [done]."); - if let Some(minify) = minify { - eprintln!( - "WARNING: --minify flag is deprecated and will be removed in a future release" - ); - if minify { - eprint!("Minifying input file for faster loading"); - interchange = interchange - .minify() - .map_err(|e| format!("Minification failed: {:?}", e))?; - eprintln!(" [done]."); - } - } - let slashing_protection_database = SlashingDatabase::open_or_create(&slashing_protection_db_path).map_err(|e| { format!( @@ -206,7 +170,6 @@ pub fn cli_run( } (EXPORT_CMD, Some(matches)) => { let export_filename: PathBuf = clap_utils::parse_required(matches, EXPORT_FILE_ARG)?; - let minify: bool = clap_utils::parse_required(matches, MINIFY_FLAG)?; let selected_pubkeys = if let Some(pubkeys) = clap_utils::parse_optional::(matches, PUBKEYS_FLAG)? @@ -237,17 +200,10 @@ pub fn cli_run( ) })?; - let mut interchange = slashing_protection_database + let interchange = slashing_protection_database .export_interchange_info(genesis_validators_root, selected_pubkeys.as_deref()) .map_err(|e| format!("Error during export: {:?}", e))?; - if minify { - eprintln!("Minifying output file"); - interchange = interchange - .minify() - .map_err(|e| format!("Unable to minify output: {:?}", e))?; - } - let output_file = File::create(export_filename) .map_err(|e| format!("Error creating output file: {:?}", e))?; diff --git a/beacon_node/beacon_chain/src/attestation_rewards.rs b/beacon_node/beacon_chain/src/attestation_rewards.rs index 992c7a479e..abd676d738 100644 --- a/beacon_node/beacon_chain/src/attestation_rewards.rs +++ b/beacon_node/beacon_chain/src/attestation_rewards.rs @@ -5,7 +5,9 @@ use participation_cache::ParticipationCache; use safe_arith::SafeArith; use serde_utils::quoted_u64::Quoted; use slog::debug; -use state_processing::per_epoch_processing::altair::process_inactivity_updates; +use state_processing::per_epoch_processing::altair::{ + process_inactivity_updates, process_justification_and_finalization, +}; use state_processing::{ common::altair::BaseRewardPerIncrement, per_epoch_processing::altair::{participation_cache, rewards_and_penalties::get_flag_weight}, @@ -27,6 +29,7 @@ use state_processing::per_epoch_processing::base::rewards_and_penalties::{ }; use state_processing::per_epoch_processing::base::validator_statuses::InclusionInfo; use state_processing::per_epoch_processing::base::{ + process_justification_and_finalization as process_justification_and_finalization_base, TotalBalances, ValidatorStatus, ValidatorStatuses, }; @@ -67,6 +70,13 @@ impl BeaconChain { let mut validator_statuses = ValidatorStatuses::new(&state, spec)?; validator_statuses.process_attestations(&state)?; + process_justification_and_finalization_base( + &state, + &validator_statuses.total_balances, + spec, + )? + .apply_changes_to_state(&mut state); + let ideal_rewards = self.compute_ideal_rewards_base(&state, &validator_statuses.total_balances)?; @@ -125,6 +135,8 @@ impl BeaconChain { // Calculate ideal_rewards let participation_cache = ParticipationCache::new(&state, spec)?; + process_justification_and_finalization(&state, &participation_cache)? + .apply_changes_to_state(&mut state); process_inactivity_updates(&mut state, &participation_cache, spec)?; let previous_epoch = state.previous_epoch(); diff --git a/beacon_node/beacon_chain/src/beacon_block_reward.rs b/beacon_node/beacon_chain/src/beacon_block_reward.rs index 8cbeae371e..d05f7cb4ff 100644 --- a/beacon_node/beacon_chain/src/beacon_block_reward.rs +++ b/beacon_node/beacon_chain/src/beacon_block_reward.rs @@ -33,6 +33,17 @@ impl BeaconChain { state.build_committee_cache(RelativeEpoch::Previous, &self.spec)?; state.build_committee_cache(RelativeEpoch::Current, &self.spec)?; + self.compute_beacon_block_reward_with_cache(block, block_root, state) + } + + // This should only be called after a committee cache has been built + // for both the previous and current epoch + fn compute_beacon_block_reward_with_cache>( + &self, + block: BeaconBlockRef<'_, T::EthSpec, Payload>, + block_root: Hash256, + state: &BeaconState, + ) -> Result { let proposer_index = block.proposer_index(); let sync_aggregate_reward = @@ -178,7 +189,7 @@ impl BeaconChain { >( &self, block: BeaconBlockRef<'_, T::EthSpec, Payload>, - state: &mut BeaconState, + state: &BeaconState, ) -> Result { let total_active_balance = state.get_total_active_balance()?; let base_reward_per_increment = @@ -191,6 +202,9 @@ impl BeaconChain { .safe_mul(WEIGHT_DENOMINATOR)? .safe_div(PROPOSER_WEIGHT)?; + let mut current_epoch_participation = state.current_epoch_participation()?.clone(); + let mut previous_epoch_participation = state.previous_epoch_participation()?.clone(); + for attestation in block.body().attestations() { let data = &attestation.data; let inclusion_delay = state.slot().safe_sub(data.slot)?.as_u64(); @@ -203,13 +217,16 @@ impl BeaconChain { )?; let attesting_indices = get_attesting_indices_from_state(state, attestation)?; - let mut proposer_reward_numerator = 0; for index in attesting_indices { let index = index as usize; for (flag_index, &weight) in PARTICIPATION_FLAG_WEIGHTS.iter().enumerate() { - let epoch_participation = - state.get_epoch_participation_mut(data.target.epoch)?; + let epoch_participation = if data.target.epoch == state.current_epoch() { + &mut current_epoch_participation + } else { + &mut previous_epoch_participation + }; + let validator_participation = epoch_participation .get_mut(index) .ok_or(BeaconStateError::ParticipationOutOfBounds(index))?; diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 9f59733934..6b893d6967 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -18,7 +18,7 @@ use crate::block_verification::{ use crate::block_verification_types::{ AsBlock, AvailableExecutedBlock, BlockImportData, ExecutedBlock, RpcBlock, }; -pub use crate::canonical_head::{CanonicalHead, CanonicalHeadRwLock}; +pub use crate::canonical_head::CanonicalHead; use crate::chain_config::ChainConfig; use crate::data_availability_checker::{ Availability, AvailabilityCheckError, AvailableBlock, DataAvailabilityChecker, @@ -72,8 +72,8 @@ use crate::{ }; use eth2::types::{EventKind, SseBlobSidecar, SseBlock, SseExtendedPayloadAttributes, SyncDuty}; use execution_layer::{ - BlockProposalContents, BuilderParams, ChainHealth, ExecutionLayer, FailedCondition, - PayloadAttributes, PayloadStatus, + BlockProposalContents, BlockProposalContentsType, BuilderParams, ChainHealth, ExecutionLayer, + FailedCondition, PayloadAttributes, PayloadStatus, }; use fork_choice::{ AttestationFromBlock, ExecutionStatus, ForkChoice, ForkchoiceUpdateParameters, @@ -120,6 +120,7 @@ use tokio_stream::Stream; use tree_hash::TreeHash; use types::beacon_state::CloneConfig; use types::blob_sidecar::{BlobSidecarList, FixedBlobSidecarList}; +use types::payload::BlockProductionVersion; use types::sidecar::BlobItems; use types::*; @@ -320,8 +321,7 @@ pub trait BeaconChainTypes: Send + Sync + 'static { type EthSpec: types::EthSpec; } -/// Used internally to split block production into discrete functions. -struct PartialBeaconBlock> { +struct PartialBeaconBlock { state: BeaconState, slot: Slot, proposer_index: u64, @@ -335,7 +335,7 @@ struct PartialBeaconBlock> { deposits: Vec, voluntary_exits: Vec, sync_aggregate: Option>, - prepare_payload_handle: Option>, + prepare_payload_handle: Option>, bls_to_execution_changes: Vec, } @@ -453,7 +453,7 @@ pub struct BeaconChain { /// A cache of eth1 deposit data at epoch boundaries for deposit finalization pub eth1_finalization_cache: TimeoutRwLock, /// Caches the beacon block proposer shuffling for a given epoch and shuffling key root. - pub beacon_proposer_cache: Mutex, + pub beacon_proposer_cache: Arc>, /// Caches a map of `validator_index -> validator_pubkey`. pub(crate) validator_pubkey_cache: TimeoutRwLock>, /// A cache used when producing attestations. @@ -484,11 +484,18 @@ pub struct BeaconChain { pub kzg: Option>, } -type BeaconBlockAndState = ( - BeaconBlock, - BeaconState, - Option>::Sidecar>>, -); +pub enum BeaconBlockResponseType { + Full(BeaconBlockResponse>), + Blinded(BeaconBlockResponse>), +} + +pub struct BeaconBlockResponse> { + pub block: BeaconBlock, + pub state: BeaconState, + pub maybe_side_car: Option>::Sidecar>>, + pub execution_payload_value: Option, + pub consensus_block_value: Option, +} impl FinalizationAndCanonicity { pub fn is_finalized(self) -> bool { @@ -3949,38 +3956,16 @@ impl BeaconChain { Ok(()) } - /// Produce a new block at the given `slot`. - /// - /// The produced block will not be inherently valid, it must be signed by a block producer. - /// Block signing is out of the scope of this function and should be done by a separate program. - pub async fn produce_block + 'static>( - self: &Arc, - randao_reveal: Signature, - slot: Slot, - validator_graffiti: Option, - ) -> Result, BlockProductionError> { - self.produce_block_with_verification( - randao_reveal, - slot, - validator_graffiti, - ProduceBlockVerification::VerifyRandao, - ) - .await - } - - /// Same as `produce_block` but allowing for configuration of RANDAO-verification. - pub async fn produce_block_with_verification< - Payload: AbstractExecPayload + 'static, - >( + pub async fn produce_block_with_verification( self: &Arc, randao_reveal: Signature, slot: Slot, validator_graffiti: Option, verification: ProduceBlockVerification, - ) -> Result, BlockProductionError> { + block_production_version: BlockProductionVersion, + ) -> Result, BlockProductionError> { metrics::inc_counter(&metrics::BLOCK_PRODUCTION_REQUESTS); let _complete_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_TIMES); - // Part 1/2 (blocking) // // Load the parent state from disk. @@ -3998,13 +3983,14 @@ impl BeaconChain { // Part 2/2 (async, with some blocking components) // // Produce the block upon the state - self.produce_block_on_state::( + self.produce_block_on_state( state, state_root_opt, slot, randao_reveal, validator_graffiti, verification, + block_production_version, ) .await } @@ -4568,7 +4554,8 @@ impl BeaconChain { /// The provided `state_root_opt` should only ever be set to `Some` if the contained value is /// equal to the root of `state`. Providing this value will serve as an optimization to avoid /// performing a tree hash in some scenarios. - pub async fn produce_block_on_state + 'static>( + #[allow(clippy::too_many_arguments)] + pub async fn produce_block_on_state( self: &Arc, state: BeaconState, state_root_opt: Option, @@ -4576,7 +4563,8 @@ impl BeaconChain { randao_reveal: Signature, validator_graffiti: Option, verification: ProduceBlockVerification, - ) -> Result, BlockProductionError> { + block_production_version: BlockProductionVersion, + ) -> Result, BlockProductionError> { // Part 1/3 (blocking) // // Perform the state advance and block-packing functions. @@ -4591,6 +4579,7 @@ impl BeaconChain { produce_at_slot, randao_reveal, validator_graffiti, + block_production_version, ) }, "produce_partial_beacon_block", @@ -4598,50 +4587,96 @@ impl BeaconChain { .ok_or(BlockProductionError::ShuttingDown)? .await .map_err(BlockProductionError::TokioJoin)??; - // Part 2/3 (async) // // Wait for the execution layer to return an execution payload (if one is required). let prepare_payload_handle = partial_beacon_block.prepare_payload_handle.take(); - let block_contents = if let Some(prepare_payload_handle) = prepare_payload_handle { - Some( - prepare_payload_handle - .await - .map_err(BlockProductionError::TokioJoin)? - .ok_or(BlockProductionError::ShuttingDown)??, - ) - } else { - None - }; - + let block_contents_type_option = + if let Some(prepare_payload_handle) = prepare_payload_handle { + Some( + prepare_payload_handle + .await + .map_err(BlockProductionError::TokioJoin)? + .ok_or(BlockProductionError::ShuttingDown)??, + ) + } else { + None + }; // Part 3/3 (blocking) - // - // Perform the final steps of combining all the parts and computing the state root. - let chain = self.clone(); - self.task_executor - .spawn_blocking_handle( - move || { - chain.complete_partial_beacon_block( - partial_beacon_block, - block_contents, - verification, - ) - }, - "complete_partial_beacon_block", - ) - .ok_or(BlockProductionError::ShuttingDown)? - .await - .map_err(BlockProductionError::TokioJoin)? + if let Some(block_contents_type) = block_contents_type_option { + match block_contents_type { + BlockProposalContentsType::Full(block_contents) => { + let chain = self.clone(); + let beacon_block_response = self + .task_executor + .spawn_blocking_handle( + move || { + chain.complete_partial_beacon_block( + partial_beacon_block, + Some(block_contents), + verification, + ) + }, + "complete_partial_beacon_block", + ) + .ok_or(BlockProductionError::ShuttingDown)? + .await + .map_err(BlockProductionError::TokioJoin)??; + + Ok(BeaconBlockResponseType::Full(beacon_block_response)) + } + BlockProposalContentsType::Blinded(block_contents) => { + let chain = self.clone(); + let beacon_block_response = self + .task_executor + .spawn_blocking_handle( + move || { + chain.complete_partial_beacon_block( + partial_beacon_block, + Some(block_contents), + verification, + ) + }, + "complete_partial_beacon_block", + ) + .ok_or(BlockProductionError::ShuttingDown)? + .await + .map_err(BlockProductionError::TokioJoin)??; + + Ok(BeaconBlockResponseType::Blinded(beacon_block_response)) + } + } + } else { + let chain = self.clone(); + let beacon_block_response = self + .task_executor + .spawn_blocking_handle( + move || { + chain.complete_partial_beacon_block( + partial_beacon_block, + None, + verification, + ) + }, + "complete_partial_beacon_block", + ) + .ok_or(BlockProductionError::ShuttingDown)? + .await + .map_err(BlockProductionError::TokioJoin)??; + + Ok(BeaconBlockResponseType::Full(beacon_block_response)) + } } - fn produce_partial_beacon_block + 'static>( + fn produce_partial_beacon_block( self: &Arc, mut state: BeaconState, state_root_opt: Option, produce_at_slot: Slot, randao_reveal: Signature, validator_graffiti: Option, - ) -> Result, BlockProductionError> { + block_production_version: BlockProductionVersion, + ) -> Result, BlockProductionError> { let eth1_chain = self .eth1_chain .as_ref() @@ -4701,6 +4736,7 @@ impl BeaconChain { parent_root, proposer_index, builder_params, + block_production_version, )?; Some(prepare_payload_handle) } @@ -4710,6 +4746,7 @@ impl BeaconChain { self.op_pool.get_slashings_and_exits(&state, &self.spec); let eth1_data = eth1_chain.eth1_data_for_block_production(&state, &self.spec)?; + let deposits = eth1_chain.deposits_for_block_inclusion(&state, ð1_data, &self.spec)?; let bls_to_execution_changes = self @@ -4880,10 +4917,10 @@ impl BeaconChain { fn complete_partial_beacon_block>( &self, - partial_beacon_block: PartialBeaconBlock, + partial_beacon_block: PartialBeaconBlock, block_contents: Option>, verification: ProduceBlockVerification, - ) -> Result, BlockProductionError> { + ) -> Result, BlockProductionError> { let PartialBeaconBlock { mut state, slot, @@ -4905,7 +4942,7 @@ impl BeaconChain { bls_to_execution_changes, } = partial_beacon_block; - let (inner_block, blobs_opt, proofs_opt) = match &state { + let (inner_block, blobs_opt, proofs_opt, execution_payload_value) = match &state { BeaconState::Base(_) => ( BeaconBlock::Base(BeaconBlockBase { slot, @@ -4926,6 +4963,7 @@ impl BeaconChain { }), None, None, + Uint256::zero(), ), BeaconState::Altair(_) => ( BeaconBlock::Altair(BeaconBlockAltair { @@ -4949,11 +4987,12 @@ impl BeaconChain { }), None, None, + Uint256::zero(), ), BeaconState::Merge(_) => { - let (payload, _, _, _) = block_contents - .ok_or(BlockProductionError::MissingExecutionPayload)? - .deconstruct(); + let block_proposal_contents = + block_contents.ok_or(BlockProductionError::MissingExecutionPayload)?; + let execution_payload_value = block_proposal_contents.block_value().to_owned(); ( BeaconBlock::Merge(BeaconBlockMerge { slot, @@ -4971,19 +5010,22 @@ impl BeaconChain { voluntary_exits: voluntary_exits.into(), sync_aggregate: sync_aggregate .ok_or(BlockProductionError::MissingSyncAggregate)?, - execution_payload: payload + execution_payload: block_proposal_contents + .to_payload() .try_into() .map_err(|_| BlockProductionError::InvalidPayloadFork)?, }, }), None, None, + execution_payload_value, ) } BeaconState::Capella(_) => { - let (payload, _, _, _) = block_contents - .ok_or(BlockProductionError::MissingExecutionPayload)? - .deconstruct(); + let block_proposal_contents = + block_contents.ok_or(BlockProductionError::MissingExecutionPayload)?; + let execution_payload_value = block_proposal_contents.block_value().to_owned(); + ( BeaconBlock::Capella(BeaconBlockCapella { slot, @@ -5001,7 +5043,8 @@ impl BeaconChain { voluntary_exits: voluntary_exits.into(), sync_aggregate: sync_aggregate .ok_or(BlockProductionError::MissingSyncAggregate)?, - execution_payload: payload + execution_payload: block_proposal_contents + .to_payload() .try_into() .map_err(|_| BlockProductionError::InvalidPayloadFork)?, bls_to_execution_changes: bls_to_execution_changes.into(), @@ -5009,12 +5052,15 @@ impl BeaconChain { }), None, None, + execution_payload_value, ) } BeaconState::Deneb(_) => { - let (payload, kzg_commitments, blobs, proofs) = block_contents - .ok_or(BlockProductionError::MissingExecutionPayload)? - .deconstruct(); + let (payload, kzg_commitments, blobs, proofs, execution_payload_value) = + block_contents + .ok_or(BlockProductionError::MissingExecutionPayload)? + .deconstruct(); + ( BeaconBlock::Deneb(BeaconBlockDeneb { slot, @@ -5042,6 +5088,7 @@ impl BeaconChain { }), blobs, proofs, + execution_payload_value, ) } }; @@ -5057,7 +5104,6 @@ impl BeaconChain { self.log, "Produced block on state"; "block_size" => block_size, - "slot" => block.slot(), ); metrics::observe(&metrics::BLOCK_SIZE, block_size as f64); @@ -5075,6 +5121,11 @@ impl BeaconChain { // Use a context without block root or proposer index so that both are checked. let mut ctxt = ConsensusContext::new(block.slot()); + let consensus_block_value = self + .compute_beacon_block_reward(block.message(), Hash256::zero(), &mut state) + .map(|reward| reward.total) + .unwrap_or(0); + per_block_processing( &mut state, &block, @@ -5154,7 +5205,13 @@ impl BeaconChain { "slot" => block.slot() ); - Ok((block, state, maybe_sidecar_list)) + Ok(BeaconBlockResponse { + block, + state, + maybe_side_car: maybe_sidecar_list, + execution_payload_value: Some(execution_payload_value), + consensus_block_value: Some(consensus_block_value), + }) } /// This method must be called whenever an execution engine indicates that a payload is @@ -6389,6 +6446,39 @@ impl BeaconChain { pub fn data_availability_boundary(&self) -> Option { self.data_availability_checker.data_availability_boundary() } + + /// Gets the `LightClientBootstrap` object for a requested block root. + /// + /// Returns `None` when the state or block is not found in the database. + #[allow(clippy::type_complexity)] + pub fn get_light_client_bootstrap( + &self, + block_root: &Hash256, + ) -> Result, ForkName)>, Error> { + let Some((state_root, slot)) = self + .get_blinded_block(block_root)? + .map(|block| (block.state_root(), block.slot())) + else { + return Ok(None); + }; + + let Some(mut state) = self.get_state(&state_root, Some(slot))? else { + return Ok(None); + }; + + let fork_name = state + .fork_name(&self.spec) + .map_err(Error::InconsistentFork)?; + + match fork_name { + ForkName::Altair | ForkName::Merge => { + LightClientBootstrap::from_beacon_state(&mut state) + .map(|bootstrap| Some((bootstrap, fork_name))) + .map_err(Error::LightClientError) + } + ForkName::Base | ForkName::Capella | ForkName::Deneb => Err(Error::UnsupportedFork), + } + } } impl Drop for BeaconChain { diff --git a/beacon_node/beacon_chain/src/beacon_proposer_cache.rs b/beacon_node/beacon_chain/src/beacon_proposer_cache.rs index eae71bd63e..b51592cadd 100644 --- a/beacon_node/beacon_chain/src/beacon_proposer_cache.rs +++ b/beacon_node/beacon_chain/src/beacon_proposer_cache.rs @@ -25,7 +25,7 @@ const CACHE_SIZE: usize = 16; /// This value is fairly unimportant, it's used to avoid heap allocations. The result of it being /// incorrect is non-substantial from a consensus perspective (and probably also from a /// performance perspective). -const TYPICAL_SLOTS_PER_EPOCH: usize = 32; +pub const TYPICAL_SLOTS_PER_EPOCH: usize = 32; /// For some given slot, this contains the proposer index (`index`) and the `fork` that should be /// used to verify their signature. diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 7f1a596ec3..65cf7a728b 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -583,29 +583,33 @@ pub fn signature_verify_chain_segment( &chain.spec, )?; + // unzip chain segment and verify kzg in bulk + let (roots, blocks): (Vec<_>, Vec<_>) = chain_segment.into_iter().unzip(); + let maybe_available_blocks = chain + .data_availability_checker + .verify_kzg_for_rpc_blocks(blocks)?; + // zip it back up + let mut signature_verified_blocks = roots + .into_iter() + .zip(maybe_available_blocks) + .map(|(block_root, maybe_available_block)| { + let consensus_context = ConsensusContext::new(maybe_available_block.slot()) + .set_current_block_root(block_root); + SignatureVerifiedBlock { + block: maybe_available_block, + block_root, + parent: None, + consensus_context, + } + }) + .collect::>(); + + // verify signatures let pubkey_cache = get_validator_pubkey_cache(chain)?; let mut signature_verifier = get_signature_verifier(&state, &pubkey_cache, &chain.spec); - - let mut signature_verified_blocks = Vec::with_capacity(chain_segment.len()); - - for (block_root, block) in &chain_segment { - let mut consensus_context = - ConsensusContext::new(block.slot()).set_current_block_root(*block_root); - - signature_verifier.include_all_signatures(block.as_block(), &mut consensus_context)?; - - let maybe_available_block = chain - .data_availability_checker - .check_rpc_block_availability(block.clone())?; - - // Save the block and its consensus context. The context will have had its proposer index - // and attesting indices filled in, which can be used to accelerate later block processing. - signature_verified_blocks.push(SignatureVerifiedBlock { - block: maybe_available_block, - block_root: *block_root, - parent: None, - consensus_context, - }); + for svb in &mut signature_verified_blocks { + signature_verifier + .include_all_signatures(svb.block.as_block(), &mut svb.consensus_context)?; } if signature_verifier.verify().is_err() { @@ -1159,10 +1163,7 @@ impl IntoExecutionPendingBlock for Arc IntoExecutionPendingBlock for RpcBlock .map_err(|e| BlockSlashInfo::SignatureNotChecked(self.signed_block_header(), e))?; let maybe_available = chain .data_availability_checker - .check_rpc_block_availability(self.clone()) + .verify_kzg_for_rpc_block(self.clone()) .map_err(|e| { BlockSlashInfo::SignatureNotChecked( self.signed_block_header(), diff --git a/beacon_node/beacon_chain/src/block_verification_types.rs b/beacon_node/beacon_chain/src/block_verification_types.rs index d236e94f93..9cd853ba8c 100644 --- a/beacon_node/beacon_chain/src/block_verification_types.rs +++ b/beacon_node/beacon_chain/src/block_verification_types.rs @@ -45,6 +45,13 @@ impl RpcBlock { RpcBlockInner::BlockAndBlobs(block, _) => block, } } + + pub fn blobs(&self) -> Option<&BlobSidecarList> { + match &self.block { + RpcBlockInner::Block(_) => None, + RpcBlockInner::BlockAndBlobs(_, blobs) => Some(blobs), + } + } } /// Note: This variant is intentionally private because we want to safely construct the diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index fd8a3f0460..bffb23aeb7 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -1,4 +1,5 @@ use crate::beacon_chain::{CanonicalHead, BEACON_CHAIN_DB_KEY, ETH1_CACHE_DB_KEY, OP_POOL_DB_KEY}; +use crate::beacon_proposer_cache::BeaconProposerCache; use crate::data_availability_checker::DataAvailabilityChecker; use crate::eth1_chain::{CachingEth1Backend, SszEth1}; use crate::eth1_finalization_cache::Eth1FinalizationCache; @@ -10,7 +11,7 @@ use crate::persisted_beacon_chain::PersistedBeaconChain; use crate::shuffling_cache::{BlockShufflingIds, ShufflingCache}; use crate::snapshot_cache::{SnapshotCache, DEFAULT_SNAPSHOT_CACHE_SIZE}; use crate::timeout_rw_lock::TimeoutRwLock; -use crate::validator_monitor::ValidatorMonitor; +use crate::validator_monitor::{ValidatorMonitor, ValidatorMonitorConfig}; use crate::validator_pubkey_cache::ValidatorPubkeyCache; use crate::ChainConfig; use crate::{ @@ -23,10 +24,10 @@ use fork_choice::{ForkChoice, ResetPayloadStatuses}; use futures::channel::mpsc::Sender; use kzg::{Kzg, TrustedSetup}; use operation_pool::{OperationPool, PersistedOperationPool}; -use parking_lot::RwLock; +use parking_lot::{Mutex, RwLock}; use proto_array::{DisallowedReOrgOffsets, ReOrgThreshold}; use slasher::Slasher; -use slog::{crit, debug, error, info, Logger}; +use slog::{crit, debug, error, info, o, Logger}; use slot_clock::{SlotClock, TestingSlotClock}; use state_processing::per_slot_processing; use std::marker::PhantomData; @@ -35,8 +36,8 @@ use std::time::Duration; use store::{Error as StoreError, HotColdDB, ItemStore, KeyValueStoreOp}; use task_executor::{ShutdownReason, TaskExecutor}; use types::{ - BeaconBlock, BeaconState, ChainSpec, Checkpoint, Epoch, EthSpec, Graffiti, Hash256, - PublicKeyBytes, Signature, SignedBeaconBlock, Slot, + BeaconBlock, BeaconState, ChainSpec, Checkpoint, Epoch, EthSpec, Graffiti, Hash256, Signature, + SignedBeaconBlock, Slot, }; /// An empty struct used to "witness" all the `BeaconChainTypes` traits. It has no user-facing @@ -93,12 +94,12 @@ pub struct BeaconChainBuilder { log: Option, graffiti: Graffiti, slasher: Option>>, - validator_monitor: Option>, // Pending I/O batch that is constructed during building and should be executed atomically // alongside `PersistedBeaconChain` storage when `BeaconChainBuilder::build` is called. pending_io_batch: Vec, trusted_setup: Option, task_executor: Option, + validator_monitor_config: Option, } impl @@ -135,10 +136,10 @@ where log: None, graffiti: Graffiti::default(), slasher: None, - validator_monitor: None, pending_io_batch: vec![], trusted_setup: None, task_executor: None, + validator_monitor_config: None, } } @@ -623,19 +624,8 @@ where /// Register some validators for additional monitoring. /// /// `validators` is a comma-separated string of 0x-formatted BLS pubkeys. - pub fn monitor_validators( - mut self, - auto_register: bool, - validators: Vec, - individual_metrics_threshold: usize, - log: Logger, - ) -> Self { - self.validator_monitor = Some(ValidatorMonitor::new( - validators, - auto_register, - individual_metrics_threshold, - log.clone(), - )); + pub fn validator_monitor_config(mut self, config: ValidatorMonitorConfig) -> Self { + self.validator_monitor_config = Some(config); self } @@ -671,11 +661,16 @@ where let genesis_state_root = self .genesis_state_root .ok_or("Cannot build without a genesis state root")?; - let mut validator_monitor = self - .validator_monitor - .ok_or("Cannot build without a validator monitor")?; + let validator_monitor_config = self.validator_monitor_config.unwrap_or_default(); let head_tracker = Arc::new(self.head_tracker.unwrap_or_default()); + let beacon_proposer_cache: Arc> = <_>::default(); + let mut validator_monitor = ValidatorMonitor::new( + validator_monitor_config, + beacon_proposer_cache.clone(), + log.new(o!("service" => "val_mon")), + ); + let current_slot = if slot_clock .is_prior_to_genesis() .ok_or("Unable to read slot clock")? @@ -911,7 +906,7 @@ where log.clone(), )), eth1_finalization_cache: TimeoutRwLock::new(Eth1FinalizationCache::new(log.clone())), - beacon_proposer_cache: <_>::default(), + beacon_proposer_cache, block_times_cache: <_>::default(), pre_finalization_block_cache: <_>::default(), validator_pubkey_cache: TimeoutRwLock::new(validator_pubkey_cache), @@ -1097,7 +1092,6 @@ fn descriptive_db_error(item: &str, error: &StoreError) -> String { mod test { use super::*; use crate::test_utils::EphemeralHarnessType; - use crate::validator_monitor::DEFAULT_INDIVIDUAL_TRACKING_THRESHOLD; use ethereum_hashing::hash; use genesis::{ generate_deterministic_keypairs, interop_genesis_state, DEFAULT_ETH1_BLOCK_HASH, @@ -1155,12 +1149,6 @@ mod test { .testing_slot_clock(Duration::from_secs(1)) .expect("should configure testing slot clock") .shutdown_sender(shutdown_tx) - .monitor_validators( - true, - vec![], - DEFAULT_INDIVIDUAL_TRACKING_THRESHOLD, - log.clone(), - ) .build() .expect("should build"); diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index a8d077a6d0..ad328077d0 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -240,9 +240,12 @@ impl DataAvailabilityChecker { .put_pending_executed_block(executed_block) } - /// Checks if a block is available, returns a `MaybeAvailableBlock` that may include the fully - /// available block. - pub fn check_rpc_block_availability( + /// Verifies kzg commitments for an RpcBlock, returns a `MaybeAvailableBlock` that may + /// include the fully available block. + /// + /// WARNING: This function assumes all required blobs are already present, it does NOT + /// check if there are any missing blobs. + pub fn verify_kzg_for_rpc_block( &self, block: RpcBlock, ) -> Result, AvailabilityCheckError> { @@ -279,6 +282,68 @@ impl DataAvailabilityChecker { } } + /// Checks if a vector of blocks are available. Returns a vector of `MaybeAvailableBlock` + /// This is more efficient than calling `verify_kzg_for_rpc_block` in a loop as it does + /// all kzg verification at once + /// + /// WARNING: This function assumes all required blobs are already present, it does NOT + /// check if there are any missing blobs. + pub fn verify_kzg_for_rpc_blocks( + &self, + blocks: Vec>, + ) -> Result>, AvailabilityCheckError> { + let mut results = Vec::with_capacity(blocks.len()); + let all_blobs: BlobSidecarList = blocks + .iter() + .filter(|block| self.blobs_required_for_block(block.as_block())) + // this clone is cheap as it's cloning an Arc + .filter_map(|block| block.blobs().cloned()) + .flatten() + .collect::>() + .into(); + + // verify kzg for all blobs at once + if !all_blobs.is_empty() { + let kzg = self + .kzg + .as_ref() + .ok_or(AvailabilityCheckError::KzgNotInitialized)?; + verify_kzg_for_blob_list(&all_blobs, kzg)?; + } + + for block in blocks { + let (block_root, block, blobs) = block.deconstruct(); + match blobs { + None => { + if self.blobs_required_for_block(&block) { + results.push(MaybeAvailableBlock::AvailabilityPending { block_root, block }) + } else { + results.push(MaybeAvailableBlock::Available(AvailableBlock { + block_root, + block, + blobs: None, + })) + } + } + Some(blob_list) => { + let verified_blobs = if self.blobs_required_for_block(&block) { + Some(blob_list) + } else { + None + }; + // already verified kzg for all blobs + results.push(MaybeAvailableBlock::Available(AvailableBlock { + block_root, + block, + blobs: verified_blobs, + })) + } + } + } + + Ok(results) + } + /// Determines the blob requirements for a block. If the block is pre-deneb, no blobs are required. /// If the block's epoch is from prior to the data availability boundary, no blobs are required. fn blobs_required_for_block(&self, block: &SignedBeaconBlock) -> bool { diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index e2eef45d25..6033293b82 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -771,12 +771,13 @@ mod test { ) -> Arc, LevelDB>> { let hot_path = db_path.path().join("hot_db"); let cold_path = db_path.path().join("cold_db"); + let blobs_path = db_path.path().join("blobs_db"); let config = StoreConfig::default(); HotColdDB::open( &hot_path, &cold_path, - None, + &blobs_path, |_, _, _| Ok(()), config, spec, diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 7c1bb04917..9c1ba06f85 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -221,6 +221,8 @@ pub enum BeaconChainError { ProposerHeadForkChoiceError(fork_choice::Error), UnableToPublish, AvailabilityCheckError(AvailabilityCheckError), + LightClientError(LightClientError), + UnsupportedFork, } easy_from_to!(SlotProcessingError, BeaconChainError); diff --git a/beacon_node/beacon_chain/src/events.rs b/beacon_node/beacon_chain/src/events.rs index 8d3a682794..0e5dfc8059 100644 --- a/beacon_node/beacon_chain/src/events.rs +++ b/beacon_node/beacon_chain/src/events.rs @@ -17,6 +17,8 @@ pub struct ServerSentEventHandler { contribution_tx: Sender>, payload_attributes_tx: Sender>, late_head: Sender>, + light_client_finality_update_tx: Sender>, + light_client_optimistic_update_tx: Sender>, block_reward_tx: Sender>, log: Logger, } @@ -40,6 +42,8 @@ impl ServerSentEventHandler { let (contribution_tx, _) = broadcast::channel(capacity); let (payload_attributes_tx, _) = broadcast::channel(capacity); let (late_head, _) = broadcast::channel(capacity); + let (light_client_finality_update_tx, _) = broadcast::channel(capacity); + let (light_client_optimistic_update_tx, _) = broadcast::channel(capacity); let (block_reward_tx, _) = broadcast::channel(capacity); Self { @@ -53,6 +57,8 @@ impl ServerSentEventHandler { contribution_tx, payload_attributes_tx, late_head, + light_client_finality_update_tx, + light_client_optimistic_update_tx, block_reward_tx, log, } @@ -108,6 +114,14 @@ impl ServerSentEventHandler { .late_head .send(kind) .map(|count| log_count("late head", count)), + EventKind::LightClientFinalityUpdate(_) => self + .light_client_finality_update_tx + .send(kind) + .map(|count| log_count("light client finality update", count)), + EventKind::LightClientOptimisticUpdate(_) => self + .light_client_optimistic_update_tx + .send(kind) + .map(|count| log_count("light client optimistic update", count)), EventKind::BlockReward(_) => self .block_reward_tx .send(kind) @@ -158,6 +172,14 @@ impl ServerSentEventHandler { self.late_head.subscribe() } + pub fn subscribe_light_client_finality_update(&self) -> Receiver> { + self.light_client_finality_update_tx.subscribe() + } + + pub fn subscribe_light_client_optimistic_update(&self) -> Receiver> { + self.light_client_optimistic_update_tx.subscribe() + } + pub fn subscribe_block_reward(&self) -> Receiver> { self.block_reward_tx.subscribe() } diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index 33c97efd26..093255b201 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -13,7 +13,8 @@ use crate::{ ExecutionPayloadError, }; use execution_layer::{ - BlockProposalContents, BuilderParams, NewPayloadRequest, PayloadAttributes, PayloadStatus, + BlockProposalContents, BlockProposalContentsType, BuilderParams, NewPayloadRequest, + PayloadAttributes, PayloadStatus, }; use fork_choice::{InvalidationOperation, PayloadVerificationStatus}; use proto_array::{Block as ProtoBlock, ExecutionStatus}; @@ -26,11 +27,11 @@ use state_processing::per_block_processing::{ use std::sync::Arc; use tokio::task::JoinHandle; use tree_hash::TreeHash; +use types::payload::BlockProductionVersion; use types::*; -pub type PreparePayloadResult = - Result, BlockProductionError>; -pub type PreparePayloadHandle = JoinHandle>>; +pub type PreparePayloadResult = Result, BlockProductionError>; +pub type PreparePayloadHandle = JoinHandle>>; #[derive(PartialEq)] pub enum AllowOptimisticImport { @@ -398,16 +399,14 @@ pub fn validate_execution_payload_for_gossip( /// Equivalent to the `get_execution_payload` function in the Validator Guide: /// /// https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/merge/validator.md#block-proposal -pub fn get_execution_payload< - T: BeaconChainTypes, - Payload: AbstractExecPayload + 'static, ->( +pub fn get_execution_payload( chain: Arc>, state: &BeaconState, parent_block_root: Hash256, proposer_index: u64, builder_params: BuilderParams, -) -> Result, BlockProductionError> { + block_production_version: BlockProductionVersion, +) -> Result, BlockProductionError> { // Compute all required values from the `state` now to avoid needing to pass it into a spawned // task. let spec = &chain.spec; @@ -440,7 +439,7 @@ pub fn get_execution_payload< .clone() .spawn_handle( async move { - prepare_execution_payload::( + prepare_execution_payload::( &chain, is_merge_transition_complete, timestamp, @@ -450,6 +449,7 @@ pub fn get_execution_payload< builder_params, withdrawals, parent_beacon_block_root, + block_production_version, ) .await }, @@ -475,7 +475,7 @@ pub fn get_execution_payload< /// /// https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/merge/validator.md#block-proposal #[allow(clippy::too_many_arguments)] -pub async fn prepare_execution_payload( +pub async fn prepare_execution_payload( chain: &Arc>, is_merge_transition_complete: bool, timestamp: u64, @@ -485,10 +485,10 @@ pub async fn prepare_execution_payload( builder_params: BuilderParams, withdrawals: Option>, parent_beacon_block_root: Option, -) -> Result, BlockProductionError> + block_production_version: BlockProductionVersion, +) -> Result, BlockProductionError> where T: BeaconChainTypes, - Payload: AbstractExecPayload, { let current_epoch = builder_params.slot.epoch(T::EthSpec::slots_per_epoch()); let spec = &chain.spec; @@ -506,7 +506,12 @@ where if is_terminal_block_hash_set && !is_activation_epoch_reached { // Use the "empty" payload if there's a terminal block hash, but we haven't reached the // terminal block epoch yet. - return BlockProposalContents::default_at_fork(fork).map_err(Into::into); + return Ok(BlockProposalContentsType::Full( + BlockProposalContents::Payload { + payload: FullPayload::default_at_fork(fork)?, + block_value: Uint256::zero(), + }, + )); } let terminal_pow_block_hash = execution_layer @@ -519,7 +524,12 @@ where } else { // If the merge transition hasn't occurred yet and the EL hasn't found the terminal // block, return an "empty" payload. - return BlockProposalContents::default_at_fork(fork).map_err(Into::into); + return Ok(BlockProposalContentsType::Full( + BlockProposalContents::Payload { + payload: FullPayload::default_at_fork(fork)?, + block_value: Uint256::zero(), + }, + )); } } else { latest_execution_payload_header_block_hash @@ -558,13 +568,14 @@ where // // This future is not executed here, it's up to the caller to await it. let block_contents = execution_layer - .get_payload::( + .get_payload( parent_hash, &payload_attributes, forkchoice_update_params, builder_params, fork, &chain.spec, + block_production_version, ) .await .map_err(BlockProductionError::GetPayloadFailed)?; diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 4efc776b2c..e2d37078ac 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -56,10 +56,10 @@ pub mod validator_monitor; pub mod validator_pubkey_cache; pub use self::beacon_chain::{ - AttestationProcessingOutcome, AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, - BeaconStore, ChainSegmentResult, ForkChoiceError, OverrideForkchoiceUpdate, - ProduceBlockVerification, StateSkipConfig, WhenSlotSkipped, - INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON, + AttestationProcessingOutcome, AvailabilityProcessingStatus, BeaconBlockResponse, + BeaconBlockResponseType, BeaconChain, BeaconChainTypes, BeaconStore, ChainSegmentResult, + ForkChoiceError, OverrideForkchoiceUpdate, ProduceBlockVerification, StateSkipConfig, + WhenSlotSkipped, INVALID_FINALIZED_MERGE_TRANSITION_BLOCK_SHUTDOWN_REASON, INVALID_JUSTIFIED_PAYLOAD_SHUTDOWN_REASON, }; pub use self::beacon_snapshot::BeaconSnapshot; diff --git a/beacon_node/beacon_chain/src/light_client_finality_update_verification.rs b/beacon_node/beacon_chain/src/light_client_finality_update_verification.rs index 86cc6695ed..791d63ccfe 100644 --- a/beacon_node/beacon_chain/src/light_client_finality_update_verification.rs +++ b/beacon_node/beacon_chain/src/light_client_finality_update_verification.rs @@ -67,7 +67,7 @@ impl VerifiedLightClientFinalityUpdate { chain: &BeaconChain, seen_timestamp: Duration, ) -> Result { - let gossiped_finality_slot = light_client_finality_update.finalized_header.slot; + let gossiped_finality_slot = light_client_finality_update.finalized_header.beacon.slot; let one_third_slot_duration = Duration::new(chain.spec.seconds_per_slot / 3, 0); let signature_slot = light_client_finality_update.signature_slot; let start_time = chain.slot_clock.start_of(signature_slot); @@ -88,7 +88,7 @@ impl VerifiedLightClientFinalityUpdate { .get_blinded_block(&finalized_block_root)? .ok_or(Error::FailedConstructingUpdate)?; let latest_seen_finality_update_slot = match latest_seen_finality_update.as_ref() { - Some(update) => update.finalized_header.slot, + Some(update) => update.finalized_header.beacon.slot, None => Slot::new(0), }; diff --git a/beacon_node/beacon_chain/src/light_client_optimistic_update_verification.rs b/beacon_node/beacon_chain/src/light_client_optimistic_update_verification.rs index 8b7e644527..374cc9a775 100644 --- a/beacon_node/beacon_chain/src/light_client_optimistic_update_verification.rs +++ b/beacon_node/beacon_chain/src/light_client_optimistic_update_verification.rs @@ -71,7 +71,7 @@ impl VerifiedLightClientOptimisticUpdate { chain: &BeaconChain, seen_timestamp: Duration, ) -> Result { - let gossiped_optimistic_slot = light_client_optimistic_update.attested_header.slot; + let gossiped_optimistic_slot = light_client_optimistic_update.attested_header.beacon.slot; let one_third_slot_duration = Duration::new(chain.spec.seconds_per_slot / 3, 0); let signature_slot = light_client_optimistic_update.signature_slot; let start_time = chain.slot_clock.start_of(signature_slot); @@ -88,7 +88,7 @@ impl VerifiedLightClientOptimisticUpdate { .get_state(&attested_block.state_root(), Some(attested_block.slot()))? .ok_or(Error::FailedConstructingUpdate)?; let latest_seen_optimistic_update_slot = match latest_seen_optimistic_update.as_ref() { - Some(update) => update.attested_header.slot, + Some(update) => update.attested_header.beacon.slot, None => Slot::new(0), }; @@ -114,6 +114,7 @@ impl VerifiedLightClientOptimisticUpdate { // otherwise queue let canonical_root = light_client_optimistic_update .attested_header + .beacon .canonical_root(); if canonical_root != head_block.message().parent_root() { diff --git a/beacon_node/beacon_chain/src/metrics.rs b/beacon_node/beacon_chain/src/metrics.rs index a23bcdc0b5..0fe68ba19e 100644 --- a/beacon_node/beacon_chain/src/metrics.rs +++ b/beacon_node/beacon_chain/src/metrics.rs @@ -1033,6 +1033,11 @@ lazy_static! { "beacon_aggregated_attestation_subsets_total", "Count of new aggregated attestations that are subsets of already known aggregates" ); + pub static ref VALIDATOR_MONITOR_MISSED_BLOCKS_TOTAL: Result = try_create_int_counter_vec( + "validator_monitor_missed_blocks_total", + "Number of non-finalized blocks missed", + &["validator"] + ); /* * Kzg related metrics diff --git a/beacon_node/beacon_chain/src/observed_blob_sidecars.rs b/beacon_node/beacon_chain/src/observed_blob_sidecars.rs index 6c2f07ff59..f16f38bad5 100644 --- a/beacon_node/beacon_chain/src/observed_blob_sidecars.rs +++ b/beacon_node/beacon_chain/src/observed_blob_sidecars.rs @@ -87,7 +87,7 @@ impl ObservedBlobSidecars { Ok(()) } - /// Prune all values earlier than the given slot. + /// Prune `blob_sidecar` observations for slots less than or equal to the given slot. pub fn prune(&mut self, finalized_slot: Slot) { if finalized_slot == 0 { return; diff --git a/beacon_node/beacon_chain/src/schema_change.rs b/beacon_node/beacon_chain/src/schema_change.rs index 88b5682505..e42ee20c48 100644 --- a/beacon_node/beacon_chain/src/schema_change.rs +++ b/beacon_node/beacon_chain/src/schema_change.rs @@ -1,20 +1,14 @@ //! Utilities for managing database schema changes. -mod migration_schema_v12; -mod migration_schema_v13; -mod migration_schema_v14; -mod migration_schema_v15; -mod migration_schema_v16; mod migration_schema_v17; mod migration_schema_v18; -use crate::beacon_chain::{BeaconChainTypes, ETH1_CACHE_DB_KEY}; -use crate::eth1_chain::SszEth1; +use crate::beacon_chain::BeaconChainTypes; use crate::types::ChainSpec; -use slog::{warn, Logger}; +use slog::Logger; use std::sync::Arc; use store::hot_cold_store::{HotColdDB, HotColdDBError}; use store::metadata::{SchemaVersion, CURRENT_SCHEMA_VERSION}; -use store::{Error as StoreError, StoreItem}; +use store::Error as StoreError; /// Migrate the database from one schema version to another, applying all requisite mutations. #[allow(clippy::only_used_in_recursion)] // spec is not used but likely to be used in future @@ -57,92 +51,8 @@ pub fn migrate_schema( } // - // Migrations from before SchemaVersion(11) are deprecated. + // Migrations from before SchemaVersion(16) are deprecated. // - - // Upgrade from v11 to v12 to store richer metadata in the attestation op pool. - (SchemaVersion(11), SchemaVersion(12)) => { - let ops = migration_schema_v12::upgrade_to_v12::(db.clone(), log)?; - db.store_schema_version_atomically(to, ops) - } - // Downgrade from v12 to v11 to drop richer metadata from the attestation op pool. - (SchemaVersion(12), SchemaVersion(11)) => { - let ops = migration_schema_v12::downgrade_from_v12::(db.clone(), log)?; - db.store_schema_version_atomically(to, ops) - } - (SchemaVersion(12), SchemaVersion(13)) => { - let mut ops = vec![]; - if let Some(persisted_eth1_v1) = db.get_item::(Ð1_CACHE_DB_KEY)? { - let upgraded_eth1_cache = - match migration_schema_v13::update_eth1_cache(persisted_eth1_v1) { - Ok(upgraded_eth1) => upgraded_eth1, - Err(e) => { - warn!(log, "Failed to deserialize SszEth1CacheV1"; "error" => ?e); - warn!(log, "Reinitializing eth1 cache"); - migration_schema_v13::reinitialized_eth1_cache_v13( - deposit_contract_deploy_block, - ) - } - }; - ops.push(upgraded_eth1_cache.as_kv_store_op(ETH1_CACHE_DB_KEY)); - } - - db.store_schema_version_atomically(to, ops)?; - - Ok(()) - } - (SchemaVersion(13), SchemaVersion(12)) => { - let mut ops = vec![]; - if let Some(persisted_eth1_v13) = db.get_item::(Ð1_CACHE_DB_KEY)? { - let downgraded_eth1_cache = match migration_schema_v13::downgrade_eth1_cache( - persisted_eth1_v13, - ) { - Ok(Some(downgraded_eth1)) => downgraded_eth1, - Ok(None) => { - warn!(log, "Unable to downgrade eth1 cache from newer version: reinitializing eth1 cache"); - migration_schema_v13::reinitialized_eth1_cache_v1( - deposit_contract_deploy_block, - ) - } - Err(e) => { - warn!(log, "Unable to downgrade eth1 cache from newer version: failed to deserialize SszEth1CacheV13"; "error" => ?e); - warn!(log, "Reinitializing eth1 cache"); - migration_schema_v13::reinitialized_eth1_cache_v1( - deposit_contract_deploy_block, - ) - } - }; - ops.push(downgraded_eth1_cache.as_kv_store_op(ETH1_CACHE_DB_KEY)); - } - - db.store_schema_version_atomically(to, ops)?; - - Ok(()) - } - (SchemaVersion(13), SchemaVersion(14)) => { - let ops = migration_schema_v14::upgrade_to_v14::(db.clone(), log)?; - db.store_schema_version_atomically(to, ops) - } - (SchemaVersion(14), SchemaVersion(13)) => { - let ops = migration_schema_v14::downgrade_from_v14::(db.clone(), log)?; - db.store_schema_version_atomically(to, ops) - } - (SchemaVersion(14), SchemaVersion(15)) => { - let ops = migration_schema_v15::upgrade_to_v15::(db.clone(), log)?; - db.store_schema_version_atomically(to, ops) - } - (SchemaVersion(15), SchemaVersion(14)) => { - let ops = migration_schema_v15::downgrade_from_v15::(db.clone(), log)?; - db.store_schema_version_atomically(to, ops) - } - (SchemaVersion(15), SchemaVersion(16)) => { - let ops = migration_schema_v16::upgrade_to_v16::(db.clone(), log)?; - db.store_schema_version_atomically(to, ops) - } - (SchemaVersion(16), SchemaVersion(15)) => { - let ops = migration_schema_v16::downgrade_from_v16::(db.clone(), log)?; - db.store_schema_version_atomically(to, ops) - } (SchemaVersion(16), SchemaVersion(17)) => { let ops = migration_schema_v17::upgrade_to_v17::(db.clone(), log)?; db.store_schema_version_atomically(to, ops) diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v12.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v12.rs deleted file mode 100644 index ab267796e1..0000000000 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v12.rs +++ /dev/null @@ -1,222 +0,0 @@ -use crate::beacon_chain::{BeaconChainTypes, FORK_CHOICE_DB_KEY, OP_POOL_DB_KEY}; -use crate::persisted_fork_choice::PersistedForkChoiceV11; -use operation_pool::{PersistedOperationPool, PersistedOperationPoolV12, PersistedOperationPoolV5}; -use slog::{debug, info, Logger}; -use state_processing::{ - common::get_indexed_attestation, per_block_processing::is_valid_indexed_attestation, - VerifyOperation, VerifySignatures, -}; -use std::sync::Arc; -use store::{Error, HotColdDB, KeyValueStoreOp, StoreItem}; - -pub fn upgrade_to_v12( - db: Arc>, - log: Logger, -) -> Result, Error> { - let spec = db.get_chain_spec(); - - // Load a V5 op pool and transform it to V12. - let Some(PersistedOperationPoolV5 { - attestations_v5, - sync_contributions, - attester_slashings_v5, - proposer_slashings_v5, - voluntary_exits_v5, - }) = db.get_item(&OP_POOL_DB_KEY)? - else { - debug!(log, "Nothing to do, no operation pool stored"); - return Ok(vec![]); - }; - - // Load the persisted fork choice so we can grab the state of the justified block and use - // it to verify the stored attestations, slashings and exits. - let fork_choice = db - .get_item::(&FORK_CHOICE_DB_KEY)? - .ok_or_else(|| Error::SchemaMigrationError("fork choice missing from database".into()))?; - let justified_block_root = fork_choice - .fork_choice_store - .unrealized_justified_checkpoint - .root; - let justified_block = db - .get_blinded_block(&justified_block_root)? - .ok_or_else(|| { - Error::SchemaMigrationError(format!( - "unrealized justified block missing for migration: {justified_block_root:?}", - )) - })?; - let justified_state_root = justified_block.state_root(); - let mut state = db - .get_state(&justified_state_root, Some(justified_block.slot()))? - .ok_or_else(|| { - Error::SchemaMigrationError(format!( - "justified state missing for migration: {justified_state_root:?}" - )) - })?; - state.build_all_committee_caches(spec).map_err(|e| { - Error::SchemaMigrationError(format!("unable to build committee caches: {e:?}")) - })?; - - // Re-verify attestations while adding attesting indices. - let attestations = attestations_v5 - .into_iter() - .flat_map(|(_, attestations)| attestations) - .filter_map(|attestation| { - let res = state - .get_beacon_committee(attestation.data.slot, attestation.data.index) - .map_err(Into::into) - .and_then(|committee| get_indexed_attestation(committee.committee, &attestation)) - .and_then(|indexed_attestation| { - is_valid_indexed_attestation( - &state, - &indexed_attestation, - VerifySignatures::True, - spec, - )?; - Ok(indexed_attestation) - }); - - match res { - Ok(indexed) => Some((attestation, indexed.attesting_indices.into())), - Err(e) => { - debug!( - log, - "Dropping attestation on migration"; - "err" => ?e, - "head_block" => ?attestation.data.beacon_block_root, - ); - None - } - } - }) - .collect::>(); - - let attester_slashings = attester_slashings_v5 - .iter() - .filter_map(|(slashing, _)| { - slashing - .clone() - .validate(&state, spec) - .map_err(|e| { - debug!( - log, - "Dropping attester slashing on migration"; - "err" => ?e, - "slashing" => ?slashing, - ); - }) - .ok() - }) - .collect::>(); - - let proposer_slashings = proposer_slashings_v5 - .iter() - .filter_map(|slashing| { - slashing - .clone() - .validate(&state, spec) - .map_err(|e| { - debug!( - log, - "Dropping proposer slashing on migration"; - "err" => ?e, - "slashing" => ?slashing, - ); - }) - .ok() - }) - .collect::>(); - - let voluntary_exits = voluntary_exits_v5 - .iter() - .filter_map(|exit| { - exit.clone() - .validate(&state, spec) - .map_err(|e| { - debug!( - log, - "Dropping voluntary exit on migration"; - "err" => ?e, - "exit" => ?exit, - ); - }) - .ok() - }) - .collect::>(); - - debug!( - log, - "Migrated op pool"; - "attestations" => attestations.len(), - "attester_slashings" => attester_slashings.len(), - "proposer_slashings" => proposer_slashings.len(), - "voluntary_exits" => voluntary_exits.len() - ); - - let v12 = PersistedOperationPool::V12(PersistedOperationPoolV12 { - attestations, - sync_contributions, - attester_slashings, - proposer_slashings, - voluntary_exits, - }); - Ok(vec![v12.as_kv_store_op(OP_POOL_DB_KEY)]) -} - -pub fn downgrade_from_v12( - db: Arc>, - log: Logger, -) -> Result, Error> { - // Load a V12 op pool and transform it to V5. - let Some(PersistedOperationPoolV12:: { - attestations, - sync_contributions, - attester_slashings, - proposer_slashings, - voluntary_exits, - }) = db.get_item(&OP_POOL_DB_KEY)? - else { - debug!(log, "Nothing to do, no operation pool stored"); - return Ok(vec![]); - }; - - info!( - log, - "Dropping attestations from pool"; - "count" => attestations.len(), - ); - - let attester_slashings_v5 = attester_slashings - .into_iter() - .filter_map(|slashing| { - let fork_version = slashing.first_fork_verified_against()?; - Some((slashing.into_inner(), fork_version)) - }) - .collect::>(); - - let proposer_slashings_v5 = proposer_slashings - .into_iter() - .map(|slashing| slashing.into_inner()) - .collect::>(); - - let voluntary_exits_v5 = voluntary_exits - .into_iter() - .map(|exit| exit.into_inner()) - .collect::>(); - - info!( - log, - "Migrated slashings and exits"; - "attester_slashings" => attester_slashings_v5.len(), - "proposer_slashings" => proposer_slashings_v5.len(), - "voluntary_exits" => voluntary_exits_v5.len(), - ); - - let v5 = PersistedOperationPoolV5 { - attestations_v5: vec![], - sync_contributions, - attester_slashings_v5, - proposer_slashings_v5, - voluntary_exits_v5, - }; - Ok(vec![v5.as_kv_store_op(OP_POOL_DB_KEY)]) -} diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v13.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v13.rs deleted file mode 100644 index d4ac974603..0000000000 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v13.rs +++ /dev/null @@ -1,150 +0,0 @@ -use crate::eth1_chain::SszEth1; -use eth1::{BlockCache, SszDepositCacheV1, SszDepositCacheV13, SszEth1CacheV1, SszEth1CacheV13}; -use ssz::{Decode, Encode}; -use state_processing::common::DepositDataTree; -use store::Error; -use types::DEPOSIT_TREE_DEPTH; - -pub fn update_eth1_cache(persisted_eth1_v1: SszEth1) -> Result { - if persisted_eth1_v1.use_dummy_backend { - // backend_bytes is empty when using dummy backend - return Ok(persisted_eth1_v1); - } - - let SszEth1 { - use_dummy_backend, - backend_bytes, - } = persisted_eth1_v1; - - let ssz_eth1_cache_v1 = SszEth1CacheV1::from_ssz_bytes(&backend_bytes)?; - let SszEth1CacheV1 { - block_cache, - deposit_cache: deposit_cache_v1, - last_processed_block, - } = ssz_eth1_cache_v1; - - let SszDepositCacheV1 { - logs, - leaves, - deposit_contract_deploy_block, - deposit_roots, - } = deposit_cache_v1; - - let deposit_cache_v13 = SszDepositCacheV13 { - logs, - leaves, - deposit_contract_deploy_block, - finalized_deposit_count: 0, - finalized_block_height: deposit_contract_deploy_block.saturating_sub(1), - deposit_tree_snapshot: None, - deposit_roots, - }; - - let ssz_eth1_cache_v13 = SszEth1CacheV13 { - block_cache, - deposit_cache: deposit_cache_v13, - last_processed_block, - }; - - let persisted_eth1_v13 = SszEth1 { - use_dummy_backend, - backend_bytes: ssz_eth1_cache_v13.as_ssz_bytes(), - }; - - Ok(persisted_eth1_v13) -} - -pub fn downgrade_eth1_cache(persisted_eth1_v13: SszEth1) -> Result, Error> { - if persisted_eth1_v13.use_dummy_backend { - // backend_bytes is empty when using dummy backend - return Ok(Some(persisted_eth1_v13)); - } - - let SszEth1 { - use_dummy_backend, - backend_bytes, - } = persisted_eth1_v13; - - let ssz_eth1_cache_v13 = SszEth1CacheV13::from_ssz_bytes(&backend_bytes)?; - let SszEth1CacheV13 { - block_cache, - deposit_cache: deposit_cache_v13, - last_processed_block, - } = ssz_eth1_cache_v13; - - let SszDepositCacheV13 { - logs, - leaves, - deposit_contract_deploy_block, - finalized_deposit_count, - finalized_block_height: _, - deposit_tree_snapshot, - deposit_roots, - } = deposit_cache_v13; - - if finalized_deposit_count == 0 && deposit_tree_snapshot.is_none() { - // This tree was never finalized and can be directly downgraded to v1 without re-initializing - let deposit_cache_v1 = SszDepositCacheV1 { - logs, - leaves, - deposit_contract_deploy_block, - deposit_roots, - }; - let ssz_eth1_cache_v1 = SszEth1CacheV1 { - block_cache, - deposit_cache: deposit_cache_v1, - last_processed_block, - }; - return Ok(Some(SszEth1 { - use_dummy_backend, - backend_bytes: ssz_eth1_cache_v1.as_ssz_bytes(), - })); - } - // deposit cache was finalized; can't downgrade - Ok(None) -} - -pub fn reinitialized_eth1_cache_v13(deposit_contract_deploy_block: u64) -> SszEth1 { - let empty_tree = DepositDataTree::create(&[], 0, DEPOSIT_TREE_DEPTH); - let deposit_cache_v13 = SszDepositCacheV13 { - logs: vec![], - leaves: vec![], - deposit_contract_deploy_block, - finalized_deposit_count: 0, - finalized_block_height: deposit_contract_deploy_block.saturating_sub(1), - deposit_tree_snapshot: empty_tree.get_snapshot(), - deposit_roots: vec![empty_tree.root()], - }; - - let ssz_eth1_cache_v13 = SszEth1CacheV13 { - block_cache: BlockCache::default(), - deposit_cache: deposit_cache_v13, - last_processed_block: None, - }; - - SszEth1 { - use_dummy_backend: false, - backend_bytes: ssz_eth1_cache_v13.as_ssz_bytes(), - } -} - -pub fn reinitialized_eth1_cache_v1(deposit_contract_deploy_block: u64) -> SszEth1 { - let empty_tree = DepositDataTree::create(&[], 0, DEPOSIT_TREE_DEPTH); - let deposit_cache_v1 = SszDepositCacheV1 { - logs: vec![], - leaves: vec![], - deposit_contract_deploy_block, - deposit_roots: vec![empty_tree.root()], - }; - - let ssz_eth1_cache_v1 = SszEth1CacheV1 { - block_cache: BlockCache::default(), - deposit_cache: deposit_cache_v1, - last_processed_block: None, - }; - - SszEth1 { - use_dummy_backend: false, - backend_bytes: ssz_eth1_cache_v1.as_ssz_bytes(), - } -} diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v14.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v14.rs deleted file mode 100644 index 52a990dc6e..0000000000 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v14.rs +++ /dev/null @@ -1,118 +0,0 @@ -use crate::beacon_chain::{BeaconChainTypes, OP_POOL_DB_KEY}; -use operation_pool::{ - PersistedOperationPool, PersistedOperationPoolV12, PersistedOperationPoolV14, -}; -use slog::{debug, error, info, Logger}; -use slot_clock::SlotClock; -use std::sync::Arc; -use std::time::Duration; -use store::{Error, HotColdDB, KeyValueStoreOp, StoreItem}; -use types::{EthSpec, Hash256, Slot}; - -/// The slot clock isn't usually available before the database is initialized, so we construct a -/// temporary slot clock by reading the genesis state. It should always exist if the database is -/// initialized at a prior schema version, however we still handle the lack of genesis state -/// gracefully. -fn get_slot_clock( - db: &HotColdDB, - log: &Logger, -) -> Result, Error> { - let spec = db.get_chain_spec(); - let Some(genesis_block) = db.get_blinded_block(&Hash256::zero())? else { - error!(log, "Missing genesis block"); - return Ok(None); - }; - let Some(genesis_state) = db.get_state(&genesis_block.state_root(), Some(Slot::new(0)))? else { - error!(log, "Missing genesis state"; "state_root" => ?genesis_block.state_root()); - return Ok(None); - }; - Ok(Some(T::SlotClock::new( - spec.genesis_slot, - Duration::from_secs(genesis_state.genesis_time()), - Duration::from_secs(spec.seconds_per_slot), - ))) -} - -pub fn upgrade_to_v14( - db: Arc>, - log: Logger, -) -> Result, Error> { - // Load a V12 op pool and transform it to V14. - let Some(PersistedOperationPoolV12:: { - attestations, - sync_contributions, - attester_slashings, - proposer_slashings, - voluntary_exits, - }) = db.get_item(&OP_POOL_DB_KEY)? - else { - debug!(log, "Nothing to do, no operation pool stored"); - return Ok(vec![]); - }; - - // initialize with empty vector - let bls_to_execution_changes = vec![]; - let v14 = PersistedOperationPool::V14(PersistedOperationPoolV14 { - attestations, - sync_contributions, - attester_slashings, - proposer_slashings, - voluntary_exits, - bls_to_execution_changes, - }); - Ok(vec![v14.as_kv_store_op(OP_POOL_DB_KEY)]) -} - -pub fn downgrade_from_v14( - db: Arc>, - log: Logger, -) -> Result, Error> { - // We cannot downgrade from V14 once the Capella fork has been reached because there will - // be HistoricalSummaries stored in the database instead of HistoricalRoots and prior versions - // of Lighthouse can't handle that. - if let Some(capella_fork_epoch) = db.get_chain_spec().capella_fork_epoch { - let current_epoch = get_slot_clock::(&db, &log)? - .and_then(|clock| clock.now()) - .map(|slot| slot.epoch(T::EthSpec::slots_per_epoch())) - .ok_or(Error::SlotClockUnavailableForMigration)?; - - if current_epoch >= capella_fork_epoch { - error!( - log, - "Capella already active: v14+ is mandatory"; - "current_epoch" => current_epoch, - "capella_fork_epoch" => capella_fork_epoch, - ); - return Err(Error::UnableToDowngrade); - } - } - - // Load a V14 op pool and transform it to V12. - let Some(PersistedOperationPoolV14:: { - attestations, - sync_contributions, - attester_slashings, - proposer_slashings, - voluntary_exits, - bls_to_execution_changes, - }) = db.get_item(&OP_POOL_DB_KEY)? - else { - debug!(log, "Nothing to do, no operation pool stored"); - return Ok(vec![]); - }; - - info!( - log, - "Dropping bls_to_execution_changes from pool"; - "count" => bls_to_execution_changes.len(), - ); - - let v12 = PersistedOperationPoolV12 { - attestations, - sync_contributions, - attester_slashings, - proposer_slashings, - voluntary_exits, - }; - Ok(vec![v12.as_kv_store_op(OP_POOL_DB_KEY)]) -} diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v15.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v15.rs deleted file mode 100644 index 0eb2c5fa3f..0000000000 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v15.rs +++ /dev/null @@ -1,74 +0,0 @@ -use crate::beacon_chain::{BeaconChainTypes, OP_POOL_DB_KEY}; -use operation_pool::{ - PersistedOperationPool, PersistedOperationPoolV14, PersistedOperationPoolV15, -}; -use slog::{debug, info, Logger}; -use std::sync::Arc; -use store::{Error, HotColdDB, KeyValueStoreOp, StoreItem}; - -pub fn upgrade_to_v15( - db: Arc>, - log: Logger, -) -> Result, Error> { - // Load a V14 op pool and transform it to V15. - let Some(PersistedOperationPoolV14:: { - attestations, - sync_contributions, - attester_slashings, - proposer_slashings, - voluntary_exits, - bls_to_execution_changes, - }) = db.get_item(&OP_POOL_DB_KEY)? - else { - debug!(log, "Nothing to do, no operation pool stored"); - return Ok(vec![]); - }; - - let v15 = PersistedOperationPool::V15(PersistedOperationPoolV15 { - attestations, - sync_contributions, - attester_slashings, - proposer_slashings, - voluntary_exits, - bls_to_execution_changes, - // Initialize with empty set - capella_bls_change_broadcast_indices: <_>::default(), - }); - Ok(vec![v15.as_kv_store_op(OP_POOL_DB_KEY)]) -} - -pub fn downgrade_from_v15( - db: Arc>, - log: Logger, -) -> Result, Error> { - // Load a V15 op pool and transform it to V14. - let Some(PersistedOperationPoolV15:: { - attestations, - sync_contributions, - attester_slashings, - proposer_slashings, - voluntary_exits, - bls_to_execution_changes, - capella_bls_change_broadcast_indices, - }) = db.get_item(&OP_POOL_DB_KEY)? - else { - debug!(log, "Nothing to do, no operation pool stored"); - return Ok(vec![]); - }; - - info!( - log, - "Forgetting address changes for Capella broadcast"; - "count" => capella_bls_change_broadcast_indices.len(), - ); - - let v14 = PersistedOperationPoolV14 { - attestations, - sync_contributions, - attester_slashings, - proposer_slashings, - voluntary_exits, - bls_to_execution_changes, - }; - Ok(vec![v14.as_kv_store_op(OP_POOL_DB_KEY)]) -} diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v16.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v16.rs deleted file mode 100644 index 230573b028..0000000000 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v16.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::beacon_chain::{BeaconChainTypes, FORK_CHOICE_DB_KEY}; -use crate::persisted_fork_choice::PersistedForkChoiceV11; -use slog::{debug, Logger}; -use std::sync::Arc; -use store::{Error, HotColdDB, KeyValueStoreOp, StoreItem}; - -pub fn upgrade_to_v16( - db: Arc>, - log: Logger, -) -> Result, Error> { - drop_balances_cache::(db, log) -} - -pub fn downgrade_from_v16( - db: Arc>, - log: Logger, -) -> Result, Error> { - drop_balances_cache::(db, log) -} - -/// Drop the balances cache from the fork choice store. -/// -/// There aren't any type-level changes in this schema migration, however the -/// way that we compute the `JustifiedBalances` has changed due to: -/// https://github.com/sigp/lighthouse/pull/3962 -pub fn drop_balances_cache( - db: Arc>, - log: Logger, -) -> Result, Error> { - let mut persisted_fork_choice = db - .get_item::(&FORK_CHOICE_DB_KEY)? - .ok_or_else(|| Error::SchemaMigrationError("fork choice missing from database".into()))?; - - debug!( - log, - "Dropping fork choice balances cache"; - "item_count" => persisted_fork_choice.fork_choice_store.balances_cache.items.len() - ); - - // Drop all items in the balances cache. - persisted_fork_choice.fork_choice_store.balances_cache = <_>::default(); - - let kv_op = persisted_fork_choice.as_kv_store_op(FORK_CHOICE_DB_KEY); - - Ok(vec![kv_op]) -} diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 7ef9842544..23af0c8126 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -1,11 +1,12 @@ use crate::block_verification_types::{AsBlock, RpcBlock}; use crate::observed_operations::ObservationOutcome; pub use crate::persisted_beacon_chain::PersistedBeaconChain; +use crate::BeaconBlockResponseType; pub use crate::{ beacon_chain::{BEACON_CHAIN_DB_KEY, ETH1_CACHE_DB_KEY, FORK_CHOICE_DB_KEY, OP_POOL_DB_KEY}, migrate::MigratorConfig, sync_committee_verification::Error as SyncCommitteeError, - validator_monitor::DEFAULT_INDIVIDUAL_TRACKING_THRESHOLD, + validator_monitor::{ValidatorMonitor, ValidatorMonitorConfig}, BeaconChainError, NotifyExecutionLayer, ProduceBlockVerification, }; use crate::{ @@ -60,6 +61,7 @@ use store::{config::StoreConfig, HotColdDB, ItemStore, LevelDB, MemoryStore}; use task_executor::TaskExecutor; use task_executor::{test_utils::TestRuntime, ShutdownReason}; use tree_hash::TreeHash; +use types::payload::BlockProductionVersion; use types::sync_selection_proof::SyncSelectionProof; pub use types::test_utils::generate_deterministic_keypairs; use types::test_utils::TestRandom; @@ -179,6 +181,7 @@ pub struct Builder { execution_layer: Option>, mock_execution_layer: Option>, testing_slot_clock: Option, + validator_monitor_config: Option, runtime: TestRuntime, log: Logger, } @@ -314,6 +317,7 @@ where execution_layer: None, mock_execution_layer: None, testing_slot_clock: None, + validator_monitor_config: None, runtime, log, } @@ -386,6 +390,14 @@ where self } + pub fn validator_monitor_config( + mut self, + validator_monitor_config: ValidatorMonitorConfig, + ) -> Self { + self.validator_monitor_config = Some(validator_monitor_config); + self + } + /// Purposefully replace the `store_mutator`. pub fn override_store_mutator(mut self, mutator: BoxedMutator) -> Self { assert!(self.store_mutator.is_some(), "store mutator not set"); @@ -492,11 +504,13 @@ where let validator_keypairs = self .validator_keypairs .expect("cannot build without validator keypairs"); - let chain_config = self.chain_config.unwrap_or_default(); let trusted_setup: TrustedSetup = serde_json::from_reader(TRUSTED_SETUP_BYTES) .map_err(|e| format!("Unable to read trusted setup file: {}", e)) .unwrap(); + let validator_monitor_config = self.validator_monitor_config.unwrap_or_default(); + + let chain_config = self.chain_config.unwrap_or_default(); let mut builder = BeaconChainBuilder::new(self.eth_spec_instance) .logger(log.clone()) .custom_spec(spec) @@ -516,7 +530,7 @@ where log.clone(), 5, ))) - .monitor_validators(true, vec![], DEFAULT_INDIVIDUAL_TRACKING_THRESHOLD, log) + .validator_monitor_config(validator_monitor_config) .trusted_setup(trusted_setup); builder = if let Some(mutator) = self.initial_mutator { @@ -878,7 +892,7 @@ where let randao_reveal = self.sign_randao_reveal(&state, proposer_index, slot); - let (block, state, maybe_blob_sidecars) = self + let BeaconBlockResponseType::Full(block_response) = self .chain .produce_block_on_state( state, @@ -887,14 +901,18 @@ where randao_reveal, Some(graffiti), ProduceBlockVerification::VerifyRandao, + BlockProductionVersion::FullV2, ) .await - .unwrap(); + .unwrap() + else { + panic!("Should always be a full payload response"); + }; - let signed_block = block.sign( + let signed_block = block_response.block.sign( &self.validator_keypairs[proposer_index].sk, - &state.fork(), - state.genesis_validators_root(), + &block_response.state.fork(), + block_response.state.genesis_validators_root(), &self.spec, ); @@ -905,11 +923,13 @@ where | SignedBeaconBlock::Capella(_) => (signed_block, None), SignedBeaconBlock::Deneb(_) => ( signed_block, - maybe_blob_sidecars.map(|blobs| self.sign_blobs(blobs, &state, proposer_index)), + block_response + .maybe_side_car + .map(|blobs| self.sign_blobs(blobs, &block_response.state, proposer_index)), ), }; - (block_contents, state) + (block_contents, block_response.state) } /// Useful for the `per_block_processing` tests. Creates a block, and returns the state after @@ -938,7 +958,7 @@ where let pre_state = state.clone(); - let (block, state, maybe_blob_sidecars) = self + let BeaconBlockResponseType::Full(block_response) = self .chain .produce_block_on_state( state, @@ -947,14 +967,18 @@ where randao_reveal, Some(graffiti), ProduceBlockVerification::VerifyRandao, + BlockProductionVersion::FullV2, ) .await - .unwrap(); + .unwrap() + else { + panic!("Should always be a full payload response"); + }; - let signed_block = block.sign( + let signed_block = block_response.block.sign( &self.validator_keypairs[proposer_index].sk, - &state.fork(), - state.genesis_validators_root(), + &block_response.state.fork(), + block_response.state.genesis_validators_root(), &self.spec, ); @@ -964,14 +988,14 @@ where | SignedBeaconBlock::Merge(_) | SignedBeaconBlock::Capella(_) => (signed_block, None), SignedBeaconBlock::Deneb(_) => { - if let Some(blobs) = maybe_blob_sidecars { + if let Some(blobs) = block_response.maybe_side_car { let signed_blobs: SignedSidecarList> = Vec::from(blobs) .into_iter() .map(|blob| { blob.sign( &self.validator_keypairs[proposer_index].sk, - &state.fork(), - state.genesis_validators_root(), + &block_response.state.fork(), + block_response.state.genesis_validators_root(), &self.spec, ) }) @@ -990,7 +1014,6 @@ where } } }; - (block_contents, pre_state) } diff --git a/beacon_node/beacon_chain/src/validator_monitor.rs b/beacon_node/beacon_chain/src/validator_monitor.rs index 396aac71b0..8cea9c0769 100644 --- a/beacon_node/beacon_chain/src/validator_monitor.rs +++ b/beacon_node/beacon_chain/src/validator_monitor.rs @@ -2,10 +2,14 @@ //! //! This component should not affect consensus. +use crate::beacon_proposer_cache::{BeaconProposerCache, TYPICAL_SLOTS_PER_EPOCH}; use crate::metrics; -use parking_lot::RwLock; -use slog::{crit, debug, info, Logger}; +use itertools::Itertools; +use parking_lot::{Mutex, RwLock}; +use serde::{Deserialize, Serialize}; +use slog::{crit, debug, error, info, warn, Logger}; use slot_clock::SlotClock; +use smallvec::SmallVec; use state_processing::per_epoch_processing::{ errors::EpochProcessingError, EpochProcessingSummary, }; @@ -14,6 +18,7 @@ use std::convert::TryFrom; use std::io; use std::marker::PhantomData; use std::str::Utf8Error; +use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use store::AbstractExecPayload; use types::{ @@ -35,7 +40,34 @@ pub const HISTORIC_EPOCHS: usize = 10; /// Once the validator monitor reaches this number of validators it will stop /// tracking their metrics/logging individually in an effort to reduce /// Prometheus cardinality and log volume. -pub const DEFAULT_INDIVIDUAL_TRACKING_THRESHOLD: usize = 64; +const DEFAULT_INDIVIDUAL_TRACKING_THRESHOLD: usize = 64; + +/// Lag slots used in detecting missed blocks for the monitored validators +pub const MISSED_BLOCK_LAG_SLOTS: usize = 4; + +/// The number of epochs to look back when determining if a validator has missed a block. This value is used with +/// the beacon_proposer_cache to determine if a validator has missed a block. +/// And so, setting this value to anything higher than 1 is likely going to be problematic because the beacon_proposer_cache +/// is only populated for the current and the previous epoch. +pub const MISSED_BLOCK_LOOKBACK_EPOCHS: u64 = 1; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +// Initial configuration values for the `ValidatorMonitor`. +pub struct ValidatorMonitorConfig { + pub auto_register: bool, + pub validators: Vec, + pub individual_tracking_threshold: usize, +} + +impl Default for ValidatorMonitorConfig { + fn default() -> Self { + Self { + auto_register: false, + validators: vec![], + individual_tracking_threshold: DEFAULT_INDIVIDUAL_TRACKING_THRESHOLD, + } + } +} #[derive(Debug)] pub enum Error { @@ -323,6 +355,13 @@ impl MonitoredValidator { } } +#[derive(PartialEq, Hash, Eq)] +struct MissedBlock { + slot: Slot, + parent_root: Hash256, + validator_index: u64, +} + /// Holds a collection of `MonitoredValidator` and is notified about a variety of events on the P2P /// network, HTTP API and `BeaconChain`. /// @@ -343,26 +382,37 @@ pub struct ValidatorMonitor { /// large validator counts causing infeasibly high cardinailty for /// Prometheus and high log volumes. individual_tracking_threshold: usize, + /// A Map representing the (non-finalized) missed blocks by epoch, validator_index(state.validators) and slot + missed_blocks: HashSet, + // A beacon proposer cache + beacon_proposer_cache: Arc>, log: Logger, _phantom: PhantomData, } impl ValidatorMonitor { pub fn new( - pubkeys: Vec, - auto_register: bool, - individual_tracking_threshold: usize, + config: ValidatorMonitorConfig, + beacon_proposer_cache: Arc>, log: Logger, ) -> Self { + let ValidatorMonitorConfig { + auto_register, + validators, + individual_tracking_threshold, + } = config; + let mut s = Self { validators: <_>::default(), indices: <_>::default(), auto_register, individual_tracking_threshold, + missed_blocks: <_>::default(), + beacon_proposer_cache, log, _phantom: PhantomData, }; - for pubkey in pubkeys { + for pubkey in validators { s.add_validator_pubkey(pubkey) } s @@ -411,6 +461,9 @@ impl ValidatorMonitor { self.indices.insert(i, validator.pubkey); }); + // Add missed non-finalized blocks for the monitored validators + self.add_validators_missed_blocks(state); + // Update metrics for individual validators. for monitored_validator in self.validators.values() { if let Some(i) = monitored_validator.index { @@ -489,6 +542,116 @@ impl ValidatorMonitor { } } } + + // Prune missed blocks that are prior to last finalized epochs - MISSED_BLOCK_LOOKBACK_EPOCHS + let finalized_epoch = state.finalized_checkpoint().epoch; + self.missed_blocks.retain(|missed_block| { + let epoch = missed_block.slot.epoch(T::slots_per_epoch()); + epoch + Epoch::new(MISSED_BLOCK_LOOKBACK_EPOCHS) >= finalized_epoch + }); + } + + /// Add missed non-finalized blocks for the monitored validators + fn add_validators_missed_blocks(&mut self, state: &BeaconState) { + // Define range variables + let current_slot = state.slot(); + let current_epoch = current_slot.epoch(T::slots_per_epoch()); + // start_slot needs to be coherent with what can be retrieved from the beacon_proposer_cache + let start_slot = current_epoch.start_slot(T::slots_per_epoch()) + - Slot::new(MISSED_BLOCK_LOOKBACK_EPOCHS * T::slots_per_epoch()); + + let end_slot = current_slot.saturating_sub(MISSED_BLOCK_LAG_SLOTS).as_u64(); + + // List of proposers per epoch from the beacon_proposer_cache + let mut proposers_per_epoch: Option> = None; + + for (prev_slot, slot) in (start_slot.as_u64()..=end_slot) + .map(Slot::new) + .tuple_windows() + { + // Condition for missed_block is defined such as block_root(slot) == block_root(slot - 1) + // where the proposer who missed the block is the proposer of the block at block_root(slot) + if let (Ok(block_root), Ok(prev_block_root)) = + (state.get_block_root(slot), state.get_block_root(prev_slot)) + { + // Found missed block + if block_root == prev_block_root { + let slot_epoch = slot.epoch(T::slots_per_epoch()); + let prev_slot_epoch = prev_slot.epoch(T::slots_per_epoch()); + + if let Ok(shuffling_decision_block) = + state.proposer_shuffling_decision_root_at_epoch(slot_epoch, *block_root) + { + // Only update the cache if it needs to be initialised or because + // slot is at epoch + 1 + if proposers_per_epoch.is_none() || slot_epoch != prev_slot_epoch { + proposers_per_epoch = self.get_proposers_by_epoch_from_cache( + slot_epoch, + shuffling_decision_block, + ); + } + + // Only add missed blocks for the proposer if it's in the list of monitored validators + let slot_in_epoch = slot % T::slots_per_epoch(); + if let Some(proposer_index) = proposers_per_epoch + .as_deref() + .and_then(|proposers| proposers.get(slot_in_epoch.as_usize())) + { + let i = *proposer_index as u64; + if let Some(pub_key) = self.indices.get(&i) { + if let Some(validator) = self.validators.get(pub_key) { + let missed_block = MissedBlock { + slot, + parent_root: *prev_block_root, + validator_index: i, + }; + // Incr missed block counter for the validator only if it doesn't already exist in the hashset + if self.missed_blocks.insert(missed_block) { + self.aggregatable_metric(&validator.id, |label| { + metrics::inc_counter_vec( + &metrics::VALIDATOR_MONITOR_MISSED_BLOCKS_TOTAL, + &[label], + ); + }); + error!( + self.log, + "Validator missed a block"; + "index" => i, + "slot" => slot, + "parent block root" => ?prev_block_root, + ); + } + } + } else { + warn!( + self.log, + "Missing validator index"; + "info" => "potentially inconsistency in the validator manager", + "index" => i, + ) + } + } else { + debug!( + self.log, + "Could not get proposers from cache"; + "epoch" => ?slot_epoch + ); + } + } + } + } + } + } + + fn get_proposers_by_epoch_from_cache( + &mut self, + epoch: Epoch, + shuffling_decision_block: Hash256, + ) -> Option> { + let mut cache = self.beacon_proposer_cache.lock(); + cache + .get_epoch::(shuffling_decision_block, epoch) + .cloned() } /// Run `func` with the `TOTAL_LABEL` and optionally the @@ -822,6 +985,17 @@ impl ValidatorMonitor { } } + pub fn get_monitored_validator_missed_block_count(&self, validator_index: u64) -> u64 { + self.missed_blocks + .iter() + .filter(|missed_block| missed_block.validator_index == validator_index) + .count() as u64 + } + + pub fn get_beacon_proposer_cache(&self) -> Arc> { + self.beacon_proposer_cache.clone() + } + /// If `self.auto_register == true`, add the `validator_index` to `self.monitored_validators`. /// Otherwise, do nothing. pub fn auto_register_local_validator(&mut self, validator_index: u64) { diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index a8ad75304b..fdc37b5529 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -140,7 +140,7 @@ async fn produces_attestations() { available_block, ) = chain .data_availability_checker - .check_rpc_block_availability(rpc_block) + .verify_kzg_for_rpc_block(rpc_block) .unwrap() else { panic!("block should be available") @@ -218,7 +218,7 @@ async fn early_attester_cache_old_request() { harness .chain .data_availability_checker - .check_rpc_block_availability(rpc_block) + .verify_kzg_for_rpc_block(rpc_block) .unwrap() else { panic!("block should be available") diff --git a/beacon_node/beacon_chain/tests/main.rs b/beacon_node/beacon_chain/tests/main.rs index 332f6a4829..e0564e1510 100644 --- a/beacon_node/beacon_chain/tests/main.rs +++ b/beacon_node/beacon_chain/tests/main.rs @@ -10,3 +10,4 @@ mod rewards; mod store_tests; mod sync_committee_verification; mod tests; +mod validator_monitor; diff --git a/beacon_node/beacon_chain/tests/op_verification.rs b/beacon_node/beacon_chain/tests/op_verification.rs index f4af490710..f6cf40a396 100644 --- a/beacon_node/beacon_chain/tests/op_verification.rs +++ b/beacon_node/beacon_chain/tests/op_verification.rs @@ -29,12 +29,13 @@ fn get_store(db_path: &TempDir) -> Arc { let spec = test_spec::(); let hot_path = db_path.path().join("hot_db"); let cold_path = db_path.path().join("cold_db"); + let blobs_path = db_path.path().join("blobs_db"); let config = StoreConfig::default(); let log = NullLoggerBuilder.build().expect("logger should build"); HotColdDB::open( &hot_path, &cold_path, - None, + &blobs_path, |_, _, _| Ok(()), config, spec, diff --git a/beacon_node/beacon_chain/tests/rewards.rs b/beacon_node/beacon_chain/tests/rewards.rs index 7c8f01cf55..a78463ef5d 100644 --- a/beacon_node/beacon_chain/tests/rewards.rs +++ b/beacon_node/beacon_chain/tests/rewards.rs @@ -219,6 +219,156 @@ async fn test_verify_attestation_rewards_base_inactivity_leak() { assert_eq!(expected_balances, balances); } +#[tokio::test] +async fn test_verify_attestation_rewards_base_inactivity_leak_justification_epoch() { + let spec = E::default_spec(); + let harness = get_harness(spec.clone()); + + let half = VALIDATOR_COUNT / 2; + let half_validators: Vec = (0..half).collect(); + // target epoch is the epoch where the chain enters inactivity leak + let mut target_epoch = &spec.min_epochs_to_inactivity_penalty + 2; + + // advance until beginning of epoch N + 2 + harness + .extend_chain( + (E::slots_per_epoch() * (target_epoch + 1)) as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::SomeValidators(half_validators.clone()), + ) + .await; + + // advance to create first justification epoch and get initial balances + harness.extend_slots(E::slots_per_epoch() as usize).await; + target_epoch += 1; + let initial_balances: Vec = harness.get_current_state().balances().clone().into(); + + //assert previous_justified_checkpoint matches 0 as we were in inactivity leak from beginning + assert_eq!( + 0, + harness + .get_current_state() + .previous_justified_checkpoint() + .epoch + .as_u64() + ); + + // extend slots to beginning of epoch N + 1 + harness.extend_slots(E::slots_per_epoch() as usize).await; + + //assert target epoch and previous_justified_checkpoint match + assert_eq!( + target_epoch, + harness + .get_current_state() + .previous_justified_checkpoint() + .epoch + .as_u64() + ); + + // compute reward deltas for all validators in epoch N + let StandardAttestationRewards { + ideal_rewards, + total_rewards, + } = harness + .chain + .compute_attestation_rewards(Epoch::new(target_epoch), vec![]) + .unwrap(); + + // assert we successfully get ideal rewards for justified epoch out of inactivity leak + assert!(ideal_rewards + .iter() + .all(|reward| reward.head > 0 && reward.target > 0 && reward.source > 0)); + + // apply attestation rewards to initial balances + let expected_balances = apply_attestation_rewards(&initial_balances, total_rewards); + + // verify expected balances against actual balances + let balances: Vec = harness.get_current_state().balances().clone().into(); + assert_eq!(expected_balances, balances); +} + +#[tokio::test] +async fn test_verify_attestation_rewards_altair() { + let spec = ForkName::Altair.make_genesis_spec(E::default_spec()); + let harness = get_harness(spec.clone()); + let target_epoch = 0; + + // advance until epoch N + 1 and get initial balances + harness + .extend_slots((E::slots_per_epoch() * (target_epoch + 1)) as usize) + .await; + let initial_balances: Vec = harness.get_current_state().balances().clone().into(); + + // advance until epoch N + 2 and build proposal rewards map + let mut proposal_rewards_map: HashMap = HashMap::new(); + let mut sync_committee_rewards_map: HashMap = HashMap::new(); + for _ in 0..E::slots_per_epoch() { + let state = harness.get_current_state(); + let slot = state.slot() + Slot::new(1); + + // calculate beacon block rewards / penalties + let ((signed_block, _maybe_blob_sidecars), mut state) = + harness.make_block_return_pre_state(state, slot).await; + let beacon_block_reward = harness + .chain + .compute_beacon_block_reward( + signed_block.message(), + signed_block.canonical_root(), + &mut state, + ) + .unwrap(); + + let total_proposer_reward = proposal_rewards_map + .get(&beacon_block_reward.proposer_index) + .unwrap_or(&0u64) + + beacon_block_reward.total; + + proposal_rewards_map.insert(beacon_block_reward.proposer_index, total_proposer_reward); + + // calculate sync committee rewards / penalties + let reward_payload = harness + .chain + .compute_sync_committee_rewards(signed_block.message(), &mut state) + .unwrap(); + + reward_payload.iter().for_each(|reward| { + let mut amount = *sync_committee_rewards_map + .get(&reward.validator_index) + .unwrap_or(&0); + amount += reward.reward; + sync_committee_rewards_map.insert(reward.validator_index, amount); + }); + + harness.extend_slots(1).await; + } + + // compute reward deltas for all validators in epoch N + let StandardAttestationRewards { + ideal_rewards, + total_rewards, + } = harness + .chain + .compute_attestation_rewards(Epoch::new(target_epoch), vec![]) + .unwrap(); + + // assert ideal rewards are greater than 0 + assert!(ideal_rewards + .iter() + .all(|reward| reward.head > 0 && reward.target > 0 && reward.source > 0)); + + // apply attestation, proposal, and sync committee rewards and penalties to initial balances + let expected_balances = apply_attestation_rewards(&initial_balances, total_rewards); + let expected_balances = apply_beacon_block_rewards(&proposal_rewards_map, expected_balances); + let expected_balances = + apply_sync_committee_rewards(&sync_committee_rewards_map, expected_balances); + + // verify expected balances against actual balances + let balances: Vec = harness.get_current_state().balances().clone().into(); + + assert_eq!(expected_balances, balances); +} + #[tokio::test] async fn test_verify_attestation_rewards_altair_inactivity_leak() { let spec = ForkName::Altair.make_genesis_spec(E::default_spec()); @@ -313,6 +463,115 @@ async fn test_verify_attestation_rewards_altair_inactivity_leak() { assert_eq!(expected_balances, balances); } +#[tokio::test] +async fn test_verify_attestation_rewards_altair_inactivity_leak_justification_epoch() { + let spec = ForkName::Altair.make_genesis_spec(E::default_spec()); + let harness = get_harness(spec.clone()); + + let half = VALIDATOR_COUNT / 2; + let half_validators: Vec = (0..half).collect(); + // target epoch is the epoch where the chain enters inactivity leak + 1 + let mut target_epoch = &spec.min_epochs_to_inactivity_penalty + 2; + + // advance until beginning of epoch N + 1 + harness + .extend_slots_some_validators( + (E::slots_per_epoch() * (target_epoch + 1)) as usize, + half_validators.clone(), + ) + .await; + + let validator_inactivity_score = harness + .get_current_state() + .get_inactivity_score(VALIDATOR_COUNT - 1) + .unwrap(); + + //assert to ensure we are in inactivity leak + assert_eq!(4, validator_inactivity_score); + + // advance for first justification epoch and get balances + harness.extend_slots(E::slots_per_epoch() as usize).await; + target_epoch += 1; + let initial_balances: Vec = harness.get_current_state().balances().clone().into(); + + // advance until epoch N + 2 and build proposal rewards map + let mut proposal_rewards_map: HashMap = HashMap::new(); + let mut sync_committee_rewards_map: HashMap = HashMap::new(); + for _ in 0..E::slots_per_epoch() { + let state = harness.get_current_state(); + let slot = state.slot() + Slot::new(1); + + // calculate beacon block rewards / penalties + let ((signed_block, _maybe_blob_sidecars), mut state) = + harness.make_block_return_pre_state(state, slot).await; + let beacon_block_reward = harness + .chain + .compute_beacon_block_reward( + signed_block.message(), + signed_block.canonical_root(), + &mut state, + ) + .unwrap(); + + let total_proposer_reward = proposal_rewards_map + .get(&beacon_block_reward.proposer_index) + .unwrap_or(&0u64) + + beacon_block_reward.total; + + proposal_rewards_map.insert(beacon_block_reward.proposer_index, total_proposer_reward); + + // calculate sync committee rewards / penalties + let reward_payload = harness + .chain + .compute_sync_committee_rewards(signed_block.message(), &mut state) + .unwrap(); + + reward_payload.iter().for_each(|reward| { + let mut amount = *sync_committee_rewards_map + .get(&reward.validator_index) + .unwrap_or(&0); + amount += reward.reward; + sync_committee_rewards_map.insert(reward.validator_index, amount); + }); + + harness.extend_slots(1).await; + } + + //assert target epoch and previous_justified_checkpoint match + assert_eq!( + target_epoch, + harness + .get_current_state() + .previous_justified_checkpoint() + .epoch + .as_u64() + ); + + // compute reward deltas for all validators in epoch N + let StandardAttestationRewards { + ideal_rewards, + total_rewards, + } = harness + .chain + .compute_attestation_rewards(Epoch::new(target_epoch), vec![]) + .unwrap(); + + // assert ideal rewards are greater than 0 + assert!(ideal_rewards + .iter() + .all(|reward| reward.head > 0 && reward.target > 0 && reward.source > 0)); + + // apply attestation, proposal, and sync committee rewards and penalties to initial balances + let expected_balances = apply_attestation_rewards(&initial_balances, total_rewards); + let expected_balances = apply_beacon_block_rewards(&proposal_rewards_map, expected_balances); + let expected_balances = + apply_sync_committee_rewards(&sync_committee_rewards_map, expected_balances); + + // verify expected balances against actual balances + let balances: Vec = harness.get_current_state().balances().clone().into(); + assert_eq!(expected_balances, balances); +} + #[tokio::test] async fn test_verify_attestation_rewards_base_subset_only() { let harness = get_harness(E::default_spec()); diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index e8bb76778b..9f7199cf3c 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -8,7 +8,6 @@ use beacon_chain::test_utils::{ mock_execution_layer_from_parts, test_spec, AttestationStrategy, BeaconChainHarness, BlockStrategy, DiskHarnessType, }; -use beacon_chain::validator_monitor::DEFAULT_INDIVIDUAL_TRACKING_THRESHOLD; use beacon_chain::{ data_availability_checker::MaybeAvailableBlock, historical_blocks::HistoricalBlockError, migrate::MigratorConfig, BeaconChain, BeaconChainError, BeaconChainTypes, BeaconSnapshot, @@ -27,12 +26,12 @@ use std::collections::HashSet; use std::convert::TryInto; use std::sync::Arc; use std::time::Duration; -use store::metadata::{SchemaVersion, CURRENT_SCHEMA_VERSION}; +use store::metadata::{SchemaVersion, CURRENT_SCHEMA_VERSION, STATE_UPPER_LIMIT_NO_RETAIN}; use store::{ chunked_vector::{chunk_key, Field}, get_key_for_col, iter::{BlockRootsIterator, StateRootsIterator}, - DBColumn, HotColdDB, KeyValueStore, KeyValueStoreOp, LevelDB, StoreConfig, + BlobInfo, DBColumn, HotColdDB, KeyValueStore, KeyValueStoreOp, LevelDB, StoreConfig, }; use tempfile::{tempdir, TempDir}; use tokio::time::sleep; @@ -63,12 +62,13 @@ fn get_store_generic( ) -> Arc, LevelDB>> { let hot_path = db_path.path().join("hot_db"); let cold_path = db_path.path().join("cold_db"); + let blobs_path = db_path.path().join("blobs_db"); let log = test_logger(); HotColdDB::open( &hot_path, &cold_path, - None, + &blobs_path, |_, _, _| Ok(()), config, spec, @@ -2358,6 +2358,7 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { Duration::from_secs(seconds_per_slot), ); slot_clock.set_slot(harness.get_current_slot().as_u64()); + let beacon_chain = BeaconChainBuilder::>::new(MinimalEthSpec) .store(store.clone()) .custom_spec(test_spec::()) @@ -2376,7 +2377,6 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { 1, ))) .execution_layer(Some(mock.el)) - .monitor_validators(true, vec![], DEFAULT_INDIVIDUAL_TRACKING_THRESHOLD, log) .trusted_setup(trusted_setup) .build() .expect("should build"); @@ -2465,10 +2465,10 @@ async fn weak_subjectivity_sync_test(slots: Vec, checkpoint_slot: Slot) { if let MaybeAvailableBlock::Available(block) = harness .chain .data_availability_checker - .check_rpc_block_availability( + .verify_kzg_for_rpc_block( RpcBlock::new(Some(block_root), Arc::new(full_block), Some(blobs)).unwrap(), ) - .expect("should check availability") + .expect("should verify kzg") { available_blocks.push(block); } @@ -2967,10 +2967,8 @@ async fn schema_downgrade_to_min_version() { // Can't downgrade beyond V18 once Deneb is reached, for simplicity don't test that // at all if Deneb is enabled. SchemaVersion(18) - } else if harness.spec.capella_fork_epoch.is_some() { - SchemaVersion(14) } else { - SchemaVersion(11) + SchemaVersion(16) }; // Save the slot clock so that the new harness doesn't revert in time. @@ -3279,6 +3277,40 @@ async fn deneb_prune_blobs_margin_test(margin: u64) { check_blob_existence(&harness, oldest_blob_slot, harness.head_slot(), true); } +/// Check that a database with `blobs_db=false` can be upgraded to `blobs_db=true` before Deneb. +#[tokio::test] +async fn change_to_separate_blobs_db_before_deneb() { + let db_path = tempdir().unwrap(); + let store = get_store(&db_path); + + // Only run this test on forks prior to Deneb. If the blobs database already has blobs, we can't + // move it. + if store.get_chain_spec().deneb_fork_epoch.is_some() { + return; + } + + let init_blob_info = store.get_blob_info(); + assert!( + init_blob_info.blobs_db, + "separate blobs DB should be the default" + ); + + // Change to `blobs_db=false` to emulate legacy Deneb DB. + let legacy_blob_info = BlobInfo { + blobs_db: false, + ..init_blob_info + }; + store + .compare_and_set_blob_info_with_write(init_blob_info.clone(), legacy_blob_info.clone()) + .unwrap(); + assert_eq!(store.get_blob_info(), legacy_blob_info); + + // Re-open the DB and check that `blobs_db` gets changed back to true. + drop(store); + let store = get_store(&db_path); + assert_eq!(store.get_blob_info(), init_blob_info); +} + /// Check that there are blob sidecars (or not) at every slot in the range. fn check_blob_existence( harness: &TestHarness, @@ -3306,6 +3338,77 @@ fn check_blob_existence( } } +#[tokio::test] +async fn prune_historic_states() { + let num_blocks_produced = E::slots_per_epoch() * 5; + let db_path = tempdir().unwrap(); + let store = get_store(&db_path); + let harness = get_harness(store.clone(), LOW_VALIDATOR_COUNT); + let genesis_state_root = harness.chain.genesis_state_root; + let genesis_state = harness + .chain + .get_state(&genesis_state_root, None) + .unwrap() + .unwrap(); + + harness + .extend_chain( + num_blocks_produced as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Check historical state is present. + let state_roots_iter = harness + .chain + .forwards_iter_state_roots(Slot::new(0)) + .unwrap(); + for (state_root, slot) in state_roots_iter + .take(E::slots_per_epoch() as usize) + .map(Result::unwrap) + { + assert!(store.get_state(&state_root, Some(slot)).unwrap().is_some()); + } + + store + .prune_historic_states(genesis_state_root, &genesis_state) + .unwrap(); + + // Check that anchor info is updated. + let anchor_info = store.get_anchor_info().unwrap(); + assert_eq!(anchor_info.state_lower_limit, 0); + assert_eq!(anchor_info.state_upper_limit, STATE_UPPER_LIMIT_NO_RETAIN); + + // Historical states should be pruned. + let state_roots_iter = harness + .chain + .forwards_iter_state_roots(Slot::new(1)) + .unwrap(); + for (state_root, slot) in state_roots_iter + .take(E::slots_per_epoch() as usize) + .map(Result::unwrap) + { + assert!(store.get_state(&state_root, Some(slot)).unwrap().is_none()); + } + + // Ensure that genesis state is still accessible + let genesis_state_root = harness.chain.genesis_state_root; + assert!(store + .get_state(&genesis_state_root, Some(Slot::new(0))) + .unwrap() + .is_some()); + + // Run for another two epochs. + let additional_blocks_produced = 2 * E::slots_per_epoch(); + harness + .extend_slots(additional_blocks_produced as usize) + .await; + + check_finalization(&harness, num_blocks_produced + additional_blocks_produced); + check_split_slot(&harness, store); +} + /// Checks that two chains are the same, for the purpose of these tests. /// /// Several fields that are hard/impossible to check are ignored (e.g., the store). diff --git a/beacon_node/beacon_chain/tests/validator_monitor.rs b/beacon_node/beacon_chain/tests/validator_monitor.rs new file mode 100644 index 0000000000..5bc6b758c2 --- /dev/null +++ b/beacon_node/beacon_chain/tests/validator_monitor.rs @@ -0,0 +1,299 @@ +use lazy_static::lazy_static; + +use beacon_chain::test_utils::{ + AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, +}; +use beacon_chain::validator_monitor::{ValidatorMonitorConfig, MISSED_BLOCK_LAG_SLOTS}; +use types::{Epoch, EthSpec, Keypair, MainnetEthSpec, PublicKeyBytes, Slot}; + +// Should ideally be divisible by 3. +pub const VALIDATOR_COUNT: usize = 48; + +lazy_static! { + /// A cached set of keys. + static ref KEYPAIRS: Vec = types::test_utils::generate_deterministic_keypairs(VALIDATOR_COUNT); +} + +type E = MainnetEthSpec; + +fn get_harness( + validator_count: usize, + validator_indexes_to_monitor: Vec, +) -> BeaconChainHarness> { + let harness = BeaconChainHarness::builder(MainnetEthSpec) + .default_spec() + .keypairs(KEYPAIRS[0..validator_count].to_vec()) + .fresh_ephemeral_store() + .mock_execution_layer() + .validator_monitor_config(ValidatorMonitorConfig { + validators: validator_indexes_to_monitor + .iter() + .map(|i| PublicKeyBytes::from(KEYPAIRS[*i].pk.clone())) + .collect(), + ..<_>::default() + }) + .build(); + + harness.advance_slot(); + + harness +} + +#[tokio::test] +async fn produces_missed_blocks() { + let validator_count = 16; + + let slots_per_epoch = E::slots_per_epoch(); + + let nb_epoch_to_simulate = Epoch::new(2); + + // Generate 63 slots (2 epochs * 32 slots per epoch - 1) + let initial_blocks = slots_per_epoch * nb_epoch_to_simulate.as_u64() - 1; + + // The validator index of the validator that is 'supposed' to miss a block + let mut validator_index_to_monitor = 1; + + // 1st scenario // + // + // Missed block happens when slot and prev_slot are in the same epoch + let harness1 = get_harness(validator_count, vec![validator_index_to_monitor]); + harness1 + .extend_chain( + initial_blocks as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let mut _state = &mut harness1.get_current_state(); + let mut epoch = _state.current_epoch(); + + // We have a total of 63 slots and we want slot 57 to be a missed block + // and this is slot=25 in epoch=1 + let mut idx = initial_blocks - 6; + let mut slot = Slot::new(idx); + let mut slot_in_epoch = slot % slots_per_epoch; + let mut prev_slot = Slot::new(idx - 1); + let mut duplicate_block_root = *_state.block_roots().get(idx as usize).unwrap(); + let mut validator_indexes = _state.get_beacon_proposer_indices(&harness1.spec).unwrap(); + let mut validator_index = validator_indexes[slot_in_epoch.as_usize()]; + let mut proposer_shuffling_decision_root = _state + .proposer_shuffling_decision_root(duplicate_block_root) + .unwrap(); + + let beacon_proposer_cache = harness1 + .chain + .validator_monitor + .read() + .get_beacon_proposer_cache(); + + // Let's fill the cache with the proposers for the current epoch + // and push the duplicate_block_root to the block_roots vector + assert_eq!( + beacon_proposer_cache.lock().insert( + epoch, + proposer_shuffling_decision_root, + validator_indexes.into_iter().collect::>(), + _state.fork() + ), + Ok(()) + ); + + // Modify the block root of the previous slot to be the same as the block root of the current slot + // in order to simulate a missed block + assert_eq!( + _state.set_block_root(prev_slot, duplicate_block_root), + Ok(()) + ); + + { + // Let's validate the state which will call the function responsible for + // adding the missed blocks to the validator monitor + let mut validator_monitor = harness1.chain.validator_monitor.write(); + validator_monitor.process_valid_state(nb_epoch_to_simulate, _state); + + // We should have one entry in the missed blocks map + assert_eq!( + validator_monitor.get_monitored_validator_missed_block_count(validator_index as u64), + 1 + ); + } + + // 2nd scenario // + // + // Missed block happens when slot and prev_slot are not in the same epoch + // making sure that the cache reloads when the epoch changes + // in that scenario the slot that missed a block is the first slot of the epoch + validator_index_to_monitor = 7; + // We are adding other validators to monitor as thoses one will miss a block depending on + // the fork name specified when running the test as the proposer cache differs depending on the fork name (cf. seed) + let validator_index_to_monitor_altair = 2; + // Same as above but for the merge upgrade + let validator_index_to_monitor_merge = 4; + // Same as above but for the capella upgrade + let validator_index_to_monitor_capella = 11; + // Same as above but for the deneb upgrade + let validator_index_to_monitor_deneb = 3; + let harness2 = get_harness( + validator_count, + vec![ + validator_index_to_monitor, + validator_index_to_monitor_altair, + validator_index_to_monitor_merge, + validator_index_to_monitor_capella, + validator_index_to_monitor_deneb, + ], + ); + let advance_slot_by = 9; + harness2 + .extend_chain( + (initial_blocks + advance_slot_by) as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let mut _state2 = &mut harness2.get_current_state(); + epoch = _state2.current_epoch(); + + // We have a total of 72 slots and we want slot 64 to be the missed block + // and this is slot=64 in epoch=2 + idx = initial_blocks + (advance_slot_by) - 8; + slot = Slot::new(idx); + prev_slot = Slot::new(idx - 1); + slot_in_epoch = slot % slots_per_epoch; + duplicate_block_root = *_state2.block_roots().get(idx as usize).unwrap(); + validator_indexes = _state2.get_beacon_proposer_indices(&harness2.spec).unwrap(); + validator_index = validator_indexes[slot_in_epoch.as_usize()]; + + let beacon_proposer_cache = harness2 + .chain + .validator_monitor + .read() + .get_beacon_proposer_cache(); + + // Let's fill the cache with the proposers for the current epoch + // and push the duplicate_block_root to the block_roots vector + assert_eq!( + beacon_proposer_cache.lock().insert( + epoch, + duplicate_block_root, + validator_indexes.into_iter().collect::>(), + _state2.fork() + ), + Ok(()) + ); + + assert_eq!( + _state2.set_block_root(prev_slot, duplicate_block_root), + Ok(()) + ); + + { + // Let's validate the state which will call the function responsible for + // adding the missed blocks to the validator monitor + let mut validator_monitor2 = harness2.chain.validator_monitor.write(); + validator_monitor2.process_valid_state(epoch, _state2); + // We should have one entry in the missed blocks map + assert_eq!( + validator_monitor2.get_monitored_validator_missed_block_count(validator_index as u64), + 1 + ); + + // 3rd scenario // + // + // A missed block happens but the validator is not monitored + // it should not be flagged as a missed block + idx = initial_blocks + (advance_slot_by) - 7; + slot = Slot::new(idx); + prev_slot = Slot::new(idx - 1); + slot_in_epoch = slot % slots_per_epoch; + duplicate_block_root = *_state2.block_roots().get(idx as usize).unwrap(); + validator_indexes = _state2.get_beacon_proposer_indices(&harness2.spec).unwrap(); + let not_monitored_validator_index = validator_indexes[slot_in_epoch.as_usize()]; + + assert_eq!( + _state2.set_block_root(prev_slot, duplicate_block_root), + Ok(()) + ); + + // Let's validate the state which will call the function responsible for + // adding the missed blocks to the validator monitor + validator_monitor2.process_valid_state(epoch, _state2); + + // We shouldn't have any entry in the missed blocks map + assert_ne!(validator_index, not_monitored_validator_index); + assert_eq!( + validator_monitor2 + .get_monitored_validator_missed_block_count(not_monitored_validator_index as u64), + 0 + ); + } + + // 4th scenario // + // + // A missed block happens at state.slot - LOG_SLOTS_PER_EPOCH + // it shouldn't be flagged as a missed block + let harness3 = get_harness(validator_count, vec![validator_index_to_monitor]); + harness3 + .extend_chain( + slots_per_epoch as usize, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + let mut _state3 = &mut harness3.get_current_state(); + epoch = _state3.current_epoch(); + + // We have a total of 32 slots and we want slot 30 to be a missed block + // and this is slot=30 in epoch=0 + idx = slots_per_epoch - MISSED_BLOCK_LAG_SLOTS as u64 + 2; + slot = Slot::new(idx); + slot_in_epoch = slot % slots_per_epoch; + prev_slot = Slot::new(idx - 1); + duplicate_block_root = *_state3.block_roots().get(idx as usize).unwrap(); + validator_indexes = _state3.get_beacon_proposer_indices(&harness3.spec).unwrap(); + validator_index = validator_indexes[slot_in_epoch.as_usize()]; + proposer_shuffling_decision_root = _state3 + .proposer_shuffling_decision_root_at_epoch(epoch, duplicate_block_root) + .unwrap(); + + let beacon_proposer_cache = harness3 + .chain + .validator_monitor + .read() + .get_beacon_proposer_cache(); + + // Let's fill the cache with the proposers for the current epoch + // and push the duplicate_block_root to the block_roots vector + assert_eq!( + beacon_proposer_cache.lock().insert( + epoch, + proposer_shuffling_decision_root, + validator_indexes.into_iter().collect::>(), + _state3.fork() + ), + Ok(()) + ); + + // Modify the block root of the previous slot to be the same as the block root of the current slot + // in order to simulate a missed block + assert_eq!( + _state3.set_block_root(prev_slot, duplicate_block_root), + Ok(()) + ); + + { + // Let's validate the state which will call the function responsible for + // adding the missed blocks to the validator monitor + let mut validator_monitor3 = harness3.chain.validator_monitor.write(); + validator_monitor3.process_valid_state(epoch, _state3); + + // We shouldn't have one entry in the missed blocks map + assert_eq!( + validator_monitor3.get_monitored_validator_missed_block_count(validator_index as u64), + 0 + ); + } +} diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index d1184cf75d..cedf347b9a 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -191,15 +191,7 @@ where .graffiti(graffiti) .event_handler(event_handler) .execution_layer(execution_layer) - .monitor_validators( - config.validator_monitor_auto, - config.validator_monitor_pubkeys.clone(), - config.validator_monitor_individual_tracking_threshold, - runtime_context - .service_context("val_mon".to_string()) - .log() - .clone(), - ); + .validator_monitor_config(config.validator_monitor.clone()); let builder = if let Some(slasher) = self.slasher.clone() { builder.slasher(slasher) @@ -909,7 +901,7 @@ where mut self, hot_path: &Path, cold_path: &Path, - blobs_path: Option, + blobs_path: &Path, config: StoreConfig, log: Logger, ) -> Result { diff --git a/beacon_node/client/src/config.rs b/beacon_node/client/src/config.rs index 8b47d0fc62..20afdb948b 100644 --- a/beacon_node/client/src/config.rs +++ b/beacon_node/client/src/config.rs @@ -1,4 +1,4 @@ -use beacon_chain::validator_monitor::DEFAULT_INDIVIDUAL_TRACKING_THRESHOLD; +use beacon_chain::validator_monitor::ValidatorMonitorConfig; use beacon_chain::TrustedSetup; use beacon_processor::BeaconProcessorConfig; use directory::DEFAULT_ROOT_DIR; @@ -9,9 +9,12 @@ use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; use std::time::Duration; -use types::{Graffiti, PublicKeyBytes}; +use types::Graffiti; + /// Default directory name for the freezer database under the top-level data dir. const DEFAULT_FREEZER_DB_DIR: &str = "freezer_db"; +/// Default directory name for the blobs database under the top-level data dir. +const DEFAULT_BLOBS_DB_DIR: &str = "blobs_db"; /// Defines how the client should initialize the `BeaconChain` and other components. #[derive(Debug, Clone, Serialize, Deserialize, Default)] @@ -56,15 +59,7 @@ pub struct Config { pub sync_eth1_chain: bool, /// Graffiti to be inserted everytime we create a block. pub graffiti: Graffiti, - /// When true, automatically monitor validators using the HTTP API. - pub validator_monitor_auto: bool, - /// A list of validator pubkeys to monitor. - pub validator_monitor_pubkeys: Vec, - /// Once the number of monitored validators goes above this threshold, we - /// will stop tracking metrics on a per-validator basis. This prevents large - /// validator counts causing infeasibly high cardinailty for Prometheus and - /// high log volumes. - pub validator_monitor_individual_tracking_threshold: usize, + pub validator_monitor: ValidatorMonitorConfig, #[serde(skip)] /// The `genesis` field is not serialized or deserialized by `serde` to ensure it is defined /// via the CLI at runtime, instead of from a configuration file saved to disk. @@ -107,9 +102,7 @@ impl Default for Config { http_metrics: <_>::default(), monitoring_api: None, slasher: None, - validator_monitor_auto: false, - validator_monitor_pubkeys: vec![], - validator_monitor_individual_tracking_threshold: DEFAULT_INDIVIDUAL_TRACKING_THRESHOLD, + validator_monitor: <_>::default(), logger_config: LoggerConfig::default(), beacon_processor: <_>::default(), genesis_state_url: <_>::default(), @@ -156,12 +149,19 @@ impl Config { .unwrap_or_else(|| self.default_freezer_db_path()) } + /// Fetch default path to use for the blobs database. + fn default_blobs_db_path(&self) -> PathBuf { + self.get_data_dir().join(DEFAULT_BLOBS_DB_DIR) + } + /// Returns the path to which the client may initialize the on-disk blobs database. /// /// Will attempt to use the user-supplied path from e.g. the CLI, or will default /// to None. - pub fn get_blobs_db_path(&self) -> Option { - self.blobs_db_path.clone() + pub fn get_blobs_db_path(&self) -> PathBuf { + self.blobs_db_path + .clone() + .unwrap_or_else(|| self.default_blobs_db_path()) } /// Get the freezer DB path, creating it if necessary. @@ -170,11 +170,8 @@ impl Config { } /// Get the blobs DB path, creating it if necessary. - pub fn create_blobs_db_path(&self) -> Result, String> { - match self.get_blobs_db_path() { - Some(blobs_db_path) => Ok(Some(ensure_dir_exists(blobs_db_path)?)), - None => Ok(None), - } + pub fn create_blobs_db_path(&self) -> Result { + ensure_dir_exists(self.get_blobs_db_path()) } /// Returns the "modern" path to the data_dir. diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index 683e39a503..19b9a58eb6 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -466,6 +466,11 @@ impl From> } } +pub enum GetPayloadResponseType { + Full(GetPayloadResponse), + Blinded(GetPayloadResponse), +} + impl GetPayloadResponse { pub fn execution_payload_ref(&self) -> ExecutionPayloadRef { self.to_ref().into() diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 623965ada3..07fdf6414c 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -42,13 +42,13 @@ use tokio_stream::wrappers::WatchStream; use tree_hash::TreeHash; use types::beacon_block_body::KzgCommitments; use types::builder_bid::BuilderBid; +use types::payload::BlockProductionVersion; use types::sidecar::{BlobItems, Sidecar}; -use types::KzgProofs; +use types::{AbstractExecPayload, ExecutionPayloadDeneb, KzgProofs}; use types::{ - AbstractExecPayload, BeaconStateError, BlindedPayload, BlockType, ChainSpec, Epoch, - ExecPayload, ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadMerge, + BeaconStateError, BlindedPayload, ChainSpec, Epoch, ExecPayload, ExecutionPayloadCapella, + ExecutionPayloadMerge, FullPayload, ProposerPreparationData, PublicKeyBytes, Signature, Slot, }; -use types::{ProposerPreparationData, PublicKeyBytes, Signature, Slot}; mod block_hash; mod engine_api; @@ -87,37 +87,33 @@ pub enum ProvenancedPayload

{ Builder(P), } -impl> TryFrom> - for ProvenancedPayload> -{ +impl TryFrom> for ProvenancedPayload> { type Error = Error; fn try_from(value: BuilderBid) -> Result { let block_proposal_contents = match value { BuilderBid::Merge(builder_bid) => BlockProposalContents::Payload { - payload: ExecutionPayloadHeader::Merge(builder_bid.header) - .try_into() - .map_err(|_| Error::InvalidPayloadConversion)?, + payload: ExecutionPayloadHeader::Merge(builder_bid.header).into(), block_value: builder_bid.value, }, BuilderBid::Capella(builder_bid) => BlockProposalContents::Payload { - payload: ExecutionPayloadHeader::Capella(builder_bid.header) - .try_into() - .map_err(|_| Error::InvalidPayloadConversion)?, + payload: ExecutionPayloadHeader::Capella(builder_bid.header).into(), block_value: builder_bid.value, }, BuilderBid::Deneb(builder_bid) => BlockProposalContents::PayloadAndBlobs { - payload: ExecutionPayloadHeader::Deneb(builder_bid.header) - .try_into() - .map_err(|_| Error::InvalidPayloadConversion)?, + payload: ExecutionPayloadHeader::Deneb(builder_bid.header).into(), block_value: builder_bid.value, kzg_commitments: builder_bid.blinded_blobs_bundle.commitments, - blobs: BlobItems::try_from_blob_roots(builder_bid.blinded_blobs_bundle.blob_roots) - .map_err(Error::InvalidBlobConversion)?, + blobs: BlobItems::::try_from_blob_roots( + builder_bid.blinded_blobs_bundle.blob_roots, + ) + .map_err(Error::InvalidBlobConversion)?, proofs: builder_bid.blinded_blobs_bundle.proofs, }, }; - Ok(ProvenancedPayload::Builder(block_proposal_contents)) + Ok(ProvenancedPayload::Builder( + BlockProposalContentsType::Blinded(block_proposal_contents), + )) } } @@ -145,6 +141,7 @@ pub enum Error { InvalidPayloadConversion, InvalidBlobConversion(String), BeaconStateError(BeaconStateError), + PayloadTypeMismatch, } impl From for Error { @@ -159,6 +156,11 @@ impl From for Error { } } +pub enum BlockProposalContentsType { + Full(BlockProposalContents>), + Blinded(BlockProposalContents>), +} + pub enum BlockProposalContents> { Payload { payload: Payload, @@ -173,6 +175,22 @@ pub enum BlockProposalContents> { }, } +impl From>> + for BlockProposalContents> +{ + fn from(item: BlockProposalContents>) -> Self { + let block_value = item.block_value().to_owned(); + + let blinded_payload: BlockProposalContents> = + BlockProposalContents::Payload { + payload: item.to_payload().execution_payload().into(), + block_value, + }; + + blinded_payload + } +} + impl> TryFrom> for BlockProposalContents { @@ -197,6 +215,17 @@ impl> TryFrom> } } +impl TryFrom> for BlockProposalContentsType { + type Error = Error; + + fn try_from(response_type: GetPayloadResponseType) -> Result { + match response_type { + GetPayloadResponseType::Full(response) => Ok(Self::Full(response.try_into()?)), + GetPayloadResponseType::Blinded(response) => Ok(Self::Blinded(response.try_into()?)), + } + } +} + #[allow(clippy::type_complexity)] impl> BlockProposalContents { pub fn deconstruct( @@ -206,19 +235,26 @@ impl> BlockProposalContents>, Option<>::BlobItems>, Option>, + Uint256, ) { match self { Self::Payload { payload, - block_value: _, - } => (payload, None, None, None), + block_value, + } => (payload, None, None, None, block_value), Self::PayloadAndBlobs { payload, - block_value: _, + block_value, kzg_commitments, blobs, proofs, - } => (payload, Some(kzg_commitments), Some(blobs), Some(proofs)), + } => ( + payload, + Some(kzg_commitments), + Some(blobs), + Some(proofs), + block_value, + ), } } @@ -284,6 +320,7 @@ pub struct BuilderParams { pub chain_health: ChainHealth, } +#[derive(PartialEq)] pub enum ChainHealth { Healthy, Unhealthy(FailedCondition), @@ -291,7 +328,7 @@ pub enum ChainHealth { PreMerge, } -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum FailedCondition { Skips, SkipsPerEpoch, @@ -795,7 +832,8 @@ impl ExecutionLayer { /// /// The result will be returned from the first node that returns successfully. No more nodes /// will be contacted. - pub async fn get_payload>( + #[allow(clippy::too_many_arguments)] + pub async fn get_payload( &self, parent_hash: ExecutionBlockHash, payload_attributes: &PayloadAttributes, @@ -803,14 +841,11 @@ impl ExecutionLayer { builder_params: BuilderParams, current_fork: ForkName, spec: &ChainSpec, - ) -> Result, Error> { - let payload_result = match Payload::block_type() { - BlockType::Blinded => { - let _timer = metrics::start_timer_vec( - &metrics::EXECUTION_LAYER_REQUEST_TIMES, - &[metrics::GET_BLINDED_PAYLOAD], - ); - self.get_blinded_payload( + block_production_version: BlockProductionVersion, + ) -> Result, Error> { + let payload_result_type = match block_production_version { + BlockProductionVersion::V3 => match self + .determine_and_fetch_payload( parent_hash, payload_attributes, forkchoice_update_params, @@ -819,27 +854,51 @@ impl ExecutionLayer { spec, ) .await - } - BlockType::Full => { + { + Ok(payload) => payload, + Err(e) => { + metrics::inc_counter_vec( + &metrics::EXECUTION_LAYER_GET_PAYLOAD_OUTCOME, + &[metrics::FAILURE], + ); + return Err(e); + } + }, + BlockProductionVersion::BlindedV2 => { let _timer = metrics::start_timer_vec( &metrics::EXECUTION_LAYER_REQUEST_TIMES, - &[metrics::GET_PAYLOAD], + &[metrics::GET_BLINDED_PAYLOAD], ); - self.get_full_payload( + self.determine_and_fetch_payload( + parent_hash, + payload_attributes, + forkchoice_update_params, + builder_params, + current_fork, + spec, + ) + .await? + } + BlockProductionVersion::FullV2 => self + .get_full_payload_with( parent_hash, payload_attributes, forkchoice_update_params, current_fork, + noop, ) .await - .and_then(GetPayloadResponse::try_into) - .map(ProvenancedPayload::Local) - } + .and_then(GetPayloadResponseType::try_into) + .map(ProvenancedPayload::Local)?, }; - // Track some metrics and return the result. - match payload_result { - Ok(ProvenancedPayload::Local(block_proposal_contents)) => { + let block_proposal_content_type = match payload_result_type { + ProvenancedPayload::Local(local_payload) => local_payload, + ProvenancedPayload::Builder(builder_payload) => builder_payload, + }; + + match block_proposal_content_type { + BlockProposalContentsType::Full(block_proposal_contents) => { metrics::inc_counter_vec( &metrics::EXECUTION_LAYER_GET_PAYLOAD_OUTCOME, &[metrics::SUCCESS], @@ -848,9 +907,15 @@ impl ExecutionLayer { &metrics::EXECUTION_LAYER_GET_PAYLOAD_SOURCE, &[metrics::LOCAL], ); - Ok(block_proposal_contents) + if matches!(block_production_version, BlockProductionVersion::BlindedV2) { + Ok(BlockProposalContentsType::Blinded( + block_proposal_contents.into(), + )) + } else { + Ok(BlockProposalContentsType::Full(block_proposal_contents)) + } } - Ok(ProvenancedPayload::Builder(block_proposal_contents)) => { + BlockProposalContentsType::Blinded(block_proposal_contents) => { metrics::inc_counter_vec( &metrics::EXECUTION_LAYER_GET_PAYLOAD_OUTCOME, &[metrics::SUCCESS], @@ -859,19 +924,78 @@ impl ExecutionLayer { &metrics::EXECUTION_LAYER_GET_PAYLOAD_SOURCE, &[metrics::BUILDER], ); - Ok(block_proposal_contents) - } - Err(e) => { - metrics::inc_counter_vec( - &metrics::EXECUTION_LAYER_GET_PAYLOAD_OUTCOME, - &[metrics::FAILURE], - ); - Err(e) + Ok(BlockProposalContentsType::Blinded(block_proposal_contents)) } } } - async fn get_blinded_payload>( + /// Fetches local and builder paylaods concurrently, Logs and returns results. + async fn fetch_builder_and_local_payloads( + &self, + builder: &BuilderHttpClient, + parent_hash: ExecutionBlockHash, + builder_params: &BuilderParams, + payload_attributes: &PayloadAttributes, + forkchoice_update_params: ForkchoiceUpdateParameters, + current_fork: ForkName, + ) -> ( + Result>>, builder_client::Error>, + Result, Error>, + ) { + let slot = builder_params.slot; + let pubkey = &builder_params.pubkey; + + info!( + self.log(), + "Requesting blinded header from connected builder"; + "slot" => ?slot, + "pubkey" => ?pubkey, + "parent_hash" => ?parent_hash, + ); + + // Wait for the builder *and* local EL to produce a payload (or return an error). + let ((relay_result, relay_duration), (local_result, local_duration)) = tokio::join!( + timed_future(metrics::GET_BLINDED_PAYLOAD_BUILDER, async { + builder + .get_builder_header::(slot, parent_hash, pubkey) + .await + }), + timed_future(metrics::GET_BLINDED_PAYLOAD_LOCAL, async { + self.get_full_payload_caching( + parent_hash, + payload_attributes, + forkchoice_update_params, + current_fork, + ) + .await + .and_then(|local_result_type| match local_result_type { + GetPayloadResponseType::Full(payload) => Ok(payload), + GetPayloadResponseType::Blinded(_) => Err(Error::PayloadTypeMismatch), + }) + }) + ); + + info!( + self.log(), + "Requested blinded execution payload"; + "relay_fee_recipient" => match &relay_result { + Ok(Some(r)) => format!("{:?}", r.data.message.header().fee_recipient()), + Ok(None) => "empty response".to_string(), + Err(_) => "request failed".to_string(), + }, + "relay_response_ms" => relay_duration.as_millis(), + "local_fee_recipient" => match &local_result { + Ok(get_payload_response) => format!("{:?}", get_payload_response.fee_recipient()), + Err(_) => "request failed".to_string() + }, + "local_response_ms" => local_duration.as_millis(), + "parent_hash" => ?parent_hash, + ); + + (relay_result, local_result) + } + + async fn determine_and_fetch_payload( &self, parent_hash: ExecutionBlockHash, payload_attributes: &PayloadAttributes, @@ -879,235 +1003,25 @@ impl ExecutionLayer { builder_params: BuilderParams, current_fork: ForkName, spec: &ChainSpec, - ) -> Result>, Error> { - if let Some(builder) = self.builder() { - let slot = builder_params.slot; - let pubkey = builder_params.pubkey; + ) -> Result>, Error> { + let Some(builder) = self.builder() else { + // no builder.. return local payload + return self + .get_full_payload_caching( + parent_hash, + payload_attributes, + forkchoice_update_params, + current_fork, + ) + .await + .and_then(GetPayloadResponseType::try_into) + .map(ProvenancedPayload::Local); + }; + // check chain health + if builder_params.chain_health != ChainHealth::Healthy { + // chain is unhealthy, gotta use local payload match builder_params.chain_health { - ChainHealth::Healthy => { - info!( - self.log(), - "Requesting blinded header from connected builder"; - "slot" => ?slot, - "pubkey" => ?pubkey, - "parent_hash" => ?parent_hash, - ); - - // Wait for the builder *and* local EL to produce a payload (or return an error). - let ((relay_result, relay_duration), (local_result, local_duration)) = tokio::join!( - timed_future(metrics::GET_BLINDED_PAYLOAD_BUILDER, async { - builder - .get_builder_header::(slot, parent_hash, &pubkey) - .await - }), - timed_future(metrics::GET_BLINDED_PAYLOAD_LOCAL, async { - self.get_full_payload_caching( - parent_hash, - payload_attributes, - forkchoice_update_params, - current_fork, - ) - .await - }) - ); - - info!( - self.log(), - "Requested blinded execution payload"; - "relay_fee_recipient" => match &relay_result { - Ok(Some(r)) => format!("{:?}", r.data.message.header().fee_recipient()), - Ok(None) => "empty response".to_string(), - Err(_) => "request failed".to_string(), - }, - "relay_response_ms" => relay_duration.as_millis(), - "local_fee_recipient" => match &local_result { - Ok(get_payload_response) => format!("{:?}", get_payload_response.fee_recipient()), - Err(_) => "request failed".to_string() - }, - "local_response_ms" => local_duration.as_millis(), - "parent_hash" => ?parent_hash, - ); - - return match (relay_result, local_result) { - (Err(e), Ok(local)) => { - warn!( - self.log(), - "Builder error when requesting payload"; - "info" => "falling back to local execution client", - "relay_error" => ?e, - "local_block_hash" => ?local.block_hash(), - "parent_hash" => ?parent_hash, - ); - Ok(ProvenancedPayload::Local(local.try_into()?)) - } - (Ok(None), Ok(local)) => { - info!( - self.log(), - "Builder did not return a payload"; - "info" => "falling back to local execution client", - "local_block_hash" => ?local.block_hash(), - "parent_hash" => ?parent_hash, - ); - Ok(ProvenancedPayload::Local(local.try_into()?)) - } - (Ok(Some(relay)), Ok(local)) => { - let header = &relay.data.message.header(); - - info!( - self.log(), - "Received local and builder payloads"; - "relay_block_hash" => ?header.block_hash(), - "local_block_hash" => ?local.block_hash(), - "parent_hash" => ?parent_hash, - ); - - let relay_value = relay.data.message.value(); - let local_value = *local.block_value(); - - if !self.inner.always_prefer_builder_payload { - if local_value >= *relay_value { - info!( - self.log(), - "Local block is more profitable than relay block"; - "local_block_value" => %local_value, - "relay_value" => %relay_value - ); - return Ok(ProvenancedPayload::Local(local.try_into()?)); - } else if local.should_override_builder().unwrap_or(false) { - let percentage_difference = - percentage_difference_u256(local_value, *relay_value); - if percentage_difference.map_or(false, |percentage| { - percentage - < self - .inner - .ignore_builder_override_suggestion_threshold - }) { - info!( - self.log(), - "Using local payload because execution engine suggested we ignore builder payload"; - "local_block_value" => %local_value, - "relay_value" => %relay_value - ); - return Ok(ProvenancedPayload::Local(local.try_into()?)); - } - } else { - info!( - self.log(), - "Relay block is more profitable than local block"; - "local_block_value" => %local_value, - "relay_value" => %relay_value - ); - } - } - - match verify_builder_bid( - &relay, - parent_hash, - payload_attributes, - Some(local.block_number()), - self.inner.builder_profit_threshold, - current_fork, - spec, - ) { - Ok(()) => Ok(ProvenancedPayload::try_from(relay.data.message)?), - Err(reason) if !reason.payload_invalid() => { - info!( - self.log(), - "Builder payload ignored"; - "info" => "using local payload", - "reason" => %reason, - "relay_block_hash" => ?header.block_hash(), - "parent_hash" => ?parent_hash, - ); - Ok(ProvenancedPayload::Local(local.try_into()?)) - } - Err(reason) => { - metrics::inc_counter_vec( - &metrics::EXECUTION_LAYER_GET_PAYLOAD_BUILDER_REJECTIONS, - &[reason.as_ref().as_ref()], - ); - warn!( - self.log(), - "Builder returned invalid payload"; - "info" => "using local payload", - "reason" => %reason, - "relay_block_hash" => ?header.block_hash(), - "parent_hash" => ?parent_hash, - ); - Ok(ProvenancedPayload::Local(local.try_into()?)) - } - } - } - (Ok(Some(relay)), Err(local_error)) => { - let header = &relay.data.message.header(); - - info!( - self.log(), - "Received builder payload with local error"; - "relay_block_hash" => ?header.block_hash(), - "local_error" => ?local_error, - "parent_hash" => ?parent_hash, - ); - - match verify_builder_bid( - &relay, - parent_hash, - payload_attributes, - None, - self.inner.builder_profit_threshold, - current_fork, - spec, - ) { - Ok(()) => Ok(ProvenancedPayload::try_from(relay.data.message)?), - // If the payload is valid then use it. The local EE failed - // to produce a payload so we have no alternative. - Err(e) if !e.payload_invalid() => { - Ok(ProvenancedPayload::try_from(relay.data.message)?) - } - Err(reason) => { - metrics::inc_counter_vec( - &metrics::EXECUTION_LAYER_GET_PAYLOAD_BUILDER_REJECTIONS, - &[reason.as_ref().as_ref()], - ); - crit!( - self.log(), - "Builder returned invalid payload"; - "info" => "no local payload either - unable to propose block", - "reason" => %reason, - "relay_block_hash" => ?header.block_hash(), - "parent_hash" => ?parent_hash, - ); - Err(Error::CannotProduceHeader) - } - } - } - (Err(relay_error), Err(local_error)) => { - crit!( - self.log(), - "Unable to produce execution payload"; - "info" => "the local EL and builder both failed - unable to propose block", - "relay_error" => ?relay_error, - "local_error" => ?local_error, - "parent_hash" => ?parent_hash, - ); - - Err(Error::CannotProduceHeader) - } - (Ok(None), Err(local_error)) => { - crit!( - self.log(), - "Unable to produce execution payload"; - "info" => "the local EL failed and the builder returned nothing - \ - the block proposal will be missed", - "local_error" => ?local_error, - "parent_hash" => ?parent_hash, - ); - - Err(Error::CannotProduceHeader) - } - }; - } ChainHealth::Unhealthy(condition) => info!( self.log(), "Chain is unhealthy, using local payload"; @@ -1123,35 +1037,220 @@ impl ExecutionLayer { "info" => "the local execution engine is syncing and the builder network \ cannot safely be used - unable to propose block" ), + ChainHealth::Healthy => crit!( + self.log(), + "got healthy but also not healthy.. this shouldn't happen!" + ), + } + return self + .get_full_payload_caching( + parent_hash, + payload_attributes, + forkchoice_update_params, + current_fork, + ) + .await + .and_then(GetPayloadResponseType::try_into) + .map(ProvenancedPayload::Local); + } + + let (relay_result, local_result) = self + .fetch_builder_and_local_payloads( + builder.as_ref(), + parent_hash, + &builder_params, + payload_attributes, + forkchoice_update_params, + current_fork, + ) + .await; + + match (relay_result, local_result) { + (Err(e), Ok(local)) => { + warn!( + self.log(), + "Builder error when requesting payload"; + "info" => "falling back to local execution client", + "relay_error" => ?e, + "local_block_hash" => ?local.block_hash(), + "parent_hash" => ?parent_hash, + ); + Ok(ProvenancedPayload::Local(BlockProposalContentsType::Full( + local.try_into()?, + ))) + } + (Ok(None), Ok(local)) => { + info!( + self.log(), + "Builder did not return a payload"; + "info" => "falling back to local execution client", + "local_block_hash" => ?local.block_hash(), + "parent_hash" => ?parent_hash, + ); + Ok(ProvenancedPayload::Local(BlockProposalContentsType::Full( + local.try_into()?, + ))) + } + (Err(relay_error), Err(local_error)) => { + crit!( + self.log(), + "Unable to produce execution payload"; + "info" => "the local EL and builder both failed - unable to propose block", + "relay_error" => ?relay_error, + "local_error" => ?local_error, + "parent_hash" => ?parent_hash, + ); + + Err(Error::CannotProduceHeader) + } + (Ok(None), Err(local_error)) => { + crit!( + self.log(), + "Unable to produce execution payload"; + "info" => "the local EL failed and the builder returned nothing - \ + the block proposal will be missed", + "local_error" => ?local_error, + "parent_hash" => ?parent_hash, + ); + + Err(Error::CannotProduceHeader) + } + (Ok(Some(relay)), Ok(local)) => { + let header = &relay.data.message.header(); + + info!( + self.log(), + "Received local and builder payloads"; + "relay_block_hash" => ?header.block_hash(), + "local_block_hash" => ?local.block_hash(), + "parent_hash" => ?parent_hash, + ); + + // check relay payload validity + if let Err(reason) = verify_builder_bid( + &relay, + parent_hash, + payload_attributes, + Some(local.block_number()), + current_fork, + spec, + ) { + // relay payload invalid -> return local + metrics::inc_counter_vec( + &metrics::EXECUTION_LAYER_GET_PAYLOAD_BUILDER_REJECTIONS, + &[reason.as_ref().as_ref()], + ); + warn!( + self.log(), + "Builder returned invalid payload"; + "info" => "using local payload", + "reason" => %reason, + "relay_block_hash" => ?header.block_hash(), + "parent_hash" => ?parent_hash, + ); + return Ok(ProvenancedPayload::Local(BlockProposalContentsType::Full( + local.try_into()?, + ))); + } + + if self.inner.always_prefer_builder_payload { + return ProvenancedPayload::try_from(relay.data.message); + } + + let relay_value = *relay.data.message.value(); + let local_value = *local.block_value(); + + if local_value >= relay_value { + info!( + self.log(), + "Local block is more profitable than relay block"; + "local_block_value" => %local_value, + "relay_value" => %relay_value + ); + return Ok(ProvenancedPayload::Local(BlockProposalContentsType::Full( + local.try_into()?, + ))); + } + + if relay_value < self.inner.builder_profit_threshold { + info!( + self.log(), + "Builder payload ignored"; + "info" => "using local payload", + "reason" => format!("payload value of {} does not meet user-configured profit-threshold of {}", relay_value, self.inner.builder_profit_threshold), + "relay_block_hash" => ?header.block_hash(), + "parent_hash" => ?parent_hash, + ); + return Ok(ProvenancedPayload::Local(BlockProposalContentsType::Full( + local.try_into()?, + ))); + } + + if local.should_override_builder().unwrap_or(false) { + let percentage_difference = + percentage_difference_u256(local_value, relay_value); + if percentage_difference.map_or(false, |percentage| { + percentage < self.inner.ignore_builder_override_suggestion_threshold + }) { + info!( + self.log(), + "Using local payload because execution engine suggested we ignore builder payload"; + "local_block_value" => %local_value, + "relay_value" => %relay_value + ); + return Ok(ProvenancedPayload::Local(BlockProposalContentsType::Full( + local.try_into()?, + ))); + } + } + + info!( + self.log(), + "Relay block is more profitable than local block"; + "local_block_value" => %local_value, + "relay_value" => %relay_value + ); + + Ok(ProvenancedPayload::try_from(relay.data.message)?) + } + (Ok(Some(relay)), Err(local_error)) => { + let header = &relay.data.message.header(); + + info!( + self.log(), + "Received builder payload with local error"; + "relay_block_hash" => ?header.block_hash(), + "local_error" => ?local_error, + "parent_hash" => ?parent_hash, + ); + + match verify_builder_bid( + &relay, + parent_hash, + payload_attributes, + None, + current_fork, + spec, + ) { + Ok(()) => Ok(ProvenancedPayload::try_from(relay.data.message)?), + Err(reason) => { + metrics::inc_counter_vec( + &metrics::EXECUTION_LAYER_GET_PAYLOAD_BUILDER_REJECTIONS, + &[reason.as_ref().as_ref()], + ); + crit!( + self.log(), + "Builder returned invalid payload"; + "info" => "no local payload either - unable to propose block", + "reason" => %reason, + "relay_block_hash" => ?header.block_hash(), + "parent_hash" => ?parent_hash, + ); + Err(Error::CannotProduceHeader) + } + } } } - self.get_full_payload_caching( - parent_hash, - payload_attributes, - forkchoice_update_params, - current_fork, - ) - .await - .and_then(GetPayloadResponse::try_into) - .map(ProvenancedPayload::Local) - } - - /// Get a full payload without caching its result in the execution layer's payload cache. - async fn get_full_payload( - &self, - parent_hash: ExecutionBlockHash, - payload_attributes: &PayloadAttributes, - forkchoice_update_params: ForkchoiceUpdateParameters, - current_fork: ForkName, - ) -> Result, Error> { - self.get_full_payload_with( - parent_hash, - payload_attributes, - forkchoice_update_params, - current_fork, - noop, - ) - .await } /// Get a full payload and cache its result in the execution layer's payload cache. @@ -1161,7 +1260,7 @@ impl ExecutionLayer { payload_attributes: &PayloadAttributes, forkchoice_update_params: ForkchoiceUpdateParameters, current_fork: ForkName, - ) -> Result, Error> { + ) -> Result, Error> { self.get_full_payload_with( parent_hash, payload_attributes, @@ -1182,7 +1281,7 @@ impl ExecutionLayer { &ExecutionLayer, PayloadContentsRefTuple, ) -> Option>, - ) -> Result, Error> { + ) -> Result, Error> { self.engine() .request(move |engine| async move { let payload_id = if let Some(id) = engine @@ -1244,6 +1343,10 @@ impl ExecutionLayer { "timestamp" => payload_attributes.timestamp(), "parent_hash" => ?parent_hash, ); + let _timer = metrics::start_timer_vec( + &metrics::EXECUTION_LAYER_REQUEST_TIMES, + &[metrics::GET_PAYLOAD], + ); engine.api.get_payload::(current_fork, payload_id).await }.await?; @@ -1268,7 +1371,7 @@ impl ExecutionLayer { ); } - Ok(payload_response) + Ok(GetPayloadResponseType::Full(payload_response)) }) .await .map_err(Box::new) @@ -1968,10 +2071,6 @@ impl ExecutionLayer { #[derive(AsRefStr)] #[strum(serialize_all = "snake_case")] enum InvalidBuilderPayload { - LowValue { - profit_threshold: Uint256, - payload_value: Uint256, - }, ParentHash { payload: ExecutionBlockHash, expected: ExecutionBlockHash, @@ -2002,34 +2101,9 @@ enum InvalidBuilderPayload { }, } -impl InvalidBuilderPayload { - /// Returns `true` if a payload is objectively invalid and should never be included on chain. - fn payload_invalid(&self) -> bool { - match self { - // A low-value payload isn't invalid, it should just be avoided if possible. - InvalidBuilderPayload::LowValue { .. } => false, - InvalidBuilderPayload::ParentHash { .. } => true, - InvalidBuilderPayload::PrevRandao { .. } => true, - InvalidBuilderPayload::Timestamp { .. } => true, - InvalidBuilderPayload::BlockNumber { .. } => true, - InvalidBuilderPayload::Fork { .. } => true, - InvalidBuilderPayload::Signature { .. } => true, - InvalidBuilderPayload::WithdrawalsRoot { .. } => true, - } - } -} - impl fmt::Display for InvalidBuilderPayload { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - InvalidBuilderPayload::LowValue { - profit_threshold, - payload_value, - } => write!( - f, - "payload value of {} does not meet user-configured profit-threshold of {}", - payload_value, profit_threshold - ), InvalidBuilderPayload::ParentHash { payload, expected } => { write!(f, "payload block hash was {} not {}", payload, expected) } @@ -2073,13 +2147,11 @@ fn verify_builder_bid( parent_hash: ExecutionBlockHash, payload_attributes: &PayloadAttributes, block_number: Option, - profit_threshold: Uint256, current_fork: ForkName, spec: &ChainSpec, ) -> Result<(), Box> { let is_signature_valid = bid.data.verify_signature(spec); let header = &bid.data.message.header(); - let payload_value = bid.data.message.value(); // Avoid logging values that we can't represent with our Prometheus library. let payload_value_gwei = bid.data.message.value() / 1_000_000_000; @@ -2098,12 +2170,7 @@ fn verify_builder_bid( .map(|withdrawals| Withdrawals::::from(withdrawals).tree_hash_root()); let payload_withdrawals_root = header.withdrawals_root().ok().copied(); - if *payload_value < profit_threshold { - Err(Box::new(InvalidBuilderPayload::LowValue { - profit_threshold, - payload_value: *payload_value, - })) - } else if header.parent_hash() != parent_hash { + if header.parent_hash() != parent_hash { Err(Box::new(InvalidBuilderPayload::ParentHash { payload: header.parent_hash(), expected: parent_hash, diff --git a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs index cf0faf655a..713ebb670c 100644 --- a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs +++ b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs @@ -655,14 +655,17 @@ pub fn load_test_blobs_bundle() -> Result<(KzgCommitment, KzgProof, Ok(( commitments - .get(0) + .first() .cloned() .ok_or("commitment missing in test bundle")?, proofs - .get(0) + .first() .cloned() .ok_or("proof missing in test bundle")?, - blobs.get(0).cloned().ok_or("blob missing in test bundle")?, + blobs + .first() + .cloned() + .ok_or("blob missing in test bundle")?, )) } diff --git a/beacon_node/execution_layer/src/test_utils/mock_builder.rs b/beacon_node/execution_layer/src/test_utils/mock_builder.rs index 1e45ef2726..32b352b6ae 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -508,11 +508,7 @@ pub fn serve( finalized_hash: Some(finalized_execution_hash), }; - let (payload, _block_value, maybe_blobs_bundle): ( - ExecutionPayload, - Uint256, - Option>, - ) = builder + let payload_response_type = builder .el .get_full_payload_caching( head_execution_hash, @@ -521,38 +517,88 @@ pub fn serve( fork, ) .await - .map_err(|_| reject("couldn't get payload"))? - .into(); + .map_err(|_| reject("couldn't get payload"))?; - let mut message = match fork { - ForkName::Deneb => BuilderBid::Deneb(BuilderBidDeneb { - header: payload - .as_deneb() - .map_err(|_| reject("incorrect payload variant"))? - .into(), - blinded_blobs_bundle: maybe_blobs_bundle - .map(Into::into) - .unwrap_or_default(), - value: Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI), - pubkey: builder.builder_sk.public_key().compress(), - }), - ForkName::Capella => BuilderBid::Capella(BuilderBidCapella { - header: payload - .as_capella() - .map_err(|_| reject("incorrect payload variant"))? - .into(), - value: Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI), - pubkey: builder.builder_sk.public_key().compress(), - }), - ForkName::Merge => BuilderBid::Merge(BuilderBidMerge { - header: payload - .as_merge() - .map_err(|_| reject("incorrect payload variant"))? - .into(), - value: Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI), - pubkey: builder.builder_sk.public_key().compress(), - }), - ForkName::Base | ForkName::Altair => return Err(reject("invalid fork")), + let mut message = match payload_response_type { + crate::GetPayloadResponseType::Full(payload_response) => { + let (payload, _block_value, maybe_blobs_bundle): ( + ExecutionPayload, + Uint256, + Option>, + ) = payload_response.into(); + + match fork { + ForkName::Deneb => BuilderBid::Deneb(BuilderBidDeneb { + header: payload + .as_deneb() + .map_err(|_| reject("incorrect payload variant"))? + .into(), + blinded_blobs_bundle: maybe_blobs_bundle + .map(Into::into) + .unwrap_or_default(), + value: Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI), + pubkey: builder.builder_sk.public_key().compress(), + }), + ForkName::Capella => BuilderBid::Capella(BuilderBidCapella { + header: payload + .as_capella() + .map_err(|_| reject("incorrect payload variant"))? + .into(), + value: Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI), + pubkey: builder.builder_sk.public_key().compress(), + }), + ForkName::Merge => BuilderBid::Merge(BuilderBidMerge { + header: payload + .as_merge() + .map_err(|_| reject("incorrect payload variant"))? + .into(), + value: Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI), + pubkey: builder.builder_sk.public_key().compress(), + }), + ForkName::Base | ForkName::Altair => { + return Err(reject("invalid fork")) + } + } + } + crate::GetPayloadResponseType::Blinded(payload_response) => { + let (payload, _block_value, maybe_blobs_bundle): ( + ExecutionPayload, + Uint256, + Option>, + ) = payload_response.into(); + match fork { + ForkName::Deneb => BuilderBid::Deneb(BuilderBidDeneb { + header: payload + .as_deneb() + .map_err(|_| reject("incorrect payload variant"))? + .into(), + blinded_blobs_bundle: maybe_blobs_bundle + .map(Into::into) + .unwrap_or_default(), + value: Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI), + pubkey: builder.builder_sk.public_key().compress(), + }), + ForkName::Capella => BuilderBid::Capella(BuilderBidCapella { + header: payload + .as_capella() + .map_err(|_| reject("incorrect payload variant"))? + .into(), + value: Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI), + pubkey: builder.builder_sk.public_key().compress(), + }), + ForkName::Merge => BuilderBid::Merge(BuilderBidMerge { + header: payload + .as_merge() + .map_err(|_| reject("incorrect payload variant"))? + .into(), + value: Uint256::from(DEFAULT_BUILDER_PAYLOAD_VALUE_WEI), + pubkey: builder.builder_sk.public_key().compress(), + }), + ForkName::Base | ForkName::Altair => { + return Err(reject("invalid fork")) + } + } + } }; message.set_gas_limit(cached_data.gas_limit); diff --git a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs index 6717428436..72f0388e24 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs @@ -5,12 +5,12 @@ use crate::{ }, Config, *, }; +use keccak_hash::H256; use kzg::Kzg; use sensitive_url::SensitiveUrl; use task_executor::TaskExecutor; use tempfile::NamedTempFile; -use tree_hash::TreeHash; -use types::{Address, ChainSpec, Epoch, EthSpec, FullPayload, Hash256, MainnetEthSpec}; +use types::{Address, ChainSpec, Epoch, EthSpec, Hash256, MainnetEthSpec}; pub struct MockExecutionLayer { pub server: MockServer, @@ -133,20 +133,25 @@ impl MockExecutionLayer { let suggested_fee_recipient = self.el.get_suggested_fee_recipient(validator_index).await; let payload_attributes = PayloadAttributes::new(timestamp, prev_randao, suggested_fee_recipient, None, None); - let payload: ExecutionPayload = self + + let block_proposal_content_type = self .el - .get_payload::>( + .get_payload( parent_hash, &payload_attributes, forkchoice_update_params, builder_params, ForkName::Merge, &self.spec, + BlockProductionVersion::FullV2, ) .await - .unwrap() - .to_payload() - .into(); + .unwrap(); + + let payload: ExecutionPayload = match block_proposal_content_type { + BlockProposalContentsType::Full(block) => block.to_payload().into(), + BlockProposalContentsType::Blinded(_) => panic!("Should always be a full payload"), + }; let block_hash = payload.block_hash(); assert_eq!(payload.parent_hash(), parent_hash); @@ -167,20 +172,64 @@ impl MockExecutionLayer { let suggested_fee_recipient = self.el.get_suggested_fee_recipient(validator_index).await; let payload_attributes = PayloadAttributes::new(timestamp, prev_randao, suggested_fee_recipient, None, None); - let payload_header = self + + let block_proposal_content_type = self .el - .get_payload::>( + .get_payload( parent_hash, &payload_attributes, forkchoice_update_params, builder_params, ForkName::Merge, &self.spec, + BlockProductionVersion::BlindedV2, ) .await - .unwrap() - .to_payload(); + .unwrap(); + match block_proposal_content_type { + BlockProposalContentsType::Full(block) => { + let payload_header = block.to_payload(); + self.assert_valid_execution_payload_on_head( + payload, + payload_header, + block_hash, + parent_hash, + block_number, + timestamp, + prev_randao, + ) + .await; + } + BlockProposalContentsType::Blinded(block) => { + let payload_header = block.to_payload(); + self.assert_valid_execution_payload_on_head( + payload, + payload_header, + block_hash, + parent_hash, + block_number, + timestamp, + prev_randao, + ) + .await; + } + }; + + self + } + + #[allow(clippy::too_many_arguments)] + pub async fn assert_valid_execution_payload_on_head>( + &self, + payload: ExecutionPayload, + payload_header: Payload, + block_hash: ExecutionBlockHash, + parent_hash: ExecutionBlockHash, + block_number: u64, + timestamp: u64, + prev_randao: H256, + ) { assert_eq!(payload_header.block_hash(), block_hash); assert_eq!(payload_header.parent_hash(), parent_hash); assert_eq!(payload_header.block_number(), block_number); @@ -224,8 +273,6 @@ impl MockExecutionLayer { assert_eq!(head_execution_block.block_number(), block_number); assert_eq!(head_execution_block.block_hash(), block_hash); assert_eq!(head_execution_block.parent_hash(), parent_hash); - - self } pub fn move_to_block_prior_to_terminal_block(self) -> Self { diff --git a/beacon_node/genesis/src/eth1_genesis_service.rs b/beacon_node/genesis/src/eth1_genesis_service.rs index b7134e37c4..fdba9f4741 100644 --- a/beacon_node/genesis/src/eth1_genesis_service.rs +++ b/beacon_node/genesis/src/eth1_genesis_service.rs @@ -1,4 +1,4 @@ -pub use crate::{common::genesis_deposits, interop::interop_genesis_state}; +pub use crate::common::genesis_deposits; pub use eth1::Config as Eth1Config; use eth1::{DepositLog, Eth1Block, Service as Eth1Service}; diff --git a/beacon_node/http_api/src/build_block_contents.rs b/beacon_node/http_api/src/build_block_contents.rs index c8f28fa9ae..f59a4b5215 100644 --- a/beacon_node/http_api/src/build_block_contents.rs +++ b/beacon_node/http_api/src/build_block_contents.rs @@ -1,62 +1,51 @@ use beacon_chain::BlockProductionError; use eth2::types::{BeaconBlockAndBlobSidecars, BlindedBeaconBlockAndBlobSidecars, BlockContents}; -use types::{ - BeaconBlock, BlindedBlobSidecarList, BlindedPayload, BlobSidecarList, EthSpec, ForkName, - FullPayload, -}; - +use types::{AbstractExecPayload, BeaconBlock, EthSpec, ForkName, SidecarList}; type Error = warp::reject::Rejection; -type FullBlockContents = BlockContents>; -type BlindedBlockContents = BlockContents>; -pub fn build_block_contents( +pub fn build_block_contents>( fork_name: ForkName, - block: BeaconBlock>, - maybe_blobs: Option>, -) -> Result, Error> { - match fork_name { - ForkName::Base | ForkName::Altair | ForkName::Merge | ForkName::Capella => { - Ok(BlockContents::Block(block)) - } - ForkName::Deneb => { - if let Some(blob_sidecars) = maybe_blobs { - let block_and_blobs = BeaconBlockAndBlobSidecars { - block, - blob_sidecars, - }; - - Ok(BlockContents::BlockAndBlobSidecars(block_and_blobs)) - } else { - Err(warp_utils::reject::block_production_error( - BlockProductionError::MissingBlobs, - )) + block: BeaconBlock, + maybe_blobs: Option>::Sidecar>>, +) -> Result, Error> { + match Payload::block_type() { + types::BlockType::Blinded => match fork_name { + ForkName::Base | ForkName::Altair | ForkName::Merge | ForkName::Capella => { + Ok(BlockContents::Block(block)) } - } - } -} - -pub fn build_blinded_block_contents( - fork_name: ForkName, - block: BeaconBlock>, - maybe_blobs: Option>, -) -> Result, Error> { - match fork_name { - ForkName::Base | ForkName::Altair | ForkName::Merge | ForkName::Capella => { - Ok(BlockContents::Block(block)) - } - ForkName::Deneb => { - if let Some(blinded_blob_sidecars) = maybe_blobs { - let block_and_blobs = BlindedBeaconBlockAndBlobSidecars { - blinded_block: block, - blinded_blob_sidecars, - }; - - Ok(BlockContents::BlindedBlockAndBlobSidecars(block_and_blobs)) - } else { - Err(warp_utils::reject::block_production_error( - BlockProductionError::MissingBlobs, - )) - } - } + ForkName::Deneb => { + if let Some(blinded_blob_sidecars) = maybe_blobs { + let block_and_blobs = BlindedBeaconBlockAndBlobSidecars { + blinded_block: block, + blinded_blob_sidecars, + }; + + Ok(BlockContents::BlindedBlockAndBlobSidecars(block_and_blobs)) + } else { + Err(warp_utils::reject::block_production_error( + BlockProductionError::MissingBlobs, + )) + } + } + }, + types::BlockType::Full => match fork_name { + ForkName::Base | ForkName::Altair | ForkName::Merge | ForkName::Capella => { + Ok(BlockContents::Block(block)) + } + ForkName::Deneb => { + if let Some(blob_sidecars) = maybe_blobs { + let block_and_blobs = BeaconBlockAndBlobSidecars { + block, + blob_sidecars, + }; + + Ok(BlockContents::BlockAndBlobSidecars(block_and_blobs)) + } else { + Err(warp_utils::reject::block_production_error( + BlockProductionError::MissingBlobs, + )) + } + } + }, } } diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index a807655189..5b00a80bdf 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -14,6 +14,7 @@ mod build_block_contents; mod builder_states; mod database; mod metrics; +mod produce_block; mod proposer_duties; mod publish_blocks; mod standard_block_rewards; @@ -27,10 +28,11 @@ mod validator; mod validator_inclusion; mod version; +use crate::produce_block::{produce_blinded_block_v2, produce_block_v2, produce_block_v3}; use beacon_chain::{ attestation_verification::VerifiedAttestation, observed_operations::ObservationOutcome, validator_monitor::timestamp_now, AttestationError as AttnError, BeaconChain, BeaconChainError, - BeaconChainTypes, ProduceBlockVerification, WhenSlotSkipped, + BeaconChainTypes, WhenSlotSkipped, }; use beacon_processor::BeaconProcessorSend; pub use block_id::BlockId; @@ -39,8 +41,7 @@ use bytes::Bytes; use directory::DEFAULT_ROOT_DIR; use eth2::types::{ self as api_types, BroadcastValidation, EndpointVersion, ForkChoice, ForkChoiceNode, - SignedBlindedBlockContents, SignedBlockContents, SkipRandaoVerification, ValidatorId, - ValidatorStatus, + SignedBlindedBlockContents, SignedBlockContents, ValidatorId, ValidatorStatus, }; use lighthouse_network::{types::SyncState, EnrExt, NetworkGlobals, PeerId, PubsubMessage}; use lighthouse_version::version_with_platform; @@ -75,15 +76,16 @@ use tokio_stream::{ }; use types::{ Attestation, AttestationData, AttestationShufflingId, AttesterSlashing, BeaconStateError, - BlindedPayload, CommitteeCache, ConfigAndPreset, Epoch, EthSpec, ForkName, FullPayload, - ProposerPreparationData, ProposerSlashing, RelativeEpoch, SignedAggregateAndProof, - SignedBlsToExecutionChange, SignedContributionAndProof, SignedValidatorRegistrationData, - SignedVoluntaryExit, Slot, SyncCommitteeMessage, SyncContributionData, + BlindedPayload, CommitteeCache, ConfigAndPreset, Epoch, EthSpec, ForkName, + ForkVersionedResponse, Hash256, ProposerPreparationData, ProposerSlashing, RelativeEpoch, + SignedAggregateAndProof, SignedBlsToExecutionChange, SignedContributionAndProof, + SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, SyncCommitteeMessage, + SyncContributionData, }; use validator::pubkey_to_validator_index; use version::{ add_consensus_version_header, execution_optimistic_finalized_fork_versioned_response, - fork_versioned_response, inconsistent_fork_rejection, unsupported_version_rejection, V1, V2, + inconsistent_fork_rejection, unsupported_version_rejection, V1, V2, V3, }; use warp::http::StatusCode; use warp::sse::Event; @@ -142,6 +144,7 @@ pub struct Config { pub enable_beacon_processor: bool, #[serde(with = "eth2::types::serde_status_code")] pub duplicate_block_status_code: StatusCode, + pub enable_light_client_server: bool, } impl Default for Config { @@ -158,6 +161,7 @@ impl Default for Config { sse_capacity_multiplier: 1, enable_beacon_processor: true, duplicate_block_status_code: StatusCode::ACCEPTED, + enable_light_client_server: false, } } } @@ -278,6 +282,18 @@ pub fn prometheus_metrics() -> warp::filters::log::Log impl Filter + Clone { + warp::any() + .and_then(move || async move { + if is_enabled { + Ok(()) + } else { + Err(warp::reject::not_found()) + } + }) + .untuple_one() +} + /// Creates a server that will serve requests using information from `ctx`. /// /// The server will shut down gracefully when the `shutdown` future resolves. @@ -568,12 +584,12 @@ pub fn serve( chain: Arc>| { task_spawner.blocking_json_task(Priority::P1, move || { let (root, execution_optimistic, finalized) = state_id.root(&chain)?; - Ok(root) - .map(api_types::RootData::from) - .map(api_types::GenericResponse::from) - .map(|resp| { - resp.add_execution_optimistic_finalized(execution_optimistic, finalized) - }) + Ok(api_types::GenericResponse::from(api_types::RootData::from( + root, + ))) + .map(|resp| { + resp.add_execution_optimistic_finalized(execution_optimistic, finalized) + }) }) }, ); @@ -1939,8 +1955,8 @@ pub fn serve( .naive_aggregation_pool .read() .iter() - .cloned() - .filter(|att| query_filter(&att.data)), + .filter(|&att| query_filter(&att.data)) + .cloned(), ); Ok(api_types::GenericResponse::from(attestations)) }) @@ -2317,11 +2333,9 @@ pub fn serve( task_spawner.blocking_json_task(Priority::P1, move || { let (rewards, execution_optimistic, finalized) = standard_block_rewards::compute_beacon_block_rewards(chain, block_id)?; - Ok(rewards) - .map(api_types::GenericResponse::from) - .map(|resp| { - resp.add_execution_optimistic_finalized(execution_optimistic, finalized) - }) + Ok(api_types::GenericResponse::from(rewards)).map(|resp| { + resp.add_execution_optimistic_finalized(execution_optimistic, finalized) + }) }) }, ); @@ -2380,6 +2394,164 @@ pub fn serve( }, ); + /* + * beacon/light_client + */ + + let beacon_light_client_path = eth_v1 + .and(warp::path("beacon")) + .and(warp::path("light_client")) + .and(chain_filter.clone()); + + // GET beacon/light_client/bootstrap/{block_root} + let get_beacon_light_client_bootstrap = beacon_light_client_path + .clone() + .and(task_spawner_filter.clone()) + .and(warp::path("bootstrap")) + .and(warp::path::param::().or_else(|_| async { + Err(warp_utils::reject::custom_bad_request( + "Invalid block root value".to_string(), + )) + })) + .and(warp::path::end()) + .and(warp::header::optional::("accept")) + .then( + |chain: Arc>, + task_spawner: TaskSpawner, + block_root: Hash256, + accept_header: Option| { + task_spawner.blocking_response_task(Priority::P1, move || { + let (bootstrap, fork_name) = match chain.get_light_client_bootstrap(&block_root) + { + Ok(Some(res)) => res, + Ok(None) => { + return Err(warp_utils::reject::custom_not_found( + "Light client bootstrap unavailable".to_string(), + )); + } + Err(e) => { + return Err(warp_utils::reject::custom_server_error(format!( + "Unable to obtain LightClientBootstrap instance: {e:?}" + ))); + } + }; + + match accept_header { + Some(api_types::Accept::Ssz) => Response::builder() + .status(200) + .header("Content-Type", "application/octet-stream") + .body(bootstrap.as_ssz_bytes().into()) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "failed to create response: {}", + e + )) + }), + _ => Ok(warp::reply::json(&ForkVersionedResponse { + version: Some(fork_name), + data: bootstrap, + }) + .into_response()), + } + .map(|resp| add_consensus_version_header(resp, fork_name)) + }) + }, + ); + + // GET beacon/light_client/optimistic_update + let get_beacon_light_client_optimistic_update = beacon_light_client_path + .clone() + .and(task_spawner_filter.clone()) + .and(warp::path("optimistic_update")) + .and(warp::path::end()) + .and(warp::header::optional::("accept")) + .then( + |chain: Arc>, + task_spawner: TaskSpawner, + accept_header: Option| { + task_spawner.blocking_response_task(Priority::P1, move || { + let update = chain + .latest_seen_optimistic_update + .lock() + .clone() + .ok_or_else(|| { + warp_utils::reject::custom_not_found( + "No LightClientOptimisticUpdate is available".to_string(), + ) + })?; + + let fork_name = chain + .spec + .fork_name_at_slot::(update.signature_slot); + match accept_header { + Some(api_types::Accept::Ssz) => Response::builder() + .status(200) + .header("Content-Type", "application/octet-stream") + .body(update.as_ssz_bytes().into()) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "failed to create response: {}", + e + )) + }), + _ => Ok(warp::reply::json(&ForkVersionedResponse { + version: Some(fork_name), + data: update, + }) + .into_response()), + } + .map(|resp| add_consensus_version_header(resp, fork_name)) + }) + }, + ); + + // GET beacon/light_client/finality_update + let get_beacon_light_client_finality_update = beacon_light_client_path + .clone() + .and(task_spawner_filter.clone()) + .and(warp::path("finality_update")) + .and(warp::path::end()) + .and(warp::header::optional::("accept")) + .then( + |chain: Arc>, + task_spawner: TaskSpawner, + accept_header: Option| { + task_spawner.blocking_response_task(Priority::P1, move || { + let update = chain + .latest_seen_finality_update + .lock() + .clone() + .ok_or_else(|| { + warp_utils::reject::custom_not_found( + "No LightClientFinalityUpdate is available".to_string(), + ) + })?; + + let fork_name = chain + .spec + .fork_name_at_slot::(update.signature_slot); + match accept_header { + Some(api_types::Accept::Ssz) => Response::builder() + .status(200) + .header("Content-Type", "application/octet-stream") + .body(update.as_ssz_bytes().into()) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "failed to create response: {}", + e + )) + }), + _ => Ok(warp::reply::json(&ForkVersionedResponse { + version: Some(fork_name), + data: update, + }) + .into_response()), + } + .map(|resp| add_consensus_version_header(resp, fork_name)) + }) + }, + ); + /* * beacon/rewards */ @@ -2434,8 +2606,7 @@ pub fn serve( let execution_optimistic = chain.is_optimistic_or_invalid_head().unwrap_or_default(); - Ok(attestation_rewards) - .map(api_types::GenericResponse::from) + Ok(api_types::GenericResponse::from(attestation_rewards)) .map(|resp| resp.add_execution_optimistic(execution_optimistic)) }) }, @@ -2461,11 +2632,9 @@ pub fn serve( chain, block_id, validators, log, )?; - Ok(rewards) - .map(api_types::GenericResponse::from) - .map(|resp| { - resp.add_execution_optimistic_finalized(execution_optimistic, finalized) - }) + Ok(api_types::GenericResponse::from(rewards)).map(|resp| { + resp.add_execution_optimistic_finalized(execution_optimistic, finalized) + }) }) }, ); @@ -3052,17 +3221,17 @@ pub fn serve( )) })) .and(warp::path::end()) + .and(warp::header::optional::("accept")) .and(not_while_syncing_filter.clone()) .and(warp::query::()) - .and(warp::header::optional::("accept")) .and(task_spawner_filter.clone()) .and(chain_filter.clone()) .and(log_filter.clone()) .then( |endpoint_version: EndpointVersion, slot: Slot, - query: api_types::ValidatorBlocksQuery, accept_header: Option, + query: api_types::ValidatorBlocksQuery, task_spawner: TaskSpawner, chain: Arc>, log: Logger| { @@ -3073,60 +3242,10 @@ pub fn serve( "slot" => slot ); - let randao_reveal = query.randao_reveal.decompress().map_err(|e| { - warp_utils::reject::custom_bad_request(format!( - "randao reveal is not a valid BLS signature: {:?}", - e - )) - })?; - - let randao_verification = - if query.skip_randao_verification == SkipRandaoVerification::Yes { - if !randao_reveal.is_infinity() { - return Err(warp_utils::reject::custom_bad_request( - "randao_reveal must be point-at-infinity if verification is skipped" - .into(), - )); - } - ProduceBlockVerification::NoVerification - } else { - ProduceBlockVerification::VerifyRandao - }; - - let (block, _, maybe_blobs) = chain - .produce_block_with_verification::>( - randao_reveal, - slot, - query.graffiti.map(Into::into), - randao_verification, - ) - .await - .map_err(warp_utils::reject::block_production_error)?; - let fork_name = block - .to_ref() - .fork_name(&chain.spec) - .map_err(inconsistent_fork_rejection)?; - - let block_contents = - build_block_contents::build_block_contents(fork_name, block, maybe_blobs)?; - - match accept_header { - Some(api_types::Accept::Ssz) => Response::builder() - .status(200) - .header("Content-Type", "application/octet-stream") - .body(block_contents.as_ssz_bytes().into()) - .map(|res: Response| { - add_consensus_version_header(res, fork_name) - }) - .map_err(|e| { - warp_utils::reject::custom_server_error(format!( - "failed to create response: {}", - e - )) - }), - _ => fork_versioned_response(endpoint_version, fork_name, block_contents) - .map(|response| warp::reply::json(&response).into_response()) - .map(|res| add_consensus_version_header(res, fork_name)), + if endpoint_version == V3 { + produce_block_v3(endpoint_version, accept_header, chain, slot, query).await + } else { + produce_block_v2(endpoint_version, accept_header, chain, slot, query).await } }) }, @@ -3154,65 +3273,8 @@ pub fn serve( task_spawner: TaskSpawner, chain: Arc>| { task_spawner.spawn_async_with_rejection(Priority::P0, async move { - let randao_reveal = query.randao_reveal.decompress().map_err(|e| { - warp_utils::reject::custom_bad_request(format!( - "randao reveal is not a valid BLS signature: {:?}", - e - )) - })?; - - let randao_verification = - if query.skip_randao_verification == SkipRandaoVerification::Yes { - if !randao_reveal.is_infinity() { - return Err(warp_utils::reject::custom_bad_request( - "randao_reveal must be point-at-infinity if verification is skipped" - .into() - )); - } - ProduceBlockVerification::NoVerification - } else { - ProduceBlockVerification::VerifyRandao - }; - - let (block, _, maybe_blobs) = chain - .produce_block_with_verification::>( - randao_reveal, - slot, - query.graffiti.map(Into::into), - randao_verification, - ) + produce_blinded_block_v2(EndpointVersion(2), accept_header, chain, slot, query) .await - .map_err(warp_utils::reject::block_production_error)?; - let fork_name = block - .to_ref() - .fork_name(&chain.spec) - .map_err(inconsistent_fork_rejection)?; - - let block_contents = build_block_contents::build_blinded_block_contents( - fork_name, - block, - maybe_blobs, - )?; - - match accept_header { - Some(api_types::Accept::Ssz) => Response::builder() - .status(200) - .header("Content-Type", "application/octet-stream") - .body(block_contents.as_ssz_bytes().into()) - .map(|res: Response| { - add_consensus_version_header(res, fork_name) - }) - .map_err(|e| { - warp_utils::reject::custom_server_error(format!( - "failed to create response: {}", - e - )) - }), - // Pose as a V2 endpoint so we return the fork `version`. - _ => fork_versioned_response(V2, fork_name, block_contents) - .map(|response| warp::reply::json(&response).into_response()) - .map(|res| add_consensus_version_header(res, fork_name)), - } }) }, ); @@ -3740,7 +3802,6 @@ pub fn serve( .as_ref() .ok_or(BeaconChainError::BuilderMissing) .map_err(warp_utils::reject::beacon_chain_error)?; - builder .post_builder_validators(&filtered_registration_data) .await @@ -4451,6 +4512,12 @@ pub fn serve( api_types::EventTopic::LateHead => { event_handler.subscribe_late_head() } + api_types::EventTopic::LightClientFinalityUpdate => { + event_handler.subscribe_light_client_finality_update() + } + api_types::EventTopic::LightClientOptimisticUpdate => { + event_handler.subscribe_light_client_optimistic_update() + } api_types::EventTopic::BlockReward => { event_handler.subscribe_block_reward() } @@ -4604,6 +4671,18 @@ pub fn serve( .uor(get_lighthouse_database_info) .uor(get_lighthouse_block_rewards) .uor(get_lighthouse_attestation_performance) + .uor( + enable(ctx.config.enable_light_client_server) + .and(get_beacon_light_client_optimistic_update), + ) + .uor( + enable(ctx.config.enable_light_client_server) + .and(get_beacon_light_client_finality_update), + ) + .uor( + enable(ctx.config.enable_light_client_server) + .and(get_beacon_light_client_bootstrap), + ) .uor(get_lighthouse_block_packing_efficiency) .uor(get_lighthouse_merge_readiness) .uor(get_events) diff --git a/beacon_node/http_api/src/produce_block.rs b/beacon_node/http_api/src/produce_block.rs new file mode 100644 index 0000000000..73da4853e6 --- /dev/null +++ b/beacon_node/http_api/src/produce_block.rs @@ -0,0 +1,231 @@ +use bytes::Bytes; +use std::sync::Arc; +use types::{payload::BlockProductionVersion, *}; + +use beacon_chain::{ + BeaconBlockResponse, BeaconBlockResponseType, BeaconChain, BeaconChainTypes, + ProduceBlockVerification, +}; +use eth2::types::{self as api_types, EndpointVersion, SkipRandaoVerification}; +use ssz::Encode; +use warp::{ + hyper::{Body, Response}, + Reply, +}; + +use crate::{ + build_block_contents, + version::{ + add_consensus_block_value_header, add_consensus_version_header, + add_execution_payload_blinded_header, add_execution_payload_value_header, + fork_versioned_response, inconsistent_fork_rejection, + }, +}; + +pub fn get_randao_verification( + query: &api_types::ValidatorBlocksQuery, + randao_reveal_infinity: bool, +) -> Result { + let randao_verification = if query.skip_randao_verification == SkipRandaoVerification::Yes { + if !randao_reveal_infinity { + return Err(warp_utils::reject::custom_bad_request( + "randao_reveal must be point-at-infinity if verification is skipped".into(), + )); + } + ProduceBlockVerification::NoVerification + } else { + ProduceBlockVerification::VerifyRandao + }; + + Ok(randao_verification) +} + +pub async fn produce_block_v3( + endpoint_version: EndpointVersion, + accept_header: Option, + chain: Arc>, + slot: Slot, + query: api_types::ValidatorBlocksQuery, +) -> Result, warp::Rejection> { + let randao_reveal = query.randao_reveal.decompress().map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "randao reveal is not a valid BLS signature: {:?}", + e + )) + })?; + + let randao_verification = get_randao_verification(&query, randao_reveal.is_infinity())?; + + let block_response_type = chain + .produce_block_with_verification( + randao_reveal, + slot, + query.graffiti.map(Into::into), + randao_verification, + BlockProductionVersion::V3, + ) + .await + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("failed to fetch a block: {:?}", e)) + })?; + + match block_response_type { + BeaconBlockResponseType::Full(block_response) => { + build_response_v3(chain, block_response, endpoint_version, accept_header) + } + BeaconBlockResponseType::Blinded(block_response) => { + build_response_v3(chain, block_response, endpoint_version, accept_header) + } + } +} + +pub fn build_response_v3>( + chain: Arc>, + block_response: BeaconBlockResponse, + endpoint_version: EndpointVersion, + accept_header: Option, +) -> Result, warp::Rejection> { + let fork_name = block_response + .block + .to_ref() + .fork_name(&chain.spec) + .map_err(inconsistent_fork_rejection)?; + + let block_contents = build_block_contents::build_block_contents( + fork_name, + block_response.block, + block_response.maybe_side_car, + )?; + + let execution_payload_blinded = Payload::block_type() == BlockType::Blinded; + + match accept_header { + Some(api_types::Accept::Ssz) => Response::builder() + .status(200) + .header("Content-Type", "application/ssz") + .body(block_contents.as_ssz_bytes().into()) + .map(|res: Response| add_consensus_version_header(res, fork_name)) + .map(|res| add_execution_payload_blinded_header(res, execution_payload_blinded)) + .map(|res: Response| { + add_execution_payload_value_header(res, block_response.execution_payload_value) + }) + .map(|res| add_consensus_block_value_header(res, block_response.consensus_block_value)) + .map_err(|e| -> warp::Rejection { + warp_utils::reject::custom_server_error(format!("failed to create response: {}", e)) + }), + _ => fork_versioned_response(endpoint_version, fork_name, block_contents) + .map(|response| warp::reply::json(&response).into_response()) + .map(|res| add_consensus_version_header(res, fork_name)) + .map(|res| add_execution_payload_blinded_header(res, execution_payload_blinded)) + .map(|res| { + add_execution_payload_value_header(res, block_response.execution_payload_value) + }) + .map(|res| add_consensus_block_value_header(res, block_response.consensus_block_value)), + } +} + +pub async fn produce_blinded_block_v2( + endpoint_version: EndpointVersion, + accept_header: Option, + chain: Arc>, + slot: Slot, + query: api_types::ValidatorBlocksQuery, +) -> Result, warp::Rejection> { + let randao_reveal = query.randao_reveal.decompress().map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "randao reveal is not a valid BLS signature: {:?}", + e + )) + })?; + + let randao_verification = get_randao_verification(&query, randao_reveal.is_infinity())?; + let block_response_type = chain + .produce_block_with_verification( + randao_reveal, + slot, + query.graffiti.map(Into::into), + randao_verification, + BlockProductionVersion::BlindedV2, + ) + .await + .map_err(warp_utils::reject::block_production_error)?; + + match block_response_type { + BeaconBlockResponseType::Full(block_response) => { + build_response_v2(chain, block_response, endpoint_version, accept_header) + } + BeaconBlockResponseType::Blinded(block_response) => { + build_response_v2(chain, block_response, endpoint_version, accept_header) + } + } +} + +pub async fn produce_block_v2( + endpoint_version: EndpointVersion, + accept_header: Option, + chain: Arc>, + slot: Slot, + query: api_types::ValidatorBlocksQuery, +) -> Result, warp::Rejection> { + let randao_reveal = query.randao_reveal.decompress().map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "randao reveal is not a valid BLS signature: {:?}", + e + )) + })?; + + let randao_verification = get_randao_verification(&query, randao_reveal.is_infinity())?; + + let block_response_type = chain + .produce_block_with_verification( + randao_reveal, + slot, + query.graffiti.map(Into::into), + randao_verification, + BlockProductionVersion::FullV2, + ) + .await + .map_err(warp_utils::reject::block_production_error)?; + + match block_response_type { + BeaconBlockResponseType::Full(block_response) => { + build_response_v2(chain, block_response, endpoint_version, accept_header) + } + BeaconBlockResponseType::Blinded(block_response) => { + build_response_v2(chain, block_response, endpoint_version, accept_header) + } + } +} + +pub fn build_response_v2>( + chain: Arc>, + block_response: BeaconBlockResponse, + endpoint_version: EndpointVersion, + accept_header: Option, +) -> Result, warp::Rejection> { + let fork_name = block_response + .block + .to_ref() + .fork_name(&chain.spec) + .map_err(inconsistent_fork_rejection)?; + + let block_contents = build_block_contents::build_block_contents( + fork_name, + block_response.block, + block_response.maybe_side_car, + )?; + + match accept_header { + Some(api_types::Accept::Ssz) => Response::builder() + .status(200) + .header("Content-Type", "application/octet-stream") + .body(block_contents.as_ssz_bytes().into()) + .map(|res: Response| add_consensus_version_header(res, fork_name)) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!("failed to create response: {}", e)) + }), + _ => fork_versioned_response(endpoint_version, fork_name, block_contents) + .map(|response| warp::reply::json(&response).into_response()) + .map(|res| add_consensus_version_header(res, fork_name)), + } +} diff --git a/beacon_node/http_api/src/proposer_duties.rs b/beacon_node/http_api/src/proposer_duties.rs index 708df39b4d..c31dd9b1fa 100644 --- a/beacon_node/http_api/src/proposer_duties.rs +++ b/beacon_node/http_api/src/proposer_duties.rs @@ -97,12 +97,12 @@ fn try_proposer_duties_from_cache( let head = chain.canonical_head.cached_head(); let head_block = &head.snapshot.beacon_block; let head_block_root = head.head_block_root(); + let head_epoch = head_block.slot().epoch(T::EthSpec::slots_per_epoch()); let head_decision_root = head .snapshot .beacon_state .proposer_shuffling_decision_root(head_block_root) .map_err(warp_utils::reject::beacon_state_error)?; - let head_epoch = head_block.slot().epoch(T::EthSpec::slots_per_epoch()); let execution_optimistic = chain .is_optimistic_or_invalid_head_block(head_block) .map_err(warp_utils::reject::beacon_chain_error)?; diff --git a/beacon_node/http_api/src/test_utils.rs b/beacon_node/http_api/src/test_utils.rs index fe47e56dc5..bafb573819 100644 --- a/beacon_node/http_api/src/test_utils.rs +++ b/beacon_node/http_api/src/test_utils.rs @@ -209,6 +209,7 @@ pub async fn create_api_server( enabled: true, listen_port: port, data_dir: std::path::PathBuf::from(DEFAULT_ROOT_DIR), + enable_light_client_server: true, ..Config::default() }, chain: Some(chain), diff --git a/beacon_node/http_api/src/validator.rs b/beacon_node/http_api/src/validator.rs index 18e9dbf636..7f11ddd8f4 100644 --- a/beacon_node/http_api/src/validator.rs +++ b/beacon_node/http_api/src/validator.rs @@ -1,5 +1,5 @@ use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes}; -use types::*; +use types::{BeaconState, PublicKeyBytes}; /// Uses the `chain.validator_pubkey_cache` to resolve a pubkey to a validator /// index and then ensures that the validator exists in the given `state`. diff --git a/beacon_node/http_api/src/version.rs b/beacon_node/http_api/src/version.rs index e01ff98220..7b06901243 100644 --- a/beacon_node/http_api/src/version.rs +++ b/beacon_node/http_api/src/version.rs @@ -1,12 +1,16 @@ use crate::api_types::fork_versioned_response::ExecutionOptimisticFinalizedForkVersionedResponse; use crate::api_types::EndpointVersion; -use eth2::CONSENSUS_VERSION_HEADER; +use eth2::{ + CONSENSUS_BLOCK_VALUE_HEADER, CONSENSUS_VERSION_HEADER, EXECUTION_PAYLOAD_BLINDED_HEADER, + EXECUTION_PAYLOAD_VALUE_HEADER, +}; use serde::Serialize; -use types::{ForkName, ForkVersionedResponse, InconsistentFork}; +use types::{ForkName, ForkVersionedResponse, InconsistentFork, Uint256}; use warp::reply::{self, Reply, Response}; pub const V1: EndpointVersion = EndpointVersion(1); pub const V2: EndpointVersion = EndpointVersion(2); +pub const V3: EndpointVersion = EndpointVersion(3); pub fn fork_versioned_response( endpoint_version: EndpointVersion, @@ -15,7 +19,7 @@ pub fn fork_versioned_response( ) -> Result, warp::reject::Rejection> { let fork_name = if endpoint_version == V1 { None - } else if endpoint_version == V2 { + } else if endpoint_version == V2 || endpoint_version == V3 { Some(fork_name) } else { return Err(unsupported_version_rejection(endpoint_version)); @@ -53,6 +57,45 @@ pub fn add_consensus_version_header(reply: T, fork_name: ForkName) -> reply::with_header(reply, CONSENSUS_VERSION_HEADER, fork_name.to_string()).into_response() } +/// Add the `Eth-Execution-Payload-Blinded` header to a response. +pub fn add_execution_payload_blinded_header( + reply: T, + execution_payload_blinded: bool, +) -> Response { + reply::with_header( + reply, + EXECUTION_PAYLOAD_BLINDED_HEADER, + execution_payload_blinded.to_string(), + ) + .into_response() +} + +/// Add the `Eth-Execution-Payload-Value` header to a response. +pub fn add_execution_payload_value_header( + reply: T, + execution_payload_value: Option, +) -> Response { + reply::with_header( + reply, + EXECUTION_PAYLOAD_VALUE_HEADER, + execution_payload_value.unwrap_or_default().to_string(), + ) + .into_response() +} + +/// Add the `Eth-Consensus-Block-Value` header to a response. +pub fn add_consensus_block_value_header( + reply: T, + consensus_payload_value: Option, +) -> Response { + reply::with_header( + reply, + CONSENSUS_BLOCK_VALUE_HEADER, + consensus_payload_value.unwrap_or_default().to_string(), + ) + .into_response() +} + pub fn inconsistent_fork_rejection(error: InconsistentFork) -> warp::reject::Rejection { warp_utils::reject::custom_server_error(format!("wrong fork: {:?}", error)) } diff --git a/beacon_node/http_api/tests/interactive_tests.rs b/beacon_node/http_api/tests/interactive_tests.rs index 17213d6f53..327215209f 100644 --- a/beacon_node/http_api/tests/interactive_tests.rs +++ b/beacon_node/http_api/tests/interactive_tests.rs @@ -21,6 +21,8 @@ use types::{ MainnetEthSpec, MinimalEthSpec, ProposerPreparationData, Slot, }; +use eth2::types::ForkVersionedBeaconBlockType::{Blinded, Full}; + type E = MainnetEthSpec; // Test that the deposit_contract endpoint returns the correct chain_id and address. @@ -617,13 +619,18 @@ pub async fn proposer_boost_re_org_test( let randao_reveal = harness .sign_randao_reveal(&state_b, proposer_index, slot_c) .into(); - let unsigned_block_contents_c = tester + let unsigned_block_type = tester .client - .get_validator_blocks(slot_c, &randao_reveal, None) + .get_validator_blocks_v3::(slot_c, &randao_reveal, None) .await - .unwrap() - .data; - let (unsigned_block_c, block_c_blobs) = unsigned_block_contents_c.deconstruct(); + .unwrap(); + + let (unsigned_block_c, block_c_blobs) = match unsigned_block_type { + Full(unsigned_block_contents_c) => unsigned_block_contents_c.data.deconstruct(), + Blinded(_) => { + panic!("Should not be a blinded block"); + } + }; let block_c = harness.sign_beacon_block(unsigned_block_c, &state_b); if should_re_org { diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 90e9969ec9..1ac9602d33 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -38,6 +38,8 @@ use types::{ MainnetEthSpec, RelativeEpoch, SelectionProof, SignedRoot, Slot, }; +use eth2::types::ForkVersionedBeaconBlockType::{Blinded, Full}; + type E = MainnetEthSpec; const SECONDS_PER_SLOT: u64 = 12; @@ -1644,6 +1646,59 @@ impl ApiTester { self } + pub async fn test_get_beacon_light_client_bootstrap(self) -> Self { + let block_id = BlockId(CoreBlockId::Finalized); + let (block_root, _, _) = block_id.root(&self.chain).unwrap(); + let (block, _, _) = block_id.full_block(&self.chain).await.unwrap(); + + let result = match self + .client + .get_light_client_bootstrap::(block_root) + .await + { + Ok(result) => result.unwrap().data, + Err(e) => panic!("query failed incorrectly: {e:?}"), + }; + + let expected = block.slot(); + assert_eq!(result.header.beacon.slot, expected); + + self + } + + pub async fn test_get_beacon_light_client_optimistic_update(self) -> Self { + // get_beacon_light_client_optimistic_update returns Ok(None) on 404 NOT FOUND + let result = match self + .client + .get_beacon_light_client_optimistic_update::() + .await + { + Ok(result) => result.map(|res| res.data), + Err(e) => panic!("query failed incorrectly: {e:?}"), + }; + + let expected = self.chain.latest_seen_optimistic_update.lock().clone(); + assert_eq!(result, expected); + + self + } + + pub async fn test_get_beacon_light_client_finality_update(self) -> Self { + let result = match self + .client + .get_beacon_light_client_finality_update::() + .await + { + Ok(result) => result.map(|res| res.data), + Err(e) => panic!("query failed incorrectly: {e:?}"), + }; + + let expected = self.chain.latest_seen_finality_update.lock().clone(); + assert_eq!(result, expected); + + self + } + pub async fn test_get_beacon_pool_attestations(self) -> Self { let result = self .client @@ -2604,6 +2659,98 @@ impl ApiTester { self } + pub async fn test_block_production_v3_ssz(self) -> Self { + let fork = self.chain.canonical_head.cached_head().head_fork(); + let genesis_validators_root = self.chain.genesis_validators_root; + + for _ in 0..E::slots_per_epoch() * 3 { + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + + let proposer_pubkey_bytes = self + .client + .get_validator_duties_proposer(epoch) + .await + .unwrap() + .data + .into_iter() + .find(|duty| duty.slot == slot) + .map(|duty| duty.pubkey) + .unwrap(); + let proposer_pubkey = (&proposer_pubkey_bytes).try_into().unwrap(); + + let sk = self + .validator_keypairs() + .iter() + .find(|kp| kp.pk == proposer_pubkey) + .map(|kp| kp.sk.clone()) + .unwrap(); + + let randao_reveal = { + let domain = self.chain.spec.get_domain( + epoch, + Domain::Randao, + &fork, + genesis_validators_root, + ); + let message = epoch.signing_root(domain); + sk.sign(message).into() + }; + + let (fork_version_response_bytes, is_blinded_payload) = self + .client + .get_validator_blocks_v3_ssz::(slot, &randao_reveal, None) + .await + .unwrap(); + + if is_blinded_payload { + let block_contents = >>::from_ssz_bytes( + &fork_version_response_bytes.unwrap(), + &self.chain.spec, + ) + .expect("block contents bytes can be decoded"); + + let signed_block_contents = + block_contents.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); + + self.client + .post_beacon_blocks_ssz(&signed_block_contents) + .await + .unwrap(); + + // This converts the generic `Payload` to a concrete type for comparison. + let signed_block = signed_block_contents.deconstruct().0; + let head_block = SignedBeaconBlock::from(signed_block.clone()); + assert_eq!(head_block, signed_block); + + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + } else { + let block_contents = >>::from_ssz_bytes( + &fork_version_response_bytes.unwrap(), + &self.chain.spec, + ) + .expect("block contents bytes can be decoded"); + + let signed_block_contents = + block_contents.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); + + self.client + .post_beacon_blocks_ssz(&signed_block_contents) + .await + .unwrap(); + + assert_eq!( + self.chain.head_beacon_block().as_ref(), + signed_block_contents.signed_block() + ); + + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + } + } + + self + } + pub async fn test_block_production_no_verify_randao(self) -> Self { for _ in 0..E::slots_per_epoch() { let slot = self.chain.slot().unwrap(); @@ -3360,6 +3507,36 @@ impl ApiTester { (proposer_index, randao_reveal) } + pub async fn test_payload_v3_respects_registration(self) -> Self { + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + + let (proposer_index, randao_reveal) = self.get_test_randao(slot, epoch).await; + + let payload_type = self + .client + .get_validator_blocks_v3::(slot, &randao_reveal, None) + .await + .unwrap(); + + let payload: BlindedPayload = match payload_type { + Blinded(payload) => payload + .data + .block() + .body() + .execution_payload() + .unwrap() + .into(), + Full(_) => panic!("Expecting a blinded payload"), + }; + + let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); + assert_eq!(payload.fee_recipient(), expected_fee_recipient); + assert_eq!(payload.gas_limit(), 11_111_111); + + self + } + pub async fn test_payload_respects_registration(self) -> Self { let slot = self.chain.slot().unwrap(); let epoch = self.chain.epoch().unwrap(); @@ -3434,6 +3611,42 @@ impl ApiTester { self } + pub async fn test_payload_v3_accepts_mutated_gas_limit(self) -> Self { + // Mutate gas limit. + self.mock_builder + .as_ref() + .unwrap() + .add_operation(Operation::GasLimit(30_000_000)); + + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + + let (proposer_index, randao_reveal) = self.get_test_randao(slot, epoch).await; + + let payload_type = self + .client + .get_validator_blocks_v3::(slot, &randao_reveal, None) + .await + .unwrap(); + + let payload: BlindedPayload = match payload_type { + Blinded(payload) => payload + .data + .block() + .body() + .execution_payload() + .unwrap() + .into(), + Full(_) => panic!("Expecting a blinded payload"), + }; + + let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); + assert_eq!(payload.fee_recipient(), expected_fee_recipient); + assert_eq!(payload.gas_limit(), 30_000_000); + + self + } + pub async fn test_payload_accepts_changed_fee_recipient(self) -> Self { let test_fee_recipient = "0x4242424242424242424242424242424242424242" .parse::

() @@ -3475,6 +3688,44 @@ impl ApiTester { self } + pub async fn test_payload_v3_accepts_changed_fee_recipient(self) -> Self { + let test_fee_recipient = "0x4242424242424242424242424242424242424242" + .parse::
() + .unwrap(); + + // Mutate fee recipient. + self.mock_builder + .as_ref() + .unwrap() + .add_operation(Operation::FeeRecipient(test_fee_recipient)); + + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + + let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; + + let payload_type = self + .client + .get_validator_blocks_v3::(slot, &randao_reveal, None) + .await + .unwrap(); + + let payload: BlindedPayload = match payload_type { + Blinded(payload) => payload + .data + .block() + .body() + .execution_payload() + .unwrap() + .into(), + Full(_) => panic!("Expecting a blinded payload"), + }; + + assert_eq!(payload.fee_recipient(), test_fee_recipient); + + self + } + pub async fn test_payload_rejects_invalid_parent_hash(self) -> Self { let invalid_parent_hash = "0x4242424242424242424242424242424242424242424242424242424242424242" @@ -3524,6 +3775,52 @@ impl ApiTester { self } + pub async fn test_payload_v3_rejects_invalid_parent_hash(self) -> Self { + let invalid_parent_hash = + "0x4242424242424242424242424242424242424242424242424242424242424242" + .parse::() + .unwrap(); + + // Mutate parent hash. + self.mock_builder + .as_ref() + .unwrap() + .add_operation(Operation::ParentHash(invalid_parent_hash)); + + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + let expected_parent_hash = self + .chain + .head_snapshot() + .beacon_state + .latest_execution_payload_header() + .unwrap() + .block_hash(); + + let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; + + let payload_type = self + .client + .get_validator_blocks_v3::(slot, &randao_reveal, None) + .await + .unwrap(); + + let payload: FullPayload = match payload_type { + Full(payload) => payload + .data + .block() + .body() + .execution_payload() + .unwrap() + .into(), + Blinded(_) => panic!("Expecting a blinded payload"), + }; + + assert_eq!(payload.parent_hash(), expected_parent_hash); + + self + } + pub async fn test_payload_rejects_invalid_prev_randao(self) -> Self { let invalid_prev_randao = "0x4242424242424242424242424242424242424242424242424242424242424242" @@ -3544,7 +3841,6 @@ impl ApiTester { .cached_head() .head_random() .unwrap(); - let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; let payload: BlindedPayload = self @@ -3572,6 +3868,50 @@ impl ApiTester { self } + pub async fn test_payload_v3_rejects_invalid_prev_randao(self) -> Self { + let invalid_prev_randao = + "0x4242424242424242424242424242424242424242424242424242424242424242" + .parse::() + .unwrap(); + + // Mutate prev randao. + self.mock_builder + .as_ref() + .unwrap() + .add_operation(Operation::PrevRandao(invalid_prev_randao)); + + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + let expected_prev_randao = self + .chain + .canonical_head + .cached_head() + .head_random() + .unwrap(); + let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; + + let payload_type = self + .client + .get_validator_blocks_v3::(slot, &randao_reveal, None) + .await + .unwrap(); + + let payload: FullPayload = match payload_type { + Full(payload) => payload + .data + .block() + .body() + .execution_payload() + .unwrap() + .into(), + Blinded(_) => panic!("Expecting a full payload"), + }; + + assert_eq!(payload.prev_randao(), expected_prev_randao); + + self + } + pub async fn test_payload_rejects_invalid_block_number(self) -> Self { let invalid_block_number = 2; @@ -3619,6 +3959,50 @@ impl ApiTester { self } + pub async fn test_payload_v3_rejects_invalid_block_number(self) -> Self { + let invalid_block_number = 2; + + // Mutate block number. + self.mock_builder + .as_ref() + .unwrap() + .add_operation(Operation::BlockNumber(invalid_block_number)); + + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + let expected_block_number = self + .chain + .head_snapshot() + .beacon_state + .latest_execution_payload_header() + .unwrap() + .block_number() + + 1; + + let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; + + let payload_type = self + .client + .get_validator_blocks_v3::(slot, &randao_reveal, None) + .await + .unwrap(); + + let payload: FullPayload = match payload_type { + Full(payload) => payload + .data + .block() + .body() + .execution_payload() + .unwrap() + .into(), + Blinded(_) => panic!("Expecting a full payload"), + }; + + assert_eq!(payload.block_number(), expected_block_number); + + self + } + pub async fn test_payload_rejects_invalid_timestamp(self) -> Self { let invalid_timestamp = 2; @@ -3665,6 +4049,49 @@ impl ApiTester { self } + pub async fn test_payload_v3_rejects_invalid_timestamp(self) -> Self { + let invalid_timestamp = 2; + + // Mutate timestamp. + self.mock_builder + .as_ref() + .unwrap() + .add_operation(Operation::Timestamp(invalid_timestamp)); + + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + let min_expected_timestamp = self + .chain + .head_snapshot() + .beacon_state + .latest_execution_payload_header() + .unwrap() + .timestamp(); + + let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; + + let payload_type = self + .client + .get_validator_blocks_v3::(slot, &randao_reveal, None) + .await + .unwrap(); + + let payload: FullPayload = match payload_type { + Full(payload) => payload + .data + .block() + .body() + .execution_payload() + .unwrap() + .into(), + Blinded(_) => panic!("Expecting a blinded payload"), + }; + + assert!(payload.timestamp() > min_expected_timestamp); + + self + } + pub async fn test_payload_rejects_invalid_signature(self) -> Self { self.mock_builder.as_ref().unwrap().invalid_signatures(); @@ -3696,6 +4123,28 @@ impl ApiTester { self } + pub async fn test_payload_v3_rejects_invalid_signature(self) -> Self { + self.mock_builder.as_ref().unwrap().invalid_signatures(); + + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + + let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; + + let payload_type = self + .client + .get_validator_blocks_v3::(slot, &randao_reveal, None) + .await + .unwrap(); + + match payload_type { + Full(_) => (), + Blinded(_) => panic!("Expecting a full payload"), + }; + + self + } + pub async fn test_builder_chain_health_skips(self) -> Self { let slot = self.chain.slot().unwrap(); @@ -3734,6 +4183,35 @@ impl ApiTester { self } + pub async fn test_builder_v3_chain_health_skips(self) -> Self { + let slot = self.chain.slot().unwrap(); + + // Since we are proposing this slot, start the count from the previous slot. + let prev_slot = slot - Slot::new(1); + let head_slot = self.chain.canonical_head.cached_head().head_slot(); + let epoch = self.chain.epoch().unwrap(); + + // Inclusive here to make sure we advance one slot past the threshold. + for _ in (prev_slot - head_slot).as_usize()..=self.chain.config.builder_fallback_skips { + self.harness.advance_slot(); + } + + let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; + + let payload_type = self + .client + .get_validator_blocks_v3::(slot, &randao_reveal, None) + .await + .unwrap(); + + match payload_type { + Full(_) => (), + Blinded(_) => panic!("Expecting a full payload"), + }; + + self + } + pub async fn test_builder_chain_health_skips_per_epoch(self) -> Self { // Fill an epoch with `builder_fallback_skips_per_epoch` skip slots. for i in 0..E::slots_per_epoch() { @@ -3809,6 +4287,61 @@ impl ApiTester { self } + pub async fn test_builder_v3_chain_health_skips_per_epoch(self) -> Self { + // Fill an epoch with `builder_fallback_skips_per_epoch` skip slots. + for i in 0..E::slots_per_epoch() { + if i == 0 || i as usize > self.chain.config.builder_fallback_skips_per_epoch { + self.harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + } + self.harness.advance_slot(); + } + + let next_slot = self.chain.slot().unwrap(); + + let (_, randao_reveal) = self + .get_test_randao(next_slot, next_slot.epoch(E::slots_per_epoch())) + .await; + + let payload_type = self + .client + .get_validator_blocks_v3::(next_slot, &randao_reveal, None) + .await + .unwrap(); + + match payload_type { + Blinded(_) => (), + Full(_) => panic!("Expecting a blinded payload"), + }; + + // Without proposing, advance into the next slot, this should make us cross the threshold + // number of skips, causing us to use the fallback. + self.harness.advance_slot(); + let next_slot = self.chain.slot().unwrap(); + + let (_, randao_reveal) = self + .get_test_randao(next_slot, next_slot.epoch(E::slots_per_epoch())) + .await; + + let payload_type = self + .client + .get_validator_blocks_v3::(next_slot, &randao_reveal, None) + .await + .unwrap(); + + match payload_type { + Full(_) => (), + Blinded(_) => panic!("Expecting a full payload"), + }; + + self + } + pub async fn test_builder_chain_health_epochs_since_finalization(self) -> Self { let skips = E::slots_per_epoch() * self.chain.config.builder_fallback_epochs_since_finalization as u64; @@ -3899,6 +4432,76 @@ impl ApiTester { self } + pub async fn test_builder_v3_chain_health_epochs_since_finalization(self) -> Self { + let skips = E::slots_per_epoch() + * self.chain.config.builder_fallback_epochs_since_finalization as u64; + + for _ in 0..skips { + self.harness.advance_slot(); + } + + // Fill the next epoch with blocks, should be enough to justify, not finalize. + for _ in 0..E::slots_per_epoch() { + self.harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + self.harness.advance_slot(); + } + + let next_slot = self.chain.slot().unwrap(); + + let (_, randao_reveal) = self + .get_test_randao(next_slot, next_slot.epoch(E::slots_per_epoch())) + .await; + + let payload_type = self + .client + .get_validator_blocks_v3::(next_slot, &randao_reveal, None) + .await + .unwrap(); + + match payload_type { + Full(_) => (), + Blinded(_) => panic!("Expecting a full payload"), + }; + + // Fill another epoch with blocks, should be enough to finalize. (Sneaky plus 1 because this + // scenario starts at an epoch boundary). + for _ in 0..E::slots_per_epoch() + 1 { + self.harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + self.harness.advance_slot(); + } + + let next_slot = self.chain.slot().unwrap(); + + let (_, randao_reveal) = self + .get_test_randao(next_slot, next_slot.epoch(E::slots_per_epoch())) + .await; + + let payload_type = self + .client + .get_validator_blocks_v3::(next_slot, &randao_reveal, None) + .await + .unwrap(); + + match payload_type { + Blinded(_) => (), + Full(_) => panic!("Expecting a blinded payload"), + }; + + self + } + pub async fn test_builder_chain_health_optimistic_head(self) -> Self { // Make sure the next payload verification will return optimistic before advancing the chain. self.harness.mock_execution_layer.as_ref().map(|el| { @@ -3946,6 +4549,49 @@ impl ApiTester { self } + pub async fn test_builder_v3_chain_health_optimistic_head(self) -> Self { + // Make sure the next payload verification will return optimistic before advancing the chain. + self.harness.mock_execution_layer.as_ref().map(|el| { + el.server.all_payloads_syncing(true); + el + }); + self.harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + self.harness.advance_slot(); + + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + + let (proposer_index, randao_reveal) = self.get_test_randao(slot, epoch).await; + + let payload_type = self + .client + .get_validator_blocks_v3::(slot, &randao_reveal, None) + .await + .unwrap(); + + let payload: FullPayload = match payload_type { + Full(payload) => payload + .data + .block() + .body() + .execution_payload() + .unwrap() + .into(), + Blinded(_) => panic!("Expecting a full payload"), + }; + + let expected_fee_recipient = Address::from_low_u64_be(proposer_index as u64); + assert_eq!(payload.fee_recipient(), expected_fee_recipient); + + self + } + pub async fn test_payload_rejects_inadequate_builder_threshold(self) -> Self { // Mutate value. self.mock_builder @@ -3983,6 +4629,34 @@ impl ApiTester { self } + pub async fn test_payload_v3_rejects_inadequate_builder_threshold(self) -> Self { + // Mutate value. + self.mock_builder + .as_ref() + .unwrap() + .add_operation(Operation::Value(Uint256::from( + DEFAULT_BUILDER_THRESHOLD_WEI - 1, + ))); + + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + + let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; + + let payload_type = self + .client + .get_validator_blocks_v3::(slot, &randao_reveal, None) + .await + .unwrap(); + + match payload_type { + Full(_) => (), + Blinded(_) => panic!("Expecting a full payload"), + }; + + self + } + pub async fn test_builder_payload_chosen_when_more_profitable(self) -> Self { // Mutate value. self.mock_builder @@ -4020,6 +4694,34 @@ impl ApiTester { self } + pub async fn test_builder_payload_v3_chosen_when_more_profitable(self) -> Self { + // Mutate value. + self.mock_builder + .as_ref() + .unwrap() + .add_operation(Operation::Value(Uint256::from( + DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI + 1, + ))); + + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + + let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; + + let payload_type = self + .client + .get_validator_blocks_v3::(slot, &randao_reveal, None) + .await + .unwrap(); + + match payload_type { + Blinded(_) => (), + Full(_) => panic!("Expecting a blinded payload"), + }; + + self + } + pub async fn test_local_payload_chosen_when_equally_profitable(self) -> Self { // Mutate value. self.mock_builder @@ -4057,6 +4759,34 @@ impl ApiTester { self } + pub async fn test_local_payload_v3_chosen_when_equally_profitable(self) -> Self { + // Mutate value. + self.mock_builder + .as_ref() + .unwrap() + .add_operation(Operation::Value(Uint256::from( + DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI, + ))); + + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + + let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; + + let payload_type = self + .client + .get_validator_blocks_v3::(slot, &randao_reveal, None) + .await + .unwrap(); + + match payload_type { + Full(_) => (), + Blinded(_) => panic!("Expecting a full payload"), + }; + + self + } + pub async fn test_local_payload_chosen_when_more_profitable(self) -> Self { // Mutate value. self.mock_builder @@ -4094,6 +4824,34 @@ impl ApiTester { self } + pub async fn test_local_payload_v3_chosen_when_more_profitable(self) -> Self { + // Mutate value. + self.mock_builder + .as_ref() + .unwrap() + .add_operation(Operation::Value(Uint256::from( + DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI - 1, + ))); + + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + + let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; + + let payload_type = self + .client + .get_validator_blocks_v3::(slot, &randao_reveal, None) + .await + .unwrap(); + + match payload_type { + Full(_) => (), + Blinded(_) => panic!("Expecting a full payload"), + }; + + self + } + pub async fn test_builder_works_post_capella(self) -> Self { // Ensure builder payload is chosen self.mock_builder @@ -4143,26 +4901,22 @@ impl ApiTester { let epoch = self.chain.epoch().unwrap(); let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; - let block_contents = self + let payload_type = self .client - .get_validator_blinded_blocks::>(slot, &randao_reveal, None) + .get_validator_blocks_v3::(slot, &randao_reveal, None) .await - .unwrap() - .data; - let (block, maybe_sidecars) = block_contents.deconstruct(); + .unwrap(); + + let block_contents = match payload_type { + Blinded(payload) => payload.data, + Full(_) => panic!("Expecting a blinded payload"), + }; + + let (_, maybe_sidecars) = block_contents.deconstruct(); // Response should contain blob sidecars assert!(maybe_sidecars.is_some()); - // The builder's payload should've been chosen, so this cache should not be populated - let payload: BlindedPayload = block.body().execution_payload().unwrap().into(); - assert!(self - .chain - .execution_layer - .as_ref() - .unwrap() - .get_payload_by_root(&payload.tree_hash_root()) - .is_none()); self } @@ -4207,6 +4961,38 @@ impl ApiTester { self } + pub async fn test_lighthouse_rejects_invalid_withdrawals_root_v3(self) -> Self { + // Ensure builder payload *would be* chosen + self.mock_builder + .as_ref() + .unwrap() + .add_operation(Operation::Value(Uint256::from( + DEFAULT_MOCK_EL_PAYLOAD_VALUE_WEI + 1, + ))); + // Set withdrawals root to something invalid + self.mock_builder + .as_ref() + .unwrap() + .add_operation(Operation::WithdrawalsRoot(Hash256::repeat_byte(0x42))); + + let slot = self.chain.slot().unwrap(); + let epoch = self.chain.epoch().unwrap(); + let (_, randao_reveal) = self.get_test_randao(slot, epoch).await; + + let payload_type = self + .client + .get_validator_blocks_v3::(slot, &randao_reveal, None) + .await + .unwrap(); + + match payload_type { + Full(_) => (), + Blinded(_) => panic!("Expecting a full payload"), + }; + + self + } + #[cfg(target_os = "linux")] pub async fn test_get_lighthouse_health(self) -> Self { self.client.get_lighthouse_health().await.unwrap(); @@ -4586,8 +5372,7 @@ impl ApiTester { assert_eq!(withdrawal_response.finalized, Some(false)); assert_eq!(withdrawal_response.data, expected_withdrawals.to_vec()); } - Err(e) => { - println!("{:?}", e); + Err(_) => { panic!("query failed incorrectly"); } } @@ -4969,6 +5754,42 @@ async fn node_get() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn get_light_client_bootstrap() { + let config = ApiTesterConfig { + spec: ForkName::Altair.make_genesis_spec(E::default_spec()), + ..<_>::default() + }; + ApiTester::new_from_config(config) + .await + .test_get_beacon_light_client_bootstrap() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn get_light_client_optimistic_update() { + let config = ApiTesterConfig { + spec: ForkName::Altair.make_genesis_spec(E::default_spec()), + ..<_>::default() + }; + ApiTester::new_from_config(config) + .await + .test_get_beacon_light_client_optimistic_update() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn get_light_client_finality_update() { + let config = ApiTesterConfig { + spec: ForkName::Altair.make_genesis_spec(E::default_spec()), + ..<_>::default() + }; + ApiTester::new_from_config(config) + .await + .test_get_beacon_light_client_finality_update() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn get_validator_duties_early() { ApiTester::new() @@ -5055,6 +5876,20 @@ async fn block_production_ssz_with_skip_slots() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn block_production_ssz_v3() { + ApiTester::new().await.test_block_production_v3_ssz().await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn block_production_v3_ssz_with_skip_slots() { + ApiTester::new() + .await + .skip_slots(E::slots_per_epoch() * 2) + .test_block_production_v3_ssz() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn blinded_block_production_full_payload_premerge() { ApiTester::new() @@ -5238,6 +6073,14 @@ async fn post_validator_register_valid() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn post_validator_register_valid_v3() { + ApiTester::new_mev_tester() + .await + .test_payload_v3_respects_registration() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn post_validator_register_gas_limit_mutation() { ApiTester::new_mev_tester() @@ -5246,6 +6089,14 @@ async fn post_validator_register_gas_limit_mutation() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn post_validator_register_gas_limit_mutation_v3() { + ApiTester::new_mev_tester() + .await + .test_payload_v3_accepts_mutated_gas_limit() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn post_validator_register_fee_recipient_mutation() { ApiTester::new_mev_tester() @@ -5254,6 +6105,14 @@ async fn post_validator_register_fee_recipient_mutation() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn post_validator_register_fee_recipient_mutation_v3() { + ApiTester::new_mev_tester() + .await + .test_payload_v3_accepts_changed_fee_recipient() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn get_blinded_block_invalid_parent_hash() { ApiTester::new_mev_tester() @@ -5262,6 +6121,14 @@ async fn get_blinded_block_invalid_parent_hash() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn get_full_block_invalid_parent_hash_v3() { + ApiTester::new_mev_tester() + .await + .test_payload_v3_rejects_invalid_parent_hash() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn get_blinded_block_invalid_prev_randao() { ApiTester::new_mev_tester() @@ -5270,6 +6137,14 @@ async fn get_blinded_block_invalid_prev_randao() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn get_full_block_invalid_prev_randao_v3() { + ApiTester::new_mev_tester() + .await + .test_payload_v3_rejects_invalid_prev_randao() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn get_blinded_block_invalid_block_number() { ApiTester::new_mev_tester() @@ -5278,6 +6153,14 @@ async fn get_blinded_block_invalid_block_number() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn get_full_block_invalid_block_number_v3() { + ApiTester::new_mev_tester() + .await + .test_payload_v3_rejects_invalid_block_number() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn get_blinded_block_invalid_timestamp() { ApiTester::new_mev_tester() @@ -5286,6 +6169,14 @@ async fn get_blinded_block_invalid_timestamp() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn get_full_block_invalid_timestamp_v3() { + ApiTester::new_mev_tester() + .await + .test_payload_v3_rejects_invalid_timestamp() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn get_blinded_block_invalid_signature() { ApiTester::new_mev_tester() @@ -5294,6 +6185,14 @@ async fn get_blinded_block_invalid_signature() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn get_full_block_invalid_signature_v3() { + ApiTester::new_mev_tester() + .await + .test_payload_v3_rejects_invalid_signature() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn builder_chain_health_skips() { ApiTester::new_mev_tester() @@ -5302,6 +6201,14 @@ async fn builder_chain_health_skips() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn builder_chain_health_skips_v3() { + ApiTester::new_mev_tester() + .await + .test_builder_v3_chain_health_skips() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn builder_chain_health_skips_per_epoch() { ApiTester::new_mev_tester() @@ -5310,6 +6217,14 @@ async fn builder_chain_health_skips_per_epoch() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn builder_chain_health_skips_per_epoch_v3() { + ApiTester::new_mev_tester() + .await + .test_builder_v3_chain_health_skips_per_epoch() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn builder_chain_health_epochs_since_finalization() { ApiTester::new_mev_tester() @@ -5318,6 +6233,14 @@ async fn builder_chain_health_epochs_since_finalization() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn builder_chain_health_epochs_since_finalization_v3() { + ApiTester::new_mev_tester() + .await + .test_builder_v3_chain_health_epochs_since_finalization() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn builder_chain_health_optimistic_head() { ApiTester::new_mev_tester() @@ -5326,6 +6249,14 @@ async fn builder_chain_health_optimistic_head() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn builder_chain_health_optimistic_head_v3() { + ApiTester::new_mev_tester() + .await + .test_builder_v3_chain_health_optimistic_head() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn builder_inadequate_builder_threshold() { ApiTester::new_mev_tester() @@ -5334,6 +6265,14 @@ async fn builder_inadequate_builder_threshold() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn builder_inadequate_builder_threshold_v3() { + ApiTester::new_mev_tester() + .await + .test_payload_v3_rejects_inadequate_builder_threshold() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn builder_payload_chosen_by_profit() { ApiTester::new_mev_tester_no_builder_threshold() @@ -5346,6 +6285,18 @@ async fn builder_payload_chosen_by_profit() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn builder_payload_chosen_by_profit_v3() { + ApiTester::new_mev_tester_no_builder_threshold() + .await + .test_builder_payload_v3_chosen_when_more_profitable() + .await + .test_local_payload_v3_chosen_when_equally_profitable() + .await + .test_local_payload_v3_chosen_when_more_profitable() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn builder_works_post_capella() { let mut config = ApiTesterConfig { @@ -5384,6 +6335,8 @@ async fn builder_works_post_deneb() { .test_post_validator_register_validator() .await .test_builder_works_post_deneb() + .await + .test_lighthouse_rejects_invalid_withdrawals_root_v3() .await; } diff --git a/beacon_node/http_metrics/src/metrics.rs b/beacon_node/http_metrics/src/metrics.rs index 785206b757..e6e06caa84 100644 --- a/beacon_node/http_metrics/src/metrics.rs +++ b/beacon_node/http_metrics/src/metrics.rs @@ -4,8 +4,6 @@ use lighthouse_metrics::TextEncoder; use lighthouse_network::prometheus_client::encoding::text::encode; use malloc_utils::scrape_allocator_metrics; -pub use lighthouse_metrics::*; - pub fn gather_prometheus_metrics( ctx: &Context, ) -> std::result::Result { diff --git a/beacon_node/lighthouse_network/src/discovery/enr.rs b/beacon_node/lighthouse_network/src/discovery/enr.rs index 8eacabb4d0..0ec7e2ab7a 100644 --- a/beacon_node/lighthouse_network/src/discovery/enr.rs +++ b/beacon_node/lighthouse_network/src/discovery/enr.rs @@ -1,6 +1,6 @@ //! Helper functions and an extension trait for Ethereum 2 ENRs. -pub use discv5::enr::{self, CombinedKey, EnrBuilder}; +pub use discv5::enr::{CombinedKey, EnrBuilder}; use super::enr_ext::CombinedKeyExt; use super::ENR_FILENAME; diff --git a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs index 7157a62721..a6bf3ffecc 100644 --- a/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs +++ b/beacon_node/lighthouse_network/src/peer_manager/peerdb.rs @@ -1489,7 +1489,7 @@ mod tests { assert!(the_best.is_some()); // Consistency check let best_peers = pdb.best_peers_by_status(PeerInfo::is_connected); - assert_eq!(the_best.unwrap(), best_peers.get(0).unwrap().0); + assert_eq!(the_best.unwrap(), best_peers.first().unwrap().0); } #[test] diff --git a/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs b/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs index f1b76674ae..787c3dcb7a 100644 --- a/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs +++ b/beacon_node/lighthouse_network/src/rpc/codec/ssz_snappy.rs @@ -15,11 +15,10 @@ use std::io::{Read, Write}; use std::marker::PhantomData; use std::sync::Arc; use tokio_util::codec::{Decoder, Encoder}; -use types::{light_client_bootstrap::LightClientBootstrap, BlobSidecar}; use types::{ - EthSpec, ForkContext, ForkName, Hash256, SignedBeaconBlock, SignedBeaconBlockAltair, - SignedBeaconBlockBase, SignedBeaconBlockCapella, SignedBeaconBlockDeneb, - SignedBeaconBlockMerge, + BlobSidecar, EthSpec, ForkContext, ForkName, Hash256, LightClientBootstrap, SignedBeaconBlock, + SignedBeaconBlockAltair, SignedBeaconBlockBase, SignedBeaconBlockCapella, + SignedBeaconBlockDeneb, SignedBeaconBlockMerge, }; use unsigned_varint::codec::Uvi; @@ -378,7 +377,7 @@ fn handle_error( Ok(None) } } - _ => Err(err).map_err(RPCError::from), + _ => Err(RPCError::from(err)), } } diff --git a/beacon_node/lighthouse_network/src/rpc/methods.rs b/beacon_node/lighthouse_network/src/rpc/methods.rs index 438c2c7544..627c871c47 100644 --- a/beacon_node/lighthouse_network/src/rpc/methods.rs +++ b/beacon_node/lighthouse_network/src/rpc/methods.rs @@ -17,8 +17,8 @@ use superstruct::superstruct; use types::blob_sidecar::BlobIdentifier; use types::consts::deneb::MAX_BLOBS_PER_BLOCK; use types::{ - blob_sidecar::BlobSidecar, light_client_bootstrap::LightClientBootstrap, Epoch, EthSpec, - Hash256, SignedBeaconBlock, Slot, + blob_sidecar::BlobSidecar, Epoch, EthSpec, Hash256, LightClientBootstrap, SignedBeaconBlock, + Slot, }; /// Maximum number of blocks in a single request. @@ -571,7 +571,11 @@ impl std::fmt::Display for RPCResponse { RPCResponse::Pong(ping) => write!(f, "Pong: {}", ping.data), RPCResponse::MetaData(metadata) => write!(f, "Metadata: {}", metadata.seq_number()), RPCResponse::LightClientBootstrap(bootstrap) => { - write!(f, "LightClientBootstrap Slot: {}", bootstrap.header.slot) + write!( + f, + "LightClientBootstrap Slot: {}", + bootstrap.header.beacon.slot + ) } } } diff --git a/beacon_node/lighthouse_network/src/service/api_types.rs b/beacon_node/lighthouse_network/src/service/api_types.rs index 6ede0666a3..96c9d28332 100644 --- a/beacon_node/lighthouse_network/src/service/api_types.rs +++ b/beacon_node/lighthouse_network/src/service/api_types.rs @@ -1,8 +1,7 @@ use std::sync::Arc; use libp2p::swarm::ConnectionId; -use types::light_client_bootstrap::LightClientBootstrap; -use types::{BlobSidecar, EthSpec, SignedBeaconBlock}; +use types::{BlobSidecar, EthSpec, LightClientBootstrap, SignedBeaconBlock}; use crate::rpc::methods::{BlobsByRangeRequest, BlobsByRootRequest}; use crate::rpc::{ diff --git a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs index 0e6c76e222..430e0571b7 100644 --- a/beacon_node/network/src/network_beacon_processor/rpc_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/rpc_methods.rs @@ -18,9 +18,7 @@ use std::sync::Arc; use task_executor::TaskExecutor; use tokio_stream::StreamExt; use types::blob_sidecar::BlobIdentifier; -use types::{ - light_client_bootstrap::LightClientBootstrap, Epoch, EthSpec, ForkName, Hash256, Slot, -}; +use types::{Epoch, EthSpec, ForkName, Hash256, Slot}; impl NetworkBeaconProcessor { /* Auxiliary functions */ @@ -224,7 +222,7 @@ impl NetworkBeaconProcessor { request_id: PeerRequestId, request: BlobsByRootRequest, ) { - let Some(requested_root) = request.blob_ids.get(0).map(|id| id.block_root) else { + let Some(requested_root) = request.blob_ids.first().map(|id| id.block_root) else { // No blob ids requested. return; }; @@ -304,66 +302,32 @@ impl NetworkBeaconProcessor { request: LightClientBootstrapRequest, ) { let block_root = request.root; - let state_root = match self.chain.get_blinded_block(&block_root) { - Ok(signed_block) => match signed_block { - Some(signed_block) => signed_block.state_root(), - None => { - self.send_error_response( - peer_id, - RPCResponseErrorCode::ResourceUnavailable, - "Bootstrap not available".into(), - request_id, - ); - return; - } - }, - Err(_) => { - self.send_error_response( - peer_id, - RPCResponseErrorCode::ResourceUnavailable, - "Bootstrap not available".into(), - request_id, - ); - return; - } - }; - let mut beacon_state = match self.chain.get_state(&state_root, None) { - Ok(beacon_state) => match beacon_state { - Some(state) => state, - None => { - self.send_error_response( - peer_id, - RPCResponseErrorCode::ResourceUnavailable, - "Bootstrap not available".into(), - request_id, - ); - return; - } - }, - Err(_) => { - self.send_error_response( - peer_id, - RPCResponseErrorCode::ResourceUnavailable, - "Bootstrap not available".into(), - request_id, - ); - return; - } - }; - let Ok(bootstrap) = LightClientBootstrap::from_beacon_state(&mut beacon_state) else { - self.send_error_response( + match self.chain.get_light_client_bootstrap(&block_root) { + Ok(Some((bootstrap, _))) => self.send_response( + peer_id, + Response::LightClientBootstrap(bootstrap), + request_id, + ), + Ok(None) => self.send_error_response( peer_id, RPCResponseErrorCode::ResourceUnavailable, "Bootstrap not available".into(), request_id, - ); - return; + ), + Err(e) => { + self.send_error_response( + peer_id, + RPCResponseErrorCode::ResourceUnavailable, + "Bootstrap not available".into(), + request_id, + ); + error!(self.log, "Error getting LightClientBootstrap instance"; + "block_root" => ?block_root, + "peer" => %peer_id, + "error" => ?e + ) + } }; - self.send_response( - peer_id, - Response::LightClientBootstrap(bootstrap), - request_id, - ) } /// Handle a `BlocksByRange` request from the peer. diff --git a/beacon_node/network/src/network_beacon_processor/sync_methods.rs b/beacon_node/network/src/network_beacon_processor/sync_methods.rs index 7d6bde6344..d76ce5aadd 100644 --- a/beacon_node/network/src/network_beacon_processor/sync_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/sync_methods.rs @@ -340,7 +340,7 @@ impl NetworkBeaconProcessor { ); } Ok(AvailabilityProcessingStatus::MissingComponents(_, _)) => { - warn!( + debug!( self.log, "Missing components over rpc"; "block_hash" => %block_root, @@ -560,14 +560,10 @@ impl NetworkBeaconProcessor { downloaded_blocks: Vec>, ) -> (usize, Result<(), ChainSegmentFailed>) { let total_blocks = downloaded_blocks.len(); - let available_blocks = match downloaded_blocks - .into_iter() - .map(|block| { - self.chain - .data_availability_checker - .check_rpc_block_availability(block) - }) - .collect::, _>>() + let available_blocks = match self + .chain + .data_availability_checker + .verify_kzg_for_rpc_blocks(downloaded_blocks) { Ok(blocks) => blocks .into_iter() diff --git a/beacon_node/network/src/subnet_service/tests/mod.rs b/beacon_node/network/src/subnet_service/tests/mod.rs index 3b8c89a442..769775a62c 100644 --- a/beacon_node/network/src/subnet_service/tests/mod.rs +++ b/beacon_node/network/src/subnet_service/tests/mod.rs @@ -2,7 +2,6 @@ use super::*; use beacon_chain::{ builder::{BeaconChainBuilder, Witness}, eth1_chain::CachingEth1Backend, - validator_monitor::DEFAULT_INDIVIDUAL_TRACKING_THRESHOLD, BeaconChain, }; use futures::prelude::*; @@ -76,7 +75,6 @@ impl TestBeaconChain { Duration::from_millis(SLOT_DURATION_MILLIS), )) .shutdown_sender(shutdown_tx) - .monitor_validators(true, vec![], DEFAULT_INDIVIDUAL_TRACKING_THRESHOLD, log) .build() .expect("should build"), ); diff --git a/beacon_node/network/src/sync/backfill_sync/mod.rs b/beacon_node/network/src/sync/backfill_sync/mod.rs index e580846976..0d7e7c16c3 100644 --- a/beacon_node/network/src/sync/backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/backfill_sync/mod.rs @@ -929,7 +929,7 @@ impl BackFillSync { .collect::>(); // Sort peers prioritizing unrelated peers with less active requests. priorized_peers.sort_unstable(); - priorized_peers.get(0).map(|&(_, _, peer)| peer) + priorized_peers.first().map(|&(_, _, peer)| peer) }; if let Some(peer) = new_peer { diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index d13bb8cb88..c5732069a0 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -786,7 +786,7 @@ impl BlockLookups { self.log, "Block component processed for lookup"; "response_type" => ?R::response_type(), - "result" => ?result, + "block_root" => ?root, ); match result { diff --git a/beacon_node/network/src/sync/block_lookups/tests.rs b/beacon_node/network/src/sync/block_lookups/tests.rs index bd1e72ee18..551f1fdae6 100644 --- a/beacon_node/network/src/sync/block_lookups/tests.rs +++ b/beacon_node/network/src/sync/block_lookups/tests.rs @@ -1635,7 +1635,7 @@ mod deneb_only { self } fn invalidate_blobs_too_many(mut self) -> Self { - let first_blob = self.blobs.get(0).expect("blob").clone(); + let first_blob = self.blobs.first().expect("blob").clone(); self.blobs.push(first_blob); self } diff --git a/beacon_node/network/src/sync/range_sync/chain.rs b/beacon_node/network/src/sync/range_sync/chain.rs index 1e5bf09b3a..5a77340e3b 100644 --- a/beacon_node/network/src/sync/range_sync/chain.rs +++ b/beacon_node/network/src/sync/range_sync/chain.rs @@ -885,7 +885,7 @@ impl SyncingChain { .collect::>(); // Sort peers prioritizing unrelated peers with less active requests. priorized_peers.sort_unstable(); - priorized_peers.get(0).map(|&(_, _, peer)| peer) + priorized_peers.first().map(|&(_, _, peer)| peer) }; if let Some(peer) = new_peer { diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 7b9cd757a5..d76f2f375f 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -388,12 +388,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { address of this server (e.g., http://localhost:5052).") .takes_value(true), ) - .arg( - Arg::with_name("http-disable-legacy-spec") - .long("http-disable-legacy-spec") - .requires("enable_http") - .hidden(true) - ) .arg( Arg::with_name("http-spec-fork") .long("http-spec-fork") @@ -569,24 +563,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .help("If present, uses an eth1 backend that generates static dummy data.\ Identical to the method used at the 2019 Canada interop.") ) - .arg( - Arg::with_name("eth1-endpoint") - .long("eth1-endpoint") - .value_name("HTTP-ENDPOINT") - .help("Deprecated. Use --eth1-endpoints.") - .takes_value(true) - ) - .arg( - Arg::with_name("eth1-endpoints") - .long("eth1-endpoints") - .value_name("HTTP-ENDPOINTS") - .conflicts_with("eth1-endpoint") - .help("One http endpoint for a web3 connection to an execution node. \ - Note: This flag is now only useful for testing, use `--execution-endpoint` \ - flag to connect to an execution node on mainnet and testnets. - Defaults to http://127.0.0.1:8545.") - .takes_value(true) - ) .arg( Arg::with_name("eth1-purge-cache") .long("eth1-purge-cache") @@ -649,14 +625,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { /* * Execution Layer Integration */ - .arg( - Arg::with_name("merge") - .long("merge") - .help("Deprecated. The feature activates automatically when --execution-endpoint \ - is supplied.") - .takes_value(false) - .hidden(true) - ) .arg( Arg::with_name("execution-endpoint") .long("execution-endpoint") @@ -1200,22 +1168,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .requires("builder") .takes_value(true) ) - .arg( - Arg::with_name("count-unrealized") - .long("count-unrealized") - .hidden(true) - .help("This flag is deprecated and has no effect.") - .takes_value(true) - .default_value("true") - ) - .arg( - Arg::with_name("count-unrealized-full") - .long("count-unrealized-full") - .hidden(true) - .help("This flag is deprecated and has no effect.") - .takes_value(true) - .default_value("false") - ) .arg( Arg::with_name("reset-payload-statuses") .long("reset-payload-statuses") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index 94b94677a5..9ceab47f33 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -120,13 +120,6 @@ pub fn get_config( client_config.http_api.allow_origin = Some(allow_origin.to_string()); } - if cli_args.is_present("http-disable-legacy-spec") { - warn!( - log, - "The flag --http-disable-legacy-spec is deprecated and will be removed" - ); - } - if let Some(fork_name) = clap_utils::parse_optional(cli_args, "http-spec-fork")? { client_config.http_api.spec_fork_name = Some(fork_name); } @@ -158,6 +151,9 @@ pub fn get_config( client_config.http_api.duplicate_block_status_code = parse_required(cli_args, "http-duplicate-block-status")?; + + client_config.http_api.enable_light_client_server = + cli_args.is_present("light-client-server"); } if let Some(cache_size) = clap_utils::parse_optional(cli_args, "shuffling-cache-size")? { @@ -240,25 +236,6 @@ pub fn get_config( client_config.sync_eth1_chain = true; } - // Defines the URL to reach the eth1 node. - if let Some(endpoint) = cli_args.value_of("eth1-endpoint") { - warn!( - log, - "The --eth1-endpoint flag is deprecated"; - "msg" => "please use --eth1-endpoints instead" - ); - client_config.sync_eth1_chain = true; - - let endpoint = SensitiveUrl::parse(endpoint) - .map_err(|e| format!("eth1-endpoint was an invalid URL: {:?}", e))?; - client_config.eth1.endpoint = Eth1Endpoint::NoAuth(endpoint); - } else if let Some(endpoint) = cli_args.value_of("eth1-endpoints") { - client_config.sync_eth1_chain = true; - let endpoint = SensitiveUrl::parse(endpoint) - .map_err(|e| format!("eth1-endpoints contains an invalid URL {:?}", e))?; - client_config.eth1.endpoint = Eth1Endpoint::NoAuth(endpoint); - } - if let Some(val) = cli_args.value_of("eth1-blocks-per-log-query") { client_config.eth1.blocks_per_log_query = val .parse() @@ -275,20 +252,6 @@ pub fn get_config( client_config.eth1.cache_follow_distance = Some(follow_distance); } - if cli_args.is_present("merge") { - if cli_args.is_present("execution-endpoint") { - warn!( - log, - "The --merge flag is deprecated"; - "info" => "the --execution-endpoint flag automatically enables this feature" - ) - } else { - return Err("The --merge flag is deprecated. \ - Supply a value to --execution-endpoint instead." - .into()); - } - } - if let Some(endpoints) = cli_args.value_of("execution-endpoint") { let mut el_config = execution_layer::Config::default(); @@ -364,16 +327,6 @@ pub fn get_config( clap_utils::parse_required(cli_args, "execution-timeout-multiplier")?; el_config.execution_timeout_multiplier = Some(execution_timeout_multiplier); - // If `--execution-endpoint` is provided, we should ignore any `--eth1-endpoints` values and - // use `--execution-endpoint` instead. Also, log a deprecation warning. - if cli_args.is_present("eth1-endpoints") || cli_args.is_present("eth1-endpoint") { - warn!( - log, - "Ignoring --eth1-endpoints flag"; - "info" => "the value for --execution-endpoint will be used instead. \ - --eth1-endpoints has been deprecated for post-merge configurations" - ); - } client_config.eth1.endpoint = Eth1Endpoint::Auth { endpoint: execution_endpoint, jwt_path: secret_file, @@ -719,7 +672,7 @@ pub fn get_config( } if cli_args.is_present("validator-monitor-auto") { - client_config.validator_monitor_auto = true; + client_config.validator_monitor.auto_register = true; } if let Some(pubkeys) = cli_args.value_of("validator-monitor-pubkeys") { @@ -729,7 +682,8 @@ pub fn get_config( .collect::, _>>() .map_err(|e| format!("Invalid --validator-monitor-pubkeys value: {:?}", e))?; client_config - .validator_monitor_pubkeys + .validator_monitor + .validators .extend_from_slice(&pubkeys); } @@ -747,14 +701,17 @@ pub fn get_config( .collect::, _>>() .map_err(|e| format!("Invalid --validator-monitor-file contents: {:?}", e))?; client_config - .validator_monitor_pubkeys + .validator_monitor + .validators .extend_from_slice(&pubkeys); } if let Some(count) = clap_utils::parse_optional(cli_args, "validator-monitor-individual-tracking-threshold")? { - client_config.validator_monitor_individual_tracking_threshold = count; + client_config + .validator_monitor + .individual_tracking_threshold = count; } if cli_args.is_present("disable-lock-timeouts") { @@ -812,22 +769,6 @@ pub fn get_config( client_config.chain.fork_choice_before_proposal_timeout_ms = timeout; } - if !clap_utils::parse_required::(cli_args, "count-unrealized")? { - warn!( - log, - "The flag --count-unrealized is deprecated and will be removed"; - "info" => "any use of the flag will have no effect" - ); - } - - if clap_utils::parse_required::(cli_args, "count-unrealized-full")? { - warn!( - log, - "The flag --count-unrealized-full is deprecated and will be removed"; - "info" => "setting it to `true` has no effect" - ); - } - client_config.chain.always_reset_payload_statuses = cli_args.is_present("reset-payload-statuses"); @@ -850,7 +791,7 @@ pub fn get_config( // Graphical user interface config. if cli_args.is_present("gui") { client_config.http_api.enabled = true; - client_config.validator_monitor_auto = true; + client_config.validator_monitor.auto_register = true; } // Optimistic finalized sync. diff --git a/beacon_node/src/lib.rs b/beacon_node/src/lib.rs index 085a2a7824..cf6d627c30 100644 --- a/beacon_node/src/lib.rs +++ b/beacon_node/src/lib.rs @@ -89,7 +89,7 @@ impl ProductionBeaconNode { .disk_store( &db_path, &freezer_db_path, - blobs_db_path, + &blobs_db_path, store_config, log.clone(), )?; diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index 640647db35..43e14c3097 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -35,7 +35,7 @@ use state_processing::{ use std::cmp::min; use std::convert::TryInto; use std::marker::PhantomData; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::sync::Arc; use std::time::Duration; use types::blob_sidecar::BlobSidecarList; @@ -61,7 +61,7 @@ pub struct HotColdDB, Cold: ItemStore> { /// Cold database containing compact historical data. pub cold_db: Cold, /// Database containing blobs. If None, store falls back to use `cold_db`. - pub blobs_db: Option, + pub blobs_db: Cold, /// Hot database containing duplicated but quick-to-access recent data. /// /// The hot database also contains all blocks. @@ -138,7 +138,6 @@ pub enum HotColdDBError { MissingExecutionPayload(Hash256), MissingFullBlockExecutionPayloadPruned(Hash256, Slot), MissingAnchorInfo, - MissingPathToBlobsDatabase, BlobsPreviouslyInDefaultStore, HotStateSummaryError(BeaconStateError), RestorePointDecodeError(ssz::DecodeError), @@ -178,7 +177,7 @@ impl HotColdDB, MemoryStore> { anchor_info: RwLock::new(None), blob_info: RwLock::new(BlobInfo::default()), cold_db: MemoryStore::open(), - blobs_db: Some(MemoryStore::open()), + blobs_db: MemoryStore::open(), hot_db: MemoryStore::open(), block_cache: Mutex::new(BlockCache::new(config.block_cache_size)), state_cache: Mutex::new(LruCache::new(config.historic_state_cache_size)), @@ -202,7 +201,7 @@ impl HotColdDB, LevelDB> { pub fn open( hot_path: &Path, cold_path: &Path, - blobs_db_path: Option, + blobs_db_path: &Path, migrate_schema: impl FnOnce(Arc, SchemaVersion, SchemaVersion) -> Result<(), Error>, config: StoreConfig, spec: ChainSpec, @@ -215,7 +214,7 @@ impl HotColdDB, LevelDB> { anchor_info: RwLock::new(None), blob_info: RwLock::new(BlobInfo::default()), cold_db: LevelDB::open(cold_path)?, - blobs_db: None, + blobs_db: LevelDB::open(blobs_db_path)?, hot_db: LevelDB::open(hot_path)?, block_cache: Mutex::new(BlockCache::new(config.block_cache_size)), state_cache: Mutex::new(LruCache::new(config.historic_state_cache_size)), @@ -271,37 +270,29 @@ impl HotColdDB, LevelDB> { Some(blob_info) => { // If the oldest block slot is already set do not allow the blob DB path to be // changed (require manual migration). - if blob_info.oldest_blob_slot.is_some() { - if blobs_db_path.is_some() && !blob_info.blobs_db { - return Err(HotColdDBError::BlobsPreviouslyInDefaultStore.into()); - } else if blobs_db_path.is_none() && blob_info.blobs_db { - return Err(HotColdDBError::MissingPathToBlobsDatabase.into()); - } + if blob_info.oldest_blob_slot.is_some() && !blob_info.blobs_db { + return Err(HotColdDBError::BlobsPreviouslyInDefaultStore.into()); } // Set the oldest blob slot to the Deneb fork slot if it is not yet set. + // Always initialize `blobs_db` to true, we no longer support storing the blobs + // in the freezer DB, because the UX is strictly worse for relocating the DB. let oldest_blob_slot = blob_info.oldest_blob_slot.or(deneb_fork_slot); BlobInfo { oldest_blob_slot, - blobs_db: blobs_db_path.is_some(), + blobs_db: true, } } // First start. None => BlobInfo { // Set the oldest blob slot to the Deneb fork slot if it is not yet set. oldest_blob_slot: deneb_fork_slot, - blobs_db: blobs_db_path.is_some(), + blobs_db: true, }, }; - if new_blob_info.blobs_db { - if let Some(path) = &blobs_db_path { - db.blobs_db = Some(LevelDB::open(path.as_path())?); - } - } db.compare_and_set_blob_info_with_write(<_>::default(), new_blob_info.clone())?; info!( db.log, "Blob DB initialized"; - "separate_db" => new_blob_info.blobs_db, "path" => ?blobs_db_path, "oldest_blob_slot" => ?new_blob_info.oldest_blob_slot, ); @@ -575,8 +566,8 @@ impl, Cold: ItemStore> HotColdDB /// Check if the blobs for a block exists on disk. pub fn blobs_exist(&self, block_root: &Hash256) -> Result { - let blobs_db = self.blobs_db.as_ref().unwrap_or(&self.cold_db); - blobs_db.key_exists(DBColumn::BeaconBlob.into(), block_root.as_bytes()) + self.blobs_db + .key_exists(DBColumn::BeaconBlob.into(), block_root.as_bytes()) } /// Determine whether a block exists in the database. @@ -592,13 +583,12 @@ impl, Cold: ItemStore> HotColdDB .key_delete(DBColumn::BeaconBlock.into(), block_root.as_bytes())?; self.hot_db .key_delete(DBColumn::ExecPayload.into(), block_root.as_bytes())?; - let blobs_db = self.blobs_db.as_ref().unwrap_or(&self.cold_db); - blobs_db.key_delete(DBColumn::BeaconBlob.into(), block_root.as_bytes()) + self.blobs_db + .key_delete(DBColumn::BeaconBlob.into(), block_root.as_bytes()) } pub fn put_blobs(&self, block_root: &Hash256, blobs: BlobSidecarList) -> Result<(), Error> { - let blobs_db = self.blobs_db.as_ref().unwrap_or(&self.cold_db); - blobs_db.put_bytes( + self.blobs_db.put_bytes( DBColumn::BeaconBlob.into(), block_root.as_bytes(), &blobs.as_ssz_bytes(), @@ -988,9 +978,9 @@ impl, Cold: ItemStore> HotColdDB let mut guard = self.block_cache.lock(); let blob_cache_ops = blobs_ops.clone(); - let blobs_db = self.blobs_db.as_ref().unwrap_or(&self.cold_db); // Try to execute blobs store ops. - blobs_db.do_atomically(self.convert_to_kv_batch(blobs_ops)?)?; + self.blobs_db + .do_atomically(self.convert_to_kv_batch(blobs_ops)?)?; let hot_db_cache_ops = hot_db_ops.clone(); // Try to execute hot db store ops. @@ -1018,7 +1008,8 @@ impl, Cold: ItemStore> HotColdDB }; *op = reverse_op; } - blobs_db.do_atomically(self.convert_to_kv_batch(blob_cache_ops)?)?; + self.blobs_db + .do_atomically(self.convert_to_kv_batch(blob_cache_ops)?)?; return Err(e); } @@ -1436,15 +1427,16 @@ impl, Cold: ItemStore> HotColdDB /// Fetch blobs for a given block from the store. pub fn get_blobs(&self, block_root: &Hash256) -> Result>, Error> { - let blobs_db = self.blobs_db.as_ref().unwrap_or(&self.cold_db); - // Check the cache. if let Some(blobs) = self.block_cache.lock().get_blobs(block_root) { metrics::inc_counter(&metrics::BEACON_BLOBS_CACHE_HIT_COUNT); return Ok(Some(blobs.clone())); } - match blobs_db.get_bytes(DBColumn::BeaconBlob.into(), block_root.as_bytes())? { + match self + .blobs_db + .get_bytes(DBColumn::BeaconBlob.into(), block_root.as_bytes())? + { Some(ref blobs_bytes) => { let blobs = BlobSidecarList::from_ssz_bytes(blobs_bytes)?; self.block_cache @@ -1640,7 +1632,7 @@ impl, Cold: ItemStore> HotColdDB }); let blob_info = BlobInfo { oldest_blob_slot, - blobs_db: self.blobs_db.is_some(), + blobs_db: true, }; self.compare_and_set_blob_info(self.get_blob_info(), blob_info) } @@ -2222,6 +2214,8 @@ impl, Cold: ItemStore> HotColdDB Ok(()) } + /// This function fills in missing block roots between last restore point slot and split + /// slot, if any. pub fn heal_freezer_block_roots(&self) -> Result<(), Error> { let split = self.get_split_info(); let last_restore_point_slot = (split.slot - 1) / self.config.slots_per_restore_point @@ -2250,6 +2244,93 @@ impl, Cold: ItemStore> HotColdDB Ok(()) } + + /// Delete *all* states from the freezer database and update the anchor accordingly. + /// + /// WARNING: this method deletes the genesis state and replaces it with the provided + /// `genesis_state`. This is to support its use in schema migrations where the storage scheme of + /// the genesis state may be modified. It is the responsibility of the caller to ensure that the + /// genesis state is correct, else a corrupt database will be created. + pub fn prune_historic_states( + &self, + genesis_state_root: Hash256, + genesis_state: &BeaconState, + ) -> Result<(), Error> { + // Make sure there is no missing block roots before pruning + self.heal_freezer_block_roots()?; + + // Update the anchor to use the dummy state upper limit and disable historic state storage. + let old_anchor = self.get_anchor_info(); + let new_anchor = if let Some(old_anchor) = old_anchor.clone() { + AnchorInfo { + state_upper_limit: STATE_UPPER_LIMIT_NO_RETAIN, + state_lower_limit: Slot::new(0), + ..old_anchor.clone() + } + } else { + AnchorInfo { + anchor_slot: Slot::new(0), + oldest_block_slot: Slot::new(0), + oldest_block_parent: Hash256::zero(), + state_upper_limit: STATE_UPPER_LIMIT_NO_RETAIN, + state_lower_limit: Slot::new(0), + } + }; + + // Commit the anchor change immediately: if the cold database ops fail they can always be + // retried, and we can't do them atomically with this change anyway. + self.compare_and_set_anchor_info_with_write(old_anchor, Some(new_anchor))?; + + // Stage freezer data for deletion. Do not bother loading and deserializing values as this + // wastes time and is less schema-agnostic. My hope is that this method will be useful for + // migrating to the tree-states schema (delete everything in the freezer then start afresh). + let mut cold_ops = vec![]; + + let columns = [ + DBColumn::BeaconState, + DBColumn::BeaconStateSummary, + DBColumn::BeaconRestorePoint, + DBColumn::BeaconStateRoots, + DBColumn::BeaconHistoricalRoots, + DBColumn::BeaconRandaoMixes, + DBColumn::BeaconHistoricalSummaries, + ]; + + for column in columns { + for res in self.cold_db.iter_column_keys::>(column) { + let key = res?; + cold_ops.push(KeyValueStoreOp::DeleteKey(get_key_for_col( + column.as_str(), + &key, + ))); + } + } + + // XXX: We need to commit the mass deletion here *before* re-storing the genesis state, as + // the current schema performs reads as part of `store_cold_state`. This can be deleted + // once the target schema is tree-states. If the process is killed before the genesis state + // is written this can be fixed by re-running. + info!( + self.log, + "Deleting historic states"; + "num_kv" => cold_ops.len(), + ); + self.cold_db.do_atomically(std::mem::take(&mut cold_ops))?; + + // If we just deleted the the genesis state, re-store it using the *current* schema, which + // may be different from the schema of the genesis state we just deleted. + if self.get_split_slot() > 0 { + info!( + self.log, + "Re-storing genesis state"; + "state_root" => ?genesis_state_root, + ); + self.store_cold_state(&genesis_state_root, genesis_state, &mut cold_ops)?; + self.cold_db.do_atomically(cold_ops)?; + } + + Ok(()) + } } /// Advance the split point of the store, moving new finalized states to the freezer. diff --git a/beacon_node/store/src/leveldb_store.rs b/beacon_node/store/src/leveldb_store.rs index 0e4cd0e3b1..62619dd2c7 100644 --- a/beacon_node/store/src/leveldb_store.rs +++ b/beacon_node/store/src/leveldb_store.rs @@ -169,6 +169,7 @@ impl KeyValueStore for LevelDB { for (start_key, end_key) in [ endpoints(DBColumn::BeaconStateTemporary), endpoints(DBColumn::BeaconState), + endpoints(DBColumn::BeaconStateSummary), ] { self.db.compact(&start_key, &end_key); } @@ -225,9 +226,9 @@ impl KeyValueStore for LevelDB { } /// Iterate through all keys and values in a particular column. - fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter { + fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter { let start_key = - BytesKey::from_vec(get_key_for_col(column.into(), Hash256::zero().as_bytes())); + BytesKey::from_vec(get_key_for_col(column.into(), &vec![0; column.key_size()])); let iter = self.db.keys_iter(self.read_options()); iter.seek(&start_key); @@ -235,13 +236,12 @@ impl KeyValueStore for LevelDB { Box::new( iter.take_while(move |key| key.matches_column(column)) .map(move |bytes_key| { - let key = - bytes_key - .remove_column(column) - .ok_or(HotColdDBError::IterationError { - unexpected_key: bytes_key, - })?; - Ok(key) + let key = bytes_key.remove_column_variable(column).ok_or_else(|| { + HotColdDBError::IterationError { + unexpected_key: bytes_key.clone(), + } + })?; + K::from_bytes(key) }), ) } diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index 0f54b4664a..eacd28d2db 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -45,7 +45,7 @@ use strum::{EnumString, IntoStaticStr}; pub use types::*; pub type ColumnIter<'a, K> = Box), Error>> + 'a>; -pub type ColumnKeyIter<'a> = Box> + 'a>; +pub type ColumnKeyIter<'a, K> = Box> + 'a>; pub type RawEntryIter<'a> = Box, Vec), Error>> + 'a>; pub type RawKeyIter<'a> = Box, Error>> + 'a>; @@ -88,6 +88,7 @@ pub trait KeyValueStore: Sync + Send + Sized + 'static { self.iter_column_from(column, &vec![0; column.key_size()]) } + /// Iterate through all keys and values in a column from a given starting point. fn iter_column_from(&self, column: DBColumn, from: &[u8]) -> ColumnIter; fn iter_raw_entries(&self, _column: DBColumn, _prefix: &[u8]) -> RawEntryIter { @@ -99,7 +100,7 @@ pub trait KeyValueStore: Sync + Send + Sized + 'static { } /// Iterate through all keys in a particular column. - fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter; + fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter; } pub trait Key: Sized + 'static { @@ -274,7 +275,7 @@ impl DBColumn { /// This function returns the number of bytes used by keys in a given column. pub fn key_size(self) -> usize { match self { - Self::OverflowLRUCache => 40, + Self::OverflowLRUCache => 33, // See `OverflowKey` encode impl. Self::BeaconMeta | Self::BeaconBlock | Self::BeaconState diff --git a/beacon_node/store/src/memory_store.rs b/beacon_node/store/src/memory_store.rs index 7a276a1fad..c2e494dce6 100644 --- a/beacon_node/store/src/memory_store.rs +++ b/beacon_node/store/src/memory_store.rs @@ -100,7 +100,7 @@ impl KeyValueStore for MemoryStore { })) } - fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter { + fn iter_column_keys(&self, column: DBColumn) -> ColumnKeyIter { Box::new(self.iter_column(column).map(|res| res.map(|(k, _)| k))) } diff --git a/beacon_node/store/src/metadata.rs b/beacon_node/store/src/metadata.rs index 124c9a2f58..6fef74d7ff 100644 --- a/beacon_node/store/src/metadata.rs +++ b/beacon_node/store/src/metadata.rs @@ -135,7 +135,7 @@ pub struct BlobInfo { /// If the `oldest_blob_slot` is `None` then this means that the Deneb fork epoch is not yet /// known. pub oldest_blob_slot: Option, - /// A separate blobs database is in use. + /// A separate blobs database is in use (deprecated, always `true`). pub blobs_db: bool, } diff --git a/book/src/database-migrations.md b/book/src/database-migrations.md index 9b60ca2e18..ed7f7d72f5 100644 --- a/book/src/database-migrations.md +++ b/book/src/database-migrations.md @@ -158,3 +158,38 @@ lighthouse db version --network mainnet ``` [run-correctly]: #how-to-run-lighthouse-db-correctly + +## How to prune historic states + +Pruning historic states helps in managing the disk space used by the Lighthouse beacon node by removing old beacon +states from the freezer database. This can be especially useful when the database has accumulated a significant amount +of historic data. This command is intended for nodes synced before 4.4.1, as newly synced node no longer store +historic states by default. + +Here are the steps to prune historic states: + +1. Before running the prune command, make sure that the Lighthouse beacon node is not running. If you are using systemd, you might stop the Lighthouse beacon node with a command like: + + ```bash + sudo systemctl stop lighthousebeacon + ``` + +2. Use the `prune-states` command to prune the historic states. You can do a test run without the `--confirm` flag to check that the database can be pruned: + + ```bash + sudo -u "$LH_USER" lighthouse db prune-states --datadir "$LH_DATADIR" --network "$NET" + ``` + +3. If you are ready to prune the states irreversibly, add the `--confirm` flag to commit the changes: + + ```bash + sudo -u "$LH_USER" lighthouse db prune-states --confirm --datadir "$LH_DATADIR" --network "$NET" + ``` + + The `--confirm` flag ensures that you are aware the action is irreversible, and historic states will be permanently removed. + +4. After successfully pruning the historic states, you can restart the Lighthouse beacon node: + + ```bash + sudo systemctl start lighthousebeacon + ``` diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index ca244fd89c..0406c36071 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -39,8 +39,12 @@ use store::fork_versioned_response::ExecutionOptimisticFinalizedForkVersionedRes pub const V1: EndpointVersion = EndpointVersion(1); pub const V2: EndpointVersion = EndpointVersion(2); +pub const V3: EndpointVersion = EndpointVersion(3); pub const CONSENSUS_VERSION_HEADER: &str = "Eth-Consensus-Version"; +pub const EXECUTION_PAYLOAD_BLINDED_HEADER: &str = "Eth-Execution-Payload-Blinded"; +pub const EXECUTION_PAYLOAD_VALUE_HEADER: &str = "Eth-Execution-Payload-Value"; +pub const CONSENSUS_BLOCK_VALUE_HEADER: &str = "Eth-Consensus-Block-Value"; #[derive(Debug)] pub enum Error { @@ -273,6 +277,31 @@ impl BeaconNodeHttpClient { } } + /// Perform a HTTP GET request using an 'accept' header, returning `None` on a 404 error. + pub async fn get_bytes_response_with_response_headers( + &self, + url: U, + accept_header: Accept, + timeout: Duration, + ) -> Result<(Option>, Option), Error> { + let opt_response = self + .get_response(url, |b| b.accept(accept_header).timeout(timeout)) + .await + .optional()?; + + // let headers = opt_response.headers(); + match opt_response { + Some(resp) => { + let response_headers = resp.headers().clone(); + Ok(( + Some(resp.bytes().await?.into_iter().collect::>()), + Some(response_headers), + )) + } + None => Ok((None, None)), + } + } + /// Perform a HTTP POST request. async fn post(&self, url: U, body: &T) -> Result<(), Error> { self.post_generic(url, body, None).await?; @@ -643,6 +672,59 @@ impl BeaconNodeHttpClient { self.get_opt(path).await } + /// `GET beacon/light_client/bootstrap` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn get_light_client_bootstrap( + &self, + block_root: Hash256, + ) -> Result>>, Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("light_client") + .push("bootstrap") + .push(&format!("{:?}", block_root)); + + self.get_opt(path).await + } + + /// `GET beacon/light_client/optimistic_update` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn get_beacon_light_client_optimistic_update( + &self, + ) -> Result>>, Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("light_client") + .push("optimistic_update"); + + self.get_opt(path).await + } + + /// `GET beacon/light_client/finality_update` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn get_beacon_light_client_finality_update( + &self, + ) -> Result>>, Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("light_client") + .push("finality_update"); + + self.get_opt(path).await + } + /// `GET beacon/headers?slot,parent_root` /// /// Returns `Ok(None)` on a 404 error. @@ -1633,6 +1715,26 @@ impl BeaconNodeHttpClient { .await } + /// `GET v2/validator/blocks/{slot}` + pub async fn get_validator_blocks_modular>( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + skip_randao_verification: SkipRandaoVerification, + ) -> Result>, Error> { + let path = self + .get_validator_blocks_path::( + slot, + randao_reveal, + graffiti, + skip_randao_verification, + ) + .await?; + + self.get(path).await + } + /// returns `GET v2/validator/blocks/{slot}` URL path pub async fn get_validator_blocks_path>( &self, @@ -1665,16 +1767,64 @@ impl BeaconNodeHttpClient { Ok(path) } - /// `GET v2/validator/blocks/{slot}` - pub async fn get_validator_blocks_modular>( + /// returns `GET v3/validator/blocks/{slot}` URL path + pub async fn get_validator_blocks_v3_path( &self, slot: Slot, randao_reveal: &SignatureBytes, graffiti: Option<&Graffiti>, skip_randao_verification: SkipRandaoVerification, - ) -> Result>, Error> { + ) -> Result { + let mut path = self.eth_path(V3)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("blocks") + .push(&slot.to_string()); + + path.query_pairs_mut() + .append_pair("randao_reveal", &randao_reveal.to_string()); + + if let Some(graffiti) = graffiti { + path.query_pairs_mut() + .append_pair("graffiti", &graffiti.to_string()); + } + + if skip_randao_verification == SkipRandaoVerification::Yes { + path.query_pairs_mut() + .append_pair("skip_randao_verification", ""); + } + + Ok(path) + } + + /// `GET v3/validator/blocks/{slot}` + pub async fn get_validator_blocks_v3( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + ) -> Result, Error> { + self.get_validator_blocks_v3_modular( + slot, + randao_reveal, + graffiti, + SkipRandaoVerification::No, + ) + .await + } + + /// `GET v3/validator/blocks/{slot}` + pub async fn get_validator_blocks_v3_modular( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + skip_randao_verification: SkipRandaoVerification, + ) -> Result, Error> { let path = self - .get_validator_blocks_path::( + .get_validator_blocks_v3_path::( slot, randao_reveal, graffiti, @@ -1682,7 +1832,77 @@ impl BeaconNodeHttpClient { ) .await?; - self.get(path).await + let response = self.get_response(path, |b| b).await?; + + let is_blinded_payload = response + .headers() + .get(EXECUTION_PAYLOAD_BLINDED_HEADER) + .map(|value| value.to_str().unwrap_or_default().to_lowercase() == "true") + .unwrap_or(false); + + if is_blinded_payload { + let blinded_payload = response + .json::>>>() + .await?; + Ok(ForkVersionedBeaconBlockType::Blinded(blinded_payload)) + } else { + let full_payload = response + .json::>>>() + .await?; + Ok(ForkVersionedBeaconBlockType::Full(full_payload)) + } + } + + /// `GET v3/validator/blocks/{slot}` in ssz format + pub async fn get_validator_blocks_v3_ssz( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + ) -> Result<(Option>, bool), Error> { + self.get_validator_blocks_v3_modular_ssz::( + slot, + randao_reveal, + graffiti, + SkipRandaoVerification::No, + ) + .await + } + + /// `GET v3/validator/blocks/{slot}` in ssz format + pub async fn get_validator_blocks_v3_modular_ssz( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + skip_randao_verification: SkipRandaoVerification, + ) -> Result<(Option>, bool), Error> { + let path = self + .get_validator_blocks_v3_path::( + slot, + randao_reveal, + graffiti, + skip_randao_verification, + ) + .await?; + + let (response_content, response_headers) = self + .get_bytes_response_with_response_headers( + path, + Accept::Ssz, + self.timeouts.get_validator_block_ssz, + ) + .await?; + + let is_blinded_payload = match response_headers { + Some(headers) => headers + .get(EXECUTION_PAYLOAD_BLINDED_HEADER) + .map(|value| value.to_str().unwrap_or_default().to_lowercase() == "true") + .unwrap_or(false), + None => false, + }; + + Ok((response_content, is_blinded_payload)) } /// `GET v2/validator/blocks/{slot}` in ssz format diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index e87f254f10..dea8b2bf56 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -1047,6 +1047,8 @@ pub enum EventKind { ChainReorg(SseChainReorg), ContributionAndProof(Box>), LateHead(SseLateHead), + LightClientFinalityUpdate(Box>), + LightClientOptimisticUpdate(Box>), #[cfg(feature = "lighthouse")] BlockReward(BlockReward), PayloadAttributes(VersionedSsePayloadAttributes), @@ -1065,6 +1067,8 @@ impl EventKind { EventKind::ContributionAndProof(_) => "contribution_and_proof", EventKind::PayloadAttributes(_) => "payload_attributes", EventKind::LateHead(_) => "late_head", + EventKind::LightClientFinalityUpdate(_) => "light_client_finality_update", + EventKind::LightClientOptimisticUpdate(_) => "light_client_optimistic_update", #[cfg(feature = "lighthouse")] EventKind::BlockReward(_) => "block_reward", } @@ -1127,6 +1131,22 @@ impl EventKind { ServerError::InvalidServerSentEvent(format!("Payload Attributes: {:?}", e)) })?, )), + "light_client_finality_update" => Ok(EventKind::LightClientFinalityUpdate( + serde_json::from_str(data).map_err(|e| { + ServerError::InvalidServerSentEvent(format!( + "Light Client Finality Update: {:?}", + e + )) + })?, + )), + "light_client_optimistic_update" => Ok(EventKind::LightClientOptimisticUpdate( + serde_json::from_str(data).map_err(|e| { + ServerError::InvalidServerSentEvent(format!( + "Light Client Optimistic Update: {:?}", + e + )) + })?, + )), #[cfg(feature = "lighthouse")] "block_reward" => Ok(EventKind::BlockReward(serde_json::from_str(data).map_err( |e| ServerError::InvalidServerSentEvent(format!("Block Reward: {:?}", e)), @@ -1158,6 +1178,8 @@ pub enum EventTopic { ContributionAndProof, LateHead, PayloadAttributes, + LightClientFinalityUpdate, + LightClientOptimisticUpdate, #[cfg(feature = "lighthouse")] BlockReward, } @@ -1177,6 +1199,8 @@ impl FromStr for EventTopic { "contribution_and_proof" => Ok(EventTopic::ContributionAndProof), "payload_attributes" => Ok(EventTopic::PayloadAttributes), "late_head" => Ok(EventTopic::LateHead), + "light_client_finality_update" => Ok(EventTopic::LightClientFinalityUpdate), + "light_client_optimistic_update" => Ok(EventTopic::LightClientOptimisticUpdate), #[cfg(feature = "lighthouse")] "block_reward" => Ok(EventTopic::BlockReward), _ => Err("event topic cannot be parsed.".to_string()), @@ -1197,6 +1221,8 @@ impl fmt::Display for EventTopic { EventTopic::ContributionAndProof => write!(f, "contribution_and_proof"), EventTopic::PayloadAttributes => write!(f, "payload_attributes"), EventTopic::LateHead => write!(f, "late_head"), + EventTopic::LightClientFinalityUpdate => write!(f, "light_client_finality_update"), + EventTopic::LightClientOptimisticUpdate => write!(f, "light_client_optimistic_update"), #[cfg(feature = "lighthouse")] EventTopic::BlockReward => write!(f, "block_reward"), } @@ -1384,6 +1410,11 @@ pub mod serde_status_code { } } +pub enum ForkVersionedBeaconBlockType { + Full(ForkVersionedResponse>>), + Blinded(ForkVersionedResponse>>), +} + #[cfg(test)] mod tests { use super::*; diff --git a/common/eth2_network_config/built_in_network_configs/holesky/config.yaml b/common/eth2_network_config/built_in_network_configs/holesky/config.yaml index 9f4faeb27a..d699b9a39f 100644 --- a/common/eth2_network_config/built_in_network_configs/holesky/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/holesky/config.yaml @@ -111,5 +111,3 @@ MAX_REQUEST_BLOB_SIDECARS: 768 MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS: 4096 # `6` BLOB_SIDECAR_SUBNET_COUNT: 6 -# `uint64(6)` -MAX_BLOBS_PER_BLOCK: 6 diff --git a/common/slot_clock/src/system_time_slot_clock.rs b/common/slot_clock/src/system_time_slot_clock.rs index c54646fbc6..770132064e 100644 --- a/common/slot_clock/src/system_time_slot_clock.rs +++ b/common/slot_clock/src/system_time_slot_clock.rs @@ -2,8 +2,6 @@ use super::{ManualSlotClock, SlotClock}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use types::Slot; -pub use std::time::SystemTimeError; - /// Determines the present slot based upon the present system time. #[derive(Clone)] pub struct SystemTimeSlotClock { diff --git a/consensus/fork_choice/tests/tests.rs b/consensus/fork_choice/tests/tests.rs index 9d39eb3e38..c21b8ed567 100644 --- a/consensus/fork_choice/tests/tests.rs +++ b/consensus/fork_choice/tests/tests.rs @@ -92,8 +92,7 @@ impl ForkChoiceTest { T: Fn(&BeaconForkChoiceStore, MemoryStore>) -> U, { func( - &self - .harness + self.harness .chain .canonical_head .fork_choice_read_lock() @@ -386,8 +385,7 @@ impl ForkChoiceTest { &self.harness.chain.spec, self.harness.logger(), ) - .err() - .expect("on_block did not return an error"); + .expect_err("on_block did not return an error"); comparison_func(err); self } @@ -841,7 +839,7 @@ async fn valid_attestation() { .apply_attestation_to_chain( MutationDelay::NoDelay, |_, _| {}, - |result| assert_eq!(result.unwrap(), ()), + |result| assert!(result.is_ok()), ) .await; } @@ -1074,7 +1072,7 @@ async fn invalid_attestation_delayed_slot() { .apply_attestation_to_chain( MutationDelay::NoDelay, |_, _| {}, - |result| assert_eq!(result.unwrap(), ()), + |result| assert!(result.is_ok()), ) .await .inspect_queued_attestations(|queue| assert_eq!(queue.len(), 1)) @@ -1183,7 +1181,7 @@ async fn weak_subjectivity_check_fails_early_epoch() { let mut checkpoint = setup_harness.harness.finalized_checkpoint(); - checkpoint.epoch = checkpoint.epoch - 1; + checkpoint.epoch -= 1; let chain_config = ChainConfig { weak_subjectivity_checkpoint: Some(checkpoint), @@ -1210,7 +1208,7 @@ async fn weak_subjectivity_check_fails_late_epoch() { let mut checkpoint = setup_harness.harness.finalized_checkpoint(); - checkpoint.epoch = checkpoint.epoch + 1; + checkpoint.epoch += 1; let chain_config = ChainConfig { weak_subjectivity_checkpoint: Some(checkpoint), diff --git a/consensus/merkle_proof/src/lib.rs b/consensus/merkle_proof/src/lib.rs index dc3de71cef..595de86e86 100644 --- a/consensus/merkle_proof/src/lib.rs +++ b/consensus/merkle_proof/src/lib.rs @@ -250,7 +250,7 @@ impl MerkleTree { if deposit_count == (0x1 << level) { return Ok(MerkleTree::Finalized( *finalized_branch - .get(0) + .first() .ok_or(MerkleTreeError::PleaseNotifyTheDevs)?, )); } diff --git a/consensus/state_processing/src/upgrade/altair.rs b/consensus/state_processing/src/upgrade/altair.rs index 26b1192bc1..5bb4f0bd59 100644 --- a/consensus/state_processing/src/upgrade/altair.rs +++ b/consensus/state_processing/src/upgrade/altair.rs @@ -54,7 +54,7 @@ pub fn upgrade_to_altair( VariableList::new(vec![ParticipationFlags::default(); pre.validators.len()])?; let inactivity_scores = VariableList::new(vec![0; pre.validators.len()])?; - let temp_sync_committee = Arc::new(SyncCommittee::temporary()?); + let temp_sync_committee = Arc::new(SyncCommittee::temporary()); // Where possible, use something like `mem::take` to move fields from behind the &mut // reference. For other fields that don't have a good default value, use `clone`. diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 18b9866f35..e2e25f24b8 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -626,6 +626,25 @@ impl BeaconState { cache.get_all_beacon_committees() } + /// Returns the block root which decided the proposer shuffling for the epoch passed in parameter. This root + /// can be used to key this proposer shuffling. + /// + /// ## Notes + /// + /// The `block_root` must be equal to the latest block applied to `self`. + pub fn proposer_shuffling_decision_root_at_epoch( + &self, + epoch: Epoch, + block_root: Hash256, + ) -> Result { + let decision_slot = self.proposer_shuffling_decision_slot(epoch); + if self.slot() <= decision_slot { + Ok(block_root) + } else { + self.get_block_root(decision_slot).map(|root| *root) + } + } + /// Returns the block root which decided the proposer shuffling for the current epoch. This root /// can be used to key this proposer shuffling. /// @@ -634,7 +653,7 @@ impl BeaconState { /// The `block_root` covers the one-off scenario where the genesis block decides its own /// shuffling. It should be set to the latest block applied to `self` or the genesis block root. pub fn proposer_shuffling_decision_root(&self, block_root: Hash256) -> Result { - let decision_slot = self.proposer_shuffling_decision_slot(); + let decision_slot = self.proposer_shuffling_decision_slot(self.current_epoch()); if self.slot() == decision_slot { Ok(block_root) } else { @@ -643,11 +662,9 @@ impl BeaconState { } /// Returns the slot at which the proposer shuffling was decided. The block root at this slot - /// can be used to key the proposer shuffling for the current epoch. - fn proposer_shuffling_decision_slot(&self) -> Slot { - self.current_epoch() - .start_slot(T::slots_per_epoch()) - .saturating_sub(1_u64) + /// can be used to key the proposer shuffling for the given epoch. + fn proposer_shuffling_decision_slot(&self, epoch: Epoch) -> Slot { + epoch.start_slot(T::slots_per_epoch()).saturating_sub(1_u64) } /// Returns the block root which decided the attester shuffling for the given `relative_epoch`. diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index 26541a188c..0f284bde9d 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -99,6 +99,7 @@ pub mod slot_data; pub mod sqlite; pub mod blob_sidecar; +pub mod light_client_header; pub mod sidecar; pub mod signed_blob; @@ -154,8 +155,11 @@ pub use crate::fork_versioned_response::{ForkVersionDeserialize, ForkVersionedRe pub use crate::graffiti::{Graffiti, GRAFFITI_BYTES_LEN}; pub use crate::historical_batch::HistoricalBatch; pub use crate::indexed_attestation::IndexedAttestation; +pub use crate::light_client_bootstrap::LightClientBootstrap; pub use crate::light_client_finality_update::LightClientFinalityUpdate; +pub use crate::light_client_header::LightClientHeader; pub use crate::light_client_optimistic_update::LightClientOptimisticUpdate; +pub use crate::light_client_update::{Error as LightClientError, LightClientUpdate}; pub use crate::participation_flags::ParticipationFlags; pub use crate::participation_list::ParticipationList; pub use crate::payload::{ diff --git a/consensus/types/src/light_client_bootstrap.rs b/consensus/types/src/light_client_bootstrap.rs index 1d70456d73..616aced483 100644 --- a/consensus/types/src/light_client_bootstrap.rs +++ b/consensus/types/src/light_client_bootstrap.rs @@ -1,10 +1,13 @@ -use super::{BeaconBlockHeader, BeaconState, EthSpec, FixedVector, Hash256, SyncCommittee}; -use crate::{light_client_update::*, test_utils::TestRandom}; -use serde::{Deserialize, Serialize}; +use super::{BeaconState, EthSpec, FixedVector, Hash256, SyncCommittee}; +use crate::{ + light_client_update::*, test_utils::TestRandom, ForkName, ForkVersionDeserialize, + LightClientHeader, +}; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::Value; use ssz_derive::{Decode, Encode}; use std::sync::Arc; use test_random_derive::TestRandom; -use tree_hash::TreeHash; /// A LightClientBootstrap is the initializer we send over to lightclient nodes /// that are trying to generate their basic storage when booting up. @@ -22,8 +25,8 @@ use tree_hash::TreeHash; #[serde(bound = "T: EthSpec")] #[arbitrary(bound = "T: EthSpec")] pub struct LightClientBootstrap { - /// Requested beacon block header. - pub header: BeaconBlockHeader, + /// The requested beacon block header. + pub header: LightClientHeader, /// The `SyncCommittee` used in the requested period. pub current_sync_committee: Arc>, /// Merkle proof for sync committee @@ -33,17 +36,37 @@ pub struct LightClientBootstrap { impl LightClientBootstrap { pub fn from_beacon_state(beacon_state: &mut BeaconState) -> Result { let mut header = beacon_state.latest_block_header().clone(); - header.state_root = beacon_state.tree_hash_root(); + header.state_root = beacon_state.update_tree_hash_cache()?; let current_sync_committee_branch = beacon_state.compute_merkle_proof(CURRENT_SYNC_COMMITTEE_INDEX)?; Ok(LightClientBootstrap { - header, + header: header.into(), current_sync_committee: beacon_state.current_sync_committee()?.clone(), current_sync_committee_branch: FixedVector::new(current_sync_committee_branch)?, }) } } +impl ForkVersionDeserialize for LightClientBootstrap { + fn deserialize_by_fork<'de, D: Deserializer<'de>>( + value: Value, + fork_name: ForkName, + ) -> Result { + match fork_name { + ForkName::Altair | ForkName::Merge => { + Ok(serde_json::from_value::>(value) + .map_err(serde::de::Error::custom))? + } + ForkName::Base | ForkName::Capella | ForkName::Deneb => { + Err(serde::de::Error::custom(format!( + "LightClientBootstrap failed to deserialize: unsupported fork '{}'", + fork_name + ))) + } + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/consensus/types/src/light_client_finality_update.rs b/consensus/types/src/light_client_finality_update.rs index 30f337fb2b..87601b8156 100644 --- a/consensus/types/src/light_client_finality_update.rs +++ b/consensus/types/src/light_client_finality_update.rs @@ -1,9 +1,12 @@ use super::{ - BeaconBlockHeader, EthSpec, FixedVector, Hash256, SignedBeaconBlock, SignedBlindedBeaconBlock, - Slot, SyncAggregate, + EthSpec, FixedVector, Hash256, SignedBeaconBlock, SignedBlindedBeaconBlock, Slot, SyncAggregate, }; -use crate::{light_client_update::*, test_utils::TestRandom, BeaconState, ChainSpec}; -use serde::{Deserialize, Serialize}; +use crate::{ + light_client_update::*, test_utils::TestRandom, BeaconState, ChainSpec, ForkName, + ForkVersionDeserialize, LightClientHeader, +}; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::Value; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash::TreeHash; @@ -25,9 +28,9 @@ use tree_hash::TreeHash; #[arbitrary(bound = "T: EthSpec")] pub struct LightClientFinalityUpdate { /// The last `BeaconBlockHeader` from the last attested block by the sync committee. - pub attested_header: BeaconBlockHeader, + pub attested_header: LightClientHeader, /// The last `BeaconBlockHeader` from the last attested finalized block (end of epoch). - pub finalized_header: BeaconBlockHeader, + pub finalized_header: LightClientHeader, /// Merkle proof attesting finalized header. pub finality_branch: FixedVector, /// current sync aggreggate @@ -68,8 +71,8 @@ impl LightClientFinalityUpdate { let finality_branch = attested_state.compute_merkle_proof(FINALIZED_ROOT_INDEX)?; Ok(Self { - attested_header, - finalized_header, + attested_header: attested_header.into(), + finalized_header: finalized_header.into(), finality_branch: FixedVector::new(finality_branch)?, sync_aggregate: sync_aggregate.clone(), signature_slot: block.slot(), @@ -77,6 +80,26 @@ impl LightClientFinalityUpdate { } } +impl ForkVersionDeserialize for LightClientFinalityUpdate { + fn deserialize_by_fork<'de, D: Deserializer<'de>>( + value: Value, + fork_name: ForkName, + ) -> Result { + match fork_name { + ForkName::Altair | ForkName::Merge => Ok(serde_json::from_value::< + LightClientFinalityUpdate, + >(value) + .map_err(serde::de::Error::custom))?, + ForkName::Base | ForkName::Capella | ForkName::Deneb => { + Err(serde::de::Error::custom(format!( + "LightClientFinalityUpdate failed to deserialize: unsupported fork '{}'", + fork_name + ))) + } + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/consensus/types/src/light_client_header.rs b/consensus/types/src/light_client_header.rs new file mode 100644 index 0000000000..8fe31f7af8 --- /dev/null +++ b/consensus/types/src/light_client_header.rs @@ -0,0 +1,26 @@ +use crate::test_utils::TestRandom; +use crate::BeaconBlockHeader; +use serde::{Deserialize, Serialize}; +use ssz_derive::{Decode, Encode}; +use test_random_derive::TestRandom; + +#[derive( + Debug, + Clone, + PartialEq, + Serialize, + Deserialize, + Encode, + Decode, + TestRandom, + arbitrary::Arbitrary, +)] +pub struct LightClientHeader { + pub beacon: BeaconBlockHeader, +} + +impl From for LightClientHeader { + fn from(beacon: BeaconBlockHeader) -> Self { + LightClientHeader { beacon } + } +} diff --git a/consensus/types/src/light_client_optimistic_update.rs b/consensus/types/src/light_client_optimistic_update.rs index fbb0558ece..d883d735f3 100644 --- a/consensus/types/src/light_client_optimistic_update.rs +++ b/consensus/types/src/light_client_optimistic_update.rs @@ -1,8 +1,10 @@ -use super::{BeaconBlockHeader, EthSpec, Slot, SyncAggregate}; +use super::{EthSpec, ForkName, ForkVersionDeserialize, Slot, SyncAggregate}; +use crate::light_client_header::LightClientHeader; use crate::{ light_client_update::Error, test_utils::TestRandom, BeaconState, ChainSpec, SignedBeaconBlock, }; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::Value; use ssz_derive::{Decode, Encode}; use test_random_derive::TestRandom; use tree_hash::TreeHash; @@ -24,7 +26,7 @@ use tree_hash::TreeHash; #[arbitrary(bound = "T: EthSpec")] pub struct LightClientOptimisticUpdate { /// The last `BeaconBlockHeader` from the last attested block by the sync committee. - pub attested_header: BeaconBlockHeader, + pub attested_header: LightClientHeader, /// current sync aggreggate pub sync_aggregate: SyncAggregate, /// Slot of the sync aggregated singature @@ -53,13 +55,33 @@ impl LightClientOptimisticUpdate { let mut attested_header = attested_state.latest_block_header().clone(); attested_header.state_root = attested_state.tree_hash_root(); Ok(Self { - attested_header, + attested_header: attested_header.into(), sync_aggregate: sync_aggregate.clone(), signature_slot: block.slot(), }) } } +impl ForkVersionDeserialize for LightClientOptimisticUpdate { + fn deserialize_by_fork<'de, D: Deserializer<'de>>( + value: Value, + fork_name: ForkName, + ) -> Result { + match fork_name { + ForkName::Altair | ForkName::Merge => Ok(serde_json::from_value::< + LightClientOptimisticUpdate, + >(value) + .map_err(serde::de::Error::custom))?, + ForkName::Base | ForkName::Capella | ForkName::Deneb => { + Err(serde::de::Error::custom(format!( + "LightClientOptimisticUpdate failed to deserialize: unsupported fork '{}'", + fork_name + ))) + } + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/consensus/types/src/light_client_update.rs b/consensus/types/src/light_client_update.rs index 6e53e14c99..718cd7553f 100644 --- a/consensus/types/src/light_client_update.rs +++ b/consensus/types/src/light_client_update.rs @@ -1,7 +1,11 @@ use super::{BeaconBlockHeader, EthSpec, FixedVector, Hash256, Slot, SyncAggregate, SyncCommittee}; -use crate::{beacon_state, test_utils::TestRandom, BeaconBlock, BeaconState, ChainSpec}; +use crate::{ + beacon_state, test_utils::TestRandom, BeaconBlock, BeaconState, ChainSpec, ForkName, + ForkVersionDeserialize, LightClientHeader, +}; use safe_arith::ArithError; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::Value; use ssz_derive::{Decode, Encode}; use ssz_types::typenum::{U5, U6}; use std::sync::Arc; @@ -67,13 +71,13 @@ impl From for Error { #[arbitrary(bound = "T: EthSpec")] pub struct LightClientUpdate { /// The last `BeaconBlockHeader` from the last attested block by the sync committee. - pub attested_header: BeaconBlockHeader, + pub attested_header: LightClientHeader, /// The `SyncCommittee` used in the next period. pub next_sync_committee: Arc>, /// Merkle proof for next sync committee pub next_sync_committee_branch: FixedVector, /// The last `BeaconBlockHeader` from the last attested finalized block (end of epoch). - pub finalized_header: BeaconBlockHeader, + pub finalized_header: LightClientHeader, /// Merkle proof attesting finalized header. pub finality_branch: FixedVector, /// current sync aggreggate @@ -128,10 +132,10 @@ impl LightClientUpdate { attested_state.compute_merkle_proof(NEXT_SYNC_COMMITTEE_INDEX)?; let finality_branch = attested_state.compute_merkle_proof(FINALIZED_ROOT_INDEX)?; Ok(Self { - attested_header, + attested_header: attested_header.into(), next_sync_committee: attested_state.next_sync_committee()?.clone(), next_sync_committee_branch: FixedVector::new(next_sync_committee_branch)?, - finalized_header, + finalized_header: finalized_header.into(), finality_branch: FixedVector::new(finality_branch)?, sync_aggregate: sync_aggregate.clone(), signature_slot: block.slot(), @@ -139,6 +143,26 @@ impl LightClientUpdate { } } +impl ForkVersionDeserialize for LightClientUpdate { + fn deserialize_by_fork<'de, D: Deserializer<'de>>( + value: Value, + fork_name: ForkName, + ) -> Result { + match fork_name { + ForkName::Altair | ForkName::Merge => { + Ok(serde_json::from_value::>(value) + .map_err(serde::de::Error::custom))? + } + ForkName::Base | ForkName::Capella | ForkName::Deneb => { + Err(serde::de::Error::custom(format!( + "LightClientUpdate failed to deserialize: unsupported fork '{}'", + fork_name + ))) + } + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/consensus/types/src/payload.rs b/consensus/types/src/payload.rs index ff7caffcf3..6d584fc1eb 100644 --- a/consensus/types/src/payload.rs +++ b/consensus/types/src/payload.rs @@ -979,3 +979,10 @@ impl From> for ExecutionPayloadHeader { } } } + +/// The block production flow version to be used. +pub enum BlockProductionVersion { + V3, + BlindedV2, + FullV2, +} diff --git a/consensus/types/src/sync_aggregator_selection_data.rs b/consensus/types/src/sync_aggregator_selection_data.rs index 2b60d01b8e..3da130bb06 100644 --- a/consensus/types/src/sync_aggregator_selection_data.rs +++ b/consensus/types/src/sync_aggregator_selection_data.rs @@ -25,11 +25,11 @@ pub struct SyncAggregatorSelectionData { pub subcommittee_index: u64, } +impl SignedRoot for SyncAggregatorSelectionData {} + #[cfg(test)] mod tests { use super::*; ssz_and_tree_hash_tests!(SyncAggregatorSelectionData); } - -impl SignedRoot for SyncAggregatorSelectionData {} diff --git a/consensus/types/src/sync_committee.rs b/consensus/types/src/sync_committee.rs index 0bcf505f25..b42a000bb0 100644 --- a/consensus/types/src/sync_committee.rs +++ b/consensus/types/src/sync_committee.rs @@ -1,5 +1,4 @@ use crate::test_utils::TestRandom; -use crate::typenum::Unsigned; use crate::{EthSpec, FixedVector, SyncSubnetId}; use bls::PublicKeyBytes; use safe_arith::{ArithError, SafeArith}; @@ -46,14 +45,11 @@ pub struct SyncCommittee { impl SyncCommittee { /// Create a temporary sync committee that should *never* be included in a legitimate consensus object. - pub fn temporary() -> Result { - Ok(Self { - pubkeys: FixedVector::new(vec![ - PublicKeyBytes::empty(); - T::SyncCommitteeSize::to_usize() - ])?, + pub fn temporary() -> Self { + Self { + pubkeys: FixedVector::from_elem(PublicKeyBytes::empty()), aggregate_pubkey: PublicKeyBytes::empty(), - }) + } } /// Return the pubkeys in this `SyncCommittee` for the given `subcommittee_index`. diff --git a/database_manager/src/lib.rs b/database_manager/src/lib.rs index 079419aba5..95af4d6382 100644 --- a/database_manager/src/lib.rs +++ b/database_manager/src/lib.rs @@ -5,17 +5,18 @@ use beacon_chain::{ use beacon_node::{get_data_dir, get_slots_per_restore_point, ClientConfig}; use clap::{App, Arg, ArgMatches}; use environment::{Environment, RuntimeContext}; -use slog::{info, Logger}; +use slog::{info, warn, Logger}; use std::fs; use std::io::Write; use std::path::PathBuf; +use store::metadata::STATE_UPPER_LIMIT_NO_RETAIN; use store::{ errors::Error, metadata::{SchemaVersion, CURRENT_SCHEMA_VERSION}, DBColumn, HotColdDB, KeyValueStore, LevelDB, }; use strum::{EnumString, EnumVariantNames, VariantNames}; -use types::EthSpec; +use types::{BeaconState, EthSpec, Slot}; pub const CMD: &str = "database_manager"; @@ -88,17 +89,35 @@ pub fn inspect_cli_app<'a, 'b>() -> App<'a, 'b> { } pub fn prune_payloads_app<'a, 'b>() -> App<'a, 'b> { - App::new("prune_payloads") + App::new("prune-payloads") + .alias("prune_payloads") .setting(clap::AppSettings::ColoredHelp) .about("Prune finalized execution payloads") } pub fn prune_blobs_app<'a, 'b>() -> App<'a, 'b> { - App::new("prune_blobs") + App::new("prune-blobs") + .alias("prune_blobs") .setting(clap::AppSettings::ColoredHelp) .about("Prune blobs older than data availability boundary") } +pub fn prune_states_app<'a, 'b>() -> App<'a, 'b> { + App::new("prune-states") + .alias("prune_states") + .arg( + Arg::with_name("confirm") + .long("confirm") + .help( + "Commit to pruning states irreversably. Without this flag the command will \ + just check that the database is capable of being pruned.", + ) + .takes_value(false), + ) + .setting(clap::AppSettings::ColoredHelp) + .about("Prune all beacon states from the freezer database") +} + pub fn cli_app<'a, 'b>() -> App<'a, 'b> { App::new(CMD) .visible_aliases(&["db"]) @@ -145,6 +164,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .subcommand(inspect_cli_app()) .subcommand(prune_payloads_app()) .subcommand(prune_blobs_app()) + .subcommand(prune_states_app()) } fn parse_client_config( @@ -190,7 +210,7 @@ pub fn display_db_version( HotColdDB::, LevelDB>::open( &hot_path, &cold_path, - blobs_path, + &blobs_path, |_, from, _| { version = from; Ok(()) @@ -213,7 +233,7 @@ pub fn display_db_version( Ok(()) } -#[derive(Debug, EnumString, EnumVariantNames)] +#[derive(Debug, PartialEq, Eq, EnumString, EnumVariantNames)] pub enum InspectTarget { #[strum(serialize = "sizes")] ValueSizes, @@ -221,6 +241,8 @@ pub enum InspectTarget { ValueTotal, #[strum(serialize = "values")] Values, + #[strum(serialize = "gaps")] + Gaps, } pub struct InspectConfig { @@ -266,7 +288,7 @@ pub fn inspect_db( let db = HotColdDB::, LevelDB>::open( &hot_path, &cold_path, - blobs_path, + &blobs_path, |_, _, _| Ok(()), client_config.store, spec, @@ -286,6 +308,9 @@ pub fn inspect_db( let skip = inspect_config.skip.unwrap_or(0); let limit = inspect_config.limit.unwrap_or(usize::MAX); + let mut prev_key = 0; + let mut found_gaps = false; + let base_path = &inspect_config.output_dir; if let InspectTarget::Values = inspect_config.target { @@ -304,6 +329,23 @@ pub fn inspect_db( InspectTarget::ValueSizes => { println!("{}: {} bytes", hex::encode(&key), value.len()); } + InspectTarget::Gaps => { + // Convert last 8 bytes of key to u64. + let numeric_key = u64::from_be_bytes( + key[key.len() - 8..] + .try_into() + .expect("key is at least 8 bytes"), + ); + + if numeric_key > prev_key + 1 { + println!( + "gap between keys {} and {} (offset: {})", + prev_key, numeric_key, num_keys, + ); + found_gaps = true; + } + prev_key = numeric_key; + } InspectTarget::ValueTotal => (), InspectTarget::Values => { let file_path = base_path.join(format!( @@ -332,6 +374,10 @@ pub fn inspect_db( num_keys += 1; } + if inspect_config.target == InspectTarget::Gaps && !found_gaps { + println!("No gaps found!"); + } + println!("Num keys: {}", num_keys); println!("Total: {} bytes", total); @@ -364,7 +410,7 @@ pub fn migrate_db( let db = HotColdDB::, LevelDB>::open( &hot_path, &cold_path, - blobs_path, + &blobs_path, |_, db_initial_version, _| { from = db_initial_version; Ok(()) @@ -404,7 +450,7 @@ pub fn prune_payloads( let db = HotColdDB::, LevelDB>::open( &hot_path, &cold_path, - blobs_path, + &blobs_path, |_, _, _| Ok(()), client_config.store, spec.clone(), @@ -430,7 +476,7 @@ pub fn prune_blobs( let db = HotColdDB::, LevelDB>::open( &hot_path, &cold_path, - blobs_path, + &blobs_path, |_, _, _| Ok(()), client_config.store, spec.clone(), @@ -442,6 +488,86 @@ pub fn prune_blobs( db.try_prune_most_blobs(true) } +pub struct PruneStatesConfig { + confirm: bool, +} + +fn parse_prune_states_config(cli_args: &ArgMatches) -> Result { + let confirm = cli_args.is_present("confirm"); + Ok(PruneStatesConfig { confirm }) +} + +pub fn prune_states( + client_config: ClientConfig, + prune_config: PruneStatesConfig, + mut genesis_state: BeaconState, + runtime_context: &RuntimeContext, + log: Logger, +) -> Result<(), String> { + let spec = &runtime_context.eth2_config.spec; + let hot_path = client_config.get_db_path(); + let cold_path = client_config.get_freezer_db_path(); + let blobs_path = client_config.get_blobs_db_path(); + + let db = HotColdDB::, LevelDB>::open( + &hot_path, + &cold_path, + &blobs_path, + |_, _, _| Ok(()), + client_config.store, + spec.clone(), + log.clone(), + ) + .map_err(|e| format!("Unable to open database: {e:?}"))?; + + // Load the genesis state from the database to ensure we're deleting states for the + // correct network, and that we don't end up storing the wrong genesis state. + let genesis_from_db = db + .load_cold_state_by_slot(Slot::new(0)) + .map_err(|e| format!("Error reading genesis state: {e:?}"))? + .ok_or("Error: genesis state missing from database. Check schema version.")?; + + if genesis_from_db.genesis_validators_root() != genesis_state.genesis_validators_root() { + return Err(format!( + "Error: Wrong network. Genesis state in DB does not match {} genesis.", + spec.config_name.as_deref().unwrap_or("") + )); + } + + // Check that the user has confirmed they want to proceed. + if !prune_config.confirm { + match db.get_anchor_info() { + Some(anchor_info) if anchor_info.state_upper_limit == STATE_UPPER_LIMIT_NO_RETAIN => { + info!(log, "States have already been pruned"); + return Ok(()); + } + _ => { + info!(log, "Ready to prune states"); + } + } + warn!( + log, + "Pruning states is irreversible"; + ); + warn!( + log, + "Re-run this command with --confirm to commit to state deletion" + ); + info!(log, "Nothing has been pruned on this run"); + return Err("Error: confirmation flag required".into()); + } + + // Delete all historic state data and *re-store* the genesis state. + let genesis_state_root = genesis_state + .update_tree_hash_cache() + .map_err(|e| format!("Error computing genesis state root: {e:?}"))?; + db.prune_historic_states(genesis_state_root, &genesis_state) + .map_err(|e| format!("Failed to prune due to error: {e:?}"))?; + + info!(log, "Historic states pruned successfully"); + Ok(()) +} + /// Run the database manager, returning an error string if the operation did not succeed. pub fn run(cli_args: &ArgMatches<'_>, env: Environment) -> Result<(), String> { let client_config = parse_client_config(cli_args, &env)?; @@ -461,10 +587,34 @@ pub fn run(cli_args: &ArgMatches<'_>, env: Environment) -> Result let inspect_config = parse_inspect_config(cli_args)?; inspect_db(inspect_config, client_config, &context, log) } - ("prune_payloads", Some(_)) => { + ("prune-payloads", Some(_)) => { prune_payloads(client_config, &context, log).map_err(format_err) } - ("prune_blobs", Some(_)) => prune_blobs(client_config, &context, log).map_err(format_err), + ("prune-blobs", Some(_)) => prune_blobs(client_config, &context, log).map_err(format_err), + ("prune-states", Some(cli_args)) => { + let executor = env.core_context().executor; + let network_config = context + .eth2_network_config + .clone() + .ok_or("Missing network config")?; + + let genesis_state = executor + .block_on_dangerous( + network_config.genesis_state::( + client_config.genesis_state_url.as_deref(), + client_config.genesis_state_url_timeout, + &log, + ), + "get_genesis_state", + ) + .ok_or("Shutting down")? + .map_err(|e| format!("Error getting genesis state: {e}"))? + .ok_or("Genesis state missing")?; + + let prune_config = parse_prune_states_config(cli_args)?; + + prune_states(client_config, prune_config, genesis_state, &context, log) + } _ => Err("Unknown subcommand, for help `lighthouse database_manager --help`".into()), } } diff --git a/lcli/Cargo.toml b/lcli/Cargo.toml index 854f718c59..d6c3ff197a 100644 --- a/lcli/Cargo.toml +++ b/lcli/Cargo.toml @@ -43,6 +43,8 @@ beacon_chain = { workspace = true } store = { workspace = true } malloc_utils = { workspace = true } rayon = { workspace = true } +execution_layer = { workspace = true } +hex = { workspace = true } [package.metadata.cargo-udeps.ignore] normal = ["malloc_utils"] diff --git a/lcli/src/main.rs b/lcli/src/main.rs index 2aa855474f..17fafe6ec1 100644 --- a/lcli/src/main.rs +++ b/lcli/src/main.rs @@ -11,6 +11,7 @@ mod indexed_attestations; mod insecure_validators; mod interop_genesis; mod mnemonic_validators; +mod mock_el; mod new_testnet; mod parse_ssz; mod replace_state_pubkeys; @@ -891,6 +892,61 @@ fn main() { .help("Number of repeat runs, useful for benchmarking."), ) ) + .subcommand( + SubCommand::with_name("mock-el") + .about("Creates a mock execution layer server. This is NOT SAFE and should only \ + be used for testing and development on testnets. Do not use in production. Do not \ + use on mainnet. It cannot perform validator duties.") + .arg( + Arg::with_name("jwt-output-path") + .long("jwt-output-path") + .value_name("PATH") + .takes_value(true) + .required(true) + .help("Path to write the JWT secret."), + ) + .arg( + Arg::with_name("listen-address") + .long("listen-address") + .value_name("IP_ADDRESS") + .takes_value(true) + .help("The server will listen on this address.") + .default_value("127.0.0.1") + ) + .arg( + Arg::with_name("listen-port") + .long("listen-port") + .value_name("PORT") + .takes_value(true) + .help("The server will listen on this port.") + .default_value("8551") + ) + .arg( + Arg::with_name("all-payloads-valid") + .long("all-payloads-valid") + .takes_value(true) + .help("Controls the response to newPayload and forkchoiceUpdated. \ + Set to 'true' to return VALID. Set to 'false' to return SYNCING.") + .default_value("false") + .hidden(true) + ) + .arg( + Arg::with_name("shanghai-time") + .long("shanghai-time") + .value_name("UNIX_TIMESTAMP") + .takes_value(true) + .help("The payload timestamp that enables Shanghai. Defaults to the mainnet value.") + .default_value("1681338479") + ) + .arg( + Arg::with_name("cancun-time") + .long("cancun-time") + .value_name("UNIX_TIMESTAMP") + .takes_value(true) + .help("The payload timestamp that enables Cancun. No default is provided \ + until Cancun is triggered on mainnet.") + ) + ) .get_matches(); let result = matches @@ -1032,6 +1088,8 @@ fn run( state_root::run::(env, network_config, matches) .map_err(|e| format!("Failed to run state-root command: {}", e)) } + ("mock-el", Some(matches)) => mock_el::run::(env, matches) + .map_err(|e| format!("Failed to run mock-el command: {}", e)), (other, _) => Err(format!("Unknown subcommand {}. See --help.", other)), } } diff --git a/lcli/src/mock_el.rs b/lcli/src/mock_el.rs new file mode 100644 index 0000000000..094e23c3b4 --- /dev/null +++ b/lcli/src/mock_el.rs @@ -0,0 +1,62 @@ +use clap::ArgMatches; +use clap_utils::{parse_optional, parse_required}; +use environment::Environment; +use execution_layer::{ + auth::JwtKey, + test_utils::{ + Config, MockExecutionConfig, MockServer, DEFAULT_JWT_SECRET, DEFAULT_TERMINAL_BLOCK, + }, +}; +use std::net::Ipv4Addr; +use std::path::PathBuf; +use types::*; + +pub fn run(mut env: Environment, matches: &ArgMatches) -> Result<(), String> { + let jwt_path: PathBuf = parse_required(matches, "jwt-output-path")?; + let listen_addr: Ipv4Addr = parse_required(matches, "listen-address")?; + let listen_port: u16 = parse_required(matches, "listen-port")?; + let all_payloads_valid: bool = parse_required(matches, "all-payloads-valid")?; + let shanghai_time = parse_required(matches, "shanghai-time")?; + let cancun_time = parse_optional(matches, "cancun-time")?; + + let handle = env.core_context().executor.handle().unwrap(); + let spec = &T::default_spec(); + let jwt_key = JwtKey::from_slice(&DEFAULT_JWT_SECRET).unwrap(); + std::fs::write(jwt_path, hex::encode(DEFAULT_JWT_SECRET)).unwrap(); + + let config = MockExecutionConfig { + server_config: Config { + listen_addr, + listen_port, + }, + jwt_key, + terminal_difficulty: spec.terminal_total_difficulty, + terminal_block: DEFAULT_TERMINAL_BLOCK, + terminal_block_hash: spec.terminal_block_hash, + shanghai_time: Some(shanghai_time), + cancun_time, + }; + let kzg = None; + let server: MockServer = MockServer::new_with_config(&handle, config, kzg); + + if all_payloads_valid { + eprintln!( + "Using --all-payloads-valid=true can be dangerous. \ + Never use this flag when operating validators." + ); + // Indicate that all payloads are valid. + server.all_payloads_valid(); + } + + eprintln!( + "This tool is for TESTING PURPOSES ONLY. Do not use in production or on mainnet. \ + It cannot perform validator duties. It may cause nodes to follow an invalid chain." + ); + eprintln!("Server listening on {}:{}", listen_addr, listen_port); + + let shutdown_reason = env.block_until_shutdown_requested()?; + + eprintln!("Shutting down: {:?}", shutdown_reason); + + Ok(()) +} diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index f98af96176..edf661abab 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -11,7 +11,7 @@ use ethereum_hashing::have_sha_extensions; use futures::TryFutureExt; use lighthouse_version::VERSION; use malloc_utils::configure_memory_allocator; -use slog::{crit, info, warn}; +use slog::{crit, info}; use std::path::PathBuf; use std::process::exit; use task_executor::ShutdownReason; @@ -81,16 +81,6 @@ fn main() { cfg!(feature = "gnosis"), ).as_str() ) - .arg( - Arg::with_name("spec") - .short("s") - .long("spec") - .value_name("DEPRECATED") - .help("This flag is deprecated, it will be disallowed in a future release. This \ - value is now derived from the --network or --testnet-dir flags.") - .takes_value(true) - .global(true) - ) .arg( Arg::with_name("env_log") .short("l") @@ -549,16 +539,9 @@ fn run( // Allow Prometheus access to the version and commit of the Lighthouse build. metrics::expose_lighthouse_version(); - if matches.is_present("spec") { - warn!( - log, - "The --spec flag is deprecated and will be removed in a future release" - ); - } - #[cfg(all(feature = "modern", target_arch = "x86_64"))] if !std::is_x86_feature_detected!("adx") { - warn!( + slog::warn!( log, "CPU seems incompatible with optimized Lighthouse build"; "advice" => "If you get a SIGILL, please try Lighthouse portable build" diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 7937b1d496..5f75cb1acf 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -243,60 +243,6 @@ fn paranoid_block_proposal_on() { .with_config(|config| assert!(config.chain.paranoid_block_proposal)); } -#[test] -fn count_unrealized_no_arg() { - CommandLineTest::new() - .flag("count-unrealized", None) - // This flag should be ignored, so there's nothing to test but that the - // client starts with the flag present. - .run_with_zero_port(); -} - -#[test] -fn count_unrealized_false() { - CommandLineTest::new() - .flag("count-unrealized", Some("false")) - // This flag should be ignored, so there's nothing to test but that the - // client starts with the flag present. - .run_with_zero_port(); -} - -#[test] -fn count_unrealized_true() { - CommandLineTest::new() - .flag("count-unrealized", Some("true")) - // This flag should be ignored, so there's nothing to test but that the - // client starts with the flag present. - .run_with_zero_port(); -} - -#[test] -fn count_unrealized_full_no_arg() { - CommandLineTest::new() - .flag("count-unrealized-full", None) - // This flag should be ignored, so there's nothing to test but that the - // client starts with the flag present. - .run_with_zero_port(); -} - -#[test] -fn count_unrealized_full_false() { - CommandLineTest::new() - .flag("count-unrealized-full", Some("false")) - // This flag should be ignored, so there's nothing to test but that the - // client starts with the flag present. - .run_with_zero_port(); -} - -#[test] -fn count_unrealized_full_true() { - CommandLineTest::new() - .flag("count-unrealized-full", Some("true")) - // This flag should be ignored, so there's nothing to test but that the - // client starts with the flag present. - .run_with_zero_port(); -} - #[test] fn reset_payload_statuses_default() { CommandLineTest::new() @@ -388,23 +334,6 @@ fn eth1_flag() { .with_config(|config| assert!(config.sync_eth1_chain)); } #[test] -fn eth1_endpoints_flag() { - CommandLineTest::new() - .flag("eth1-endpoints", Some("http://localhost:9545")) - .run_with_zero_port() - .with_config(|config| { - assert_eq!( - config.eth1.endpoint.get_endpoint().full.to_string(), - "http://localhost:9545/" - ); - assert_eq!( - config.eth1.endpoint.get_endpoint().to_string(), - "http://localhost:9545/" - ); - assert!(config.sync_eth1_chain); - }); -} -#[test] fn eth1_blocks_per_log_query_flag() { CommandLineTest::new() .flag("eth1-blocks-per-log-query", Some("500")) @@ -527,49 +456,6 @@ fn merge_execution_endpoints_flag() { fn merge_execution_endpoint_flag() { run_merge_execution_endpoints_flag_test("execution-endpoint") } -fn run_execution_endpoints_overrides_eth1_endpoints_test(eth1_flag: &str, execution_flag: &str) { - use sensitive_url::SensitiveUrl; - - let eth1_endpoint = "http://bad.bad"; - let execution_endpoint = "http://good.good"; - - assert!(eth1_endpoint != execution_endpoint); - - let dir = TempDir::new().expect("Unable to create temporary directory"); - let jwt_path = dir.path().join("jwt-file"); - - CommandLineTest::new() - .flag(eth1_flag, Some(ð1_endpoint)) - .flag(execution_flag, Some(&execution_endpoint)) - .flag("execution-jwt", jwt_path.as_os_str().to_str()) - .run_with_zero_port() - .with_config(|config| { - assert_eq!( - config.execution_layer.as_ref().unwrap().execution_endpoints, - vec![SensitiveUrl::parse(execution_endpoint).unwrap()] - ); - - // The eth1 endpoint should have been set to the --execution-endpoint value in defiance - // of --eth1-endpoints. - assert_eq!( - config.eth1.endpoint, - Eth1Endpoint::Auth { - endpoint: SensitiveUrl::parse(execution_endpoint).unwrap(), - jwt_path: jwt_path.clone(), - jwt_id: None, - jwt_version: None, - } - ); - }); -} -#[test] -fn execution_endpoints_overrides_eth1_endpoints() { - run_execution_endpoints_overrides_eth1_endpoints_test("eth1-endpoints", "execution-endpoints"); -} -#[test] -fn execution_endpoint_overrides_eth1_endpoint() { - run_execution_endpoints_overrides_eth1_endpoints_test("eth1-endpoint", "execution-endpoint"); -} #[test] fn merge_jwt_secrets_flag() { let dir = TempDir::new().expect("Unable to create temporary directory"); @@ -1772,11 +1658,17 @@ fn metrics_allow_origin_all_flag() { // Tests for Validator Monitor flags. #[test] +fn validator_monitor_default_values() { + CommandLineTest::new() + .run_with_zero_port() + .with_config(|config| assert!(config.validator_monitor == <_>::default())); +} +#[test] fn validator_monitor_auto_flag() { CommandLineTest::new() .flag("validator-monitor-auto", None) .run_with_zero_port() - .with_config(|config| assert!(config.validator_monitor_auto)); + .with_config(|config| assert!(config.validator_monitor.auto_register)); } #[test] fn validator_monitor_pubkeys_flag() { @@ -1785,8 +1677,8 @@ fn validator_monitor_pubkeys_flag() { 0xbeefdeadbeefdeaddeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")) .run_with_zero_port() .with_config(|config| { - assert_eq!(config.validator_monitor_pubkeys[0].to_string(), "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); - assert_eq!(config.validator_monitor_pubkeys[1].to_string(), "0xbeefdeadbeefdeaddeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); + assert_eq!(config.validator_monitor.validators[0].to_string(), "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); + assert_eq!(config.validator_monitor.validators[1].to_string(), "0xbeefdeadbeefdeaddeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); }); } #[test] @@ -1800,8 +1692,8 @@ fn validator_monitor_file_flag() { .flag("validator-monitor-file", dir.path().join("pubkeys.txt").as_os_str().to_str()) .run_with_zero_port() .with_config(|config| { - assert_eq!(config.validator_monitor_pubkeys[0].to_string(), "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); - assert_eq!(config.validator_monitor_pubkeys[1].to_string(), "0xbeefdeadbeefdeaddeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); + assert_eq!(config.validator_monitor.validators[0].to_string(), "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); + assert_eq!(config.validator_monitor.validators[1].to_string(), "0xbeefdeadbeefdeaddeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); }); } #[test] @@ -1810,7 +1702,7 @@ fn validator_monitor_metrics_threshold_default() { .run_with_zero_port() .with_config(|config| { assert_eq!( - config.validator_monitor_individual_tracking_threshold, + config.validator_monitor.individual_tracking_threshold, // If this value changes make sure to update the help text for // the CLI command. 64 @@ -1826,7 +1718,7 @@ fn validator_monitor_metrics_threshold_custom() { ) .run_with_zero_port() .with_config(|config| { - assert_eq!(config.validator_monitor_individual_tracking_threshold, 42) + assert_eq!(config.validator_monitor.individual_tracking_threshold, 42) }); } @@ -2454,7 +2346,10 @@ fn sync_eth1_chain_disable_deposit_contract_sync_flag() { fn light_client_server_default() { CommandLineTest::new() .run_with_zero_port() - .with_config(|config| assert_eq!(config.network.enable_light_client_server, false)); + .with_config(|config| { + assert_eq!(config.network.enable_light_client_server, false); + assert_eq!(config.http_api.enable_light_client_server, false); + }); } #[test] @@ -2462,7 +2357,20 @@ fn light_client_server_enabled() { CommandLineTest::new() .flag("light-client-server", None) .run_with_zero_port() - .with_config(|config| assert_eq!(config.network.enable_light_client_server, true)); + .with_config(|config| { + assert_eq!(config.network.enable_light_client_server, true); + }); +} + +#[test] +fn light_client_http_server_enabled() { + CommandLineTest::new() + .flag("http", None) + .flag("light-client-server", None) + .run_with_zero_port() + .with_config(|config| { + assert_eq!(config.http_api.enable_light_client_server, true); + }); } #[test] @@ -2472,7 +2380,7 @@ fn gui_flag() { .run_with_zero_port() .with_config(|config| { assert!(config.http_api.enabled); - assert!(config.validator_monitor_auto); + assert!(config.validator_monitor.auto_register); }); } diff --git a/lighthouse/tests/validator_client.rs b/lighthouse/tests/validator_client.rs index 5268f3230a..1275a78aad 100644 --- a/lighthouse/tests/validator_client.rs +++ b/lighthouse/tests/validator_client.rs @@ -1,4 +1,4 @@ -use validator_client::Config; +use validator_client::{ApiTopic, Config}; use crate::exec::CommandLineTestExec; use bls::{Keypair, PublicKeyBytes}; @@ -101,12 +101,6 @@ fn beacon_nodes_flag() { }); } -#[test] -fn allow_unsynced_flag() { - // No-op, but doesn't crash. - CommandLineTest::new().flag("allow-unsynced", None).run(); -} - #[test] fn disable_auto_discover_flag() { CommandLineTest::new() @@ -428,24 +422,6 @@ fn no_doppelganger_protection_flag() { .with_config(|config| assert!(!config.enable_doppelganger_protection)); } #[test] -fn block_delay_ms() { - CommandLineTest::new() - .flag("block-delay-ms", Some("2000")) - .run() - .with_config(|config| { - assert_eq!( - config.block_delay, - Some(std::time::Duration::from_millis(2000)) - ) - }); -} -#[test] -fn no_block_delay_ms() { - CommandLineTest::new() - .run() - .with_config(|config| assert_eq!(config.block_delay, None)); -} -#[test] fn no_gas_limit_flag() { CommandLineTest::new() .run() @@ -503,16 +479,57 @@ fn monitoring_endpoint() { fn disable_run_on_all_default() { CommandLineTest::new().run().with_config(|config| { assert!(!config.beacon_node_fallback.disable_run_on_all); + assert_eq!(config.broadcast_topics, vec![]); + }); + // --broadcast flag takes precedence + CommandLineTest::new() + .flag("disable-run-on-all", None) + .flag("broadcast", Some("attestations")) + .run() + .with_config(|config| { + assert_eq!(config.broadcast_topics, vec![ApiTopic::Attestations]); + }); +} + +#[test] +fn no_broadcast_flag() { + CommandLineTest::new().run().with_config(|config| { + assert_eq!(config.broadcast_topics, vec![ApiTopic::Subscriptions]); }); } #[test] -fn disable_run_on_all() { +fn broadcast_flag() { + // "none" variant CommandLineTest::new() - .flag("disable-run-on-all", None) + .flag("broadcast", Some("none")) .run() .with_config(|config| { - assert!(config.beacon_node_fallback.disable_run_on_all); + assert_eq!(config.broadcast_topics, vec![]); + }); + // "none" with other values is ignored + CommandLineTest::new() + .flag("broadcast", Some("none,sync-committee")) + .run() + .with_config(|config| { + assert_eq!(config.broadcast_topics, vec![ApiTopic::SyncCommittee]); + }); + // Other valid variants + CommandLineTest::new() + .flag("broadcast", Some("blocks, subscriptions")) + .run() + .with_config(|config| { + assert_eq!( + config.broadcast_topics, + vec![ApiTopic::Blocks, ApiTopic::Subscriptions], + ); + }); + // Omitted "subscription" overrides default + CommandLineTest::new() + .flag("broadcast", Some("attestations")) + .run() + .with_config(|config| { + assert_eq!(config.broadcast_topics, vec![ApiTopic::Attestations]); }); } @@ -551,6 +568,21 @@ fn beacon_node_medium_sync_distance_modifier_flag() { }); } +#[test] +#[should_panic(expected = "Unknown API topic")] +fn wrong_broadcast_flag() { + CommandLineTest::new() + .flag("broadcast", Some("foo, subscriptions")) + .run() + .with_config(|config| { + assert_eq!( + config.broadcast_topics, + vec![ApiTopic::Blocks, ApiTopic::Subscriptions], + >>>>>>> unstable + ); + }); +} + #[test] fn latency_measurement_service() { CommandLineTest::new().run().with_config(|config| { diff --git a/scripts/local_testnet/README.md b/scripts/local_testnet/README.md index 4f54154d1d..87565b0cae 100644 --- a/scripts/local_testnet/README.md +++ b/scripts/local_testnet/README.md @@ -5,58 +5,105 @@ This setup can be useful for testing and development. ## Requirements -The scripts require `lcli`, `lighthouse`, `geth`, `bootnode` to be installed on `PATH`. +The scripts require `lcli`, `lighthouse`, `geth`, `bootnode` to be installed on `PATH` (run `echo $PATH` to view all `PATH` directories). MacOS users need to install GNU `sed` and GNU `grep`, and add them both to `PATH` as well. -From the -root of this repository, run: +The first step is to install Rust and dependencies. Refer to the [Lighthouse Book](https://lighthouse-book.sigmaprime.io/installation-source.html#dependencies) for installation. We will also need [jq](https://jqlang.github.io/jq/), which can be installed with `sudo apt install jq`. + +Then, we clone the Lighthouse repository: +```bash +cd ~ +git clone https://github.com/sigp/lighthouse.git +cd lighthouse +``` +We are now ready to build Lighthouse. Run the command: ```bash make make install-lcli ``` +This will build `lighthouse` and `lcli`. For `geth` and `bootnode`, go to [geth website](https://geth.ethereum.org/downloads) and download the `Geth & Tools`. For example, to download and extract `Geth & Tools 1.13.1`: + +```bash +cd ~ +curl -LO https://gethstore.blob.core.windows.net/builds/geth-alltools-linux-amd64-1.13.1-3f40e65c.tar.gz +tar xvf geth-alltools-linux-amd64-1.13.1-3f40e65c.tar.gz +``` + +After extraction, copy `geth` and `bootnode` to the `PATH`. A typical directory is `/usr/local/bin`. + +```bash +cd geth-alltools-linux-amd64-1.13.1-3f40e65c +sudo cp geth bootnode /usr/local/bin +``` + +After that We can remove the downloaded files: + +```bash +cd ~ +rm -r geth-alltools-linux-amd64-1.13.1-3f40e65c geth-alltools-linux-amd64-1.13.1-3f40e65c.tar.gz +``` + +We are now ready to start a local testnet. + ## Starting the testnet -Modify `vars.env` as desired. +To start a testnet using the predetermined settings: + +```bash +cd ~ +cd ./lighthouse/scripts/local_testnet +./start_local_testnet.sh genesis.json +``` + +This will execute the script and if the testnet setup is successful, you will see "Started!" at the end. The testnet starts with a post-merge genesis state. -Start a consensus layer and execution layer boot node along with `BN_COUNT` -number of beacon nodes each connected to a geth execution client and `VC_COUNT` validator clients. +The testnet starts a consensus layer and execution layer boot node along with `BN_COUNT` +(the number of beacon nodes) each connected to a geth execution client and `VC_COUNT` (the number of validator clients). By default, `BN_COUNT=4`, `VC_COUNT=4`. The `start_local_testnet.sh` script takes four options `-v VC_COUNT`, `-d DEBUG_LEVEL`, `-p` to enable builder proposals and `-h` for help. It also takes a mandatory `GENESIS_FILE` for initialising geth's state. A sample `genesis.json` is provided in this directory. -The `ETH1_BLOCK_HASH` environment variable is set to the block_hash of the genesis execution layer block which depends on the contents of `genesis.json`. Users of these scripts need to ensure that the `ETH1_BLOCK_HASH` variable is updated if genesis file is modified. - The options may be in any order or absent in which case they take the default value specified. - VC_COUNT: the number of validator clients to create, default: `BN_COUNT` - DEBUG_LEVEL: one of { error, warn, info, debug, trace }, default: `info` +The `ETH1_BLOCK_HASH` environment variable is set to the block_hash of the genesis execution layer block which depends on the contents of `genesis.json`. Users of these scripts need to ensure that the `ETH1_BLOCK_HASH` variable is updated if genesis file is modified. +To view the beacon, validator client and geth logs: ```bash -./start_local_testnet.sh genesis.json +tail -f ~/.lighthouse/local-testnet/testnet/beacon_node_1.log +taif -f ~/.lighthouse/local-testnet/testnet/validator_node_1.log +tail -f ~/.lighthouse/local-testnet/testnet/geth_1.log ``` +where `beacon_node_1` can be changed to `beacon_node_2`, `beacon_node_3` or `beacon_node_4` to view logs for different beacon nodes. The same applies to validator clients and geth nodes. + ## Stopping the testnet -This is not necessary before `start_local_testnet.sh` as it invokes `stop_local_testnet.sh` automatically. +To stop the testnet, navigate to the directory `cd ~/lighthouse/scripts/local_testnet`, then run the command: + ```bash ./stop_local_testnet.sh ``` +Once a testnet is stopped, it cannot be continued from where it left off. When the start local testnet command is run, it will start a new local testnet. + ## Manual creation of local testnet -These scripts are used by ./start_local_testnet.sh and may be used to manually +In [Starting the testnet](./README.md#starting-the-testnet), the testnet is started automatically with predetermined parameters (database directory, ports used etc). This section describes some modifications of the local testnet settings, e.g., changing the database directory, or changing the ports used. -Assuming you are happy with the configuration in `vars.env`, -create the testnet directory, genesis state with embedded validators and validator keys with: + +The testnet also contains parameters that are specified in `vars.env`, such as the slot time `SECONDS_PER_SLOT=3` (instead of 12 seconds on mainnet). You may change these parameters to suit your testing purposes. After that, in the `local_testnet` directory, run the following command to create genesis state with embedded validators and validator keys, and also to update the time in `genesis.json`: ```bash ./setup.sh +./setup_time.sh genesis.json ``` Note: The generated genesis validators are embedded into the genesis state as genesis validators and hence do not require manual deposits to activate. @@ -73,17 +120,17 @@ Start a geth node: ``` e.g. ```bash -./geth.sh $HOME/.lighthouse/local-testnet/geth_1 5000 6000 7000 genesis.json +./geth.sh $HOME/.lighthouse/local-testnet/geth_1 7001 6001 5001 genesis.json ``` Start a beacon node: ```bash -./beacon_node.sh +./beacon_node.sh ``` e.g. ```bash -./beacon_node.sh $HOME/.lighthouse/local-testnet/node_1 9000 8000 http://localhost:6000 ~/.lighthouse/local-testnet/geth_1/geth/jwtsecret +./beacon_node.sh $HOME/.lighthouse/local-testnet/node_1 9001 9101 8001 http://localhost:5001 ~/.lighthouse/local-testnet/geth_1/geth/jwtsecret ``` In a new terminal, start the validator client which will attach to the first @@ -94,10 +141,16 @@ beacon node: ``` e.g. to attach to the above created beacon node ```bash -./validator_client.sh $HOME/.lighthouse/local-testnet/node_1 http://localhost:8000 +./validator_client.sh $HOME/.lighthouse/local-testnet/node_1 http://localhost:8001 ``` -You can create additional beacon node and validator client instances with appropriate parameters. +You can create additional geth, beacon node and validator client instances by changing the ports, e.g., for a second geth, beacon node and validator client: + +```bash +./geth.sh $HOME/.lighthouse/local-testnet/geth_2 7002 6002 5002 genesis.json +./beacon_node.sh $HOME/.lighthouse/local-testnet/node_2 9002 9102 8002 http://localhost:5002 ~/.lighthouse/local-testnet/geth_2/geth/jwtsecret +./validator_client.sh $HOME/.lighthouse/local-testnet/node_2 http://localhost:8002 +``` ## Additional Info @@ -109,7 +162,7 @@ instances using the `--datadir` parameter. ### Starting fresh -Delete the current testnet and all related files using. Generally not necessary as `start_local_test.sh` does this each time it starts. +You can delete the current testnet and all related files using the following command. Alternatively, if you wish to start another testnet, doing the steps [Starting the testnet](./README.md#starting-the-testnet) will automatically delete the files and start a fresh local testnet. ```bash ./clean.sh @@ -131,12 +184,12 @@ Update the genesis time to now using: ### Testing builder flow -1. Add builder URL to `BN_ARGS` in `./var.env`, e.g. `--builder http://localhost:8650`. Some mock builder server options: +1. Add builder URL to `BN_ARGS` in `./vars.env`, e.g. `--builder http://localhost:8650`. Some mock builder server options: - [`mock-relay`](https://github.com/realbigsean/mock-relay) - [`dummy-builder`](https://github.com/michaelsproul/dummy_builder) 2. (Optional) Add `--always-prefer-builder-payload` to `BN_ARGS`. 3. The above mock builders do not support non-mainnet presets as of now, and will require setting `SECONDS_PER_SLOT` and `SECONDS_PER_ETH1_BLOCK` to `12` in `./vars.env`. -4. Start the testnet with the following command (the `-p` flag enables the validator client `--builder-proposals` flag: +4. Start the testnet with the following command (the `-p` flag enables the validator client `--builder-proposals` flag): ```bash ./start_local_testnet.sh -p genesis.json ``` diff --git a/scripts/local_testnet/el_bootnode.sh b/scripts/local_testnet/el_bootnode.sh index d73a463f6d..ee437a491c 100755 --- a/scripts/local_testnet/el_bootnode.sh +++ b/scripts/local_testnet/el_bootnode.sh @@ -1,3 +1,3 @@ priv_key="02fd74636e96a8ffac8e7b01b0de8dea94d6bcf4989513b38cf59eb32163ff91" source ./vars.env -$EL_BOOTNODE_BINARY --nodekeyhex $priv_key \ No newline at end of file +exec $EL_BOOTNODE_BINARY --nodekeyhex $priv_key \ No newline at end of file diff --git a/scripts/local_testnet/setup.sh b/scripts/local_testnet/setup.sh index 7e000251a2..d7a6016aa8 100755 --- a/scripts/local_testnet/setup.sh +++ b/scripts/local_testnet/setup.sh @@ -32,7 +32,7 @@ lcli \ --ttd $TTD \ --eth1-block-hash $ETH1_BLOCK_HASH \ --eth1-id $CHAIN_ID \ - --eth1-follow-distance 1 \ + --eth1-follow-distance 128 \ --seconds-per-slot $SECONDS_PER_SLOT \ --seconds-per-eth1-block $SECONDS_PER_ETH1_BLOCK \ --proposer-score-boost "$PROPOSER_SCORE_BOOST" \ diff --git a/scripts/local_testnet/setup_time.sh b/scripts/local_testnet/setup_time.sh new file mode 100755 index 0000000000..21a8ae7ac1 --- /dev/null +++ b/scripts/local_testnet/setup_time.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +source ./vars.env + +# Function to output SLOT_PER_EPOCH for mainnet or minimal +get_spec_preset_value() { + case "$SPEC_PRESET" in + mainnet) echo 32 ;; + minimal) echo 8 ;; + gnosis) echo 16 ;; + *) echo "Unsupported preset: $SPEC_PRESET" >&2; exit 1 ;; + esac +} + +SLOT_PER_EPOCH=$(get_spec_preset_value $SPEC_PRESET) +echo "slot_per_epoch=$SLOT_PER_EPOCH" + +genesis_file=$1 + +# Update future hardforks time in the EL genesis file based on the CL genesis time +GENESIS_TIME=$(lcli pretty-ssz --spec $SPEC_PRESET --testnet-dir $TESTNET_DIR BeaconState $TESTNET_DIR/genesis.ssz | jq | grep -Po 'genesis_time": "\K.*\d') +echo $GENESIS_TIME +CAPELLA_TIME=$((GENESIS_TIME + (CAPELLA_FORK_EPOCH * $SLOT_PER_EPOCH * SECONDS_PER_SLOT))) +echo $CAPELLA_TIME +sed -i 's/"shanghaiTime".*$/"shanghaiTime": '"$CAPELLA_TIME"',/g' $genesis_file +CANCUN_TIME=$((GENESIS_TIME + (DENEB_FORK_EPOCH * $SLOT_PER_EPOCH * SECONDS_PER_SLOT))) +echo $CANCUN_TIME +sed -i 's/"cancunTime".*$/"cancunTime": '"$CANCUN_TIME"',/g' $genesis_file +cat $genesis_file + diff --git a/scripts/local_testnet/start_local_testnet.sh b/scripts/local_testnet/start_local_testnet.sh index c796050bc4..512b1e98d1 100755 --- a/scripts/local_testnet/start_local_testnet.sh +++ b/scripts/local_testnet/start_local_testnet.sh @@ -102,16 +102,8 @@ execute_command_add_PID() { echo "executing: ./setup.sh >> $LOG_DIR/setup.log" ./setup.sh >> $LOG_DIR/setup.log 2>&1 -# Update future hardforks time in the EL genesis file based on the CL genesis time -GENESIS_TIME=$(lcli pretty-ssz --spec $SPEC_PRESET --testnet-dir $TESTNET_DIR BeaconState $TESTNET_DIR/genesis.ssz | jq | grep -Po 'genesis_time": "\K.*\d') -echo $GENESIS_TIME -CAPELLA_TIME=$((GENESIS_TIME + (CAPELLA_FORK_EPOCH * 32 * SECONDS_PER_SLOT))) -echo $CAPELLA_TIME -sed -i 's/"shanghaiTime".*$/"shanghaiTime": '"$CAPELLA_TIME"',/g' $genesis_file -CANCUN_TIME=$((GENESIS_TIME + (DENEB_FORK_EPOCH * 32 * SECONDS_PER_SLOT))) -echo $CANCUN_TIME -sed -i 's/"cancunTime".*$/"cancunTime": '"$CANCUN_TIME"',/g' $genesis_file -cat $genesis_file +# Call setup_time.sh to update future hardforks time in the EL genesis file based on the CL genesis time +./setup_time.sh genesis.json # Delay to let boot_enr.yaml to be created execute_command_add_PID bootnode.log ./bootnode.sh diff --git a/scripts/local_testnet/vars.env b/scripts/local_testnet/vars.env index d04a235497..31274d2c57 100644 --- a/scripts/local_testnet/vars.env +++ b/scripts/local_testnet/vars.env @@ -56,7 +56,7 @@ SPEC_PRESET=mainnet SECONDS_PER_SLOT=3 # Seconds per Eth1 block -SECONDS_PER_ETH1_BLOCK=1 +SECONDS_PER_ETH1_BLOCK=3 # Proposer score boost percentage PROPOSER_SCORE_BOOST=40 diff --git a/testing/execution_engine_integration/src/test_rig.rs b/testing/execution_engine_integration/src/test_rig.rs index 48195f871d..42667f27c6 100644 --- a/testing/execution_engine_integration/src/test_rig.rs +++ b/testing/execution_engine_integration/src/test_rig.rs @@ -4,7 +4,8 @@ use crate::execution_engine::{ use crate::transactions::transactions; use ethers_providers::Middleware; use execution_layer::{ - BuilderParams, ChainHealth, ExecutionLayer, PayloadAttributes, PayloadStatus, + BlockProposalContentsType, BuilderParams, ChainHealth, ExecutionLayer, PayloadAttributes, + PayloadStatus, }; use fork_choice::ForkchoiceUpdateParameters; use reqwest::{header::CONTENT_TYPE, Client}; @@ -14,9 +15,10 @@ use std::sync::Arc; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use task_executor::TaskExecutor; use tokio::time::sleep; +use types::payload::BlockProductionVersion; use types::{ Address, ChainSpec, EthSpec, ExecutionBlockHash, ExecutionPayload, ExecutionPayloadHeader, - ForkName, FullPayload, Hash256, MainnetEthSpec, PublicKeyBytes, Slot, Uint256, + ForkName, Hash256, MainnetEthSpec, PublicKeyBytes, Slot, Uint256, }; const EXECUTION_ENGINE_START_TIMEOUT: Duration = Duration::from_secs(60); @@ -322,21 +324,26 @@ impl TestRig { Some(vec![]), None, ); - let valid_payload = self + let block_proposal_content_type = self .ee_a .execution_layer - .get_payload::>( + .get_payload( parent_hash, &payload_attributes, forkchoice_update_params, builder_params, TEST_FORK, &self.spec, + BlockProductionVersion::FullV2, ) .await - .unwrap() - .to_payload() - .execution_payload(); + .unwrap(); + + let valid_payload = match block_proposal_content_type { + BlockProposalContentsType::Full(block) => block.to_payload().execution_payload(), + BlockProposalContentsType::Blinded(_) => panic!("Should always be a full payload"), + }; + assert_eq!(valid_payload.transactions().len(), pending_txs.len()); /* @@ -468,21 +475,25 @@ impl TestRig { Some(vec![]), None, ); - let second_payload = self + let block_proposal_content_type = self .ee_a .execution_layer - .get_payload::>( + .get_payload( parent_hash, &payload_attributes, forkchoice_update_params, builder_params, TEST_FORK, &self.spec, + BlockProductionVersion::FullV2, ) .await - .unwrap() - .to_payload() - .execution_payload(); + .unwrap(); + + let second_payload = match block_proposal_content_type { + BlockProposalContentsType::Full(block) => block.to_payload().execution_payload(), + BlockProposalContentsType::Blinded(_) => panic!("Should always be a full payload"), + }; /* * Execution Engine A: diff --git a/testing/node_test_rig/Cargo.toml b/testing/node_test_rig/Cargo.toml index 5fe820d15b..4696d8d2f1 100644 --- a/testing/node_test_rig/Cargo.toml +++ b/testing/node_test_rig/Cargo.toml @@ -11,7 +11,7 @@ types = { workspace = true } tempfile = { workspace = true } eth2 = { workspace = true } validator_client = { workspace = true } -validator_dir = { workspace = true } +validator_dir = { workspace = true, features = ["insecure_keys"] } sensitive_url = { workspace = true } execution_layer = { workspace = true } tokio = { workspace = true } diff --git a/testing/node_test_rig/src/lib.rs b/testing/node_test_rig/src/lib.rs index 3f08c83716..6c9af707f5 100644 --- a/testing/node_test_rig/src/lib.rs +++ b/testing/node_test_rig/src/lib.rs @@ -21,7 +21,7 @@ pub use eth2; pub use execution_layer::test_utils::{ Config as MockServerConfig, MockExecutionConfig, MockServer, }; -pub use validator_client::Config as ValidatorConfig; +pub use validator_client::{ApiTopic, Config as ValidatorConfig}; /// The global timeout for HTTP requests to the beacon node. const HTTP_TIMEOUT: Duration = Duration::from_secs(8); diff --git a/testing/simulator/src/eth1_sim.rs b/testing/simulator/src/eth1_sim.rs index 3657f548a3..cf0a66578d 100644 --- a/testing/simulator/src/eth1_sim.rs +++ b/testing/simulator/src/eth1_sim.rs @@ -7,7 +7,7 @@ use crate::retry::with_retry; use futures::prelude::*; use node_test_rig::{ environment::{EnvironmentBuilder, LoggerConfig}, - testing_validator_config, ValidatorFiles, + testing_validator_config, ApiTopic, ValidatorFiles, }; use rayon::prelude::*; use std::cmp::max; @@ -154,10 +154,25 @@ pub fn run_eth1_sim(matches: &ArgMatches) -> Result<(), String> { validator_config.fee_recipient = Some(SUGGESTED_FEE_RECIPIENT.into()); } println!("Adding validator client {}", i); - network_1 - .add_validator_client(validator_config, i, files, i % 2 == 0) - .await - .expect("should add validator"); + + // Enable broadcast on every 4th node. + if i % 4 == 0 { + validator_config.broadcast_topics = ApiTopic::all(); + let beacon_nodes = vec![i, (i + 1) % node_count]; + network_1 + .add_validator_client_with_fallbacks( + validator_config, + i, + beacon_nodes, + files, + ) + .await + } else { + network_1 + .add_validator_client(validator_config, i, files, i % 2 == 0) + .await + } + .expect("should add validator"); }, "vc", ); diff --git a/testing/state_transition_vectors/src/exit.rs b/testing/state_transition_vectors/src/exit.rs index 3b9235cc4e..29f5c015e3 100644 --- a/testing/state_transition_vectors/src/exit.rs +++ b/testing/state_transition_vectors/src/exit.rs @@ -127,7 +127,7 @@ vectors_and_tests!( ExitTest { block_modifier: Box::new(|_, block| { // Duplicate the exit - let exit = block.body().voluntary_exits().get(0).unwrap().clone(); + let exit = block.body().voluntary_exits().first().unwrap().clone(); block.body_mut().voluntary_exits_mut().push(exit).unwrap(); }), expected: Err(BlockProcessingError::ExitInvalid { diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index 0b648a8155..90a82b7e3b 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -61,3 +61,4 @@ malloc_utils = { workspace = true } sysinfo = { workspace = true } system_health = { path = "../common/system_health" } logging = { workspace = true } +strum = { workspace = true } diff --git a/validator_client/src/attestation_service.rs b/validator_client/src/attestation_service.rs index dc5fc27488..90ce52b156 100644 --- a/validator_client/src/attestation_service.rs +++ b/validator_client/src/attestation_service.rs @@ -1,4 +1,4 @@ -use crate::beacon_node_fallback::BeaconNodeFallback; +use crate::beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; use crate::{ duties_service::{DutiesService, DutyAndProof}, http_metrics::metrics, @@ -428,7 +428,7 @@ impl AttestationService { // Post the attestations to the BN. match self .beacon_nodes - .first_success(|beacon_node| async move { + .request(ApiTopic::Attestations, |beacon_node| async move { let _timer = metrics::start_timer_vec( &metrics::ATTESTATION_SERVICE_TIMES, &[metrics::ATTESTATIONS_HTTP_POST], diff --git a/validator_client/src/beacon_node_fallback.rs b/validator_client/src/beacon_node_fallback.rs index c15e55f9cc..0a99efc868 100644 --- a/validator_client/src/beacon_node_fallback.rs +++ b/validator_client/src/beacon_node_fallback.rs @@ -22,6 +22,7 @@ use std::future::Future; use std::marker::PhantomData; use std::sync::Arc; use std::time::{Duration, Instant}; +use strum::{EnumString, EnumVariantNames}; use tokio::{sync::RwLock, time::sleep}; use types::{ChainSpec, Config as ConfigSpec, EthSpec, Slot}; @@ -342,9 +343,9 @@ impl CandidateBeaconNode { #[derive(Clone, Debug)] pub struct BeaconNodeFallback { pub candidates: Arc>>>, - disable_run_on_all: bool, distance_tiers: BeaconNodeSyncDistanceTiers, slot_clock: Option, + broadcast_topics: Vec, spec: ChainSpec, log: Logger, } @@ -353,15 +354,16 @@ impl BeaconNodeFallback { pub fn new( candidates: Vec>, config: Config, + broadcast_topics: Vec, spec: ChainSpec, log: Logger, ) -> Self { let distance_tiers = BeaconNodeSyncDistanceTiers::from_config(&config); Self { candidates: Arc::new(RwLock::new(candidates)), - disable_run_on_all: config.disable_run_on_all, distance_tiers, slot_clock: None, + broadcast_topics, spec, log, } @@ -587,7 +589,7 @@ impl BeaconNodeFallback { /// It returns a list of errors along with the beacon node id that failed for `func`. /// Since this ignores the actual result of `func`, this function should only be used for beacon /// node calls whose results we do not care about, only that they completed successfully. - pub async fn run_on_all(&self, func: F) -> Result<(), Errors> + pub async fn broadcast(&self, func: F) -> Result<(), Errors> where F: Fn(BeaconNodeHttpClient) -> R, R: Future>, @@ -635,32 +637,61 @@ impl BeaconNodeFallback { } /// Call `func` on first beacon node that returns success or on all beacon nodes - /// depending on the value of `disable_run_on_all`. - pub async fn run(&self, func: F) -> Result<(), Errors> + /// depending on the `topic` and configuration. + pub async fn request(&self, topic: ApiTopic, func: F) -> Result<(), Errors> where F: Fn(BeaconNodeHttpClient) -> R, R: Future>, Err: Debug, { - if self.disable_run_on_all { + if self.broadcast_topics.contains(&topic) { + self.broadcast(func).await + } else { self.first_success(func).await?; Ok(()) - } else { - self.run_on_all(func).await } } } +/// Serves as a cue for `BeaconNodeFallback` to tell which requests need to be broadcasted. +#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize, EnumString, EnumVariantNames)] +#[strum(serialize_all = "kebab-case")] +pub enum ApiTopic { + Attestations, + Blocks, + Subscriptions, + SyncCommittee, +} + +impl ApiTopic { + pub fn all() -> Vec { + use ApiTopic::*; + vec![Attestations, Blocks, Subscriptions, SyncCommittee] + } +} + #[cfg(test)] mod tests { use super::*; use crate::beacon_node_health::BeaconNodeHealthTier; use crate::SensitiveUrl; use eth2::Timeouts; + use std::str::FromStr; + use strum::VariantNames; use types::{MainnetEthSpec, Slot}; type E = MainnetEthSpec; + #[test] + fn api_topic_all() { + let all = ApiTopic::all(); + assert_eq!(all.len(), ApiTopic::VARIANTS.len()); + assert!(ApiTopic::VARIANTS + .iter() + .map(|topic| ApiTopic::from_str(topic).unwrap()) + .eq(all.into_iter())); + } + #[test] fn check_candidate_order() { // These fields is irrelvant for sorting. They are set to arbitrary values. diff --git a/validator_client/src/block_service.rs b/validator_client/src/block_service.rs index 0bf9ad6a15..b30bac7306 100644 --- a/validator_client/src/block_service.rs +++ b/validator_client/src/block_service.rs @@ -1,6 +1,8 @@ use crate::beacon_node_fallback::{Error as FallbackError, Errors}; use crate::{ - beacon_node_fallback::BeaconNodeFallback, determine_graffiti, graffiti_file::GraffitiFile, + beacon_node_fallback::{ApiTopic, BeaconNodeFallback}, + determine_graffiti, + graffiti_file::GraffitiFile, }; use crate::{ http_metrics::metrics, @@ -18,7 +20,6 @@ use std::ops::Deref; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; -use tokio::time::sleep; use types::{ AbstractExecPayload, BlindedPayload, BlockType, EthSpec, FullPayload, Graffiti, PublicKeyBytes, Slot, @@ -54,7 +55,6 @@ pub struct BlockServiceBuilder { context: Option>, graffiti: Option, graffiti_file: Option, - block_delay: Option, } impl BlockServiceBuilder { @@ -67,7 +67,6 @@ impl BlockServiceBuilder { context: None, graffiti: None, graffiti_file: None, - block_delay: None, } } @@ -106,11 +105,6 @@ impl BlockServiceBuilder { self } - pub fn block_delay(mut self, block_delay: Option) -> Self { - self.block_delay = block_delay; - self - } - pub fn build(self) -> Result, String> { Ok(BlockService { inner: Arc::new(Inner { @@ -129,7 +123,6 @@ impl BlockServiceBuilder { proposer_nodes: self.proposer_nodes, graffiti: self.graffiti, graffiti_file: self.graffiti_file, - block_delay: self.block_delay, }), }) } @@ -144,31 +137,29 @@ pub struct ProposerFallback { impl ProposerFallback { // Try `func` on `self.proposer_nodes` first. If that doesn't work, try `self.beacon_nodes`. - pub async fn first_success_try_proposers_first( - &self, - func: F, - ) -> Result> + pub async fn request_proposers_first(&self, func: F) -> Result<(), Errors> where F: Fn(BeaconNodeHttpClient) -> R + Clone, - R: Future>, + R: Future>, Err: Debug, { // If there are proposer nodes, try calling `func` on them and return early if they are successful. if let Some(proposer_nodes) = &self.proposer_nodes { - if let Ok(result) = proposer_nodes.first_success(func.clone()).await { - return Ok(result); + if proposer_nodes + .request(ApiTopic::Blocks, func.clone()) + .await + .is_ok() + { + return Ok(()); } } // If the proposer nodes failed, try on the non-proposer nodes. - self.beacon_nodes.first_success(func).await + self.beacon_nodes.request(ApiTopic::Blocks, func).await } // Try `func` on `self.beacon_nodes` first. If that doesn't work, try `self.proposer_nodes`. - pub async fn first_success_try_proposers_last( - &self, - func: F, - ) -> Result> + pub async fn request_proposers_last(&self, func: F) -> Result> where F: Fn(BeaconNodeHttpClient) -> R + Clone, R: Future>, @@ -197,7 +188,6 @@ pub struct Inner { context: RuntimeContext, graffiti: Option, graffiti_file: Option, - block_delay: Option, } /// Attempts to produce attestations for any block producer(s) at the start of the epoch. @@ -241,18 +231,7 @@ impl BlockService { executor.spawn( async move { while let Some(notif) = notification_rx.recv().await { - let service = self.clone(); - - if let Some(delay) = service.block_delay { - debug!( - service.context.log(), - "Delaying block production by {}ms", - delay.as_millis() - ); - sleep(delay).await; - } - - service.do_update(notif).await.ok(); + self.do_update(notif).await.ok(); } debug!(log, "Block service shutting down"); }, @@ -457,8 +436,7 @@ impl BlockService { // Try the proposer nodes last, since it's likely that they don't have a // great view of attestations on the network. let block_contents = proposer_fallback - .first_success_try_proposers_last(|beacon_node| { - let beacon_node = beacon_node; + .request_proposers_last(move |beacon_node| { Self::get_validator_block( beacon_node, slot, @@ -547,8 +525,7 @@ impl BlockService { // protect them from DoS attacks and they're most likely to successfully // publish a block. proposer_fallback - .first_success_try_proposers_first(|beacon_node| async { - let beacon_node = beacon_node; + .request_proposers_first(|beacon_node| async { self.publish_signed_block_contents::(&signed_block_contents, beacon_node) .await }) diff --git a/validator_client/src/cli.rs b/validator_client/src/cli.rs index e0c6225789..2974c49d5d 100644 --- a/validator_client/src/cli.rs +++ b/validator_client/src/cli.rs @@ -8,15 +8,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { "When connected to a beacon node, performs the duties of a staked \ validator (e.g., proposing blocks and attestations).", ) - // This argument is deprecated, use `--beacon-nodes` instead. - .arg( - Arg::with_name("beacon-node") - .long("beacon-node") - .value_name("NETWORK_ADDRESS") - .help("Deprecated. Use --beacon-nodes.") - .takes_value(true) - .conflicts_with("beacon-nodes"), - ) .arg( Arg::with_name("beacon-nodes") .long("beacon-nodes") @@ -35,24 +26,28 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { ) .takes_value(true), ) + // TODO remove this flag in a future release .arg( Arg::with_name("disable-run-on-all") .long("disable-run-on-all") .value_name("DISABLE_RUN_ON_ALL") - .help("By default, Lighthouse publishes attestation, sync committee subscriptions \ + .help("DEPRECATED. Use --broadcast. \ + By default, Lighthouse publishes attestation, sync committee subscriptions \ and proposer preparation messages to all beacon nodes provided in the \ `--beacon-nodes flag`. This option changes that behaviour such that these \ api calls only go out to the first available and synced beacon node") - .takes_value(false) + .takes_value(false), ) - // This argument is deprecated, use `--beacon-nodes` instead. .arg( - Arg::with_name("server") - .long("server") - .value_name("NETWORK_ADDRESS") - .help("Deprecated. Use --beacon-nodes.") - .takes_value(true) - .conflicts_with_all(&["beacon-node", "beacon-nodes"]), + Arg::with_name("broadcast") + .long("broadcast") + .value_name("API_TOPICS") + .help("Comma-separated list of beacon API topics to broadcast to all beacon nodes. \ + Possible values are: none, attestations, blocks, subscriptions, \ + sync-committee. Default (when flag is omitted) is to broadcast \ + subscriptions only." + ) + .takes_value(true), ) .arg( Arg::with_name("validators-dir") @@ -80,13 +75,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .takes_value(true) .conflicts_with("datadir") ) - .arg( - Arg::with_name("delete-lockfiles") - .long("delete-lockfiles") - .help( - "DEPRECATED. This flag does nothing and will be removed in a future release." - ) - ) .arg( Arg::with_name("init-slashing-protection") .long("init-slashing-protection") @@ -106,11 +94,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { will need to be manually added to the validator_definitions.yml file." ) ) - .arg( - Arg::with_name("allow-unsynced") - .long("allow-unsynced") - .help("DEPRECATED: this flag does nothing"), - ) .arg( Arg::with_name("use-long-timeouts") .long("use-long-timeouts") @@ -319,18 +302,6 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { headers during proposals and will sign over headers. Useful for outsourcing \ execution payload construction during proposals.") .takes_value(false), - ).arg( - Arg::with_name("strict-fee-recipient") - .long("strict-fee-recipient") - .help("[DEPRECATED] If this flag is set, Lighthouse will refuse to sign any block whose \ - `fee_recipient` does not match the `suggested_fee_recipient` sent by this validator. \ - This applies to both the normal block proposal flow, as well as block proposals \ - through the builder API. Proposals through the builder API are more likely to have a \ - discrepancy in `fee_recipient` so you should be aware of how your connected relay \ - sends proposer payments before using this flag. If this flag is used, a fee recipient \ - mismatch in the builder API flow will result in a fallback to the local execution engine \ - for payload construction, where a strict fee recipient check will still be applied.") - .takes_value(false), ) .arg( Arg::with_name("builder-registration-timestamp-override") @@ -395,16 +366,4 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { Beacon Nodes. The range falls immediately after the end of the `small` range.") .takes_value(true) ) - /* - * Experimental/development options. - */ - .arg( - Arg::with_name("block-delay-ms") - .long("block-delay-ms") - .value_name("MILLIS") - .hidden(true) - .help("Time to delay block production from the start of the slot. Should only be \ - used for testing.") - .takes_value(true), - ) } diff --git a/validator_client/src/config.rs b/validator_client/src/config.rs index ea4c4f0465..f368c88750 100644 --- a/validator_client/src/config.rs +++ b/validator_client/src/config.rs @@ -1,3 +1,4 @@ +use crate::beacon_node_fallback::ApiTopic; use crate::graffiti_file::GraffitiFile; use crate::{beacon_node_fallback, http_api, http_metrics}; use clap::ArgMatches; @@ -13,7 +14,6 @@ use slog::{info, warn, Logger}; use std::fs; use std::net::IpAddr; use std::path::PathBuf; -use std::time::Duration; use types::{Address, GRAFFITI_BYTES_LEN}; pub const DEFAULT_BEACON_NODE: &str = "http://localhost:5052/"; @@ -71,10 +71,8 @@ pub struct Config { /// A list of custom certificates that the validator client will additionally use when /// connecting to a beacon node over SSL/TLS. pub beacon_nodes_tls_certs: Option>, - /// Delay from the start of the slot to wait before publishing a block. - /// - /// This is *not* recommended in prod and should only be used for testing. - pub block_delay: Option, + /// Enables broadcasting of various requests (by topic) to all beacon nodes. + pub broadcast_topics: Vec, /// Enables a service which attempts to measure latency between the VC and BNs. pub enable_latency_measurement_service: bool, /// Defines the number of validators per `validator/register_validator` request sent to the BN. @@ -114,10 +112,10 @@ impl Default for Config { enable_doppelganger_protection: false, enable_high_validator_count_metrics: false, beacon_nodes_tls_certs: None, - block_delay: None, builder_proposals: false, builder_registration_timestamp_override: None, gas_limit: None, + broadcast_topics: vec![ApiTopic::Subscriptions], enable_latency_measurement_service: true, validator_registration_batch_size: 500, } @@ -171,27 +169,6 @@ impl Config { .collect::>() .map_err(|e| format!("Unable to parse beacon node URL: {:?}", e))?; } - // To be deprecated. - else if let Some(beacon_node) = parse_optional::(cli_args, "beacon-node")? { - warn!( - log, - "The --beacon-node flag is deprecated"; - "msg" => "please use --beacon-nodes instead" - ); - config.beacon_nodes = vec![SensitiveUrl::parse(&beacon_node) - .map_err(|e| format!("Unable to parse beacon node URL: {:?}", e))?]; - } - // To be deprecated. - else if let Some(server) = parse_optional::(cli_args, "server")? { - warn!( - log, - "The --server flag is deprecated"; - "msg" => "please use --beacon-nodes instead" - ); - config.beacon_nodes = vec![SensitiveUrl::parse(&server) - .map_err(|e| format!("Unable to parse beacon node URL: {:?}", e))?]; - } - if let Some(proposer_nodes) = parse_optional::(cli_args, "proposer_nodes")? { config.proposer_nodes = proposer_nodes .split(',') @@ -200,21 +177,6 @@ impl Config { .map_err(|e| format!("Unable to parse proposer node URL: {:?}", e))?; } - if cli_args.is_present("delete-lockfiles") { - warn!( - log, - "The --delete-lockfiles flag is deprecated"; - "msg" => "it is no longer necessary, and no longer has any effect", - ); - } - - if cli_args.is_present("allow-unsynced") { - warn!( - log, - "The --allow-unsynced flag is deprecated"; - "msg" => "it no longer has any effect", - ); - } config.disable_auto_discover = cli_args.is_present("disable-auto-discover"); config.init_slashing_protection = cli_args.is_present("init-slashing-protection"); config.use_long_timeouts = cli_args.is_present("use-long-timeouts"); @@ -257,6 +219,26 @@ impl Config { config.beacon_nodes_tls_certs = Some(tls_certs.split(',').map(PathBuf::from).collect()); } + if cli_args.is_present("disable-run-on-all") { + warn!( + log, + "The --disable-run-on-all flag is deprecated"; + "msg" => "please use --broadcast instead" + ); + config.broadcast_topics = vec![]; + } + if let Some(broadcast_topics) = cli_args.value_of("broadcast") { + config.broadcast_topics = broadcast_topics + .split(',') + .filter(|t| *t != "none") + .map(|t| { + t.trim() + .parse::() + .map_err(|_| format!("Unknown API topic to broadcast: {t}")) + }) + .collect::>()?; + } + /* * Beacon node fallback */ @@ -411,14 +393,6 @@ impl Config { ); } - if cli_args.is_present("strict-fee-recipient") { - warn!( - log, - "The flag `--strict-fee-recipient` has been deprecated due to a bug causing \ - missed proposals. The flag will be ignored." - ); - } - config.enable_latency_measurement_service = parse_optional(cli_args, "latency-measurement-service")?.unwrap_or(true); @@ -428,13 +402,6 @@ impl Config { return Err("validator-registration-batch-size cannot be 0".to_string()); } - /* - * Experimental - */ - if let Some(delay_ms) = parse_optional::(cli_args, "block-delay-ms")? { - config.block_delay = Some(Duration::from_millis(delay_ms)); - } - Ok(config) } } diff --git a/validator_client/src/duties_service.rs b/validator_client/src/duties_service.rs index 6ab3d4969d..863e85529d 100644 --- a/validator_client/src/duties_service.rs +++ b/validator_client/src/duties_service.rs @@ -8,7 +8,7 @@ mod sync; -use crate::beacon_node_fallback::BeaconNodeFallback; +use crate::beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; use crate::http_metrics::metrics::{get_int_gauge, set_int_gauge, ATTESTATION_DUTY}; use crate::{ block_service::BlockServiceNotification, @@ -696,7 +696,7 @@ async fn poll_beacon_attesters( let subscriptions_ref = &subscriptions; if let Err(e) = duties_service .beacon_nodes - .run(|beacon_node| async move { + .request(ApiTopic::Subscriptions, |beacon_node| async move { let _timer = metrics::start_timer_vec( &metrics::DUTIES_SERVICE_TIMES, &[metrics::SUBSCRIPTIONS_HTTP_POST], diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index b45e5ad1ef..63c4c8c42c 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -20,6 +20,7 @@ pub mod http_api; pub mod initialized_validators; pub mod validator_store; +pub use beacon_node_fallback::ApiTopic; pub use cli::cli_app; pub use config::Config; use initialized_validators::InitializedValidators; @@ -373,6 +374,7 @@ impl ProductionValidatorClient { let mut beacon_nodes: BeaconNodeFallback<_, T> = BeaconNodeFallback::new( candidates, config.beacon_node_fallback, + config.broadcast_topics.clone(), context.eth2_config.spec.clone(), log.clone(), ); @@ -380,6 +382,7 @@ impl ProductionValidatorClient { let mut proposer_nodes: BeaconNodeFallback<_, T> = BeaconNodeFallback::new( proposer_candidates, config.beacon_node_fallback, + config.broadcast_topics.clone(), context.eth2_config.spec.clone(), log.clone(), ); @@ -474,8 +477,7 @@ impl ProductionValidatorClient { .beacon_nodes(beacon_nodes.clone()) .runtime_context(context.service_context("block".into())) .graffiti(config.graffiti) - .graffiti_file(config.graffiti_file.clone()) - .block_delay(config.block_delay); + .graffiti_file(config.graffiti_file.clone()); // If we have proposer nodes, add them to the block service builder. if proposer_nodes_num > 0 { diff --git a/validator_client/src/preparation_service.rs b/validator_client/src/preparation_service.rs index c250a124d2..4768575b6f 100644 --- a/validator_client/src/preparation_service.rs +++ b/validator_client/src/preparation_service.rs @@ -1,4 +1,4 @@ -use crate::beacon_node_fallback::BeaconNodeFallback; +use crate::beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; use crate::validator_store::{DoppelgangerStatus, Error as ValidatorStoreError, ValidatorStore}; use bls::PublicKeyBytes; use environment::RuntimeContext; @@ -341,7 +341,7 @@ impl PreparationService { let preparation_entries = preparation_data.as_slice(); match self .beacon_nodes - .run(|beacon_node| async move { + .request(ApiTopic::Subscriptions, |beacon_node| async move { beacon_node .post_validator_prepare_beacon_proposer(preparation_entries) .await diff --git a/validator_client/src/sync_committee_service.rs b/validator_client/src/sync_committee_service.rs index 2560561c81..3f86054f44 100644 --- a/validator_client/src/sync_committee_service.rs +++ b/validator_client/src/sync_committee_service.rs @@ -1,4 +1,4 @@ -use crate::beacon_node_fallback::BeaconNodeFallback; +use crate::beacon_node_fallback::{ApiTopic, BeaconNodeFallback}; use crate::{ duties_service::DutiesService, validator_store::{Error as ValidatorStoreError, ValidatorStore}, @@ -296,7 +296,7 @@ impl SyncCommitteeService { .collect::>(); self.beacon_nodes - .first_success(|beacon_node| async move { + .request(ApiTopic::SyncCommittee, |beacon_node| async move { beacon_node .post_beacon_pool_sync_committee_signatures(committee_signatures) .await @@ -579,7 +579,7 @@ impl SyncCommitteeService { if let Err(e) = self .beacon_nodes - .run(|beacon_node| async move { + .request(ApiTopic::Subscriptions, |beacon_node| async move { beacon_node .post_validator_sync_committee_subscriptions(subscriptions_slice) .await diff --git a/watch/src/blockprint/mod.rs b/watch/src/blockprint/mod.rs index b8107e5bf5..532776f425 100644 --- a/watch/src/blockprint/mod.rs +++ b/watch/src/blockprint/mod.rs @@ -17,7 +17,7 @@ pub use config::Config; pub use database::{ get_blockprint_by_root, get_blockprint_by_slot, get_highest_blockprint, get_lowest_blockprint, get_unknown_blockprint, get_validators_clients_at_slot, insert_batch_blockprint, - list_consensus_clients, WatchBlockprint, + WatchBlockprint, }; pub use server::blockprint_routes; diff --git a/watch/src/database/mod.rs b/watch/src/database/mod.rs index b9a7a900a5..841ebe5ee7 100644 --- a/watch/src/database/mod.rs +++ b/watch/src/database/mod.rs @@ -26,24 +26,29 @@ pub use self::error::Error; pub use self::models::{WatchBeaconBlock, WatchCanonicalSlot, WatchProposerInfo, WatchValidator}; pub use self::watch_types::{WatchHash, WatchPK, WatchSlot}; +// Clippy has false positives on these re-exports from Rust 1.75.0-beta.1. +#[allow(unused_imports)] pub use crate::block_rewards::{ get_block_rewards_by_root, get_block_rewards_by_slot, get_highest_block_rewards, get_lowest_block_rewards, get_unknown_block_rewards, insert_batch_block_rewards, WatchBlockRewards, }; +#[allow(unused_imports)] pub use crate::block_packing::{ get_block_packing_by_root, get_block_packing_by_slot, get_highest_block_packing, get_lowest_block_packing, get_unknown_block_packing, insert_batch_block_packing, WatchBlockPacking, }; +#[allow(unused_imports)] pub use crate::suboptimal_attestations::{ get_all_suboptimal_attestations_for_epoch, get_attestation_by_index, get_attestation_by_pubkey, get_highest_attestation, get_lowest_attestation, insert_batch_suboptimal_attestations, WatchAttestation, WatchSuboptimalAttestation, }; +#[allow(unused_imports)] pub use crate::blockprint::{ get_blockprint_by_root, get_blockprint_by_slot, get_highest_blockprint, get_lowest_blockprint, get_unknown_blockprint, get_validators_clients_at_slot, insert_batch_blockprint,