diff --git a/Cargo.lock b/Cargo.lock index 5a63ab1e72..5a8e76a8a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4217,6 +4217,7 @@ dependencies = [ "parking_lot", "proto_array", "rand 0.9.2", + "reqwest", "safe_arith", "sensitive_url", "serde", @@ -6159,6 +6160,7 @@ dependencies = [ "environment", "eth2", "execution_layer", + "reqwest", "sensitive_url", "tempfile", "tokio", @@ -9688,6 +9690,7 @@ dependencies = [ "graffiti_file", "logging", "parking_lot", + "reqwest", "safe_arith", "slot_clock", "task_executor", @@ -9716,6 +9719,7 @@ dependencies = [ "eth2", "mockito", "regex", + "reqwest", "sensitive_url", "serde_json", "tracing", @@ -9810,6 +9814,7 @@ dependencies = [ "bytes", "eth2", "headers", + "reqwest", "safe_arith", "serde", "serde_array_query", diff --git a/beacon_node/beacon_chain/src/attestation_verification.rs b/beacon_node/beacon_chain/src/attestation_verification.rs index faa396966f..667bafe445 100644 --- a/beacon_node/beacon_chain/src/attestation_verification.rs +++ b/beacon_node/beacon_chain/src/attestation_verification.rs @@ -61,8 +61,9 @@ use tracing::{debug, error}; use tree_hash::TreeHash; use types::{ Attestation, AttestationData, AttestationRef, BeaconCommittee, - BeaconStateError::NoCommitteeFound, ChainSpec, CommitteeIndex, Epoch, EthSpec, Hash256, - IndexedAttestation, SelectionProof, SignedAggregateAndProof, SingleAttestation, Slot, SubnetId, + BeaconStateError::NoCommitteeFound, ChainSpec, CommitteeIndex, Epoch, EthSpec, ForkName, + Hash256, IndexedAttestation, SelectionProof, SignedAggregateAndProof, SingleAttestation, Slot, + SubnetId, }; pub use batch::{batch_verify_aggregated_attestations, batch_verify_unaggregated_attestations}; @@ -160,6 +161,12 @@ pub enum Error { /// /// The peer has sent an invalid message. CommitteeIndexNonZero(usize), + /// The validator index is set to an invalid value after Gloas. + /// + /// ## Peer scoring + /// + /// The peer has sent an invalid message. + CommitteeIndexInvalid, /// The `attestation.data.beacon_block_root` block is unknown. /// /// ## Peer scoring @@ -550,8 +557,12 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { } .tree_hash_root(); + let fork_name = chain + .spec + .fork_name_at_slot::(attestation.data().slot); + // [New in Electra:EIP7549] - verify_committee_index(attestation)?; + verify_committee_index(attestation, fork_name)?; if chain .observed_attestations @@ -595,6 +606,17 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { // attestation and do not delay consideration for later. let head_block = verify_head_block_is_known(chain, attestation.data(), None)?; + // [New in Gloas]: If the attested block is from the same slot as the attestation, + // index must be 0. + if fork_name.gloas_enabled() + && head_block.slot == attestation.data().slot + && attestation.data().index != 0 + { + return Err(Error::CommitteeIndexNonZero( + attestation.data().index as usize, + )); + } + // Check the attestation target root is consistent with the head root. // // This check is not in the specification, however we guard against it since it opens us up @@ -871,7 +893,12 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> { let fork_name = chain .spec .fork_name_at_slot::(attestation.data.slot); - if fork_name.electra_enabled() { + if fork_name.gloas_enabled() { + // [New in Gloas] + if attestation.data.index >= 2 { + return Err(Error::CommitteeIndexInvalid); + } + } else if fork_name.electra_enabled() { // [New in Electra:EIP7549] if attestation.data.index != 0 { return Err(Error::CommitteeIndexNonZero( @@ -890,6 +917,17 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> { chain.config.import_max_skip_slots, )?; + // [New in Gloas]: If the attested block is from the same slot as the attestation, + // index must be 0. + if fork_name.gloas_enabled() + && head_block.slot == attestation.data.slot + && attestation.data.index != 0 + { + return Err(Error::CommitteeIndexNonZero( + attestation.data.index as usize, + )); + } + // Check the attestation target root is consistent with the head root. verify_attestation_target_root::(&head_block, &attestation.data)?; @@ -1404,7 +1442,10 @@ pub fn verify_signed_aggregate_signatures( /// Verify that the `attestation` committee index is properly set for the attestation's fork. /// This function will only apply verification post-Electra. -pub fn verify_committee_index(attestation: AttestationRef) -> Result<(), Error> { +pub fn verify_committee_index( + attestation: AttestationRef, + fork_name: ForkName, +) -> Result<(), Error> { if let Ok(committee_bits) = attestation.committee_bits() { // Check to ensure that the attestation is for a single committee. let num_committee_bits = get_committee_indices::(committee_bits); @@ -1414,11 +1455,18 @@ pub fn verify_committee_index(attestation: AttestationRef) -> Res )); } - // Ensure the attestation index is set to zero post Electra. - if attestation.data().index != 0 { - return Err(Error::CommitteeIndexNonZero( - attestation.data().index as usize, - )); + // Ensure the attestation index is valid for the fork. + let index = attestation.data().index; + if fork_name.gloas_enabled() { + // [New in Gloas]: index must be < 2. + if index >= 2 { + return Err(Error::CommitteeIndexInvalid); + } + } else { + // [New in Electra:EIP7549]: index must be 0. + if index != 0 { + return Err(Error::CommitteeIndexNonZero(index as usize)); + } } } Ok(()) diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 4af147addb..1ed5579fea 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -32,7 +32,7 @@ use crate::errors::{BeaconChainError as Error, BlockProductionError}; use crate::events::ServerSentEventHandler; use crate::execution_payload::{NotifyExecutionLayer, PreparePayloadHandle, get_execution_payload}; use crate::fetch_blobs::EngineGetBlobsOutput; -use crate::fork_choice_signal::{ForkChoiceSignalRx, ForkChoiceSignalTx, ForkChoiceWaitResult}; +use crate::fork_choice_signal::{ForkChoiceSignalRx, ForkChoiceSignalTx}; use crate::graffiti_calculator::{GraffitiCalculator, GraffitiSettings}; use crate::kzg_utils::reconstruct_blobs; use crate::light_client_finality_update_verification::{ @@ -60,6 +60,7 @@ use crate::observed_slashable::ObservedSlashable; use crate::payload_envelope_verification::{ EnvelopeError, ExecutedEnvelope, ExecutionPendingEnvelope, }; +use crate::pending_payload_envelopes::PendingPayloadEnvelopes; use crate::persisted_beacon_chain::PersistedBeaconChain; use crate::persisted_custody::persist_custody_context; use crate::persisted_fork_choice::PersistedForkChoice; @@ -239,7 +240,7 @@ pub struct PrePayloadAttributes { /// /// The parent block number is not part of the payload attributes sent to the EL, but *is* /// sent to builders via SSE. - pub parent_block_number: u64, + pub parent_block_number: Option, /// The block root of the block being built upon (same block as fcU `headBlockHash`). pub parent_beacon_block_root: Hash256, } @@ -423,6 +424,9 @@ pub struct BeaconChain { RwLock, T::EthSpec>>, /// Maintains a record of slashable message seen over the gossip network or RPC. pub observed_slashable: RwLock>, + /// Cache of pending execution payload envelopes for local block building. + /// Envelopes are stored here during block production and eventually published. + pub pending_payload_envelopes: RwLock>, /// Maintains a record of which validators have submitted voluntary exits. pub observed_voluntary_exits: Mutex>, /// Maintains a record of which validators we've seen proposer slashings for. @@ -4554,55 +4558,6 @@ impl BeaconChain { Ok(()) } - /// If configured, wait for the fork choice run at the start of the slot to complete. - #[instrument(level = "debug", skip_all)] - fn wait_for_fork_choice_before_block_production( - self: &Arc, - slot: Slot, - ) -> Result<(), BlockProductionError> { - if let Some(rx) = &self.fork_choice_signal_rx { - let current_slot = self - .slot() - .map_err(|_| BlockProductionError::UnableToReadSlot)?; - - let timeout = Duration::from_millis(self.config.fork_choice_before_proposal_timeout_ms); - - if slot == current_slot || slot == current_slot + 1 { - match rx.wait_for_fork_choice(slot, timeout) { - ForkChoiceWaitResult::Success(fc_slot) => { - debug!( - %slot, - fork_choice_slot = %fc_slot, - "Fork choice successfully updated before block production" - ); - } - ForkChoiceWaitResult::Behind(fc_slot) => { - warn!( - fork_choice_slot = %fc_slot, - %slot, - message = "this block may be orphaned", - "Fork choice notifier out of sync with block production" - ); - } - ForkChoiceWaitResult::TimeOut => { - warn!( - message = "this block may be orphaned", - "Timed out waiting for fork choice before proposal" - ); - } - } - } else { - error!( - %slot, - %current_slot, - message = "check clock sync, this block may be orphaned", - "Producing block at incorrect slot" - ); - } - } - Ok(()) - } - pub async fn produce_block_with_verification( self: &Arc, randao_reveal: Signature, @@ -4649,165 +4604,6 @@ impl BeaconChain { .await } - /// Load a beacon state from the database for block production. This is a long-running process - /// that should not be performed in an `async` context. - fn load_state_for_block_production( - self: &Arc, - slot: Slot, - ) -> Result<(BeaconState, Option), BlockProductionError> { - let fork_choice_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_FORK_CHOICE_TIMES); - self.wait_for_fork_choice_before_block_production(slot)?; - drop(fork_choice_timer); - - let state_load_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_STATE_LOAD_TIMES); - - // Atomically read some values from the head whilst avoiding holding cached head `Arc` any - // longer than necessary. - let (head_slot, head_block_root, head_state_root) = { - let head = self.canonical_head.cached_head(); - ( - head.head_slot(), - head.head_block_root(), - head.head_state_root(), - ) - }; - let (state, state_root_opt) = if head_slot < slot { - // Attempt an aggressive re-org if configured and the conditions are right. - if let Some((re_org_state, re_org_state_root)) = - self.get_state_for_re_org(slot, head_slot, head_block_root) - { - info!( - %slot, - head_to_reorg = %head_block_root, - "Proposing block to re-org current head" - ); - (re_org_state, Some(re_org_state_root)) - } else { - // Fetch the head state advanced through to `slot`, which should be present in the - // state cache thanks to the state advance timer. - let (state_root, state) = self - .store - .get_advanced_hot_state(head_block_root, slot, head_state_root) - .map_err(BlockProductionError::FailedToLoadState)? - .ok_or(BlockProductionError::UnableToProduceAtSlot(slot))?; - (state, Some(state_root)) - } - } else { - warn!( - message = "this block is more likely to be orphaned", - %slot, - "Producing block that conflicts with head" - ); - let state = self - .state_at_slot(slot - 1, StateSkipConfig::WithStateRoots) - .map_err(|_| BlockProductionError::UnableToProduceAtSlot(slot))?; - - (state, None) - }; - - drop(state_load_timer); - - Ok((state, state_root_opt)) - } - - /// Fetch the beacon state to use for producing a block if a 1-slot proposer re-org is viable. - /// - /// This function will return `None` if proposer re-orgs are disabled. - #[instrument(skip_all, level = "debug")] - fn get_state_for_re_org( - &self, - slot: Slot, - head_slot: Slot, - canonical_head: Hash256, - ) -> Option<(BeaconState, Hash256)> { - let re_org_head_threshold = self.config.re_org_head_threshold?; - let re_org_parent_threshold = self.config.re_org_parent_threshold?; - - if self.spec.proposer_score_boost.is_none() { - warn!( - reason = "this network does not have proposer boost enabled", - "Ignoring proposer re-org configuration" - ); - return None; - } - - let slot_delay = self - .slot_clock - .seconds_from_current_slot_start() - .or_else(|| { - warn!(error = "unable to read slot clock", "Not attempting re-org"); - None - })?; - - // Attempt a proposer re-org if: - // - // 1. It seems we have time to propagate and still receive the proposer boost. - // 2. The current head block was seen late. - // 3. The `get_proposer_head` conditions from fork choice pass. - let proposing_on_time = - slot_delay < self.config.re_org_cutoff(self.spec.get_slot_duration()); - if !proposing_on_time { - debug!(reason = "not proposing on time", "Not attempting re-org"); - return None; - } - - let head_late = self.block_observed_after_attestation_deadline(canonical_head, head_slot); - if !head_late { - debug!(reason = "head not late", "Not attempting re-org"); - return None; - } - - // Is the current head weak and appropriate for re-orging? - let proposer_head_timer = - metrics::start_timer(&metrics::BLOCK_PRODUCTION_GET_PROPOSER_HEAD_TIMES); - let proposer_head = self - .canonical_head - .fork_choice_read_lock() - .get_proposer_head( - slot, - canonical_head, - re_org_head_threshold, - re_org_parent_threshold, - &self.config.re_org_disallowed_offsets, - self.config.re_org_max_epochs_since_finalization, - ) - .map_err(|e| match e { - ProposerHeadError::DoNotReOrg(reason) => { - debug!( - %reason, - "Not attempting re-org" - ); - } - ProposerHeadError::Error(e) => { - warn!( - error = ?e, - "Not attempting re-org" - ); - } - }) - .ok()?; - drop(proposer_head_timer); - let re_org_parent_block = proposer_head.parent_node.root; - - let (state_root, state) = self - .store - .get_advanced_hot_state_from_cache(re_org_parent_block, slot) - .or_else(|| { - warn!(reason = "no state in cache", "Not attempting re-org"); - None - })?; - - info!( - weak_head = ?canonical_head, - parent = ?re_org_parent_block, - head_weight = proposer_head.head_node.weight, - threshold_weight = proposer_head.re_org_head_weight_threshold, - "Attempting re-org due to weak head" - ); - - Some((state, state_root)) - } - /// Get the proposer index and `prev_randao` value for a proposal at slot `proposal_slot`. /// /// The `proposer_head` may be the head block of `cached_head` or its parent. An error will @@ -4890,15 +4686,25 @@ impl BeaconChain { return Ok(None); }; - // Get the `prev_randao` and parent block number. - let head_block_number = cached_head.head_block_number()?; - let (prev_randao, parent_block_number) = if proposer_head == head_parent_block_root { - ( - cached_head.parent_random()?, - head_block_number.saturating_sub(1), - ) + // TODO(gloas) not sure what to do here see this issue + // https://github.com/sigp/lighthouse/issues/8817 + let (prev_randao, parent_block_number) = if self + .spec + .fork_name_at_slot::(proposal_slot) + .gloas_enabled() + { + (cached_head.head_random()?, None) } else { - (cached_head.head_random()?, head_block_number) + // Get the `prev_randao` and parent block number. + let head_block_number = cached_head.head_block_number()?; + if proposer_head == head_parent_block_root { + ( + cached_head.parent_random()?, + Some(head_block_number.saturating_sub(1)), + ) + } else { + (cached_head.head_random()?, Some(head_block_number)) + } }; Ok(Some(PrePayloadAttributes { @@ -5143,7 +4949,11 @@ impl BeaconChain { } /// Check if the block with `block_root` was observed after the attestation deadline of `slot`. - fn block_observed_after_attestation_deadline(&self, block_root: Hash256, slot: Slot) -> bool { + pub(crate) fn block_observed_after_attestation_deadline( + &self, + block_root: Hash256, + slot: Slot, + ) -> bool { let block_delays = self.block_times_cache.read().get_block_delays( block_root, self.slot_clock @@ -5502,7 +5312,7 @@ impl BeaconChain { err = ?e, block_slot = %state.slot(), ?exit, - "Attempted to include an invalid proposer slashing" + "Attempted to include an invalid voluntary exit" ); }) .is_ok() @@ -5910,7 +5720,11 @@ impl BeaconChain { execution_payload_value, ) } - BeaconState::Gloas(_) => return Err(BlockProductionError::GloasNotImplemented), + BeaconState::Gloas(_) => { + return Err(BlockProductionError::GloasNotImplemented( + "Attempting to produce gloas beacon block via non gloas code path".to_owned(), + )); + } }; let block = SignedBeaconBlock::from_block( @@ -6248,13 +6062,14 @@ impl BeaconChain { // Push a server-sent event (probably to a block builder or relay). if let Some(event_handler) = &self.event_handler && event_handler.has_payload_attributes_subscribers() + && let Some(parent_block_number) = pre_payload_attributes.parent_block_number { event_handler.register(EventKind::PayloadAttributes(ForkVersionedResponse { data: SseExtendedPayloadAttributes { proposal_slot: prepare_slot, proposer_index: proposer, parent_block_root: head_root, - parent_block_number: pre_payload_attributes.parent_block_number, + parent_block_number, parent_block_hash: forkchoice_update_params.head_hash.unwrap_or_default(), payload_attributes: payload_attributes.into(), }, diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs new file mode 100644 index 0000000000..607090c59d --- /dev/null +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -0,0 +1,862 @@ +use std::collections::HashMap; +use std::marker::PhantomData; +use std::sync::Arc; + +use bls::Signature; +use execution_layer::{ + BlockProposalContentsGloas, BuilderParams, PayloadAttributes, PayloadParameters, +}; +use operation_pool::CompactAttestationRef; +use ssz::Encode; +use state_processing::common::get_attesting_indices_from_state; +use state_processing::envelope_processing::{VerifyStateRoot, process_execution_payload_envelope}; +use state_processing::epoch_cache::initialize_epoch_cache; +use state_processing::per_block_processing::{ + compute_timestamp_at_slot, get_expected_withdrawals, verify_attestation_for_block_inclusion, +}; +use state_processing::{ + BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot, VerifySignatures, +}; +use state_processing::{VerifyOperation, state_advance::complete_state_advance}; +use task_executor::JoinHandle; +use tracing::{Instrument, Span, debug, debug_span, error, instrument, trace, warn}; +use tree_hash::TreeHash; +use types::consts::gloas::BUILDER_INDEX_SELF_BUILD; +use types::{ + Address, Attestation, AttestationElectra, AttesterSlashing, AttesterSlashingElectra, + BeaconBlock, BeaconBlockBodyGloas, BeaconBlockGloas, BeaconState, BeaconStateError, + BuilderIndex, Deposit, Eth1Data, EthSpec, ExecutionBlockHash, ExecutionPayloadBid, + ExecutionPayloadEnvelope, ExecutionPayloadGloas, ExecutionRequests, FullPayload, Graffiti, + Hash256, PayloadAttestation, ProposerSlashing, RelativeEpoch, SignedBeaconBlock, + SignedBlsToExecutionChange, SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, + SignedVoluntaryExit, Slot, SyncAggregate, Withdrawal, Withdrawals, +}; + +use crate::{ + BeaconChain, BeaconChainError, BeaconChainTypes, BlockProductionError, + ProduceBlockVerification, graffiti_calculator::GraffitiSettings, metrics, +}; + +pub const BID_VALUE_SELF_BUILD: u64 = 0; +pub const EXECUTION_PAYMENT_TRUSTLESS_BUILD: u64 = 0; + +type ConsensusBlockValue = u64; +type BlockProductionResult = (BeaconBlock>, ConsensusBlockValue); + +pub type PreparePayloadResult = Result, BlockProductionError>; +pub type PreparePayloadHandle = JoinHandle>>; + +pub struct PartialBeaconBlock { + slot: Slot, + proposer_index: u64, + parent_root: Hash256, + randao_reveal: Signature, + eth1_data: Eth1Data, + graffiti: Graffiti, + proposer_slashings: Vec, + attester_slashings: Vec>, + attestations: Vec>, + payload_attestations: Vec>, + deposits: Vec, + voluntary_exits: Vec, + sync_aggregate: SyncAggregate, + bls_to_execution_changes: Vec, +} + +/// Data needed to construct an ExecutionPayloadEnvelope. +/// The envelope requires the beacon_block_root which can only be computed after the block exists. +pub struct ExecutionPayloadData { + pub payload: ExecutionPayloadGloas, + pub execution_requests: ExecutionRequests, + pub builder_index: BuilderIndex, + pub slot: Slot, +} + +impl BeaconChain { + pub async fn produce_block_with_verification_gloas( + self: &Arc, + randao_reveal: Signature, + slot: Slot, + graffiti_settings: GraffitiSettings, + verification: ProduceBlockVerification, + _builder_boost_factor: Option, + ) -> 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. + let chain = self.clone(); + let span = Span::current(); + let (state, state_root_opt) = self + .task_executor + .spawn_blocking_handle( + move || { + let _guard = + debug_span!(parent: span, "load_state_for_block_production").entered(); + chain.load_state_for_block_production(slot) + }, + "load_state_for_block_production", + ) + .ok_or(BlockProductionError::ShuttingDown)? + .await + .map_err(BlockProductionError::TokioJoin)??; + + // Part 2/2 (async, with some blocking components) + // + // Produce the block upon the state + self.produce_block_on_state_gloas( + state, + state_root_opt, + slot, + randao_reveal, + graffiti_settings, + verification, + ) + .await + } + + // TODO(gloas) need to implement builder boost factor logic + #[instrument(level = "debug", skip_all)] + pub async fn produce_block_on_state_gloas( + self: &Arc, + state: BeaconState, + state_root_opt: Option, + produce_at_slot: Slot, + randao_reveal: Signature, + graffiti_settings: GraffitiSettings, + verification: ProduceBlockVerification, + ) -> Result, BlockProductionError> { + // Part 1/3 (blocking) + // + // Perform the state advance and block-packing functions. + let chain = self.clone(); + let graffiti = self + .graffiti_calculator + .get_graffiti(graffiti_settings) + .await; + let span = Span::current(); + let (partial_beacon_block, state) = self + .task_executor + .spawn_blocking_handle( + move || { + let _guard = + debug_span!(parent: span, "produce_partial_beacon_block_gloas").entered(); + chain.produce_partial_beacon_block_gloas( + state, + state_root_opt, + produce_at_slot, + randao_reveal, + graffiti, + ) + }, + "produce_partial_beacon_block_gloas", + ) + .ok_or(BlockProductionError::ShuttingDown)? + .await + .map_err(BlockProductionError::TokioJoin)??; + + // Part 2/3 (async) + // + // Produce the execution payload bid. + // TODO(gloas) this is strictly for building local bids + // We'll need to build out trustless/trusted bid paths. + let (execution_payload_bid, state, payload_data) = self + .clone() + .produce_execution_payload_bid( + state, + produce_at_slot, + BID_VALUE_SELF_BUILD, + BUILDER_INDEX_SELF_BUILD, + ) + .await?; + + // Part 3/3 (blocking) + // + // Complete the block with the execution payload bid. + let chain = self.clone(); + let span = Span::current(); + self.task_executor + .spawn_blocking_handle( + move || { + let _guard = + debug_span!(parent: span, "complete_partial_beacon_block_gloas").entered(); + chain.complete_partial_beacon_block_gloas( + partial_beacon_block, + execution_payload_bid, + payload_data, + state, + verification, + ) + }, + "complete_partial_beacon_block_gloas", + ) + .ok_or(BlockProductionError::ShuttingDown)? + .await + .map_err(BlockProductionError::TokioJoin)? + } + + #[allow(clippy::too_many_arguments)] + #[allow(clippy::type_complexity)] + fn produce_partial_beacon_block_gloas( + self: &Arc, + mut state: BeaconState, + state_root_opt: Option, + produce_at_slot: Slot, + randao_reveal: Signature, + graffiti: Graffiti, + ) -> Result<(PartialBeaconBlock, BeaconState), BlockProductionError> + { + // It is invalid to try to produce a block using a state from a future slot. + if state.slot() > produce_at_slot { + return Err(BlockProductionError::StateSlotTooHigh { + produce_at_slot, + state_slot: state.slot(), + }); + } + + let slot_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_SLOT_PROCESS_TIMES); + + // Ensure the state has performed a complete transition into the required slot. + complete_state_advance(&mut state, state_root_opt, produce_at_slot, &self.spec)?; + + drop(slot_timer); + + state.build_committee_cache(RelativeEpoch::Current, &self.spec)?; + state.apply_pending_mutations()?; + + let parent_root = if state.slot() > 0 { + *state + .get_block_root(state.slot() - 1) + .map_err(|_| BlockProductionError::UnableToGetBlockRootFromState)? + } else { + state.latest_block_header().canonical_root() + }; + + let proposer_index = state.get_beacon_proposer_index(state.slot(), &self.spec)? as u64; + + let slashings_and_exits_span = debug_span!("get_slashings_and_exits").entered(); + let (mut proposer_slashings, mut attester_slashings, mut voluntary_exits) = + self.op_pool.get_slashings_and_exits(&state, &self.spec); + + drop(slashings_and_exits_span); + + let eth1_data = state.eth1_data().clone(); + + let deposits = vec![]; + + let bls_changes_span = debug_span!("get_bls_to_execution_changes").entered(); + let bls_to_execution_changes = self + .op_pool + .get_bls_to_execution_changes(&state, &self.spec); + drop(bls_changes_span); + + // Iterate through the naive aggregation pool and ensure all the attestations from there + // are included in the operation pool. + { + let _guard = debug_span!("import_naive_aggregation_pool").entered(); + let _unagg_import_timer = + metrics::start_timer(&metrics::BLOCK_PRODUCTION_UNAGGREGATED_TIMES); + for attestation in self.naive_aggregation_pool.read().iter() { + let import = |attestation: &Attestation| { + let attesting_indices = + get_attesting_indices_from_state(&state, attestation.to_ref())?; + self.op_pool + .insert_attestation(attestation.clone(), attesting_indices) + }; + if let Err(e) = import(attestation) { + // Don't stop block production if there's an error, just create a log. + error!( + reason = ?e, + "Attestation did not transfer to op pool" + ); + } + } + }; + + let mut attestations = { + let _guard = debug_span!("pack_attestations").entered(); + let _attestation_packing_timer = + metrics::start_timer(&metrics::BLOCK_PRODUCTION_ATTESTATION_TIMES); + + // Epoch cache and total balance cache are required for op pool packing. + state.build_total_active_balance_cache(&self.spec)?; + initialize_epoch_cache(&mut state, &self.spec)?; + + let mut prev_filter_cache = HashMap::new(); + let prev_attestation_filter = |att: &CompactAttestationRef| { + self.filter_op_pool_attestation(&mut prev_filter_cache, att, &state) + }; + let mut curr_filter_cache = HashMap::new(); + let curr_attestation_filter = |att: &CompactAttestationRef| { + self.filter_op_pool_attestation(&mut curr_filter_cache, att, &state) + }; + + self.op_pool + .get_attestations( + &state, + prev_attestation_filter, + curr_attestation_filter, + &self.spec, + ) + .map_err(BlockProductionError::OpPoolError)? + }; + + // If paranoid mode is enabled re-check the signatures of every included message. + // This will be a lot slower but guards against bugs in block production and can be + // quickly rolled out without a release. + if self.config.paranoid_block_proposal { + let mut tmp_ctxt = ConsensusContext::new(state.slot()); + attestations.retain(|att| { + verify_attestation_for_block_inclusion( + &state, + att.to_ref(), + &mut tmp_ctxt, + VerifySignatures::True, + &self.spec, + ) + .map_err(|e| { + warn!( + err = ?e, + block_slot = %state.slot(), + attestation = ?att, + "Attempted to include an invalid attestation" + ); + }) + .is_ok() + }); + + proposer_slashings.retain(|slashing| { + slashing + .clone() + .validate(&state, &self.spec) + .map_err(|e| { + warn!( + err = ?e, + block_slot = %state.slot(), + ?slashing, + "Attempted to include an invalid proposer slashing" + ); + }) + .is_ok() + }); + + attester_slashings.retain(|slashing| { + slashing + .clone() + .validate(&state, &self.spec) + .map_err(|e| { + warn!( + err = ?e, + block_slot = %state.slot(), + ?slashing, + "Attempted to include an invalid attester slashing" + ); + }) + .is_ok() + }); + + voluntary_exits.retain(|exit| { + exit.clone() + .validate(&state, &self.spec) + .map_err(|e| { + warn!( + err = ?e, + block_slot = %state.slot(), + ?exit, + "Attempted to include an invalid voluntary exit" + ); + }) + .is_ok() + }); + + // TODO(gloas) verify payload attestation signature here as well + } + + let attester_slashings = attester_slashings + .into_iter() + .filter_map(|a| match a { + AttesterSlashing::Base(_) => None, + AttesterSlashing::Electra(a) => Some(a), + }) + .collect::>(); + + let attestations = attestations + .into_iter() + .filter_map(|a| match a { + Attestation::Base(_) => None, + Attestation::Electra(a) => Some(a), + }) + .collect::>(); + + let slot = state.slot(); + + let sync_aggregate = self + .op_pool + .get_sync_aggregate(&state) + .map_err(BlockProductionError::OpPoolError)? + .unwrap_or_else(|| { + warn!( + slot = %state.slot(), + "Producing block with no sync contributions" + ); + SyncAggregate::new() + }); + + Ok(( + PartialBeaconBlock { + slot, + proposer_index, + parent_root, + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + // TODO(gloas) need to implement payload attestations + payload_attestations: vec![], + bls_to_execution_changes, + }, + state, + )) + } + + #[allow(clippy::type_complexity)] + fn complete_partial_beacon_block_gloas( + &self, + partial_beacon_block: PartialBeaconBlock, + signed_execution_payload_bid: SignedExecutionPayloadBid, + payload_data: Option>, + mut state: BeaconState, + verification: ProduceBlockVerification, + ) -> Result<(BeaconBlock>, u64), BlockProductionError> { + let PartialBeaconBlock { + slot, + proposer_index, + parent_root, + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + payload_attestations, + bls_to_execution_changes, + } = partial_beacon_block; + + let beacon_block = match &state { + BeaconState::Base(_) + | BeaconState::Altair(_) + | BeaconState::Bellatrix(_) + | BeaconState::Capella(_) + | BeaconState::Deneb(_) + | BeaconState::Electra(_) + | BeaconState::Fulu(_) => { + return Err(BlockProductionError::InvalidBlockVariant( + "Cannot construct a block pre-Gloas".to_owned(), + )); + } + BeaconState::Gloas(_) => BeaconBlock::Gloas(BeaconBlockGloas { + slot, + proposer_index, + parent_root, + state_root: Hash256::ZERO, + body: BeaconBlockBodyGloas { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings: proposer_slashings + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attester_slashings: attester_slashings + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + attestations: attestations + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + deposits: deposits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + voluntary_exits: voluntary_exits + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + sync_aggregate, + bls_to_execution_changes: bls_to_execution_changes + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + signed_execution_payload_bid, + payload_attestations: payload_attestations + .try_into() + .map_err(BlockProductionError::SszTypesError)?, + _phantom: PhantomData::>, + }, + }), + }; + + let signed_beacon_block = SignedBeaconBlock::from_block( + beacon_block, + // The block is not signed here, that is the task of a validator client. + Signature::empty(), + ); + + let block_size = signed_beacon_block.ssz_bytes_len(); + debug!(%block_size, "Produced block on state"); + + metrics::observe(&metrics::BLOCK_SIZE, block_size as f64); + + if block_size > self.config.max_network_size { + return Err(BlockProductionError::BlockTooLarge(block_size)); + } + + let process_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_PROCESS_TIMES); + let signature_strategy = match verification { + ProduceBlockVerification::VerifyRandao => BlockSignatureStrategy::VerifyRandao, + ProduceBlockVerification::NoVerification => BlockSignatureStrategy::NoVerification, + }; + + // Use a context without block root or proposer index so that both are checked. + let mut ctxt = ConsensusContext::new(signed_beacon_block.slot()); + + let consensus_block_value = self + .compute_beacon_block_reward(signed_beacon_block.message(), &mut state) + .map(|reward| reward.total) + .unwrap_or(0); + + state_processing::per_block_processing( + &mut state, + &signed_beacon_block, + signature_strategy, + VerifyBlockRoot::True, + &mut ctxt, + &self.spec, + )?; + drop(process_timer); + + let state_root_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_STATE_ROOT_TIMES); + + let state_root = state.update_tree_hash_cache()?; + + drop(state_root_timer); + + let (mut block, _) = signed_beacon_block.deconstruct(); + *block.state_root_mut() = state_root; + + // Construct and cache the ExecutionPayloadEnvelope if we have payload data. + // For local building, we always have payload data. + // For trustless building, the builder will provide the envelope separately. + if let Some(payload_data) = payload_data { + let beacon_block_root = block.tree_hash_root(); + let execution_payload_envelope = ExecutionPayloadEnvelope { + payload: payload_data.payload, + execution_requests: payload_data.execution_requests, + builder_index: payload_data.builder_index, + beacon_block_root, + slot: payload_data.slot, + state_root: Hash256::ZERO, + }; + + let mut signed_envelope = SignedExecutionPayloadEnvelope { + message: execution_payload_envelope, + signature: Signature::empty(), + }; + + // We skip state root verification here because the relevant state root + // cant be calculated until after the new block has been constructed. + process_execution_payload_envelope( + &mut state, + None, + &signed_envelope, + VerifySignatures::False, + VerifyStateRoot::False, + &self.spec, + ) + .map_err(BlockProductionError::EnvelopeProcessingError)?; + + signed_envelope.message.state_root = state.update_tree_hash_cache()?; + + // Cache the envelope for later retrieval by the validator for signing and publishing. + let envelope_slot = payload_data.slot; + // TODO(gloas) might be safer to cache by root instead of by slot. + // We should revisit this once this code path + beacon api spec matures + self.pending_payload_envelopes + .write() + .insert(envelope_slot, signed_envelope.message); + + debug!( + %beacon_block_root, + slot = %envelope_slot, + "Cached pending execution payload envelope" + ); + } + + metrics::inc_counter(&metrics::BLOCK_PRODUCTION_SUCCESSES); + + trace!( + parent = ?block.parent_root(), + attestations = block.body().attestations_len(), + slot = %block.slot(), + "Produced beacon block" + ); + + Ok((block, consensus_block_value)) + } + + // TODO(gloas) introduce `ProposerPreferences` so we can build out trustless + // bid building. Right now this only works for local building. + /// Produce an `ExecutionPayloadBid` for some `slot` upon the given `state`. + /// This function assumes we've already advanced `state`. + /// + /// Returns the signed bid, the state, and optionally the payload data needed to construct + /// the `ExecutionPayloadEnvelope` after the beacon block is created. + /// + /// For local building, payload data is always returned (`Some`). + /// For trustless building, the builder provides the envelope separately, so `None` is returned. + #[allow(clippy::type_complexity)] + #[instrument(level = "debug", skip_all)] + pub async fn produce_execution_payload_bid( + self: Arc, + mut state: BeaconState, + produce_at_slot: Slot, + bid_value: u64, + builder_index: BuilderIndex, + ) -> Result< + ( + SignedExecutionPayloadBid, + BeaconState, + Option>, + ), + BlockProductionError, + > { + // TODO(gloas) For non local building, add sanity check on value + // The builder MUST have enough excess balance to fulfill this bid (i.e. `value`) and all pending payments. + + // TODO(gloas) add metrics for execution payload bid production + + let parent_root = if state.slot() > 0 { + *state + .get_block_root(state.slot() - 1) + .map_err(|_| BlockProductionError::UnableToGetBlockRootFromState)? + } else { + state.latest_block_header().canonical_root() + }; + + let proposer_index = state.get_beacon_proposer_index(state.slot(), &self.spec)? as u64; + + let pubkey = state + .validators() + .get(proposer_index as usize) + .map(|v| v.pubkey) + .ok_or(BlockProductionError::BeaconChain(Box::new( + BeaconChainError::ValidatorIndexUnknown(proposer_index as usize), + )))?; + + let builder_params = BuilderParams { + pubkey, + slot: state.slot(), + chain_health: self + .is_healthy(&parent_root) + .map_err(|e| BlockProductionError::BeaconChain(Box::new(e)))?, + }; + + // TODO(gloas) this should be BlockProductionVersion::V4 + // V3 is okay for now as long as we're not connected to a builder + // TODO(gloas) add builder boost factor + let prepare_payload_handle = get_execution_payload_gloas( + self.clone(), + &state, + parent_root, + proposer_index, + builder_params, + )?; + + let block_proposal_contents = prepare_payload_handle + .await + .map_err(BlockProductionError::TokioJoin)? + .ok_or(BlockProductionError::ShuttingDown)??; + + let BlockProposalContentsGloas { + payload, + payload_value: _, + execution_requests, + blob_kzg_commitments, + blobs_and_proofs: _, + } = block_proposal_contents; + + let state_root = state.update_tree_hash_cache()?; + + // TODO(gloas) since we are defaulting to local building, execution payment is 0 + // execution payment should only be set to > 0 for trusted building. + let bid = ExecutionPayloadBid:: { + parent_block_hash: state.latest_block_hash()?.to_owned(), + parent_block_root: state.get_latest_block_root(state_root), + block_hash: payload.block_hash, + prev_randao: payload.prev_randao, + fee_recipient: Address::ZERO, + gas_limit: payload.gas_limit, + builder_index, + slot: produce_at_slot, + value: bid_value, + execution_payment: EXECUTION_PAYMENT_TRUSTLESS_BUILD, + blob_kzg_commitments, + }; + + // Store payload data for envelope construction after block is created + let payload_data = ExecutionPayloadData { + payload, + execution_requests, + builder_index, + slot: produce_at_slot, + }; + + // TODO(gloas) this is only local building + // we'll need to implement builder signature for the trustless path + Ok(( + SignedExecutionPayloadBid { + message: bid, + signature: Signature::infinity().map_err(BlockProductionError::BlsError)?, + }, + state, + // Local building always returns payload data. + // Trustless building would return None here. + Some(payload_data), + )) + } +} + +/// Gets an execution payload for inclusion in a block. +/// +/// ## Errors +/// +/// Will return an error when using a pre-Gloas `state`. Ensure to only run this function +/// after the Gloas fork. +fn get_execution_payload_gloas( + chain: Arc>, + state: &BeaconState, + parent_beacon_block_root: Hash256, + proposer_index: u64, + builder_params: BuilderParams, +) -> Result, BlockProductionError> { + // Compute all required values from the `state` now to avoid needing to pass it into a spawned + // task. + let spec = &chain.spec; + let current_epoch = state.current_epoch(); + let timestamp = + compute_timestamp_at_slot(state, state.slot(), spec).map_err(BeaconStateError::from)?; + let random = *state.get_randao_mix(current_epoch)?; + + let latest_execution_block_hash = *state.latest_block_hash()?; + let latest_gas_limit = state.latest_execution_payload_bid()?.gas_limit; + + let withdrawals = + Withdrawals::::from(get_expected_withdrawals(state, spec)?).into(); + + // Spawn a task to obtain the execution payload from the EL via a series of async calls. The + // `join_handle` can be used to await the result of the function. + let join_handle = chain + .task_executor + .clone() + .spawn_handle( + async move { + prepare_execution_payload::( + &chain, + timestamp, + random, + proposer_index, + latest_execution_block_hash, + latest_gas_limit, + builder_params, + withdrawals, + parent_beacon_block_root, + ) + .await + } + .instrument(debug_span!("prepare_execution_payload")), + "prepare_execution_payload", + ) + .ok_or(BlockProductionError::ShuttingDown)?; + + Ok(join_handle) +} + +/// Prepares an execution payload for inclusion in a block. +/// +/// ## Errors +/// +/// Will return an error when using a pre-Gloas fork `state`. Ensure to only run this function +/// after the Gloas fork. +#[allow(clippy::too_many_arguments)] +async fn prepare_execution_payload( + chain: &Arc>, + timestamp: u64, + random: Hash256, + proposer_index: u64, + parent_block_hash: ExecutionBlockHash, + parent_gas_limit: u64, + builder_params: BuilderParams, + withdrawals: Vec, + parent_beacon_block_root: Hash256, +) -> Result, BlockProductionError> +where + T: BeaconChainTypes, +{ + let spec = &chain.spec; + let fork = spec.fork_name_at_slot::(builder_params.slot); + let execution_layer = chain + .execution_layer + .as_ref() + .ok_or(BlockProductionError::ExecutionLayerMissing)?; + + // Try to obtain the fork choice update parameters from the cached head. + // + // Use a blocking task to interact with the `canonical_head` lock otherwise we risk blocking the + // core `tokio` executor. + let inner_chain = chain.clone(); + let forkchoice_update_params = chain + .spawn_blocking_handle( + move || { + inner_chain + .canonical_head + .cached_head() + .forkchoice_update_parameters() + }, + "prepare_execution_payload_forkchoice_update_params", + ) + .instrument(debug_span!("forkchoice_update_params")) + .await + .map_err(|e| BlockProductionError::BeaconChain(Box::new(e)))?; + + let suggested_fee_recipient = execution_layer + .get_suggested_fee_recipient(proposer_index) + .await; + let payload_attributes = PayloadAttributes::new( + timestamp, + random, + suggested_fee_recipient, + Some(withdrawals), + Some(parent_beacon_block_root), + ); + + let target_gas_limit = execution_layer.get_proposer_gas_limit(proposer_index).await; + let payload_parameters = PayloadParameters { + parent_hash: parent_block_hash, + parent_gas_limit, + proposer_gas_limit: target_gas_limit, + payload_attributes: &payload_attributes, + forkchoice_update_params: &forkchoice_update_params, + current_fork: fork, + }; + + let block_contents = execution_layer + .get_payload_gloas(payload_parameters) + .await + .map_err(BlockProductionError::GetPayloadFailed)?; + + Ok(block_contents) +} diff --git a/beacon_node/beacon_chain/src/block_production/mod.rs b/beacon_node/beacon_chain/src/block_production/mod.rs new file mode 100644 index 0000000000..76c8b77e93 --- /dev/null +++ b/beacon_node/beacon_chain/src/block_production/mod.rs @@ -0,0 +1,223 @@ +use std::{sync::Arc, time::Duration}; + +use proto_array::ProposerHeadError; +use slot_clock::SlotClock; +use tracing::{debug, error, info, instrument, warn}; +use types::{BeaconState, Hash256, Slot}; + +use crate::{ + BeaconChain, BeaconChainTypes, BlockProductionError, StateSkipConfig, + fork_choice_signal::ForkChoiceWaitResult, metrics, +}; + +mod gloas; + +impl BeaconChain { + /// Load a beacon state from the database for block production. This is a long-running process + /// that should not be performed in an `async` context. + pub(crate) fn load_state_for_block_production( + self: &Arc, + slot: Slot, + ) -> Result<(BeaconState, Option), BlockProductionError> { + let fork_choice_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_FORK_CHOICE_TIMES); + self.wait_for_fork_choice_before_block_production(slot)?; + drop(fork_choice_timer); + + let state_load_timer = metrics::start_timer(&metrics::BLOCK_PRODUCTION_STATE_LOAD_TIMES); + + // Atomically read some values from the head whilst avoiding holding cached head `Arc` any + // longer than necessary. + let (head_slot, head_block_root, head_state_root) = { + let head = self.canonical_head.cached_head(); + ( + head.head_slot(), + head.head_block_root(), + head.head_state_root(), + ) + }; + let (state, state_root_opt) = if head_slot < slot { + // Attempt an aggressive re-org if configured and the conditions are right. + if let Some((re_org_state, re_org_state_root)) = + self.get_state_for_re_org(slot, head_slot, head_block_root) + { + info!( + %slot, + head_to_reorg = %head_block_root, + "Proposing block to re-org current head" + ); + (re_org_state, Some(re_org_state_root)) + } else { + // Fetch the head state advanced through to `slot`, which should be present in the + // state cache thanks to the state advance timer. + let (state_root, state) = self + .store + .get_advanced_hot_state(head_block_root, slot, head_state_root) + .map_err(BlockProductionError::FailedToLoadState)? + .ok_or(BlockProductionError::UnableToProduceAtSlot(slot))?; + (state, Some(state_root)) + } + } else { + warn!( + message = "this block is more likely to be orphaned", + %slot, + "Producing block that conflicts with head" + ); + let state = self + .state_at_slot(slot - 1, StateSkipConfig::WithStateRoots) + .map_err(|_| BlockProductionError::UnableToProduceAtSlot(slot))?; + + (state, None) + }; + + drop(state_load_timer); + + Ok((state, state_root_opt)) + } + + /// If configured, wait for the fork choice run at the start of the slot to complete. + #[instrument(level = "debug", skip_all)] + fn wait_for_fork_choice_before_block_production( + self: &Arc, + slot: Slot, + ) -> Result<(), BlockProductionError> { + if let Some(rx) = &self.fork_choice_signal_rx { + let current_slot = self + .slot() + .map_err(|_| BlockProductionError::UnableToReadSlot)?; + + let timeout = Duration::from_millis(self.config.fork_choice_before_proposal_timeout_ms); + + if slot == current_slot || slot == current_slot + 1 { + match rx.wait_for_fork_choice(slot, timeout) { + ForkChoiceWaitResult::Success(fc_slot) => { + debug!( + %slot, + fork_choice_slot = %fc_slot, + "Fork choice successfully updated before block production" + ); + } + ForkChoiceWaitResult::Behind(fc_slot) => { + warn!( + fork_choice_slot = %fc_slot, + %slot, + message = "this block may be orphaned", + "Fork choice notifier out of sync with block production" + ); + } + ForkChoiceWaitResult::TimeOut => { + warn!( + message = "this block may be orphaned", + "Timed out waiting for fork choice before proposal" + ); + } + } + } else { + error!( + %slot, + %current_slot, + message = "check clock sync, this block may be orphaned", + "Producing block at incorrect slot" + ); + } + } + Ok(()) + } + + /// Fetch the beacon state to use for producing a block if a 1-slot proposer re-org is viable. + /// + /// This function will return `None` if proposer re-orgs are disabled. + #[instrument(skip_all, level = "debug")] + fn get_state_for_re_org( + &self, + slot: Slot, + head_slot: Slot, + canonical_head: Hash256, + ) -> Option<(BeaconState, Hash256)> { + let re_org_head_threshold = self.config.re_org_head_threshold?; + let re_org_parent_threshold = self.config.re_org_parent_threshold?; + + if self.spec.proposer_score_boost.is_none() { + warn!( + reason = "this network does not have proposer boost enabled", + "Ignoring proposer re-org configuration" + ); + return None; + } + + let slot_delay = self + .slot_clock + .seconds_from_current_slot_start() + .or_else(|| { + warn!(error = "unable to read slot clock", "Not attempting re-org"); + None + })?; + + // Attempt a proposer re-org if: + // + // 1. It seems we have time to propagate and still receive the proposer boost. + // 2. The current head block was seen late. + // 3. The `get_proposer_head` conditions from fork choice pass. + let proposing_on_time = + slot_delay < self.config.re_org_cutoff(self.spec.get_slot_duration()); + if !proposing_on_time { + debug!(reason = "not proposing on time", "Not attempting re-org"); + return None; + } + + let head_late = self.block_observed_after_attestation_deadline(canonical_head, head_slot); + if !head_late { + debug!(reason = "head not late", "Not attempting re-org"); + return None; + } + + // Is the current head weak and appropriate for re-orging? + let proposer_head_timer = + metrics::start_timer(&metrics::BLOCK_PRODUCTION_GET_PROPOSER_HEAD_TIMES); + let proposer_head = self + .canonical_head + .fork_choice_read_lock() + .get_proposer_head( + slot, + canonical_head, + re_org_head_threshold, + re_org_parent_threshold, + &self.config.re_org_disallowed_offsets, + self.config.re_org_max_epochs_since_finalization, + ) + .map_err(|e| match e { + ProposerHeadError::DoNotReOrg(reason) => { + debug!( + %reason, + "Not attempting re-org" + ); + } + ProposerHeadError::Error(e) => { + warn!( + error = ?e, + "Not attempting re-org" + ); + } + }) + .ok()?; + drop(proposer_head_timer); + let re_org_parent_block = proposer_head.parent_node.root; + + let (state_root, state) = self + .store + .get_advanced_hot_state_from_cache(re_org_parent_block, slot) + .or_else(|| { + warn!(reason = "no state in cache", "Not attempting re-org"); + None + })?; + + info!( + weak_head = ?canonical_head, + parent = ?re_org_parent_block, + head_weight = proposer_head.head_node.weight, + threshold_weight = proposer_head.re_org_head_weight_threshold, + "Attempting re-org due to weak head" + ); + + Some((state, state_root)) + } +} diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index 3a912016dc..aef90f46e3 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -1036,6 +1036,7 @@ where observed_column_sidecars: RwLock::new(ObservedDataSidecars::new(self.spec.clone())), observed_blob_sidecars: RwLock::new(ObservedDataSidecars::new(self.spec.clone())), observed_slashable: <_>::default(), + pending_payload_envelopes: <_>::default(), observed_voluntary_exits: <_>::default(), observed_proposer_slashings: <_>::default(), observed_attester_slashings: <_>::default(), diff --git a/beacon_node/beacon_chain/src/data_column_verification.rs b/beacon_node/beacon_chain/src/data_column_verification.rs index cf3385ec5b..08acfdffa4 100644 --- a/beacon_node/beacon_chain/src/data_column_verification.rs +++ b/beacon_node/beacon_chain/src/data_column_verification.rs @@ -496,7 +496,6 @@ where Ok(()) } -// TODO(gloas) make sure the gloas variant uses the same span name #[instrument( skip_all, name = "validate_data_column_sidecar_for_gossip", diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 816e75fd24..6c8f0d2794 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -16,6 +16,7 @@ use milhouse::Error as MilhouseError; use operation_pool::OpPoolError; use safe_arith::ArithError; use ssz_types::Error as SszTypesError; +use state_processing::envelope_processing::EnvelopeProcessingError; use state_processing::{ BlockProcessingError, BlockReplayError, EpochProcessingError, SlotProcessingError, block_signature_verifier::Error as BlockSignatureVerifierError, @@ -318,8 +319,10 @@ pub enum BlockProductionError { FailedToBuildBlobSidecars(String), MissingExecutionRequests, SszTypesError(ssz_types::Error), + EnvelopeProcessingError(EnvelopeProcessingError), + BlsError(bls::Error), // TODO(gloas): Remove this once Gloas is implemented - GloasNotImplemented, + GloasNotImplemented(String), } easy_from_to!(BlockProcessingError, BlockProductionError); diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 5acc3edadd..21b32141a8 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -9,6 +9,7 @@ pub mod beacon_proposer_cache; mod beacon_snapshot; pub mod bellatrix_readiness; pub mod blob_verification; +mod block_production; pub mod block_reward; mod block_times_cache; mod block_verification; @@ -44,6 +45,7 @@ pub mod observed_data_sidecars; pub mod observed_operations; mod observed_slashable; pub mod payload_envelope_verification; +pub mod pending_payload_envelopes; pub mod persisted_beacon_chain; pub mod persisted_custody; mod persisted_fork_choice; diff --git a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs index 7188793492..f2633e8d5f 100644 --- a/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs +++ b/beacon_node/beacon_chain/src/payload_envelope_verification/import.rs @@ -148,7 +148,10 @@ impl BeaconChain { } // The block failed verification. Err(other) => { - warn!(reason = other.to_string(), "Execution payload envelope rejected"); + warn!( + reason = other.to_string(), + "Execution payload envelope rejected" + ); Err(other) } } diff --git a/beacon_node/beacon_chain/src/pending_payload_envelopes.rs b/beacon_node/beacon_chain/src/pending_payload_envelopes.rs new file mode 100644 index 0000000000..336ab5323f --- /dev/null +++ b/beacon_node/beacon_chain/src/pending_payload_envelopes.rs @@ -0,0 +1,151 @@ +//! Provides the `PendingPayloadEnvelopes` cache for storing execution payload envelopes +//! that have been produced during local block production. +//! +//! For local building, the envelope is created during block production. +//! This cache holds the envelopes temporarily until the validator fetches, signs, +//! and publishes the payload. + +use std::collections::HashMap; +use types::{EthSpec, ExecutionPayloadEnvelope, Slot}; + +/// Cache for pending execution payload envelopes awaiting publishing. +/// +/// Envelopes are keyed by slot and pruned based on slot age. +/// This cache is only used for local building. +pub struct PendingPayloadEnvelopes { + /// Maximum number of slots to keep envelopes before pruning. + max_slot_age: u64, + /// The envelopes, keyed by slot. + envelopes: HashMap>, +} + +impl Default for PendingPayloadEnvelopes { + fn default() -> Self { + Self::new(Self::DEFAULT_MAX_SLOT_AGE) + } +} + +impl PendingPayloadEnvelopes { + /// Default maximum slot age before pruning (2 slots). + pub const DEFAULT_MAX_SLOT_AGE: u64 = 2; + + /// Create a new cache with the specified maximum slot age. + pub fn new(max_slot_age: u64) -> Self { + Self { + max_slot_age, + envelopes: HashMap::new(), + } + } + + /// Insert a pending envelope into the cache. + pub fn insert(&mut self, slot: Slot, envelope: ExecutionPayloadEnvelope) { + // TODO(gloas): we may want to check for duplicates here, which shouldn't be allowed + self.envelopes.insert(slot, envelope); + } + + /// Get a pending envelope by slot. + pub fn get(&self, slot: Slot) -> Option<&ExecutionPayloadEnvelope> { + self.envelopes.get(&slot) + } + + /// Remove and return a pending envelope by slot. + pub fn remove(&mut self, slot: Slot) -> Option> { + self.envelopes.remove(&slot) + } + + /// Check if an envelope exists for the given slot. + pub fn contains(&self, slot: Slot) -> bool { + self.envelopes.contains_key(&slot) + } + + /// Prune envelopes older than `current_slot - max_slot_age`. + /// + /// This removes stale envelopes from blocks that were never published. + // TODO(gloas) implement pruning + pub fn prune(&mut self, current_slot: Slot) { + let min_slot = current_slot.saturating_sub(self.max_slot_age); + self.envelopes.retain(|slot, _| *slot >= min_slot); + } + + /// Returns the number of pending envelopes in the cache. + pub fn len(&self) -> usize { + self.envelopes.len() + } + + /// Returns true if the cache is empty. + pub fn is_empty(&self) -> bool { + self.envelopes.is_empty() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use types::{ExecutionPayloadGloas, ExecutionRequests, Hash256, MainnetEthSpec}; + + type E = MainnetEthSpec; + + fn make_envelope(slot: Slot) -> ExecutionPayloadEnvelope { + ExecutionPayloadEnvelope { + payload: ExecutionPayloadGloas::default(), + execution_requests: ExecutionRequests::default(), + builder_index: 0, + beacon_block_root: Hash256::ZERO, + slot, + state_root: Hash256::ZERO, + } + } + + #[test] + fn insert_and_get() { + let mut cache = PendingPayloadEnvelopes::::default(); + let slot = Slot::new(1); + let envelope = make_envelope(slot); + + assert!(!cache.contains(slot)); + assert_eq!(cache.len(), 0); + + cache.insert(slot, envelope.clone()); + + assert!(cache.contains(slot)); + assert_eq!(cache.len(), 1); + assert_eq!(cache.get(slot), Some(&envelope)); + } + + #[test] + fn remove() { + let mut cache = PendingPayloadEnvelopes::::default(); + let slot = Slot::new(1); + let envelope = make_envelope(slot); + + cache.insert(slot, envelope.clone()); + assert!(cache.contains(slot)); + + let removed = cache.remove(slot); + assert_eq!(removed, Some(envelope)); + assert!(!cache.contains(slot)); + assert_eq!(cache.len(), 0); + } + + #[test] + fn prune_old_envelopes() { + let mut cache = PendingPayloadEnvelopes::::new(2); + + // Insert envelope at slot 5 + let slot_1 = Slot::new(5); + cache.insert(slot_1, make_envelope(slot_1)); + + // Insert envelope at slot 10 + let slot_2 = Slot::new(10); + cache.insert(slot_2, make_envelope(slot_2)); + + assert_eq!(cache.len(), 2); + + // Prune at slot 10 with max_slot_age=2, should keep slots >= 8 + cache.prune(Slot::new(10)); + + assert_eq!(cache.len(), 1); + assert!(!cache.contains(slot_1)); // slot 5 < 8, pruned + assert!(cache.contains(slot_2)); // slot 10 >= 8, kept + } +} diff --git a/beacon_node/beacon_chain/tests/attestation_verification.rs b/beacon_node/beacon_chain/tests/attestation_verification.rs index 8aeb881aa4..96071be89f 100644 --- a/beacon_node/beacon_chain/tests/attestation_verification.rs +++ b/beacon_node/beacon_chain/tests/attestation_verification.rs @@ -368,6 +368,13 @@ impl GossipTester { self.harness.chain.epoch().unwrap() } + pub fn is_gloas(&self) -> bool { + self.harness + .spec + .fork_name_at_slot::(self.valid_attestation.data.slot) + .gloas_enabled() + } + pub fn earliest_valid_attestation_slot(&self) -> Slot { let offset = if self .harness @@ -522,6 +529,44 @@ impl GossipTester { self } + + /// Like `inspect_aggregate_err`, but only runs the check if gloas is enabled. + /// If gloas is not enabled, this is a no-op that returns self. + pub fn inspect_aggregate_err_if_gloas( + self, + desc: &str, + get_attn: G, + inspect_err: I, + ) -> Self + where + G: Fn(&Self, &mut SignedAggregateAndProof), + I: Fn(&Self, AttnError), + { + if self.is_gloas() { + self.inspect_aggregate_err(desc, get_attn, inspect_err) + } else { + self + } + } + + /// Like `inspect_unaggregate_err`, but only runs the check if gloas is enabled. + /// If gloas is not enabled, this is a no-op that returns self. + pub fn inspect_unaggregate_err_if_gloas( + self, + desc: &str, + get_attn: G, + inspect_err: I, + ) -> Self + where + G: Fn(&Self, &mut SingleAttestation, &mut SubnetId, &ChainSpec), + I: Fn(&Self, AttnError), + { + if self.is_gloas() { + self.inspect_unaggregate_err(desc, get_attn, inspect_err) + } else { + self + } + } } /// Tests verification of `SignedAggregateAndProof` from the gossip network. #[tokio::test] @@ -854,6 +899,27 @@ async fn aggregated_gossip_verification() { )) }, ) + /* + * [New in Gloas]: attestation.data.index must be < 2 + */ + .inspect_aggregate_err_if_gloas( + "gloas: aggregate with index >= 2", + |_, a| match a.to_mut() { + SignedAggregateAndProofRefMut::Base(_) => { + panic!("Expected Electra attestation variant"); + } + SignedAggregateAndProofRefMut::Electra(att) => { + att.message.aggregate.data.index = 2; + } + }, + |_, err| { + assert!( + matches!(err, AttnError::CommitteeIndexInvalid), + "expected CommitteeIndexInvalid, got {:?}", + err + ) + }, + ) // NOTE: from here on, the tests are stateful, and rely on the valid attestation having // been seen. .import_valid_aggregate() @@ -1071,6 +1137,22 @@ async fn unaggregated_gossip_verification() { )) }, ) + /* + * [New in Gloas]: attestation.data.index must be < 2 + */ + .inspect_unaggregate_err_if_gloas( + "gloas: attestation with index >= 2", + |_, a, _, _| { + a.data.index = 2; + }, + |_, err| { + assert!( + matches!(err, AttnError::CommitteeIndexInvalid), + "expected CommitteeIndexInvalid, got {:?}", + err + ) + }, + ) // NOTE: from here on, the tests are stateful, and rely on the valid attestation having // been seen. .import_valid_unaggregate() @@ -1700,3 +1782,180 @@ async fn aggregated_attestation_verification_use_head_state_fork() { ); } } + +/// [New in Gloas]: Tests that unaggregated attestations with `data.index == 1` are rejected +/// when `head_block.slot == attestation.data.slot`. +/// +/// This test only runs when `FORK_NAME=gloas` is set with `fork_from_env` feature. +// TODO(EIP-7732): Enable this test once gloas block production works in test harness. +// `state.latest_execution_payload_header()` not available in Gloas. +#[ignore] +#[tokio::test] +async fn gloas_unaggregated_attestation_same_slot_index_must_be_zero() { + let harness = get_harness(VALIDATOR_COUNT); + + // Skip this test if not running with gloas fork + if !harness + .spec + .fork_name_at_epoch(Epoch::new(0)) + .gloas_enabled() + { + return; + } + + // Extend the chain out a few epochs so we have some chain depth to play with. + harness + .extend_chain( + MainnetEthSpec::slots_per_epoch() as usize * 3 - 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Produce a block in the current slot (this creates the same-slot scenario) + harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::SomeValidators(vec![]), + ) + .await; + + let current_slot = harness.chain.slot().expect("should get slot"); + let head = harness.chain.head_snapshot(); + + // Verify head block is in the current slot + assert_eq!( + head.beacon_block.slot(), + current_slot, + "head block should be in current slot for same-slot test" + ); + + // Produce an attestation for the current slot + let (mut attestation, _attester_sk, subnet_id) = + get_valid_unaggregated_attestation(&harness.chain); + + // Verify we have a same-slot scenario + let attested_block_slot = harness + .chain + .canonical_head + .fork_choice_read_lock() + .get_block(&attestation.data.beacon_block_root) + .expect("block should exist") + .slot; + assert_eq!( + attested_block_slot, attestation.data.slot, + "attested block slot should equal attestation slot for same-slot test" + ); + + // index == 1 should be rejected when head_block.slot == attestation.data.slot + attestation.data.index = 1; + let result = harness + .chain + .verify_unaggregated_attestation_for_gossip(&attestation, Some(subnet_id)); + assert!( + matches!(result, Err(AttnError::CommitteeIndexNonZero(_))), + "gloas: attestation with index == 1 when head_block.slot == attestation.data.slot should be rejected, got {:?}", + result.err() + ); +} + +/// [New in Gloas]: Tests that aggregated attestations with `data.index == 1` are rejected +/// when `head_block.slot == attestation.data.slot`. +/// +/// This test only runs when `FORK_NAME=gloas` is set with `fork_from_env` feature. +// TODO(EIP-7732): Enable this test once gloas block production works in test harness. +// `state.latest_execution_payload_header()` not available in Gloas. +#[ignore] +#[tokio::test] +async fn gloas_aggregated_attestation_same_slot_index_must_be_zero() { + let harness = get_harness(VALIDATOR_COUNT); + + // Skip this test if not running with gloas fork + if !harness + .spec + .fork_name_at_epoch(Epoch::new(0)) + .gloas_enabled() + { + return; + } + + // Extend the chain out a few epochs so we have some chain depth to play with. + harness + .extend_chain( + MainnetEthSpec::slots_per_epoch() as usize * 3 - 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::AllValidators, + ) + .await; + + // Produce a block in the current slot (this creates the same-slot scenario) + harness + .extend_chain( + 1, + BlockStrategy::OnCanonicalHead, + AttestationStrategy::SomeValidators(vec![]), + ) + .await; + + let current_slot = harness.chain.slot().expect("should get slot"); + let head = harness.chain.head_snapshot(); + + // Verify head block is in the current slot + assert_eq!( + head.beacon_block.slot(), + current_slot, + "head block should be in current slot for same-slot test" + ); + + // Produce an attestation for the current slot + let (valid_attestation, _attester_sk, _subnet_id) = + get_valid_unaggregated_attestation(&harness.chain); + + // Verify we have a same-slot scenario + let attested_block_slot = harness + .chain + .canonical_head + .fork_choice_read_lock() + .get_block(&valid_attestation.data.beacon_block_root) + .expect("block should exist") + .slot; + assert_eq!( + attested_block_slot, valid_attestation.data.slot, + "attested block slot should equal attestation slot for same-slot test" + ); + + // Convert to aggregate + let committee = head + .beacon_state + .get_beacon_committee(current_slot, valid_attestation.committee_index) + .expect("should get committee"); + let fork_name = harness + .spec + .fork_name_at_slot::(valid_attestation.data.slot); + let aggregate_attestation = + single_attestation_to_attestation(&valid_attestation, committee.committee, fork_name) + .unwrap(); + + let (mut valid_aggregate, _, _) = + get_valid_aggregated_attestation(&harness.chain, aggregate_attestation); + + // index == 1 should be rejected when head_block.slot == attestation.data.slot + match valid_aggregate.to_mut() { + SignedAggregateAndProofRefMut::Base(att) => { + att.message.aggregate.data.index = 1; + } + SignedAggregateAndProofRefMut::Electra(att) => { + att.message.aggregate.data.index = 1; + } + } + + let result = harness + .chain + .verify_aggregated_attestation_for_gossip(&valid_aggregate); + assert!( + matches!(result, Err(AttnError::CommitteeIndexNonZero(_))), + "gloas: aggregate with index == 1 when head_block.slot == attestation.data.slot should be rejected, got {:?}", + result.err() + ); +} diff --git a/beacon_node/builder_client/src/lib.rs b/beacon_node/builder_client/src/lib.rs index b17a824fd7..7dc0cbfc6d 100644 --- a/beacon_node/builder_client/src/lib.rs +++ b/beacon_node/builder_client/src/lib.rs @@ -10,10 +10,10 @@ use eth2::types::{ use eth2::types::{FullPayloadContents, SignedBlindedBeaconBlock}; use eth2::{ CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, JSON_CONTENT_TYPE_HEADER, - SSZ_CONTENT_TYPE_HEADER, StatusCode, ok_or_error, success_or_error, + SSZ_CONTENT_TYPE_HEADER, ok_or_error, success_or_error, }; use reqwest::header::{ACCEPT, HeaderMap, HeaderValue}; -use reqwest::{IntoUrl, Response}; +use reqwest::{IntoUrl, Response, StatusCode}; use sensitive_url::SensitiveUrl; use serde::Serialize; use serde::de::DeserializeOwned; diff --git a/beacon_node/client/src/notifier.rs b/beacon_node/client/src/notifier.rs index 3f01622c35..21a5abeb6c 100644 --- a/beacon_node/client/src/notifier.rs +++ b/beacon_node/client/src/notifier.rs @@ -431,6 +431,16 @@ async fn bellatrix_readiness_logging( current_slot: Slot, beacon_chain: &BeaconChain, ) { + // There is no execution payload in gloas blocks, so this will trigger + // bellatrix readiness logging in gloas if we dont skip the check below + if beacon_chain + .spec + .fork_name_at_slot::(current_slot) + .gloas_enabled() + { + return; + } + let merge_completed = beacon_chain .canonical_head .cached_head() diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 33b83aab09..157fe152ef 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -48,7 +48,6 @@ use tree_hash::TreeHash; use types::builder::BuilderBid; use types::execution::BlockProductionVersion; use types::kzg_ext::KzgCommitments; -use types::new_non_zero_usize; use types::{ AbstractExecPayload, BlobsList, ExecutionPayloadDeneb, ExecutionRequests, KzgProofs, SignedBlindedBeaconBlock, @@ -58,6 +57,7 @@ use types::{ ExecutionPayloadCapella, ExecutionPayloadElectra, ExecutionPayloadFulu, FullPayload, ProposerPreparationData, Slot, }; +use types::{ExecutionPayloadGloas, new_non_zero_usize}; mod block_hash; mod engine_api; @@ -168,6 +168,7 @@ pub enum Error { BeaconStateError(BeaconStateError), PayloadTypeMismatch, VerifyingVersionedHashes(versioned_hashes::Error), + Unexpected(String), } impl From for Error { @@ -204,6 +205,26 @@ pub enum BlockProposalContentsType { Blinded(BlockProposalContents>), } +pub struct BlockProposalContentsGloas { + pub payload: ExecutionPayloadGloas, + pub payload_value: Uint256, + pub blob_kzg_commitments: KzgCommitments, + pub blobs_and_proofs: (BlobsList, KzgProofs), + pub execution_requests: ExecutionRequests, +} + +impl From> for BlockProposalContentsGloas { + fn from(response: GetPayloadResponseGloas) -> Self { + Self { + payload: response.execution_payload, + payload_value: response.block_value, + blob_kzg_commitments: response.blobs_bundle.commitments, + blobs_and_proofs: (response.blobs_bundle.blobs, response.blobs_bundle.proofs), + execution_requests: response.requests, + } + } +} + pub enum BlockProposalContents> { Payload { payload: Payload, @@ -884,6 +905,44 @@ impl ExecutionLayer { .and_then(|entry| entry.gas_limit) } + /// Maps to the `engine_getPayload` JSON-RPC call for post-Gloas payload construction. + /// + /// However, it will attempt to call `self.prepare_payload` if it cannot find an existing + /// payload id for the given parameters. + /// + /// ## Fallback Behavior + /// + /// The result will be returned from the first node that returns successfully. No more nodes + /// will be contacted. + #[instrument(level = "debug", skip_all)] + pub async fn get_payload_gloas( + &self, + payload_parameters: PayloadParameters<'_>, + ) -> Result, Error> { + let payload_response_type = self.get_full_payload_caching(payload_parameters).await?; + let GetPayloadResponseType::Full(payload_response) = payload_response_type else { + return Err(Error::Unexpected( + "get_payload_gloas should never return a blinded payload".to_owned(), + )); + }; + let GetPayloadResponse::Gloas(payload_response) = payload_response else { + return Err(Error::Unexpected( + "get_payload_gloas should always return a gloas `GetPayloadResponse` variant" + .to_owned(), + )); + }; + metrics::inc_counter_vec( + &metrics::EXECUTION_LAYER_GET_PAYLOAD_OUTCOME, + &[metrics::SUCCESS], + ); + metrics::inc_counter_vec( + &metrics::EXECUTION_LAYER_GET_PAYLOAD_SOURCE, + &[metrics::LOCAL], + ); + + Ok(payload_response.into()) + } + /// Maps to the `engine_getPayload` JSON-RPC call. /// /// However, it will attempt to call `self.prepare_payload` if it cannot find an existing 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 464879288b..7b6c4e8310 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -245,7 +245,7 @@ impl BidStuff for BuilderBid { } fn sign_builder_message(&mut self, sk: &SecretKey, spec: &ChainSpec) -> Signature { - let domain = spec.get_builder_domain(); + let domain = spec.get_builder_application_domain(); let message = self.signing_root(domain); sk.sign(message) } diff --git a/beacon_node/http_api/Cargo.toml b/beacon_node/http_api/Cargo.toml index 78e7af71f4..dd15a76c7a 100644 --- a/beacon_node/http_api/Cargo.toml +++ b/beacon_node/http_api/Cargo.toml @@ -33,6 +33,7 @@ operation_pool = { workspace = true } parking_lot = { workspace = true } proto_array = { workspace = true } rand = { workspace = true } +reqwest = { workspace = true } safe_arith = { workspace = true } sensitive_url = { workspace = true } serde = { workspace = true } diff --git a/beacon_node/http_api/src/beacon/execution_payload_envelope.rs b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs new file mode 100644 index 0000000000..81f2ea41ea --- /dev/null +++ b/beacon_node/http_api/src/beacon/execution_payload_envelope.rs @@ -0,0 +1,116 @@ +use crate::task_spawner::{Priority, TaskSpawner}; +use crate::utils::{ChainFilter, EthV1Filter, NetworkTxFilter, ResponseFilter, TaskSpawnerFilter}; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use bytes::Bytes; +use eth2::{CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER}; +use lighthouse_network::PubsubMessage; +use network::NetworkMessage; +use ssz::Decode; +use std::sync::Arc; +use tokio::sync::mpsc::UnboundedSender; +use tracing::{info, warn}; +use types::SignedExecutionPayloadEnvelope; +use warp::{Filter, Rejection, Reply, reply::Response}; + +// POST beacon/execution_payload_envelope (SSZ) +pub(crate) fn post_beacon_execution_payload_envelope_ssz( + eth_v1: EthV1Filter, + task_spawner_filter: TaskSpawnerFilter, + chain_filter: ChainFilter, + network_tx_filter: NetworkTxFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("beacon")) + .and(warp::path("execution_payload_envelope")) + .and(warp::path::end()) + .and(warp::header::exact( + CONTENT_TYPE_HEADER, + SSZ_CONTENT_TYPE_HEADER, + )) + .and(warp::body::bytes()) + .and(task_spawner_filter) + .and(chain_filter) + .and(network_tx_filter) + .then( + |body_bytes: Bytes, + task_spawner: TaskSpawner, + chain: Arc>, + network_tx: UnboundedSender>| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + let envelope = + SignedExecutionPayloadEnvelope::::from_ssz_bytes(&body_bytes) + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("invalid SSZ: {e:?}")) + })?; + publish_execution_payload_envelope(envelope, chain, &network_tx).await + }) + }, + ) + .boxed() +} + +// POST beacon/execution_payload_envelope +pub(crate) fn post_beacon_execution_payload_envelope( + eth_v1: EthV1Filter, + task_spawner_filter: TaskSpawnerFilter, + chain_filter: ChainFilter, + network_tx_filter: NetworkTxFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("beacon")) + .and(warp::path("execution_payload_envelope")) + .and(warp::path::end()) + .and(warp::body::json()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .and(network_tx_filter.clone()) + .then( + |envelope: SignedExecutionPayloadEnvelope, + task_spawner: TaskSpawner, + chain: Arc>, + network_tx: UnboundedSender>| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + publish_execution_payload_envelope(envelope, chain, &network_tx).await + }) + }, + ) + .boxed() +} +/// Publishes a signed execution payload envelope to the network. +pub async fn publish_execution_payload_envelope( + envelope: SignedExecutionPayloadEnvelope, + chain: Arc>, + network_tx: &UnboundedSender>, +) -> Result { + let slot = envelope.message.slot; + let beacon_block_root = envelope.message.beacon_block_root; + + // TODO(gloas): Replace this check once we have gossip validation. + if !chain.spec.is_gloas_scheduled() { + return Err(warp_utils::reject::custom_bad_request( + "Execution payload envelopes are not supported before the Gloas fork".into(), + )); + } + + // TODO(gloas): We should probably add validation here i.e. BroadcastValidation::Gossip + info!( + %slot, + %beacon_block_root, + builder_index = envelope.message.builder_index, + "Publishing signed execution payload envelope to network" + ); + + // Publish to the network + crate::utils::publish_pubsub_message( + network_tx, + PubsubMessage::ExecutionPayload(Box::new(envelope)), + ) + .map_err(|_| { + warn!(%slot, "Failed to publish execution payload envelope to network"); + warp_utils::reject::custom_server_error( + "Unable to publish execution payload envelope to network".into(), + ) + })?; + + Ok(warp::reply().into_response()) +} diff --git a/beacon_node/http_api/src/beacon/mod.rs b/beacon_node/http_api/src/beacon/mod.rs index df5e6eee5c..9ec1c476f6 100644 --- a/beacon_node/http_api/src/beacon/mod.rs +++ b/beacon_node/http_api/src/beacon/mod.rs @@ -1,2 +1,3 @@ +pub mod execution_payload_envelope; pub mod pool; pub mod states; diff --git a/beacon_node/http_api/src/beacon/states.rs b/beacon_node/http_api/src/beacon/states.rs index 828efb86a7..50be7211d8 100644 --- a/beacon_node/http_api/src/beacon/states.rs +++ b/beacon_node/http_api/src/beacon/states.rs @@ -28,7 +28,6 @@ pub fn get_beacon_state_pending_consolidations( beacon_states_path: BeaconStatesPath, ) -> ResponseFilter { beacon_states_path - .clone() .and(warp::path("pending_consolidations")) .and(warp::path::end()) .then( diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index c4b2cded51..92a1ad934d 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -36,6 +36,9 @@ mod validator_inclusion; mod validators; mod version; +use crate::beacon::execution_payload_envelope::{ + post_beacon_execution_payload_envelope, post_beacon_execution_payload_envelope_ssz, +}; use crate::beacon::pool::*; use crate::light_client::{get_light_client_bootstrap, get_light_client_updates}; use crate::utils::{AnyVersionFilter, EthV1Filter}; @@ -50,7 +53,6 @@ use builder_states::get_next_withdrawals; use bytes::Bytes; use context_deserialize::ContextDeserialize; use directory::DEFAULT_ROOT_DIR; -use eth2::StatusCode; use eth2::lighthouse::sync_state::SyncState; use eth2::types::{ self as api_types, BroadcastValidation, EndpointVersion, ForkChoice, ForkChoiceExtraData, @@ -69,6 +71,7 @@ use parking_lot::RwLock; pub use publish_blocks::{ ProvenancedBlock, publish_blinded_block, publish_block, reconstruct_block, }; +use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use slot_clock::SlotClock; use ssz::Encode; @@ -92,6 +95,7 @@ use types::{ BeaconStateError, Checkpoint, ConfigAndPreset, Epoch, EthSpec, ForkName, Hash256, SignedBlindedBeaconBlock, Slot, }; +use validator::execution_payload_envelope::get_validator_execution_payload_envelope; use version::{ ResponseIncludesVersion, V1, V2, add_consensus_version_header, add_ssz_content_type_header, execution_optimistic_finalized_beacon_response, inconsistent_fork_rejection, @@ -1486,6 +1490,22 @@ pub fn serve( let post_beacon_pool_bls_to_execution_changes = post_beacon_pool_bls_to_execution_changes(&network_tx_filter, &beacon_pool_path); + // POST beacon/execution_payload_envelope + let post_beacon_execution_payload_envelope = post_beacon_execution_payload_envelope( + eth_v1.clone(), + task_spawner_filter.clone(), + chain_filter.clone(), + network_tx_filter.clone(), + ); + + // POST beacon/execution_payload_envelope (SSZ) + let post_beacon_execution_payload_envelope_ssz = post_beacon_execution_payload_envelope_ssz( + eth_v1.clone(), + task_spawner_filter.clone(), + chain_filter.clone(), + network_tx_filter.clone(), + ); + let beacon_rewards_path = eth_v1 .clone() .and(warp::path("beacon")) @@ -2444,7 +2464,7 @@ pub fn serve( // GET validator/duties/proposer/{epoch} let get_validator_duties_proposer = get_validator_duties_proposer( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), @@ -2452,7 +2472,7 @@ pub fn serve( // GET validator/blocks/{slot} let get_validator_blocks = get_validator_blocks( - any_version.clone().clone(), + any_version.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), @@ -2460,7 +2480,15 @@ pub fn serve( // GET validator/blinded_blocks/{slot} let get_validator_blinded_blocks = get_validator_blinded_blocks( - eth_v1.clone().clone(), + eth_v1.clone(), + chain_filter.clone(), + not_while_syncing_filter.clone(), + task_spawner_filter.clone(), + ); + + // GET validator/execution_payload_envelope/{slot}/{builder_index} + let get_validator_execution_payload_envelope = get_validator_execution_payload_envelope( + eth_v1.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), @@ -2468,7 +2496,7 @@ pub fn serve( // GET validator/attestation_data?slot,committee_index let get_validator_attestation_data = get_validator_attestation_data( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), @@ -2476,7 +2504,7 @@ pub fn serve( // GET validator/aggregate_attestation?attestation_data_root,slot let get_validator_aggregate_attestation = get_validator_aggregate_attestation( - any_version.clone().clone(), + any_version.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), @@ -2484,7 +2512,7 @@ pub fn serve( // POST validator/duties/attester/{epoch} let post_validator_duties_attester = post_validator_duties_attester( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), @@ -2492,7 +2520,7 @@ pub fn serve( // POST validator/duties/sync/{epoch} let post_validator_duties_sync = post_validator_duties_sync( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), @@ -2500,7 +2528,7 @@ pub fn serve( // GET validator/sync_committee_contribution let get_validator_sync_committee_contribution = get_validator_sync_committee_contribution( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), not_while_syncing_filter.clone(), task_spawner_filter.clone(), @@ -2508,7 +2536,7 @@ pub fn serve( // POST validator/aggregate_and_proofs let post_validator_aggregate_and_proofs = post_validator_aggregate_and_proofs( - any_version.clone().clone(), + any_version.clone(), chain_filter.clone(), network_tx_filter.clone(), not_while_syncing_filter.clone(), @@ -2516,7 +2544,7 @@ pub fn serve( ); let post_validator_contribution_and_proofs = post_validator_contribution_and_proofs( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), network_tx_filter.clone(), not_while_syncing_filter.clone(), @@ -2526,7 +2554,7 @@ pub fn serve( // POST validator/beacon_committee_subscriptions let post_validator_beacon_committee_subscriptions = post_validator_beacon_committee_subscriptions( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), validator_subscription_tx_filter.clone(), task_spawner_filter.clone(), @@ -2534,7 +2562,7 @@ pub fn serve( // POST validator/prepare_beacon_proposer let post_validator_prepare_beacon_proposer = post_validator_prepare_beacon_proposer( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), network_tx_filter.clone(), not_while_syncing_filter.clone(), @@ -2543,13 +2571,13 @@ pub fn serve( // POST validator/register_validator let post_validator_register_validator = post_validator_register_validator( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), task_spawner_filter.clone(), ); // POST validator/sync_committee_subscriptions let post_validator_sync_committee_subscriptions = post_validator_sync_committee_subscriptions( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), validator_subscription_tx_filter.clone(), task_spawner_filter.clone(), @@ -2557,7 +2585,7 @@ pub fn serve( // POST validator/liveness/{epoch} let post_validator_liveness_epoch = post_validator_liveness_epoch( - eth_v1.clone().clone(), + eth_v1.clone(), chain_filter.clone(), task_spawner_filter.clone(), ); @@ -3336,6 +3364,7 @@ pub fn serve( .uor(get_validator_duties_proposer) .uor(get_validator_blocks) .uor(get_validator_blinded_blocks) + .uor(get_validator_execution_payload_envelope) .uor(get_validator_attestation_data) .uor(get_validator_aggregate_attestation) .uor(get_validator_sync_committee_contribution) @@ -3374,7 +3403,8 @@ pub fn serve( post_beacon_blocks_ssz .uor(post_beacon_blocks_v2_ssz) .uor(post_beacon_blinded_blocks_ssz) - .uor(post_beacon_blinded_blocks_v2_ssz), + .uor(post_beacon_blinded_blocks_v2_ssz) + .uor(post_beacon_execution_payload_envelope_ssz), ) .uor(post_beacon_blocks) .uor(post_beacon_blinded_blocks) @@ -3386,6 +3416,7 @@ pub fn serve( .uor(post_beacon_pool_voluntary_exits) .uor(post_beacon_pool_sync_committees) .uor(post_beacon_pool_bls_to_execution_changes) + .uor(post_beacon_execution_payload_envelope) .uor(post_beacon_state_validators) .uor(post_beacon_state_validator_balances) .uor(post_beacon_state_validator_identities) diff --git a/beacon_node/http_api/src/produce_block.rs b/beacon_node/http_api/src/produce_block.rs index 6a549c91ef..607221686f 100644 --- a/beacon_node/http_api/src/produce_block.rs +++ b/beacon_node/http_api/src/produce_block.rs @@ -10,8 +10,8 @@ use beacon_chain::graffiti_calculator::GraffitiSettings; use beacon_chain::{ BeaconBlockResponseWrapper, BeaconChain, BeaconChainTypes, ProduceBlockVerification, }; -use eth2::beacon_response::ForkVersionedResponse; use eth2::types::{self as api_types, ProduceBlockV3Metadata, SkipRandaoVerification}; +use eth2::{beacon_response::ForkVersionedResponse, types::ProduceBlockV4Metadata}; use ssz::Encode; use std::sync::Arc; use tracing::instrument; @@ -43,6 +43,49 @@ pub fn get_randao_verification( Ok(randao_verification) } +#[instrument( + name = "lh_produce_block_v4", + skip_all, + fields(%slot) +)] +pub async fn produce_block_v4( + 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 builder_boost_factor = if query.builder_boost_factor == Some(DEFAULT_BOOST_FACTOR) { + None + } else { + query.builder_boost_factor + }; + + let graffiti_settings = GraffitiSettings::new(query.graffiti, query.graffiti_policy); + + let (block, consensus_block_value) = chain + .produce_block_with_verification_gloas( + randao_reveal, + slot, + graffiti_settings, + randao_verification, + builder_boost_factor, + ) + .await + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!("failed to fetch a block: {:?}", e)) + })?; + + build_response_v4::(block, consensus_block_value, accept_header, &chain.spec) +} + #[instrument( name = "lh_produce_block_v3", skip_all, @@ -87,6 +130,45 @@ pub async fn produce_block_v3( build_response_v3(chain, block_response_type, accept_header) } +pub fn build_response_v4( + block: BeaconBlock>, + consensus_block_value: u64, + accept_header: Option, + spec: &ChainSpec, +) -> Result, warp::Rejection> { + let fork_name = block + .to_ref() + .fork_name(spec) + .map_err(inconsistent_fork_rejection)?; + let consensus_block_value_wei = + Uint256::from(consensus_block_value) * Uint256::from(1_000_000_000u64); + + let metadata = ProduceBlockV4Metadata { + consensus_version: fork_name, + consensus_block_value: consensus_block_value_wei, + }; + + match accept_header { + Some(api_types::Accept::Ssz) => Response::builder() + .status(200) + .body(block.as_ssz_bytes().into()) + .map(|res: Response| add_ssz_content_type_header(res)) + .map(|res: Response| add_consensus_version_header(res, fork_name)) + .map(|res| add_consensus_block_value_header(res, consensus_block_value_wei)) + .map_err(|e| -> warp::Rejection { + warp_utils::reject::custom_server_error(format!("failed to create response: {}", e)) + }), + _ => Ok(warp::reply::json(&ForkVersionedResponse { + version: fork_name, + metadata, + data: block, + }) + .into_response()) + .map(|res| add_consensus_version_header(res, fork_name)) + .map(|res| add_consensus_block_value_header(res, consensus_block_value_wei)), + } +} + pub fn build_response_v3( chain: Arc>, block_response: BeaconBlockResponseWrapper, diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 7826ec55e1..bbf92a4dda 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -9,18 +9,16 @@ use beacon_chain::{ AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, IntoGossipVerifiedBlock, NotifyExecutionLayer, build_blob_data_column_sidecars, }; -use eth2::{ - StatusCode, - types::{ - BlobsBundle, BroadcastValidation, ErrorMessage, ExecutionPayloadAndBlobs, - FullPayloadContents, PublishBlockRequest, SignedBlockContents, - }, +use eth2::types::{ + BlobsBundle, BroadcastValidation, ErrorMessage, ExecutionPayloadAndBlobs, FullPayloadContents, + PublishBlockRequest, SignedBlockContents, }; use execution_layer::{ProvenancedPayload, SubmitBlindedBlockResponse}; use futures::TryFutureExt; use lighthouse_network::PubsubMessage; use network::NetworkMessage; use rand::prelude::SliceRandom; +use reqwest::StatusCode; use std::marker::PhantomData; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; diff --git a/beacon_node/http_api/src/validator/execution_payload_envelope.rs b/beacon_node/http_api/src/validator/execution_payload_envelope.rs new file mode 100644 index 0000000000..c40b375e49 --- /dev/null +++ b/beacon_node/http_api/src/validator/execution_payload_envelope.rs @@ -0,0 +1,110 @@ +use crate::task_spawner::{Priority, TaskSpawner}; +use crate::utils::{ + ChainFilter, EthV1Filter, NotWhileSyncingFilter, ResponseFilter, TaskSpawnerFilter, +}; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use eth2::beacon_response::{EmptyMetadata, ForkVersionedResponse}; +use eth2::types::Accept; +use ssz::Encode; +use std::sync::Arc; +use tracing::debug; +use types::Slot; +use warp::http::Response; +use warp::{Filter, Rejection}; + +// GET validator/execution_payload_envelope/{slot}/{builder_index} +pub fn get_validator_execution_payload_envelope( + eth_v1: EthV1Filter, + chain_filter: ChainFilter, + not_while_syncing_filter: NotWhileSyncingFilter, + task_spawner_filter: TaskSpawnerFilter, +) -> ResponseFilter { + eth_v1 + .and(warp::path("validator")) + .and(warp::path("execution_payload_envelope")) + .and(warp::path::param::().or_else(|_| async { + Err(warp_utils::reject::custom_bad_request( + "Invalid slot".to_string(), + )) + })) + .and(warp::path::param::().or_else(|_| async { + Err(warp_utils::reject::custom_bad_request( + "Invalid builder_index".to_string(), + )) + })) + .and(warp::path::end()) + .and(warp::header::optional::("accept")) + .and(not_while_syncing_filter) + .and(task_spawner_filter) + .and(chain_filter) + .then( + |slot: Slot, + // TODO(gloas) we're only doing local building + // we'll need to implement builder index logic + // eventually. + _builder_index: u64, + accept_header: Option, + not_synced_filter: Result<(), Rejection>, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.spawn_async_with_rejection(Priority::P0, async move { + debug!(?slot, "Execution payload envelope request from HTTP API"); + + not_synced_filter?; + + // Get the envelope from the pending cache (local building only) + let envelope = chain + .pending_payload_envelopes + .read() + .get(slot) + .cloned() + .ok_or_else(|| { + warp_utils::reject::custom_not_found(format!( + "Execution payload envelope not available for slot {slot}" + )) + })?; + + let fork_name = chain.spec.fork_name_at_slot::(slot); + + match accept_header { + Some(Accept::Ssz) => Response::builder() + .status(200) + .header("Content-Type", "application/octet-stream") + .header("Eth-Consensus-Version", fork_name.to_string()) + .body(envelope.as_ssz_bytes().into()) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "Failed to build SSZ response: {e}" + )) + }), + _ => { + let json_response = ForkVersionedResponse { + version: fork_name, + metadata: EmptyMetadata {}, + data: envelope, + }; + Response::builder() + .status(200) + .header("Content-Type", "application/json") + .header("Eth-Consensus-Version", fork_name.to_string()) + .body( + serde_json::to_string(&json_response) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "Failed to serialize response: {e}" + )) + })? + .into(), + ) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "Failed to build JSON response: {e}" + )) + }) + } + } + }) + }, + ) + .boxed() +} diff --git a/beacon_node/http_api/src/validator/mod.rs b/beacon_node/http_api/src/validator/mod.rs index b1ab4c648a..df237d9f9b 100644 --- a/beacon_node/http_api/src/validator/mod.rs +++ b/beacon_node/http_api/src/validator/mod.rs @@ -1,4 +1,6 @@ -use crate::produce_block::{produce_blinded_block_v2, produce_block_v2, produce_block_v3}; +use crate::produce_block::{ + produce_blinded_block_v2, produce_block_v2, produce_block_v3, produce_block_v4, +}; use crate::task_spawner::{Priority, TaskSpawner}; use crate::utils::{ AnyVersionFilter, ChainFilter, EthV1Filter, NetworkTxFilter, NotWhileSyncingFilter, @@ -10,7 +12,6 @@ use beacon_chain::attestation_verification::VerifiedAttestation; use beacon_chain::validator_monitor::timestamp_now; use beacon_chain::{AttestationError, BeaconChain, BeaconChainError, BeaconChainTypes}; use bls::PublicKeyBytes; -use eth2::StatusCode; use eth2::types::{ Accept, BeaconCommitteeSubscription, EndpointVersion, Failure, GenericResponse, StandardLivenessResponseData, StateId as CoreStateId, ValidatorAggregateAttestationQuery, @@ -18,6 +19,7 @@ use eth2::types::{ }; use lighthouse_network::PubsubMessage; use network::{NetworkMessage, ValidatorSubscriptionMessage}; +use reqwest::StatusCode; use slot_clock::SlotClock; use std::sync::Arc; use tokio::sync::mpsc::{Sender, UnboundedSender}; @@ -31,6 +33,8 @@ use types::{ use warp::{Filter, Rejection, Reply}; use warp_utils::reject::convert_rejection; +pub mod execution_payload_envelope; + /// 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`. pub fn pubkey_to_validator_index( @@ -316,7 +320,11 @@ pub fn get_validator_blocks( not_synced_filter?; - if endpoint_version == V3 { + // Use V4 block production for Gloas fork + let fork_name = chain.spec.fork_name_at_slot::(slot); + if fork_name.gloas_enabled() { + produce_block_v4(accept_header, chain, slot, query).await + } else if endpoint_version == V3 { produce_block_v3(accept_header, chain, slot, query).await } else { produce_block_v2(accept_header, chain, slot, query).await @@ -662,15 +670,26 @@ pub fn post_validator_prepare_beacon_proposer( ) .await; - chain - .prepare_beacon_proposer(current_slot) - .await - .map_err(|e| { - warp_utils::reject::custom_bad_request(format!( - "error updating proposer preparations: {:?}", - e - )) - })?; + // TODO(gloas): verify this is correct. We skip proposer preparation for + // GLOAS because the execution payload is no longer embedded in the beacon + // block (it's in the payload envelope), so the head block's + // execution_payload() is unavailable. + let next_slot = current_slot + 1; + if !chain + .spec + .fork_name_at_slot::(next_slot) + .gloas_enabled() + { + chain + .prepare_beacon_proposer(current_slot) + .await + .map_err(|e| { + warp_utils::reject::custom_bad_request(format!( + "error updating proposer preparations: {:?}", + e + )) + })?; + } if chain.spec.is_peer_das_scheduled() { let (finalized_beacon_state, _, _) = diff --git a/beacon_node/http_api/tests/broadcast_validation_tests.rs b/beacon_node/http_api/tests/broadcast_validation_tests.rs index 357b78cf41..ef5c508595 100644 --- a/beacon_node/http_api/tests/broadcast_validation_tests.rs +++ b/beacon_node/http_api/tests/broadcast_validation_tests.rs @@ -4,11 +4,11 @@ use beacon_chain::{ GossipVerifiedBlock, IntoGossipVerifiedBlock, WhenSlotSkipped, test_utils::{AttestationStrategy, BlockStrategy}, }; -use eth2::reqwest::{Response, StatusCode}; use eth2::types::{BroadcastValidation, PublishBlockRequest}; use fixed_bytes::FixedBytesExtended; use http_api::test_utils::InteractiveTester; use http_api::{Config, ProvenancedBlock, publish_blinded_block, publish_block, reconstruct_block}; +use reqwest::{Response, StatusCode}; use std::collections::HashSet; use std::sync::Arc; use types::{ColumnIndex, Epoch, EthSpec, ForkName, Hash256, MainnetEthSpec, Slot}; diff --git a/beacon_node/http_api/tests/status_tests.rs b/beacon_node/http_api/tests/status_tests.rs index 556b75cb85..6bca9e51f6 100644 --- a/beacon_node/http_api/tests/status_tests.rs +++ b/beacon_node/http_api/tests/status_tests.rs @@ -3,9 +3,9 @@ use beacon_chain::{ BlockError, test_utils::{AttestationStrategy, BlockStrategy, LightClientStrategy, SyncCommitteeStrategy}, }; -use eth2::StatusCode; use execution_layer::{PayloadStatusV1, PayloadStatusV1Status}; use http_api::test_utils::InteractiveTester; +use reqwest::StatusCode; use types::{EthSpec, ExecPayload, ForkName, MinimalEthSpec, Slot, Uint256}; type E = MinimalEthSpec; diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 367a0e3f05..7e3eb8b980 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -6,15 +6,15 @@ use beacon_chain::{ AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, test_spec, }, }; -use bls::{AggregateSignature, Keypair, PublicKeyBytes, Signature, SignatureBytes}; +use bls::{AggregateSignature, Keypair, PublicKeyBytes, SecretKey, Signature, SignatureBytes}; use eth2::{ BeaconNodeHttpClient, Error, Error::ServerMessage, - StatusCode, Timeouts, + Timeouts, mixin::{RequestAccept, ResponseForkName, ResponseOptional}, - reqwest::{RequestBuilder, Response}, types::{ - BlockId as CoreBlockId, ForkChoiceNode, ProduceBlockV3Response, StateId as CoreStateId, *, + BlockId as CoreBlockId, ForkChoiceNode, ProduceBlockV3Response, ProduceBlockV4Metadata, + StateId as CoreStateId, *, }, }; use execution_layer::expected_gas_limit; @@ -34,6 +34,7 @@ use network::NetworkReceivers; use network_utils::enr_ext::EnrExt; use operation_pool::attestation_storage::CheckpointKey; use proto_array::ExecutionStatus; +use reqwest::{RequestBuilder, Response, StatusCode}; use sensitive_url::SensitiveUrl; use slot_clock::SlotClock; use ssz::BitList; @@ -47,7 +48,8 @@ use tree_hash::TreeHash; use types::ApplicationDomain; use types::{ Domain, EthSpec, ExecutionBlockHash, Hash256, MainnetEthSpec, RelativeEpoch, SelectionProof, - SignedRoot, SingleAttestation, Slot, attestation::AttestationBase, + SignedExecutionPayloadEnvelope, SignedRoot, SingleAttestation, Slot, + attestation::AttestationBase, consts::gloas::BUILDER_INDEX_SELF_BUILD, }; type E = MainnetEthSpec; @@ -3726,6 +3728,229 @@ impl ApiTester { self } + /// Get the proposer secret key and randao reveal for the given slot. + async fn proposer_setup( + &self, + slot: Slot, + epoch: Epoch, + fork: &Fork, + genesis_validators_root: Hash256, + ) -> (SecretKey, SignatureBytes) { + 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() + }; + + (sk, randao_reveal) + } + + /// Assert block metadata and verify the envelope cache. + fn assert_v4_block_metadata( + &self, + block: &BeaconBlock, + metadata: &ProduceBlockV4Metadata, + slot: Slot, + ) { + assert_eq!( + metadata.consensus_version, + block.to_ref().fork_name(&self.chain.spec).unwrap() + ); + assert!(!metadata.consensus_block_value.is_zero()); + + let block_root = block.tree_hash_root(); + let envelope = self + .chain + .pending_payload_envelopes + .read() + .get(slot) + .cloned() + .expect("envelope should exist in pending cache for local building"); + assert_eq!(envelope.beacon_block_root, block_root); + assert_eq!(envelope.slot, slot); + } + + /// Assert envelope fields match the expected block root and slot. + fn assert_envelope_fields( + &self, + envelope: &ExecutionPayloadEnvelope, + block_root: Hash256, + slot: Slot, + ) { + assert_eq!(envelope.beacon_block_root, block_root); + assert_eq!(envelope.slot, slot); + assert_eq!(envelope.builder_index, BUILDER_INDEX_SELF_BUILD); + assert_ne!(envelope.state_root, Hash256::ZERO); + } + + /// Sign an execution payload envelope. + fn sign_envelope( + &self, + envelope: ExecutionPayloadEnvelope, + sk: &SecretKey, + epoch: Epoch, + fork: &Fork, + genesis_validators_root: Hash256, + ) -> SignedExecutionPayloadEnvelope { + let domain = + self.chain + .spec + .get_domain(epoch, Domain::BeaconBuilder, fork, genesis_validators_root); + let signing_root = envelope.signing_root(domain); + let signature = sk.sign(signing_root); + + SignedExecutionPayloadEnvelope { + message: envelope, + signature, + } + } + + /// Test V4 block production (JSON). Only runs if Gloas is scheduled. + pub async fn test_block_production_v4(self) -> Self { + if !self.chain.spec.is_gloas_scheduled() { + return 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 fork_name = self.chain.spec.fork_name_at_slot::(slot); + + if !fork_name.gloas_enabled() { + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + continue; + } + + let (sk, randao_reveal) = self + .proposer_setup(slot, epoch, &fork, genesis_validators_root) + .await; + + let (response, metadata) = self + .client + .get_validator_blocks_v4::(slot, &randao_reveal, None, None, None) + .await + .unwrap(); + let block = response.data; + + self.assert_v4_block_metadata(&block, &metadata, slot); + + let envelope = self + .client + .get_validator_execution_payload_envelope::(slot, BUILDER_INDEX_SELF_BUILD) + .await + .unwrap() + .data; + + self.assert_envelope_fields(&envelope, block.tree_hash_root(), slot); + + let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); + let signed_block_request = + PublishBlockRequest::try_from(Arc::new(signed_block.clone())).unwrap(); + self.client + .post_beacon_blocks_v2(&signed_block_request, None) + .await + .unwrap(); + assert_eq!(self.chain.head_beacon_block(), Arc::new(signed_block)); + + let signed_envelope = + self.sign_envelope(envelope, &sk, epoch, &fork, genesis_validators_root); + self.client + .post_beacon_execution_payload_envelope(&signed_envelope, fork_name) + .await + .unwrap(); + + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + } + + self + } + + /// Test V4 block production (SSZ). Only runs if Gloas is scheduled. + pub async fn test_block_production_v4_ssz(self) -> Self { + if !self.chain.spec.is_gloas_scheduled() { + return 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 fork_name = self.chain.spec.fork_name_at_slot::(slot); + + if !fork_name.gloas_enabled() { + self.chain.slot_clock.set_slot(slot.as_u64() + 1); + continue; + } + + let (sk, randao_reveal) = self + .proposer_setup(slot, epoch, &fork, genesis_validators_root) + .await; + + let (block, metadata) = self + .client + .get_validator_blocks_v4_ssz::(slot, &randao_reveal, None, None, None) + .await + .unwrap(); + + self.assert_v4_block_metadata(&block, &metadata, slot); + + let envelope = self + .client + .get_validator_execution_payload_envelope_ssz::(slot, BUILDER_INDEX_SELF_BUILD) + .await + .unwrap(); + + self.assert_envelope_fields(&envelope, block.tree_hash_root(), slot); + + let signed_block = block.sign(&sk, &fork, genesis_validators_root, &self.chain.spec); + let signed_block_request = + PublishBlockRequest::try_from(Arc::new(signed_block.clone())).unwrap(); + self.client + .post_beacon_blocks_v2_ssz(&signed_block_request, None) + .await + .unwrap(); + assert_eq!(self.chain.head_beacon_block(), Arc::new(signed_block)); + + let signed_envelope = + self.sign_envelope(envelope, &sk, epoch, &fork, genesis_validators_root); + self.client + .post_beacon_execution_payload_envelope_ssz(&signed_envelope, fork_name) + .await + .unwrap(); + + 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(); @@ -7459,6 +7684,22 @@ async fn block_production_v3_ssz_with_skip_slots() { .await; } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn block_production_v4() { + ApiTester::new_with_hard_forks() + .await + .test_block_production_v4() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn block_production_v4_ssz() { + ApiTester::new_with_hard_forks() + .await + .test_block_production_v4_ssz() + .await; +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn blinded_block_production_full_payload_premerge() { ApiTester::new().await.test_blinded_block_production().await; diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index 1dcfea96d9..70871d5936 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -2418,6 +2418,25 @@ impl NetworkBeaconProcessor { "attn_comm_index_non_zero", ); } + AttnError::CommitteeIndexInvalid => { + /* + * The committee index is invalid after Gloas. + * + * The peer has published an invalid consensus message. + */ + debug!( + %peer_id, + block = ?beacon_block_root, + ?attestation_type, + "Committee index invalid" + ); + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); + self.gossip_penalize_peer( + peer_id, + PeerAction::LowToleranceError, + "attn_comm_index_invalid", + ); + } AttnError::UnknownHeadBlock { beacon_block_root } => { trace!( %peer_id, diff --git a/beacon_node/store/src/consensus_context.rs b/beacon_node/store/src/consensus_context.rs deleted file mode 100644 index 281106d9aa..0000000000 --- a/beacon_node/store/src/consensus_context.rs +++ /dev/null @@ -1,65 +0,0 @@ -use ssz_derive::{Decode, Encode}; -use state_processing::ConsensusContext; -use std::collections::HashMap; -use types::{EthSpec, Hash256, IndexedAttestation, Slot}; - -/// The consensus context is stored on disk as part of the data availability overflow cache. -/// -/// We use this separate struct to keep the on-disk format stable in the presence of changes to the -/// in-memory `ConsensusContext`. You MUST NOT change the fields of this struct without -/// superstructing it and implementing a schema migration. -#[derive(Debug, PartialEq, Clone, Encode, Decode)] -pub struct OnDiskConsensusContext { - /// Slot to act as an identifier/safeguard - slot: Slot, - /// Proposer index of the block at `slot`. - proposer_index: Option, - /// Block root of the block at `slot`. - current_block_root: Option, - /// We keep the indexed attestations in the *in-memory* version of this struct so that we don't - /// need to regenerate them if roundtripping via this type *without* going to disk. - /// - /// They are not part of the on-disk format. - #[ssz(skip_serializing, skip_deserializing)] - indexed_attestations: HashMap>, -} - -impl OnDiskConsensusContext { - pub fn from_consensus_context(ctxt: ConsensusContext) -> Self { - // Match exhaustively on fields here so we are forced to *consider* updating the on-disk - // format when the `ConsensusContext` fields change. - let ConsensusContext { - slot, - previous_epoch: _, - current_epoch: _, - proposer_index, - current_block_root, - indexed_attestations, - } = ctxt; - OnDiskConsensusContext { - slot, - proposer_index, - current_block_root, - indexed_attestations, - } - } - - pub fn into_consensus_context(self) -> ConsensusContext { - let OnDiskConsensusContext { - slot, - proposer_index, - current_block_root, - indexed_attestations, - } = self; - - let mut ctxt = ConsensusContext::new(slot); - - if let Some(proposer_index) = proposer_index { - ctxt = ctxt.set_proposer_index(proposer_index); - } - if let Some(block_root) = current_block_root { - ctxt = ctxt.set_current_block_root(block_root); - } - ctxt.set_indexed_attestations(indexed_attestations) - } -} diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index ee9cfce0ec..3363eb800c 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -9,7 +9,6 @@ //! tests for implementation examples. pub mod blob_sidecar_list_from_root; pub mod config; -pub mod consensus_context; pub mod errors; mod forwards_iter; pub mod hdiff; @@ -27,7 +26,6 @@ pub mod iter; pub use self::blob_sidecar_list_from_root::BlobSidecarListFromRoot; pub use self::config::StoreConfig; -pub use self::consensus_context::OnDiskConsensusContext; pub use self::hot_cold_store::{HotColdDB, HotStateSummary, Split}; pub use self::memory_store::MemoryStore; pub use crate::metadata::BlobInfo; diff --git a/common/eth2/src/error.rs b/common/eth2/src/error.rs index 671a617c9e..45f2599493 100644 --- a/common/eth2/src/error.rs +++ b/common/eth2/src/error.rs @@ -102,6 +102,7 @@ impl Error { None } } + #[cfg(feature = "events")] Error::SseEventSource(_) => None, Error::ServerMessage(msg) => StatusCode::try_from(msg.code).ok(), Error::ServerIndexedMessage(msg) => StatusCode::try_from(msg.code).ok(), diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 95744a4137..a5b4f9afdd 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -22,8 +22,6 @@ pub use beacon_response::{ }; pub use self::error::{Error, ok_or_error, success_or_error}; -pub use reqwest; -pub use reqwest::{StatusCode, Url}; pub use sensitive_url::SensitiveUrl; use self::mixin::{RequestAccept, ResponseOptional}; @@ -38,13 +36,13 @@ use futures_util::StreamExt; #[cfg(feature = "network")] use libp2p_identity::PeerId; use reqwest::{ - Body, IntoUrl, RequestBuilder, Response, + Body, IntoUrl, RequestBuilder, Response, StatusCode, Url, header::{HeaderMap, HeaderValue}, }; #[cfg(feature = "events")] use reqwest_eventsource::{Event, RequestBuilderExt}; use serde::{Serialize, de::DeserializeOwned}; -use ssz::Encode; +use ssz::{Decode, Encode}; use std::fmt; use std::future::Future; use std::time::Duration; @@ -52,6 +50,7 @@ use std::time::Duration; pub const V1: EndpointVersion = EndpointVersion(1); pub const V2: EndpointVersion = EndpointVersion(2); pub const V3: EndpointVersion = EndpointVersion(3); +pub const V4: EndpointVersion = EndpointVersion(4); pub const CONSENSUS_VERSION_HEADER: &str = "Eth-Consensus-Version"; pub const EXECUTION_PAYLOAD_BLINDED_HEADER: &str = "Eth-Execution-Payload-Blinded"; @@ -2408,6 +2407,277 @@ impl BeaconNodeHttpClient { opt_response.ok_or(Error::StatusCode(StatusCode::NOT_FOUND)) } + /// returns `GET v4/validator/blocks/{slot}` URL path + pub async fn get_validator_blocks_v4_path( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + skip_randao_verification: SkipRandaoVerification, + builder_booster_factor: Option, + graffiti_policy: Option, + ) -> Result { + let mut path = self.eth_path(V4)?; + + 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", ""); + } + + if let Some(builder_booster_factor) = builder_booster_factor { + path.query_pairs_mut() + .append_pair("builder_boost_factor", &builder_booster_factor.to_string()); + } + + if let Some(GraffitiPolicy::AppendClientVersions) = graffiti_policy { + path.query_pairs_mut() + .append_pair("graffiti_policy", "AppendClientVersions"); + } + + Ok(path) + } + + /// `GET v4/validator/blocks/{slot}` + pub async fn get_validator_blocks_v4( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + builder_booster_factor: Option, + graffiti_policy: Option, + ) -> Result< + ( + ForkVersionedResponse, ProduceBlockV4Metadata>, + ProduceBlockV4Metadata, + ), + Error, + > { + self.get_validator_blocks_v4_modular( + slot, + randao_reveal, + graffiti, + SkipRandaoVerification::No, + builder_booster_factor, + graffiti_policy, + ) + .await + } + + /// `GET v4/validator/blocks/{slot}` + pub async fn get_validator_blocks_v4_modular( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + skip_randao_verification: SkipRandaoVerification, + builder_booster_factor: Option, + graffiti_policy: Option, + ) -> Result< + ( + ForkVersionedResponse, ProduceBlockV4Metadata>, + ProduceBlockV4Metadata, + ), + Error, + > { + let path = self + .get_validator_blocks_v4_path( + slot, + randao_reveal, + graffiti, + skip_randao_verification, + builder_booster_factor, + graffiti_policy, + ) + .await?; + + let opt_result = self + .get_response_with_response_headers( + path, + Accept::Json, + self.timeouts.get_validator_block, + |response, headers| async move { + let header_metadata = ProduceBlockV4Metadata::try_from(&headers) + .map_err(Error::InvalidHeaders)?; + let block_response = response + .json::, ProduceBlockV4Metadata>>() + .await?; + Ok((block_response, header_metadata)) + }, + ) + .await?; + + opt_result.ok_or(Error::StatusCode(StatusCode::NOT_FOUND)) + } + + /// `GET v4/validator/blocks/{slot}` in ssz format + pub async fn get_validator_blocks_v4_ssz( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + builder_booster_factor: Option, + graffiti_policy: Option, + ) -> Result<(BeaconBlock, ProduceBlockV4Metadata), Error> { + self.get_validator_blocks_v4_modular_ssz::( + slot, + randao_reveal, + graffiti, + SkipRandaoVerification::No, + builder_booster_factor, + graffiti_policy, + ) + .await + } + + /// `GET v4/validator/blocks/{slot}` in ssz format + pub async fn get_validator_blocks_v4_modular_ssz( + &self, + slot: Slot, + randao_reveal: &SignatureBytes, + graffiti: Option<&Graffiti>, + skip_randao_verification: SkipRandaoVerification, + builder_booster_factor: Option, + graffiti_policy: Option, + ) -> Result<(BeaconBlock, ProduceBlockV4Metadata), Error> { + let path = self + .get_validator_blocks_v4_path( + slot, + randao_reveal, + graffiti, + skip_randao_verification, + builder_booster_factor, + graffiti_policy, + ) + .await?; + + let opt_response = self + .get_response_with_response_headers( + path, + Accept::Ssz, + self.timeouts.get_validator_block, + |response, headers| async move { + let metadata = ProduceBlockV4Metadata::try_from(&headers) + .map_err(Error::InvalidHeaders)?; + let response_bytes = response.bytes().await?; + + let block = BeaconBlock::from_ssz_bytes_for_fork( + &response_bytes, + metadata.consensus_version, + ) + .map_err(Error::InvalidSsz)?; + + Ok((block, metadata)) + }, + ) + .await?; + + opt_response.ok_or(Error::StatusCode(StatusCode::NOT_FOUND)) + } + + /// `GET v1/validator/execution_payload_envelope/{slot}/{builder_index}` + pub async fn get_validator_execution_payload_envelope( + &self, + slot: Slot, + builder_index: u64, + ) -> Result>, Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("execution_payload_envelope") + .push(&slot.to_string()) + .push(&builder_index.to_string()); + + self.get(path).await + } + + /// `GET v1/validator/execution_payload_envelope/{slot}/{builder_index}` in SSZ format + pub async fn get_validator_execution_payload_envelope_ssz( + &self, + slot: Slot, + builder_index: u64, + ) -> Result, Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("validator") + .push("execution_payload_envelope") + .push(&slot.to_string()) + .push(&builder_index.to_string()); + + let opt_response = self + .get_bytes_opt_accept_header(path, Accept::Ssz, self.timeouts.get_validator_block) + .await?; + + let response_bytes = opt_response.ok_or(Error::StatusCode(StatusCode::NOT_FOUND))?; + + ExecutionPayloadEnvelope::from_ssz_bytes(&response_bytes).map_err(Error::InvalidSsz) + } + + /// `POST v1/beacon/execution_payload_envelope` + pub async fn post_beacon_execution_payload_envelope( + &self, + envelope: &SignedExecutionPayloadEnvelope, + fork_name: ForkName, + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("execution_payload_envelope"); + + self.post_generic_with_consensus_version( + path, + envelope, + Some(self.timeouts.proposal), + fork_name, + ) + .await?; + + Ok(()) + } + + /// `POST v1/beacon/execution_payload_envelope` in SSZ format + pub async fn post_beacon_execution_payload_envelope_ssz( + &self, + envelope: &SignedExecutionPayloadEnvelope, + fork_name: ForkName, + ) -> Result<(), Error> { + let mut path = self.eth_path(V1)?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("beacon") + .push("execution_payload_envelope"); + + self.post_generic_with_consensus_version_and_ssz_body( + path, + envelope.as_ssz_bytes(), + Some(self.timeouts.proposal), + fork_name, + ) + .await?; + + Ok(()) + } + /// `GET v2/validator/blocks/{slot}` in ssz format pub async fn get_validator_blocks_ssz( &self, diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index 8b33a4dfb9..f8376d430c 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -1604,7 +1604,7 @@ pub struct BroadcastValidationQuery { } pub mod serde_status_code { - use crate::StatusCode; + use reqwest::StatusCode; use serde::{Deserialize, Serialize, de::Error}; pub fn serialize(status_code: &StatusCode, ser: S) -> Result @@ -1723,7 +1723,7 @@ pub type JsonProduceBlockV3Response = pub enum FullBlockContents { /// This is a full deneb variant with block and blobs. BlockContents(BlockContents), - /// This variant is for all pre-deneb full blocks. + /// This variant is for all pre-deneb full blocks or post-gloas beacon block. Block(BeaconBlock), } @@ -1751,6 +1751,20 @@ pub struct ProduceBlockV3Metadata { pub consensus_block_value: Uint256, } +/// Metadata about a `produce_block_v4` response which is returned in the body & headers. +#[derive(Debug, Deserialize, Serialize)] +pub struct ProduceBlockV4Metadata { + // The consensus version is serialized & deserialized by `ForkVersionedResponse`. + #[serde( + skip_serializing, + skip_deserializing, + default = "dummy_consensus_version" + )] + pub consensus_version: ForkName, + #[serde(with = "serde_utils::u256_dec")] + pub consensus_block_value: Uint256, +} + impl FullBlockContents { pub fn new(block: BeaconBlock, blob_data: Option<(KzgProofs, BlobsList)>) -> Self { match blob_data { @@ -1907,6 +1921,27 @@ impl TryFrom<&HeaderMap> for ProduceBlockV3Metadata { } } +impl TryFrom<&HeaderMap> for ProduceBlockV4Metadata { + type Error = String; + + fn try_from(headers: &HeaderMap) -> Result { + let consensus_version = parse_required_header(headers, CONSENSUS_VERSION_HEADER, |s| { + s.parse::() + .map_err(|e| format!("invalid {CONSENSUS_VERSION_HEADER}: {e:?}")) + })?; + let consensus_block_value = + parse_required_header(headers, CONSENSUS_BLOCK_VALUE_HEADER, |s| { + Uint256::from_str_radix(s, 10) + .map_err(|e| format!("invalid {CONSENSUS_BLOCK_VALUE_HEADER}: {e:?}")) + })?; + + Ok(ProduceBlockV4Metadata { + consensus_version, + consensus_block_value, + }) + } +} + /// A wrapper over a [`SignedBeaconBlock`] or a [`SignedBlockContents`]. #[derive(Clone, Debug, PartialEq, Encode, Serialize)] #[serde(untagged)] @@ -1954,7 +1989,7 @@ impl PublishBlockRequest { /// SSZ decode with fork variant determined by `fork_name`. pub fn from_ssz_bytes(bytes: &[u8], fork_name: ForkName) -> Result { - if fork_name.deneb_enabled() { + if fork_name.deneb_enabled() && !fork_name.gloas_enabled() { let mut builder = ssz::SszDecoderBuilder::new(bytes); builder.register_anonymous_variable_length_item()?; builder.register_type::>()?; diff --git a/common/health_metrics/src/observe.rs b/common/health_metrics/src/observe.rs index 81bb8e6f7e..5bc3770301 100644 --- a/common/health_metrics/src/observe.rs +++ b/common/health_metrics/src/observe.rs @@ -121,7 +121,7 @@ impl Observe for ProcessHealth { pid_mem_shared_memory_size: process_mem.shared(), pid_process_seconds_total: process_times.busy().as_secs() + process_times.children_system().as_secs() - + process_times.children_system().as_secs(), + + process_times.children_user().as_secs(), }) } } diff --git a/common/warp_utils/Cargo.toml b/common/warp_utils/Cargo.toml index 32a540a69d..80bc247cbf 100644 --- a/common/warp_utils/Cargo.toml +++ b/common/warp_utils/Cargo.toml @@ -9,6 +9,7 @@ edition = { workspace = true } bytes = { workspace = true } eth2 = { workspace = true } headers = "0.3.2" +reqwest = { workspace = true } safe_arith = { workspace = true } serde = { workspace = true } serde_array_query = "0.1.0" diff --git a/common/warp_utils/src/status_code.rs b/common/warp_utils/src/status_code.rs index 1b05297359..a654b6d2c5 100644 --- a/common/warp_utils/src/status_code.rs +++ b/common/warp_utils/src/status_code.rs @@ -1,4 +1,4 @@ -use eth2::StatusCode; +use reqwest::StatusCode; use warp::Rejection; /// Convert from a "new" `http::StatusCode` to a `warp` compatible one. diff --git a/consensus/state_processing/src/common/get_payload_attesting_indices.rs b/consensus/state_processing/src/common/get_payload_attesting_indices.rs new file mode 100644 index 0000000000..407e4f1372 --- /dev/null +++ b/consensus/state_processing/src/common/get_payload_attesting_indices.rs @@ -0,0 +1,42 @@ +use crate::per_block_processing::errors::{ + BlockOperationError, PayloadAttestationInvalid as Invalid, +}; +use ssz_types::VariableList; +use types::{ + BeaconState, BeaconStateError, ChainSpec, EthSpec, IndexedPayloadAttestation, + PayloadAttestation, +}; + +pub fn get_indexed_payload_attestation( + state: &BeaconState, + payload_attestation: &PayloadAttestation, + spec: &ChainSpec, +) -> Result, BlockOperationError> { + let attesting_indices = get_payload_attesting_indices(state, payload_attestation, spec)?; + + Ok(IndexedPayloadAttestation { + attesting_indices: VariableList::new(attesting_indices)?, + data: payload_attestation.data.clone(), + signature: payload_attestation.signature.clone(), + }) +} + +pub fn get_payload_attesting_indices( + state: &BeaconState, + payload_attestation: &PayloadAttestation, + spec: &ChainSpec, +) -> Result, BeaconStateError> { + let slot = payload_attestation.data.slot; + let ptc = state.get_ptc(slot, spec)?; + let bits = &payload_attestation.aggregation_bits; + + let mut attesting_indices = vec![]; + for (i, index) in ptc.into_iter().enumerate() { + if let Ok(true) = bits.get(i) { + attesting_indices.push(index as u64); + } + } + attesting_indices.sort_unstable(); + + Ok(attesting_indices) +} diff --git a/consensus/state_processing/src/common/mod.rs b/consensus/state_processing/src/common/mod.rs index 0287748fd0..e550a6c48b 100644 --- a/consensus/state_processing/src/common/mod.rs +++ b/consensus/state_processing/src/common/mod.rs @@ -1,6 +1,7 @@ mod deposit_data_tree; mod get_attestation_participation; mod get_attesting_indices; +mod get_payload_attesting_indices; mod initiate_validator_exit; mod slash_validator; @@ -13,6 +14,9 @@ pub use get_attestation_participation::get_attestation_participation_flag_indice pub use get_attesting_indices::{ attesting_indices_base, attesting_indices_electra, get_attesting_indices_from_state, }; +pub use get_payload_attesting_indices::{ + get_indexed_payload_attestation, get_payload_attesting_indices, +}; pub use initiate_validator_exit::initiate_validator_exit; pub use slash_validator::slash_validator; diff --git a/consensus/state_processing/src/consensus_context.rs b/consensus/state_processing/src/consensus_context.rs index 07d554e303..bc7bd20384 100644 --- a/consensus/state_processing/src/consensus_context.rs +++ b/consensus/state_processing/src/consensus_context.rs @@ -1,11 +1,16 @@ use crate::EpochCacheError; -use crate::common::{attesting_indices_base, attesting_indices_electra}; -use crate::per_block_processing::errors::{AttestationInvalid, BlockOperationError}; +use crate::common::{ + attesting_indices_base, attesting_indices_electra, get_indexed_payload_attestation, +}; +use crate::per_block_processing::errors::{ + AttestationInvalid, BlockOperationError, PayloadAttestationInvalid, +}; use std::collections::{HashMap, hash_map::Entry}; use tree_hash::TreeHash; use types::{ AbstractExecPayload, AttestationRef, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, - Hash256, IndexedAttestation, IndexedAttestationRef, SignedBeaconBlock, Slot, + Hash256, IndexedAttestation, IndexedAttestationRef, IndexedPayloadAttestation, + PayloadAttestation, SignedBeaconBlock, Slot, }; #[derive(Debug, PartialEq, Clone)] @@ -22,6 +27,8 @@ pub struct ConsensusContext { pub current_block_root: Option, /// Cache of indexed attestations constructed during block processing. pub indexed_attestations: HashMap>, + /// Cache of indexed payload attestations constructed during block processing. + pub indexed_payload_attestations: HashMap>, } #[derive(Debug, PartialEq, Clone)] @@ -55,6 +62,7 @@ impl ConsensusContext { proposer_index: None, current_block_root: None, indexed_attestations: HashMap::new(), + indexed_payload_attestations: HashMap::new(), } } @@ -177,6 +185,24 @@ impl ConsensusContext { .map(|indexed_attestation| (*indexed_attestation).to_ref()) } + pub fn get_indexed_payload_attestation<'a>( + &'a mut self, + state: &BeaconState, + payload_attestation: &'a PayloadAttestation, + spec: &ChainSpec, + ) -> Result<&'a IndexedPayloadAttestation, BlockOperationError> + { + let key = payload_attestation.tree_hash_root(); + match self.indexed_payload_attestations.entry(key) { + Entry::Occupied(occupied) => Ok(occupied.into_mut()), + Entry::Vacant(vacant) => { + let indexed_payload_attestation = + get_indexed_payload_attestation(state, payload_attestation, spec)?; + Ok(vacant.insert(indexed_payload_attestation)) + } + } + } + pub fn num_cached_indexed_attestations(&self) -> usize { self.indexed_attestations.len() } diff --git a/consensus/state_processing/src/envelope_processing.rs b/consensus/state_processing/src/envelope_processing.rs index d46728dbbc..c2cfeae5d3 100644 --- a/consensus/state_processing/src/envelope_processing.rs +++ b/consensus/state_processing/src/envelope_processing.rs @@ -20,6 +20,23 @@ macro_rules! envelope_verify { }; } +/// The strategy to be used when validating the payloads state root. +#[cfg_attr(feature = "arbitrary-fuzz", derive(arbitrary::Arbitrary))] +#[derive(PartialEq, Clone, Copy)] +pub enum VerifyStateRoot { + /// Validate state root. + True, + /// Do not validate state root. Use with caution. + /// This should only be used when first constructing the payload envelope. + False, +} + +impl VerifyStateRoot { + pub fn is_true(self) -> bool { + self == VerifyStateRoot::True + } +} + #[derive(Debug, Clone)] pub enum EnvelopeProcessingError { /// Bad Signature @@ -111,6 +128,7 @@ pub fn process_execution_payload_envelope( parent_state_root: Option, signed_envelope: &SignedExecutionPayloadEnvelope, verify_signatures: VerifySignatures, + verify_state_root: VerifyStateRoot, spec: &ChainSpec, ) -> Result<(), EnvelopeProcessingError> { if verify_signatures.is_true() { @@ -264,15 +282,17 @@ pub fn process_execution_payload_envelope( .map_err(EnvelopeProcessingError::BitFieldError)?; *state.latest_block_hash_mut()? = payload.block_hash; - // Verify the state root - let state_root = state.canonical_root()?; - envelope_verify!( - envelope.state_root == state_root, - EnvelopeProcessingError::InvalidStateRoot { - state: state_root, - envelope: envelope.state_root, - } - ); + if verify_state_root.is_true() { + // Verify the state root + let state_root = state.canonical_root()?; + envelope_verify!( + envelope.state_root == state_root, + EnvelopeProcessingError::InvalidStateRoot { + state: state_root, + envelope: envelope.state_root, + } + ); + } Ok(()) } diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index d9a41418cf..037e1c7cc7 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -20,6 +20,7 @@ pub use self::verify_proposer_slashing::verify_proposer_slashing; pub use altair::sync_committee::process_sync_aggregate; pub use block_signature_verifier::{BlockSignatureVerifier, ParallelSignatureSets}; pub use is_valid_indexed_attestation::is_valid_indexed_attestation; +pub use is_valid_indexed_payload_attestation::is_valid_indexed_payload_attestation; pub use process_operations::process_operations; pub use verify_attestation::{ verify_attestation_for_block_inclusion, verify_attestation_for_state, @@ -37,6 +38,7 @@ pub mod builder; pub mod deneb; pub mod errors; mod is_valid_indexed_attestation; +mod is_valid_indexed_payload_attestation; pub mod process_operations; pub mod signature_sets; pub mod tests; @@ -45,6 +47,7 @@ mod verify_attester_slashing; mod verify_bls_to_execution_change; mod verify_deposit; mod verify_exit; +mod verify_payload_attestation; mod verify_proposer_slashing; pub mod withdrawals; diff --git a/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs b/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs index e82ce537fd..eea9c17a14 100644 --- a/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs +++ b/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs @@ -1,7 +1,9 @@ #![allow(clippy::arithmetic_side_effects)] use super::signature_sets::{Error as SignatureSetError, *}; -use crate::per_block_processing::errors::{AttestationInvalid, BlockOperationError}; +use crate::per_block_processing::errors::{ + AttestationInvalid, BlockOperationError, PayloadAttestationInvalid, +}; use crate::{ConsensusContext, ContextError}; use bls::{PublicKey, PublicKeyBytes, SignatureSet, verify_signature_sets}; use std::borrow::Cow; @@ -18,6 +20,8 @@ pub enum Error { SignatureInvalid, /// An attestation in the block was invalid. The block is invalid. AttestationValidationError(BlockOperationError), + /// A payload attestation in the block was invalid. The block is invalid. + PayloadAttestationValidationError(BlockOperationError), /// There was an error attempting to read from a `BeaconState`. Block /// validity was not determined. BeaconStateError(BeaconStateError), @@ -66,6 +70,12 @@ impl From> for Error { } } +impl From> for Error { + fn from(e: BlockOperationError) -> Error { + Error::PayloadAttestationValidationError(e) + } +} + /// Reads the BLS signatures and keys from a `SignedBeaconBlock`, storing them as a `Vec`. /// /// This allows for optimizations related to batch BLS operations (see the @@ -171,6 +181,7 @@ where self.include_sync_aggregate(block)?; self.include_bls_to_execution_changes(block)?; self.include_execution_payload_bid(block)?; + self.include_payload_attestations(block, ctxt)?; Ok(()) } @@ -296,6 +307,39 @@ where }) } + /// Includes all signatures in `self.block.body.payload_attestations` for verification. + pub fn include_payload_attestations>( + &mut self, + block: &'a SignedBeaconBlock, + ctxt: &mut ConsensusContext, + ) -> Result<()> { + let Ok(payload_attestations) = block.message().body().payload_attestations() else { + // Nothing to do pre-Gloas. + return Ok(()); + }; + + self.sets.sets.reserve(payload_attestations.len()); + + payload_attestations + .iter() + .try_for_each(|payload_attestation| { + let indexed_payload_attestation = ctxt.get_indexed_payload_attestation( + self.state, + payload_attestation, + self.spec, + )?; + + self.sets.push(indexed_payload_attestation_signature_set( + self.state, + self.get_pubkey.clone(), + &payload_attestation.signature, + indexed_payload_attestation, + self.spec, + )?); + Ok(()) + }) + } + /// Includes all signatures in `self.block.body.voluntary_exits` for verification. pub fn include_exits>( &mut self, diff --git a/consensus/state_processing/src/per_block_processing/errors.rs b/consensus/state_processing/src/per_block_processing/errors.rs index 53178a7a64..71083378db 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -41,6 +41,10 @@ pub enum BlockProcessingError { index: usize, reason: AttestationInvalid, }, + PayloadAttestationInvalid { + index: usize, + reason: PayloadAttestationInvalid, + }, DepositInvalid { index: usize, reason: DepositInvalid, @@ -217,6 +221,7 @@ impl_into_block_processing_error_with_index!( AttesterSlashingInvalid, IndexedAttestationInvalid, AttestationInvalid, + PayloadAttestationInvalid, DepositInvalid, ExitInvalid, BlsExecutionChangeInvalid @@ -422,6 +427,52 @@ pub enum IndexedAttestationInvalid { SignatureSetError(SignatureSetError), } +#[derive(Debug, PartialEq, Clone)] +pub enum PayloadAttestationInvalid { + /// Block root does not match the parent beacon block root. + BlockRootMismatch { + expected: Hash256, + found: Hash256, + }, + /// The attestation slot is not the previous slot. + SlotMismatch { + expected: Slot, + found: Slot, + }, + BadIndexedPayloadAttestation(IndexedPayloadAttestationInvalid), +} + +impl From> + for BlockOperationError +{ + fn from(e: BlockOperationError) -> Self { + match e { + BlockOperationError::Invalid(e) => BlockOperationError::invalid( + PayloadAttestationInvalid::BadIndexedPayloadAttestation(e), + ), + BlockOperationError::BeaconStateError(e) => BlockOperationError::BeaconStateError(e), + BlockOperationError::SignatureSetError(e) => BlockOperationError::SignatureSetError(e), + BlockOperationError::SszTypesError(e) => BlockOperationError::SszTypesError(e), + BlockOperationError::BitfieldError(e) => BlockOperationError::BitfieldError(e), + BlockOperationError::ConsensusContext(e) => BlockOperationError::ConsensusContext(e), + BlockOperationError::ArithError(e) => BlockOperationError::ArithError(e), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum IndexedPayloadAttestationInvalid { + /// The number of indices is 0. + IndicesEmpty, + /// The validator indices were not in increasing order. + BadValidatorIndicesOrdering, + /// The indexed attestation aggregate signature was not valid. + BadSignature, + /// There was an error whilst attempting to get a set of signatures. The signatures may have + /// been invalid or an internal error occurred. + SignatureSetError(SignatureSetError), +} + #[derive(Debug, PartialEq, Clone)] pub enum DepositInvalid { /// The signature (proof-of-possession) does not match the given pubkey. diff --git a/consensus/state_processing/src/per_block_processing/is_valid_indexed_payload_attestation.rs b/consensus/state_processing/src/per_block_processing/is_valid_indexed_payload_attestation.rs new file mode 100644 index 0000000000..534f553247 --- /dev/null +++ b/consensus/state_processing/src/per_block_processing/is_valid_indexed_payload_attestation.rs @@ -0,0 +1,32 @@ +use super::errors::{BlockOperationError, IndexedPayloadAttestationInvalid as Invalid}; +use super::signature_sets::{get_pubkey_from_state, indexed_payload_attestation_signature_set}; +use crate::VerifySignatures; +use types::*; + +pub fn is_valid_indexed_payload_attestation( + state: &BeaconState, + indexed_payload_attestation: &IndexedPayloadAttestation, + verify_signatures: VerifySignatures, + spec: &ChainSpec, +) -> Result<(), BlockOperationError> { + // Verify indices are non-empty and sorted (duplicates allowed) + let indices = &indexed_payload_attestation.attesting_indices; + verify!(!indices.is_empty(), Invalid::IndicesEmpty); + verify!(indices.is_sorted(), Invalid::BadValidatorIndicesOrdering); + + if verify_signatures.is_true() { + verify!( + indexed_payload_attestation_signature_set( + state, + |i| get_pubkey_from_state(state, i), + &indexed_payload_attestation.signature, + indexed_payload_attestation, + spec + )? + .verify(), + Invalid::BadSignature + ); + } + + Ok(()) +} diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index 19109f1508..9743812632 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -5,6 +5,7 @@ use crate::common::{ slash_validator, }; use crate::per_block_processing::errors::{BlockProcessingError, IntoWithIndex}; +use crate::per_block_processing::verify_payload_attestation::verify_payload_attestation; use bls::{PublicKeyBytes, SignatureBytes}; use ssz_types::FixedVector; use typenum::U33; @@ -39,8 +40,15 @@ pub fn process_operations>( process_bls_to_execution_changes(state, bls_to_execution_changes, verify_signatures, spec)?; } - if state.fork_name_unchecked().electra_enabled() && !state.fork_name_unchecked().gloas_enabled() - { + if state.fork_name_unchecked().gloas_enabled() { + process_payload_attestations( + state, + block_body.payload_attestations()?.iter(), + verify_signatures, + ctxt, + spec, + )?; + } else if state.fork_name_unchecked().electra_enabled() { state.update_pubkey_cache()?; process_deposit_requests_pre_gloas( state, @@ -868,7 +876,7 @@ pub fn apply_deposit_for_builder( signature: SignatureBytes, slot: Slot, spec: &ChainSpec, -) -> Result<(), BlockProcessingError> { +) -> Result<(), BeaconStateError> { match builder_index_opt { None => { // Verify the deposit signature (proof of possession) which is not checked by the deposit contract @@ -1074,3 +1082,45 @@ pub fn process_consolidation_request( Ok(()) } + +pub fn process_payload_attestation( + state: &mut BeaconState, + payload_attestation: &PayloadAttestation, + att_index: usize, + verify_signatures: VerifySignatures, + ctxt: &mut ConsensusContext, + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> { + verify_payload_attestation(state, payload_attestation, ctxt, verify_signatures, spec) + .map_err(|e| e.into_with_index(att_index)) +} + +pub fn process_payload_attestations<'a, E: EthSpec, I>( + state: &mut BeaconState, + payload_attestations: I, + verify_signatures: VerifySignatures, + ctxt: &mut ConsensusContext, + spec: &ChainSpec, +) -> Result<(), BlockProcessingError> +where + I: Iterator>, +{ + // Presently the PTC cache requires the committee cache for `state.slot() - 1` which is either + // in the current or previous epoch. + // TODO(gloas): These requirements may change if we introduce a PTC cache. + state.build_committee_cache(RelativeEpoch::Current, spec)?; + state.build_committee_cache(RelativeEpoch::Previous, spec)?; + + payload_attestations + .enumerate() + .try_for_each(|(i, payload_attestation)| { + process_payload_attestation( + state, + payload_attestation, + i, + verify_signatures, + ctxt, + spec, + ) + }) +} diff --git a/consensus/state_processing/src/per_block_processing/signature_sets.rs b/consensus/state_processing/src/per_block_processing/signature_sets.rs index 0cc591ba4c..71ee1f8993 100644 --- a/consensus/state_processing/src/per_block_processing/signature_sets.rs +++ b/consensus/state_processing/src/per_block_processing/signature_sets.rs @@ -10,10 +10,10 @@ use typenum::Unsigned; use types::{ AbstractExecPayload, AttesterSlashingRef, BeaconBlockRef, BeaconState, BeaconStateError, BuilderIndex, ChainSpec, DepositData, Domain, Epoch, EthSpec, Fork, Hash256, InconsistentFork, - IndexedAttestation, IndexedAttestationRef, ProposerSlashing, SignedAggregateAndProof, - SignedBeaconBlock, SignedBeaconBlockHeader, SignedBlsToExecutionChange, - SignedContributionAndProof, SignedExecutionPayloadBid, SignedRoot, SignedVoluntaryExit, - SigningData, Slot, SyncAggregate, SyncAggregatorSelectionData, + IndexedAttestation, IndexedAttestationRef, IndexedPayloadAttestation, ProposerSlashing, + SignedAggregateAndProof, SignedBeaconBlock, SignedBeaconBlockHeader, + SignedBlsToExecutionChange, SignedContributionAndProof, SignedExecutionPayloadBid, SignedRoot, + SignedVoluntaryExit, SigningData, Slot, SyncAggregate, SyncAggregatorSelectionData, consts::gloas::BUILDER_INDEX_SELF_BUILD, }; @@ -355,6 +355,40 @@ where Ok(SignatureSet::multiple_pubkeys(signature, pubkeys, message)) } +pub fn indexed_payload_attestation_signature_set<'a, 'b, E, F>( + state: &'a BeaconState, + get_pubkey: F, + signature: &'a AggregateSignature, + indexed_payload_attestation: &'b IndexedPayloadAttestation, + spec: &'a ChainSpec, +) -> Result> +where + E: EthSpec, + F: Fn(usize) -> Option>, +{ + let mut pubkeys = Vec::with_capacity(indexed_payload_attestation.attesting_indices.len()); + for &validator_idx in indexed_payload_attestation.attesting_indices.iter() { + pubkeys.push( + get_pubkey(validator_idx as usize).ok_or(Error::ValidatorUnknown(validator_idx))?, + ); + } + + let epoch = indexed_payload_attestation + .data + .slot + .epoch(E::slots_per_epoch()); + let domain = spec.get_domain( + epoch, + Domain::PTCAttester, + &state.fork(), + state.genesis_validators_root(), + ); + + let message = indexed_payload_attestation.data.signing_root(domain); + + Ok(SignatureSet::multiple_pubkeys(signature, pubkeys, message)) +} + pub fn execution_payload_bid_signature_set<'a, E, F>( state: &'a BeaconState, get_builder_pubkey: F, diff --git a/consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs b/consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs new file mode 100644 index 0000000000..1c15e1c21c --- /dev/null +++ b/consensus/state_processing/src/per_block_processing/verify_payload_attestation.rs @@ -0,0 +1,46 @@ +use super::VerifySignatures; +use super::errors::{BlockOperationError, PayloadAttestationInvalid as Invalid}; +use crate::ConsensusContext; +use crate::per_block_processing::is_valid_indexed_payload_attestation; +use safe_arith::SafeArith; +use types::*; + +pub fn verify_payload_attestation<'ctxt, E: EthSpec>( + state: &mut BeaconState, + payload_attestation: &'ctxt PayloadAttestation, + ctxt: &'ctxt mut ConsensusContext, + verify_signatures: VerifySignatures, + spec: &ChainSpec, +) -> Result<(), BlockOperationError> { + let data = &payload_attestation.data; + + // Check that the attestation is for the parent beacon block + verify!( + data.beacon_block_root == state.latest_block_header().parent_root, + Invalid::BlockRootMismatch { + expected: state.latest_block_header().parent_root, + found: data.beacon_block_root, + } + ); + + // Check that the attestation is for the previous slot + verify!( + data.slot.safe_add(1)? == state.slot(), + Invalid::SlotMismatch { + expected: state.slot().saturating_sub(Slot::new(1)), + found: data.slot, + } + ); + + let indexed_payload_attestation = + ctxt.get_indexed_payload_attestation(state, payload_attestation, spec)?; + + is_valid_indexed_payload_attestation( + state, + indexed_payload_attestation, + verify_signatures, + spec, + )?; + + Ok(()) +} diff --git a/consensus/state_processing/src/upgrade/gloas.rs b/consensus/state_processing/src/upgrade/gloas.rs index 0e0f39fa02..7a88383ab0 100644 --- a/consensus/state_processing/src/upgrade/gloas.rs +++ b/consensus/state_processing/src/upgrade/gloas.rs @@ -1,10 +1,14 @@ +use crate::per_block_processing::{ + is_valid_deposit_signature, process_operations::apply_deposit_for_builder, +}; use milhouse::{List, Vector}; use ssz_types::BitVector; +use std::collections::HashSet; use std::mem; use typenum::Unsigned; use types::{ BeaconState, BeaconStateError as Error, BeaconStateGloas, BuilderPendingPayment, ChainSpec, - EthSpec, ExecutionPayloadBid, Fork, + DepositData, EthSpec, ExecutionPayloadBid, Fork, is_builder_withdrawal_credential, }; /// Transform a `Fulu` state into a `Gloas` state. @@ -30,7 +34,7 @@ pub fn upgrade_state_to_gloas( // // Fixed size vectors get cloned because replacing them would require the same size // allocation as cloning. - let post = BeaconState::Gloas(BeaconStateGloas { + let mut post = BeaconState::Gloas(BeaconStateGloas { // Versioning genesis_time: pre.genesis_time, genesis_validators_root: pre.genesis_validators_root, @@ -114,5 +118,81 @@ pub fn upgrade_state_to_gloas( slashings_cache: mem::take(&mut pre.slashings_cache), epoch_cache: mem::take(&mut pre.epoch_cache), }); + // [New in Gloas:EIP7732] + onboard_builders_from_pending_deposits(&mut post, spec)?; + Ok(post) } + +/// Applies any pending deposit for builders, effectively onboarding builders at the fork. +fn onboard_builders_from_pending_deposits( + state: &mut BeaconState, + spec: &ChainSpec, +) -> Result<(), Error> { + // Rather than tracking all `validator_pubkeys` in one place as the spec does, we keep a + // hashset for *just* the new validator pubkeys, and use the state's efficient + // `get_validator_index` function instead of an O(n) iteration over the full validator list. + let mut new_validator_pubkeys = HashSet::new(); + + // Clone pending deposits to avoid borrow conflicts when mutating state. + let current_pending_deposits = state.pending_deposits()?.clone(); + + let mut pending_deposits = List::empty(); + + for deposit in ¤t_pending_deposits { + // Deposits for existing validators stay in the pending queue. + if new_validator_pubkeys.contains(&deposit.pubkey) + || state.get_validator_index(&deposit.pubkey)?.is_some() + { + pending_deposits.push(deposit.clone())?; + continue; + } + + // Re-scan builder list each iteration because `apply_deposit_for_builder` may add + // new builders to the registry. + // TODO(gloas): this linear scan could be optimized, see: + // https://github.com/sigp/lighthouse/issues/8783 + let builder_index = state + .builders()? + .iter() + .position(|b| b.pubkey == deposit.pubkey); + + let has_builder_credentials = + is_builder_withdrawal_credential(deposit.withdrawal_credentials, spec); + + if builder_index.is_some() || has_builder_credentials { + let builder_index_opt = builder_index.map(|i| i as u64); + apply_deposit_for_builder( + state, + builder_index_opt, + deposit.pubkey, + deposit.withdrawal_credentials, + deposit.amount, + deposit.signature.clone(), + deposit.slot, + spec, + )?; + continue; + } + + // If there is a pending deposit for a new validator that has a valid signature, + // track the pubkey so that subsequent builder deposits for the same pubkey stay + // in pending (applied to the validator later) rather than creating a builder. + // Deposits with invalid signatures are dropped since they would fail in + // apply_pending_deposit anyway. + let deposit_data = DepositData { + pubkey: deposit.pubkey, + withdrawal_credentials: deposit.withdrawal_credentials, + amount: deposit.amount, + signature: deposit.signature.clone(), + }; + if is_valid_deposit_signature(&deposit_data, spec).is_ok() { + new_validator_pubkeys.insert(deposit.pubkey); + pending_deposits.push(deposit.clone())?; + } + } + + *state.pending_deposits_mut()? = pending_deposits; + + Ok(()) +} diff --git a/consensus/types/src/builder/builder_bid.rs b/consensus/types/src/builder/builder_bid.rs index 1018fadb64..e706b01283 100644 --- a/consensus/types/src/builder/builder_bid.rs +++ b/consensus/types/src/builder/builder_bid.rs @@ -196,7 +196,7 @@ impl SignedBuilderBid { .pubkey() .decompress() .map(|pubkey| { - let domain = spec.get_builder_domain(); + let domain = spec.get_builder_application_domain(); let message = self.message.signing_root(domain); self.signature.verify(&pubkey, message) }) diff --git a/consensus/types/src/core/application_domain.rs b/consensus/types/src/core/application_domain.rs index 5e33f2dfd5..ff55a91034 100644 --- a/consensus/types/src/core/application_domain.rs +++ b/consensus/types/src/core/application_domain.rs @@ -4,6 +4,7 @@ pub const APPLICATION_DOMAIN_BUILDER: u32 = 16777216; #[derive(Debug, PartialEq, Clone, Copy)] pub enum ApplicationDomain { + /// NOTE: This domain is only used for out-of-protocol block building, DO NOT use it for Gloas/ePBS. Builder, } diff --git a/consensus/types/src/core/chain_spec.rs b/consensus/types/src/core/chain_spec.rs index 6d25e3baf4..adf87dee94 100644 --- a/consensus/types/src/core/chain_spec.rs +++ b/consensus/types/src/core/chain_spec.rs @@ -549,7 +549,9 @@ impl ChainSpec { // This should be updated to include the current fork and the genesis validators root, but discussion is ongoing: // // https://github.com/ethereum/builder-specs/issues/14 - pub fn get_builder_domain(&self) -> Hash256 { + // + // NOTE: This domain is only used for out-of-protocol block building, DO NOT use it for Gloas/ePBS. + pub fn get_builder_application_domain(&self) -> Hash256 { self.compute_domain( Domain::ApplicationMask(ApplicationDomain::Builder), self.genesis_fork_version, diff --git a/consensus/types/src/validator/validator_registration_data.rs b/consensus/types/src/validator/validator_registration_data.rs index a0a1df7dc5..df2293cbae 100644 --- a/consensus/types/src/validator/validator_registration_data.rs +++ b/consensus/types/src/validator/validator_registration_data.rs @@ -31,7 +31,7 @@ impl SignedValidatorRegistrationData { .pubkey .decompress() .map(|pubkey| { - let domain = spec.get_builder_domain(); + let domain = spec.get_builder_application_domain(); let message = self.message.signing_root(domain); self.signature.verify(&pubkey, message) }) diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index 00638f7b1e..782b554ff1 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -47,10 +47,7 @@ excluded_paths = [ "bls12-381-tests/hash_to_G2", "tests/.*/eip7732", "tests/.*/eip7805", - # TODO(gloas): remove these ignores as more Gloas operations are implemented - "tests/.*/gloas/operations/payload_attestation/.*", # TODO(gloas): remove these ignores as Gloas consensus is implemented - "tests/.*/gloas/fork/.*", "tests/.*/gloas/fork_choice/.*", # Ignore MatrixEntry SSZ tests for now. "tests/.*/.*/ssz_static/MatrixEntry/.*", @@ -70,8 +67,6 @@ excluded_paths = [ # Ignore full epoch tests for now (just test the sub-transitions). "tests/.*/.*/epoch_processing/.*/pre_epoch.ssz_snappy", "tests/.*/.*/epoch_processing/.*/post_epoch.ssz_snappy", - # Ignore inactivity_scores tests for now (should implement soon). - "tests/.*/.*/rewards/inactivity_scores/.*", # Ignore KZG tests that target internal kzg library functions "tests/.*/compute_verify_cell_kzg_proof_batch_challenge/.*", "tests/.*/compute_challenge/.*", diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index 2f08727045..ca0124e1aa 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -5,6 +5,7 @@ use crate::decode::{ssz_decode_file, ssz_decode_file_with, ssz_decode_state, yam use serde::Deserialize; use ssz::Decode; use state_processing::common::update_progressive_balances_cache::initialize_progressive_balances_cache; +use state_processing::envelope_processing::VerifyStateRoot; use state_processing::epoch_cache::initialize_epoch_cache; use state_processing::per_block_processing::process_operations::{ process_consolidation_requests, process_deposit_requests_post_gloas, @@ -20,7 +21,7 @@ use state_processing::{ process_operations::{ altair_deneb, base, gloas, process_attester_slashings, process_bls_to_execution_changes, process_deposits, process_exits, - process_proposer_slashings, + process_payload_attestation, process_proposer_slashings, }, process_sync_aggregate, withdrawals, }, @@ -30,8 +31,9 @@ use types::{ Attestation, AttesterSlashing, BeaconBlock, BeaconBlockBody, BeaconBlockBodyBellatrix, BeaconBlockBodyCapella, BeaconBlockBodyDeneb, BeaconBlockBodyElectra, BeaconBlockBodyFulu, BeaconState, BlindedPayload, ConsolidationRequest, Deposit, DepositRequest, ExecutionPayload, - ForkVersionDecode, FullPayload, ProposerSlashing, SignedBlsToExecutionChange, - SignedExecutionPayloadEnvelope, SignedVoluntaryExit, SyncAggregate, WithdrawalRequest, + ForkVersionDecode, FullPayload, PayloadAttestation, ProposerSlashing, + SignedBlsToExecutionChange, SignedExecutionPayloadEnvelope, SignedVoluntaryExit, SyncAggregate, + WithdrawalRequest, }; #[derive(Debug, Clone, Default, Deserialize)] @@ -458,7 +460,14 @@ impl Operation for SignedExecutionPayloadEnvelope { .as_ref() .is_some_and(|e| e.execution_valid); if valid { - process_execution_payload_envelope(state, None, self, VerifySignatures::True, spec) + process_execution_payload_envelope( + state, + None, + self, + VerifySignatures::True, + VerifyStateRoot::True, + spec, + ) } else { Err(EnvelopeProcessingError::ExecutionInvalid) } @@ -659,6 +668,32 @@ impl Operation for ConsolidationRequest { } } +impl Operation for PayloadAttestation { + type Error = BlockProcessingError; + + fn handler_name() -> String { + "payload_attestation".into() + } + + fn is_enabled_for_fork(fork_name: ForkName) -> bool { + fork_name.gloas_enabled() + } + + fn decode(path: &Path, _fork_name: ForkName, _spec: &ChainSpec) -> Result { + ssz_decode_file(path) + } + + fn apply_to( + &self, + state: &mut BeaconState, + spec: &ChainSpec, + _extra: &Operations, + ) -> Result<(), BlockProcessingError> { + let mut ctxt = ConsensusContext::new(state.slot()); + process_payload_attestation(state, self, 0, VerifySignatures::True, &mut ctxt, spec) + } +} + impl> LoadCase for Operations { fn load_from_dir(path: &Path, fork_name: ForkName) -> Result { let spec = &testing_spec::(fork_name); diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index 45bca21c6f..da3c5533b6 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -592,6 +592,15 @@ impl Handler for RewardsHandler { fn handler_name(&self) -> String { self.handler_name.to_string() } + + fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { + if self.handler_name == "inactivity_scores" { + // These tests were added in v1.7.0-alpha.2 and are available for Altair and later. + fork_name.altair_enabled() + } else { + true + } + } } #[derive(Educe)] @@ -612,11 +621,6 @@ impl Handler for ForkHandler { fn handler_name(&self) -> String { "fork".into() } - - fn disabled_forks(&self) -> Vec { - // TODO(gloas): remove once onboard_builders_from_pending_deposits is implemented - vec![ForkName::Gloas] - } } #[derive(Educe)] @@ -1168,25 +1172,6 @@ impl> Handler for OperationsHandler fn handler_name(&self) -> String { O::handler_name() } - - fn is_enabled_for_fork(&self, fork_name: ForkName) -> bool { - Self::Case::is_enabled_for_fork(fork_name) - && (!fork_name.gloas_enabled() - || self.handler_name() == "attestation" - || self.handler_name() == "attester_slashing" - || self.handler_name() == "block_header" - || self.handler_name() == "bls_to_execution_change" - || self.handler_name() == "consolidation_request" - || self.handler_name() == "deposit_request" - || self.handler_name() == "deposit" - || self.handler_name() == "execution_payload" - || self.handler_name() == "execution_payload_bid" - || self.handler_name() == "proposer_slashing" - || self.handler_name() == "sync_aggregate" - || self.handler_name() == "withdrawal_request" - || self.handler_name() == "withdrawals" - || self.handler_name() == "voluntary_exit") - } } #[derive(Educe)] diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index c3481a2405..3893df2ef7 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -99,6 +99,12 @@ fn operations_execution_payload_bid() { OperationsHandler::>::default().run(); } +#[test] +fn operations_payload_attestation() { + OperationsHandler::>::default().run(); + OperationsHandler::>::default().run(); +} + #[test] fn operations_withdrawals() { OperationsHandler::>::default().run(); @@ -1113,7 +1119,7 @@ fn kzg_inclusion_merkle_proof_validity() { #[test] fn rewards() { - for handler in &["basic", "leak", "random"] { + for handler in &["basic", "leak", "random", "inactivity_scores"] { RewardsHandler::::new(handler).run(); RewardsHandler::::new(handler).run(); } diff --git a/testing/execution_engine_integration/src/test_rig.rs b/testing/execution_engine_integration/src/test_rig.rs index 24d75f5a11..5c3061166e 100644 --- a/testing/execution_engine_integration/src/test_rig.rs +++ b/testing/execution_engine_integration/src/test_rig.rs @@ -563,7 +563,7 @@ impl TestRig { * * Indicate that the payload is the head of the chain, providing payload attributes. */ - let head_block_hash = valid_payload.block_hash(); + let head_block_hash = second_payload.block_hash(); let finalized_block_hash = ExecutionBlockHash::zero(); // To save sending proposer preparation data, just set the fee recipient // to the fee recipient configured for EE A. diff --git a/testing/node_test_rig/Cargo.toml b/testing/node_test_rig/Cargo.toml index 0d9db528da..21ec6fac12 100644 --- a/testing/node_test_rig/Cargo.toml +++ b/testing/node_test_rig/Cargo.toml @@ -10,6 +10,7 @@ beacon_node_fallback = { workspace = true } environment = { workspace = true } eth2 = { workspace = true } execution_layer = { workspace = true } +reqwest = { workspace = true } sensitive_url = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true } diff --git a/testing/node_test_rig/src/lib.rs b/testing/node_test_rig/src/lib.rs index ece6001802..76a5b7ddb2 100644 --- a/testing/node_test_rig/src/lib.rs +++ b/testing/node_test_rig/src/lib.rs @@ -4,7 +4,8 @@ use beacon_node::ProductionBeaconNode; use environment::RuntimeContext; -use eth2::{BeaconNodeHttpClient, Timeouts, reqwest::ClientBuilder}; +use eth2::{BeaconNodeHttpClient, Timeouts}; +use reqwest::ClientBuilder; use sensitive_url::SensitiveUrl; use std::path::PathBuf; use std::time::Duration; diff --git a/testing/validator_test_rig/Cargo.toml b/testing/validator_test_rig/Cargo.toml index f28a423433..2057a9fdc8 100644 --- a/testing/validator_test_rig/Cargo.toml +++ b/testing/validator_test_rig/Cargo.toml @@ -7,6 +7,7 @@ edition = { workspace = true } eth2 = { workspace = true } mockito = { workspace = true } regex = { workspace = true } +reqwest = { workspace = true } sensitive_url = { workspace = true } serde_json = { workspace = true } tracing = { workspace = true } diff --git a/testing/validator_test_rig/src/mock_beacon_node.rs b/testing/validator_test_rig/src/mock_beacon_node.rs index ff1e772d54..1ecdd85f3b 100644 --- a/testing/validator_test_rig/src/mock_beacon_node.rs +++ b/testing/validator_test_rig/src/mock_beacon_node.rs @@ -1,7 +1,8 @@ use eth2::types::{GenericResponse, SyncingData}; -use eth2::{BeaconNodeHttpClient, StatusCode, Timeouts}; +use eth2::{BeaconNodeHttpClient, Timeouts}; use mockito::{Matcher, Mock, Server, ServerGuard}; use regex::Regex; +use reqwest::StatusCode; use sensitive_url::SensitiveUrl; use std::marker::PhantomData; use std::str::FromStr; diff --git a/validator_client/lighthouse_validator_store/src/lib.rs b/validator_client/lighthouse_validator_store/src/lib.rs index 7b6a582363..7806482ffb 100644 --- a/validator_client/lighthouse_validator_store/src/lib.rs +++ b/validator_client/lighthouse_validator_store/src/lib.rs @@ -20,12 +20,12 @@ use task_executor::TaskExecutor; use tracing::{error, info, instrument, warn}; use types::{ AbstractExecPayload, Address, AggregateAndProof, Attestation, BeaconBlock, BlindedPayload, - ChainSpec, ContributionAndProof, Domain, Epoch, EthSpec, Fork, Graffiti, Hash256, - SelectionProof, SignedAggregateAndProof, SignedBeaconBlock, SignedContributionAndProof, - SignedRoot, SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, - SyncAggregatorSelectionData, SyncCommitteeContribution, SyncCommitteeMessage, - SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, VoluntaryExit, - graffiti::GraffitiString, + ChainSpec, ContributionAndProof, Domain, Epoch, EthSpec, ExecutionPayloadEnvelope, Fork, + FullPayload, Graffiti, Hash256, SelectionProof, SignedAggregateAndProof, SignedBeaconBlock, + SignedContributionAndProof, SignedExecutionPayloadEnvelope, SignedRoot, + SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, SyncAggregatorSelectionData, + SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, + ValidatorRegistrationData, VoluntaryExit, graffiti::GraffitiString, }; use validator_store::{ DoppelgangerStatus, Error as ValidatorStoreError, ProposalData, SignedBlock, UnsignedBlock, @@ -954,7 +954,7 @@ impl ValidatorStore for LighthouseValidatorS &self, validator_registration_data: ValidatorRegistrationData, ) -> Result { - let domain_hash = self.spec.get_builder_domain(); + let domain_hash = self.spec.get_builder_application_domain(); let signing_root = validator_registration_data.signing_root(domain_hash); let signing_method = @@ -1242,4 +1242,35 @@ impl ValidatorStore for LighthouseValidatorS .get_builder_proposals_defaulting(validator.get_builder_proposals()), }) } + + /// Sign an `ExecutionPayloadEnvelope` for Gloas (local building). + /// The proposer acts as the builder and signs with the BeaconBuilder domain. + async fn sign_execution_payload_envelope( + &self, + validator_pubkey: PublicKeyBytes, + envelope: ExecutionPayloadEnvelope, + ) -> Result, Error> { + let signing_context = self.signing_context( + Domain::BeaconBuilder, + envelope.slot.epoch(E::slots_per_epoch()), + ); + + // Execution payload envelope signing is not slashable, bypass doppelganger protection. + let signing_method = self.doppelganger_bypassed_signing_method(validator_pubkey)?; + + let signature = signing_method + .get_signature::>( + SignableMessage::ExecutionPayloadEnvelope(&envelope), + signing_context, + &self.spec, + &self.task_executor, + ) + .await + .map_err(Error::SpecificError)?; + + Ok(SignedExecutionPayloadEnvelope { + message: envelope, + signature, + }) + } } diff --git a/validator_client/signing_method/src/lib.rs b/validator_client/signing_method/src/lib.rs index bf3cc6a17d..c132d86c17 100644 --- a/validator_client/signing_method/src/lib.rs +++ b/validator_client/signing_method/src/lib.rs @@ -49,6 +49,7 @@ pub enum SignableMessage<'a, E: EthSpec, Payload: AbstractExecPayload = FullP SignedContributionAndProof(&'a ContributionAndProof), ValidatorRegistration(&'a ValidatorRegistrationData), VoluntaryExit(&'a VoluntaryExit), + ExecutionPayloadEnvelope(&'a ExecutionPayloadEnvelope), } impl> SignableMessage<'_, E, Payload> { @@ -70,6 +71,7 @@ impl> SignableMessage<'_, E, Payload SignableMessage::SignedContributionAndProof(c) => c.signing_root(domain), SignableMessage::ValidatorRegistration(v) => v.signing_root(domain), SignableMessage::VoluntaryExit(exit) => exit.signing_root(domain), + SignableMessage::ExecutionPayloadEnvelope(e) => e.signing_root(domain), } } } @@ -233,6 +235,9 @@ impl SigningMethod { Web3SignerObject::ValidatorRegistration(v) } SignableMessage::VoluntaryExit(e) => Web3SignerObject::VoluntaryExit(e), + SignableMessage::ExecutionPayloadEnvelope(e) => { + Web3SignerObject::ExecutionPayloadEnvelope(e) + } }; // Determine the Web3Signer message type. diff --git a/validator_client/signing_method/src/web3signer.rs b/validator_client/signing_method/src/web3signer.rs index 246d9e9e09..e6fc8f3ba2 100644 --- a/validator_client/signing_method/src/web3signer.rs +++ b/validator_client/signing_method/src/web3signer.rs @@ -19,6 +19,8 @@ pub enum MessageType { SyncCommitteeSelectionProof, SyncCommitteeContributionAndProof, ValidatorRegistration, + // TODO(gloas) verify w/ web3signer specs + ExecutionPayloadEnvelope, } #[derive(Debug, PartialEq, Copy, Clone, Serialize)] @@ -75,6 +77,7 @@ pub enum Web3SignerObject<'a, E: EthSpec, Payload: AbstractExecPayload> { SyncAggregatorSelectionData(&'a SyncAggregatorSelectionData), ContributionAndProof(&'a ContributionAndProof), ValidatorRegistration(&'a ValidatorRegistrationData), + ExecutionPayloadEnvelope(&'a ExecutionPayloadEnvelope), } impl<'a, E: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, E, Payload> { @@ -140,6 +143,7 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> Web3SignerObject<'a, E, Pa MessageType::SyncCommitteeContributionAndProof } Web3SignerObject::ValidatorRegistration(_) => MessageType::ValidatorRegistration, + Web3SignerObject::ExecutionPayloadEnvelope(_) => MessageType::ExecutionPayloadEnvelope, } } } diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index c0d561b175..f70d5830ec 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -19,11 +19,11 @@ use beacon_node_fallback::{ use clap::ArgMatches; use doppelganger_service::DoppelgangerService; use environment::RuntimeContext; -use eth2::{BeaconNodeHttpClient, StatusCode, Timeouts, reqwest::ClientBuilder}; +use eth2::{BeaconNodeHttpClient, Timeouts}; use initialized_validators::Error::UnableToOpenVotingKeystore; use lighthouse_validator_store::LighthouseValidatorStore; use parking_lot::RwLock; -use reqwest::Certificate; +use reqwest::{Certificate, ClientBuilder, StatusCode}; use slot_clock::SlotClock; use slot_clock::SystemTimeSlotClock; use std::fs::File; diff --git a/validator_client/validator_services/Cargo.toml b/validator_client/validator_services/Cargo.toml index c914940914..2582968265 100644 --- a/validator_client/validator_services/Cargo.toml +++ b/validator_client/validator_services/Cargo.toml @@ -13,6 +13,7 @@ futures = { workspace = true } graffiti_file = { workspace = true } logging = { workspace = true } parking_lot = { workspace = true } +reqwest = { workspace = true } safe_arith = { workspace = true } slot_clock = { workspace = true } task_executor = { workspace = true } diff --git a/validator_client/validator_services/src/block_service.rs b/validator_client/validator_services/src/block_service.rs index 625f8db7cb..1535f50663 100644 --- a/validator_client/validator_services/src/block_service.rs +++ b/validator_client/validator_services/src/block_service.rs @@ -1,9 +1,10 @@ use beacon_node_fallback::{ApiTopic, BeaconNodeFallback, Error as FallbackError, Errors}; use bls::PublicKeyBytes; +use eth2::BeaconNodeHttpClient; use eth2::types::GraffitiPolicy; -use eth2::{BeaconNodeHttpClient, StatusCode}; use graffiti_file::{GraffitiFile, determine_graffiti}; use logging::crit; +use reqwest::StatusCode; use slot_clock::SlotClock; use std::fmt::Debug; use std::future::Future; @@ -13,6 +14,7 @@ use std::time::Duration; use task_executor::TaskExecutor; use tokio::sync::mpsc; use tracing::{Instrument, debug, error, info, info_span, instrument, trace, warn}; +use types::consts::gloas::BUILDER_INDEX_SELF_BUILD; use types::{BlockType, ChainSpec, EthSpec, Graffiti, Slot}; use validator_store::{Error as ValidatorStoreError, SignedBlock, UnsignedBlock, ValidatorStore}; @@ -333,7 +335,7 @@ impl BlockService { #[instrument(skip_all, fields(%slot, ?validator_pubkey))] async fn sign_and_publish_block( &self, - proposer_fallback: ProposerFallback, + proposer_fallback: &ProposerFallback, slot: Slot, graffiti: Option, validator_pubkey: &PublicKeyBytes, @@ -459,73 +461,145 @@ impl BlockService { info!(slot = slot.as_u64(), "Requesting unsigned block"); - // Request an SSZ block from all beacon nodes in order, returning on the first successful response. - // If all nodes fail, run a second pass falling back to JSON. - // - // Proposer nodes will always be tried last during each pass since it's likely that they don't have a - // great view of attestations on the network. - let ssz_block_response = proposer_fallback - .request_proposers_last(|beacon_node| async move { - let _get_timer = validator_metrics::start_timer_vec( - &validator_metrics::BLOCK_SERVICE_TIMES, - &[validator_metrics::BEACON_BLOCK_HTTP_GET], - ); - beacon_node - .get_validator_blocks_v3_ssz::( - slot, - randao_reveal_ref, - graffiti.as_ref(), - builder_boost_factor, - self_ref.graffiti_policy, - ) - .await - }) - .await; + // Check if Gloas fork is active at this slot + let fork_name = self_ref.chain_spec.fork_name_at_slot::(slot); - let block_response = match ssz_block_response { - Ok((ssz_block_response, _metadata)) => ssz_block_response, - Err(e) => { - warn!( - slot = slot.as_u64(), - error = %e, - "SSZ block production failed, falling back to JSON" - ); + let (block_proposer, unsigned_block) = if fork_name.gloas_enabled() { + // Use V4 block production for Gloas + // Request an SSZ block from all beacon nodes in order, returning on the first successful response. + // If all nodes fail, run a second pass falling back to JSON. + let ssz_block_response = proposer_fallback + .request_proposers_last(|beacon_node| async move { + let _get_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK_HTTP_GET], + ); + beacon_node + .get_validator_blocks_v4_ssz::( + slot, + randao_reveal_ref, + graffiti.as_ref(), + builder_boost_factor, + self_ref.graffiti_policy, + ) + .await + }) + .await; - proposer_fallback - .request_proposers_last(|beacon_node| async move { - let _get_timer = validator_metrics::start_timer_vec( - &validator_metrics::BLOCK_SERVICE_TIMES, - &[validator_metrics::BEACON_BLOCK_HTTP_GET], - ); - let (json_block_response, _metadata) = beacon_node - .get_validator_blocks_v3::( - slot, - randao_reveal_ref, - graffiti.as_ref(), - builder_boost_factor, - self_ref.graffiti_policy, - ) - .await - .map_err(|e| { - BlockError::Recoverable(format!( - "Error from beacon node when producing block: {:?}", - e - )) - })?; + let block_response = match ssz_block_response { + Ok((ssz_block_response, _metadata)) => ssz_block_response, + Err(e) => { + warn!( + slot = slot.as_u64(), + error = %e, + "SSZ V4 block production failed, falling back to JSON" + ); - Ok(json_block_response.data) - }) - .await - .map_err(BlockError::from)? - } - }; + proposer_fallback + .request_proposers_last(|beacon_node| async move { + let _get_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK_HTTP_GET], + ); + let (json_block_response, _metadata) = beacon_node + .get_validator_blocks_v4::( + slot, + randao_reveal_ref, + graffiti.as_ref(), + builder_boost_factor, + self_ref.graffiti_policy, + ) + .await + .map_err(|e| { + BlockError::Recoverable(format!( + "Error from beacon node when producing block: {:?}", + e + )) + })?; - let (block_proposer, unsigned_block) = match block_response { - eth2::types::ProduceBlockV3Response::Full(block) => { - (block.block().proposer_index(), UnsignedBlock::Full(block)) - } - eth2::types::ProduceBlockV3Response::Blinded(block) => { - (block.proposer_index(), UnsignedBlock::Blinded(block)) + Ok(json_block_response.data) + }) + .await + .map_err(BlockError::from)? + } + }; + + // Gloas blocks don't have blobs (they're in the execution layer) + let block_contents = eth2::types::FullBlockContents::Block(block_response); + ( + block_contents.block().proposer_index(), + UnsignedBlock::Full(block_contents), + ) + } else { + // Use V3 block production for pre-Gloas forks + // Request an SSZ block from all beacon nodes in order, returning on the first successful response. + // If all nodes fail, run a second pass falling back to JSON. + // + // Proposer nodes will always be tried last during each pass since it's likely that they don't have a + // great view of attestations on the network. + let ssz_block_response = proposer_fallback + .request_proposers_last(|beacon_node| async move { + let _get_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK_HTTP_GET], + ); + beacon_node + .get_validator_blocks_v3_ssz::( + slot, + randao_reveal_ref, + graffiti.as_ref(), + builder_boost_factor, + self_ref.graffiti_policy, + ) + .await + }) + .await; + + let block_response = match ssz_block_response { + Ok((ssz_block_response, _metadata)) => ssz_block_response, + Err(e) => { + warn!( + slot = slot.as_u64(), + error = %e, + "SSZ block production failed, falling back to JSON" + ); + + proposer_fallback + .request_proposers_last(|beacon_node| async move { + let _get_timer = validator_metrics::start_timer_vec( + &validator_metrics::BLOCK_SERVICE_TIMES, + &[validator_metrics::BEACON_BLOCK_HTTP_GET], + ); + let (json_block_response, _metadata) = beacon_node + .get_validator_blocks_v3::( + slot, + randao_reveal_ref, + graffiti.as_ref(), + builder_boost_factor, + self_ref.graffiti_policy, + ) + .await + .map_err(|e| { + BlockError::Recoverable(format!( + "Error from beacon node when producing block: {:?}", + e + )) + })?; + + Ok(json_block_response.data) + }) + .await + .map_err(BlockError::from)? + } + }; + + match block_response { + eth2::types::ProduceBlockV3Response::Full(block) => { + (block.block().proposer_index(), UnsignedBlock::Full(block)) + } + eth2::types::ProduceBlockV3Response::Blinded(block) => { + (block.proposer_index(), UnsignedBlock::Blinded(block)) + } } }; @@ -538,7 +612,7 @@ impl BlockService { self_ref .sign_and_publish_block( - proposer_fallback, + &proposer_fallback, slot, graffiti, &validator_pubkey, @@ -546,6 +620,108 @@ impl BlockService { ) .await?; + // TODO(gloas) we only need to fetch, sign and publish the envelope in the local building case. + // Right now we always default to local building. Once we implement trustless/trusted builder logic + // we should check the bid for index == BUILDER_INDEX_SELF_BUILD + if fork_name.gloas_enabled() { + self_ref + .fetch_sign_and_publish_payload_envelope( + &proposer_fallback, + slot, + &validator_pubkey, + ) + .await?; + } + + Ok(()) + } + + /// Fetch, sign, and publish the execution payload envelope for Gloas. + /// This should be called after the block has been published. + /// + /// TODO(gloas): For multi-BN setups, we need to track which beacon node produced the block + /// and fetch the envelope from that same node. The envelope is cached per-BN, + /// so fetching from a different BN than the one that built the block will fail. + /// See: https://github.com/sigp/lighthouse/pull/8313 + #[instrument(skip_all)] + async fn fetch_sign_and_publish_payload_envelope( + &self, + _proposer_fallback: &ProposerFallback, + slot: Slot, + validator_pubkey: &PublicKeyBytes, + ) -> Result<(), BlockError> { + info!(slot = slot.as_u64(), "Fetching execution payload envelope"); + + // Fetch the envelope from the beacon node. Use builder_index=BUILDER_INDEX_SELF_BUILD for local building. + // TODO(gloas): Use proposer_fallback once multi-BN is supported. + let envelope = self + .beacon_nodes + .first_success(|beacon_node| async move { + beacon_node + .get_validator_execution_payload_envelope_ssz::( + slot, + BUILDER_INDEX_SELF_BUILD, + ) + .await + .map_err(|e| { + BlockError::Recoverable(format!( + "Error fetching execution payload envelope: {:?}", + e + )) + }) + }) + .await?; + + info!( + slot = slot.as_u64(), + beacon_block_root = %envelope.beacon_block_root, + "Received execution payload envelope, signing" + ); + + // Sign the envelope + let signed_envelope = self + .validator_store + .sign_execution_payload_envelope(*validator_pubkey, envelope) + .await + .map_err(|e| { + BlockError::Recoverable(format!( + "Error signing execution payload envelope: {:?}", + e + )) + })?; + + info!( + slot = slot.as_u64(), + "Signed execution payload envelope, publishing" + ); + + let fork_name = self.chain_spec.fork_name_at_slot::(slot); + + // Publish the signed envelope + // TODO(gloas): Use proposer_fallback once multi-BN is supported. + self.beacon_nodes + .first_success(|beacon_node| { + let signed_envelope = signed_envelope.clone(); + async move { + beacon_node + .post_beacon_execution_payload_envelope_ssz(&signed_envelope, fork_name) + .await + .map_err(|e| { + BlockError::Recoverable(format!( + "Error publishing execution payload envelope: {:?}", + e + )) + }) + } + }) + .await?; + + info!( + slot = slot.as_u64(), + beacon_block_root = %signed_envelope.message.beacon_block_root, + "Successfully published signed execution payload envelope" + ); + Ok(()) } diff --git a/validator_client/validator_store/src/lib.rs b/validator_client/validator_store/src/lib.rs index 4fdbb8064c..87ab669e8d 100644 --- a/validator_client/validator_store/src/lib.rs +++ b/validator_client/validator_store/src/lib.rs @@ -5,8 +5,9 @@ use std::fmt::Debug; use std::future::Future; use std::sync::Arc; use types::{ - Address, Attestation, AttestationError, BlindedBeaconBlock, Epoch, EthSpec, Graffiti, Hash256, - SelectionProof, SignedAggregateAndProof, SignedBlindedBeaconBlock, SignedContributionAndProof, + Address, Attestation, AttestationError, BlindedBeaconBlock, Epoch, EthSpec, + ExecutionPayloadEnvelope, Graffiti, Hash256, SelectionProof, SignedAggregateAndProof, + SignedBlindedBeaconBlock, SignedContributionAndProof, SignedExecutionPayloadEnvelope, SignedValidatorRegistrationData, Slot, SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, }; @@ -178,6 +179,13 @@ pub trait ValidatorStore: Send + Sync { /// runs. fn prune_slashing_protection_db(&self, current_epoch: Epoch, first_run: bool); + /// Sign an `ExecutionPayloadEnvelope` for Gloas. + fn sign_execution_payload_envelope( + &self, + validator_pubkey: PublicKeyBytes, + envelope: ExecutionPayloadEnvelope, + ) -> impl Future, Error>> + Send; + /// Returns `ProposalData` for the provided `pubkey` if it exists in `InitializedValidators`. /// `ProposalData` fields include defaulting logic described in `get_fee_recipient_defaulting`, /// `get_gas_limit_defaulting`, and `get_builder_proposals_defaulting`.