diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 1d66bd30e7..3db4804bd1 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -304,7 +304,7 @@ jobs: cache-target: release - name: Create log dir run: mkdir ${{ runner.temp }}/basic_simulator_logs - - name: Run a basic beacon chain sim that starts from Deneb + - name: Run a basic beacon chain sim run: cargo run --release --bin simulator basic-sim --disable-stdout-logging --log-dir ${{ runner.temp }}/basic_simulator_logs - name: Upload logs if: always() diff --git a/Cargo.lock b/Cargo.lock index d42bcd8fc1..a9fdfe70bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2855,8 +2855,11 @@ dependencies = [ "fs2", "hex", "kzg", + "lighthouse_network", "logging", "milhouse", + "network", + "proto_array", "rayon", "serde", "serde_json", @@ -8199,6 +8202,7 @@ dependencies = [ name = "simulator" version = "0.2.0" dependencies = [ + "beacon_chain", "clap", "environment", "execution_layer", diff --git a/Makefile b/Makefile index dd57bb038e..3c00883ce9 100644 --- a/Makefile +++ b/Makefile @@ -33,11 +33,11 @@ PROFILE ?= release # List of all hard forks up to gloas. This list is used to set env variables for several tests so that # they run for different forks. # TODO(EIP-7732) Remove this once we extend network tests to support gloas and use RECENT_FORKS instead -RECENT_FORKS_BEFORE_GLOAS=electra fulu +RECENT_FORKS_BEFORE_GLOAS=fulu # List of all recent hard forks. This list is used to set env variables for http_api tests # Include phase0 to test the code paths in sync that are pre blobs -RECENT_FORKS=electra fulu gloas +RECENT_FORKS=fulu gloas # For network tests include phase0 to cover genesis syncing (blocks without blobs or columns) TEST_NETWORK_FORKS=phase0 $(RECENT_FORKS_BEFORE_GLOAS) diff --git a/beacon_node/beacon_chain/src/attestation_verification.rs b/beacon_node/beacon_chain/src/attestation_verification.rs index f35de59e1f..635ca3a2ae 100644 --- a/beacon_node/beacon_chain/src/attestation_verification.rs +++ b/beacon_node/beacon_chain/src/attestation_verification.rs @@ -1023,7 +1023,8 @@ impl<'a, T: BeaconChainTypes> VerifiedUnaggregatedAttestation<'a, T> { let (committee_opt, committees_per_slot) = chain.with_committee_cache( attestation.data.target.root, attestation.data.slot.epoch(T::EthSpec::slots_per_epoch()), - |committee_cache, _| { + |cached_shuffling, _| { + let committee_cache = cached_shuffling.committee_cache.as_ref(); let committee_opt = committee_cache .get_beacon_committee(attestation.data.slot, attestation.committee_index) .map(|beacon_committee| beacon_committee.committee.to_vec()); @@ -1574,7 +1575,8 @@ where return Err(Error::UnknownTargetRoot(target.root)); } - chain.with_committee_cache(target.root, attestation_epoch, |committee_cache, _| { + chain.with_committee_cache(target.root, attestation_epoch, |cached_shuffling, _| { + let committee_cache = cached_shuffling.committee_cache.as_ref(); let committees_per_slot = committee_cache.committees_per_slot(); Ok(committee_cache diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index f3f6cd299e..d826895a25 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -5,7 +5,6 @@ use crate::attestation_verification::{ }; use crate::beacon_block_streamer::{BeaconBlockStreamer, CheckCaches}; use crate::beacon_proposer_cache::{BeaconProposerCache, EpochBlockProposers}; -use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use crate::block_times_cache::BlockTimesCache; use crate::block_verification::{ BlockError, ExecutionPendingBlock, GossipVerifiedBlock, IntoExecutionPendingBlock, @@ -26,16 +25,15 @@ use crate::data_availability_checker::{ use crate::data_availability_checker::DataAvailabilityChecker; use crate::data_column_verification::{ GossipDataColumnError, GossipPartialDataColumnError, GossipVerifiedDataColumn, - GossipVerifiedPartialDataColumnHeader, KzgVerifiedCustodyPartialDataColumn, - KzgVerifiedPartialDataColumn, PartialColumnVerificationResult, - validate_partial_data_column_sidecar_for_gossip, + GossipVerifiedPartialDataColumnHeader, KzgVerifiedCustodyDataColumn, + KzgVerifiedCustodyPartialDataColumn, KzgVerifiedPartialDataColumn, + PartialColumnVerificationResult, validate_partial_data_column_sidecar_for_gossip, }; use crate::early_attester_cache::EarlyAttesterCache; use crate::envelope_times_cache::EnvelopeTimesCache; 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}; use crate::graffiti_calculator::{GraffitiCalculator, GraffitiSettings}; use crate::light_client_finality_update_verification::{ @@ -77,7 +75,7 @@ use crate::persisted_custody::persist_custody_context; use crate::persisted_fork_choice::PersistedForkChoice; use crate::pre_finalization_cache::PreFinalizationBlockCache; use crate::proposer_preferences_verification::proposer_preference_cache::GossipVerifiedProposerPreferenceCache; -use crate::shuffling_cache::{BlockShufflingIds, ShufflingCache}; +use crate::shuffling_cache::{CachedPTCs, CachedShuffling, ShufflingCache, with_cached_shuffling}; use crate::sync_committee_verification::{ Error as SyncCommitteeError, VerifiedSyncCommitteeMessage, VerifiedSyncContribution, }; @@ -96,8 +94,8 @@ use eth2::types::{ SseExtendedPayloadAttributes, SseHead, }; use execution_layer::{ - BlockProposalContents, BlockProposalContentsType, BuilderParams, ChainHealth, ExecutionLayer, - FailedCondition, PayloadAttributes, PayloadStatus, + BlockProposalContents, BlockProposalContentsType, BuilderParams, ChainHealth, + DEFAULT_GAS_LIMIT, ExecutionLayer, FailedCondition, PayloadAttributes, PayloadStatus, }; use fixed_bytes::FixedBytesExtended; use fork_choice::{ @@ -113,7 +111,7 @@ use operation_pool::{ CompactAttestationRef, OperationPool, PersistedOperationPool, ReceivedPreCapella, }; use parking_lot::{Mutex, RwLock, RwLockWriteGuard}; -use proto_array::{DoNotReOrg, ProposerHeadError}; +use proto_array::{DoNotReOrg, ProposerHeadError, ReOrgThreshold}; use rand::RngCore; use safe_arith::SafeArith; use slasher::Slasher; @@ -325,8 +323,8 @@ pub enum StateSkipConfig { } pub trait BeaconChainTypes: Send + Sync + 'static { - type HotStore: store::ItemStore; - type ColdStore: store::ItemStore; + type HotStore: store::ItemStore; + type ColdStore: store::ItemStore; type SlotClock: slot_clock::SlotClock; type EthSpec: types::EthSpec; } @@ -431,8 +429,6 @@ pub struct BeaconChain { pub(crate) observed_payload_attesters: RwLock>, /// Maintains a record of which validators have proposed blocks for each slot. pub observed_block_producers: RwLock>, - /// Maintains a record of blob sidecars seen over the gossip network. - pub observed_blob_sidecars: RwLock, T::EthSpec>>, /// Maintains a record of column sidecars seen over the gossip network. pub observed_column_sidecars: RwLock, T::EthSpec>>, @@ -472,7 +468,7 @@ pub struct BeaconChain { /// HTTP server is enabled. pub event_handler: Option>, /// Caches the attester shuffling for a given epoch and shuffling key root. - pub shuffling_cache: RwLock, + pub shuffling_cache: RwLock>, /// Caches the beacon block proposer shuffling for a given epoch and shuffling key root. pub beacon_proposer_cache: Arc>, /// Caches a map of `validator_index -> validator_pubkey`. @@ -1696,7 +1692,8 @@ impl BeaconChain { let (duties, dependent_root) = self.with_committee_cache( head_block_root, epoch, - |committee_cache, dependent_root| { + |cached_shuffling, dependent_root| { + let committee_cache = cached_shuffling.committee_cache.as_ref(); let duties = validator_indices .iter() .map(|validator_index| { @@ -2185,12 +2182,20 @@ impl BeaconChain { // TODO(gloas) do we want to use a dedicated envelope cache instead? // Maybe the new gloas DA cache? (Or should the gloas DA cache use - // the envelopes_times_cache internally?) + // the envelopes_times_cache internally? + // The payload is considered present only if it was observed before + // the payload due deadline (PAYLOAD_DUE_BPS into the slot). + let payload_due = self.spec.get_payload_due(); let payload_present = self .envelope_times_cache .read() .cache - .contains_key(&beacon_block_root); + .get(&beacon_block_root) + .and_then(|entry| entry.timestamps.observed) + .is_some_and(|observed| { + let slot_start = self.slot_clock.start_of(request_slot); + slot_start.is_some_and(|start| observed.saturating_sub(start) < payload_due) + }); // TODO(EIP-7732): Check blob data availability. For now, default to true. let blob_data_available = true; @@ -2444,19 +2449,6 @@ impl BeaconChain { ret } - #[instrument(skip_all, level = "trace")] - pub fn verify_blob_sidecar_for_gossip( - self: &Arc, - blob_sidecar: Arc>, - subnet_id: u64, - ) -> Result, GossipBlobError> { - metrics::inc_counter(&metrics::BLOBS_SIDECAR_PROCESSING_REQUESTS); - let _timer = metrics::start_timer(&metrics::BLOBS_SIDECAR_GOSSIP_VERIFICATION_TIMES); - GossipVerifiedBlob::new(blob_sidecar, subnet_id, self).inspect(|_| { - metrics::inc_counter(&metrics::BLOBS_SIDECAR_PROCESSING_SUCCESSES); - }) - } - /// Accepts some 'LightClientOptimisticUpdate' from the network and attempts to verify it pub fn verify_optimistic_update_for_gossip( self: &Arc, @@ -3244,35 +3236,6 @@ impl BeaconChain { .map_err(BeaconChainError::TokioJoin)? } - /// Cache the blob in the processing cache, process it, then evict it from the cache if it was - /// imported or errors. - #[instrument(skip_all, level = "debug")] - pub async fn process_gossip_blob( - self: &Arc, - blob: GossipVerifiedBlob, - ) -> Result { - let block_root = blob.block_root(); - - // If this block has already been imported to forkchoice it must have been available, so - // we don't need to process its blobs again. - if self - .canonical_head - .fork_choice_read_lock() - .contains_block(&block_root) - { - return Err(BlockError::DuplicateFullyImported(blob.block_root())); - } - - // No need to process and import blobs beyond the PeerDAS epoch. - if self.spec.is_peer_das_enabled_for_epoch(blob.epoch()) { - return Err(BlockError::BlobNotRequired(blob.slot())); - } - - self.emit_sse_blob_sidecar_events(&block_root, std::iter::once(blob.as_blob())); - - self.check_gossip_blob_availability_and_import(blob).await - } - /// Cache the data columns in the processing cache, process it, then evict it from the cache if it was /// imported or errors. /// Only accepts full columns. Partials are handled via PartialDataColumnAssembler. @@ -3419,19 +3382,21 @@ impl BeaconChain { return Err(BlockError::DuplicateFullyImported(block_root)); } - // Reject RPC blobs referencing unknown parents. Otherwise we allow potentially invalid data - // into the da_checker, where invalid = descendant of invalid blocks. - // Note: blobs should have at least one item and all items have the same parent root. - if let Some(parent_root) = blobs - .iter() - .filter_map(|b| b.as_ref().map(|b| b.block_parent_root())) - .next() - && !self - .canonical_head - .fork_choice_read_lock() - .contains_block(&parent_root) - { - return Err(BlockError::ParentUnknown { parent_root }); + for blob in &blobs { + if let Some(blob) = blob.as_ref() { + // Reject RPC blobs referencing unknown parents. Otherwise we allow potentially invalid data + // into the da_checker, where invalid = descendant of invalid blocks. + // Note: blobs should have at least one item and all items have the same parent root. + if !self + .canonical_head + .fork_choice_read_lock() + .contains_block(&blob.block_parent_root()) + { + return Err(BlockError::ParentUnknown { + parent_root: blob.block_parent_root(), + }); + } + } } self.emit_sse_blob_sidecar_events(&block_root, blobs.iter().flatten().map(Arc::as_ref)); @@ -3445,7 +3410,7 @@ impl BeaconChain { self: &Arc, slot: Slot, block_root: Hash256, - engine_get_blobs_output: EngineGetBlobsOutput, + engine_get_blobs_output: Vec>, ) -> Result { // If this block has already been imported to forkchoice it must have been available, so // we don't need to process its blobs again. @@ -3457,17 +3422,12 @@ impl BeaconChain { return Err(BlockError::DuplicateFullyImported(block_root)); } - match &engine_get_blobs_output { - EngineGetBlobsOutput::Blobs(blobs) => { - self.emit_sse_blob_sidecar_events(&block_root, blobs.iter().map(|b| b.as_blob())); - } - EngineGetBlobsOutput::CustodyColumns(columns) => { - self.emit_sse_data_column_sidecar_events( - &block_root, - columns.iter().map(|column| column.as_data_column()), - ); - } - } + self.emit_sse_data_column_sidecar_events( + &block_root, + engine_get_blobs_output + .iter() + .map(|column| column.as_data_column()), + ); self.check_engine_blobs_availability_and_import(slot, block_root, engine_get_blobs_output) .await @@ -3906,24 +3866,6 @@ impl BeaconChain { .await } - /// Checks if the provided blob can make any cached blocks available, and imports immediately - /// if so, otherwise caches the blob in the data availability checker. - async fn check_gossip_blob_availability_and_import( - self: &Arc, - blob: GossipVerifiedBlob, - ) -> Result { - let slot = blob.slot(); - if let Some(slasher) = self.slasher.as_ref() { - slasher.accept_block_header(blob.signed_block_header()); - } - let availability = self - .data_availability_checker - .put_gossip_verified_blobs(blob.block_root(), std::iter::once(blob))?; - - self.process_availability(slot, availability, || Ok(())) - .await - } - /// Checks if the provided data column can make any cached blocks available, and imports immediately /// if so, otherwise caches the data column in the data availability checker. /// Check gossip data columns for availability and import. Only accepts full columns. @@ -4006,7 +3948,7 @@ impl BeaconChain { ) -> Result { self.check_blob_header_signature_and_slashability( block_root, - blobs.iter().flatten().map(Arc::as_ref), + blobs.iter().flatten().map(|b| b.as_ref()), )?; let availability = self .data_availability_checker @@ -4021,56 +3963,36 @@ impl BeaconChain { self: &Arc, slot: Slot, block_root: Hash256, - engine_get_blobs_output: EngineGetBlobsOutput, + engine_get_blobs_output: Vec>, ) -> Result { - match engine_get_blobs_output { - EngineGetBlobsOutput::Blobs(blobs) => { - self.check_blob_header_signature_and_slashability( - block_root, - blobs.iter().map(|b| b.as_blob()), - )?; - let availability = self - .data_availability_checker - .put_kzg_verified_blobs(block_root, blobs) - .map_err(BlockError::from)?; - - Ok(self - .process_availability(slot, availability, || Ok(())) - .await?) - } - EngineGetBlobsOutput::CustodyColumns(data_columns) => { - // TODO(gloas) verify that this check is no longer relevant for gloas - self.check_data_column_sidecar_header_signature_and_slashability( - block_root, - data_columns - .iter() - .filter_map(|c| match c.as_data_column() { - DataColumnSidecar::Fulu(column) => Some(column), - _ => None, - }), - )?; - if self - .spec - .fork_name_at_slot::(slot) - .gloas_enabled() - { - let availability = self - .pending_payload_cache - .put_kzg_verified_custody_data_columns(block_root, &data_columns) - .map_err(BlockError::from)?; - Ok(self - .process_payload_envelope_availability(slot, availability, || Ok(())) - .await?) - } else { - let availability = self - .data_availability_checker - .put_kzg_verified_custody_data_columns(block_root, data_columns) - .map_err(BlockError::from)?; - Ok(self - .process_availability(slot, availability, || Ok(())) - .await?) - } - } + // TODO(gloas) verify that this check is no longer relevant for gloas + self.check_data_column_sidecar_header_signature_and_slashability( + block_root, + engine_get_blobs_output + .iter() + .filter_map(|c| match c.as_data_column() { + DataColumnSidecar::Fulu(column) => Some(column), + _ => None, + }), + )?; + if self + .spec + .fork_name_at_slot::(slot) + .gloas_enabled() + { + let availability = self + .pending_payload_cache + .put_kzg_verified_custody_data_columns(block_root, &engine_get_blobs_output) + .map_err(BlockError::from)?; + self.process_payload_envelope_availability(slot, availability, || Ok(())) + .await + } else { + let availability = self + .data_availability_checker + .put_kzg_verified_custody_data_columns(block_root, engine_get_blobs_output) + .map_err(BlockError::from)?; + self.process_availability(slot, availability, || Ok(())) + .await } } @@ -4906,15 +4828,20 @@ impl BeaconChain { ) -> Result<(), BlockError> { for relative_epoch in [RelativeEpoch::Current, RelativeEpoch::Next] { let shuffling_id = AttestationShufflingId::new(block_root, state, relative_epoch)?; + let shuffling_epoch = relative_epoch.into_epoch(state.current_epoch()); - let shuffling_is_cached = self.shuffling_cache.read().contains(&shuffling_id); + if self.shuffling_cache.read().contains(&shuffling_id) { + continue; + } - if !shuffling_is_cached { - state.build_committee_cache(relative_epoch, &self.spec)?; - let committee_cache = state.committee_cache(relative_epoch)?; - self.shuffling_cache - .write() - .insert_committee_cache(shuffling_id, committee_cache); + state.build_committee_cache(relative_epoch, &self.spec)?; + let committee_cache = state.committee_cache(relative_epoch)?.clone(); + + if let Some(ptcs) = CachedPTCs::try_from_state(state, shuffling_epoch, &self.spec)? { + self.shuffling_cache.write().insert_committee_cache( + shuffling_id, + CachedShuffling::new(committee_cache, ptcs), + ); } } Ok(()) @@ -5231,15 +5158,14 @@ impl BeaconChain { let _timer = metrics::start_timer(&metrics::FORK_CHOICE_OVERRIDE_FCU_TIMES); // Never override if proposer re-orgs are disabled. - let re_org_head_threshold = self - .config - .re_org_head_threshold - .ok_or(Box::new(DoNotReOrg::ReOrgsDisabled.into()))?; + if self.config.disable_proposer_reorg { + return Err(Box::new(DoNotReOrg::ReOrgsDisabled.into())); + }; - let re_org_parent_threshold = self - .config - .re_org_parent_threshold - .ok_or(Box::new(DoNotReOrg::ReOrgsDisabled.into()))?; + let re_org_head_threshold = ReOrgThreshold(self.spec.reorg_head_weight_threshold); + let re_org_parent_threshold = ReOrgThreshold(self.spec.reorg_parent_weight_threshold); + let re_org_max_epochs_since_finalization = + Epoch::new(self.spec.reorg_max_epochs_since_finalization); let head_block_root = canonical_forkchoice_params.head_root; @@ -5252,7 +5178,7 @@ impl BeaconChain { re_org_head_threshold, re_org_parent_threshold, &self.config.re_org_disallowed_offsets, - self.config.re_org_max_epochs_since_finalization, + re_org_max_epochs_since_finalization, ) .map_err(|e| e.map_inner_error(Error::ProposerHeadForkChoiceError))?; @@ -5273,7 +5199,12 @@ impl BeaconChain { .and_then(|slot_start| { let now = self.slot_clock.now_duration()?; let slot_delay = now.saturating_sub(slot_start); - Some(slot_delay <= self.config.re_org_cutoff(self.spec.get_slot_duration())) + let re_org_cutoff_duration = self + .spec + .compute_slot_component_duration(self.spec.proposer_reorg_cutoff_bps) + .ok()?; + + Some(slot_delay <= re_org_cutoff_duration) }) .unwrap_or(false) } else { @@ -6476,6 +6407,19 @@ impl BeaconChain { None }; + let target_gas_limit = if prepare_slot_fork.gloas_enabled() { + let proposer_gas_limit = execution_layer.get_proposer_gas_limit(proposer).await; + if proposer_gas_limit.is_none() { + warn!( + %proposer, + "No proposer gas limit configured, falling back to parent gas limit" + ); + } + proposer_gas_limit.or(Some(DEFAULT_GAS_LIMIT)) + } else { + None + }; + let payload_attributes = PayloadAttributes::new( self.slot_clock .start_of(prepare_slot) @@ -6486,6 +6430,7 @@ impl BeaconChain { withdrawals.map(Into::into), parent_beacon_block_root, slot_number, + target_gas_limit, ); execution_layer @@ -6987,11 +6932,11 @@ impl BeaconChain { ) } - /// Runs the `map_fn` with the committee cache for `shuffling_epoch` from the chain with head + /// Runs the `map_fn` with the cached shuffling for `shuffling_epoch` from the chain with head /// `head_block_root`. The `map_fn` will be supplied two values: /// - /// - `&CommitteeCache`: the committee cache that serves the given parameters. - /// - `Hash256`: the "shuffling decision root" which uniquely identifies the `CommitteeCache`. + /// - `&CachedShuffling`: the committee cache and optional PTCs that serve the given parameters. + /// - `Hash256`: the "shuffling decision root" which uniquely identifies the cached shuffling. /// /// It's not necessary that `head_block_root` matches our current view of the chain, it can be /// any block that is: @@ -7008,12 +6953,12 @@ impl BeaconChain { /// /// ## Notes /// - /// This function exists in this odd "map" pattern because efficiently obtaining a committee + /// This function exists in this odd "map" pattern because efficiently obtaining a shuffling /// can be complex. It might involve reading straight from the `beacon_chain.shuffling_cache` /// or it might involve reading it from a state from the DB. Due to the complexities of /// `RwLock`s on the shuffling cache, a simple `Cow` isn't suitable here. /// - /// If the committee for `(head_block_root, shuffling_epoch)` isn't found in the + /// If the shuffling for `(head_block_root, shuffling_epoch)` isn't found in the /// `shuffling_cache`, we will read a state from disk and then update the `shuffling_cache`. pub fn with_committee_cache( &self, @@ -7022,149 +6967,17 @@ impl BeaconChain { map_fn: F, ) -> Result where - F: Fn(&CommitteeCache, Hash256) -> Result, + F: Fn(&CachedShuffling, Hash256) -> Result, { - let head_block = self - .canonical_head - .fork_choice_read_lock() - .get_block(&head_block_root) - .ok_or(Error::MissingBeaconBlock(head_block_root))?; - - let shuffling_id = BlockShufflingIds { - current: head_block.current_epoch_shuffling_id.clone(), - next: head_block.next_epoch_shuffling_id.clone(), - previous: None, - block_root: head_block.root, - } - .id_for_epoch(shuffling_epoch) - .ok_or_else(|| Error::InvalidShufflingId { + with_cached_shuffling( + &self.canonical_head, + &self.shuffling_cache, + &self.store, + &self.spec, + head_block_root, shuffling_epoch, - head_block_epoch: head_block.slot.epoch(T::EthSpec::slots_per_epoch()), - })?; - - // Obtain the shuffling cache, timing how long we wait. - let mut shuffling_cache = { - let _ = - metrics::start_timer(&metrics::ATTESTATION_PROCESSING_SHUFFLING_CACHE_WAIT_TIMES); - self.shuffling_cache.write() - }; - - if let Some(cache_item) = shuffling_cache.get(&shuffling_id) { - // The shuffling cache is no longer required, drop the write-lock to allow concurrent - // access. - drop(shuffling_cache); - - let committee_cache = cache_item.wait()?; - map_fn(&committee_cache, shuffling_id.shuffling_decision_block) - } else { - // Create an entry in the cache that "promises" this value will eventually be computed. - // This avoids the case where multiple threads attempt to produce the same value at the - // same time. - // - // Creating the promise whilst we hold the `shuffling_cache` lock will prevent the same - // promise from being created twice. - let sender = shuffling_cache.create_promise(shuffling_id.clone())?; - - // Drop the shuffling cache to avoid holding the lock for any longer than - // required. - drop(shuffling_cache); - - debug!( - shuffling_id = ?shuffling_epoch, - head_block_root = head_block_root.to_string(), - "Committee cache miss" - ); - - // If the block's state will be so far ahead of `shuffling_epoch` that even its - // previous epoch committee cache will be too new, then error. Callers of this function - // shouldn't be requesting such old shufflings for this `head_block_root`. - let head_block_epoch = head_block.slot.epoch(T::EthSpec::slots_per_epoch()); - if head_block_epoch > shuffling_epoch + 1 { - return Err(Error::InvalidStateForShuffling { - state_epoch: head_block_epoch, - shuffling_epoch, - }); - } - - let state_read_timer = - metrics::start_timer(&metrics::ATTESTATION_PROCESSING_STATE_READ_TIMES); - - // If the head of the chain can serve this request, use it. - // - // This code is a little awkward because we need to ensure that the head we read and - // the head we copy is identical. Taking one lock to read the head values and another - // to copy the head is liable to race-conditions. - let head_state_opt = self.with_head(|head| { - if head.beacon_block_root == head_block_root { - Ok(Some((head.beacon_state.clone(), head.beacon_state_root()))) - } else { - Ok::<_, Error>(None) - } - })?; - - // Compute the `target_slot` to advance the block's state to. - // - // Since there's a one-epoch look-ahead on the attester shuffling, it suffices to - // only advance into the first slot of the epoch prior to `shuffling_epoch`. - // - // If the `head_block` is already ahead of that slot, then we should load the state - // at that slot, as we've determined above that the `shuffling_epoch` cache will - // not be too far in the past. - let target_slot = std::cmp::max( - shuffling_epoch - .saturating_sub(1_u64) - .start_slot(T::EthSpec::slots_per_epoch()), - head_block.slot, - ); - - // If the head state is useful for this request, use it. Otherwise, read a state from - // disk that is advanced as close as possible to `target_slot`. - let (mut state, state_root) = if let Some((state, state_root)) = head_state_opt { - (state, state_root) - } else { - // We assume that the `Pending` state has the same shufflings as a `Full` state - // for the same block. Analysis: https://hackmd.io/@dapplion/gloas_dependant_root - let (state_root, state) = self - .store - .get_advanced_hot_state(head_block_root, target_slot, head_block.state_root)? - .ok_or(Error::MissingBeaconState(head_block.state_root))?; - (state, state_root) - }; - - metrics::stop_timer(state_read_timer); - let state_skip_timer = - metrics::start_timer(&metrics::ATTESTATION_PROCESSING_STATE_SKIP_TIMES); - - // If the state is still in an earlier epoch, advance it to the `target_slot` so - // that its next epoch committee cache matches the `shuffling_epoch`. - if state.current_epoch() + 1 < shuffling_epoch { - // Advance the state into the required slot, using the "partial" method since the - // state roots are not relevant for the shuffling. - partial_state_advance(&mut state, Some(state_root), target_slot, &self.spec)?; - } - metrics::stop_timer(state_skip_timer); - - let committee_building_timer = - metrics::start_timer(&metrics::ATTESTATION_PROCESSING_COMMITTEE_BUILDING_TIMES); - - let relative_epoch = RelativeEpoch::from_epoch(state.current_epoch(), shuffling_epoch) - .map_err(Error::IncorrectStateForAttestation)?; - - state.build_committee_cache(relative_epoch, &self.spec)?; - - let committee_cache = state.committee_cache(relative_epoch)?.clone(); - let shuffling_decision_block = shuffling_id.shuffling_decision_block; - - self.shuffling_cache - .write() - .insert_committee_cache(shuffling_id, &committee_cache); - - metrics::stop_timer(committee_building_timer); - - sender.send(committee_cache.clone()); - - map_fn(&committee_cache, shuffling_decision_block) - } + map_fn, + ) } /// Dumps the entire canonical chain, from the head to genesis to a vector for analysis. diff --git a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs index 95fde28f5b..133eaa2fc6 100644 --- a/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs +++ b/beacon_node/beacon_chain/src/beacon_fork_choice_store.rs @@ -129,8 +129,8 @@ impl BalancesCache { /// Implements `fork_choice::ForkChoiceStore` in order to provide a persistent backing to the /// `fork_choice::ForkChoice` struct. #[derive(Debug, Educe)] -#[educe(PartialEq(bound(E: EthSpec, Hot: ItemStore, Cold: ItemStore)))] -pub struct BeaconForkChoiceStore, Cold: ItemStore> { +#[educe(PartialEq(bound(E: EthSpec, Hot: ItemStore, Cold: ItemStore)))] +pub struct BeaconForkChoiceStore { #[educe(PartialEq(ignore))] store: Arc>, balances_cache: BalancesCache, @@ -150,8 +150,8 @@ pub struct BeaconForkChoiceStore, Cold: ItemStore< impl BeaconForkChoiceStore where E: EthSpec, - Hot: ItemStore, - Cold: ItemStore, + Hot: ItemStore, + Cold: ItemStore, { /// Initialize `Self` from some `anchor` checkpoint which may or may not be the genesis state. /// @@ -267,8 +267,8 @@ where impl ForkChoiceStore for BeaconForkChoiceStore where E: EthSpec, - Hot: ItemStore, - Cold: ItemStore, + Hot: ItemStore, + Cold: ItemStore, { type Error = Error; diff --git a/beacon_node/beacon_chain/src/blob_verification.rs b/beacon_node/beacon_chain/src/blob_verification.rs index e557a24369..79b2969645 100644 --- a/beacon_node/beacon_chain/src/blob_verification.rs +++ b/beacon_node/beacon_chain/src/blob_verification.rs @@ -1,249 +1,11 @@ -use educe::Educe; -use slot_clock::SlotClock; -use std::marker::PhantomData; -use std::sync::Arc; - -use crate::beacon_chain::{BeaconChain, BeaconChainTypes}; -use crate::block_verification::{ - BlockSlashInfo, get_validator_pubkey_cache, process_block_slash_info, -}; use crate::kzg_utils::{validate_blob, validate_blobs}; -use crate::observed_data_sidecars::{ - Error as ObservedDataSidecarsError, ObservationStrategy, Observe, -}; -use crate::{BeaconChainError, metrics}; +use educe::Educe; use kzg::{Error as KzgError, Kzg, KzgCommitment}; use ssz_derive::{Decode, Encode}; +use std::sync::Arc; use std::time::Duration; -use tracing::{debug, instrument}; -use tree_hash::TreeHash; -use types::data::BlobIdentifier; -use types::{ - BeaconStateError, BlobSidecar, Epoch, EthSpec, Hash256, SignedBeaconBlockHeader, Slot, -}; - -/// An error occurred while validating a gossip blob. -#[derive(Debug)] -pub enum GossipBlobError { - /// The blob sidecar is from a slot that is later than the current slot (with respect to the - /// gossip clock disparity). - /// - /// ## Peer scoring - /// - /// Assuming the local clock is correct, the peer has sent an invalid message. - FutureSlot { - message_slot: Slot, - latest_permissible_slot: Slot, - }, - - /// There was an error whilst processing the blob. It is not known if it is - /// valid or invalid. - /// - /// ## Peer scoring - /// - /// We were unable to process this blob due to an internal error. It's - /// unclear if the blob is valid. - BeaconChainError(Box), - - /// The `BlobSidecar` was gossiped over an incorrect subnet. - /// - /// ## Peer scoring - /// - /// The blob is invalid or the peer is faulty. - InvalidSubnet { expected: u64, received: u64 }, - - /// The sidecar corresponds to a slot older than the finalized head slot. - /// - /// ## Peer scoring - /// - /// It's unclear if this blob is valid, but this blob is for a finalized slot and is - /// therefore useless to us. - PastFinalizedSlot { - blob_slot: Slot, - finalized_slot: Slot, - }, - - /// The proposer index specified in the sidecar does not match the locally computed - /// proposer index. - /// - /// ## Peer scoring - /// - /// The blob is invalid and the peer is faulty. - ProposerIndexMismatch { sidecar: usize, local: usize }, - - /// The proposal signature in invalid. - /// - /// ## Peer scoring - /// - /// The blob is invalid and the peer is faulty. - ProposalSignatureInvalid, - - /// The proposal_index corresponding to blob.beacon_block_root is not known. - /// - /// ## Peer scoring - /// - /// The blob is invalid and the peer is faulty. - UnknownValidator(u64), - - /// The provided blob is not from a later slot than its parent. - /// - /// ## Peer scoring - /// - /// The blob is invalid and the peer is faulty. - BlobIsNotLaterThanParent { blob_slot: Slot, parent_slot: Slot }, - - /// The provided blob's parent block is unknown. - /// - /// ## Peer scoring - /// - /// We cannot process the blob without validating its parent, the peer isn't necessarily faulty. - ParentUnknown { parent_root: Hash256 }, - - /// Invalid kzg commitment inclusion proof - /// ## Peer scoring - /// - /// The blob sidecar is invalid and the peer is faulty - InvalidInclusionProof, - - /// A blob has already been seen for the given `(sidecar.block_root, sidecar.index)` tuple - /// over gossip or no gossip sources. - /// - /// ## Peer scoring - /// - /// The peer isn't faulty, but we do not forward it over gossip. - RepeatBlob { - proposer: u64, - slot: Slot, - index: u64, - }, - - /// The kzg verification failed. - /// - /// ## Peer scoring - /// - /// The blob sidecar is invalid and the peer is faulty. - KzgError(kzg::Error), - - /// The pubkey cache timed out. - /// - /// ## Peer scoring - /// - /// The blob sidecar may be valid, this is an internal error. - PubkeyCacheTimeout, - - /// The block conflicts with finalization, no need to propagate. - /// - /// ## Peer scoring - /// - /// It's unclear if this block is valid, but it conflicts with finality and shouldn't be - /// imported. - NotFinalizedDescendant { block_parent_root: Hash256 }, -} - -impl std::fmt::Display for GossipBlobError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) - } -} - -impl From for GossipBlobError { - fn from(e: BeaconChainError) -> Self { - GossipBlobError::BeaconChainError(e.into()) - } -} - -impl From for GossipBlobError { - fn from(e: BeaconStateError) -> Self { - GossipBlobError::BeaconChainError(BeaconChainError::BeaconStateError(e).into()) - } -} - -/// A wrapper around a `BlobSidecar` that indicates it has been approved for re-gossiping on -/// the p2p network. -#[derive(Debug)] -pub struct GossipVerifiedBlob { - block_root: Hash256, - blob: KzgVerifiedBlob, - _phantom: PhantomData, -} - -impl Clone for GossipVerifiedBlob { - fn clone(&self) -> Self { - Self { - block_root: self.block_root, - blob: self.blob.clone(), - _phantom: PhantomData, - } - } -} - -impl GossipVerifiedBlob { - pub fn new( - blob: Arc>, - subnet_id: u64, - chain: &BeaconChain, - ) -> Result { - let header = blob.signed_block_header.clone(); - // We only process slashing info if the gossip verification failed - // since we do not process the blob any further in that case. - validate_blob_sidecar_for_gossip::(blob, subnet_id, chain).map_err(|e| { - process_block_slash_info::<_, GossipBlobError>( - chain, - BlockSlashInfo::from_early_error_blob(header, e), - ) - }) - } - /// Construct a `GossipVerifiedBlob` that is assumed to be valid. - /// - /// This should ONLY be used for testing. - pub fn __assumed_valid(blob: Arc>) -> Self { - Self { - block_root: blob.block_root(), - blob: KzgVerifiedBlob { - blob, - seen_timestamp: Duration::from_secs(0), - }, - _phantom: PhantomData, - } - } - pub fn id(&self) -> BlobIdentifier { - BlobIdentifier { - block_root: self.block_root, - index: self.blob.blob_index(), - } - } - pub fn block_root(&self) -> Hash256 { - self.block_root - } - pub fn slot(&self) -> Slot { - self.blob.blob.slot() - } - pub fn epoch(&self) -> Epoch { - self.blob.blob.epoch() - } - pub fn index(&self) -> u64 { - self.blob.blob.index - } - pub fn kzg_commitment(&self) -> KzgCommitment { - self.blob.blob.kzg_commitment - } - pub fn signed_block_header(&self) -> SignedBeaconBlockHeader { - self.blob.blob.signed_block_header.clone() - } - pub fn block_proposer_index(&self) -> u64 { - self.blob.blob.block_proposer_index() - } - pub fn into_inner(self) -> KzgVerifiedBlob { - self.blob - } - pub fn as_blob(&self) -> &BlobSidecar { - self.blob.as_blob() - } - /// This is cheap as we're calling clone on an Arc - pub fn clone_blob(&self) -> Arc> { - self.blob.clone_blob() - } -} +use tracing::instrument; +use types::{BlobSidecar, EthSpec}; /// Wrapper over a `BlobSidecar` for which we have completed kzg verification. /// i.e. `verify_blob_kzg_proof(blob, commitment, proof) == true`. @@ -388,239 +150,3 @@ where .unzip(); validate_blobs::(kzg, commitments.as_slice(), blobs, proofs.as_slice()) } - -pub fn validate_blob_sidecar_for_gossip( - blob_sidecar: Arc>, - subnet: u64, - chain: &BeaconChain, -) -> Result, GossipBlobError> { - let blob_slot = blob_sidecar.slot(); - let blob_index = blob_sidecar.index; - let block_parent_root = blob_sidecar.block_parent_root(); - let blob_proposer_index = blob_sidecar.block_proposer_index(); - let block_root = blob_sidecar.block_root(); - let blob_epoch = blob_slot.epoch(T::EthSpec::slots_per_epoch()); - let signed_block_header = &blob_sidecar.signed_block_header; - - let seen_timestamp = chain.slot_clock.now_duration().unwrap_or_default(); - - // This condition is not possible if we have received the blob from the network - // since we only subscribe to `MaxBlobsPerBlock` subnets over gossip network. - // We include this check only for completeness. - // Getting this error would imply something very wrong with our networking decoding logic. - if blob_index >= chain.spec.max_blobs_per_block(blob_epoch) { - return Err(GossipBlobError::InvalidSubnet { - expected: subnet, - received: blob_index, - }); - } - - // Verify that the blob_sidecar was received on the correct subnet. - if blob_index != subnet { - return Err(GossipBlobError::InvalidSubnet { - expected: subnet, - received: blob_index, - }); - } - - // Verify that the sidecar is not from a future slot. - let latest_permissible_slot = chain - .slot_clock - .now_with_future_tolerance(chain.spec.maximum_gossip_clock_disparity()) - .ok_or(BeaconChainError::UnableToReadSlot)?; - if blob_slot > latest_permissible_slot { - return Err(GossipBlobError::FutureSlot { - message_slot: blob_slot, - latest_permissible_slot, - }); - } - - // Verify that the sidecar slot is greater than the latest finalized slot - let latest_finalized_slot = chain - .head() - .finalized_checkpoint() - .epoch - .start_slot(T::EthSpec::slots_per_epoch()); - if blob_slot <= latest_finalized_slot { - return Err(GossipBlobError::PastFinalizedSlot { - blob_slot, - finalized_slot: latest_finalized_slot, - }); - } - - // Verify that this is the first blob sidecar received for the tuple: - // (block_header.slot, block_header.proposer_index, blob_sidecar.index) - if chain - .observed_blob_sidecars - .read() - .observation_key_is_known(&blob_sidecar) - .map_err(|e| GossipBlobError::BeaconChainError(Box::new(e.into())))? - .is_some() - { - return Err(GossipBlobError::RepeatBlob { - proposer: blob_proposer_index, - slot: blob_slot, - index: blob_index, - }); - } - - // Verify the inclusion proof in the sidecar - let _timer = metrics::start_timer(&metrics::BLOB_SIDECAR_INCLUSION_PROOF_VERIFICATION); - if !blob_sidecar.verify_blob_sidecar_inclusion_proof() { - return Err(GossipBlobError::InvalidInclusionProof); - } - drop(_timer); - - let fork_choice = chain.canonical_head.fork_choice_read_lock(); - - // We have already verified that the blob is past finalization, so we can - // just check fork choice for the block's parent. - let Some(parent_block) = fork_choice.get_block(&block_parent_root) else { - return Err(GossipBlobError::ParentUnknown { - parent_root: block_parent_root, - }); - }; - - // Do not process a blob that does not descend from the finalized root. - // We just loaded the parent_block, so we can be sure that it exists in fork choice. - if !fork_choice.is_finalized_checkpoint_or_descendant(block_parent_root) { - return Err(GossipBlobError::NotFinalizedDescendant { block_parent_root }); - } - drop(fork_choice); - - if parent_block.slot >= blob_slot { - return Err(GossipBlobError::BlobIsNotLaterThanParent { - blob_slot, - parent_slot: parent_block.slot, - }); - } - - let proposer_shuffling_root = - parent_block.proposer_shuffling_root_for_child_block(blob_epoch, &chain.spec); - - let proposer = chain.with_proposer_cache( - proposer_shuffling_root, - blob_epoch, - |proposers| proposers.get_slot::(blob_slot), - || { - debug!( - %block_root, - index = %blob_index, - "Proposer shuffling cache miss for blob verification" - ); - // Blob verification is only relevant pre-Fulu and pre-Gloas, so `Pending` payload - // status is sufficient. - chain - .store - .get_advanced_hot_state(block_parent_root, blob_slot, parent_block.state_root) - .map_err(|e| GossipBlobError::BeaconChainError(Box::new(e.into())))? - .ok_or_else(|| { - GossipBlobError::BeaconChainError(Box::new(BeaconChainError::DBInconsistent( - format!("Missing state for parent block {block_parent_root:?}",), - ))) - }) - }, - )?; - let proposer_index = proposer.index; - let fork = proposer.fork; - - // Signature verify the signed block header. - let signature_is_valid = { - let pubkey_cache = - get_validator_pubkey_cache(chain).map_err(|_| GossipBlobError::PubkeyCacheTimeout)?; - - let pubkey = pubkey_cache - .get(proposer_index) - .ok_or_else(|| GossipBlobError::UnknownValidator(proposer_index as u64))?; - signed_block_header.verify_signature::( - pubkey, - &fork, - chain.genesis_validators_root, - &chain.spec, - ) - }; - - if !signature_is_valid { - return Err(GossipBlobError::ProposalSignatureInvalid); - } - - if proposer_index != blob_proposer_index as usize { - return Err(GossipBlobError::ProposerIndexMismatch { - sidecar: blob_proposer_index as usize, - local: proposer_index, - }); - } - - // Kzg verification for gossip blob sidecar - let kzg = chain.kzg.as_ref(); - - let kzg_verified_blob = KzgVerifiedBlob::new(blob_sidecar.clone(), kzg, seen_timestamp) - .map_err(GossipBlobError::KzgError)?; - let blob_sidecar = &kzg_verified_blob.blob; - - chain - .observed_slashable - .write() - .observe_slashable( - blob_sidecar.slot(), - blob_sidecar.block_proposer_index(), - block_root, - ) - .map_err(|e| GossipBlobError::BeaconChainError(Box::new(e.into())))?; - - if O::observe() { - observe_gossip_blob(&kzg_verified_blob.blob, chain)?; - } - - Ok(GossipVerifiedBlob { - block_root, - blob: kzg_verified_blob, - _phantom: PhantomData, - }) -} - -pub fn observe_gossip_blob( - blob_sidecar: &BlobSidecar, - chain: &BeaconChain, -) -> Result<(), GossipBlobError> { - // Now the signature is valid, store the proposal so we don't accept another blob sidecar - // with the same `BlobIdentifier`. It's important to double-check that the proposer still - // hasn't been observed so we don't have a race-condition when verifying two blocks - // simultaneously. - // - // Note: If this BlobSidecar goes on to fail full verification, we do not evict it from the - // seen_cache as alternate blob_sidecars for the same identifier can still be retrieved over - // rpc. Evicting them from this cache would allow faster propagation over gossip. So we - // allow retrieval of potentially valid blocks over rpc, but try to punish the proposer for - // signing invalid messages. Issue for more background - // https://github.com/ethereum/consensus-specs/issues/3261 - if chain - .observed_blob_sidecars - .write() - .observe_sidecar(blob_sidecar) - .map_err(|e: ObservedDataSidecarsError| { - GossipBlobError::BeaconChainError(Box::new(e.into())) - })? - .is_some() - { - return Err(GossipBlobError::RepeatBlob { - proposer: blob_sidecar.block_proposer_index(), - slot: blob_sidecar.slot(), - index: blob_sidecar.index, - }); - } - Ok(()) -} - -/// Returns the canonical root of the given `blob`. -/// -/// Use this function to ensure that we report the blob hashing time Prometheus metric. -pub fn get_blob_root(blob: &BlobSidecar) -> Hash256 { - let blob_root_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_BLOB_ROOT); - - let blob_root = blob.tree_hash_root(); - - metrics::stop_timer(blob_root_timer); - - blob_root -} diff --git a/beacon_node/beacon_chain/src/block_production/gloas.rs b/beacon_node/beacon_chain/src/block_production/gloas.rs index 6510c20ba7..82dad6f6ad 100644 --- a/beacon_node/beacon_chain/src/block_production/gloas.rs +++ b/beacon_node/beacon_chain/src/block_production/gloas.rs @@ -2,11 +2,13 @@ use std::collections::{HashMap, HashSet}; use std::marker::PhantomData; use std::sync::Arc; +use proto_array::PayloadStatus; + use bls::{PublicKeyBytes, Signature}; use execution_layer::{ - BlockProposalContentsGloas, BuilderParams, PayloadAttributes, PayloadParameters, + BlockProposalContentsGloas, BuilderParams, DEFAULT_GAS_LIMIT, PayloadAttributes, + PayloadParameters, }; -use fork_choice::PayloadStatus; use operation_pool::CompactAttestationRef; use ssz::Encode; use state_processing::common::{get_attesting_indices_from_state, get_indexed_payload_attestation}; @@ -150,8 +152,24 @@ impl BeaconChain { verification: ProduceBlockVerification, builder_boost_factor: Option, ) -> Result, BlockProductionError> { - // Extract the parent's execution requests from the envelope (if parent was full). - let parent_execution_requests = if parent_payload_status == PayloadStatus::Full { + 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 should_build_on_full = self + .canonical_head + .fork_choice_read_lock() + .should_build_on_full(&parent_root, parent_payload_status) + .map_err(|e| { + BlockProductionError::BeaconChain(Box::new(BeaconChainError::ForkChoiceError(e))) + })?; + + // Extract the parent's execution requests from the envelope (if building on full). + let parent_execution_requests = if should_build_on_full { parent_envelope .as_ref() .map(|env| env.message.execution_requests.clone()) @@ -197,7 +215,7 @@ impl BeaconChain { .clone() .produce_execution_payload_bid( state, - parent_payload_status, + should_build_on_full, parent_envelope, produce_at_slot, BID_VALUE_SELF_BUILD, @@ -700,12 +718,12 @@ impl BeaconChain { /// data needed to construct the `ExecutionPayloadEnvelope` after the beacon block is /// created, plus the EL block value and `should_override_builder` flag used by the /// caller to compare against any cached p2p builder bid. - #[allow(clippy::type_complexity)] + #[allow(clippy::type_complexity, clippy::too_many_arguments)] #[instrument(level = "debug", skip_all)] pub async fn produce_execution_payload_bid( self: Arc, state: BeaconState, - parent_payload_status: PayloadStatus, + should_build_on_full: bool, parent_envelope: Option>>, produce_at_slot: Slot, bid_value: u64, @@ -751,20 +769,18 @@ impl BeaconChain { let parent_bid = state.latest_execution_payload_bid()?; - // TODO(gloas): need should_extend_payload check here as well let parent_block_slot = state.latest_block_header().slot; let parent_is_pre_gloas = !self .spec .fork_name_at_slot::(parent_block_slot) .gloas_enabled(); - let parent_block_hash = - if parent_payload_status == PayloadStatus::Full || parent_is_pre_gloas { - // Build on parent bid's payload. - parent_bid.block_hash - } else { - // Skip parent bid's payload. For genesis this is the EL genesis hash. - parent_bid.parent_block_hash - }; + let parent_block_hash = if should_build_on_full || parent_is_pre_gloas { + // Build on parent bid's payload. + parent_bid.block_hash + } else { + // Skip parent bid's payload. For genesis this is the EL genesis hash. + parent_bid.parent_block_hash + }; // TODO(gloas) this should be BlockProductionVersion::V4 // V3 is okay for now as long as we're not connected to a builder @@ -953,10 +969,7 @@ fn get_execution_payload_gloas( compute_timestamp_at_slot(state, state.slot(), spec).map_err(BeaconStateError::from)?; let random = *state.get_randao_mix(current_epoch)?; - // TODO(gloas): this gas limit calc is not necessarily right let parent_bid = state.latest_execution_payload_bid()?; - let latest_gas_limit = parent_bid.gas_limit; - let is_parent_block_full = parent_block_hash == parent_bid.block_hash; let withdrawals = if is_parent_block_full { @@ -992,7 +1005,6 @@ fn get_execution_payload_gloas( random, proposer_index, parent_block_hash, - latest_gas_limit, builder_params, withdrawals, parent_beacon_block_root, @@ -1020,7 +1032,6 @@ async fn prepare_execution_payload( random: Hash256, proposer_index: u64, parent_block_hash: ExecutionBlockHash, - parent_gas_limit: u64, builder_params: BuilderParams, withdrawals: Vec, parent_beacon_block_root: Hash256, @@ -1058,6 +1069,10 @@ where .get_suggested_fee_recipient(proposer_index) .await; let slot_number = Some(builder_params.slot.as_u64()); + let target_gas_limit = execution_layer + .get_proposer_gas_limit(proposer_index) + .await + .unwrap_or(DEFAULT_GAS_LIMIT); let payload_attributes = PayloadAttributes::new( timestamp, @@ -1066,13 +1081,12 @@ where Some(withdrawals), Some(parent_beacon_block_root), slot_number, + Some(target_gas_limit), ); - - 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, + parent_gas_limit: None, + proposer_gas_limit: Some(target_gas_limit), payload_attributes: &payload_attributes, forkchoice_update_params: &forkchoice_update_params, current_fork: fork, diff --git a/beacon_node/beacon_chain/src/block_production/mod.rs b/beacon_node/beacon_chain/src/block_production/mod.rs index fd5e381023..a94bc697b9 100644 --- a/beacon_node/beacon_chain/src/block_production/mod.rs +++ b/beacon_node/beacon_chain/src/block_production/mod.rs @@ -1,10 +1,10 @@ use std::{sync::Arc, time::Duration}; use fork_choice::PayloadStatus; -use proto_array::ProposerHeadError; +use proto_array::{ProposerHeadError, ReOrgThreshold}; use slot_clock::SlotClock; use tracing::{debug, error, info, instrument, warn}; -use types::{BeaconState, Hash256, SignedExecutionPayloadEnvelope, Slot}; +use types::{BeaconState, Epoch, Hash256, SignedExecutionPayloadEnvelope, Slot}; use crate::{ BeaconChain, BeaconChainTypes, BlockProductionError, StateSkipConfig, @@ -174,8 +174,10 @@ impl BeaconChain { 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?; + let re_org_head_threshold = ReOrgThreshold(self.spec.reorg_head_weight_threshold); + let re_org_parent_threshold = ReOrgThreshold(self.spec.reorg_parent_weight_threshold); + let re_org_max_epochs_since_finalization = + Epoch::new(self.spec.reorg_max_epochs_since_finalization); if self.spec.proposer_score_boost.is_none() { warn!( @@ -198,8 +200,12 @@ impl BeaconChain { // 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()); + let re_org_cutoff_duration = self + .spec + .compute_slot_component_duration(self.spec.proposer_reorg_cutoff_bps) + .ok()?; + + let proposing_on_time = slot_delay < re_org_cutoff_duration; if !proposing_on_time { debug!(reason = "not proposing on time", "Not attempting re-org"); return None; @@ -223,7 +229,7 @@ impl BeaconChain { re_org_head_threshold, re_org_parent_threshold, &self.config.re_org_disallowed_offsets, - self.config.re_org_max_epochs_since_finalization, + re_org_max_epochs_since_finalization, ) .map_err(|e| match e { ProposerHeadError::DoNotReOrg(reason) => { diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index 24f971f736..22e50e4185 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -49,7 +49,6 @@ #![allow(clippy::result_large_err)] use crate::beacon_snapshot::PreProcessingSnapshot; -use crate::blob_verification::GossipBlobError; use crate::block_verification_types::{AsBlock, BlockImportData, LookupBlock, RangeSyncBlock}; use crate::data_availability_checker::{ AvailabilityCheckError, AvailableBlock, AvailableBlockData, MaybeAvailableBlock, @@ -290,14 +289,6 @@ pub enum BlockError { EnvelopeBlockRootUnknown(Hash256), /// Optimistic sync is not supported for Gloas payload envelopes. OptimisticSyncNotSupported { block_root: Hash256 }, - /// A Blob with a slot after PeerDAS is received and is not required to be imported. - /// This can happen because we stay subscribed to the blob subnet after 2 epochs, as we could - /// still receive valid blobs from a Deneb epoch after PeerDAS is activated. - /// - /// ## Peer scoring - /// - /// This indicates the peer is sending an unexpected gossip blob and should be penalised. - BlobNotRequired(Slot), /// An internal error has occurred when processing the block or sidecars. /// /// ## Peer scoring @@ -520,17 +511,6 @@ impl BlockSlashInfo { } } -impl BlockSlashInfo { - pub fn from_early_error_blob(header: SignedBeaconBlockHeader, e: GossipBlobError) -> Self { - match e { - GossipBlobError::ProposalSignatureInvalid => BlockSlashInfo::SignatureInvalid(e), - // `InvalidSignature` could indicate any signature in the block, so we want - // to recheck the proposer signature alone. - _ => BlockSlashInfo::SignatureNotChecked(header, e), - } - } -} - impl BlockSlashInfo { pub fn from_early_error_data_column( header: SignedBeaconBlockHeader, @@ -2038,23 +2018,6 @@ impl BlockBlobError for BlockError { } } -impl BlockBlobError for GossipBlobError { - fn not_later_than_parent_error(blob_slot: Slot, parent_slot: Slot) -> Self { - GossipBlobError::BlobIsNotLaterThanParent { - blob_slot, - parent_slot, - } - } - - fn unknown_validator_error(validator_index: u64) -> Self { - GossipBlobError::UnknownValidator(validator_index) - } - - fn proposer_signature_invalid() -> Self { - GossipBlobError::ProposalSignatureInvalid - } -} - impl BlockBlobError for GossipDataColumnError { fn not_later_than_parent_error(data_column_slot: Slot, parent_slot: Slot) -> Self { GossipDataColumnError::IsNotLaterThanParent { diff --git a/beacon_node/beacon_chain/src/builder.rs b/beacon_node/beacon_chain/src/builder.rs index e668bef7c0..6df0b9c1a9 100644 --- a/beacon_node/beacon_chain/src/builder.rs +++ b/beacon_node/beacon_chain/src/builder.rs @@ -30,7 +30,7 @@ use kzg::Kzg; use logging::crit; use operation_pool::{OperationPool, PersistedOperationPool}; use parking_lot::{Mutex, RwLock}; -use proto_array::{DisallowedReOrgOffsets, ReOrgThreshold}; +use proto_array::DisallowedReOrgOffsets; use rand::RngCore; use rayon::prelude::*; use slasher::Slasher; @@ -47,8 +47,8 @@ use tracing::{debug, error, info, warn}; use tree_hash::TreeHash; use types::data::CustodyIndex; use types::{ - BeaconState, BlobSidecarList, ChainSpec, ColumnIndex, DataColumnSidecarList, Epoch, EthSpec, - Hash256, SignedBeaconBlock, Slot, + BeaconState, BlobSidecarList, ChainSpec, ColumnIndex, DataColumnSidecarList, EthSpec, Hash256, + SignedBeaconBlock, Slot, }; /// An empty struct used to "witness" all the `BeaconChainTypes` traits. It has no user-facing @@ -60,8 +60,8 @@ pub struct Witness( impl BeaconChainTypes for Witness where - THotStore: ItemStore + 'static, - TColdStore: ItemStore + 'static, + THotStore: ItemStore + 'static, + TColdStore: ItemStore + 'static, TSlotClock: SlotClock + 'static, E: EthSpec + 'static, { @@ -115,8 +115,8 @@ pub struct BeaconChainBuilder { impl BeaconChainBuilder> where - THotStore: ItemStore + 'static, - TColdStore: ItemStore + 'static, + THotStore: ItemStore + 'static, + TColdStore: ItemStore + 'static, TSlotClock: SlotClock + 'static, E: EthSpec + 'static, { @@ -176,21 +176,6 @@ where self } - /// Sets the proposer re-org threshold. - pub fn proposer_re_org_head_threshold(mut self, threshold: Option) -> Self { - self.chain_config.re_org_head_threshold = threshold; - self - } - - /// Sets the proposer re-org max epochs since finalization. - pub fn proposer_re_org_max_epochs_since_finalization( - mut self, - epochs_since_finalization: Epoch, - ) -> Self { - self.chain_config.re_org_max_epochs_since_finalization = epochs_since_finalization; - self - } - /// Sets the proposer re-org disallowed offsets list. pub fn proposer_re_org_disallowed_offsets( mut self, @@ -1022,7 +1007,6 @@ where // TODO: allow for persisting and loading the pool from disk. observed_block_producers: <_>::default(), 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(), @@ -1162,8 +1146,8 @@ where impl BeaconChainBuilder> where - THotStore: ItemStore + 'static, - TColdStore: ItemStore + 'static, + THotStore: ItemStore + 'static, + TColdStore: ItemStore + 'static, E: EthSpec + 'static, { /// Sets the `BeaconChain` slot clock to `TestingSlotClock`. @@ -1301,11 +1285,8 @@ mod test { let validator_count = 1; let genesis_time = 13_371_337; - let store: HotColdDB< - MinimalEthSpec, - MemoryStore, - MemoryStore, - > = HotColdDB::open_ephemeral(StoreConfig::default(), ChainSpec::minimal().into()).unwrap(); + let store: HotColdDB = + HotColdDB::open_ephemeral(StoreConfig::default(), ChainSpec::minimal().into()).unwrap(); let spec = MinimalEthSpec::default_spec(); let genesis_state = interop_genesis_state( diff --git a/beacon_node/beacon_chain/src/canonical_head.rs b/beacon_node/beacon_chain/src/canonical_head.rs index b3ab2e6975..1eab7ccf7a 100644 --- a/beacon_node/beacon_chain/src/canonical_head.rs +++ b/beacon_node/beacon_chain/src/canonical_head.rs @@ -967,13 +967,6 @@ impl BeaconChain { .start_slot(T::EthSpec::slots_per_epoch()), ); - self.observed_blob_sidecars.write().prune( - new_view - .finalized_checkpoint - .epoch - .start_slot(T::EthSpec::slots_per_epoch()), - ); - self.observed_column_sidecars.write().prune( new_view .finalized_checkpoint diff --git a/beacon_node/beacon_chain/src/chain_config.rs b/beacon_node/beacon_chain/src/chain_config.rs index b2c017a469..dde09bf105 100644 --- a/beacon_node/beacon_chain/src/chain_config.rs +++ b/beacon_node/beacon_chain/src/chain_config.rs @@ -1,15 +1,10 @@ use crate::custody_context::NodeCustodyType; -pub use proto_array::{DisallowedReOrgOffsets, ReOrgThreshold}; +pub use proto_array::DisallowedReOrgOffsets; use serde::{Deserialize, Serialize}; use std::str::FromStr; use std::{collections::HashSet, sync::LazyLock, time::Duration}; -use types::{Checkpoint, Epoch, Hash256}; +use types::{Checkpoint, Hash256}; -pub const DEFAULT_RE_ORG_HEAD_THRESHOLD: ReOrgThreshold = ReOrgThreshold(20); -pub const DEFAULT_RE_ORG_PARENT_THRESHOLD: ReOrgThreshold = ReOrgThreshold(160); -pub const DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION: Epoch = Epoch::new(2); -/// Default to 1/12th of the slot, which is 1 second on mainnet. -pub const DEFAULT_RE_ORG_CUTOFF_DENOMINATOR: u32 = 12; pub const DEFAULT_FORK_CHOICE_BEFORE_PROPOSAL_TIMEOUT: u64 = 250; /// Default fraction of a slot lookahead for payload preparation (12/3 = 4 seconds on mainnet). @@ -41,14 +36,6 @@ pub struct ChainConfig { pub archive: bool, /// The max size of a message that can be sent over the network. pub max_network_size: usize, - /// Maximum percentage of the head committee weight at which to attempt re-orging the canonical head. - pub re_org_head_threshold: Option, - /// Minimum percentage of the parent committee weight at which to attempt re-orging the canonical head. - pub re_org_parent_threshold: Option, - /// Maximum number of epochs since finalization for attempting a proposer re-org. - pub re_org_max_epochs_since_finalization: Epoch, - /// Maximum delay after the start of the slot at which to propose a reorging block. - pub re_org_cutoff_millis: Option, /// Additional epoch offsets at which re-orging block proposals are not permitted. /// /// By default this list is empty, but it can be useful for reacting to network conditions, e.g. @@ -125,6 +112,8 @@ pub struct ChainConfig { pub enable_partial_columns: bool, /// The node's custody type, determining how many data columns to custody and sample. pub node_custody_type: NodeCustodyType, + /// Disable proposer re-org + pub disable_proposer_reorg: bool, } impl Default for ChainConfig { @@ -134,10 +123,6 @@ impl Default for ChainConfig { weak_subjectivity_checkpoint: None, archive: false, max_network_size: 10 * 1_048_576, // 10M - re_org_head_threshold: Some(DEFAULT_RE_ORG_HEAD_THRESHOLD), - re_org_parent_threshold: Some(DEFAULT_RE_ORG_PARENT_THRESHOLD), - re_org_max_epochs_since_finalization: DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, - re_org_cutoff_millis: None, re_org_disallowed_offsets: DisallowedReOrgOffsets::default(), fork_choice_before_proposal_timeout_ms: DEFAULT_FORK_CHOICE_BEFORE_PROPOSAL_TIMEOUT, // Builder fallback configs that are set in `clap` will override these. @@ -168,15 +153,7 @@ impl Default for ChainConfig { disable_get_blobs: false, enable_partial_columns: false, node_custody_type: NodeCustodyType::Fullnode, + disable_proposer_reorg: false, } } } - -impl ChainConfig { - /// The latest delay from the start of the slot at which to attempt a 1-slot re-org. - pub fn re_org_cutoff(&self, slot_duration: Duration) -> Duration { - self.re_org_cutoff_millis - .map(Duration::from_millis) - .unwrap_or_else(|| slot_duration / DEFAULT_RE_ORG_CUTOFF_DENOMINATOR) - } -} diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index cfd8ee7d34..3c2ba13fed 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -1,6 +1,4 @@ -use crate::blob_verification::{ - GossipVerifiedBlob, KzgVerifiedBlob, KzgVerifiedBlobList, verify_kzg_for_blob_list, -}; +use crate::blob_verification::{KzgVerifiedBlob, KzgVerifiedBlobList, verify_kzg_for_blob_list}; use crate::block_verification_types::{AvailabilityPendingExecutedBlock, AvailableExecutedBlock}; use crate::data_availability_checker::overflow_lru_cache::{ DataAvailabilityCheckerInner, ReconstructColumnsDecision, @@ -364,24 +362,6 @@ impl DataAvailabilityChecker { .put_kzg_verified_data_columns(block_root, verified_custody_columns) } - /// Check if we've cached other blobs for this block. If it completes a set and we also - /// have a block cached, return the `Availability` variant triggering block import. - /// Otherwise cache the blob sidecar. - /// - /// This should only accept gossip verified blobs, so we should not have to worry about dupes. - #[instrument(skip_all, level = "trace")] - pub fn put_gossip_verified_blobs< - I: IntoIterator>, - O: ObservationStrategy, - >( - &self, - block_root: Hash256, - blobs: I, - ) -> Result, AvailabilityCheckError> { - self.availability_cache - .put_kzg_verified_blobs(block_root, blobs.into_iter().map(|b| b.into_inner())) - } - #[instrument(skip_all, level = "trace")] pub fn put_kzg_verified_blobs>>( &self, diff --git a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs index 3034e196b9..2ce0b4cd4a 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs @@ -780,9 +780,11 @@ impl DataAvailabilityCheckerInner { mod test { use super::*; - use crate::test_utils::generate_data_column_indices_rand_order; + use crate::data_column_verification::{GossipVerifiedDataColumn, KzgVerifiedCustodyDataColumn}; + use crate::test_utils::{ + generate_data_column_indices_rand_order, generate_data_column_sidecars_from_block, + }; use crate::{ - blob_verification::GossipVerifiedBlob, block_verification::PayloadVerificationOutcome, block_verification_types::{AsBlock, BlockImportData}, custody_context::NodeCustodyType, @@ -794,15 +796,15 @@ mod test { use store::{HotColdDB, ItemStore, StoreConfig, database::interface::BeaconNodeBackend}; use tempfile::{TempDir, tempdir}; use tracing::info; - use types::MinimalEthSpec; use types::new_non_zero_usize; + use types::{DataColumnSubnetId, MinimalEthSpec}; const LOW_VALIDATOR_COUNT: usize = 32; fn get_store_with_spec( db_path: &TempDir, spec: Arc, - ) -> Arc, BeaconNodeBackend>> { + ) -> Arc> { let hot_path = db_path.path().join("hot_db"); let cold_path = db_path.path().join("cold_db"); let blobs_path = db_path.path().join("blobs_db"); @@ -819,21 +821,25 @@ mod test { .expect("disk store should initialize") } - // get a beacon chain harness advanced to just before deneb fork - async fn get_deneb_chain( + // get a beacon chain harness advanced to just before fulu fork + async fn get_fulu_chain( db_path: &TempDir, ) -> BeaconChainHarness> { let altair_fork_epoch = Epoch::new(0); let bellatrix_fork_epoch = Epoch::new(0); let capella_fork_epoch = Epoch::new(3); let deneb_fork_epoch = Epoch::new(4); - let deneb_fork_slot = deneb_fork_epoch.start_slot(E::slots_per_epoch()); + let electra_fork_epoch = Epoch::new(5); + let fulu_fork_epoch = Epoch::new(6); + let fulu_fork_slot = fulu_fork_epoch.start_slot(E::slots_per_epoch()); let mut spec = E::default_spec(); spec.altair_fork_epoch = Some(altair_fork_epoch); spec.bellatrix_fork_epoch = Some(bellatrix_fork_epoch); spec.capella_fork_epoch = Some(capella_fork_epoch); spec.deneb_fork_epoch = Some(deneb_fork_epoch); + spec.electra_fork_epoch = Some(electra_fork_epoch); + spec.fulu_fork_epoch = Some(fulu_fork_epoch); let spec = Arc::new(spec); let chain_store = get_store_with_spec::(db_path, spec.clone()); @@ -846,8 +852,10 @@ mod test { .mock_execution_layer() .build(); - // go right before deneb slot - harness.extend_to_slot(deneb_fork_slot - 1).await; + harness.execution_block_generator().set_min_blob_count(1); + + // go right before fulu slot + harness.extend_to_slot(fulu_fork_slot - 1).await; harness } @@ -856,12 +864,12 @@ mod test { harness: &BeaconChainHarness>, ) -> ( AvailabilityPendingExecutedBlock, - Vec>>, + Vec>>, ) where E: EthSpec, - Hot: ItemStore, - Cold: ItemStore, + Hot: ItemStore, + Cold: ItemStore, { let chain = &harness.chain; let head = chain.head_snapshot(); @@ -874,7 +882,7 @@ mod test { .expect("should get block") .expect("should have block"); - let (signed_beacon_block_hash, (block, maybe_blobs), state) = harness + let (signed_beacon_block_hash, (block, _maybe_blobs), state) = harness .add_block_at_slot(target_slot, parent_state) .await .expect("should add block"); @@ -892,27 +900,25 @@ mod test { .message() .body() .blob_kzg_commitments() - .expect("should be deneb fork") + .expect("should be fulu fork") .clone(), ) { info!(commitment = ?comm, "kzg commitment"); } info!("done printing kzg commitments"); - let gossip_verified_blobs = if let Some((kzg_proofs, blobs)) = maybe_blobs { - let sidecars = - BlobSidecar::build_sidecars(blobs, &block, kzg_proofs, &chain.spec).unwrap(); - Vec::from(sidecars) - .into_iter() - .map(|sidecar| { - let subnet = sidecar.index; - GossipVerifiedBlob::new(sidecar, subnet, &harness.chain) - .expect("should validate blob") - }) - .collect() - } else { - vec![] - }; + // Generate data columns from the block + let data_columns = generate_data_column_sidecars_from_block(&block, &harness.spec); + + let gossip_verified_columns: Vec<_> = data_columns + .into_iter() + .map(|sidecar| { + let subnet_id = + DataColumnSubnetId::from_column_index(*sidecar.index(), &harness.spec); + GossipVerifiedDataColumn::new(sidecar, subnet_id, &harness.chain) + .expect("should validate data column") + }) + .collect(); let slot = block.slot(); let consensus_context = ConsensusContext::::new(slot); @@ -933,7 +939,7 @@ mod test { payload_verification_outcome, }; - (availability_pending_block, gossip_verified_blobs) + (availability_pending_block, gossip_verified_columns) } async fn setup_harness_and_cache( @@ -946,14 +952,14 @@ mod test { where E: EthSpec, T: BeaconChainTypes< - HotStore = BeaconNodeBackend, - ColdStore = BeaconNodeBackend, + HotStore = BeaconNodeBackend, + ColdStore = BeaconNodeBackend, EthSpec = E, >, { create_test_tracing_subscriber(); let chain_db_path = tempdir().expect("should get temp dir"); - let harness = get_deneb_chain(&chain_db_path).await; + let harness = get_fulu_chain(&chain_db_path).await; let spec = harness.spec.clone(); let capacity_non_zero = new_non_zero_usize(capacity); let custody_context = Arc::new(CustodyContext::new( @@ -979,20 +985,27 @@ mod test { let capacity = 4; let (harness, cache, _path) = setup_harness_and_cache::(capacity).await; - let (pending_block, blobs) = availability_pending_block(&harness).await; + let (pending_block, columns) = availability_pending_block(&harness).await; let root = pending_block.import_data.block_root; + let epoch = pending_block.block.epoch(); - let blobs_expected = pending_block.num_blobs_expected(); + let num_blobs_expected = pending_block.num_blobs_expected(); + let columns_expected = cache + .custody_context + .num_of_data_columns_to_sample(epoch, &harness.spec); + + // All columns are returned from availability_pending_block (E::number_of_columns()) + // but we only need custody columns assert_eq!( - blobs.len(), - blobs_expected, - "should have expected number of blobs" + columns.len(), + E::number_of_columns(), + "should have all data columns from block" ); assert!(cache.critical.read().is_empty(), "cache should be empty"); let availability = cache .put_executed_block(pending_block) .expect("should put block"); - if blobs_expected == 0 { + if num_blobs_expected == 0 { assert!( matches!(availability, Availability::Available(_)), "block doesn't have blobs, should be available" @@ -1005,7 +1018,7 @@ mod test { } else { assert!( matches!(availability, Availability::MissingComponents(_)), - "should be pending blobs" + "should be pending columns" ); assert_eq!( cache.critical.read().len(), @@ -1018,13 +1031,26 @@ mod test { ); } - let mut kzg_verified_blobs = Vec::new(); - for (blob_index, gossip_blob) in blobs.into_iter().enumerate() { - kzg_verified_blobs.push(gossip_blob.into_inner()); + // Get sampling column indices for this epoch + let sampling_column_indices = cache + .custody_context + .sampling_columns_for_epoch(epoch, &harness.spec); + + // Filter to only sampling columns + let sampling_columns: Vec<_> = columns + .into_iter() + .filter(|col| sampling_column_indices.contains(&col.index())) + .collect(); + + let mut kzg_verified_columns = Vec::new(); + for (col_index, gossip_column) in sampling_columns.into_iter().enumerate() { + kzg_verified_columns.push(KzgVerifiedCustodyDataColumn::from_asserted_custody( + gossip_column.into_inner(), + )); let availability = cache - .put_kzg_verified_blobs(root, kzg_verified_blobs.clone()) - .expect("should put blob"); - if blob_index == blobs_expected - 1 { + .put_kzg_verified_data_columns(root, kzg_verified_columns.clone()) + .expect("should put column"); + if col_index == columns_expected - 1 { assert!(matches!(availability, Availability::Available(_))); } else { assert!(matches!(availability, Availability::MissingComponents(_))); @@ -1032,20 +1058,36 @@ mod test { } } - let (pending_block, blobs) = availability_pending_block(&harness).await; - let blobs_expected = pending_block.num_blobs_expected(); + let (pending_block, columns) = availability_pending_block(&harness).await; + let _num_blobs_expected = pending_block.num_blobs_expected(); + let epoch = pending_block.block.epoch(); + // All columns returned assert_eq!( - blobs.len(), - blobs_expected, - "should have expected number of blobs" + columns.len(), + E::number_of_columns(), + "should have all data columns" ); let root = pending_block.import_data.block_root; - let mut kzg_verified_blobs = vec![]; - for gossip_blob in blobs { - kzg_verified_blobs.push(gossip_blob.into_inner()); + + // Get sampling column indices for this epoch + let sampling_column_indices = cache + .custody_context + .sampling_columns_for_epoch(epoch, &harness.spec); + + // Filter to only sampling columns + let sampling_columns: Vec<_> = columns + .into_iter() + .filter(|col| sampling_column_indices.contains(&col.index())) + .collect(); + + let mut kzg_verified_columns = vec![]; + for gossip_column in sampling_columns { + kzg_verified_columns.push(KzgVerifiedCustodyDataColumn::from_asserted_custody( + gossip_column.into_inner(), + )); let availability = cache - .put_kzg_verified_blobs(root, kzg_verified_blobs.clone()) - .expect("should put blob"); + .put_kzg_verified_data_columns(root, kzg_verified_columns.clone()) + .expect("should put column"); assert!( matches!(availability, Availability::MissingComponents(_)), "should be pending block" diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 9802f091e0..5efe9a3c23 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -251,6 +251,13 @@ pub enum BeaconChainError { request_epoch: Epoch, cache_epoch: Epoch, }, + AttesterCachePtcOutOfBounds { + slot: Slot, + epoch: Epoch, + }, + AttesterCacheNoPtcPreGloas { + slot: Slot, + }, SkipProposerPreparation, FailedColumnCustodyInfoUpdate, } diff --git a/beacon_node/beacon_chain/src/execution_payload.rs b/beacon_node/beacon_chain/src/execution_payload.rs index 16542eea2d..c8976fc6a8 100644 --- a/beacon_node/beacon_chain/src/execution_payload.rs +++ b/beacon_node/beacon_chain/src/execution_payload.rs @@ -342,7 +342,7 @@ pub fn get_execution_payload( Ok(join_handle) } -/// Prepares an execution payload for inclusion in a block. +/// Prepares an execution payload (pre-gloas) for inclusion in a block. /// /// ## Errors /// @@ -373,6 +373,13 @@ where { let spec = &chain.spec; let fork = spec.fork_name_at_slot::(builder_params.slot); + + if fork.gloas_enabled() { + return Err(BlockProductionError::InvalidBlockVariant( + "Called pre-gloas prepare_execution_payload on a gloas block".to_string(), + )); + } + let execution_layer = chain .execution_layer .as_ref() @@ -403,25 +410,20 @@ where .get_suggested_fee_recipient(proposer_index) .await; - let slot_number = if fork.gloas_enabled() { - Some(builder_params.slot.as_u64()) - } else { - None - }; - let payload_attributes = PayloadAttributes::new( timestamp, random, suggested_fee_recipient, withdrawals, parent_beacon_block_root, - slot_number, + None, + None, ); let target_gas_limit = execution_layer.get_proposer_gas_limit(proposer_index).await; let payload_parameters = PayloadParameters { parent_hash, - parent_gas_limit: latest_execution_payload_header_gas_limit, + parent_gas_limit: Some(latest_execution_payload_header_gas_limit), proposer_gas_limit: target_gas_limit, payload_attributes: &payload_attributes, forkchoice_update_params: &forkchoice_update_params, diff --git a/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs b/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs index f5ba647fce..b75fcdac5c 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/fetch_blobs_beacon_adapter.rs @@ -1,8 +1,9 @@ -use crate::fetch_blobs::{EngineGetBlobsOutput, FetchEngineBlobError}; +use crate::data_column_verification::KzgVerifiedCustodyDataColumn; +use crate::fetch_blobs::FetchEngineBlobError; use crate::observed_data_sidecars::ObservationKey; use crate::partial_data_column_assembler::PartialDataColumnAssembler; use crate::{AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes}; -use execution_layer::json_structures::{BlobAndProofV1, BlobAndProofV2, BlobAndProofV3}; +use execution_layer::json_structures::{BlobAndProofV2, BlobAndProofV3}; use kzg::Kzg; #[cfg(test)] use mockall::automock; @@ -43,22 +44,6 @@ impl FetchBlobsBeaconAdapter { .cloned() } - pub(crate) async fn get_blobs_v1( - &self, - versioned_hashes: Vec, - ) -> Result>>, FetchEngineBlobError> { - let execution_layer = self - .chain - .execution_layer - .as_ref() - .ok_or(FetchEngineBlobError::ExecutionLayerMissing)?; - - execution_layer - .get_blobs_v1(versioned_hashes) - .await - .map_err(FetchEngineBlobError::RequestFailed) - } - pub(crate) async fn get_blobs_v2( &self, versioned_hashes: Vec, @@ -91,17 +76,6 @@ impl FetchBlobsBeaconAdapter { .map_err(FetchEngineBlobError::RequestFailed) } - pub(crate) fn blobs_known_for_observation_key( - &self, - observation_key: ObservationKey, - ) -> Option> { - self.chain - .observed_blob_sidecars - .read() - .known_for_observation_key(&observation_key) - .cloned() - } - pub(crate) fn data_column_known_for_observation_key( &self, observation_key: ObservationKey, @@ -113,12 +87,6 @@ impl FetchBlobsBeaconAdapter { .cloned() } - pub(crate) fn cached_blob_indexes(&self, block_root: &Hash256) -> Option> { - self.chain - .data_availability_checker - .cached_blob_indexes(block_root) - } - pub(crate) fn cached_data_column_indexes( &self, block_root: &Hash256, @@ -131,7 +99,7 @@ impl FetchBlobsBeaconAdapter { &self, slot: Slot, block_root: Hash256, - blobs: EngineGetBlobsOutput, + blobs: Vec>, ) -> Result { self.chain .process_engine_blobs(slot, block_root, blobs) diff --git a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs index 351e35666a..158cef0003 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/mod.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/mod.rs @@ -12,7 +12,6 @@ mod fetch_blobs_beacon_adapter; #[cfg(test)] mod tests; -use crate::blob_verification::{GossipBlobError, KzgVerifiedBlob}; use crate::data_column_verification::{ KzgVerifiedCustodyDataColumn, KzgVerifiedCustodyPartialDataColumn, KzgVerifiedPartialDataColumn, }; @@ -25,26 +24,15 @@ use crate::{ metrics, }; use execution_layer::Error as ExecutionLayerError; -use execution_layer::json_structures::{BlobAndProofV1, BlobAndProofV2, BlobAndProofV3}; +use execution_layer::json_structures::{BlobAndProofV2, BlobAndProofV3}; use metrics::{TryExt, inc_counter}; #[cfg(test)] use mockall_double::double; -use slot_clock::timestamp_now; use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash; use std::sync::Arc; use tracing::{debug, instrument, warn}; use types::data::{BlobSidecarError, ColumnIndex, DataColumnSidecarError, PartialDataColumnHeader}; -use types::{BeaconStateError, BlobSidecar, EthSpec, Hash256, VersionedHash}; - -/// Result from engine get blobs to be passed onto `DataAvailabilityChecker` and published to the -/// gossip network. The blobs / data columns have not been marked as observed yet, as they may not -/// be published immediately. -#[derive(Debug)] -pub enum EngineGetBlobsOutput { - Blobs(Vec>), - /// A filtered list of custody data columns to be imported into the `DataAvailabilityChecker`. - CustodyColumns(Vec>), -} +use types::{BeaconStateError, EthSpec, Hash256, VersionedHash}; #[derive(Debug)] pub enum FetchEngineBlobError { @@ -55,22 +43,21 @@ pub enum FetchEngineBlobError { DataColumnSidecarError(DataColumnSidecarError), ExecutionLayerMissing, InternalError(String), - GossipBlob(GossipBlobError), KzgError(kzg::Error), RequestFailed(ExecutionLayerError), RuntimeShutdown, TokioJoin(tokio::task::JoinError), } -/// Fetches blobs from the EL mempool and processes them. It also broadcasts unseen blobs or -/// data columns (PeerDAS onwards) to the network, using the supplied `publish_fn`. +/// Fetches blobs from the EL mempool and processes them as data columns. It also broadcasts +/// unseen data columns to the network, using the supplied `publish_fn`. #[instrument(skip_all)] pub async fn fetch_and_process_engine_blobs( chain: Arc>, block_root: Hash256, header: Arc>, custody_columns: &[ColumnIndex], - publish_fn: impl Fn(EngineGetBlobsOutput) + Send + 'static, + publish_fn: impl Fn(Vec>) + Send + 'static, ) -> Result, FetchEngineBlobError> { fetch_and_process_engine_blobs_inner( FetchBlobsBeaconAdapter::new(chain), @@ -89,7 +76,7 @@ async fn fetch_and_process_engine_blobs_inner( block_root: Hash256, header: Arc>, custody_columns: &[ColumnIndex], - publish_fn: impl Fn(EngineGetBlobsOutput) + Send + 'static, + publish_fn: impl Fn(Vec>) + Send + 'static, ) -> Result, FetchEngineBlobError> { let versioned_hashes = header .kzg_commitments @@ -120,104 +107,12 @@ async fn fetch_and_process_engine_blobs_inner( ) .await } else { - fetch_and_process_blobs_v1( - chain_adapter, - block_root, - &header, - versioned_hashes, - publish_fn, - ) - .await + Err(FetchEngineBlobError::InternalError( + "fetch blobs v1 no longer supported".to_owned(), + )) } } -#[instrument(skip_all, level = "debug")] -async fn fetch_and_process_blobs_v1( - chain_adapter: FetchBlobsBeaconAdapter, - block_root: Hash256, - header: &PartialDataColumnHeader, - versioned_hashes: Vec, - publish_fn: impl Fn(EngineGetBlobsOutput) + Send + Sized, -) -> Result, FetchEngineBlobError> { - let num_expected_blobs = versioned_hashes.len(); - metrics::observe(&metrics::BLOBS_FROM_EL_EXPECTED, num_expected_blobs as f64); - debug!(num_expected_blobs, "Fetching blobs from the EL"); - let response = chain_adapter - .get_blobs_v1(versioned_hashes) - .await - .inspect_err(|_| { - inc_counter(&metrics::BLOBS_FROM_EL_ERROR_TOTAL); - })?; - - let num_fetched_blobs = response.iter().filter(|opt| opt.is_some()).count(); - metrics::observe(&metrics::BLOBS_FROM_EL_RECEIVED, num_fetched_blobs as f64); - - if num_fetched_blobs == 0 { - debug!(num_expected_blobs, "No blobs fetched from the EL"); - inc_counter(&metrics::BLOBS_FROM_EL_MISS_TOTAL); - return Ok(None); - } else { - debug!( - num_expected_blobs, - num_fetched_blobs, "Received blobs from the EL" - ); - inc_counter(&metrics::BLOBS_FROM_EL_HIT_TOTAL); - } - - if chain_adapter.fork_choice_contains_block(&block_root) { - // Avoid computing sidecars if the block has already been imported. - debug!( - info = "block has already been imported", - "Ignoring EL blobs response" - ); - return Ok(None); - } - - let mut blob_sidecar_list = build_blob_sidecars(header, response)?; - - let observation_key = ObservationKey::new_proposer_key( - header.signed_block_header.message.proposer_index, - header.slot(), - ); - - if let Some(observed_blobs) = chain_adapter.blobs_known_for_observation_key(observation_key) { - blob_sidecar_list.retain(|blob| !observed_blobs.contains(&blob.blob_index())); - if blob_sidecar_list.is_empty() { - debug!( - info = "blobs have already been seen on gossip", - "Ignoring EL blobs response" - ); - return Ok(None); - } - } - - if let Some(known_blobs) = chain_adapter.cached_blob_indexes(&block_root) { - blob_sidecar_list.retain(|blob| !known_blobs.contains(&blob.blob_index())); - if blob_sidecar_list.is_empty() { - debug!( - info = "blobs have already been imported into data availability checker", - "Ignoring EL blobs response" - ); - return Ok(None); - } - } - - // Up until this point we have not observed the blobs in the gossip cache, which allows them to - // arrive independently while this function is running. In `publish_fn` we will observe them - // and then publish any blobs that had not already been observed. - publish_fn(EngineGetBlobsOutput::Blobs(blob_sidecar_list.clone())); - - let availability_processing_status = chain_adapter - .process_engine_blobs( - header.slot(), - block_root, - EngineGetBlobsOutput::Blobs(blob_sidecar_list), - ) - .await?; - - Ok(Some(availability_processing_status)) -} - #[instrument(skip_all, level = "debug")] async fn fetch_and_process_blobs_v2_or_v3( chain_adapter: FetchBlobsBeaconAdapter, @@ -225,7 +120,7 @@ async fn fetch_and_process_blobs_v2_or_v3( header: Arc>, versioned_hashes: Vec, custody_columns_indices: &[ColumnIndex], - publish_fn: impl Fn(EngineGetBlobsOutput) + Send + 'static, + publish_fn: impl Fn(Vec>) + Send + 'static, ) -> Result, FetchEngineBlobError> { let num_expected_blobs = versioned_hashes.len(); let slot = header.slot(); @@ -354,7 +249,7 @@ async fn fetch_and_process_blobs_v2_or_v3( // Publish complete columns if !full_columns.is_empty() { - publish_fn(EngineGetBlobsOutput::CustodyColumns(full_columns.clone())); + publish_fn(full_columns.clone()); } // We publish all partials at the calling site, regardless of result, as previous publishs // have been blocked, waiting for the results of this call @@ -362,11 +257,7 @@ async fn fetch_and_process_blobs_v2_or_v3( // Process complete columns through DA checker let availability_processing_status = if !full_columns.is_empty() { chain_adapter - .process_engine_blobs( - slot, - block_root, - EngineGetBlobsOutput::CustodyColumns(full_columns), - ) + .process_engine_blobs(slot, block_root, full_columns) .await? } else { // No complete columns yet, still missing components @@ -461,30 +352,3 @@ async fn compute_custody_columns_to_import( .await .map_err(FetchEngineBlobError::TokioJoin)? } - -fn build_blob_sidecars( - header: &PartialDataColumnHeader, - response: Vec>>, -) -> Result>, FetchEngineBlobError> { - let mut sidecars = vec![]; - for (index, blob_and_proof) in response - .into_iter() - .enumerate() - .filter_map(|(index, opt_blob)| Some((index, opt_blob?))) - { - let blob_sidecar = BlobSidecar::new_with_existing_proof( - index, - blob_and_proof.blob, - header.clone(), - blob_and_proof.proof, - ) - .map_err(FetchEngineBlobError::BlobSidecarError)?; - - sidecars.push(KzgVerifiedBlob::from_execution_verified( - Arc::new(blob_sidecar), - timestamp_now(), - )); - } - - Ok(sidecars) -} diff --git a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs index 37d40f3a27..99cb4b5a78 100644 --- a/beacon_node/beacon_chain/src/fetch_blobs/tests.rs +++ b/beacon_node/beacon_chain/src/fetch_blobs/tests.rs @@ -1,8 +1,7 @@ use crate::AvailabilityProcessingStatus; +use crate::data_column_verification::KzgVerifiedCustodyDataColumn; use crate::fetch_blobs::fetch_blobs_beacon_adapter::MockFetchBlobsBeaconAdapter; -use crate::fetch_blobs::{ - EngineGetBlobsOutput, FetchEngineBlobError, fetch_and_process_engine_blobs_inner, -}; +use crate::fetch_blobs::{FetchEngineBlobError, fetch_and_process_engine_blobs_inner}; use crate::partial_data_column_assembler::PartialDataColumnAssembler; use crate::test_utils::{EphemeralHarnessType, get_kzg}; use bls::Signature; @@ -226,7 +225,7 @@ mod get_blobs_v2 { assert!( matches!( published_columns, - EngineGetBlobsOutput::CustodyColumns(columns) if columns.len() == custody_columns.len() + columns if columns.len() == custody_columns.len() ), "should publish custody columns" ); @@ -251,284 +250,10 @@ mod get_blobs_v2 { } } -mod get_blobs_v1 { - use super::*; - use crate::block_verification_types::AsBlock; - use std::collections::HashSet; - use types::{ColumnIndex, FullPayload, PartialDataColumnHeader}; - - const ELECTRA_FORK: ForkName = ForkName::Electra; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_fetch_blobs_v1_no_blobs_in_block() { - let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK, false); - let spec = mock_adapter.spec(); - let (publish_fn, _s) = mock_publish_fn(); - let block_no_blobs = SignedBeaconBlock::>::from_block( - BeaconBlock::empty(spec), - Signature::empty(), - ); - let block_root = block_no_blobs.canonical_root(); - - // Expectations: engine fetch blobs should not be triggered - mock_adapter.expect_get_blobs_v1().times(0); - - // WHEN: Trigger fetch blobs on the block - let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; - let processing_status = fetch_and_process_engine_blobs_inner( - mock_adapter, - block_root, - Arc::new(PartialDataColumnHeader::try_from(&block_no_blobs).unwrap()), - &custody_columns, - publish_fn, - ) - .await - .expect("fetch blobs should succeed"); - - // THEN: No blob is processed - assert_eq!(processing_status, None); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_fetch_blobs_v1_no_blobs_returned() { - let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK, false); - let (publish_fn, _) = mock_publish_fn(); - let (block, _blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2); - let block_root = block.canonical_root(); - - // GIVEN: No blobs in EL response - let expected_blob_count = block.message().body().blob_kzg_commitments().unwrap().len(); - mock_get_blobs_v1_response(&mut mock_adapter, vec![None; expected_blob_count]); - - // WHEN: Trigger fetch blobs on the block - let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; - let processing_status = fetch_and_process_engine_blobs_inner( - mock_adapter, - block_root, - Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()), - &custody_columns, - publish_fn, - ) - .await - .expect("fetch blobs should succeed"); - - // THEN: No blob is processed - assert_eq!(processing_status, None); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_fetch_blobs_v1_partial_blobs_returned() { - let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK, false); - let (publish_fn, publish_fn_args) = mock_publish_fn(); - let blob_count = 2; - let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, blob_count); - let block_slot = block.slot(); - let block_root = block.canonical_root(); - - // GIVEN: Missing a blob in EL response (remove 1 blob from response) - let mut blob_and_proof_opts = blobs_and_proofs.into_iter().map(Some).collect::>(); - blob_and_proof_opts.first_mut().unwrap().take(); - mock_get_blobs_v1_response(&mut mock_adapter, blob_and_proof_opts); - // AND block is not imported into fork choice - mock_fork_choice_contains_block(&mut mock_adapter, vec![]); - // AND all blobs have not yet been seen - mock_adapter - .expect_cached_blob_indexes() - .returning(|_| None); - mock_adapter - .expect_blobs_known_for_observation_key() - .returning(|_| None); - // Returned blobs should be processed - mock_process_engine_blobs_result( - &mut mock_adapter, - Ok(AvailabilityProcessingStatus::MissingComponents( - block_slot, block_root, - )), - ); - - // WHEN: Trigger fetch blobs on the block - let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; - let processing_status = fetch_and_process_engine_blobs_inner( - mock_adapter, - block_root, - Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()), - &custody_columns, - publish_fn, - ) - .await - .expect("fetch blobs should succeed"); - - // THEN: Returned blobs are processed and published - assert_eq!( - processing_status, - Some(AvailabilityProcessingStatus::MissingComponents( - block_slot, block_root, - )) - ); - assert!( - matches!( - extract_published_blobs(publish_fn_args), - EngineGetBlobsOutput::Blobs(blobs) if blobs.len() == blob_count - 1 - ), - "partial blob results should still be published" - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_fetch_blobs_v1_block_imported_after_el_response() { - let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK, false); - let (publish_fn, publish_fn_args) = mock_publish_fn(); - let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2); - let block_root = block.canonical_root(); - - // GIVEN: All blobs returned, but fork choice already imported the block - let blob_and_proof_opts = blobs_and_proofs.into_iter().map(Some).collect::>(); - mock_get_blobs_v1_response(&mut mock_adapter, blob_and_proof_opts); - mock_fork_choice_contains_block(&mut mock_adapter, vec![block.canonical_root()]); - - // WHEN: Trigger fetch blobs on the block - let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; - let processing_status = fetch_and_process_engine_blobs_inner( - mock_adapter, - block_root, - Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()), - &custody_columns, - publish_fn, - ) - .await - .expect("fetch blobs should succeed"); - - // THEN: Returned blobs should NOT be processed or published. - assert_eq!(processing_status, None); - assert_eq!( - publish_fn_args.lock().unwrap().len(), - 0, - "no blobs should be published" - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_fetch_blobs_v1_no_new_blobs_to_import() { - let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK, false); - let (publish_fn, publish_fn_args) = mock_publish_fn(); - let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, 2); - let block_root = block.canonical_root(); - - // **GIVEN**: - // All blobs returned - let blob_and_proof_opts = blobs_and_proofs.into_iter().map(Some).collect::>(); - let all_blob_indices = blob_and_proof_opts - .iter() - .enumerate() - .map(|(i, _)| i as u64) - .collect::>(); - - mock_get_blobs_v1_response(&mut mock_adapter, blob_and_proof_opts); - // block not yet imported into fork choice - mock_fork_choice_contains_block(&mut mock_adapter, vec![]); - // All blobs already seen on gossip - mock_adapter - .expect_cached_blob_indexes() - .returning(|_| None); - mock_adapter - .expect_blobs_known_for_observation_key() - .returning(move |_| Some(all_blob_indices.clone())); - - // **WHEN**: Trigger `fetch_blobs` on the block - let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; - let processing_status = fetch_and_process_engine_blobs_inner( - mock_adapter, - block_root, - Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()), - &custody_columns, - publish_fn, - ) - .await - .expect("fetch blobs should succeed"); - - // **THEN**: Should NOT be processed and no blobs should be published. - assert_eq!(processing_status, None); - assert_eq!( - publish_fn_args.lock().unwrap().len(), - 0, - "no blobs should be published" - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_fetch_blobs_v1_success() { - let mut mock_adapter = mock_beacon_adapter(ELECTRA_FORK, false); - let (publish_fn, publish_fn_args) = mock_publish_fn(); - let blob_count = 2; - let (block, blobs_and_proofs) = create_test_block_and_blobs(&mock_adapter, blob_count); - let block_root = block.canonical_root(); - - // All blobs returned, fork choice doesn't contain block - let blob_and_proof_opts = blobs_and_proofs.into_iter().map(Some).collect::>(); - mock_get_blobs_v1_response(&mut mock_adapter, blob_and_proof_opts); - mock_fork_choice_contains_block(&mut mock_adapter, vec![]); - mock_adapter - .expect_cached_blob_indexes() - .returning(|_| None); - mock_adapter - .expect_blobs_known_for_observation_key() - .returning(|_| None); - mock_process_engine_blobs_result( - &mut mock_adapter, - Ok(AvailabilityProcessingStatus::Imported(block_root)), - ); - - // Trigger fetch blobs on the block - let custody_columns: [ColumnIndex; 3] = [0, 1, 2]; - let processing_status = fetch_and_process_engine_blobs_inner( - mock_adapter, - block_root, - Arc::new(PartialDataColumnHeader::try_from(block.as_ref()).unwrap()), - &custody_columns, - publish_fn, - ) - .await - .expect("fetch blobs should succeed"); - - // THEN all fetched blobs are processed and published - assert_eq!( - processing_status, - Some(AvailabilityProcessingStatus::Imported(block_root)) - ); - - let published_blobs = extract_published_blobs(publish_fn_args); - assert!( - matches!( - published_blobs, - EngineGetBlobsOutput::Blobs(blobs) if blobs.len() == blob_count - ), - "should publish fetched blobs" - ); - } - - fn mock_get_blobs_v1_response( - mock_adapter: &mut MockFetchBlobsBeaconAdapter, - blobs_and_proofs_opt: Vec>>, - ) { - let blobs_and_proofs_v1 = blobs_and_proofs_opt - .into_iter() - .map(|blob_and_proof_opt| { - blob_and_proof_opt.map(|blob_and_proof| match blob_and_proof { - BlobAndProof::V1(inner) => inner, - _ => panic!("BlobAndProofV1 not expected"), - }) - }) - .collect(); - mock_adapter - .expect_get_blobs_v1() - .return_once(move |_| Ok(blobs_and_proofs_v1)); - } -} - -/// Extract the `EngineGetBlobsOutput` passed to the `publish_fn`. +/// Extract the `Vec>` passed to the `publish_fn`. fn extract_published_blobs( - publish_fn_args: Arc>>>, -) -> EngineGetBlobsOutput { + publish_fn_args: Arc>>>>, +) -> Vec> { let mut calls = publish_fn_args.lock().unwrap(); assert_eq!(calls.len(), 1); calls.pop().unwrap() @@ -597,8 +322,8 @@ fn create_test_block_and_blobs( #[allow(clippy::type_complexity)] fn mock_publish_fn() -> ( - impl Fn(EngineGetBlobsOutput) + Send + 'static, - Arc>>>, + impl Fn(Vec>) + Send + 'static, + Arc>>>>, ) { // Keep track of the arguments captured by `publish_fn`. let captured_args = Arc::new(Mutex::new(vec![])); diff --git a/beacon_node/beacon_chain/src/migrate.rs b/beacon_node/beacon_chain/src/migrate.rs index 3c17c1ebba..9c70bcafa2 100644 --- a/beacon_node/beacon_chain/src/migrate.rs +++ b/beacon_node/beacon_chain/src/migrate.rs @@ -30,7 +30,7 @@ pub const DEFAULT_EPOCHS_PER_MIGRATION: u64 = 1; /// The background migrator runs a thread to perform pruning and migrate state from the hot /// to the cold database. -pub struct BackgroundMigrator, Cold: ItemStore> { +pub struct BackgroundMigrator { db: Arc>, /// Record of when the last migration ran, for enforcing `epochs_per_migration`. prev_migration: Arc>, @@ -135,7 +135,7 @@ pub struct FinalizationNotification { pub prev_migration: Arc>, } -impl, Cold: ItemStore> BackgroundMigrator { +impl BackgroundMigrator { /// Create a new `BackgroundMigrator` and spawn its thread if necessary. pub fn new(db: Arc>, config: MigratorConfig) -> Self { // Estimate last migration run from DB split slot. diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs index c36c73b344..3e9f9e4b60 100644 --- a/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/gossip_verified_payload_attestation.rs @@ -2,26 +2,29 @@ use super::Error; use crate::beacon_chain::BeaconStore; use crate::canonical_head::CanonicalHead; use crate::observed_attesters::ObservedPayloadAttesters; +use crate::shuffling_cache::{ShufflingCache, with_cached_shuffling}; use crate::validator_pubkey_cache::ValidatorPubkeyCache; use crate::{BeaconChain, BeaconChainError, BeaconChainTypes, metrics}; use bls::AggregateSignature; use educe::Educe; use eth2::types::{EventKind, ForkVersionedResponse}; use parking_lot::RwLock; -use safe_arith::SafeArith; use slot_clock::SlotClock; -use state_processing::per_block_processing::signature_sets::indexed_payload_attestation_signature_set; -use state_processing::state_advance::partial_state_advance; +use state_processing::per_block_processing::signature_sets::indexed_payload_attestation_signature_set_from_pubkeys; use std::borrow::Cow; -use types::{ChainSpec, EthSpec, IndexedPayloadAttestation, PTC, PayloadAttestationMessage, Slot}; +use types::{ + ChainSpec, EthSpec, Hash256, IndexedPayloadAttestation, PTC, PayloadAttestationMessage, Slot, +}; pub struct GossipVerificationContext<'a, T: BeaconChainTypes> { pub slot_clock: &'a T::SlotClock, pub spec: &'a ChainSpec, pub observed_payload_attesters: &'a RwLock>, pub canonical_head: &'a CanonicalHead, + pub shuffling_cache: &'a RwLock>, pub validator_pubkey_cache: &'a RwLock>, pub store: &'a BeaconStore, + pub genesis_validators_root: Hash256, } /// A `PayloadAttestationMessage` that has been verified for propagation on the gossip network. @@ -76,56 +79,18 @@ impl VerifiedPayloadAttestationMessage { return Err(Error::UnknownHeadBlock { beacon_block_root }); } - // Get head state for PTC computation. If the cached head state is too stale - // (e.g. during liveness failures with many skipped slots), fall back to loading - // a more recent state from the store and advancing it if necessary. - let head = ctx.canonical_head.cached_head(); - let head_state = &head.snapshot.beacon_state; - let message_epoch = slot.epoch(T::EthSpec::slots_per_epoch()); - let state_epoch = head_state.current_epoch(); - - // get_ptc can serve epochs in [state_epoch - 1, state_epoch + min_seed_lookahead]. - // If the message epoch is beyond that range, the head state is stale. - let advanced_state = if message_epoch - > state_epoch - .safe_add(ctx.spec.min_seed_lookahead) - .map_err(BeaconChainError::from)? - { - let head_block_root = head.head_block_root(); - let target_slot = message_epoch.start_slot(T::EthSpec::slots_per_epoch()); - - let (state_root, mut state) = ctx - .store - .get_advanced_hot_state( - head_block_root, - target_slot, - head.snapshot.beacon_state_root(), - ) - .map_err(BeaconChainError::from)? - .ok_or(BeaconChainError::MissingBeaconState( - head.snapshot.beacon_state_root(), - ))?; - - if state - .current_epoch() - .safe_add(ctx.spec.min_seed_lookahead) - .map_err(BeaconChainError::from)? - < message_epoch - { - partial_state_advance(&mut state, Some(state_root), target_slot, ctx.spec) - .map_err(BeaconChainError::from)?; - } - - Some(state) - } else { - None - }; - - let state = advanced_state.as_ref().unwrap_or(head_state); + let ptc = with_cached_shuffling( + ctx.canonical_head, + ctx.shuffling_cache, + ctx.store, + ctx.spec, + beacon_block_root, + message_epoch, + |cached_shuffling, _| cached_shuffling.ptc_for_slot(slot), + )?; // [REJECT] `validator_index` is within `get_ptc(state, data.slot)`. - let ptc = state.get_ptc(slot, ctx.spec)?; if !ptc.0.contains(&(validator_index as usize)) { return Err(Error::NotInPTC { validator_index, @@ -145,11 +110,11 @@ impl VerifiedPayloadAttestationMessage { { // [REJECT] The signature is valid with respect to the `validator_index`. let pubkey_cache = ctx.validator_pubkey_cache.read(); - let signature_set = indexed_payload_attestation_signature_set( - state, + let signature_set = indexed_payload_attestation_signature_set_from_pubkeys( |validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed), &indexed_payload_attestation.signature, &indexed_payload_attestation, + ctx.genesis_validators_root, ctx.spec, ) .map_err(|_| Error::UnknownValidatorIndex(validator_index))?; @@ -204,8 +169,10 @@ impl BeaconChain { spec: &self.spec, observed_payload_attesters: &self.observed_payload_attesters, canonical_head: &self.canonical_head, + shuffling_cache: &self.shuffling_cache, validator_pubkey_cache: &self.validator_pubkey_cache, store: &self.store, + genesis_validators_root: self.genesis_validators_root, } } diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/mod.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/mod.rs index 477527c0aa..89ae1bbbdd 100644 --- a/beacon_node/beacon_chain/src/payload_attestation_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/mod.rs @@ -9,7 +9,7 @@ use crate::BeaconChainError; use strum::AsRefStr; -use types::{BeaconStateError, Hash256, Slot}; +use types::{Hash256, Slot}; pub mod gossip_verified_payload_attestation; @@ -86,12 +86,6 @@ pub enum Error { /// We were unable to process this message due to an internal error. It's unclear if the /// message is valid. BeaconChainError(Box), - /// An error reading beacon state. - /// - /// ## Peer scoring - /// - /// We were unable to process this message due to an internal error. - BeaconStateError(BeaconStateError), } impl From for Error { @@ -100,11 +94,5 @@ impl From for Error { } } -impl From for Error { - fn from(e: BeaconStateError) -> Self { - Error::BeaconStateError(e) - } -} - #[cfg(test)] mod tests; diff --git a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs index 7faad98e55..d4b82c41fc 100644 --- a/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_attestation_verification/tests.rs @@ -1,25 +1,15 @@ use std::sync::Arc; use std::time::Duration; -use bls::{Keypair, Signature}; -use fork_choice::ForkChoice; -use genesis::{generate_deterministic_keypairs, interop_genesis_state}; -use parking_lot::RwLock; -use proto_array::PayloadStatus; +use bls::Signature; use slot_clock::{SlotClock, TestingSlotClock}; use state_processing::AllCaches; -use state_processing::genesis::genesis_block; -use store::{HotColdDB, StoreConfig}; use types::{ - ChainSpec, Checkpoint, Domain, Epoch, EthSpec, Hash256, MinimalEthSpec, PayloadAttestationData, - PayloadAttestationMessage, SignedBeaconBlock, SignedRoot, Slot, + Domain, Epoch, EthSpec, ForkName, Hash256, MinimalEthSpec, PayloadAttestationData, + PayloadAttestationMessage, SignedRoot, Slot, }; use crate::{ - beacon_fork_choice_store::BeaconForkChoiceStore, - beacon_snapshot::BeaconSnapshot, - canonical_head::CanonicalHead, - observed_attesters::ObservedPayloadAttesters, payload_attestation_verification::{ Error as PayloadAttestationError, gossip_verified_payload_attestation::{ @@ -27,7 +17,6 @@ use crate::{ }, }, test_utils::{BeaconChainHarness, EphemeralHarnessType, fork_name_from_env, test_spec}, - validator_pubkey_cache::ValidatorPubkeyCache, }; type E = MinimalEthSpec; @@ -36,96 +25,48 @@ type T = EphemeralHarnessType; const NUM_VALIDATORS: usize = 64; struct TestContext { - canonical_head: CanonicalHead, - observed_payload_attesters: RwLock>, - validator_pubkey_cache: RwLock>, - slot_clock: TestingSlotClock, - keypairs: Vec, - spec: ChainSpec, + harness: BeaconChainHarness, genesis_block_root: Hash256, - store: Arc, store::MemoryStore>>, } impl TestContext { fn new() -> Self { - let spec = test_spec::(); - let store = Arc::new( - HotColdDB::open_ephemeral(StoreConfig::default(), Arc::new(spec.clone())) - .expect("should open ephemeral store"), - ); - - let keypairs = generate_deterministic_keypairs(NUM_VALIDATORS); - - let mut state = - interop_genesis_state::(&keypairs, 0, Hash256::repeat_byte(0x42), None, &spec) - .expect("should build genesis state"); - - *state.finalized_checkpoint_mut() = Checkpoint { - epoch: Epoch::new(1), - root: Hash256::ZERO, - }; - - let mut block = genesis_block(&state, &spec).expect("should build genesis block"); - *block.state_root_mut() = state - .update_tree_hash_cache() - .expect("should hash genesis state"); - let signed_block = SignedBeaconBlock::from_block(block, Signature::empty()); - let block_root = signed_block.canonical_root(); - - let snapshot = BeaconSnapshot::new( - Arc::new(signed_block.clone()), - None, - block_root, - state.clone(), - ); - - let fc_store = BeaconForkChoiceStore::get_forkchoice_store(store.clone(), snapshot.clone()) - .expect("should create fork choice store"); - let fork_choice = - ForkChoice::from_anchor(fc_store, block_root, &signed_block, &state, None, &spec) - .expect("should create fork choice"); - - let canonical_head = - CanonicalHead::new(fork_choice, Arc::new(snapshot), PayloadStatus::Pending); - + let spec = Arc::new(test_spec::()); let slot_clock = TestingSlotClock::new( Slot::new(0), Duration::from_secs(0), spec.get_slot_duration(), ); - // Advance past genesis so `now_with_past_tolerance` doesn't underflow. - slot_clock.set_current_time(spec.get_slot_duration()); + let harness = BeaconChainHarness::builder(E::default()) + .spec(spec) + .deterministic_keypairs(NUM_VALIDATORS) + .fresh_ephemeral_store() + .testing_slot_clock(slot_clock) + .build(); - let validator_pubkey_cache = - ValidatorPubkeyCache::new(&state, store.clone()).expect("should create pubkey cache"); + // Advance past genesis so `now_with_past_tolerance` doesn't underflow. + harness + .chain + .slot_clock + .set_current_time(harness.spec.get_slot_duration()); + let genesis_block_root = harness.chain.genesis_block_root; Self { - canonical_head, - observed_payload_attesters: RwLock::new(ObservedPayloadAttesters::default()), - validator_pubkey_cache: RwLock::new(validator_pubkey_cache), - slot_clock, - keypairs, - spec, - genesis_block_root: block_root, - store, + harness, + genesis_block_root, } } fn gossip_ctx(&self) -> GossipVerificationContext<'_, T> { - GossipVerificationContext { - slot_clock: &self.slot_clock, - spec: &self.spec, - observed_payload_attesters: &self.observed_payload_attesters, - canonical_head: &self.canonical_head, - validator_pubkey_cache: &self.validator_pubkey_cache, - store: &self.store, - } + self.harness.chain.payload_attestation_gossip_context() } fn ptc_members(&self, slot: Slot) -> Vec { - let head = self.canonical_head.cached_head(); + let head = self.harness.chain.canonical_head.cached_head(); let state = &head.snapshot.beacon_state; - let ptc = state.get_ptc(slot, &self.spec).expect("should get PTC"); + let ptc = state + .get_ptc(slot, &self.harness.spec) + .expect("should get PTC"); ptc.0.to_vec() } @@ -134,16 +75,18 @@ impl TestContext { data: PayloadAttestationData, validator_index: u64, ) -> PayloadAttestationMessage { - let head = self.canonical_head.cached_head(); + let head = self.harness.chain.canonical_head.cached_head(); let state = &head.snapshot.beacon_state; - let domain = self.spec.get_domain( + let domain = self.harness.spec.get_domain( data.slot.epoch(E::slots_per_epoch()), Domain::PTCAttester, &state.fork(), state.genesis_validators_root(), ); let message = data.signing_root(domain); - let signature = self.keypairs[validator_index as usize].sk.sign(message); + let signature = self.harness.validator_keypairs[validator_index as usize] + .sk + .sign(message); PayloadAttestationMessage { validator_index, data, @@ -192,7 +135,7 @@ fn past_slot() { return; } let ctx = TestContext::new(); - ctx.slot_clock.set_slot(5); + ctx.harness.chain.slot_clock.set_slot(5); let gossip = ctx.gossip_ctx(); let msg = make_payload_attestation(Slot::new(0), 0, ctx.genesis_block_root); @@ -328,20 +271,95 @@ fn duplicate_after_valid() { )); } -/// Exercises the `partial_state_advance` fallback in gossip verification when -/// the head state is too stale to compute PTC membership (e.g., during a -/// network liveness failure with many missed slots). #[tokio::test] -async fn stale_head_with_partial_advance() { +async fn ptc_cache_is_primed_at_gloas_fork_boundary() { + // Only run this test once, when FORK_NAME=gloas exactly. + let mut spec = test_spec::(); + if spec.fork_name_at_epoch(Epoch::new(0)) != ForkName::Gloas { + return; + } + + let gloas_fork_epoch = Epoch::new(2); + spec.gloas_fork_epoch = Some(gloas_fork_epoch); + assert_eq!( + spec.fork_name_at_epoch(gloas_fork_epoch - 1), + ForkName::Fulu + ); + assert_eq!(spec.fork_name_at_epoch(gloas_fork_epoch), ForkName::Gloas); + + let slots_per_epoch = E::slots_per_epoch(); + let fork_boundary_slot = gloas_fork_epoch.start_slot(slots_per_epoch); + let test_slots = (fork_boundary_slot.as_u64() + ..fork_boundary_slot.as_u64() + slots_per_epoch * 2) + .map(Slot::new); + + let harness = BeaconChainHarness::builder(E::default()) + .spec(Arc::new(spec)) + .deterministic_keypairs(NUM_VALIDATORS) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + harness.extend_to_slot(fork_boundary_slot).await; + + for slot in test_slots { + harness.chain.slot_clock.set_slot(slot.as_u64()); + assert!( + harness + .chain + .shuffling_cache + .read() + .check_gloas_ptcs_invariant(&harness.spec), + "shuffling cache should satisfy the Gloas PTC invariant" + ); + + let head = harness.chain.canonical_head.cached_head(); + let state = &head.snapshot.beacon_state; + let ptc = state.get_ptc(slot, &harness.spec).expect("should get PTC"); + let validator_index = *ptc.0.first().expect("PTC should have a member") as u64; + let data = PayloadAttestationData { + beacon_block_root: head.head_block_root(), + slot, + payload_present: true, + blob_data_available: true, + }; + let domain = harness.spec.get_domain( + data.slot.epoch(slots_per_epoch), + Domain::PTCAttester, + &state.fork(), + state.genesis_validators_root(), + ); + let signature = harness.validator_keypairs[validator_index as usize] + .sk + .sign(data.signing_root(domain)); + let msg = PayloadAttestationMessage { + validator_index, + data, + signature, + }; + + let result = harness + .chain + .verify_payload_attestation_message_for_gossip(msg); + assert!( + result.is_ok(), + "expected PTC payload attestation to verify at slot {}, got: {:?}", + slot, + result.unwrap_err() + ); + } +} + +/// Exercises payload attestation gossip verification when the message epoch is ahead of the +/// canonical head due to many missed slots. +#[tokio::test] +async fn stale_head_payload_attestation() { if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { return; } let slots_per_epoch = E::slots_per_epoch(); - // Head at epoch 1, message at epoch 5 — 4 epochs of missed slots. - // This exceeds min_seed_lookahead (1), triggering the fallback path: - // get_advanced_hot_state loads the stored state, then partial_state_advance - // advances it through epoch boundaries to populate ptc_window. + // Head at epoch 1, message at epoch 5: 4 epochs of missed slots. let head_slot = Slot::new(slots_per_epoch); let missed_epochs = 4; let target_slot = Slot::new(slots_per_epoch * (1 + missed_epochs)); @@ -360,7 +378,7 @@ async fn stale_head_with_partial_advance() { let head_epoch = head.snapshot.beacon_state.current_epoch(); assert!( target_epoch > head_epoch + harness.spec.min_seed_lookahead, - "precondition: message epoch must exceed head + min_seed_lookahead to trigger fallback" + "precondition: message epoch must exceed head + min_seed_lookahead" ); // GIVEN a slot clock advanced to epoch 5 without producing blocks @@ -385,7 +403,9 @@ async fn stale_head_with_partial_advance() { .expect("should get PTC from reference state"); let validator_index = *ptc.0.first().expect("PTC should have at least one member") as u64; - // WHEN a properly-signed payload attestation from a PTC member is verified. + // WHEN a properly-signed payload attestation from a PTC member is verified. The signature + // domain should come from the spec fork schedule and genesis validators root, not a loaded + // state in the verifier. let domain = harness.spec.get_domain( target_epoch, Domain::PTCAttester, @@ -420,3 +440,105 @@ async fn stale_head_with_partial_advance() { result.unwrap_err() ); } + +/// Exercises payload attestation gossip verification for a non-canonical block whose PTC differs +/// from the canonical chain's PTC for the same slot. +#[tokio::test] +async fn side_chain_payload_attestation_uses_side_chain_ptc() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } + + let slots_per_epoch = E::slots_per_epoch(); + let fork_slot = Slot::new(slots_per_epoch); + let target_slot = Slot::new(slots_per_epoch * 4); + let target_epoch = target_slot.epoch(slots_per_epoch); + + let harness = BeaconChainHarness::builder(E::default()) + .default_spec() + .deterministic_keypairs(NUM_VALIDATORS) + .fresh_ephemeral_store() + .mock_execution_layer() + .build(); + + // Build a common prefix through epoch 1. + harness.extend_to_slot(fork_slot).await; + let fork_state = harness.chain.head_snapshot().beacon_state.clone(); + + // Build two branches for several epochs. The side chain skips its first slot, giving it + // different RANDAO mixes and therefore a different PTC by the target slot. The canonical chain + // is processed second and receives sub-finality attestations, so it remains the head without + // finalizing past the side-chain fork point. + let side_slots: Vec<_> = ((fork_slot + 2).as_u64()..=target_slot.as_u64()) + .map(Slot::new) + .collect(); + let canonical_slots: Vec<_> = ((fork_slot + 1).as_u64()..=target_slot.as_u64()) + .map(Slot::new) + .collect(); + let canonical_validators = (0..NUM_VALIDATORS / 2).collect::>(); + + let results = harness + .add_blocks_on_multiple_chains(vec![ + (fork_state.clone(), side_slots, vec![]), + (fork_state, canonical_slots, canonical_validators), + ]) + .await; + + let side_head_root: Hash256 = results[0].2.into(); + let side_head_state = &results[0].3; + let canonical_head_root: Hash256 = results[1].2.into(); + let canonical_head_state = &results[1].3; + + assert_ne!(side_head_root, canonical_head_root); + assert_eq!( + harness.chain.head_snapshot().beacon_block_root, + canonical_head_root + ); + + let side_ptc = side_head_state + .get_ptc(target_slot, &harness.spec) + .expect("should get side-chain PTC"); + let canonical_ptc = canonical_head_state + .get_ptc(target_slot, &harness.spec) + .expect("should get canonical PTC"); + assert_ne!( + side_ptc, canonical_ptc, + "precondition: side-chain PTC should differ from canonical PTC" + ); + + let validator_index = side_ptc + .0 + .iter() + .copied() + .find(|validator_index| !canonical_ptc.0.contains(validator_index)) + .expect("should find a validator in the side-chain PTC only") + as u64; + + let domain = harness.spec.get_domain( + target_epoch, + Domain::PTCAttester, + &side_head_state.fork(), + side_head_state.genesis_validators_root(), + ); + let data = PayloadAttestationData { + beacon_block_root: side_head_root, + slot: target_slot, + payload_present: true, + blob_data_available: true, + }; + let message = data.signing_root(domain); + let signature = harness.validator_keypairs[validator_index as usize] + .sk + .sign(message); + let msg = PayloadAttestationMessage { + validator_index, + data, + signature, + }; + + let verified = harness + .chain + .verify_payload_attestation_message_for_gossip(msg) + .expect("side-chain payload attestation should verify"); + assert_eq!(verified.ptc(), &side_ptc); +} diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs b/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs index 1f3f074598..354705b92c 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/gossip_verified_bid.rs @@ -43,9 +43,6 @@ pub(crate) fn verify_bid_consistency( if bid.fee_recipient != proposer_preferences.message.fee_recipient { return Err(PayloadBidError::InvalidFeeRecipient); } - if bid.gas_limit != proposer_preferences.message.gas_limit { - return Err(PayloadBidError::InvalidGasLimit); - } let max_blobs_per_block = spec.max_blobs_per_block(bid_slot.epoch(E::slots_per_epoch())) as usize; @@ -161,7 +158,23 @@ impl GossipVerifiedPayloadBid { }); } - // TODO(gloas) [IGNORE] bid.parent_block_hash is the block hash of a known execution payload in fork choice. + // TODO(gloas): [IGNORE] bid.parent_block_hash is the block hash of a known execution + // payload in fork choice. + + // TODO(gloas): This uses head state's bid gas_limit as parent_gas_limit, which is only + // correct when the bid's parent is the head. If the parent is an ancestor further back + // this check may be inaccurate. Fixing this requires storing + // gas_limit in fork choice or looking it up from the store by parent_block_hash. Taking the above + // TODO into consideration maybe should persist parent block hash and gas limit in fork choice? + if let Ok(parent_bid) = head_state.latest_execution_payload_bid() + && !is_gas_limit_target_compatible( + parent_bid.gas_limit, + signed_bid.message.gas_limit, + proposer_preferences.message.target_gas_limit, + )? + { + return Err(PayloadBidError::InvalidGasLimit); + } drop(fork_choice); @@ -263,8 +276,36 @@ impl BeaconChain { } } +/// Check if `gas_limit` is compatible with `target_gas_limit` under the +/// EIP-1559 transition rule from `parent_gas_limit`. +pub fn is_gas_limit_target_compatible( + parent_gas_limit: u64, + gas_limit: u64, + target_gas_limit: u64, +) -> Result { + let max_gas_limit_difference = (parent_gas_limit / 1024) + .max(1) + .checked_sub(1) + .ok_or(PayloadBidError::InvalidGasLimit)?; + let min_gas_limit = parent_gas_limit + .checked_sub(max_gas_limit_difference) + .ok_or(PayloadBidError::InvalidGasLimit)?; + let max_gas_limit = parent_gas_limit + .checked_add(max_gas_limit_difference) + .ok_or(PayloadBidError::InvalidGasLimit)?; + + if target_gas_limit >= min_gas_limit && target_gas_limit <= max_gas_limit { + Ok(gas_limit == target_gas_limit) + } else if target_gas_limit > max_gas_limit { + Ok(gas_limit == max_gas_limit) + } else { + Ok(gas_limit == min_gas_limit) + } +} + #[cfg(test)] mod tests { + use super::is_gas_limit_target_compatible; use bls::Signature; use kzg::KzgCommitment; use ssz_types::VariableList; @@ -288,11 +329,14 @@ mod tests { } } - fn make_preferences(fee_recipient: Address, gas_limit: u64) -> SignedProposerPreferences { + fn make_preferences( + fee_recipient: Address, + target_gas_limit: u64, + ) -> SignedProposerPreferences { SignedProposerPreferences { message: ProposerPreferences { fee_recipient, - gas_limit, + target_gas_limit, ..ProposerPreferences::default() }, signature: Signature::empty(), @@ -382,13 +426,41 @@ mod tests { } #[test] - fn test_gas_limit_mismatch() { - let (state, spec) = state_and_spec(); - let current_slot = Slot::new(10); - let bid = make_bid(current_slot, Address::ZERO, 30_000_000); - let prefs = make_preferences(Address::ZERO, 50_000_000); + fn test_is_gas_limit_target_compatible_increase_within_limit() { + assert!(is_gas_limit_target_compatible(60_000_000, 60_000_100, 60_000_100).unwrap()); + } - let result = verify_bid_consistency::(&bid, current_slot, &prefs, &state, &spec); - assert!(matches!(result, Err(PayloadBidError::InvalidGasLimit))); + #[test] + fn test_is_gas_limit_target_compatible_increase_exceeding_limit() { + // max_diff = 60_000_000 / 1024 - 1 = 58_592 + // max_gas_limit = 60_000_000 + 58_592 = 60_058_592 + assert!(is_gas_limit_target_compatible(60_000_000, 60_058_592, 100_000_000).unwrap()); + } + + #[test] + fn test_is_gas_limit_target_compatible_increase_exceeding_off_by_one() { + assert!(!is_gas_limit_target_compatible(60_000_000, 60_058_593, 100_000_000).unwrap()); + } + + #[test] + fn test_is_gas_limit_target_compatible_decrease_within_limit() { + assert!(is_gas_limit_target_compatible(60_000_000, 59_999_990, 59_999_990).unwrap()); + } + + #[test] + fn test_is_gas_limit_target_compatible_decrease_exceeding_limit() { + // min_gas_limit = 60_000_000 - 58_592 = 59_941_408 + assert!(is_gas_limit_target_compatible(60_000_000, 59_941_408, 30_000_000).unwrap()); + } + + #[test] + fn test_is_gas_limit_target_compatible_target_equals_parent() { + assert!(is_gas_limit_target_compatible(60_000_000, 60_000_000, 60_000_000).unwrap()); + } + + #[test] + fn test_is_gas_limit_target_compatible_parent_underflows() { + // parent=1023: max(1023/1024, 1) - 1 = max(0, 1) - 1 = 0, no change allowed + assert!(is_gas_limit_target_compatible(1023, 1023, 60_000_000).unwrap()); } } diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs b/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs index 514695f5c0..a40fd14872 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/mod.rs @@ -48,7 +48,7 @@ pub enum PayloadBidError { }, /// The bids fee recipient doesn't match the proposer preferences fee recipient. InvalidFeeRecipient, - /// The bids gas limit doesn't match the proposer preferences gas limit. + /// The bid's gas limit is not compatible with the proposer's target gas limit. InvalidGasLimit, /// The bids execution payment is non-zero ExecutionPaymentNonZero { execution_payment: u64 }, diff --git a/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs b/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs index c68e6d9d32..ccdf64d41d 100644 --- a/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs +++ b/beacon_node/beacon_chain/src/payload_bid_verification/tests.rs @@ -101,6 +101,17 @@ impl TestContext { root: Hash256::ZERO, }; + // Set a non-zero gas_limit on latest_execution_payload_bid so the gas limit + // compatibility check doesn't reject all bids at genesis. + if let Ok(bid) = state.latest_execution_payload_bid_mut() { + bid.gas_limit = 30_000_000; + } + // Update body_root to reflect the modified bid (genesis block embeds it). + let genesis_body_root = genesis_block(&state, &spec) + .expect("should build genesis block") + .body_root(); + state.latest_block_header_mut().body_root = genesis_body_root; + let inactive_keypair = &keypairs[NUM_BUILDERS]; let inactive_creds = builder_withdrawal_credentials(&inactive_keypair.pk, &spec); let inactive_builder_index = state @@ -248,7 +259,7 @@ fn make_signed_preferences( proposal_slot: Slot, validator_index: u64, fee_recipient: Address, - gas_limit: u64, + target_gas_limit: u64, ) -> Arc { Arc::new(SignedProposerPreferences { message: ProposerPreferences { @@ -256,7 +267,7 @@ fn make_signed_preferences( proposal_slot, validator_index, fee_recipient, - gas_limit, + target_gas_limit, }, signature: Signature::empty(), }) diff --git a/beacon_node/beacon_chain/src/persisted_custody.rs b/beacon_node/beacon_chain/src/persisted_custody.rs index ba221c67b5..cc7219fa90 100644 --- a/beacon_node/beacon_chain/src/persisted_custody.rs +++ b/beacon_node/beacon_chain/src/persisted_custody.rs @@ -9,7 +9,7 @@ pub const CUSTODY_DB_KEY: Hash256 = Hash256::ZERO; pub struct PersistedCustody(pub CustodyContextSsz); -pub fn load_custody_context, Cold: ItemStore>( +pub fn load_custody_context( store: Arc>, ) -> Option { let res: Result, _> = @@ -22,7 +22,7 @@ pub fn load_custody_context, Cold: ItemStore>( } /// Attempt to persist the custody context object to `self.store`. -pub fn persist_custody_context, Cold: ItemStore>( +pub fn persist_custody_context( store: Arc>, custody_context: CustodyContextSsz, ) -> Result<(), store::Error> { diff --git a/beacon_node/beacon_chain/src/proposer_preferences_verification/gossip_verified_proposer_preferences.rs b/beacon_node/beacon_chain/src/proposer_preferences_verification/gossip_verified_proposer_preferences.rs index 4ba33fde72..586721d8c1 100644 --- a/beacon_node/beacon_chain/src/proposer_preferences_verification/gossip_verified_proposer_preferences.rs +++ b/beacon_node/beacon_chain/src/proposer_preferences_verification/gossip_verified_proposer_preferences.rs @@ -18,13 +18,16 @@ pub(crate) fn verify_preferences_consistency( preferences: &ProposerPreferences, current_slot: Slot, head_state: &BeaconState, + spec: &ChainSpec, ) -> Result<(), ProposerPreferencesError> { let proposal_slot = preferences.proposal_slot; let validator_index = preferences.validator_index; let current_epoch = current_slot.epoch(E::slots_per_epoch()); let proposal_epoch = proposal_slot.epoch(E::slots_per_epoch()); - if proposal_epoch < current_epoch || proposal_epoch > current_epoch.saturating_add(1u64) { + if proposal_epoch < current_epoch + || proposal_epoch > current_epoch.saturating_add(spec.min_seed_lookahead) + { return Err(ProposerPreferencesError::InvalidProposalEpoch { proposal_epoch }); } @@ -35,7 +38,7 @@ pub(crate) fn verify_preferences_consistency( }); } - if !head_state.is_valid_proposal_slot(preferences)? { + if !head_state.is_valid_proposal_slot(preferences, spec)? { return Err(ProposerPreferencesError::InvalidProposalSlot { validator_index, proposal_slot, @@ -83,7 +86,12 @@ impl GossipVerifiedProposerPreferences { }); } - verify_preferences_consistency(&signed_preferences.message, current_slot, head_state)?; + verify_preferences_consistency( + &signed_preferences.message, + current_slot, + head_state, + ctx.spec, + )?; // Verify signature proposer_preferences_signature_set( @@ -155,11 +163,13 @@ impl BeaconChain { #[cfg(test)] mod tests { use types::{ - Address, BeaconState, EthSpec, Hash256, MinimalEthSpec, ProposerPreferences, Slot, + Address, BeaconState, ChainSpec, EthSpec, Hash256, MinimalEthSpec, ProposerPreferences, + Slot, }; use super::verify_preferences_consistency; use crate::proposer_preferences_verification::ProposerPreferencesError; + use crate::test_utils::{fork_name_from_env, test_spec}; type E = MinimalEthSpec; @@ -169,20 +179,28 @@ mod tests { proposal_slot, validator_index, fee_recipient: Address::ZERO, - gas_limit: 30_000_000, + target_gas_limit: 30_000_000, } } fn state() -> BeaconState { - BeaconState::new(0, <_>::default(), &E::default_spec()) + let spec = spec(); + BeaconState::new(0, <_>::default(), &spec) + } + + fn spec() -> ChainSpec { + test_spec::() } #[test] fn test_invalid_epoch_too_old() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let current_slot = Slot::new(2 * E::slots_per_epoch()); let prefs = make_preferences(Slot::new(3), 0); - let result = verify_preferences_consistency::(&prefs, current_slot, &state()); + let result = verify_preferences_consistency::(&prefs, current_slot, &state(), &spec()); assert!(matches!( result, Err(ProposerPreferencesError::InvalidProposalEpoch { .. }) @@ -191,10 +209,13 @@ mod tests { #[test] fn test_invalid_epoch_too_far_ahead() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let current_slot = Slot::new(E::slots_per_epoch()); let prefs = make_preferences(Slot::new(3 * E::slots_per_epoch() + 1), 0); - let result = verify_preferences_consistency::(&prefs, current_slot, &state()); + let result = verify_preferences_consistency::(&prefs, current_slot, &state(), &spec()); assert!(matches!( result, Err(ProposerPreferencesError::InvalidProposalEpoch { .. }) @@ -203,10 +224,13 @@ mod tests { #[test] fn test_proposal_slot_already_passed() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let current_slot = Slot::new(10); let prefs = make_preferences(Slot::new(9), 0); - let result = verify_preferences_consistency::(&prefs, current_slot, &state()); + let result = verify_preferences_consistency::(&prefs, current_slot, &state(), &spec()); assert!(matches!( result, Err(ProposerPreferencesError::ProposalSlotAlreadyPassed { .. }) @@ -215,10 +239,13 @@ mod tests { #[test] fn test_proposal_slot_equal_to_current() { + if !fork_name_from_env().is_some_and(|f| f.gloas_enabled()) { + return; + } let current_slot = Slot::new(10); let prefs = make_preferences(Slot::new(10), 0); - let result = verify_preferences_consistency::(&prefs, current_slot, &state()); + let result = verify_preferences_consistency::(&prefs, current_slot, &state(), &spec()); assert!(matches!( result, Err(ProposerPreferencesError::ProposalSlotAlreadyPassed { .. }) diff --git a/beacon_node/beacon_chain/src/proposer_preferences_verification/mod.rs b/beacon_node/beacon_chain/src/proposer_preferences_verification/mod.rs index a2e96dfce1..6c79e56733 100644 --- a/beacon_node/beacon_chain/src/proposer_preferences_verification/mod.rs +++ b/beacon_node/beacon_chain/src/proposer_preferences_verification/mod.rs @@ -24,7 +24,7 @@ mod tests; #[derive(Debug)] pub enum ProposerPreferencesError { - /// The proposal slot is not in the current or next epoch. + /// The proposal slot is not within the proposer lookahead. InvalidProposalEpoch { proposal_epoch: Epoch }, /// The proposal slot has already passed. ProposalSlotAlreadyPassed { diff --git a/beacon_node/beacon_chain/src/proposer_preferences_verification/proposer_preference_cache.rs b/beacon_node/beacon_chain/src/proposer_preferences_verification/proposer_preference_cache.rs index 7bbdf34888..c423418fbc 100644 --- a/beacon_node/beacon_chain/src/proposer_preferences_verification/proposer_preference_cache.rs +++ b/beacon_node/beacon_chain/src/proposer_preferences_verification/proposer_preference_cache.rs @@ -87,7 +87,7 @@ mod tests { proposal_slot: slot, validator_index, fee_recipient: Address::ZERO, - gas_limit: 30_000_000, + target_gas_limit: 30_000_000, }, signature: Signature::empty(), }), diff --git a/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs b/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs index 468e08ff3b..53c1c4ded3 100644 --- a/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs +++ b/beacon_node/beacon_chain/src/proposer_preferences_verification/tests.rs @@ -112,7 +112,7 @@ impl TestContext { let slot_in_epoch = slot.as_usize() % E::slots_per_epoch() as usize; let epoch = slot.epoch(E::slots_per_epoch()); let current_epoch = state.slot().epoch(E::slots_per_epoch()); - let index = if epoch == current_epoch.saturating_add(1u64) { + let index = if epoch == current_epoch.saturating_add(self.spec.min_seed_lookahead) { E::slots_per_epoch() as usize + slot_in_epoch } else { slot_in_epoch @@ -131,7 +131,7 @@ fn make_signed_preferences( proposal_slot, validator_index, fee_recipient: Address::ZERO, - gas_limit: 30_000_000, + target_gas_limit: 30_000_000, }, signature: Signature::empty(), }) @@ -271,7 +271,7 @@ fn same_validator_different_dependent_root_not_deduplicated() { validator_index: 42, dependent_root: Hash256::repeat_byte(0xaa), fee_recipient: Address::ZERO, - gas_limit: 30_000_000, + target_gas_limit: 30_000_000, }, signature: Signature::empty(), }), diff --git a/beacon_node/beacon_chain/src/shuffling_cache.rs b/beacon_node/beacon_chain/src/shuffling_cache.rs index 0377b553e3..daaede6ed1 100644 --- a/beacon_node/beacon_chain/src/shuffling_cache.rs +++ b/beacon_node/beacon_chain/src/shuffling_cache.rs @@ -3,23 +3,28 @@ use std::sync::Arc; use itertools::Itertools; use oneshot_broadcast::{Receiver, Sender, oneshot}; +use parking_lot::RwLock; +use state_processing::state_advance::partial_state_advance; use tracing::debug; use types::{ - AttestationShufflingId, BeaconState, Epoch, EthSpec, Hash256, RelativeEpoch, - state::CommitteeCache, + AttestationShufflingId, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, Hash256, PTC, + RelativeEpoch, Slot, state::CommitteeCache, }; -use crate::{BeaconChainError, metrics}; +use crate::{ + BeaconChainError, BeaconChainTypes, BeaconStore, canonical_head::CanonicalHead, metrics, +}; -/// The size of the cache that stores committee caches for quicker verification. +/// The size of the cache that stores shufflings for quicker verification. /// -/// Each entry should be `8 + 800,000 = 800,008` bytes in size with 100k validators. (8-byte hash + -/// 100k indices). Therefore, this cache should be approx `16 * 800,008 = 12.8 MB`. (Note: this -/// ignores a few extra bytes in the caches that should be insignificant compared to the indices). +/// Each entry should be around `8 * 2M + 128KB ~= 16 MB` in size with 2M validators +/// and 32 512-validator PTCs. Therefore, this cache should be approx +/// `16 * 16 MB ~= 256 MB`. (Note: this ignores a few extra bytes in the +/// caches that should be insignificant compared to the indices). pub const DEFAULT_CACHE_SIZE: usize = 16; -/// The maximum number of concurrent committee cache "promises" that can be issued. In effect, this -/// limits the number of concurrent states that can be loaded into memory for the committee cache. +/// The maximum number of concurrent shuffling "promises" that can be issued. In effect, this +/// limits the number of concurrent states that can be loaded into memory for the shuffling. /// This prevents excessive memory usage at the cost of rejecting some attestations. /// /// We set this value to 2 since states can be quite large and have a significant impact on memory @@ -30,19 +35,82 @@ pub const DEFAULT_CACHE_SIZE: usize = 16; const MAX_CONCURRENT_PROMISES: usize = 2; #[derive(Clone)] -pub enum CacheItem { - /// A committee. - Committee(Arc), - /// A promise for a future committee. - Promise(Receiver>), +pub struct CachedShuffling { + pub committee_cache: Arc, + pub ptcs: CachedPTCs, } -impl CacheItem { +#[derive(Clone)] +pub enum CachedPTCs { + PreGloas, + PostGloas(Vec>, Epoch), +} + +impl CachedPTCs { + /// Returns `None` at the Gloas fork boundary (pre-Gloas state, Gloas shuffling epoch); the + /// on-demand miss path in `with_cached_shuffling` handles those. + pub fn try_from_state( + state: &BeaconState, + epoch: Epoch, + spec: &ChainSpec, + ) -> Result, BeaconChainError> { + if shuffling_requires_ptcs(epoch, spec) { + if !state.fork_name_unchecked().gloas_enabled() { + return Ok(None); + } + let ptcs = epoch + .slot_iter(E::slots_per_epoch()) + .map(|slot| state.get_ptc(slot, spec)) + .collect::, _>>()?; + Ok(Some(Self::PostGloas(ptcs, epoch))) + } else { + Ok(Some(Self::PreGloas)) + } + } +} + +impl CachedShuffling { + pub fn new(committee_cache: Arc, ptcs: CachedPTCs) -> Self { + Self { + committee_cache, + ptcs, + } + } + + pub fn ptc_for_slot(&self, slot: Slot) -> Result, BeaconChainError> { + match &self.ptcs { + CachedPTCs::PreGloas => Err(BeaconChainError::AttesterCacheNoPtcPreGloas { slot }), + &CachedPTCs::PostGloas(ref ptcs, epoch) => { + if slot.epoch(E::slots_per_epoch()) != epoch { + Err(BeaconChainError::AttesterCachePtcOutOfBounds { slot, epoch }) + } else { + ptcs.get(slot.as_usize() % E::slots_per_epoch() as usize) + .cloned() + .ok_or(BeaconChainError::AttesterCachePtcOutOfBounds { slot, epoch }) + } + } + } + } +} + +fn shuffling_requires_ptcs(shuffling_epoch: Epoch, spec: &ChainSpec) -> bool { + spec.fork_name_at_epoch(shuffling_epoch).gloas_enabled() +} + +#[derive(Clone)] +pub enum CacheItem { + /// A cached shuffling. + Committee(CachedShuffling), + /// A promise for a future cached shuffling. + Promise(Receiver>), +} + +impl CacheItem { pub fn is_promise(&self) -> bool { matches!(self, CacheItem::Promise(_)) } - pub fn wait(self) -> Result, BeaconChainError> { + pub fn wait(self) -> Result, BeaconChainError> { match self { CacheItem::Committee(cache) => Ok(cache), CacheItem::Promise(receiver) => receiver @@ -52,17 +120,17 @@ impl CacheItem { } } -/// Provides a cache for `CommitteeCache`. +/// Provides a cache for `CommitteeCache` and the associated optional PTCs. /// /// It has been named `ShufflingCache` because `CommitteeCacheCache` is a bit weird and looks like /// a find/replace error. -pub struct ShufflingCache { - cache: HashMap, +pub struct ShufflingCache { + cache: HashMap>, cache_size: usize, head_shuffling_ids: BlockShufflingIds, } -impl ShufflingCache { +impl ShufflingCache { pub fn new(cache_size: usize, head_shuffling_ids: BlockShufflingIds) -> Self { Self { cache: HashMap::new(), @@ -71,22 +139,22 @@ impl ShufflingCache { } } - pub fn get(&mut self, key: &AttestationShufflingId) -> Option { + pub fn get(&mut self, key: &AttestationShufflingId) -> Option> { match self.cache.get(key) { - // The cache contained the committee cache, return it. + // The cache contained the shuffling, return it. item @ Some(CacheItem::Committee(_)) => { metrics::inc_counter(&metrics::SHUFFLING_CACHE_HITS); item.cloned() } - // The cache contains a promise for the committee cache. Check to see if the promise has + // The cache contains a promise for the shuffling. Check to see if the promise has // already been resolved, without waiting for it. item @ Some(CacheItem::Promise(receiver)) => match receiver.try_recv() { // The promise has already been resolved. Replace the entry in the cache with a - // `Committee` entry and then return the committee. - Ok(Some(committee)) => { + // `Committee` entry and then return the cached shuffling. + Ok(Some(cached_shuffling)) => { metrics::inc_counter(&metrics::SHUFFLING_CACHE_PROMISE_HITS); metrics::inc_counter(&metrics::SHUFFLING_CACHE_HITS); - let ready = CacheItem::Committee(committee); + let ready = CacheItem::Committee(cached_shuffling); self.insert_cache_item(key.clone(), ready.clone()); Some(ready) } @@ -97,8 +165,8 @@ impl ShufflingCache { metrics::inc_counter(&metrics::SHUFFLING_CACHE_HITS); item.cloned() } - // The sender has been dropped without sending a committee. There was most likely an - // error computing the committee cache. Drop the key from the cache and return + // The sender has been dropped without sending a shuffling. There was most likely an + // error computing the shuffling. Drop the key from the cache and return // `None` so the caller can recompute the committee. // // It's worth noting that this is the only place where we removed unresolved @@ -113,7 +181,7 @@ impl ShufflingCache { None } }, - // The cache does not have this committee and it's not already promised to be computed. + // The cache does not have this shuffling and it's not already promised to be computed. None => { metrics::inc_counter(&metrics::SHUFFLING_CACHE_MISSES); None @@ -125,27 +193,41 @@ impl ShufflingCache { self.cache.contains_key(key) } - pub fn insert_committee_cache( + /// Check that all entries for Gloas epochs have PTCs. + #[cfg(test)] + pub fn check_gloas_ptcs_invariant(&self, spec: &ChainSpec) -> bool { + self.cache.iter().all(|(key, item)| { + if shuffling_requires_ptcs(key.shuffling_epoch, spec) { + match item { + CacheItem::Committee(cached_shuffling) => { + matches!(cached_shuffling.ptcs, CachedPTCs::PostGloas(..)) + } + CacheItem::Promise(_) => true, + } + } else { + true + } + }) + } + + pub fn insert_committee_cache( &mut self, key: AttestationShufflingId, - committee_cache: &C, + cached_shuffling: CachedShuffling, ) { - if self - .cache - .get(&key) - // Replace the committee if it's not present or if it's a promise. A bird in the hand is - // worth two in the promise-bush! - .is_none_or(CacheItem::is_promise) - { - self.insert_cache_item( - key, - CacheItem::Committee(committee_cache.to_arc_committee_cache()), - ); + match self.cache.get(&key) { + Some(CacheItem::Committee(_)) => { + // Calculation is deterministic, so no need to replace the existing entry. + } + // A bird in the hand is worth two in the promise-bush! + Some(CacheItem::Promise(_)) | None => { + self.insert_cache_item(key, CacheItem::Committee(cached_shuffling)); + } } } /// Prunes the cache first before inserting a new cache item. - fn insert_cache_item(&mut self, key: AttestationShufflingId, cache_item: CacheItem) { + fn insert_cache_item(&mut self, key: AttestationShufflingId, cache_item: CacheItem) { self.prune_cache(); self.cache.insert(key, cache_item); } @@ -188,7 +270,7 @@ impl ShufflingCache { pub fn create_promise( &mut self, key: AttestationShufflingId, - ) -> Result>, BeaconChainError> { + ) -> Result>, BeaconChainError> { let num_active_promises = self .cache .iter() @@ -212,20 +294,170 @@ impl ShufflingCache { } } -/// A helper trait to allow lazy-cloning of the committee cache when inserting into the cache. -pub trait ToArcCommitteeCache { - fn to_arc_committee_cache(&self) -> Arc; -} +pub fn with_cached_shuffling( + canonical_head: &CanonicalHead, + shuffling_cache_lock: &RwLock>, + store: &BeaconStore, + spec: &ChainSpec, + head_block_root: Hash256, + shuffling_epoch: Epoch, + map_fn: F, +) -> Result +where + T: BeaconChainTypes, + F: Fn(&CachedShuffling, Hash256) -> Result, + Error: From, +{ + let head_block = canonical_head + .fork_choice_read_lock() + .get_block(&head_block_root) + .ok_or(BeaconChainError::MissingBeaconBlock(head_block_root))?; -impl ToArcCommitteeCache for CommitteeCache { - fn to_arc_committee_cache(&self) -> Arc { - Arc::new(self.clone()) + let shuffling_id = BlockShufflingIds { + current: head_block.current_epoch_shuffling_id.clone(), + next: head_block.next_epoch_shuffling_id.clone(), + previous: None, + block_root: head_block.root, } -} + .id_for_epoch(shuffling_epoch) + .ok_or_else(|| BeaconChainError::InvalidShufflingId { + shuffling_epoch, + head_block_epoch: head_block.slot.epoch(T::EthSpec::slots_per_epoch()), + })?; -impl ToArcCommitteeCache for Arc { - fn to_arc_committee_cache(&self) -> Arc { - self.clone() + let mut shuffling_cache = { + let _ = metrics::start_timer(&metrics::ATTESTATION_PROCESSING_SHUFFLING_CACHE_WAIT_TIMES); + shuffling_cache_lock.write() + }; + + if let Some(cache_item) = shuffling_cache.get(&shuffling_id) { + drop(shuffling_cache); + + let cached_shuffling = cache_item.wait()?; + map_fn(&cached_shuffling, shuffling_id.shuffling_decision_block) + } else { + // Create an entry in the cache that "promises" this value will eventually be computed. + // This avoids the case where multiple threads attempt to produce the same value at the + // same time. + // + // Creating the promise whilst we hold the `shuffling_cache` lock will prevent the same + // promise from being created twice. + let sender = shuffling_cache.create_promise(shuffling_id.clone())?; + + // Drop the shuffling cache to avoid holding the lock for any longer than required. + drop(shuffling_cache); + + debug!( + shuffling_id = ?shuffling_epoch, + head_block_root = head_block_root.to_string(), + "Committee cache miss" + ); + + // If the block's state will be so far ahead of `shuffling_epoch` that even its previous + // epoch committee cache will be too new, then error. Callers of this function shouldn't be + // requesting such old shufflings for this `head_block_root`. + let head_block_epoch = head_block.slot.epoch(T::EthSpec::slots_per_epoch()); + if head_block_epoch > shuffling_epoch + 1 { + return Err(BeaconChainError::InvalidStateForShuffling { + state_epoch: head_block_epoch, + shuffling_epoch, + } + .into()); + } + + let state_read_timer = + metrics::start_timer(&metrics::ATTESTATION_PROCESSING_STATE_READ_TIMES); + + let cached_head = canonical_head.cached_head(); + let head_state_opt = if cached_head.head_block_root() == head_block_root { + Some(( + cached_head.snapshot.beacon_state.clone(), + cached_head.head_state_root(), + )) + } else { + None + }; + + // Compute the `target_slot` to advance the block's state to. + // + // Since there's a one-epoch look-ahead on the attester shuffling, it suffices to only + // advance into the first slot of the epoch prior to `shuffling_epoch`. + // + // If the `head_block` is already ahead of that slot, then we should load the state at that + // slot, as we've determined above that the `shuffling_epoch` cache will not be too far in + // the past. + let mut target_slot = std::cmp::max( + shuffling_epoch + .saturating_sub(1_u64) + .start_slot(T::EthSpec::slots_per_epoch()), + head_block.slot, + ); + if spec.gloas_fork_epoch == Some(shuffling_epoch) { + target_slot = std::cmp::max( + target_slot, + shuffling_epoch.start_slot(T::EthSpec::slots_per_epoch()), + ); + } + + // If the head state is useful for this request, use it. Otherwise, read a state from disk + // that is advanced as close as possible to `target_slot`. + let (mut state, state_root) = if let Some((state, state_root)) = head_state_opt { + (state, state_root) + } else { + let (state_root, state) = store + .get_advanced_hot_state(head_block_root, target_slot, head_block.state_root) + .map_err(BeaconChainError::DBError)? + .ok_or(BeaconChainError::MissingBeaconState(head_block.state_root))?; + (state, state_root) + }; + + metrics::stop_timer(state_read_timer); + let state_skip_timer = + metrics::start_timer(&metrics::ATTESTATION_PROCESSING_STATE_SKIP_TIMES); + + // If the state is still in an earlier epoch, advance it to the `target_slot` so that its + // next epoch committee cache matches the `shuffling_epoch`. + let advance_to_gloas_fork = spec.gloas_fork_epoch == Some(shuffling_epoch) + && state.current_epoch() < shuffling_epoch; + if state.current_epoch() + 1 < shuffling_epoch || advance_to_gloas_fork { + // Advance the state into the required slot, using the "partial" method since the state + // roots are not relevant for the shuffling. + partial_state_advance(&mut state, Some(state_root), target_slot, spec) + .map_err(BeaconChainError::from)?; + } + metrics::stop_timer(state_skip_timer); + + let committee_building_timer = + metrics::start_timer(&metrics::ATTESTATION_PROCESSING_COMMITTEE_BUILDING_TIMES); + + let relative_epoch = RelativeEpoch::from_epoch(state.current_epoch(), shuffling_epoch) + .map_err(BeaconChainError::IncorrectStateForAttestation)?; + + state + .build_committee_cache(relative_epoch, spec) + .map_err(BeaconChainError::from)?; + + let committee_cache = state + .committee_cache(relative_epoch) + .map_err(BeaconChainError::from)? + .clone(); + // The state has been advanced through the upgrade if needed, so `try_from_state` + // cannot return None here. + let ptcs = CachedPTCs::try_from_state(&state, shuffling_epoch, spec)?.ok_or( + BeaconChainError::BeaconStateError(BeaconStateError::IncorrectStateVariant), + )?; + let shuffling_decision_block = shuffling_id.shuffling_decision_block; + let cached_shuffling = CachedShuffling::new(committee_cache, ptcs); + + shuffling_cache_lock + .write() + .insert_committee_cache(shuffling_id, cached_shuffling.clone()); + + metrics::stop_timer(committee_building_timer); + + sender.send(cached_shuffling.clone()); + + map_fn(&cached_shuffling, shuffling_decision_block) } } @@ -304,7 +536,7 @@ mod test { const TEST_CACHE_SIZE: usize = 5; // Creates a new shuffling cache for testing - fn new_shuffling_cache() -> ShufflingCache { + fn new_shuffling_cache() -> ShufflingCache { create_test_tracing_subscriber(); let current_epoch = 8; @@ -318,6 +550,10 @@ mod test { ShufflingCache::new(TEST_CACHE_SIZE, head_shuffling_ids) } + fn cached_shuffling(committee_cache: Arc) -> CachedShuffling { + CachedShuffling::new(committee_cache, CachedPTCs::PreGloas) + } + /// Returns two different committee caches for testing. fn committee_caches() -> (Arc, Arc) { let harness = BeaconChainHarness::builder(MinimalEthSpec) @@ -366,12 +602,12 @@ mod test { ); // Resolve the promise. - sender.send(committee_a.clone()); + sender.send(cached_shuffling(committee_a.clone())); // Ensure the promise has been resolved. let item = cache.get(&id_a).unwrap(); assert!( - matches!(item, CacheItem::Committee(committee) if committee == committee_a), + matches!(item, CacheItem::Committee(cached_shuffling) if cached_shuffling.committee_cache == committee_a), "the promise should be resolved" ); assert_eq!(cache.cache.len(), 1, "the cache should have one entry"); @@ -428,30 +664,30 @@ mod test { ); // Resolve promise A. - sender_a.send(committee_a.clone()); + sender_a.send(cached_shuffling(committee_a.clone())); // Ensure promise A has been resolved. let item = cache.get(&id_a).unwrap(); assert!( - matches!(item, CacheItem::Committee(committee) if committee == committee_a), + matches!(item, CacheItem::Committee(cached_shuffling) if cached_shuffling.committee_cache == committee_a), "promise A should be resolved" ); // Resolve promise B. - sender_b.send(committee_b.clone()); + sender_b.send(cached_shuffling(committee_b.clone())); // Ensure promise B has been resolved. let item = cache.get(&id_b).unwrap(); assert!( - matches!(item, CacheItem::Committee(committee) if committee == committee_b), + matches!(item, CacheItem::Committee(cached_shuffling) if cached_shuffling.committee_cache == committee_b), "promise B should be resolved" ); // Check both entries again. assert!( - matches!(cache.get(&id_a).unwrap(), CacheItem::Committee(committee) if committee == committee_a), + matches!(cache.get(&id_a).unwrap(), CacheItem::Committee(cached_shuffling) if cached_shuffling.committee_cache == committee_a), "promise A should remain resolved" ); assert!( - matches!(cache.get(&id_b).unwrap(), CacheItem::Committee(committee) if committee == committee_b), + matches!(cache.get(&id_b).unwrap(), CacheItem::Committee(cached_shuffling) if cached_shuffling.committee_cache == committee_b), "promise B should remain resolved" ); assert_eq!(cache.cache.len(), 2, "the cache should have two entries"); @@ -485,9 +721,9 @@ mod test { let mut cache = new_shuffling_cache(); let id_a = shuffling_id(1); let committee_cache_a = Arc::new(CommitteeCache::default()); - cache.insert_committee_cache(id_a.clone(), &committee_cache_a); + cache.insert_committee_cache(id_a.clone(), cached_shuffling(committee_cache_a.clone())); assert!( - matches!(cache.get(&id_a).unwrap(), CacheItem::Committee(committee_cache) if committee_cache == committee_cache_a), + matches!(cache.get(&id_a).unwrap(), CacheItem::Committee(cached_shuffling) if cached_shuffling.committee_cache == committee_cache_a), "should insert committee cache" ); } @@ -500,7 +736,10 @@ mod test { .collect::>(); for (shuffling_id, committee_cache) in shuffling_id_and_committee_caches.iter() { - cache.insert_committee_cache(shuffling_id.clone(), committee_cache); + cache.insert_committee_cache( + shuffling_id.clone(), + cached_shuffling(committee_cache.clone()), + ); } for i in 1..(TEST_CACHE_SIZE + 1) { @@ -533,7 +772,7 @@ mod test { shuffling_epoch: (current_epoch + 1).into(), shuffling_decision_block: Hash256::from_low_u64_be(current_epoch + i as u64), }; - cache.insert_committee_cache(shuffling_id, &committee_cache); + cache.insert_committee_cache(shuffling_id, cached_shuffling(committee_cache.clone())); } // Now, update the head shuffling ids @@ -546,11 +785,17 @@ mod test { cache.update_head_shuffling_ids(head_shuffling_ids.clone()); // Insert head state shuffling ids. Should not be overridden by other shuffling ids. - cache.insert_committee_cache(head_shuffling_ids.current.clone(), &committee_cache); - cache.insert_committee_cache(head_shuffling_ids.next.clone(), &committee_cache); + cache.insert_committee_cache( + head_shuffling_ids.current.clone(), + cached_shuffling(committee_cache.clone()), + ); + cache.insert_committee_cache( + head_shuffling_ids.next.clone(), + cached_shuffling(committee_cache.clone()), + ); cache.insert_committee_cache( head_shuffling_ids.previous.clone().unwrap(), - &committee_cache, + cached_shuffling(committee_cache.clone()), ); // Insert a few entries for older epochs. @@ -559,7 +804,7 @@ mod test { shuffling_epoch: Epoch::from(i), shuffling_decision_block: Hash256::from_low_u64_be(i as u64), }; - cache.insert_committee_cache(shuffling_id, &committee_cache); + cache.insert_committee_cache(shuffling_id, cached_shuffling(committee_cache.clone())); } assert!( @@ -580,4 +825,41 @@ mod test { "should limit cache size" ); } + + /// Pre-Gloas state across the Gloas fork: epoch G-1 returns `Some(PreGloas)`, epoch G and + /// G+1 return `None` (the boundary skip). + #[test] + fn try_from_state_skips_at_gloas_boundary() { + create_test_tracing_subscriber(); + + let mut spec = ForkName::Fulu.make_genesis_spec(E::default_spec()); + let gloas_fork_epoch = Epoch::new(2); + spec.gloas_fork_epoch = Some(gloas_fork_epoch); + + let harness = BeaconChainHarness::builder(MinimalEthSpec) + .spec(Arc::new(spec.clone())) + .deterministic_keypairs(8) + .fresh_ephemeral_store() + .build(); + let state = harness.get_current_state(); + assert!(!state.fork_name_unchecked().gloas_enabled()); + + for (epoch, expect_pre_gloas) in [ + (gloas_fork_epoch - 1, true), + (gloas_fork_epoch, false), + (gloas_fork_epoch + 1, false), + ] { + let result = CachedPTCs::::try_from_state(&state, epoch, &spec) + .expect("must not error at the boundary"); + if expect_pre_gloas { + assert!( + matches!(result, Some(CachedPTCs::PreGloas)), + "epoch {}: expected Some(PreGloas)", + epoch + ); + } else { + assert!(result.is_none(), "epoch {}: expected None", epoch); + } + } + } } diff --git a/beacon_node/beacon_chain/src/state_advance_timer.rs b/beacon_node/beacon_chain/src/state_advance_timer.rs index cb916cb514..6408f861f8 100644 --- a/beacon_node/beacon_chain/src/state_advance_timer.rs +++ b/beacon_node/beacon_chain/src/state_advance_timer.rs @@ -15,7 +15,9 @@ //! 2. There's a possibility that the head block is never built upon, causing wasted CPU cycles. use crate::validator_monitor::HISTORIC_EPOCHS as VALIDATOR_MONITOR_HISTORIC_EPOCHS; use crate::{ - BeaconChain, BeaconChainError, BeaconChainTypes, chain_config::FORK_CHOICE_LOOKAHEAD_FACTOR, + BeaconChain, BeaconChainError, BeaconChainTypes, + chain_config::FORK_CHOICE_LOOKAHEAD_FACTOR, + shuffling_cache::{CachedPTCs, CachedShuffling}, }; use slot_clock::SlotClock; use state_processing::per_slot_processing; @@ -394,19 +396,30 @@ fn advance_head(beacon_chain: &Arc>) -> Resu .map_err(BeaconChainError::from)?; let committee_cache = state .committee_cache(RelativeEpoch::Next) - .map_err(BeaconChainError::from)?; - beacon_chain - .shuffling_cache - .write() - .insert_committee_cache(shuffling_id.clone(), committee_cache); + .map_err(BeaconChainError::from)? + .clone(); + let shuffling_epoch = RelativeEpoch::Next.into_epoch(state.current_epoch()); - debug!( - ?head_block_root, - next_epoch_shuffling_root = ?shuffling_id.shuffling_decision_block, - state_epoch = %state.current_epoch(), - current_epoch = %current_slot.epoch(T::EthSpec::slots_per_epoch()), - "Primed proposer and attester caches" - ); + if let Some(ptcs) = CachedPTCs::try_from_state(&state, shuffling_epoch, &beacon_chain.spec)? + { + beacon_chain.shuffling_cache.write().insert_committee_cache( + shuffling_id.clone(), + CachedShuffling::new(committee_cache, ptcs), + ); + + debug!( + ?head_block_root, + next_epoch_shuffling_root = ?shuffling_id.shuffling_decision_block, + state_epoch = %state.current_epoch(), + current_epoch = %current_slot.epoch(T::EthSpec::slots_per_epoch()), + "Primed proposer and attester caches" + ); + } else { + debug!( + %shuffling_epoch, + "Skipping priming of attester cache for Gloas boundary epoch" + ); + } } let final_slot = state.slot(); diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 8e9cc61208..919bb43bfd 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -1,4 +1,3 @@ -use crate::blob_verification::GossipVerifiedBlob; use crate::block_verification_types::{AsBlock, AvailableBlockData, LookupBlock, RangeSyncBlock}; use crate::custody_context::NodeCustodyType; use crate::data_availability_checker::DataAvailabilityChecker; @@ -124,8 +123,8 @@ pub fn get_kzg(spec: &ChainSpec) -> Arc { pub type BaseHarnessType = Witness; -pub type DiskHarnessType = BaseHarnessType, BeaconNodeBackend>; -pub type EphemeralHarnessType = BaseHarnessType, MemoryStore>; +pub type DiskHarnessType = BaseHarnessType; +pub type EphemeralHarnessType = BaseHarnessType; pub type BoxedMutator = Box< dyn FnOnce( @@ -334,7 +333,7 @@ impl Builder> { /// Manually restore from a given `MemoryStore`. pub fn resumed_ephemeral_store( mut self, - store: Arc, MemoryStore>>, + store: Arc>, ) -> Self { let mutator = move |builder: BeaconChainBuilder<_>| { builder @@ -350,7 +349,7 @@ impl Builder> { /// Disk store, start from genesis. pub fn fresh_disk_store( mut self, - store: Arc, BeaconNodeBackend>>, + store: Arc>, ) -> Self { let validator_keypairs = self .validator_keypairs @@ -384,7 +383,7 @@ impl Builder> { /// Disk store, resume. pub fn resumed_disk_store( mut self, - store: Arc, BeaconNodeBackend>>, + store: Arc>, ) -> Self { let mutator = move |builder: BeaconChainBuilder<_>| { builder @@ -399,8 +398,8 @@ impl Builder> { impl Builder> where E: EthSpec, - Hot: ItemStore, - Cold: ItemStore, + Hot: ItemStore, + Cold: ItemStore, { pub fn new(eth_spec_instance: E) -> Self { let runtime = TestRuntime::default(); @@ -760,8 +759,8 @@ pub type HarnessSyncContributions = Vec<( impl BeaconChainHarness> where E: EthSpec, - Hot: ItemStore, - Cold: ItemStore, + Hot: ItemStore, + Cold: ItemStore, { pub fn builder(eth_spec_instance: E) -> Builder> { create_test_tracing_subscriber(); @@ -3696,55 +3695,39 @@ where Ok(()) } - /// Simulate some of the blobs / data columns being seen on gossip. - /// Converts the blobs to data columns if the slot is Fulu or later. - pub async fn process_gossip_blobs_or_columns<'a>( + /// Simulate the block's custody data columns (or those in `custody_columns_opt`) being + /// seen on gossip. Panics unless PeerDAS is enabled for the block's epoch. + pub async fn process_gossip_columns( &self, block: &SignedBeaconBlock, - blobs: impl Iterator>, - proofs: impl Iterator, custody_columns_opt: Option>, ) { - let is_peerdas_enabled = self.chain.spec.is_peer_das_enabled_for_epoch(block.epoch()); - if is_peerdas_enabled { - let custody_columns = custody_columns_opt.unwrap_or_else(|| { - let epoch = block.slot().epoch(E::slots_per_epoch()); - self.chain - .sampling_columns_for_epoch(epoch) - .iter() - .copied() - .collect() - }); + assert!(self.chain.spec.is_peer_das_enabled_for_epoch(block.epoch())); + let custody_columns = custody_columns_opt.unwrap_or_else(|| { + let epoch = block.slot().epoch(E::slots_per_epoch()); + self.chain + .sampling_columns_for_epoch(epoch) + .iter() + .copied() + .collect() + }); - let verified_columns = generate_data_column_sidecars_from_block(block, &self.spec) - .into_iter() - .filter(|c| custody_columns.contains(c.index())) - .map(|sidecar| { - let subnet_id = - DataColumnSubnetId::from_column_index(*sidecar.index(), &self.spec); - self.chain - .verify_data_column_sidecar_for_gossip(sidecar, subnet_id) - }) - .collect::, _>>() + let verified_columns = generate_data_column_sidecars_from_block(block, &self.spec) + .into_iter() + .filter(|c| custody_columns.contains(c.index())) + .map(|sidecar| { + let subnet_id = DataColumnSubnetId::from_column_index(*sidecar.index(), &self.spec); + self.chain + .verify_data_column_sidecar_for_gossip(sidecar, subnet_id) + }) + .collect::, _>>() + .unwrap(); + + if !verified_columns.is_empty() { + self.chain + .process_gossip_data_columns(verified_columns, || Ok(())) + .await .unwrap(); - - if !verified_columns.is_empty() { - self.chain - .process_gossip_data_columns(verified_columns, || Ok(())) - .await - .unwrap(); - } - } else { - for (i, (kzg_proof, blob)) in proofs.into_iter().zip(blobs).enumerate() { - let sidecar = - Arc::new(BlobSidecar::new(i, blob.clone(), block, *kzg_proof).unwrap()); - let gossip_blob = GossipVerifiedBlob::new(sidecar, i as u64, &self.chain) - .expect("should obtain gossip verified blob"); - self.chain - .process_gossip_blob(gossip_blob) - .await - .expect("should import valid gossip verified blob"); - } } } } diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 533ef61219..67fe0eaae0 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -1456,20 +1456,8 @@ async fn verify_and_process_gossip_data_sidecars( data_sidecars: DataSidecars, ) { match data_sidecars { - DataSidecars::Blobs(blob_sidecars) => { - for blob_sidecar in blob_sidecars { - let blob_index = blob_sidecar.index; - let gossip_verified = harness - .chain - .verify_blob_sidecar_for_gossip(blob_sidecar.clone(), blob_index) - .expect("should obtain gossip verified blob"); - - harness - .chain - .process_gossip_blob(gossip_verified) - .await - .expect("should import valid gossip verified blob"); - } + DataSidecars::Blobs(_blob_sidecars) => { + // Blob gossip is deprecated, blobs are available via RPC. } DataSidecars::DataColumns(column_sidecars) => { let gossip_verified = column_sidecars @@ -1521,14 +1509,9 @@ async fn verify_block_for_gossip_slashing_detection() { let verified_block = harness.chain.verify_block_for_gossip(block1).await.unwrap(); - if let Some((kzg_proofs, blobs)) = blobs1 { + if blobs1.is_some() { harness - .process_gossip_blobs_or_columns( - verified_block.block(), - blobs.iter(), - kzg_proofs.iter(), - None, - ) + .process_gossip_columns(verified_block.block(), None) .await; } harness diff --git a/beacon_node/beacon_chain/tests/events.rs b/beacon_node/beacon_chain/tests/events.rs index 29d0e38b93..baa6975303 100644 --- a/beacon_node/beacon_chain/tests/events.rs +++ b/beacon_node/beacon_chain/tests/events.rs @@ -1,12 +1,9 @@ use arbitrary::Arbitrary; -use beacon_chain::blob_verification::GossipVerifiedBlob; use beacon_chain::data_column_verification::GossipVerifiedDataColumn; use beacon_chain::test_utils::{ BeaconChainHarness, fork_name_from_env, generate_data_column_sidecars_from_block, test_spec, }; use eth2::types::{EventKind, SseBlobSidecar, SseDataColumnSidecar}; -use rand::SeedableRng; -use rand::rngs::StdRng; use std::sync::Arc; use types::data::FixedBlobSidecarList; use types::{ @@ -17,44 +14,6 @@ use types::{ type E = MinimalEthSpec; -/// Verifies that a blob event is emitted when a gossip verified blob is received via gossip or the publish block API. -#[tokio::test] -async fn blob_sidecar_event_on_process_gossip_blob() { - if fork_name_from_env().is_some_and(|f| !f.deneb_enabled() || f.fulu_enabled()) { - return; - }; - - let spec = Arc::new(test_spec::()); - let harness = BeaconChainHarness::builder(E::default()) - .spec(spec) - .deterministic_keypairs(8) - .fresh_ephemeral_store() - .mock_execution_layer() - .build(); - - // subscribe to blob sidecar events - let event_handler = harness.chain.event_handler.as_ref().unwrap(); - let mut blob_event_receiver = event_handler.subscribe_blob_sidecar(); - - // build and process a gossip verified blob - let kzg = harness.chain.kzg.as_ref(); - let mut rng = StdRng::seed_from_u64(0xDEADBEEF0BAD5EEDu64); - let sidecar = BlobSidecar::random_valid(&mut rng, kzg) - .map(Arc::new) - .unwrap(); - let gossip_verified_blob = GossipVerifiedBlob::__assumed_valid(sidecar); - let expected_sse_blobs = SseBlobSidecar::from_blob_sidecar(gossip_verified_blob.as_blob()); - - let _ = harness - .chain - .process_gossip_blob(gossip_verified_blob) - .await - .unwrap(); - - let sidecar_event = blob_event_receiver.try_recv().unwrap(); - assert_eq!(sidecar_event, EventKind::BlobSidecar(expected_sse_blobs)); -} - /// Verifies that a data column event is emitted when a gossip verified data column is received via gossip or the publish block API. #[tokio::test] async fn data_column_sidecar_event_on_process_gossip_data_column() { diff --git a/beacon_node/beacon_chain/tests/op_verification.rs b/beacon_node/beacon_chain/tests/op_verification.rs index 2f97f10745..adc14541a9 100644 --- a/beacon_node/beacon_chain/tests/op_verification.rs +++ b/beacon_node/beacon_chain/tests/op_verification.rs @@ -27,7 +27,7 @@ static KEYPAIRS: LazyLock> = type E = MinimalEthSpec; type TestHarness = BeaconChainHarness>; -type HotColdDB = store::HotColdDB, BeaconNodeBackend>; +type HotColdDB = store::HotColdDB; fn get_store(db_path: &TempDir) -> Arc { let spec = Arc::new(test_spec::()); diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index be85fc2245..abf1fe48a6 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -1035,6 +1035,7 @@ async fn payload_preparation() { None, None, None, + None, ); assert_eq!(rig.previous_payload_attributes(), payload_attributes); } diff --git a/beacon_node/beacon_chain/tests/prepare_payload.rs b/beacon_node/beacon_chain/tests/prepare_payload.rs index 47dd1ef517..de8bfb3865 100644 --- a/beacon_node/beacon_chain/tests/prepare_payload.rs +++ b/beacon_node/beacon_chain/tests/prepare_payload.rs @@ -34,7 +34,7 @@ type TestHarness = BeaconChainHarness>; fn get_store( db_path: &TempDir, spec: Arc, -) -> Arc, BeaconNodeBackend>> { +) -> Arc> { let store_config = StoreConfig { prune_payloads: false, ..StoreConfig::default() @@ -46,7 +46,7 @@ fn get_store_generic( db_path: &TempDir, config: StoreConfig, spec: Arc, -) -> Arc, BeaconNodeBackend>> { +) -> Arc> { create_test_tracing_subscriber(); let hot_path = db_path.path().join("chain_db"); let cold_path = db_path.path().join("freezer_db"); @@ -64,7 +64,7 @@ fn get_store_generic( } fn get_harness( - store: Arc, BeaconNodeBackend>>, + store: Arc>, validator_count: usize, ) -> TestHarness { // Most tests expect to retain historic states, so we use this as the default. @@ -81,7 +81,7 @@ fn get_harness( } fn get_harness_generic( - store: Arc, BeaconNodeBackend>>, + store: Arc>, validator_count: usize, chain_config: ChainConfig, node_custody_type: NodeCustodyType, diff --git a/beacon_node/beacon_chain/tests/schema_stability.rs b/beacon_node/beacon_chain/tests/schema_stability.rs index 8200748ae6..899a40511d 100644 --- a/beacon_node/beacon_chain/tests/schema_stability.rs +++ b/beacon_node/beacon_chain/tests/schema_stability.rs @@ -20,7 +20,7 @@ use tempfile::{TempDir, tempdir}; use types::{ChainSpec, Hash256, MainnetEthSpec, Slot}; type E = MainnetEthSpec; -type Store = Arc, BeaconNodeBackend>>; +type Store = Arc>; type TestHarness = BeaconChainHarness>; const VALIDATOR_COUNT: usize = 32; diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 0ff9f6841d..0ac77dcfaa 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -106,7 +106,7 @@ fn get_or_reconstruct_blobs( } } -fn get_store(db_path: &TempDir) -> Arc, BeaconNodeBackend>> { +fn get_store(db_path: &TempDir) -> Arc> { let store_config = StoreConfig { prune_payloads: false, ..StoreConfig::default() @@ -118,7 +118,7 @@ fn get_store_generic( db_path: &TempDir, config: StoreConfig, spec: ChainSpec, -) -> Arc, BeaconNodeBackend>> { +) -> Arc> { create_test_tracing_subscriber(); let hot_path = db_path.path().join("chain_db"); let cold_path = db_path.path().join("freezer_db"); @@ -136,7 +136,7 @@ fn get_store_generic( } fn get_harness( - store: Arc, BeaconNodeBackend>>, + store: Arc>, validator_count: usize, ) -> TestHarness { // Most tests expect to retain historic states, so we use this as the default. @@ -153,7 +153,7 @@ fn get_harness( } fn get_harness_import_all_data_columns( - store: Arc, BeaconNodeBackend>>, + store: Arc>, validator_count: usize, ) -> TestHarness { // Most tests expect to retain historic states, so we use this as the default. @@ -171,7 +171,7 @@ fn get_harness_import_all_data_columns( } fn get_harness_generic( - store: Arc, BeaconNodeBackend>>, + store: Arc>, validator_count: usize, chain_config: ChainConfig, node_custody_type: NodeCustodyType, @@ -205,7 +205,7 @@ fn check_db_invariants(harness: &TestHarness) { } fn get_states_descendant_of_block( - store: &HotColdDB, BeaconNodeBackend>, + store: &HotColdDB, block_root: Hash256, ) -> Vec<(Hash256, Slot)> { let summaries = store.load_hot_state_summaries().unwrap(); @@ -1209,7 +1209,8 @@ fn check_shuffling_compatible( .with_committee_cache( block_root, head_state.current_epoch(), - |committee_cache, _| { + |cached_shuffling, _| { + let committee_cache = cached_shuffling.committee_cache.as_ref(); let state_cache = head_state.committee_cache(RelativeEpoch::Current).unwrap(); // We used to check for false negatives here, but had to remove that check // because `shuffling_is_compatible` does not guarantee their absence. @@ -1247,7 +1248,8 @@ fn check_shuffling_compatible( .with_committee_cache( block_root, head_state.previous_epoch(), - |committee_cache, _| { + |cached_shuffling, _| { + let committee_cache = cached_shuffling.committee_cache.as_ref(); let state_cache = head_state.committee_cache(RelativeEpoch::Previous).unwrap(); if previous_epoch_shuffling_is_compatible { assert_eq!(committee_cache, state_cache.as_ref()); @@ -5859,7 +5861,7 @@ async fn test_gloas_hot_state_hierarchy() { /// Check that the HotColdDB's split_slot is equal to the start slot of the last finalized epoch. fn check_split_slot( harness: &TestHarness, - store: Arc, BeaconNodeBackend>>, + store: Arc>, ) { let split_slot = store.get_split_slot(); assert_eq!( diff --git a/beacon_node/beacon_processor/src/lib.rs b/beacon_node/beacon_processor/src/lib.rs index ce3851ea54..af3ff09c8a 100644 --- a/beacon_node/beacon_processor/src/lib.rs +++ b/beacon_node/beacon_processor/src/lib.rs @@ -390,7 +390,6 @@ pub enum Work { process_batch: Box>) + Send + Sync>, }, GossipBlock(AsyncFn), - GossipBlobSidecar(AsyncFn), GossipDataColumnSidecar(AsyncFn), GossipPartialDataColumnSidecar(AsyncFn), DelayedImportBlock { @@ -471,7 +470,6 @@ pub enum WorkType { UnknownLightClientOptimisticUpdate, GossipAggregateBatch, GossipBlock, - GossipBlobSidecar, GossipDataColumnSidecar, GossipPartialDataColumnSidecar, DelayedImportBlock, @@ -528,7 +526,6 @@ impl Work { Work::GossipAggregate { .. } => WorkType::GossipAggregate, Work::GossipAggregateBatch { .. } => WorkType::GossipAggregateBatch, Work::GossipBlock(_) => WorkType::GossipBlock, - Work::GossipBlobSidecar(_) => WorkType::GossipBlobSidecar, Work::GossipDataColumnSidecar(_) => WorkType::GossipDataColumnSidecar, Work::GossipPartialDataColumnSidecar(_) => WorkType::GossipPartialDataColumnSidecar, Work::DelayedImportBlock { .. } => WorkType::DelayedImportBlock, @@ -843,8 +840,6 @@ impl BeaconProcessor { } else if let Some(item) = work_queues.gossip_execution_payload_queue.pop() { Some(item) - } else if let Some(item) = work_queues.gossip_blob_queue.pop() { - Some(item) } else if let Some(item) = work_queues.gossip_data_column_queue.pop() { Some(item) } else if let Some(item) = @@ -1157,9 +1152,6 @@ impl BeaconProcessor { Work::GossipBlock { .. } => { work_queues.gossip_block_queue.push(work, work_id) } - Work::GossipBlobSidecar { .. } => { - work_queues.gossip_blob_queue.push(work, work_id) - } Work::GossipDataColumnSidecar { .. } => { work_queues.gossip_data_column_queue.push(work, work_id) } @@ -1306,7 +1298,6 @@ impl BeaconProcessor { } WorkType::GossipAggregateBatch => 0, // No queue WorkType::GossipBlock => work_queues.gossip_block_queue.len(), - WorkType::GossipBlobSidecar => work_queues.gossip_blob_queue.len(), WorkType::GossipDataColumnSidecar => { work_queues.gossip_data_column_queue.len() } @@ -1536,7 +1527,6 @@ impl BeaconProcessor { | Work::ColumnReconstruction(process_fn) => task_spawner.spawn_async(process_fn), Work::IgnoredRpcBlock { process_fn } => task_spawner.spawn_blocking(process_fn), Work::GossipBlock(work) - | Work::GossipBlobSidecar(work) | Work::GossipDataColumnSidecar(work) | Work::GossipPartialDataColumnSidecar(work) | Work::GossipExecutionPayload(work) => task_spawner.spawn_async(async move { diff --git a/beacon_node/beacon_processor/src/scheduler/work_queue.rs b/beacon_node/beacon_processor/src/scheduler/work_queue.rs index 2fdc15182c..ebd66e743d 100644 --- a/beacon_node/beacon_processor/src/scheduler/work_queue.rs +++ b/beacon_node/beacon_processor/src/scheduler/work_queue.rs @@ -125,7 +125,6 @@ pub struct BeaconProcessorQueueLengths { chain_segment_queue: usize, backfill_chain_segment: usize, gossip_block_queue: usize, - gossip_blob_queue: usize, gossip_data_column_queue: usize, gossip_partial_data_column_queue: usize, delayed_block_queue: usize, @@ -202,7 +201,6 @@ impl BeaconProcessorQueueLengths { chain_segment_queue: 64, backfill_chain_segment: 64, gossip_block_queue: 1024, - gossip_blob_queue: 1024, gossip_data_column_queue: 1024, gossip_partial_data_column_queue: 1024, delayed_block_queue: 1024, @@ -218,7 +216,7 @@ impl BeaconProcessorQueueLengths { payload_envelopes_brange_queue: 1024, payload_envelopes_broots_queue: 1024, gossip_bls_to_execution_change_queue: 16384, - // TODO(EIP-7732): verify 1024 is preferable. I used same value as `gossip_block_queue` and `gossip_blob_queue` + // TODO(EIP-7732): verify 1024 is preferable. gossip_execution_payload_queue: 1024, // TODO(EIP-7732) how big should this queue be? gossip_execution_payload_bid_queue: 1024, @@ -261,7 +259,6 @@ pub struct WorkQueues { pub chain_segment_queue: FifoQueue>, pub backfill_chain_segment: FifoQueue>, pub gossip_block_queue: FifoQueue>, - pub gossip_blob_queue: FifoQueue>, pub gossip_data_column_queue: FifoQueue>, pub gossip_partial_data_column_queue: FifoQueue>, pub delayed_block_queue: FifoQueue>, @@ -332,7 +329,6 @@ impl WorkQueues { let chain_segment_queue = FifoQueue::new(queue_lengths.chain_segment_queue); let backfill_chain_segment = FifoQueue::new(queue_lengths.backfill_chain_segment); let gossip_block_queue = FifoQueue::new(queue_lengths.gossip_block_queue); - let gossip_blob_queue = FifoQueue::new(queue_lengths.gossip_blob_queue); let gossip_data_column_queue = FifoQueue::new(queue_lengths.gossip_data_column_queue); let gossip_partial_data_column_queue = FifoQueue::new(queue_lengths.gossip_partial_data_column_queue); @@ -401,7 +397,6 @@ impl WorkQueues { column_reconstruction_queue, backfill_chain_segment, gossip_block_queue, - gossip_blob_queue, gossip_data_column_queue, gossip_partial_data_column_queue, delayed_block_queue, diff --git a/beacon_node/client/src/builder.rs b/beacon_node/client/src/builder.rs index f532ef716e..0a3c414632 100644 --- a/beacon_node/client/src/builder.rs +++ b/beacon_node/client/src/builder.rs @@ -98,8 +98,8 @@ impl where TSlotClock: SlotClock + Clone + 'static, E: EthSpec + 'static, - THotStore: ItemStore + 'static, - TColdStore: ItemStore + 'static, + THotStore: ItemStore + 'static, + TColdStore: ItemStore + 'static, { /// Instantiates a new, empty builder. /// @@ -815,8 +815,8 @@ impl where TSlotClock: SlotClock + Clone + 'static, E: EthSpec + 'static, - THotStore: ItemStore + 'static, - TColdStore: ItemStore + 'static, + THotStore: ItemStore + 'static, + TColdStore: ItemStore + 'static, { /// Consumes the internal `BeaconChainBuilder`, attaching the resulting `BeaconChain` to self. #[instrument(skip_all)] @@ -847,8 +847,7 @@ where } } -impl - ClientBuilder, BeaconNodeBackend>> +impl ClientBuilder> where TSlotClock: SlotClock + 'static, E: EthSpec + 'static, @@ -889,8 +888,8 @@ where impl ClientBuilder> where E: EthSpec + 'static, - THotStore: ItemStore + 'static, - TColdStore: ItemStore + 'static, + THotStore: ItemStore + 'static, + TColdStore: ItemStore + 'static, { /// Specifies that the slot clock should read the time from the computers system clock. pub fn system_time_slot_clock(mut self) -> Result { diff --git a/beacon_node/execution_layer/src/engine_api.rs b/beacon_node/execution_layer/src/engine_api.rs index acf5f2778b..d9dd9aaf4c 100644 --- a/beacon_node/execution_layer/src/engine_api.rs +++ b/beacon_node/execution_layer/src/engine_api.rs @@ -1,12 +1,11 @@ use crate::engines::ForkchoiceState; use crate::http::{ ENGINE_FORKCHOICE_UPDATED_V1, ENGINE_FORKCHOICE_UPDATED_V2, ENGINE_FORKCHOICE_UPDATED_V3, - ENGINE_FORKCHOICE_UPDATED_V4, ENGINE_GET_BLOBS_V1, ENGINE_GET_BLOBS_V2, - ENGINE_GET_CLIENT_VERSION_V1, ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, - ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, ENGINE_GET_PAYLOAD_V1, ENGINE_GET_PAYLOAD_V2, - ENGINE_GET_PAYLOAD_V3, ENGINE_GET_PAYLOAD_V4, ENGINE_GET_PAYLOAD_V5, ENGINE_GET_PAYLOAD_V6, - ENGINE_NEW_PAYLOAD_V1, ENGINE_NEW_PAYLOAD_V2, ENGINE_NEW_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V4, - ENGINE_NEW_PAYLOAD_V5, + ENGINE_FORKCHOICE_UPDATED_V4, ENGINE_GET_BLOBS_V2, ENGINE_GET_CLIENT_VERSION_V1, + ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, + ENGINE_GET_PAYLOAD_V1, ENGINE_GET_PAYLOAD_V2, ENGINE_GET_PAYLOAD_V3, ENGINE_GET_PAYLOAD_V4, + ENGINE_GET_PAYLOAD_V5, ENGINE_GET_PAYLOAD_V6, ENGINE_NEW_PAYLOAD_V1, ENGINE_NEW_PAYLOAD_V2, + ENGINE_NEW_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V4, ENGINE_NEW_PAYLOAD_V5, }; use eth2::types::{ BlobsBundle, SsePayloadAttributes, SsePayloadAttributesV1, SsePayloadAttributesV2, @@ -178,6 +177,8 @@ pub struct PayloadAttributes { pub parent_beacon_block_root: Hash256, #[superstruct(only(V4), partial_getter(copy))] pub slot_number: u64, + #[superstruct(only(V4), partial_getter(copy))] + pub target_gas_limit: u64, } impl PayloadAttributes { @@ -188,19 +189,29 @@ impl PayloadAttributes { withdrawals: Option>, parent_beacon_block_root: Option, slot_number: Option, + target_gas_limit: Option, ) -> Self { - match (withdrawals, parent_beacon_block_root, slot_number) { - (Some(withdrawals), Some(parent_beacon_block_root), Some(slot_number)) => { - PayloadAttributes::V4(PayloadAttributesV4 { - timestamp, - prev_randao, - suggested_fee_recipient, - withdrawals, - parent_beacon_block_root, - slot_number, - }) - } - (Some(withdrawals), Some(parent_beacon_block_root), None) => { + match ( + withdrawals, + parent_beacon_block_root, + slot_number, + target_gas_limit, + ) { + ( + Some(withdrawals), + Some(parent_beacon_block_root), + Some(slot_number), + Some(target_gas_limit), + ) => PayloadAttributes::V4(PayloadAttributesV4 { + timestamp, + prev_randao, + suggested_fee_recipient, + withdrawals, + parent_beacon_block_root, + slot_number, + target_gas_limit, + }), + (Some(withdrawals), Some(parent_beacon_block_root), _, _) => { PayloadAttributes::V3(PayloadAttributesV3 { timestamp, prev_randao, @@ -209,13 +220,13 @@ impl PayloadAttributes { parent_beacon_block_root, }) } - (Some(withdrawals), None, _) => PayloadAttributes::V2(PayloadAttributesV2 { + (Some(withdrawals), None, _, _) => PayloadAttributes::V2(PayloadAttributesV2 { timestamp, prev_randao, suggested_fee_recipient, withdrawals, }), - (None, _, _) => PayloadAttributes::V1(PayloadAttributesV1 { + (None, _, _, _) => PayloadAttributes::V1(PayloadAttributesV1 { timestamp, prev_randao, suggested_fee_recipient, @@ -260,7 +271,7 @@ impl From for SsePayloadAttributes { withdrawals, parent_beacon_block_root, }), - // V4 maps to V3 for SSE (slot_number is not part of the SSE spec) + // V4 maps to V3 for SSE (slot_number/target_gas_limit are not part of the SSE spec) PayloadAttributes::V4(PayloadAttributesV4 { timestamp, prev_randao, @@ -268,6 +279,7 @@ impl From for SsePayloadAttributes { withdrawals, parent_beacon_block_root, slot_number: _, + target_gas_limit: _, }) => Self::V3(SsePayloadAttributesV3 { timestamp, prev_randao, @@ -594,7 +606,6 @@ pub struct EngineCapabilities { pub get_payload_v5: bool, pub get_payload_v6: bool, pub get_client_version_v1: bool, - pub get_blobs_v1: bool, pub get_blobs_v2: bool, pub get_blobs_v3: bool, } @@ -656,9 +667,6 @@ impl EngineCapabilities { if self.get_client_version_v1 { response.push(ENGINE_GET_CLIENT_VERSION_V1); } - if self.get_blobs_v1 { - response.push(ENGINE_GET_BLOBS_V1); - } if self.get_blobs_v2 { response.push(ENGINE_GET_BLOBS_V2); } diff --git a/beacon_node/execution_layer/src/engine_api/http.rs b/beacon_node/execution_layer/src/engine_api/http.rs index 110e155c77..8df7d2a54b 100644 --- a/beacon_node/execution_layer/src/engine_api/http.rs +++ b/beacon_node/execution_layer/src/engine_api/http.rs @@ -62,7 +62,6 @@ pub const ENGINE_EXCHANGE_CAPABILITIES_TIMEOUT: Duration = Duration::from_secs(1 pub const ENGINE_GET_CLIENT_VERSION_V1: &str = "engine_getClientVersionV1"; pub const ENGINE_GET_CLIENT_VERSION_TIMEOUT: Duration = Duration::from_secs(1); -pub const ENGINE_GET_BLOBS_V1: &str = "engine_getBlobsV1"; pub const ENGINE_GET_BLOBS_V2: &str = "engine_getBlobsV2"; pub const ENGINE_GET_BLOBS_V3: &str = "engine_getBlobsV3"; pub const ENGINE_GET_BLOBS_TIMEOUT: Duration = Duration::from_secs(1); @@ -92,8 +91,8 @@ pub static LIGHTHOUSE_CAPABILITIES: &[&str] = &[ ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1, ENGINE_GET_CLIENT_VERSION_V1, - ENGINE_GET_BLOBS_V1, ENGINE_GET_BLOBS_V2, + ENGINE_GET_BLOBS_V3, ]; /// We opt to initialize the JsonClientVersionV1 rather than the ClientVersionV1 @@ -716,20 +715,6 @@ impl HttpJsonRpc { } } - pub async fn get_blobs_v1( - &self, - versioned_hashes: Vec, - ) -> Result>>, Error> { - let params = json!([versioned_hashes]); - - self.rpc_request( - ENGINE_GET_BLOBS_V1, - params, - ENGINE_GET_BLOBS_TIMEOUT * self.execution_timeout_multiplier, - ) - .await - } - pub async fn get_blobs_v2( &self, versioned_hashes: Vec, @@ -1271,7 +1256,6 @@ impl HttpJsonRpc { get_payload_v5: capabilities.contains(ENGINE_GET_PAYLOAD_V5), get_payload_v6: capabilities.contains(ENGINE_GET_PAYLOAD_V6), get_client_version_v1: capabilities.contains(ENGINE_GET_CLIENT_VERSION_V1), - get_blobs_v1: capabilities.contains(ENGINE_GET_BLOBS_V1), get_blobs_v2: capabilities.contains(ENGINE_GET_BLOBS_V2), get_blobs_v3: capabilities.contains(ENGINE_GET_BLOBS_V3), }) diff --git a/beacon_node/execution_layer/src/engine_api/json_structures.rs b/beacon_node/execution_layer/src/engine_api/json_structures.rs index 9d9391a1e1..fb516e3e16 100644 --- a/beacon_node/execution_layer/src/engine_api/json_structures.rs +++ b/beacon_node/execution_layer/src/engine_api/json_structures.rs @@ -777,6 +777,9 @@ pub struct JsonPayloadAttributes { #[superstruct(only(V4))] #[serde(with = "serde_utils::u64_hex_be")] pub slot_number: u64, + #[superstruct(only(V4))] + #[serde(with = "serde_utils::u64_hex_be")] + pub target_gas_limit: u64, } impl From for JsonPayloadAttributes { @@ -807,6 +810,7 @@ impl From for JsonPayloadAttributes { withdrawals: pa.withdrawals.into_iter().map(Into::into).collect(), parent_beacon_block_root: pa.parent_beacon_block_root, slot_number: pa.slot_number, + target_gas_limit: pa.target_gas_limit, }), } } @@ -840,6 +844,7 @@ impl From for PayloadAttributes { withdrawals: jpa.withdrawals.into_iter().map(Into::into).collect(), parent_beacon_block_root: jpa.parent_beacon_block_root, slot_number: jpa.slot_number, + target_gas_limit: jpa.target_gas_limit, }), } } diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index b2dabb7c01..78076bee6c 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -4,7 +4,7 @@ //! This crate only provides useful functionality for "The Merge", it does not provide any of the //! deposit-contract functionality that the `beacon_node/eth1` crate already provides. -use crate::json_structures::{BlobAndProofV1, BlobAndProofV2, BlobAndProofV3}; +use crate::json_structures::{BlobAndProofV2, BlobAndProofV3}; use crate::payload_cache::PayloadCache; use arc_swap::ArcSwapOption; use auth::{Auth, JwtKey, strip_prefix}; @@ -73,6 +73,8 @@ pub const DEFAULT_EXECUTION_ENDPOINT: &str = "http://localhost:8551/"; /// Name for the default file used for the jwt secret. pub const DEFAULT_JWT_FILE: &str = "jwt.hex"; +pub const DEFAULT_GAS_LIMIT: u64 = 60_000_000; + /// A fee recipient address for use during block production. Only used as a very last resort if /// there is no address provided by the user. /// @@ -358,7 +360,10 @@ impl> BlockProposalContents { pub parent_hash: ExecutionBlockHash, - pub parent_gas_limit: u64, + // NOTE: The `parent_gas_limit` is a bit scuffed. We made it optional for Gloas because it + // isn't currently required, but it should possibly be made non-optional again if needed. + // Or we should superstruct this type. + pub parent_gas_limit: Option, pub proposer_gas_limit: Option, pub payload_attributes: &'a PayloadAttributes, pub forkchoice_update_params: &'a ForkchoiceUpdateParameters, @@ -1717,23 +1722,6 @@ impl ExecutionLayer { } } - pub async fn get_blobs_v1( - &self, - query: Vec, - ) -> Result>>, Error> { - let capabilities = self.get_engine_capabilities(None).await?; - - if capabilities.get_blobs_v1 { - self.engine() - .request(|engine| async move { engine.api.get_blobs_v1(query).await }) - .await - .map_err(Box::new) - .map_err(Error::EngineError) - } else { - Err(Error::GetBlobsNotSupported) - } - } - pub async fn get_blobs_v2( &self, query: Vec, @@ -2082,7 +2070,7 @@ fn verify_builder_bid( let payload_withdrawals_root = header.withdrawals_root().ok(); let expected_gas_limit = proposer_gas_limit - .and_then(|target_gas_limit| expected_gas_limit(parent_gas_limit, target_gas_limit, spec)); + .and_then(|target_gas_limit| expected_gas_limit(parent_gas_limit?, target_gas_limit, spec)); if header.parent_hash() != parent_hash { Err(Box::new(InvalidBuilderPayload::ParentHash { diff --git a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs index 4a46ce0f88..b05db6e8bd 100644 --- a/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs +++ b/beacon_node/execution_layer/src/test_utils/execution_block_generator.rs @@ -565,20 +565,16 @@ impl ExecutionBlockGenerator { self.insert_block(Block::PoS(payload))?; } - // Post-Gloas, the justified and finalized block hashes must be non-zero, since the - // CL always has a known parent_block_hash to reference. - if let Some(head_block) = self.blocks.get(&head_block_hash) - && self - .get_fork_at_timestamp(head_block.timestamp()) - .gloas_enabled() - { + // If Gloas was enabled from genesis, the justified and finalized block hashes must be + // non-zero, since the CL always has a known parent_block_hash to reference. + if self.get_fork_at_timestamp(0).gloas_enabled() { assert!( forkchoice_state.safe_block_hash != ExecutionBlockHash::zero(), - "post-Gloas safe_block_hash must not be zero" + "for Gloas genesis safe_block_hash must not be zero" ); assert!( forkchoice_state.finalized_block_hash != ExecutionBlockHash::zero(), - "post-Gloas finalized_block_hash must not be zero" + "for Gloas genesis finalized_block_hash must not be zero" ); } diff --git a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs index 64eecccc58..9924fbe474 100644 --- a/beacon_node/execution_layer/src/test_utils/handle_rpc.rs +++ b/beacon_node/execution_layer/src/test_utils/handle_rpc.rs @@ -494,20 +494,6 @@ pub async fn handle_rpc( _ => unreachable!(), } } - ENGINE_GET_BLOBS_V1 => { - let versioned_hashes = - get_param::>(params, 0).map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?; - let generator = ctx.execution_block_generator.read(); - // V1: per-element nullable array, positionally matching the request. - let response: Vec>> = versioned_hashes - .iter() - .map(|hash| match generator.get_blob_and_proof(hash) { - Some(BlobAndProof::V1(v1)) => Some(v1), - _ => None, - }) - .collect(); - Ok(serde_json::to_value(response).unwrap()) - } ENGINE_GET_BLOBS_V2 => { let versioned_hashes = get_param::>(params, 0).map_err(|s| (s, BAD_PARAMS_ERROR_CODE))?; 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 d6243a7c4d..d456c9adc1 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_builder.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_builder.rs @@ -282,7 +282,7 @@ impl BidStuff for BuilderBid { #[derive(Clone)] pub struct PayloadParametersCloned { pub parent_hash: ExecutionBlockHash, - pub parent_gas_limit: u64, + pub parent_gas_limit: Option, pub proposer_gas_limit: Option, pub payload_attributes: PayloadAttributes, pub forkchoice_update_params: ForkchoiceUpdateParameters, @@ -903,6 +903,7 @@ impl MockBuilder { expected_withdrawals, None, None, + None, ), ForkName::Deneb | ForkName::Electra | ForkName::Fulu => PayloadAttributes::new( timestamp, @@ -911,6 +912,7 @@ impl MockBuilder { expected_withdrawals, Some(head_block_root), None, + None, ), ForkName::Gloas => PayloadAttributes::new( timestamp, @@ -919,6 +921,7 @@ impl MockBuilder { expected_withdrawals, Some(head_block_root), Some(slot.as_u64()), + None, // TODO(gloas): pass target_gas_limit ), ForkName::Base | ForkName::Altair => { return Err("invalid fork".to_string()); @@ -969,7 +972,7 @@ impl MockBuilder { let payload_parameters = PayloadParametersCloned { parent_hash: head_execution_hash, - parent_gas_limit: head_gas_limit, + parent_gas_limit: Some(head_gas_limit), proposer_gas_limit: Some(proposer_gas_limit), payload_attributes, forkchoice_update_params, diff --git a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs index 5b721bcab2..583808281f 100644 --- a/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs +++ b/beacon_node/execution_layer/src/test_utils/mock_execution_layer.rs @@ -105,6 +105,7 @@ impl MockExecutionLayer { None, None, None, + None, ); // Insert a proposer to ensure the fork choice updated command works. @@ -146,11 +147,12 @@ impl MockExecutionLayer { None, None, None, + None, ); let payload_parameters = PayloadParameters { parent_hash, - parent_gas_limit, + parent_gas_limit: Some(parent_gas_limit), proposer_gas_limit: None, payload_attributes: &payload_attributes, forkchoice_update_params: &forkchoice_update_params, @@ -199,11 +201,12 @@ impl MockExecutionLayer { None, None, None, + None, ); let payload_parameters = PayloadParameters { parent_hash, - parent_gas_limit, + parent_gas_limit: Some(parent_gas_limit), proposer_gas_limit: None, payload_attributes: &payload_attributes, forkchoice_update_params: &forkchoice_update_params, diff --git a/beacon_node/execution_layer/src/test_utils/mod.rs b/beacon_node/execution_layer/src/test_utils/mod.rs index 4eb03778f8..570008b62a 100644 --- a/beacon_node/execution_layer/src/test_utils/mod.rs +++ b/beacon_node/execution_layer/src/test_utils/mod.rs @@ -57,7 +57,6 @@ pub const DEFAULT_ENGINE_CAPABILITIES: EngineCapabilities = EngineCapabilities { get_payload_v5: true, get_payload_v6: true, get_client_version_v1: true, - get_blobs_v1: true, get_blobs_v2: true, get_blobs_v3: true, }; diff --git a/beacon_node/http_api/src/block_id.rs b/beacon_node/http_api/src/block_id.rs index e6b1ed0879..8843541c11 100644 --- a/beacon_node/http_api/src/block_id.rs +++ b/beacon_node/http_api/src/block_id.rs @@ -129,6 +129,15 @@ impl BlockId { .is_finalized_block(root, block_slot) .map_err(warp_utils::reject::unhandled_error)?; Ok((*root, execution_optimistic, finalized)) + } else if chain.early_attester_cache.get_block(*root).is_some() { + // Fall back to the early attester cache for blocks that are in fork choice + // but haven't been written to disk yet. + let execution_optimistic = chain + .canonical_head + .fork_choice_read_lock() + .is_optimistic_or_invalid_block(root) + .unwrap_or(false); + Ok((*root, execution_optimistic, false)) } else { Err(warp_utils::reject::custom_not_found(format!( "beacon block with root {}", @@ -143,9 +152,18 @@ impl BlockId { root: &Hash256, chain: &BeaconChain, ) -> Result>, warp::Rejection> { - chain + if let Some(block) = chain .get_blinded_block(root) - .map_err(warp_utils::reject::unhandled_error) + .map_err(warp_utils::reject::unhandled_error)? + { + return Ok(Some(block)); + } + // Fall back to the early attester cache for blocks that are in fork choice + // but haven't been written to disk yet. + Ok(chain + .early_attester_cache + .get_block(*root) + .map(|b| b.clone_as_blinded())) } /// Return the `SignedBeaconBlock` identified by `self`. @@ -253,20 +271,20 @@ impl BlockId { } _ => { let (root, execution_optimistic, finalized) = self.root(chain)?; - chain + let block_opt = chain .get_block(&root) .await - .map_err(warp_utils::reject::unhandled_error) - .and_then(|block_opt| { - block_opt - .map(|block| (Arc::new(block), execution_optimistic, finalized)) - .ok_or_else(|| { - warp_utils::reject::custom_not_found(format!( - "beacon block with root {}", - root - )) - }) - }) + .map_err(warp_utils::reject::unhandled_error)?; + let block = block_opt + .map(Arc::new) + .or_else(|| chain.early_attester_cache.get_block(root)) + .ok_or_else(|| { + warp_utils::reject::custom_not_found(format!( + "beacon block with root {}", + root + )) + })?; + Ok((block, execution_optimistic, finalized)) } } } @@ -290,16 +308,20 @@ impl BlockId { } let data_column_sidecars = if let Some(indices) = query.indices { - indices - .iter() - .filter_map(|index| chain.get_data_column(&root, index, fork_name).transpose()) - .collect::, _>>() + chain + .get_data_columns_checking_all_caches(root, &indices) .map_err(warp_utils::reject::unhandled_error)? } else { chain - .get_data_columns(&root, fork_name) + .early_attester_cache + .get_data_columns(root) + .map(Ok) + .unwrap_or_else(|| { + chain + .get_data_columns(&root, fork_name) + .map(|opt| opt.unwrap_or_default()) + }) .map_err(warp_utils::reject::unhandled_error)? - .unwrap_or_default() }; let fork_name = block @@ -433,20 +455,18 @@ impl BlockId { warp_utils::reject::custom_not_found(format!("no blobs stored for block {root}")) })?; - let blob_sidecar_list_filtered = match indices { - Some(vec) => { - let list: Vec<_> = vec - .into_iter() - .flat_map(|index| blob_sidecar_list.get(index as usize).cloned()) - .collect(); + let blob_sidecar_list: Vec<_> = blob_sidecar_list.into_iter().collect(); - BlobSidecarList::new(list, max_blobs_per_block) - .map_err(|e| warp_utils::reject::custom_server_error(format!("{:?}", e)))? - } + let blob_sidecar_list = match indices { + Some(indices) => indices + .into_iter() + .filter_map(|i| blob_sidecar_list.get(i as usize).cloned()) + .collect(), None => blob_sidecar_list, }; - Ok(blob_sidecar_list_filtered) + BlobSidecarList::new(blob_sidecar_list, max_blobs_per_block) + .map_err(|e| warp_utils::reject::custom_server_error(format!("{:?}", e))) } fn get_blobs_from_data_columns( @@ -507,3 +527,177 @@ impl fmt::Display for BlockId { write!(f, "{}", self.0) } } + +#[cfg(test)] +mod tests { + use super::*; + use beacon_chain::{ + PayloadVerificationStatus, + block_verification_types::{AvailableBlockData, RangeSyncBlock}, + test_utils::{ + BeaconChainHarness, EphemeralHarnessType, fork_name_from_env, + generate_data_column_sidecars_from_block, + }, + }; + use std::time::Duration; + use types::MinimalEthSpec; + + type TestHarness = BeaconChainHarness>; + + fn harness() -> TestHarness { + BeaconChainHarness::builder(MinimalEthSpec) + .default_spec() + .deterministic_keypairs(8) + .fresh_ephemeral_store() + .mock_execution_layer() + .build() + } + + #[tokio::test] + async fn root_uses_early_attester_cache_for_unpersisted_block() { + let Some(fork_name) = fork_name_from_env().filter(|fork_name| fork_name.fulu_enabled()) + else { + return; + }; + let harness = harness(); + let chain = &harness.chain; + + harness.execution_block_generator().set_min_blob_count(1); + harness.advance_slot(); + + let (block_contents, post_state) = harness + .make_block(harness.get_current_state(), harness.get_current_slot()) + .await; + let (block, _) = block_contents; + let block_root = block.canonical_root(); + let block_fork_name = chain.spec.fork_name_at_epoch(block.epoch()); + + assert_eq!( + block_fork_name, fork_name, + "precondition: test block must be produced at {fork_name:?}" + ); + assert!( + block.num_expected_blobs() > 0, + "precondition: {fork_name:?} test block must have blobs that can be converted to data columns" + ); + + assert!( + !chain.store.block_exists(&block_root).unwrap(), + "precondition: test block must not be persisted" + ); + assert!( + chain.get_blinded_block(&block_root).unwrap().is_none(), + "precondition: test block must not be retrievable from the store" + ); + assert!( + chain + .get_data_columns(&block_root, block_fork_name) + .unwrap() + .is_none(), + "precondition: test data columns must not be retrievable from the store" + ); + assert!( + !chain.block_is_known_to_fork_choice(&block_root), + "precondition: test block must not be imported into fork choice yet" + ); + + let sampling_columns = chain.sampling_columns_for_epoch(block.epoch()); + let data_columns = generate_data_column_sidecars_from_block(&block, &chain.spec) + .into_iter() + .filter(|column| sampling_columns.contains(column.index())) + .collect::>(); + assert!( + !data_columns.is_empty(), + "precondition: {fork_name:?} test block must produce data columns" + ); + + let available_block = RangeSyncBlock::new( + block.clone(), + AvailableBlockData::new_with_data_columns(data_columns), + &chain.data_availability_checker, + chain.spec.clone(), + ) + .unwrap() + .into_available_block(); + + let current_slot = harness.get_current_slot(); + let cached_head = chain.canonical_head.cached_head(); + let canonical_head_proposer_index = chain + .canonical_head_proposer_index(current_slot, &cached_head) + .unwrap(); + + chain + .canonical_head + .fork_choice_write_lock() + .on_block( + current_slot, + block.message(), + block_root, + Duration::ZERO, + &post_state, + PayloadVerificationStatus::Verified, + canonical_head_proposer_index, + &chain.spec, + ) + .unwrap(); + + assert!( + chain.block_is_known_to_fork_choice(&block_root), + "precondition: test block must be imported into fork choice" + ); + assert!( + !chain.store.block_exists(&block_root).unwrap(), + "precondition: fork choice insertion must not persist the block" + ); + + let proto_block = chain + .canonical_head + .fork_choice_read_lock() + .get_block(&block_root) + .unwrap(); + + chain + .early_attester_cache + .add_head_block(block_root, &available_block, proto_block, &post_state) + .unwrap(); + + let cached_data_columns = chain + .early_attester_cache + .get_data_columns(block_root) + .expect("precondition: data columns must be cached"); + assert!( + !cached_data_columns.is_empty(), + "precondition: cached data columns must be non-empty" + ); + + assert_eq!( + BlockId(CoreBlockId::Root(block_root)).root(chain).unwrap(), + (block_root, false, false) + ); + + let (blinded_block, execution_optimistic, finalized) = + BlockId(CoreBlockId::Root(block_root)) + .blinded_block(chain) + .unwrap(); + assert_eq!(blinded_block.canonical_root(), block_root); + assert_eq!(blinded_block.slot(), block.slot()); + assert!(!execution_optimistic); + assert!(!finalized); + + let (data_columns, data_columns_fork_name, execution_optimistic, finalized) = + BlockId(CoreBlockId::Root(block_root)) + .get_data_columns(DataColumnIndicesQuery { indices: None }, chain) + .unwrap(); + assert_eq!(data_columns, cached_data_columns); + assert_eq!(data_columns_fork_name, fork_name); + assert!(!execution_optimistic); + assert!(!finalized); + + chain.early_attester_cache.clear(); + + assert!( + BlockId(CoreBlockId::Root(block_root)).root(chain).is_err(), + "root lookup should fail once the unpersisted block leaves the early attester cache" + ); + } +} diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index ca4ab85524..b46576ddad 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -1,7 +1,6 @@ use crate::metrics; use std::future::Future; -use beacon_chain::blob_verification::{GossipBlobError, GossipVerifiedBlob}; use beacon_chain::block_verification_types::{AsBlock, LookupBlock}; use beacon_chain::data_column_verification::GossipVerifiedDataColumn; use beacon_chain::validator_monitor::get_block_delay_ms; @@ -26,13 +25,12 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; use tokio::sync::mpsc::UnboundedSender; -use tracing::{Span, debug, debug_span, error, field, info, instrument, warn}; +use tracing::{Span, debug, error, field, info, instrument, warn}; use tree_hash::TreeHash; use types::{ - AbstractExecPayload, BeaconBlockRef, BlobSidecar, BlobsList, BlockImportSource, - DataColumnSidecar, DataColumnSubnetId, EthSpec, ExecPayload, ExecutionBlockHash, ForkName, - FullPayload, FullPayloadBellatrix, Hash256, KzgProofs, SignedBeaconBlock, - SignedBlindedBeaconBlock, + AbstractExecPayload, BeaconBlockRef, BlobsList, BlockImportSource, DataColumnSidecar, + DataColumnSubnetId, EthSpec, ExecPayload, ExecutionBlockHash, ForkName, FullPayload, + FullPayloadBellatrix, Hash256, KzgProofs, SignedBeaconBlock, SignedBlindedBeaconBlock, }; use warp::{Rejection, Reply, reply::Response}; @@ -195,23 +193,8 @@ pub async fn publish_block>( Ok(()) }; - // Wait for blobs/columns to get gossip verified before proceeding further as we need them for import. - let (gossip_verified_blobs, gossip_verified_columns) = build_sidecar_task_handle.await?; - - for blob in gossip_verified_blobs.into_iter().flatten() { - publish_blob_sidecars(network_tx, &blob).map_err(|_| { - warp_utils::reject::custom_server_error("unable to publish blob sidecars".into()) - })?; - if let Err(e) = Box::pin(chain.process_gossip_blob(blob)).await { - let msg = format!("Invalid blob: {e}"); - return if let BroadcastValidation::Gossip = validation_level { - Err(warp_utils::reject::broadcast_without_import(msg)) - } else { - error!(reason = &msg, "Invalid blob provided to HTTP API"); - Err(warp_utils::reject::custom_bad_request(msg)) - }; - } - } + // Wait for columns to get gossip verified before proceeding further as we need them for import. + let gossip_verified_columns = build_sidecar_task_handle.await?; if !gossip_verified_columns.is_empty() { if let Some(data_column_publishing_delay) = data_column_publishing_delay_for_testing { @@ -342,18 +325,9 @@ pub async fn publish_block>( } } -type BuildDataSidecarTaskResult = Result< - ( - Vec>>, - Vec>, - ), - Rejection, ->; +type BuildDataSidecarTaskResult = Result>, Rejection>; -/// Convert blobs to either: -/// -/// 1. Blob sidecars if prior to peer DAS, or -/// 2. Data column sidecars if post peer DAS. +/// Convert blobs to data column sidecars. fn spawn_build_data_sidecar_task( chain: Arc>, block: Arc>>, @@ -365,22 +339,9 @@ fn spawn_build_data_sidecar_task( .spawn_blocking_handle( move || { let Some((kzg_proofs, blobs)) = proofs_and_blobs else { - return Ok((vec![], vec![])); + return Ok(vec![]); }; - let _span = debug_span!("build_data_sidecars").entered(); - - let peer_das_enabled = chain.spec.is_peer_das_enabled_for_epoch(block.epoch()); - if !peer_das_enabled { - // Pre-PeerDAS: construct blob sidecars for the network. - let gossip_verified_blobs = - build_gossip_verified_blobs(&chain, &block, blobs, kzg_proofs)?; - Ok((gossip_verified_blobs, vec![])) - } else { - // Post PeerDAS: construct data columns. - let gossip_verified_data_columns = - build_data_columns(&chain, &block, blobs, kzg_proofs)?; - Ok((vec![], gossip_verified_data_columns)) - } + build_data_columns(&chain, &block, blobs, kzg_proofs) }, "build_data_sidecars", ) @@ -424,76 +385,6 @@ fn build_data_columns( Ok(gossip_verified_data_columns) } -fn build_gossip_verified_blobs( - chain: &BeaconChain, - block: &SignedBeaconBlock>, - blobs: BlobsList, - kzg_proofs: KzgProofs, -) -> Result>>, Rejection> { - let slot = block.slot(); - let gossip_verified_blobs = kzg_proofs - .into_iter() - .zip(blobs) - .enumerate() - .map(|(i, (proof, unverified_blob))| { - let timer = metrics::start_timer( - &beacon_chain::metrics::BLOB_SIDECAR_INCLUSION_PROOF_COMPUTATION, - ); - let blob_sidecar = BlobSidecar::new(i, unverified_blob, block, proof) - .map(Arc::new) - .map_err(|e| { - error!( - error = ?e, - blob_index = i, - %slot, - "Invalid blob - not publishing block" - ); - warp_utils::reject::custom_bad_request(format!("{e:?}")) - })?; - drop(timer); - - let gossip_verified_blob = - GossipVerifiedBlob::new(blob_sidecar.clone(), blob_sidecar.index, chain); - - match gossip_verified_blob { - Ok(blob) => Ok(Some(blob)), - Err(GossipBlobError::RepeatBlob { proposer, .. }) => { - // Log the error but do not abort publication, we may need to publish the block - // or some of the other blobs if the block & blobs are only partially published - // by the other publisher. - debug!( - blob_index = blob_sidecar.index, - %slot, - proposer, - "Blob for publication already known" - ); - Ok(None) - } - Err(e) => { - error!( - blob_index = blob_sidecar.index, - %slot, - error = ?e, - "Blob for publication is gossip-invalid" - ); - Err(warp_utils::reject::custom_bad_request(e.to_string())) - } - } - }) - .collect::, Rejection>>()?; - - Ok(gossip_verified_blobs) -} - -fn publish_blob_sidecars( - sender_clone: &UnboundedSender>, - blob: &GossipVerifiedBlob, -) -> Result<(), BlockError> { - let pubsub_message = PubsubMessage::BlobSidecar(Box::new((blob.index(), blob.clone_blob()))); - crate::utils::publish_pubsub_message(sender_clone, pubsub_message) - .map_err(|_| BlockError::BeaconChainError(Box::new(BeaconChainError::UnableToPublish))) -} - pub(crate) fn publish_column_sidecars( sender_clone: &UnboundedSender>, data_column_sidecars: &[GossipVerifiedDataColumn], diff --git a/beacon_node/http_api/src/test_utils.rs b/beacon_node/http_api/src/test_utils.rs index f27a04d17a..467a5216b1 100644 --- a/beacon_node/http_api/src/test_utils.rs +++ b/beacon_node/http_api/src/test_utils.rs @@ -57,7 +57,7 @@ pub struct ApiServer> { type HarnessBuilder = Builder>; type Initializer = Box) -> HarnessBuilder>; -type Mutator = BoxedMutator, MemoryStore>; +type Mutator = BoxedMutator; impl InteractiveTester { pub async fn new(spec: Option, validator_count: usize) -> Self { diff --git a/beacon_node/http_api/tests/broadcast_validation_tests.rs b/beacon_node/http_api/tests/broadcast_validation_tests.rs index a189be1cfc..98629a1c5e 100644 --- a/beacon_node/http_api/tests/broadcast_validation_tests.rs +++ b/beacon_node/http_api/tests/broadcast_validation_tests.rs @@ -1587,7 +1587,7 @@ pub async fn block_seen_on_gossip_without_blobs_or_columns() { let state = tester.harness.get_current_state(); let fork_name = state.fork_name(&tester.harness.spec).unwrap(); // Gloas blocks don't carry blobs (execution data comes via envelopes). - if !fork_name.deneb_enabled() || fork_name.gloas_enabled() { + if !fork_name.fulu_enabled() || fork_name.gloas_enabled() { return; } @@ -1647,7 +1647,7 @@ pub async fn block_seen_on_gossip_without_blobs_or_columns() { /// This test checks that an HTTP POST request with the block & blobs/columns succeeds with a 200 response /// even if the block has already been seen on gossip without all blobs/columns. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -pub async fn block_seen_on_gossip_with_some_blobs_or_columns() { +pub async fn block_seen_on_gossip_with_columns() { let validation_level: Option = Some(BroadcastValidation::Gossip); // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing @@ -1658,7 +1658,7 @@ pub async fn block_seen_on_gossip_with_some_blobs_or_columns() { let state = tester.harness.get_current_state(); let fork_name = state.fork_name(&tester.harness.spec).unwrap(); // Gloas blocks don't carry blobs (execution data comes via envelopes). - if !fork_name.deneb_enabled() || fork_name.gloas_enabled() { + if !fork_name.fulu_enabled() || fork_name.gloas_enabled() { return; } @@ -1690,9 +1690,6 @@ pub async fn block_seen_on_gossip_with_some_blobs_or_columns() { blobs.0.len() ); - let partial_kzg_proofs = [*blobs.0.first().unwrap()]; - let partial_blobs = [blobs.1.first().unwrap().clone()]; - // Simulate the block being seen on gossip. block .clone() @@ -1702,12 +1699,7 @@ pub async fn block_seen_on_gossip_with_some_blobs_or_columns() { // Simulate some of the blobs being seen on gossip. tester .harness - .process_gossip_blobs_or_columns( - &block, - partial_blobs.iter(), - partial_kzg_proofs.iter(), - Some(get_custody_columns(&tester, block.slot())), - ) + .process_gossip_columns(&block, Some(get_custody_columns(&tester, block.slot()))) .await; // It should not yet be added to fork choice because all blobs have not been seen. @@ -1740,7 +1732,7 @@ pub async fn block_seen_on_gossip_with_some_blobs_or_columns() { /// This test checks that an HTTP POST request with the block & blobs/columns succeeds with a 200 response /// even if the blobs/columns have already been seen on gossip. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -pub async fn blobs_or_columns_seen_on_gossip_without_block() { +pub async fn columns_seen_on_gossip_without_block() { let spec = test_spec::(); let validation_level: Option = Some(BroadcastValidation::Gossip); @@ -1752,7 +1744,7 @@ pub async fn blobs_or_columns_seen_on_gossip_without_block() { let state = tester.harness.get_current_state(); let fork_name = state.fork_name(&tester.harness.spec).unwrap(); // Gloas blocks don't carry blobs (execution data comes via envelopes). - if !fork_name.deneb_enabled() || fork_name.gloas_enabled() { + if !fork_name.fulu_enabled() || fork_name.gloas_enabled() { return; } @@ -1778,12 +1770,7 @@ pub async fn blobs_or_columns_seen_on_gossip_without_block() { // Simulate the blobs being seen on gossip. tester .harness - .process_gossip_blobs_or_columns( - &block, - blobs.iter(), - kzg_proofs.iter(), - Some(get_custody_columns(&tester, block.slot())), - ) + .process_gossip_columns(&block, Some(get_custody_columns(&tester, block.slot()))) .await; // It should not yet be added to fork choice because the block has not been seen. @@ -1816,7 +1803,7 @@ pub async fn blobs_or_columns_seen_on_gossip_without_block() { /// This test checks that an HTTP POST request with the block succeeds with a 200 response /// if just the blobs have already been seen on gossip. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn blobs_or_columns_seen_on_gossip_without_block_and_no_http_blobs_or_columns() { +async fn columns_seen_on_gossip_without_block_and_no_http_columns() { let validation_level: Option = Some(BroadcastValidation::Gossip); // Validator count needs to be at least 32 or proposer boost gets set to 0 when computing @@ -1827,7 +1814,7 @@ async fn blobs_or_columns_seen_on_gossip_without_block_and_no_http_blobs_or_colu let state = tester.harness.get_current_state(); let fork_name = state.fork_name(&tester.harness.spec).unwrap(); // Gloas blocks don't carry blobs (execution data comes via envelopes). - if !fork_name.deneb_enabled() || fork_name.gloas_enabled() { + if !fork_name.fulu_enabled() || fork_name.gloas_enabled() { return; } @@ -1848,18 +1835,13 @@ async fn blobs_or_columns_seen_on_gossip_without_block_and_no_http_blobs_or_colu let state_a = tester.harness.get_current_state(); let ((block, blobs), _) = tester.harness.make_block(state_a, slot_b).await; - let (kzg_proofs, blobs) = blobs.expect("should have some blobs"); + let (_, blobs) = blobs.expect("should have some blobs"); assert!(!blobs.is_empty()); // Simulate the blobs being seen on gossip. tester .harness - .process_gossip_blobs_or_columns( - &block, - blobs.iter(), - kzg_proofs.iter(), - Some(get_custody_columns(&tester, block.slot())), - ) + .process_gossip_columns(&block, Some(get_custody_columns(&tester, block.slot()))) .await; // It should not yet be added to fork choice because the block has not been seen. @@ -1893,7 +1875,7 @@ async fn blobs_or_columns_seen_on_gossip_without_block_and_no_http_blobs_or_colu } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn slashable_blobs_or_columns_seen_on_gossip_cause_failure() { +async fn slashable_columns_seen_on_gossip_cause_failure() { let validation_level: Option = Some(BroadcastValidation::ConsensusAndEquivocation); @@ -1905,7 +1887,7 @@ async fn slashable_blobs_or_columns_seen_on_gossip_cause_failure() { let state = tester.harness.get_current_state(); let fork_name = state.fork_name(&tester.harness.spec).unwrap(); // Gloas blocks don't carry blobs (execution data comes via envelopes). - if !fork_name.deneb_enabled() || fork_name.gloas_enabled() { + if !fork_name.fulu_enabled() || fork_name.gloas_enabled() { return; } @@ -1926,19 +1908,13 @@ async fn slashable_blobs_or_columns_seen_on_gossip_cause_failure() { let state_a = tester.harness.get_current_state(); let ((block_a, blobs_a), _) = tester.harness.make_block(state_a.clone(), slot_b).await; - let ((block_b, blobs_b), _) = tester.harness.make_block(state_a, slot_b).await; + let ((block_b, _), _) = tester.harness.make_block(state_a, slot_b).await; let (kzg_proofs_a, blobs_a) = blobs_a.expect("should have some blobs"); - let (kzg_proofs_b, blobs_b) = blobs_b.expect("should have some blobs"); // Simulate the blobs of block B being seen on gossip. tester .harness - .process_gossip_blobs_or_columns( - &block_b, - blobs_b.iter(), - kzg_proofs_b.iter(), - Some(get_custody_columns(&tester, block_b.slot())), - ) + .process_gossip_columns(&block_b, Some(get_custody_columns(&tester, block_b.slot()))) .await; // It should not yet be added to fork choice because block B has not been seen. @@ -1984,7 +1960,7 @@ pub async fn duplicate_block_status_code() { // Gloas blocks don't carry blobs (execution data comes via envelopes). let spec = test_spec::(); let genesis_fork = spec.fork_name_at_slot::(Slot::new(0)); - if !genesis_fork.deneb_enabled() || genesis_fork.gloas_enabled() { + if !genesis_fork.fulu_enabled() || genesis_fork.gloas_enabled() { return; } diff --git a/beacon_node/http_api/tests/interactive_tests.rs b/beacon_node/http_api/tests/interactive_tests.rs index b47f8e946a..7b5fb02714 100644 --- a/beacon_node/http_api/tests/interactive_tests.rs +++ b/beacon_node/http_api/tests/interactive_tests.rs @@ -2,7 +2,7 @@ use beacon_chain::custody_context::NodeCustodyType; use beacon_chain::{ ChainConfig, - chain_config::{DisallowedReOrgOffsets, ReOrgThreshold}, + chain_config::DisallowedReOrgOffsets, test_utils::{ AttestationStrategy, BlockStrategy, LightClientStrategy, SyncCommitteeStrategy, test_spec, }, @@ -23,7 +23,7 @@ use std::sync::Arc; use std::time::Duration; use types::{ Address, Epoch, EthSpec, ExecPayload, ExecutionBlockHash, ForkName, Hash256, MainnetEthSpec, - MinimalEthSpec, ProposerPreparationData, Slot, Uint256, + MinimalEthSpec, ProposerPreparationData, Slot, }; type E = MainnetEthSpec; @@ -181,8 +181,6 @@ pub struct ReOrgTest { parent_distance: u64, /// Number of slots between head block and block proposal slot. head_distance: u64, - re_org_threshold: u64, - max_epochs_since_finalization: u64, percent_parent_votes: usize, percent_empty_votes: usize, percent_head_votes: usize, @@ -201,8 +199,6 @@ impl Default for ReOrgTest { head_slot: Slot::new(E::slots_per_epoch() - 2), parent_distance: 1, head_distance: 1, - re_org_threshold: 20, - max_epochs_since_finalization: 2, percent_parent_votes: 100, percent_empty_votes: 100, percent_head_votes: 0, @@ -388,8 +384,6 @@ pub async fn proposer_boost_re_org_test( head_slot, parent_distance, head_distance, - re_org_threshold, - max_epochs_since_finalization, percent_parent_votes, percent_empty_votes, percent_head_votes, @@ -403,8 +397,7 @@ pub async fn proposer_boost_re_org_test( // TODO(EIP-7732): extend test for Gloas — `get_validator_blocks_v3` is missing the // `Eth-Execution-Payload-Blinded` header for Gloas block production responses. - let mut spec = ForkName::Fulu.make_genesis_spec(E::default_spec()); - spec.terminal_total_difficulty = Uint256::from(1); + let spec = ForkName::Fulu.make_genesis_spec(E::default_spec()); // Ensure there are enough validators to have `attesters_per_slot`. let attesters_per_slot = 10; @@ -427,14 +420,9 @@ pub async fn proposer_boost_re_org_test( validator_count, None, Some(Box::new(move |builder| { - builder - .proposer_re_org_head_threshold(Some(ReOrgThreshold(re_org_threshold))) - .proposer_re_org_max_epochs_since_finalization(Epoch::new( - max_epochs_since_finalization, - )) - .proposer_re_org_disallowed_offsets( - DisallowedReOrgOffsets::new::(disallowed_offsets).unwrap(), - ) + builder.proposer_re_org_disallowed_offsets( + DisallowedReOrgOffsets::new::(disallowed_offsets).unwrap(), + ) })), Default::default(), false, diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index a7fe34593a..06b3a6197b 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -2929,7 +2929,7 @@ impl ApiTester { proposal_slot, validator_index: validator_index as u64, fee_recipient: Address::repeat_byte(0xaa), - gas_limit: 30_000_000, + target_gas_limit: 30_000_000, }; let epoch = proposal_slot.epoch(E::slots_per_epoch()); @@ -9167,11 +9167,17 @@ async fn builder_works_post_deneb() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn get_blob_sidecars() { - let mut config = ApiTesterConfig::default(); + let mut config = ApiTesterConfig { + retain_historic_states: false, + spec: E::default_spec(), + node_custody_type: NodeCustodyType::Supernode, + }; config.spec.altair_fork_epoch = Some(Epoch::new(0)); config.spec.bellatrix_fork_epoch = Some(Epoch::new(0)); config.spec.capella_fork_epoch = Some(Epoch::new(0)); config.spec.deneb_fork_epoch = Some(Epoch::new(0)); + config.spec.electra_fork_epoch = Some(Epoch::new(0)); + config.spec.fulu_fork_epoch = Some(Epoch::new(0)); ApiTester::new_from_config(config) .await diff --git a/beacon_node/lighthouse_network/src/service/gossip_cache.rs b/beacon_node/lighthouse_network/src/service/gossip_cache.rs index e9862e3f74..4b96fe884e 100644 --- a/beacon_node/lighthouse_network/src/service/gossip_cache.rs +++ b/beacon_node/lighthouse_network/src/service/gossip_cache.rs @@ -20,8 +20,6 @@ pub struct GossipCache { topic_msgs: HashMap, Key>>, /// Timeout for blocks. beacon_block: Option, - /// Timeout for blobs. - blob_sidecar: Option, /// Timeout for data columns. data_column_sidecar: Option, /// Timeout for aggregate attestations. @@ -59,8 +57,6 @@ pub struct GossipCacheBuilder { default_timeout: Option, /// Timeout for blocks. beacon_block: Option, - /// Timeout for blob sidecars. - blob_sidecar: Option, /// Timeout for data column sidecars. data_column_sidecar: Option, /// Timeout for aggregate attestations. @@ -195,7 +191,6 @@ impl GossipCacheBuilder { let GossipCacheBuilder { default_timeout, beacon_block, - blob_sidecar, data_column_sidecar, aggregates, attestation, @@ -216,7 +211,6 @@ impl GossipCacheBuilder { expirations: DelayQueue::default(), topic_msgs: HashMap::default(), beacon_block: beacon_block.or(default_timeout), - blob_sidecar: blob_sidecar.or(default_timeout), data_column_sidecar: data_column_sidecar.or(default_timeout), aggregates: aggregates.or(default_timeout), attestation: attestation.or(default_timeout), @@ -247,7 +241,6 @@ impl GossipCache { pub fn insert(&mut self, topic: GossipTopic, data: Vec) { let expire_timeout = match topic.kind() { GossipKind::BeaconBlock => self.beacon_block, - GossipKind::BlobSidecar(_) => self.blob_sidecar, GossipKind::DataColumnSidecar(_) => self.data_column_sidecar, GossipKind::BeaconAggregateAndProof => self.aggregates, GossipKind::Attestation(_) => self.attestation, diff --git a/beacon_node/lighthouse_network/src/service/utils.rs b/beacon_node/lighthouse_network/src/service/utils.rs index d235e4b28f..c7dabcb391 100644 --- a/beacon_node/lighthouse_network/src/service/utils.rs +++ b/beacon_node/lighthouse_network/src/service/utils.rs @@ -284,10 +284,6 @@ pub(crate) fn create_whitelist_filter( for id in 0..sync_committee_subnet_count { add(SyncCommitteeMessage(SyncSubnetId::new(id))); } - let blob_subnet_count = spec.blob_sidecar_subnet_count_max(); - for id in 0..blob_subnet_count { - add(BlobSidecar(id)); - } for id in 0..spec.data_column_sidecar_subnet_count { add(DataColumnSidecar(DataColumnSubnetId::new(id))); } diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index e5a703ff1e..043d1cfb88 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -7,10 +7,10 @@ use ssz::{Decode, Encode}; use std::io::{Error, ErrorKind}; use std::sync::Arc; use types::{ - AttesterSlashing, AttesterSlashingBase, AttesterSlashingElectra, BlobSidecar, - DataColumnSidecar, DataColumnSubnetId, EthSpec, ForkContext, ForkName, Hash256, - LightClientFinalityUpdate, LightClientOptimisticUpdate, PartialDataColumn, - PartialDataColumnSidecar, PayloadAttestationMessage, ProposerSlashing, SignedAggregateAndProof, + AttesterSlashing, AttesterSlashingBase, AttesterSlashingElectra, DataColumnSidecar, + DataColumnSubnetId, EthSpec, ForkContext, ForkName, Hash256, LightClientFinalityUpdate, + LightClientOptimisticUpdate, PartialDataColumn, PartialDataColumnSidecar, + PayloadAttestationMessage, ProposerSlashing, SignedAggregateAndProof, SignedAggregateAndProofBase, SignedAggregateAndProofElectra, SignedBeaconBlock, SignedBeaconBlockAltair, SignedBeaconBlockBase, SignedBeaconBlockBellatrix, SignedBeaconBlockCapella, SignedBeaconBlockDeneb, SignedBeaconBlockElectra, @@ -24,8 +24,6 @@ use types::{ pub enum PubsubMessage { /// Gossipsub message providing notification of a new block. BeaconBlock(Arc>), - /// Gossipsub message providing notification of a [`BlobSidecar`] along with the subnet id where it was received. - BlobSidecar(Box<(u64, Arc>)>), /// Gossipsub message providing notification of a [`DataColumnSidecar`] along with the subnet id where it was received. DataColumnSidecar(Box<(DataColumnSubnetId, Arc>)>), /// Gossipsub message providing notification of a Aggregate attestation and associated proof. @@ -139,9 +137,6 @@ impl PubsubMessage { pub fn kind(&self) -> GossipKind { match self { PubsubMessage::BeaconBlock(_) => GossipKind::BeaconBlock, - PubsubMessage::BlobSidecar(blob_sidecar_data) => { - GossipKind::BlobSidecar(blob_sidecar_data.0) - } PubsubMessage::DataColumnSidecar(column_sidecar_data) => { GossipKind::DataColumnSidecar(column_sidecar_data.0) } @@ -266,26 +261,6 @@ impl PubsubMessage { }; Ok(PubsubMessage::BeaconBlock(Arc::new(beacon_block))) } - GossipKind::BlobSidecar(blob_index) => { - if let Some(fork_name) = - fork_context.get_fork_from_context_bytes(gossip_topic.fork_digest) - && fork_name.deneb_enabled() - { - let blob_sidecar = Arc::new( - BlobSidecar::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?, - ); - return Ok(PubsubMessage::BlobSidecar(Box::new(( - *blob_index, - blob_sidecar, - )))); - } - - Err(format!( - "beacon_blobs_and_sidecar topic invalid for given fork digest {:?}", - gossip_topic.fork_digest - )) - } GossipKind::DataColumnSidecar(subnet_id) => { match fork_context.get_fork_from_context_bytes(gossip_topic.fork_digest) { Some(fork) if fork.fulu_enabled() => { @@ -444,7 +419,6 @@ impl PubsubMessage { // messages for us. match &self { PubsubMessage::BeaconBlock(data) => data.as_ssz_bytes(), - PubsubMessage::BlobSidecar(data) => data.1.as_ssz_bytes(), PubsubMessage::DataColumnSidecar(data) => data.1.as_ssz_bytes(), PubsubMessage::AggregateAndProofAttestation(data) => data.as_ssz_bytes(), PubsubMessage::VoluntaryExit(data) => data.as_ssz_bytes(), @@ -502,12 +476,6 @@ impl std::fmt::Display for PubsubMessage { block.slot(), block.message().proposer_index() ), - PubsubMessage::BlobSidecar(data) => write!( - f, - "BlobSidecar: slot: {}, blob index: {}", - data.1.slot(), - data.1.index, - ), PubsubMessage::DataColumnSidecar(data) => write!( f, "DataColumnSidecar: slot: {}, column index: {}", diff --git a/beacon_node/lighthouse_network/src/types/topics.rs b/beacon_node/lighthouse_network/src/types/topics.rs index b51c459a80..1a5acd79b4 100644 --- a/beacon_node/lighthouse_network/src/types/topics.rs +++ b/beacon_node/lighthouse_network/src/types/topics.rs @@ -21,7 +21,6 @@ pub const SSZ_SNAPPY_ENCODING_POSTFIX: &str = "ssz_snappy"; pub const BEACON_BLOCK_TOPIC: &str = "beacon_block"; pub const BEACON_AGGREGATE_AND_PROOF_TOPIC: &str = "beacon_aggregate_and_proof"; pub const BEACON_ATTESTATION_PREFIX: &str = "beacon_attestation_"; -pub const BLOB_SIDECAR_PREFIX: &str = "blob_sidecar_"; pub const DATA_COLUMN_SIDECAR_PREFIX: &str = "data_column_sidecar_"; pub const VOLUNTARY_EXIT_TOPIC: &str = "voluntary_exit"; pub const PROPOSER_SLASHING_TOPIC: &str = "proposer_slashing"; @@ -82,13 +81,6 @@ pub fn core_topics_to_subscribe( topics.push(GossipKind::BlsToExecutionChange); } - if fork_name.deneb_enabled() && !fork_name.fulu_enabled() { - // All of deneb blob topics are core topics - for i in 0..spec.blob_sidecar_subnet_count(fork_name) { - topics.push(GossipKind::BlobSidecar(i)); - } - } - if fork_name.fulu_enabled() { for subnet in &opts.sampling_subnets { topics.push(GossipKind::DataColumnSidecar(*subnet)); @@ -118,7 +110,6 @@ pub fn is_fork_non_core_topic(topic: &GossipTopic, _fork_name: ForkName) -> bool // All these topics are core-only GossipKind::BeaconBlock | GossipKind::BeaconAggregateAndProof - | GossipKind::BlobSidecar(_) | GossipKind::DataColumnSidecar(_) | GossipKind::VoluntaryExit | GossipKind::ProposerSlashing @@ -166,8 +157,6 @@ pub enum GossipKind { BeaconBlock, /// Topic for publishing aggregate attestations and proofs. BeaconAggregateAndProof, - /// Topic for publishing BlobSidecars. - BlobSidecar(u64), /// Topic for publishing DataColumnSidecars. DataColumnSidecar(DataColumnSubnetId), /// Topic for publishing raw attestations on a particular subnet. @@ -216,9 +205,6 @@ impl std::fmt::Display for GossipKind { GossipKind::SyncCommitteeMessage(subnet_id) => { write!(f, "sync_committee_{}", **subnet_id) } - GossipKind::BlobSidecar(blob_index) => { - write!(f, "{}{}", BLOB_SIDECAR_PREFIX, blob_index) - } GossipKind::DataColumnSidecar(column_subnet_id) => { write!(f, "{}{}", DATA_COLUMN_SIDECAR_PREFIX, **column_subnet_id) } @@ -349,9 +335,6 @@ impl std::fmt::Display for GossipTopic { GossipKind::SyncCommitteeMessage(index) => { format!("{}{}", SYNC_COMMITTEE_PREFIX_TOPIC, *index) } - GossipKind::BlobSidecar(blob_index) => { - format!("{}{}", BLOB_SIDECAR_PREFIX, blob_index) - } GossipKind::DataColumnSidecar(column_subnet_id) => { format!("{}{}", DATA_COLUMN_SIDECAR_PREFIX, *column_subnet_id) } @@ -401,8 +384,6 @@ fn subnet_topic_index(topic: &str) -> Option { return Some(GossipKind::SyncCommitteeMessage(SyncSubnetId::new( index.parse::().ok()?, ))); - } else if let Some(index) = topic.strip_prefix(BLOB_SIDECAR_PREFIX) { - return Some(GossipKind::BlobSidecar(index.parse::().ok()?)); } else if let Some(index) = topic.strip_prefix(DATA_COLUMN_SIDECAR_PREFIX) { return Some(GossipKind::DataColumnSidecar(DataColumnSubnetId::new( index.parse::().ok()?, @@ -576,17 +557,6 @@ mod tests { } } - #[test] - fn blobs_are_not_subscribed_in_peerdas() { - let spec = get_spec(); - let s = get_sampling_subnets(); - let topic_config = get_topic_config(&s); - assert!( - !core_topics_to_subscribe::(ForkName::Fulu, &topic_config, &spec,) - .contains(&GossipKind::BlobSidecar(0)) - ); - } - #[test] fn columns_are_subscribed_in_peerdas() { let spec = get_spec(); diff --git a/beacon_node/network/src/lib.rs b/beacon_node/network/src/lib.rs index 2a7fedb53e..dc45f53c70 100644 --- a/beacon_node/network/src/lib.rs +++ b/beacon_node/network/src/lib.rs @@ -11,6 +11,7 @@ mod subnet_service; mod sync; pub use lighthouse_network::NetworkConfig; +pub use network_beacon_processor::NetworkBeaconProcessor; pub use service::{ NetworkMessage, NetworkReceivers, NetworkSenders, NetworkService, ValidatorSubscriptionMessage, }; diff --git a/beacon_node/network/src/metrics.rs b/beacon_node/network/src/metrics.rs index 4b34d7bfc0..c043133cee 100644 --- a/beacon_node/network/src/metrics.rs +++ b/beacon_node/network/src/metrics.rs @@ -128,13 +128,6 @@ pub static BEACON_PROCESSOR_GOSSIP_BLOCK_EARLY_SECONDS: LazyLock> = - LazyLock::new(|| { - try_create_int_counter( - "beacon_processor_gossip_blob_verified_total", - "Total number of gossip blob verified for propagation.", - ) - }); pub static BEACON_PROCESSOR_GOSSIP_DATA_COLUMN_SIDECAR_VERIFIED_TOTAL: LazyLock< Result, > = LazyLock::new(|| { @@ -600,12 +593,6 @@ pub static BEACON_BLOCK_DELAY_GOSSIP_ARRIVED_LATE_TOTAL: LazyLock> = LazyLock::new(|| { - try_create_int_gauge( - "beacon_blob_delay_gossip_last_delay", - "The first time we see this blob as a delay from the start of the slot", - ) -}); pub static BEACON_DATA_COLUMN_GOSSIP_PROPAGATION_VERIFICATION_DELAY_TIME: LazyLock< Result, @@ -664,14 +651,6 @@ pub static BEACON_USEFUL_FULL_COLUMNS_RECEIVED_TOTAL: LazyLock> = LazyLock::new( - || { - try_create_int_gauge( - "beacon_blob_delay_gossip_verification", - "Keeps track of the time delay from the start of the slot to the point we propagate the blob", - ) - }, -); pub static BEACON_BLOB_DELAY_FULL_VERIFICATION: LazyLock> = LazyLock::new(|| { try_create_int_gauge( "beacon_blob_last_full_verification_delay", @@ -695,15 +674,6 @@ pub static BEACON_BLOB_RPC_SLOT_START_DELAY_TIME: LazyLock> = }, ); -pub static BEACON_BLOB_GOSSIP_ARRIVED_LATE_TOTAL: LazyLock> = LazyLock::new( - || { - try_create_int_counter( - "beacon_blob_gossip_arrived_late_total", - "Count of times when a gossip blob arrived from the network later than the attestation deadline.", - ) - }, -); - /* * Light client update reprocessing queue metrics. */ 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 7a902649cb..65c95eff35 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -11,6 +11,9 @@ use beacon_chain::data_column_verification::{ PartialColumnVerificationResult, }; use beacon_chain::payload_bid_verification::PayloadBidError; +use beacon_chain::payload_envelope_verification::{ + EnvelopeError, gossip_verified_envelope::GossipVerifiedEnvelope, +}; use beacon_chain::proposer_preferences_verification::ProposerPreferencesError; use beacon_chain::store::Error; use beacon_chain::{ @@ -27,12 +30,6 @@ use beacon_chain::{ sync_committee_verification::{self, Error as SyncCommitteeError}, validator_monitor::{get_block_delay_ms, get_slot_delay_ms}, }; -use beacon_chain::{ - blob_verification::{GossipBlobError, GossipVerifiedBlob}, - payload_envelope_verification::{ - EnvelopeError, gossip_verified_envelope::GossipVerifiedEnvelope, - }, -}; use beacon_processor::{Work, WorkEvent}; use lighthouse_network::{ Client, GossipTopic, MessageAcceptance, MessageId, PeerAction, PeerId, PubsubMessage, @@ -50,13 +47,13 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use store::hot_cold_store::HotColdDBError; use tracing::{Instrument, Span, debug, error, info, instrument, trace, warn}; use types::{ - Attestation, AttestationData, AttestationRef, AttesterSlashing, BlobSidecar, ColumnIndex, - DataColumnSidecar, DataColumnSubnetId, EthSpec, Hash256, IndexedAttestation, - LightClientFinalityUpdate, LightClientOptimisticUpdate, PartialDataColumn, - PartialDataColumnHeader, PayloadAttestationMessage, ProposerSlashing, SignedAggregateAndProof, - SignedBeaconBlock, SignedBlsToExecutionChange, SignedContributionAndProof, - SignedExecutionPayloadBid, SignedExecutionPayloadEnvelope, SignedProposerPreferences, - SignedVoluntaryExit, SingleAttestation, Slot, SubnetId, SyncCommitteeMessage, SyncSubnetId, + Attestation, AttestationData, AttestationRef, AttesterSlashing, ColumnIndex, DataColumnSidecar, + DataColumnSubnetId, EthSpec, Hash256, IndexedAttestation, LightClientFinalityUpdate, + LightClientOptimisticUpdate, PartialDataColumn, PartialDataColumnHeader, + PayloadAttestationMessage, ProposerSlashing, SignedAggregateAndProof, SignedBeaconBlock, + SignedBlsToExecutionChange, SignedContributionAndProof, SignedExecutionPayloadBid, + SignedExecutionPayloadEnvelope, SignedProposerPreferences, SignedVoluntaryExit, + SingleAttestation, Slot, SubnetId, SyncCommitteeMessage, SyncSubnetId, block::BlockImportSource, }; @@ -174,6 +171,17 @@ impl FailedAtt { } } +/// `MessageAcceptance` doesn't implement clone so we do a manual match here. +/// TODO: remove this once `Clone` is available on this type: +/// https://github.com/libp2p/rust-libp2p/pull/6445 +fn clone_message_acceptance(a: &MessageAcceptance) -> MessageAcceptance { + match a { + MessageAcceptance::Accept => MessageAcceptance::Accept, + MessageAcceptance::Reject => MessageAcceptance::Reject, + MessageAcceptance::Ignore => MessageAcceptance::Ignore, + } +} + impl NetworkBeaconProcessor { /* Auxiliary functions */ @@ -833,13 +841,109 @@ impl NetworkBeaconProcessor { } } - #[instrument( - name = "lh_process_gossip_partial_data_column", - parent = None, - level = "debug", - skip_all, - fields(block_root = ?column.block_root, index = column.index), - )] + async fn process_gossip_verified_data_column( + self: &Arc, + peer_id: PeerId, + verified_data_column: GossipVerifiedDataColumn, + // This value is not used presently, but it might come in handy for debugging. + _seen_duration: Duration, + ) { + let processing_start_time = Instant::now(); + let block_root = verified_data_column.block_root(); + let data_column_slot = verified_data_column.slot(); + let data_column_index = verified_data_column.index(); + + // TODO(gloas): implement partial messages + if let DataColumnSidecar::Fulu(col) = verified_data_column.as_data_column() + && self + .chain + .data_availability_checker + .partial_assembler() + .is_some_and(|a| !a.is_complete(block_root, verified_data_column.index())) + { + metrics::inc_counter_vec( + &metrics::BEACON_USEFUL_FULL_COLUMNS_RECEIVED_TOTAL, + &[&data_column_index.to_string()], + ); + + match col.to_partial() { + Ok(mut column) => { + let header = column.sidecar.header.take(); + if let Some(header) = header { + self.send_network_message(NetworkMessage::PublishPartialColumns { + columns: vec![Arc::new(column)], + header: Arc::new(header), + }); + } else { + crit!("Converting from full to partial yielded headerless partial") + }; + } + Err(err) => crit!(?err, "Could not convert from full to partial"), + } + } + + let result = self + .chain + .process_gossip_data_columns(vec![verified_data_column], || Ok(())) + .await; + register_process_result_metrics(&result, metrics::BlockSource::Gossip, "data_column"); + + match result { + Ok(availability) => match availability { + AvailabilityProcessingStatus::Imported(block_root) => { + debug!( + %block_root, + "Gossipsub data column processed, imported fully available block" + ); + self.chain.recompute_head_at_current_slot().await; + + metrics::set_gauge( + &metrics::BEACON_BLOB_DELAY_FULL_VERIFICATION, + processing_start_time.elapsed().as_millis() as i64, + ); + + // If a block is in the da_checker, sync maybe awaiting for an event when block is finally + // imported. A block can become imported both after processing a block or data column. If + // importing a block results in `Imported`, notify. Do not notify of data column errors. + self.send_sync_message(SyncMessage::GossipBlockProcessResult { + block_root, + imported: true, + }); + } + AvailabilityProcessingStatus::MissingComponents(slot, block_root) => { + trace!( + %slot, + %data_column_index, + %block_root, + "Processed data column, waiting for other components" + ); + + self.check_reconstruction_trigger(slot, &block_root).await; + } + }, + Err(BlockError::DuplicateFullyImported(_)) => { + debug!( + ?block_root, + data_column_index, "Ignoring gossip column already imported" + ); + } + Err(err) => { + debug!( + outcome = ?err, + ?block_root, + block_slot = %data_column_slot, + data_column_index, + "Invalid gossip data column" + ); + self.gossip_penalize_peer( + peer_id, + PeerAction::MidToleranceError, + "bad_gossip_data_column_ssz", + ); + } + } + } + pub async fn process_gossip_partial_data_column_sidecar( self: &Arc, peer_id: PeerId, @@ -997,7 +1101,6 @@ impl NetworkBeaconProcessor { %index, "Could not verify partial column for gossip. Rejecting the column sidecar" ); - // Prevent recurring behaviour by penalizing the peer slightly. self.gossip_penalize_peer( peer_id, PeerAction::LowToleranceError, @@ -1006,9 +1109,6 @@ impl NetworkBeaconProcessor { self.propagate_partial_validation_failure(peer_id, topic); } GossipDataColumnError::PriorKnown { .. } => { - // Data column is available via either the EL or reconstruction. - // Do not penalise the peer. - // Gossip filter should filter any duplicates received after this. debug!( %block_root, %index, @@ -1023,7 +1123,6 @@ impl NetworkBeaconProcessor { %index, "Could not verify column sidecar for gossip. Ignoring the partial column sidecar" ); - // Prevent recurring behaviour by penalizing the peer slightly. self.gossip_penalize_peer( peer_id, PeerAction::HighToleranceError, @@ -1108,357 +1207,6 @@ impl NetworkBeaconProcessor { } } - #[allow(clippy::too_many_arguments)] - #[instrument( - name = "lh_process_gossip_blob", - parent = None, - level = "debug", - skip_all, - fields( - slot = ?blob_sidecar.slot(), - block_root = ?blob_sidecar.block_root(), - index = blob_sidecar.index), - )] - pub async fn process_gossip_blob( - self: &Arc, - message_id: MessageId, - peer_id: PeerId, - _peer_client: Client, - blob_index: u64, - blob_sidecar: Arc>, - seen_duration: Duration, - ) { - let slot = blob_sidecar.slot(); - let root = blob_sidecar.block_root(); - let index = blob_sidecar.index; - let commitment = blob_sidecar.kzg_commitment; - let delay = get_slot_delay_ms(seen_duration, slot, &self.chain.slot_clock); - // Log metrics to track delay from other nodes on the network. - metrics::set_gauge(&metrics::BEACON_BLOB_DELAY_GOSSIP, delay.as_millis() as i64); - match self - .chain - .verify_blob_sidecar_for_gossip(blob_sidecar.clone(), blob_index) - { - Ok(gossip_verified_blob) => { - metrics::inc_counter(&metrics::BEACON_PROCESSOR_GOSSIP_BLOB_VERIFIED_TOTAL); - - if delay >= self.chain.spec.get_unaggregated_attestation_due() { - metrics::inc_counter(&metrics::BEACON_BLOB_GOSSIP_ARRIVED_LATE_TOTAL); - debug!( - block_root = ?gossip_verified_blob.block_root(), - proposer_index = gossip_verified_blob.block_proposer_index(), - slot = %gossip_verified_blob.slot(), - delay = ?delay, - commitment = %gossip_verified_blob.kzg_commitment(), - "Gossip blob arrived late" - ); - } - - debug!( - %slot, - %root, - %index, - commitment = %gossip_verified_blob.kzg_commitment(), - "Successfully verified gossip blob" - ); - - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept); - - // Log metrics to keep track of propagation delay times. - if let Some(duration) = SystemTime::now() - .duration_since(UNIX_EPOCH) - .ok() - .and_then(|now| now.checked_sub(seen_duration)) - { - metrics::set_gauge( - &metrics::BEACON_BLOB_DELAY_GOSSIP_VERIFICATION, - duration.as_millis() as i64, - ); - } - self.process_gossip_verified_blob(peer_id, gossip_verified_blob, seen_duration) - .await - } - Err(err) => { - match err { - GossipBlobError::ParentUnknown { parent_root } => { - debug!( - action = "requesting parent", - block_root = %root, - parent_root = %parent_root, - %commitment, - "Unknown parent hash for blob" - ); - self.send_sync_message(SyncMessage::UnknownParentBlob( - peer_id, - blob_sidecar, - )); - } - GossipBlobError::PubkeyCacheTimeout | GossipBlobError::BeaconChainError(_) => { - crit!( - error = ?err, - "Internal error when verifying blob sidecar" - ) - } - GossipBlobError::ProposalSignatureInvalid - | GossipBlobError::UnknownValidator(_) - | GossipBlobError::ProposerIndexMismatch { .. } - | GossipBlobError::BlobIsNotLaterThanParent { .. } - | GossipBlobError::InvalidSubnet { .. } - | GossipBlobError::InvalidInclusionProof - | GossipBlobError::KzgError(_) - | GossipBlobError::NotFinalizedDescendant { .. } => { - warn!( - error = ?err, - %slot, - %root, - %index, - %commitment, - "Could not verify blob sidecar for gossip. Rejecting the blob sidecar" - ); - // Prevent recurring behaviour by penalizing the peer. - self.gossip_penalize_peer( - peer_id, - PeerAction::LowToleranceError, - "gossip_blob_low", - ); - self.propagate_validation_result( - message_id, - peer_id, - MessageAcceptance::Reject, - ); - } - GossipBlobError::RepeatBlob { .. } => { - // We may have received the blob from the EL. Do not penalise the peer. - // Gossip filter should filter any duplicates received after this. - debug!( - %slot, - %root, - %index, - "Received already available blob sidecar. Ignoring the blob sidecar" - ) - } - GossipBlobError::FutureSlot { .. } => { - debug!( - error = ?err, - %slot, - %root, - %index, - %commitment, - "Could not verify blob sidecar for gossip. Ignoring the blob sidecar" - ); - // Prevent recurring behaviour by penalizing the peer slightly. - self.gossip_penalize_peer( - peer_id, - PeerAction::HighToleranceError, - "gossip_blob_high", - ); - self.propagate_validation_result( - message_id, - peer_id, - MessageAcceptance::Ignore, - ); - } - GossipBlobError::PastFinalizedSlot { .. } => { - debug!( - error = ?err, - %slot, - %root, - %index, - %commitment, - "Could not verify blob sidecar for gossip. Ignoring the blob sidecar" - ); - // Prevent recurring behaviour by penalizing the peer. A low-tolerance - // error is fine because there's no reason for peers to be propagating old - // blobs on gossip, even if their view of finality is lagging. - self.gossip_penalize_peer( - peer_id, - PeerAction::LowToleranceError, - "gossip_blob_low", - ); - self.propagate_validation_result( - message_id, - peer_id, - MessageAcceptance::Ignore, - ); - } - } - } - } - } - - async fn process_gossip_verified_blob( - self: &Arc, - peer_id: PeerId, - verified_blob: GossipVerifiedBlob, - _seen_duration: Duration, - ) { - let processing_start_time = Instant::now(); - let block_root = verified_blob.block_root(); - let blob_slot = verified_blob.slot(); - let blob_index = verified_blob.id().index; - - let result = self.chain.process_gossip_blob(verified_blob).await; - register_process_result_metrics(&result, metrics::BlockSource::Gossip, "blob"); - - match &result { - Ok(AvailabilityProcessingStatus::Imported(block_root)) => { - debug!( - %block_root, - "Gossipsub blob processed - imported fully available block" - ); - self.chain.recompute_head_at_current_slot().await; - - metrics::set_gauge( - &metrics::BEACON_BLOB_DELAY_FULL_VERIFICATION, - processing_start_time.elapsed().as_millis() as i64, - ); - } - Ok(AvailabilityProcessingStatus::MissingComponents(slot, block_root)) => { - debug!( - %slot, - %blob_index, - %block_root, - "Processed gossip blob - waiting for other components" - ); - } - Err(BlockError::DuplicateFullyImported(_)) => { - debug!( - ?block_root, - blob_index, "Ignoring gossip blob already imported" - ); - } - Err(err) => { - debug!( - outcome = ?err, - ?block_root, - %blob_slot, - blob_index, - "Invalid gossip blob" - ); - self.gossip_penalize_peer( - peer_id, - PeerAction::MidToleranceError, - "bad_gossip_blob_ssz", - ); - } - } - - // If a block is in the da_checker, sync maybe awaiting for an event when block is finally - // imported. A block can become imported both after processing a block or blob. If a - // importing a block results in `Imported`, notify. Do not notify of blob errors. - if matches!(result, Ok(AvailabilityProcessingStatus::Imported(_))) { - self.send_sync_message(SyncMessage::GossipBlockProcessResult { - block_root, - imported: true, - }); - } - } - - /// Process a gossip-verified full data column (not partial). - /// Partials are handled by process_gossip_verified_partial_data_column. - async fn process_gossip_verified_data_column( - self: &Arc, - peer_id: PeerId, - verified_data_column: GossipVerifiedDataColumn, - // This value is not used presently, but it might come in handy for debugging. - _seen_duration: Duration, - ) { - let processing_start_time = Instant::now(); - let block_root = verified_data_column.block_root(); - let data_column_slot = verified_data_column.slot(); - let data_column_index = verified_data_column.index(); - - // TODO(gloas): implement partial messages - if let DataColumnSidecar::Fulu(col) = verified_data_column.as_data_column() - && self - .chain - .data_availability_checker - .partial_assembler() - .is_some_and(|a| !a.is_complete(block_root, verified_data_column.index())) - { - metrics::inc_counter_vec( - &metrics::BEACON_USEFUL_FULL_COLUMNS_RECEIVED_TOTAL, - &[&data_column_index.to_string()], - ); - - match col.to_partial() { - Ok(mut column) => { - let header = column.sidecar.header.take(); - if let Some(header) = header { - self.send_network_message(NetworkMessage::PublishPartialColumns { - columns: vec![Arc::new(column)], - header: Arc::new(header), - }); - } else { - crit!("Converting from full to partial yielded headerless partial") - }; - } - Err(err) => crit!(?err, "Could not convert from full to partial"), - } - } - - let result = self - .chain - .process_gossip_data_columns(vec![verified_data_column], || Ok(())) - .await; - register_process_result_metrics(&result, metrics::BlockSource::Gossip, "data_column"); - - match result { - Ok(availability) => match availability { - AvailabilityProcessingStatus::Imported(block_root) => { - debug!( - %block_root, - "Gossipsub data column processed, imported fully available block" - ); - self.chain.recompute_head_at_current_slot().await; - - metrics::set_gauge( - &metrics::BEACON_BLOB_DELAY_FULL_VERIFICATION, - processing_start_time.elapsed().as_millis() as i64, - ); - - // If a block is in the da_checker, sync maybe awaiting for an event when block is finally - // imported. A block can become imported both after processing a block or data column. If - // importing a block results in `Imported`, notify. Do not notify of data column errors. - self.send_sync_message(SyncMessage::GossipBlockProcessResult { - block_root, - imported: true, - }); - } - AvailabilityProcessingStatus::MissingComponents(slot, block_root) => { - trace!( - %slot, - %data_column_index, - %block_root, - "Processed data column, waiting for other components" - ); - - self.check_reconstruction_trigger(slot, &block_root).await; - } - }, - Err(BlockError::DuplicateFullyImported(_)) => { - debug!( - ?block_root, - data_column_index, "Ignoring gossip column already imported" - ); - } - Err(err) => { - debug!( - outcome = ?err, - ?block_root, - block_slot = %data_column_slot, - data_column_index, - "Invalid gossip data column" - ); - self.gossip_penalize_peer( - peer_id, - PeerAction::MidToleranceError, - "bad_gossip_data_column_ssz", - ); - } - } - } - /// Process a gossip-verified partial data column by merging it in the assembler async fn process_gossip_verified_partial_data_column( self: &Arc, @@ -1874,9 +1622,7 @@ impl NetworkBeaconProcessor { crit!(error = %e, "Internal block gossip validation error. Availability check during gossip validation"); return None; } - // BlobNotRequired is unreachable. Only constructed in `process_gossip_blob` Err(e @ BlockError::InternalError(_)) - | Err(e @ BlockError::BlobNotRequired(_)) | Err(e @ BlockError::EnvelopeBlockRootUnknown(_)) | Err(e @ BlockError::OptimisticSyncNotSupported { .. }) => { error!(error = %e, "Internal block gossip validation error"); @@ -2194,14 +1940,14 @@ impl NetworkBeaconProcessor { message_id: MessageId, peer_id: PeerId, proposer_slashing: ProposerSlashing, - ) { + ) -> MessageAcceptance { let validator_index = proposer_slashing.signed_header_1.message.proposer_index; - let slashing = match self + let (validation_result, verified_slashing_opt) = match self .chain .verify_proposer_slashing_for_gossip(proposer_slashing) { - Ok(ObservationOutcome::New(slashing)) => slashing, + Ok(ObservationOutcome::New(slashing)) => (MessageAcceptance::Accept, Some(slashing)), Ok(ObservationOutcome::AlreadyKnown) => { debug!( reason = "Already seen a proposer slashing for that validator", @@ -2209,44 +1955,54 @@ impl NetworkBeaconProcessor { peer = %peer_id, "Dropping proposer slashing" ); - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); - return; + (MessageAcceptance::Ignore, None) } Err(e) => { - // This is likely a fault with the beacon chain and not necessarily a - // malicious message from the peer. debug!( validator_index, %peer_id, error = ?e, - "Dropping invalid proposer slashing" + "Dropping proposer slashing due to an error" ); - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); - // Penalize peer slightly for invalids. - self.gossip_penalize_peer( - peer_id, - PeerAction::HighToleranceError, - "invalid_gossip_proposer_slashing", - ); - return; + if matches!(e, BeaconChainError::ProposerSlashingValidationError(_)) { + // Penalize peer slightly for invalids. + self.gossip_penalize_peer( + peer_id, + PeerAction::HighToleranceError, + "invalid_gossip_proposer_slashing", + ); + (MessageAcceptance::Reject, None) + } else { + // This is likely a fault with the beacon chain and not necessarily a + // malicious message from the peer. + (MessageAcceptance::Ignore, None) + } } }; - metrics::inc_counter(&metrics::BEACON_PROCESSOR_PROPOSER_SLASHING_VERIFIED_TOTAL); + self.propagate_validation_result( + message_id, + peer_id, + clone_message_acceptance(&validation_result), + ); - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept); + if let Some(slashing) = verified_slashing_opt { + metrics::inc_counter(&metrics::BEACON_PROCESSOR_PROPOSER_SLASHING_VERIFIED_TOTAL); - // Register the slashing with any monitored validators. - self.chain - .validator_monitor - .read() - .register_gossip_proposer_slashing(slashing.as_inner()); + // Register the slashing with any monitored validators. + self.chain + .validator_monitor + .read() + .register_gossip_proposer_slashing(slashing.as_inner()); - self.chain.import_proposer_slashing(slashing); - debug!("Successfully imported proposer slashing"); + self.chain.import_proposer_slashing(slashing); + debug!("Successfully imported proposer slashing"); - metrics::inc_counter(&metrics::BEACON_PROCESSOR_PROPOSER_SLASHING_IMPORTED_TOTAL); + metrics::inc_counter(&metrics::BEACON_PROCESSOR_PROPOSER_SLASHING_IMPORTED_TOTAL); + } + + validation_result } pub fn process_gossip_attester_slashing( @@ -2254,51 +2010,64 @@ impl NetworkBeaconProcessor { message_id: MessageId, peer_id: PeerId, attester_slashing: AttesterSlashing, - ) { - let slashing = match self + ) -> MessageAcceptance { + let (validation_result, verified_slashing_opt) = match self .chain .verify_attester_slashing_for_gossip(attester_slashing) { - Ok(ObservationOutcome::New(slashing)) => slashing, + Ok(ObservationOutcome::New(slashing)) => (MessageAcceptance::Accept, Some(slashing)), Ok(ObservationOutcome::AlreadyKnown) => { debug!( reason = "Slashings already known for all slashed validators", peer = %peer_id, "Dropping attester slashing" ); - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); - return; + (MessageAcceptance::Ignore, None) } Err(e) => { debug!( %peer_id, error = ?e, - "Dropping invalid attester slashing" + "Dropping attester slashing due to an error" ); - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); - // Penalize peer slightly for invalids. - self.gossip_penalize_peer( - peer_id, - PeerAction::HighToleranceError, - "invalid_gossip_attester_slashing", - ); - return; + + if matches!(e, BeaconChainError::AttesterSlashingValidationError(_)) { + // Penalize peer slightly for invalids. + self.gossip_penalize_peer( + peer_id, + PeerAction::HighToleranceError, + "invalid_gossip_attester_slashing", + ); + (MessageAcceptance::Reject, None) + } else { + // This is likely a fault with the beacon chain and not necessarily a + // malicious message from the peer. + (MessageAcceptance::Ignore, None) + } } }; - metrics::inc_counter(&metrics::BEACON_PROCESSOR_ATTESTER_SLASHING_VERIFIED_TOTAL); + self.propagate_validation_result( + message_id, + peer_id, + clone_message_acceptance(&validation_result), + ); - self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Accept); + if let Some(slashing) = verified_slashing_opt { + metrics::inc_counter(&metrics::BEACON_PROCESSOR_ATTESTER_SLASHING_VERIFIED_TOTAL); - // Register the slashing with any monitored validators. - self.chain - .validator_monitor - .read() - .register_gossip_attester_slashing(slashing.as_inner().to_ref()); + // Register the slashing with any monitored validators. + self.chain + .validator_monitor + .read() + .register_gossip_attester_slashing(slashing.as_inner().to_ref()); - self.chain.import_attester_slashing(slashing); - debug!("Successfully imported attester slashing"); - metrics::inc_counter(&metrics::BEACON_PROCESSOR_ATTESTER_SLASHING_IMPORTED_TOTAL); + self.chain.import_attester_slashing(slashing); + debug!("Successfully imported attester slashing"); + metrics::inc_counter(&metrics::BEACON_PROCESSOR_ATTESTER_SLASHING_IMPORTED_TOTAL); + } + + validation_result } pub fn process_gossip_bls_to_execution_change( @@ -4058,7 +3827,6 @@ impl NetworkBeaconProcessor { PayloadBidError::BadSignature | PayloadBidError::InvalidBuilder { .. } | PayloadBidError::InvalidFeeRecipient - | PayloadBidError::InvalidGasLimit | PayloadBidError::ExecutionPaymentNonZero { .. } | PayloadBidError::InvalidBlobKzgCommitments { .. }, ) => { @@ -4076,6 +3844,7 @@ impl NetworkBeaconProcessor { | PayloadBidError::ParentBlockRootUnknown { .. } | PayloadBidError::ParentBlockRootNotCanonical { .. } | PayloadBidError::BuilderCantCoverBid { .. } + | PayloadBidError::InvalidGasLimit | PayloadBidError::BeaconStateError(_) | PayloadBidError::InternalError(_) | PayloadBidError::InvalidBidSlot { .. } @@ -4258,8 +4027,7 @@ impl NetworkBeaconProcessor { "payload_attn_invalid_sig", ); } - PayloadAttestationError::BeaconChainError(_) - | PayloadAttestationError::BeaconStateError(_) => { + PayloadAttestationError::BeaconChainError(_) => { debug!( %peer_id, %message_slot, diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index 7817feb0bd..97673aa8b8 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -1,12 +1,12 @@ use crate::sync::manager::BlockProcessType; use crate::{service::NetworkMessage, sync::manager::SyncMessage}; -use beacon_chain::blob_verification::{GossipBlobError, observe_gossip_blob}; use beacon_chain::block_verification_types::LookupBlock; use beacon_chain::block_verification_types::RangeSyncBlock; -use beacon_chain::data_column_verification::{GossipDataColumnError, observe_gossip_data_column}; -use beacon_chain::fetch_blobs::{ - EngineGetBlobsOutput, FetchEngineBlobError, fetch_and_process_engine_blobs, +use beacon_chain::data_column_verification::{ + GossipDataColumnError, KzgVerifiedCustodyDataColumn, observe_gossip_data_column, }; +use beacon_chain::fetch_blobs::{FetchEngineBlobError, fetch_and_process_engine_blobs}; +use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; use beacon_chain::{AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError}; use beacon_processor::{ BeaconProcessorSend, DuplicateCache, GossipAggregatePackage, GossipAttestationPackage, Work, @@ -20,7 +20,7 @@ use lighthouse_network::rpc::methods::{ }; use lighthouse_network::service::api_types::CustodyBackfillBatchId; use lighthouse_network::{ - Client, GossipTopic, MessageId, NetworkGlobals, PeerId, PubsubMessage, + Client, GossipTopic, MessageId, NetworkConfig, NetworkGlobals, PeerId, PubsubMessage, rpc::{BlocksByRangeRequest, BlocksByRootRequest, LightClientBootstrapRequest, StatusMessage}, }; use rand::prelude::SliceRandom; @@ -31,6 +31,10 @@ use task_executor::TaskExecutor; use tokio::sync::mpsc::{self, error::TrySendError}; use tracing::{debug, error, instrument, trace, warn}; use types::*; +use { + beacon_chain::builder::Witness, beacon_processor::BeaconProcessorChannels, + slot_clock::ManualSlotClock, store::MemoryStore, tokio::sync::mpsc::UnboundedSender, +}; pub use sync_methods::ChainSegmentProcessId; use types::data::FixedBlobSidecarList; @@ -65,9 +69,6 @@ pub struct NetworkBeaconProcessor { pub executor: TaskExecutor, } -// Publish blobs in batches of exponentially increasing size. -const BLOB_PUBLICATION_EXP_FACTOR: usize = 2; - impl NetworkBeaconProcessor { fn try_send(&self, event: BeaconWorkEvent) -> Result<(), Error> { self.beacon_processor_send.try_send(event) @@ -193,36 +194,6 @@ impl NetworkBeaconProcessor { }) } - /// Create a new `Work` event for some blob sidecar. - pub fn send_gossip_blob_sidecar( - self: &Arc, - message_id: MessageId, - peer_id: PeerId, - peer_client: Client, - blob_index: u64, - blob_sidecar: Arc>, - seen_timestamp: Duration, - ) -> Result<(), Error> { - let processor = self.clone(); - let process_fn = async move { - processor - .process_gossip_blob( - message_id, - peer_id, - peer_client, - blob_index, - blob_sidecar, - seen_timestamp, - ) - .await - }; - - self.try_send(BeaconWorkEvent { - drop_during_sync: false, - work: Work::GossipBlobSidecar(Box::pin(process_fn)), - }) - } - /// Create a new `Work` event for some data column sidecar. pub fn send_gossip_data_column_sidecar( self: &Arc, @@ -353,7 +324,7 @@ impl NetworkBeaconProcessor { ) -> Result<(), Error> { let processor = self.clone(); let process_fn = move || { - processor.process_gossip_proposer_slashing(message_id, peer_id, *proposer_slashing) + processor.process_gossip_proposer_slashing(message_id, peer_id, *proposer_slashing); }; self.try_send(BeaconWorkEvent { @@ -420,7 +391,7 @@ impl NetworkBeaconProcessor { ) -> Result<(), Error> { let processor = self.clone(); let process_fn = move || { - processor.process_gossip_attester_slashing(message_id, peer_id, *attester_slashing) + processor.process_gossip_attester_slashing(message_id, peer_id, *attester_slashing); }; self.try_send(BeaconWorkEvent { @@ -965,22 +936,12 @@ impl NetworkBeaconProcessor { let epoch = header.slot().epoch(T::EthSpec::slots_per_epoch()); let custody_columns = self.chain.sampling_columns_for_epoch(epoch); let self_cloned = self.clone(); - let publish_fn = move |blobs_or_data_column| { + let publish_fn = move |columns: Vec>| { if publish_blobs { - match blobs_or_data_column { - EngineGetBlobsOutput::Blobs(blobs) => { - self_cloned.publish_blobs_gradually( - blobs.into_iter().map(|b| b.to_blob()).collect(), - block_root, - ); - } - EngineGetBlobsOutput::CustodyColumns(columns) => { - self_cloned.publish_data_columns_gradually( - columns.into_iter().map(|c| c.clone_arc()).collect(), - block_root, - ); - } - }; + self_cloned.publish_data_columns_gradually( + columns.into_iter().map(|c| c.clone_arc()).collect(), + block_root, + ); } }; @@ -1098,84 +1059,6 @@ impl NetworkBeaconProcessor { } } - /// This function gradually publishes blobs to the network in randomised batches. - /// - /// This is an optimisation to reduce outbound bandwidth and ensures each blob is published - /// by some nodes on the network as soon as possible. Our hope is that some blobs arrive from - /// other nodes in the meantime, obviating the need for us to publish them. If no other - /// publisher exists for a blob, it will eventually get published here. - fn publish_blobs_gradually( - self: &Arc, - mut blobs: Vec>>, - block_root: Hash256, - ) { - let self_clone = self.clone(); - - self.executor.spawn( - async move { - let chain = self_clone.chain.clone(); - let publish_fn = |blobs: Vec>>| { - self_clone.send_network_message(NetworkMessage::Publish { - messages: blobs - .into_iter() - .map(|blob| PubsubMessage::BlobSidecar(Box::new((blob.index, blob)))) - .collect(), - }); - }; - - // Permute the blobs and split them into batches. - // The hope is that we won't need to publish some blobs because we will receive them - // on gossip from other nodes. - blobs.shuffle(&mut rand::rng()); - - let blob_publication_batch_interval = chain.config.blob_publication_batch_interval; - let mut publish_count = 0usize; - let blob_count = blobs.len(); - let mut blobs_iter = blobs.into_iter().peekable(); - let mut batch_size = 1usize; - - while blobs_iter.peek().is_some() { - let batch = blobs_iter.by_ref().take(batch_size); - let publishable = batch - .filter_map(|blob| match observe_gossip_blob(&blob, &chain) { - Ok(()) => Some(blob), - Err(GossipBlobError::RepeatBlob { .. }) => None, - Err(e) => { - warn!( - error = ?e, - "Previously verified blob is invalid" - ); - None - } - }) - .collect::>(); - - if !publishable.is_empty() { - debug!( - publish_count = publishable.len(), - ?block_root, - "Publishing blob batch" - ); - publish_count += publishable.len(); - publish_fn(publishable); - } - - tokio::time::sleep(blob_publication_batch_interval).await; - batch_size *= BLOB_PUBLICATION_EXP_FACTOR; - } - - debug!( - batch_interval = blob_publication_batch_interval.as_millis(), - blob_count, - publish_count, - ?block_root, - "Batch blob publication complete" - ) - }, - "gradual_blob_publication", - ); - } - /// This function gradually publishes data columns to the network in randomised batches. /// /// This is an optimisation to reduce outbound bandwidth and ensures each column is published @@ -1260,17 +1143,8 @@ impl NetworkBeaconProcessor { } } -#[cfg(test)] -use { - beacon_chain::builder::Witness, beacon_processor::BeaconProcessorChannels, - slot_clock::ManualSlotClock, store::MemoryStore, tokio::sync::mpsc::UnboundedSender, -}; +pub(crate) type TestBeaconChainType = Witness; -#[cfg(test)] -pub(crate) type TestBeaconChainType = - Witness, MemoryStore>; - -#[cfg(test)] impl NetworkBeaconProcessor> { // Instantiates a mostly non-functional version of `Self` and returns the // event receiver that would normally go to the beacon processor. This is @@ -1302,4 +1176,22 @@ impl NetworkBeaconProcessor> { (network_beacon_processor, beacon_processor_rx) } + + /// Constructs a mostly non-functional `NetworkBeaconProcessor` from a test harness, + /// suitable for directly calling gossip processing methods in tests. + pub fn null_from_harness(harness: &BeaconChainHarness>) -> Self { + let network_globals = NetworkGlobals::new_test_globals( + vec![], + Arc::new(NetworkConfig::default()), + harness.spec.clone(), + ); + + Self::null_for_testing( + Arc::new(network_globals), + mpsc::unbounded_channel().0, + harness.chain.clone(), + harness.runtime.task_executor.clone(), + ) + .0 + } } diff --git a/beacon_node/network/src/network_beacon_processor/tests.rs b/beacon_node/network/src/network_beacon_processor/tests.rs index 18d34b40b3..42d3b8f33d 100644 --- a/beacon_node/network/src/network_beacon_processor/tests.rs +++ b/beacon_node/network/src/network_beacon_processor/tests.rs @@ -409,22 +409,6 @@ impl TestRig { .unwrap(); } - pub fn enqueue_gossip_blob(&self, blob_index: usize) { - if let Some(blobs) = self.next_blobs.as_ref() { - let blob = blobs.get(blob_index).unwrap(); - self.network_beacon_processor - .send_gossip_blob_sidecar( - junk_message_id(), - junk_peer_id(), - Client::default(), - blob.index, - blob.clone(), - Duration::from_secs(0), - ) - .unwrap(); - } - } - pub fn enqueue_gossip_data_columns(&self, col_index: usize) { if let Some(data_columns) = self.next_data_columns.as_ref() { let data_column = data_columns.get(col_index).unwrap(); @@ -1101,13 +1085,6 @@ async fn import_gossip_block_acceptably_early() { rig.assert_event_journal_completes(&[WorkType::GossipBlock]) .await; - let num_blobs = rig.next_blobs.as_ref().map(|b| b.len()).unwrap_or(0); - for i in 0..num_blobs { - rig.enqueue_gossip_blob(i); - rig.assert_event_journal_completes(&[WorkType::GossipBlobSidecar]) - .await; - } - let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); for i in 0..num_data_columns { rig.enqueue_gossip_data_columns(i); @@ -1242,13 +1219,6 @@ async fn import_gossip_block_at_current_slot() { rig.assert_event_journal_completes(&[WorkType::GossipBlock]) .await; - let num_blobs = rig.next_blobs.as_ref().map(|b| b.len()).unwrap_or(0); - for i in 0..num_blobs { - rig.enqueue_gossip_blob(i); - rig.assert_event_journal_completes(&[WorkType::GossipBlobSidecar]) - .await; - } - let num_data_columns = rig.next_data_columns.as_ref().map(|c| c.len()).unwrap_or(0); for i in 0..num_data_columns { rig.enqueue_gossip_data_columns(i); @@ -1315,10 +1285,6 @@ async fn attestation_to_unknown_block_processed(import_method: BlockImportMethod BlockImportMethod::Gossip => { rig.enqueue_gossip_block(); events.push(WorkType::GossipBlock); - for i in 0..num_blobs { - rig.enqueue_gossip_blob(i); - events.push(WorkType::GossipBlobSidecar); - } for i in 0..num_data_columns { rig.enqueue_gossip_data_columns(i); events.push(WorkType::GossipDataColumnSidecar); @@ -1401,10 +1367,6 @@ async fn aggregate_attestation_to_unknown_block(import_method: BlockImportMethod BlockImportMethod::Gossip => { rig.enqueue_gossip_block(); events.push(WorkType::GossipBlock); - for i in 0..num_blobs { - rig.enqueue_gossip_blob(i); - events.push(WorkType::GossipBlobSidecar); - } for i in 0..num_data_columns { rig.enqueue_gossip_data_columns(i); events.push(WorkType::GossipDataColumnSidecar) diff --git a/beacon_node/network/src/persisted_dht.rs b/beacon_node/network/src/persisted_dht.rs index 113b3cdd32..3672f97113 100644 --- a/beacon_node/network/src/persisted_dht.rs +++ b/beacon_node/network/src/persisted_dht.rs @@ -6,7 +6,7 @@ use types::{EthSpec, Hash256}; /// 32-byte key for accessing the `DhtEnrs`. All zero because `DhtEnrs` has its own column. pub const DHT_DB_KEY: Hash256 = Hash256::ZERO; -pub fn load_dht, Cold: ItemStore>( +pub fn load_dht( store: Arc>, ) -> Vec { // Load DHT from store @@ -20,7 +20,7 @@ pub fn load_dht, Cold: ItemStore>( } /// Attempt to persist the ENR's in the DHT to `self.store`. -pub fn persist_dht, Cold: ItemStore>( +pub fn persist_dht( store: Arc>, enrs: Vec, ) -> Result<(), store::Error> { @@ -28,7 +28,7 @@ pub fn persist_dht, Cold: ItemStore>( } /// Attempts to clear any DHT entries. -pub fn clear_dht, Cold: ItemStore>( +pub fn clear_dht( store: Arc>, ) -> Result<(), store::Error> { store.hot_db.delete::(&DHT_DB_KEY) @@ -75,11 +75,8 @@ mod tests { use types::{ChainSpec, MinimalEthSpec}; #[test] fn test_persisted_dht() { - let store: HotColdDB< - MinimalEthSpec, - MemoryStore, - MemoryStore, - > = HotColdDB::open_ephemeral(StoreConfig::default(), ChainSpec::minimal().into()).unwrap(); + let store: HotColdDB = + HotColdDB::open_ephemeral(StoreConfig::default(), ChainSpec::minimal().into()).unwrap(); let enrs = vec![Enr::from_str("enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8").unwrap()]; store .put_item(&DHT_DB_KEY, &PersistedDht { enrs: enrs.clone() }) diff --git a/beacon_node/network/src/router.rs b/beacon_node/network/src/router.rs index 35939c6f39..d2098d341e 100644 --- a/beacon_node/network/src/router.rs +++ b/beacon_node/network/src/router.rs @@ -412,19 +412,6 @@ impl Router { seen_timestamp, ), ), - PubsubMessage::BlobSidecar(data) => { - let (blob_index, blob_sidecar) = *data; - self.handle_beacon_processor_send_result( - self.network_beacon_processor.send_gossip_blob_sidecar( - message_id, - peer_id, - self.network_globals.client(&peer_id), - blob_index, - blob_sidecar, - seen_timestamp, - ), - ) - } PubsubMessage::DataColumnSidecar(data) => { let (subnet_id, column_sidecar) = *data; self.handle_beacon_processor_send_result( diff --git a/beacon_node/network/src/subnet_service/tests/mod.rs b/beacon_node/network/src/subnet_service/tests/mod.rs index 619154d738..745934053a 100644 --- a/beacon_node/network/src/subnet_service/tests/mod.rs +++ b/beacon_node/network/src/subnet_service/tests/mod.rs @@ -25,12 +25,7 @@ const SLOT_DURATION_MILLIS: u64 = 400; const TEST_LOG_LEVEL: Option<&str> = None; -type TestBeaconChainType = Witness< - SystemTimeSlotClock, - MainnetEthSpec, - MemoryStore, - MemoryStore, ->; +type TestBeaconChainType = Witness; pub struct TestBeaconChain { chain: Arc>, diff --git a/beacon_node/network/src/sync/block_lookups/mod.rs b/beacon_node/network/src/sync/block_lookups/mod.rs index f10610c751..ff3bf6f998 100644 --- a/beacon_node/network/src/sync/block_lookups/mod.rs +++ b/beacon_node/network/src/sync/block_lookups/mod.rs @@ -80,7 +80,6 @@ const MAX_LOOKUPS: usize = 200; /// The values for `Blob`, `DataColumn` and `PartialDataColumn` is the parent root of the column. pub enum BlockComponent { Block(DownloadResult>>), - Blob(DownloadResult), DataColumn(DownloadResult), PartialDataColumn(DownloadResult), } @@ -89,15 +88,13 @@ impl BlockComponent { fn parent_root(&self) -> Hash256 { match self { BlockComponent::Block(block) => block.value.parent_root(), - BlockComponent::Blob(parent_root) - | BlockComponent::DataColumn(parent_root) + BlockComponent::DataColumn(parent_root) | BlockComponent::PartialDataColumn(parent_root) => parent_root.value, } } fn get_type(&self) -> &'static str { match self { BlockComponent::Block(_) => "block", - BlockComponent::Blob(_) => "blob", BlockComponent::DataColumn(_) => "data_column", BlockComponent::PartialDataColumn(_) => "partial_data_column", } @@ -214,9 +211,9 @@ impl BlockLookups { block_root, Some(block_component), Some(parent_root), - // On a `UnknownParentBlock` or `UnknownParentBlob` event the peer is not required - // to have the rest of the block components (refer to decoupled blob gossip). Create - // the lookup with zero peers to house the block components. + // On a `UnknownParentBlock` or `UnknownParentDataColumn` event the peer is not + // required to have the rest of the block components. Create the lookup with zero + // peers to house the block components. &[], cx, ) diff --git a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs index 23bfd531f0..d54480e8e5 100644 --- a/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs +++ b/beacon_node/network/src/sync/block_lookups/single_block_lookup.rs @@ -156,9 +156,7 @@ impl SingleBlockLookup { .block_request_state .state .insert_verified_response(block), - BlockComponent::Blob(_) - | BlockComponent::DataColumn(_) - | BlockComponent::PartialDataColumn(_) => { + BlockComponent::DataColumn(_) | BlockComponent::PartialDataColumn(_) => { // For now ignore single blobs and columns, as the blob request state assumes all blobs are // attributed to the same peer = the peer serving the remaining blobs. Ignoring this // block component has a minor effect, causing the node to re-request this blob diff --git a/beacon_node/network/src/sync/custody_backfill_sync/mod.rs b/beacon_node/network/src/sync/custody_backfill_sync/mod.rs index fe4c7dfe4c..c85610613c 100644 --- a/beacon_node/network/src/sync/custody_backfill_sync/mod.rs +++ b/beacon_node/network/src/sync/custody_backfill_sync/mod.rs @@ -593,7 +593,7 @@ impl CustodyBackFillSync { Err(err) => { debug!(batch_epoch = %batch_id, error = ?err, "Batch download failed"); - // If there are any coupling errors, penalize the appropriate peers + // If there are any coupling errors, penalize the appropriate peers. if let RpcResponseError::BlockComponentCouplingError(coupling_error) = err && let CouplingError::DataColumnPeerFailure { error, @@ -601,15 +601,19 @@ impl CustodyBackFillSync { exceeded_retries: _, } = coupling_error { + let mut failed_peers = HashSet::new(); for (column_index, faulty_peer) in faulty_peers { debug!( ?error, ?column_index, ?faulty_peer, - "Custody backfill sync penalizing peer" + "Custody backfill sync: peer failed to serve column" ); + failed_peers.insert(faulty_peer); + } + for peer in failed_peers { network.report_peer( - faulty_peer, + peer, PeerAction::LowToleranceError, "Peer failed to serve column", ); diff --git a/beacon_node/network/src/sync/manager.rs b/beacon_node/network/src/sync/manager.rs index 14a38f0e72..534e0bc7c8 100644 --- a/beacon_node/network/src/sync/manager.rs +++ b/beacon_node/network/src/sync/manager.rs @@ -144,9 +144,6 @@ pub enum SyncMessage { /// A block with an unknown parent has been received. UnknownParentBlock(PeerId, Arc>, Hash256), - /// A blob with an unknown parent has been received. - UnknownParentBlob(PeerId, Arc>), - /// A data column with an unknown parent has been received. UnknownParentDataColumn(PeerId, Arc>), @@ -890,24 +887,6 @@ impl SyncManager { }), ); } - SyncMessage::UnknownParentBlob(peer_id, blob) => { - let blob_slot = blob.slot(); - let block_root = blob.block_root(); - let parent_root = blob.block_parent_root(); - debug!(%block_root, %parent_root, "Received unknown parent blob message"); - self.handle_unknown_parent( - peer_id, - block_root, - parent_root, - blob_slot, - BlockComponent::Blob(DownloadResult { - value: parent_root, - block_root, - seen_timestamp: self.chain.slot_clock.now_duration().unwrap_or_default(), - peer_group: PeerGroup::from_single(peer_id), - }), - ); - } SyncMessage::UnknownParentDataColumn(peer_id, data_column) => { let data_column_slot = data_column.slot(); let block_root = data_column.block_root(); diff --git a/beacon_node/network/src/sync/network_context/custody.rs b/beacon_node/network/src/sync/network_context/custody.rs index 620962b40b..2b96800e37 100644 --- a/beacon_node/network/src/sync/network_context/custody.rs +++ b/beacon_node/network/src/sync/network_context/custody.rs @@ -305,7 +305,12 @@ impl ActiveCustodyRequest { // must have its columns in custody. In that case, set `true = enforce max_requests` // and downscore if data_columns_by_root does not return the expected custody // columns. For the rest of peers, don't downscore if columns are missing. - lookup_peers.contains(&peer_id), + // + // Post-Gloas, blocks and payload envelopes are decoupled. A peer may + // have the block but not yet imported the envelope and data columns. + // Don't enforce max_responses in this case. + lookup_peers.contains(&peer_id) + && !cx.fork_context.current_fork_name().gloas_enabled(), ) .map_err(Error::SendFailed)?; diff --git a/beacon_node/network/src/sync/network_context/requests/blobs_by_range.rs b/beacon_node/network/src/sync/network_context/requests/blobs_by_range.rs index 9c6f516199..1da0fb52f7 100644 --- a/beacon_node/network/src/sync/network_context/requests/blobs_by_range.rs +++ b/beacon_node/network/src/sync/network_context/requests/blobs_by_range.rs @@ -33,6 +33,7 @@ impl ActiveRequestItems for BlobsByRangeRequestItems { if blob.index >= self.max_blobs_per_block { return Err(LookupVerifyError::UnrequestedIndex(blob.index)); } + if !blob.verify_blob_sidecar_inclusion_proof() { return Err(LookupVerifyError::InvalidInclusionProof); } diff --git a/beacon_node/network/src/sync/network_context/requests/blobs_by_root.rs b/beacon_node/network/src/sync/network_context/requests/blobs_by_root.rs index 556985c2b4..f0ff99867b 100644 --- a/beacon_node/network/src/sync/network_context/requests/blobs_by_root.rs +++ b/beacon_node/network/src/sync/network_context/requests/blobs_by_root.rs @@ -50,9 +50,11 @@ impl ActiveRequestItems for BlobsByRootRequestItems { if self.request.block_root != block_root { return Err(LookupVerifyError::UnrequestedBlockRoot(block_root)); } + if !blob.verify_blob_sidecar_inclusion_proof() { return Err(LookupVerifyError::InvalidInclusionProof); } + if !self.request.indices.contains(&blob.index) { return Err(LookupVerifyError::UnrequestedIndex(blob.index)); } diff --git a/beacon_node/network/src/sync/tests/lookups.rs b/beacon_node/network/src/sync/tests/lookups.rs index c1b2793491..5c9e18362c 100644 --- a/beacon_node/network/src/sync/tests/lookups.rs +++ b/beacon_node/network/src/sync/tests/lookups.rs @@ -1205,17 +1205,6 @@ impl TestRig { self.trigger_unknown_parent_block(peer_id, last_block); } - fn trigger_with_last_unknown_blob_parent(&mut self) { - let peer_id = self.new_connected_supernode_peer(); - let blobs = self - .get_last_block() - .block_data() - .blobs() - .expect("no blobs"); - let blob = blobs.first().expect("empty blobs"); - self.trigger_unknown_parent_blob(peer_id, blob.clone()); - } - fn trigger_with_last_unknown_data_column_parent(&mut self) { let peer_id = self.new_connected_supernode_peer(); let columns = self @@ -1224,7 +1213,7 @@ impl TestRig { .data_columns() .expect("No data columns"); let column = columns.first().expect("empty columns"); - self.trigger_unknown_parent_column(peer_id, column.clone()); + self.trigger_unknown_parent_data_column(peer_id, column.clone()); } // Post-test assertions @@ -1428,6 +1417,10 @@ impl TestRig { genesis_fork().deneb_enabled().then(Self::default) } + fn new_after_fulu() -> Option { + genesis_fork().fulu_enabled().then(Self::default) + } + fn new_after_deneb_before_fulu() -> Option { let fork = genesis_fork(); if fork.deneb_enabled() && !fork.fulu_enabled() { @@ -1463,16 +1456,12 @@ impl TestRig { self.send_sync_message(SyncMessage::UnknownParentBlock(peer_id, block, block_root)) } - fn trigger_unknown_parent_blob(&mut self, peer_id: PeerId, blob: Arc>) { - self.send_sync_message(SyncMessage::UnknownParentBlob(peer_id, blob)); - } - - fn trigger_unknown_parent_column( + fn trigger_unknown_parent_data_column( &mut self, peer_id: PeerId, - column: Arc>, + data_column: Arc>, ) { - self.send_sync_message(SyncMessage::UnknownParentDataColumn(peer_id, column)); + self.send_sync_message(SyncMessage::UnknownParentDataColumn(peer_id, data_column)); } fn trigger_unknown_block_from_attestation(&mut self, block_root: Hash256, peer_id: PeerId) { @@ -1757,9 +1746,9 @@ impl TestRig { ) .unwrap() { - Availability::Available(_) => panic!("blob removed from da_checker, available"), + Availability::Available(_) => panic!("column removed from da_checker, available"), Availability::MissingComponents(block_root) => { - self.log(&format!("inserted blob to da_checker {block_root:?}")) + self.log(&format!("inserted column to da_checker {block_root:?}")) } }; } @@ -1944,35 +1933,29 @@ async fn happy_path_unknown_block_parent(depth: usize) { } } -/// Assert that sync completes from a GossipUnknownParentBlob / UnknownDataColumnParent +/// Assert that sync completes from an UnknownDataColumnParent async fn happy_path_unknown_data_parent(depth: usize) { - let Some(mut r) = TestRig::new_after_deneb() else { + let Some(mut r) = TestRig::new_after_fulu() else { return; }; r.build_chain(depth).await; - if r.is_after_fulu() { - r.trigger_with_last_unknown_data_column_parent(); - } else if r.is_after_deneb() { - r.trigger_with_last_unknown_blob_parent(); - } + r.trigger_with_last_unknown_data_column_parent(); r.simulate(SimulateConfig::happy_path()).await; r.assert_successful_lookup_sync_parent_trigger(); } /// Assert that multiple trigger types don't create extra lookups async fn happy_path_multiple_triggers(depth: usize) { - let mut r = TestRig::default(); + let Some(mut r) = TestRig::new_after_fulu() else { + return; + }; // + 1, because the unknown parent trigger needs two new blocks r.build_chain(depth + 1).await; r.trigger_with_last_block(); r.trigger_with_last_block(); r.trigger_with_last_unknown_block_parent(); r.trigger_with_last_unknown_block_parent(); - if r.is_after_fulu() { - r.trigger_with_last_unknown_data_column_parent(); - } else if r.is_after_deneb() { - r.trigger_with_last_unknown_blob_parent(); - } + r.trigger_with_last_unknown_data_column_parent(); r.simulate(SimulateConfig::happy_path()).await; assert_eq!(r.created_lookups(), depth + 1, "Don't create extra lookups"); r.assert_successful_lookup_sync(); @@ -2105,18 +2088,14 @@ async fn too_many_processing_failures(depth: usize) { #[tokio::test] /// Assert that multiple trigger types don't create extra lookups async fn unknown_parent_does_not_add_peers_to_itself() { - let Some(mut r) = TestRig::new_after_deneb() else { + let Some(mut r) = TestRig::new_after_fulu() else { return; }; // 2, because the unknown parent trigger needs two new blocks r.build_chain(2).await; r.trigger_with_last_unknown_block_parent(); r.trigger_with_last_unknown_block_parent(); - if r.is_after_fulu() { - r.trigger_with_last_unknown_data_column_parent(); - } else if r.is_after_deneb() { - r.trigger_with_last_unknown_blob_parent(); - } + r.trigger_with_last_unknown_data_column_parent(); r.simulate(SimulateConfig::happy_path()).await; r.assert_peers_at_lookup_of_slot(2, 0); r.assert_peers_at_lookup_of_slot(1, 3); diff --git a/beacon_node/network/src/sync/tests/mod.rs b/beacon_node/network/src/sync/tests/mod.rs index dd8c3ae432..4e185cc081 100644 --- a/beacon_node/network/src/sync/tests/mod.rs +++ b/beacon_node/network/src/sync/tests/mod.rs @@ -26,7 +26,7 @@ use types::{ForkName, Hash256, MinimalEthSpec as E, Slot}; mod lookups; mod range; -type T = Witness, MemoryStore>; +type T = Witness; /// This test utility enables integration testing of Lighthouse sync components. /// diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index 51cda0fac3..647b5858cb 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -674,11 +674,22 @@ pub fn cli_app() -> Command { Arg::new("enable-partial-columns") .long("enable-partial-columns") .help("Enable partial messages for data columns. This can reduce the amount of \ - data sent over the network.") + data sent over the network. Enabled by default on Hoodi and Sepolia; use \ + --disable-partial-columns to opt out.") .action(ArgAction::SetTrue) .help_heading(FLAG_HEADER) .display_order(0) ) + .arg( + Arg::new("disable-partial-columns") + .long("disable-partial-columns") + .help("Disable partial messages for data columns. Use this on Hoodi or Sepolia \ + to opt out of the default-enabled behavior.") + .action(ArgAction::SetTrue) + .conflicts_with("enable-partial-columns") + .help_heading(FLAG_HEADER) + .display_order(0) + ) /* * Monitoring metrics */ @@ -1320,8 +1331,7 @@ pub fn cli_app() -> Command { .long("proposer-reorg-threshold") .action(ArgAction::Set) .value_name("PERCENT") - .help("Percentage of head vote weight below which to attempt a proposer reorg. \ - Default: 20%") + .help("DEPRECATED. This flag has no effect.") .conflicts_with("disable-proposer-reorgs") .display_order(0) ) @@ -1329,8 +1339,7 @@ pub fn cli_app() -> Command { Arg::new("proposer-reorg-parent-threshold") .long("proposer-reorg-parent-threshold") .value_name("PERCENT") - .help("Percentage of parent vote weight above which to attempt a proposer reorg. \ - Default: 160%") + .help("DEPRECATED. This flag has no effect.") .conflicts_with("disable-proposer-reorgs") .action(ArgAction::Set) .display_order(0) @@ -1340,8 +1349,7 @@ pub fn cli_app() -> Command { .long("proposer-reorg-epochs-since-finalization") .action(ArgAction::Set) .value_name("EPOCHS") - .help("Maximum number of epochs since finalization at which proposer reorgs are \ - allowed. Default: 2") + .help("DEPRECATED. This flag has no effect.") .conflicts_with("disable-proposer-reorgs") .display_order(0) ) @@ -1350,10 +1358,7 @@ pub fn cli_app() -> Command { .long("proposer-reorg-cutoff") .value_name("MILLISECONDS") .action(ArgAction::Set) - .help("Maximum delay after the start of the slot at which to propose a reorging \ - block. Lower values can prevent failed reorgs by ensuring the block has \ - ample time to propagate and be processed by the network. The default is \ - 1/12th of a slot (1 second on mainnet)") + .help("DEPRECATED. This flag has no effect.") .conflicts_with("disable-proposer-reorgs") .display_order(0) ) diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index f10f9e3b45..045b432dc9 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -1,8 +1,6 @@ use account_utils::{STDIN_INPUTS_FLAG, read_input_from_user}; use beacon_chain::chain_config::{ - DEFAULT_PREPARE_PAYLOAD_LOOKAHEAD_FACTOR, DEFAULT_RE_ORG_HEAD_THRESHOLD, - DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, DEFAULT_RE_ORG_PARENT_THRESHOLD, - DisallowedReOrgOffsets, INVALID_HOLESKY_BLOCK_ROOT, ReOrgThreshold, + DEFAULT_PREPARE_PAYLOAD_LOOKAHEAD_FACTOR, DisallowedReOrgOffsets, INVALID_HOLESKY_BLOCK_ROOT, }; use beacon_chain::custody_context::NodeCustodyType; use beacon_chain::graffiti_calculator::GraffitiOrigin; @@ -110,7 +108,16 @@ pub fn get_config( set_network_config(&mut client_config.network, cli_args, &data_dir_ref)?; - if parse_flag(cli_args, "enable-partial-columns") { + let default_partial_columns_enabled = spec + .config_name + .as_ref() + .is_some_and(|name| matches!(name.as_str(), "hoodi" | "sepolia")); + let user_disable_partial_columns = parse_flag(cli_args, "disable-partial-columns"); + let user_enable_partial_columns = parse_flag(cli_args, "enable-partial-columns"); + let enable_partial_columns = !user_disable_partial_columns + && (user_enable_partial_columns || default_partial_columns_enabled); + + if enable_partial_columns { // Partial messages assume that each subnet maps to exactly one column. // Check this here to avoid weird issues on networks where this is not the case. if spec.data_column_sidecar_subnet_count == E::number_of_columns() as u64 { @@ -744,41 +751,39 @@ pub fn get_config( .individual_tracking_threshold = count; } - if cli_args.get_flag("disable-proposer-reorgs") { - client_config.chain.re_org_head_threshold = None; - client_config.chain.re_org_parent_threshold = None; - } else { - client_config.chain.re_org_head_threshold = Some( - clap_utils::parse_optional(cli_args, "proposer-reorg-threshold")? - .map(ReOrgThreshold) - .unwrap_or(DEFAULT_RE_ORG_HEAD_THRESHOLD), - ); - client_config.chain.re_org_max_epochs_since_finalization = - clap_utils::parse_optional(cli_args, "proposer-reorg-epochs-since-finalization")? - .unwrap_or(DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION); - client_config.chain.re_org_cutoff_millis = - clap_utils::parse_optional(cli_args, "proposer-reorg-cutoff")?; + client_config.chain.disable_proposer_reorg = cli_args.get_flag("disable-proposer-reorgs"); - client_config.chain.re_org_parent_threshold = Some( - clap_utils::parse_optional(cli_args, "proposer-reorg-parent-threshold")? - .map(ReOrgThreshold) - .unwrap_or(DEFAULT_RE_ORG_PARENT_THRESHOLD), - ); + if clap_utils::parse_optional::(cli_args, "proposer-reorg-threshold")?.is_some() { + warn!("The proposer-reorg-threshold flag is deprecated"); + } - if let Some(disallowed_offsets_str) = - clap_utils::parse_optional::(cli_args, "proposer-reorg-disallowed-offsets")? - { - let disallowed_offsets = disallowed_offsets_str - .split(',') - .map(|s| { - s.parse() - .map_err(|e| format!("invalid disallowed-offsets: {e:?}")) - }) - .collect::, _>>()?; - client_config.chain.re_org_disallowed_offsets = - DisallowedReOrgOffsets::new::(disallowed_offsets) - .map_err(|e| format!("invalid disallowed-offsets: {e:?}"))?; - } + if clap_utils::parse_optional::(cli_args, "proposer-reorg-epochs-since-finalization")? + .is_some() + { + warn!("The proposer-reorg-epochs-since-finalization flag is deprecated"); + } + + if clap_utils::parse_optional::(cli_args, "proposer-reorg-cutoff")?.is_some() { + warn!("The proposer-reorg-cutoff flag is deprecated"); + } + + if clap_utils::parse_optional::(cli_args, "proposer-reorg-parent-threshold")?.is_some() { + warn!("The proposer-reorg-parent-threshold flag is deprecated"); + } + + if let Some(disallowed_offsets_str) = + clap_utils::parse_optional::(cli_args, "proposer-reorg-disallowed-offsets")? + { + let disallowed_offsets = disallowed_offsets_str + .split(',') + .map(|s| { + s.parse() + .map_err(|e| format!("invalid disallowed-offsets: {e:?}")) + }) + .collect::, _>>()?; + client_config.chain.re_org_disallowed_offsets = + DisallowedReOrgOffsets::new::(disallowed_offsets) + .map_err(|e| format!("invalid disallowed-offsets: {e:?}"))?; } client_config.chain.prepare_payload_lookahead = diff --git a/beacon_node/src/lib.rs b/beacon_node/src/lib.rs index e33da17e26..6400427f8c 100644 --- a/beacon_node/src/lib.rs +++ b/beacon_node/src/lib.rs @@ -20,7 +20,7 @@ use types::{ChainSpec, Epoch, EthSpec, ForkName}; /// A type-alias to the tighten the definition of a production-intended `Client`. pub type ProductionClient = - Client, BeaconNodeBackend>>; + Client>; /// The beacon node `Client` that is used in production. /// diff --git a/beacon_node/store/src/database/interface.rs b/beacon_node/store/src/database/interface.rs index 5646f1179c..7e0a09a3e9 100644 --- a/beacon_node/store/src/database/interface.rs +++ b/beacon_node/store/src/database/interface.rs @@ -6,18 +6,17 @@ use crate::{ColumnIter, ColumnKeyIter, DBColumn, Error, ItemStore, Key, KeyValue use crate::{KeyValueStoreOp, StoreConfig, config::DatabaseBackend}; use std::collections::HashSet; use std::path::Path; -use types::EthSpec; -pub enum BeaconNodeBackend { +pub enum BeaconNodeBackend { #[cfg(feature = "leveldb")] - LevelDb(leveldb_impl::LevelDB), + LevelDb(leveldb_impl::LevelDB), #[cfg(feature = "redb")] - Redb(redb_impl::Redb), + Redb(redb_impl::Redb), } -impl ItemStore for BeaconNodeBackend {} +impl ItemStore for BeaconNodeBackend {} -impl KeyValueStore for BeaconNodeBackend { +impl KeyValueStore for BeaconNodeBackend { fn get_bytes(&self, column: DBColumn, key: &[u8]) -> Result>, Error> { match self { #[cfg(feature = "leveldb")] @@ -183,7 +182,7 @@ impl KeyValueStore for BeaconNodeBackend { } } -impl BeaconNodeBackend { +impl BeaconNodeBackend { pub fn open(config: &StoreConfig, path: &Path) -> Result { metrics::inc_counter_vec(&metrics::DISK_DB_TYPE, &[&config.backend.to_string()]); match config.backend { diff --git a/beacon_node/store/src/database/leveldb_impl.rs b/beacon_node/store/src/database/leveldb_impl.rs index 6e01648263..0531eb900e 100644 --- a/beacon_node/store/src/database/leveldb_impl.rs +++ b/beacon_node/store/src/database/leveldb_impl.rs @@ -15,15 +15,13 @@ use leveldb::{ options::{Options, ReadOptions}, }; use std::collections::HashSet; -use std::marker::PhantomData; use std::path::Path; -use types::{EthSpec, Hash256}; +use types::Hash256; use super::interface::WriteOptions; -pub struct LevelDB { +pub struct LevelDB { db: Database, - _phantom: PhantomData, } impl From for leveldb::options::WriteOptions { @@ -34,7 +32,7 @@ impl From for leveldb::options::WriteOptions { } } -impl LevelDB { +impl LevelDB { pub fn open(path: &Path) -> Result { let mut options = Options::new(); @@ -42,10 +40,7 @@ impl LevelDB { let db = Database::open(path, options)?; - Ok(Self { - db, - _phantom: PhantomData, - }) + Ok(Self { db }) } pub fn read_options(&self) -> ReadOptions<'_, BytesKey> { diff --git a/beacon_node/store/src/database/redb_impl.rs b/beacon_node/store/src/database/redb_impl.rs index 4077326eca..dc39f22114 100644 --- a/beacon_node/store/src/database/redb_impl.rs +++ b/beacon_node/store/src/database/redb_impl.rs @@ -3,17 +3,15 @@ use crate::{DBColumn, Error, KeyValueStoreOp}; use parking_lot::RwLock; use redb::TableDefinition; use std::collections::HashSet; -use std::{borrow::BorrowMut, marker::PhantomData, path::Path}; +use std::{borrow::BorrowMut, path::Path}; use strum::IntoEnumIterator; -use types::EthSpec; use super::interface::WriteOptions; pub const DB_FILE_NAME: &str = "database.redb"; -pub struct Redb { +pub struct Redb { db: RwLock, - _phantom: PhantomData, } impl From for redb::Durability { @@ -26,19 +24,16 @@ impl From for redb::Durability { } } -impl Redb { +impl Redb { pub fn open(path: &Path) -> Result { let db_file = path.join(DB_FILE_NAME); let db = redb::Database::create(db_file)?; for column in DBColumn::iter() { - Redb::::create_table(&db, column.into())?; + Self::create_table(&db, column.into())?; } - Ok(Self { - db: db.into(), - _phantom: PhantomData, - }) + Ok(Self { db: db.into() }) } fn create_table(db: &redb::Database, table_name: &str) -> Result<(), Error> { diff --git a/beacon_node/store/src/forwards_iter.rs b/beacon_node/store/src/forwards_iter.rs index 255b7d8eac..ef4312f506 100644 --- a/beacon_node/store/src/forwards_iter.rs +++ b/beacon_node/store/src/forwards_iter.rs @@ -9,7 +9,7 @@ pub type HybridForwardsBlockRootsIterator<'a, E, Hot, Cold> = pub type HybridForwardsStateRootsIterator<'a, E, Hot, Cold> = HybridForwardsIterator<'a, E, Hot, Cold>; -impl, Cold: ItemStore> HotColdDB { +impl HotColdDB { fn simple_forwards_iterator( &self, column: DBColumn, @@ -116,7 +116,7 @@ impl, Cold: ItemStore> HotColdDB } /// Forwards root iterator that makes use of a slot -> root mapping in the freezer DB. -pub struct FrozenForwardsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { +pub struct FrozenForwardsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { inner: ColumnIter<'a, Vec>, column: DBColumn, next_slot: Slot, @@ -124,9 +124,7 @@ pub struct FrozenForwardsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemS _phantom: PhantomData<(E, Hot, Cold)>, } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> - FrozenForwardsIterator<'a, E, Hot, Cold> -{ +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> FrozenForwardsIterator<'a, E, Hot, Cold> { /// `end_slot` is EXCLUSIVE here. pub fn new( store: &'a HotColdDB, @@ -148,7 +146,7 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> } } -impl, Cold: ItemStore> Iterator +impl Iterator for FrozenForwardsIterator<'_, E, Hot, Cold> { type Item = Result<(Hash256, Slot)>; @@ -199,7 +197,7 @@ impl Iterator for SimpleForwardsIterator { } /// Fusion of the above two approaches to forwards iteration. Fast and efficient. -pub enum HybridForwardsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { +pub enum HybridForwardsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { PreFinalization { iter: Box>, store: &'a HotColdDB, @@ -220,9 +218,7 @@ pub enum HybridForwardsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemSto Finished, } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> - HybridForwardsIterator<'a, E, Hot, Cold> -{ +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> HybridForwardsIterator<'a, E, Hot, Cold> { /// Construct a new hybrid iterator. /// /// The `get_state` closure should return a beacon state and final block/state root to backtrack @@ -349,7 +345,7 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> } } -impl, Cold: ItemStore> Iterator +impl Iterator for HybridForwardsIterator<'_, E, Hot, Cold> { type Item = Result<(Hash256, Slot)>; diff --git a/beacon_node/store/src/hot_cold_store.rs b/beacon_node/store/src/hot_cold_store.rs index e9b9de76e6..a625a97004 100644 --- a/beacon_node/store/src/hot_cold_store.rs +++ b/beacon_node/store/src/hot_cold_store.rs @@ -49,7 +49,7 @@ use zstd::{Decoder, Encoder}; /// Stores vector fields like the `block_roots` and `state_roots` separately, and only stores /// intermittent "restore point" states pre-finalization. #[derive(Debug)] -pub struct HotColdDB, Cold: ItemStore> { +pub struct HotColdDB { /// The slot and state root at the point where the database is split between hot and cold. /// /// States with slots less than `split.slot` are in the cold DB, while states with slots @@ -217,11 +217,11 @@ pub enum HotColdDBError { Rollback, } -impl HotColdDB, MemoryStore> { +impl HotColdDB { pub fn open_ephemeral( config: StoreConfig, spec: Arc, - ) -> Result, MemoryStore>, Error> { + ) -> Result, Error> { config.verify::()?; let hierarchy = config.hierarchy_config.to_moduli()?; @@ -258,7 +258,7 @@ impl HotColdDB, MemoryStore> { } } -impl HotColdDB, BeaconNodeBackend> { +impl HotColdDB { /// Open a new or existing database, with the given paths to the hot and cold DBs. /// /// The `migrate_schema` function is passed in so that the parent `BeaconChain` can provide @@ -451,7 +451,7 @@ impl HotColdDB, BeaconNodeBackend> { } } -impl, Cold: ItemStore> HotColdDB { +impl HotColdDB { fn cold_storage_strategy(&self, slot: Slot) -> Result { // The start slot for the freezer HDiff is always 0 Ok(self.hierarchy.storage_strategy(slot, Slot::new(0))?) @@ -3575,7 +3575,7 @@ impl, Cold: ItemStore> HotColdDB /// This function previously did a combination of freezer migration alongside pruning. Now it is /// *just* responsible for copying relevant data to the freezer, while pruning is implemented /// in `prune_hot_db`. -pub fn migrate_database, Cold: ItemStore>( +pub fn migrate_database( store: Arc>, finalized_state_root: Hash256, finalized_block_root: Hash256, @@ -3786,7 +3786,7 @@ pub enum StateSummaryIteratorError { /// Return the ancestor state root of a state beyond SlotsPerHistoricalRoot using the roots iterator /// and the store -pub fn get_ancestor_state_root<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore>( +pub fn get_ancestor_state_root<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore>( store: &'a HotColdDB, from_state: &'a BeaconState, target_slot: Slot, @@ -3993,7 +3993,7 @@ impl StoreItem for HotStateSummary { impl HotStateSummary { /// Construct a new summary of the given state. - pub fn new, Cold: ItemStore>( + pub fn new( store: &HotColdDB, state_root: Hash256, state: &BeaconState, diff --git a/beacon_node/store/src/invariants.rs b/beacon_node/store/src/invariants.rs index d251fb8800..4ec72b82bd 100644 --- a/beacon_node/store/src/invariants.rs +++ b/beacon_node/store/src/invariants.rs @@ -242,7 +242,7 @@ pub enum InvariantViolation { ColdStateBaseSummaryMissing { slot: Slot, base_slot: Slot }, } -impl, Cold: ItemStore> HotColdDB { +impl HotColdDB { /// Run all database invariant checks. /// /// The `ctx` parameter provides data from the beacon chain layer (fork choice, state cache, diff --git a/beacon_node/store/src/iter.rs b/beacon_node/store/src/iter.rs index 0cb803d1ed..cf1ab86ffe 100644 --- a/beacon_node/store/src/iter.rs +++ b/beacon_node/store/src/iter.rs @@ -13,12 +13,12 @@ use types::{ /// /// It is assumed that all ancestors for this object are stored in the database. If this is not the /// case, the iterator will start returning `None` prior to genesis. -pub trait AncestorIter<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore, I: Iterator> { +pub trait AncestorIter<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore, I: Iterator> { /// Returns an iterator over the roots of the ancestors of `self`. fn try_iter_ancestor_roots(&self, store: &'a HotColdDB) -> Option; } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> AncestorIter<'a, E, Hot, Cold, BlockRootsIterator<'a, E, Hot, Cold>> for SignedBeaconBlock { /// Iterates across all available prior block roots of `self`, starting at the most recent and ending @@ -37,7 +37,7 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> } } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> AncestorIter<'a, E, Hot, Cold, StateRootsIterator<'a, E, Hot, Cold>> for BeaconState { /// Iterates across all available prior state roots of `self`, starting at the most recent and ending @@ -51,13 +51,11 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> } } -pub struct StateRootsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { +pub struct StateRootsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { inner: RootsIterator<'a, E, Hot, Cold>, } -impl, Cold: ItemStore> Clone - for StateRootsIterator<'_, E, Hot, Cold> -{ +impl Clone for StateRootsIterator<'_, E, Hot, Cold> { fn clone(&self) -> Self { Self { inner: self.inner.clone(), @@ -65,7 +63,7 @@ impl, Cold: ItemStore> Clone } } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> StateRootsIterator<'a, E, Hot, Cold> { +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> StateRootsIterator<'a, E, Hot, Cold> { pub fn new(store: &'a HotColdDB, beacon_state: &'a BeaconState) -> Self { Self { inner: RootsIterator::new(store, beacon_state), @@ -79,7 +77,7 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> StateRootsIterator<' } } -impl, Cold: ItemStore> Iterator +impl Iterator for StateRootsIterator<'_, E, Hot, Cold> { type Item = Result<(Hash256, Slot), Error>; @@ -99,13 +97,11 @@ impl, Cold: ItemStore> Iterator /// exhausted. /// /// Returns `None` for roots prior to genesis or when there is an error reading from `Store`. -pub struct BlockRootsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { +pub struct BlockRootsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { inner: RootsIterator<'a, E, Hot, Cold>, } -impl, Cold: ItemStore> Clone - for BlockRootsIterator<'_, E, Hot, Cold> -{ +impl Clone for BlockRootsIterator<'_, E, Hot, Cold> { fn clone(&self) -> Self { Self { inner: self.inner.clone(), @@ -113,7 +109,7 @@ impl, Cold: ItemStore> Clone } } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> BlockRootsIterator<'a, E, Hot, Cold> { +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> BlockRootsIterator<'a, E, Hot, Cold> { /// Create a new iterator over all block roots in the given `beacon_state` and prior states. pub fn new(store: &'a HotColdDB, beacon_state: &'a BeaconState) -> Self { Self { @@ -138,7 +134,7 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> BlockRootsIterator<' } } -impl, Cold: ItemStore> Iterator +impl Iterator for BlockRootsIterator<'_, E, Hot, Cold> { type Item = Result<(Hash256, Slot), Error>; @@ -151,13 +147,13 @@ impl, Cold: ItemStore> Iterator } /// Iterator over state and block roots that backtracks using the vectors from a `BeaconState`. -pub struct RootsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { +pub struct RootsIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { store: &'a HotColdDB, beacon_state: Cow<'a, BeaconState>, slot: Slot, } -impl, Cold: ItemStore> Clone for RootsIterator<'_, E, Hot, Cold> { +impl Clone for RootsIterator<'_, E, Hot, Cold> { fn clone(&self) -> Self { Self { store: self.store, @@ -167,7 +163,7 @@ impl, Cold: ItemStore> Clone for RootsIterator< } } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> RootsIterator<'a, E, Hot, Cold> { +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> RootsIterator<'a, E, Hot, Cold> { pub fn new(store: &'a HotColdDB, beacon_state: &'a BeaconState) -> Self { Self { store, @@ -234,9 +230,7 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> RootsIterator<'a, E, } } -impl, Cold: ItemStore> Iterator - for RootsIterator<'_, E, Hot, Cold> -{ +impl Iterator for RootsIterator<'_, E, Hot, Cold> { /// (block_root, state_root, slot) type Item = Result<(Hash256, Hash256, Slot), Error>; @@ -246,15 +240,13 @@ impl, Cold: ItemStore> Iterator } /// Block iterator that uses the `parent_root` of each block to backtrack. -pub struct ParentRootBlockIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { +pub struct ParentRootBlockIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { store: &'a HotColdDB, next_block_root: Hash256, _phantom: PhantomData, } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> - ParentRootBlockIterator<'a, E, Hot, Cold> -{ +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> ParentRootBlockIterator<'a, E, Hot, Cold> { pub fn new(store: &'a HotColdDB, start_block_root: Hash256) -> Self { Self { store, @@ -283,7 +275,7 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> } } -impl, Cold: ItemStore> Iterator +impl Iterator for ParentRootBlockIterator<'_, E, Hot, Cold> { type Item = Result<(Hash256, SignedBeaconBlock>), Error>; @@ -295,11 +287,11 @@ impl, Cold: ItemStore> Iterator #[derive(Clone)] /// Extends `BlockRootsIterator`, returning `SignedBeaconBlock` instances, instead of their roots. -pub struct BlockIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { +pub struct BlockIterator<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> { roots: BlockRootsIterator<'a, E, Hot, Cold>, } -impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> BlockIterator<'a, E, Hot, Cold> { +impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> BlockIterator<'a, E, Hot, Cold> { /// Create a new iterator over all blocks in the given `beacon_state` and prior states. pub fn new(store: &'a HotColdDB, beacon_state: &'a BeaconState) -> Self { Self { @@ -324,9 +316,7 @@ impl<'a, E: EthSpec, Hot: ItemStore, Cold: ItemStore> BlockIterator<'a, E, } } -impl, Cold: ItemStore> Iterator - for BlockIterator<'_, E, Hot, Cold> -{ +impl Iterator for BlockIterator<'_, E, Hot, Cold> { type Item = Result>, Error>; fn next(&mut self) -> Option { @@ -338,7 +328,7 @@ impl, Cold: ItemStore> Iterator /// /// Return `Err(HistoryUnavailable)` in the case where no more backtrack states are available /// due to weak subjectivity sync. -fn next_historical_root_backtrack_state, Cold: ItemStore>( +fn next_historical_root_backtrack_state( store: &HotColdDB, current_state: &BeaconState, ) -> Result, Error> { @@ -386,7 +376,7 @@ mod test { harness.get_current_state() } - fn get_store() -> HotColdDB, MemoryStore> { + fn get_store() -> HotColdDB { let store = HotColdDB::open_ephemeral(Config::default(), Arc::new(E::default_spec())).unwrap(); // Init achor info so anchor slot is set. Use a random block as it is only used for the diff --git a/beacon_node/store/src/lib.rs b/beacon_node/store/src/lib.rs index bd8caa3ad5..56cdd18fbe 100644 --- a/beacon_node/store/src/lib.rs +++ b/beacon_node/store/src/lib.rs @@ -46,7 +46,7 @@ pub type ColumnKeyIter<'a, K> = Box> + 'a>; pub type RawEntryIter<'a> = Result, Vec), Error>> + 'a>, Error>; -pub trait KeyValueStore: Sync + Send + Sized + 'static { +pub trait KeyValueStore: Sync + Send + Sized + 'static { /// Retrieve some bytes in `column` with `key`. fn get_bytes(&self, column: DBColumn, key: &[u8]) -> Result>, Error>; @@ -177,7 +177,7 @@ pub enum KeyValueStoreOp { DeleteKey(DBColumn, Vec), } -pub trait ItemStore: KeyValueStore + Sync + Send + Sized + 'static { +pub trait ItemStore: KeyValueStore + Sync + Send + Sized + 'static { /// Store an item in `Self`. fn put(&self, key: &Hash256, item: &I) -> Result<(), Error> { let column = I::db_column(); @@ -493,7 +493,7 @@ mod tests { } } - fn test_impl(store: impl ItemStore) { + fn test_impl(store: impl ItemStore) { let key = Hash256::random(); let item = StorableThing { a: 1, b: 42 }; @@ -531,7 +531,7 @@ mod tests { #[test] fn exists() { - let store = MemoryStore::::open(); + let store = MemoryStore::open(); let key = Hash256::random(); let item = StorableThing { a: 1, b: 42 }; diff --git a/beacon_node/store/src/memory_store.rs b/beacon_node/store/src/memory_store.rs index 6baef61c9d..8be9278d90 100644 --- a/beacon_node/store/src/memory_store.rs +++ b/beacon_node/store/src/memory_store.rs @@ -4,28 +4,24 @@ use crate::{ }; use parking_lot::RwLock; use std::collections::{BTreeMap, HashSet}; -use std::marker::PhantomData; -use types::*; type DBMap = BTreeMap>; /// A thread-safe `BTreeMap` wrapper. -pub struct MemoryStore { +pub struct MemoryStore { db: RwLock, - _phantom: PhantomData, } -impl MemoryStore { +impl MemoryStore { /// Create a new, empty database. pub fn open() -> Self { Self { db: RwLock::new(BTreeMap::new()), - _phantom: PhantomData, } } } -impl KeyValueStore for MemoryStore { +impl KeyValueStore for MemoryStore { /// Get the value of some key from the database. Returns `None` if the key does not exist. fn get_bytes(&self, col: DBColumn, key: &[u8]) -> Result>, Error> { let column_key = BytesKey::from_vec(get_key_for_col(col, key)); @@ -148,4 +144,4 @@ impl KeyValueStore for MemoryStore { } } -impl ItemStore for MemoryStore {} +impl ItemStore for MemoryStore {} diff --git a/beacon_node/store/src/reconstruct.rs b/beacon_node/store/src/reconstruct.rs index 04a519af02..2fb40daa0d 100644 --- a/beacon_node/store/src/reconstruct.rs +++ b/beacon_node/store/src/reconstruct.rs @@ -15,8 +15,8 @@ use types::{EthSpec, Slot}; impl HotColdDB where E: EthSpec, - Hot: ItemStore, - Cold: ItemStore, + Hot: ItemStore, + Cold: ItemStore, { pub fn reconstruct_historic_states( self: &Arc, diff --git a/book/src/advanced_re-orgs.md b/book/src/advanced_re-orgs.md index 3a31778786..71751f354f 100644 --- a/book/src/advanced_re-orgs.md +++ b/book/src/advanced_re-orgs.md @@ -14,14 +14,6 @@ attestations and transactions that can be included. There are three flags which control the re-orging behaviour: * `--disable-proposer-reorgs`: turn re-orging off (it's on by default). -* `--proposer-reorg-threshold N`: attempt to orphan blocks with less than N% of the committee vote. If this parameter isn't set then N defaults to 20% when the feature is enabled. -* `--proposer-reorg-epochs-since-finalization N`: only attempt to re-org late blocks when the number of epochs since finalization is less than or equal to N. The default is 2 epochs, - meaning re-orgs will only be attempted when the chain is finalizing optimally. -* `--proposer-reorg-cutoff T`: only attempt to re-org late blocks when the proposal is being made - before T milliseconds into the slot. Delays between the validator client and the beacon node can - cause some blocks to be requested later than the start of the slot, which makes them more likely - to fail. The default cutoff is 1000ms on mainnet, which gives blocks 3000ms to be signed and - propagated before the attestation deadline at 4000ms. * `--proposer-reorg-disallowed-offsets N1,N2,N3...`: Prohibit Lighthouse from attempting to reorg at specific offsets in each epoch. A disallowed offset `N` prevents reorging blocks from being proposed at any `slot` such that `slot % SLOTS_PER_EPOCH == N`. The value to this flag is a diff --git a/book/src/help_bn.md b/book/src/help_bn.md index b580bcae52..30163f1f0c 100644 --- a/book/src/help_bn.md +++ b/book/src/help_bn.md @@ -306,10 +306,7 @@ Options: values are useful for ensuring the EL is given ample notice. Default: 1/3 of a slot. --proposer-reorg-cutoff - Maximum delay after the start of the slot at which to propose a - reorging block. Lower values can prevent failed reorgs by ensuring the - block has ample time to propagate and be processed by the network. The - default is 1/12th of a slot (1 second on mainnet) + DEPRECATED. This flag has no effect. --proposer-reorg-disallowed-offsets Comma-separated list of integer offsets which can be used to avoid proposing reorging blocks at certain slots. An offset of N means that @@ -318,14 +315,11 @@ Options: avoided. Any offsets supplied with this flag will impose additional restrictions. --proposer-reorg-epochs-since-finalization - Maximum number of epochs since finalization at which proposer reorgs - are allowed. Default: 2 + DEPRECATED. This flag has no effect. --proposer-reorg-parent-threshold - Percentage of parent vote weight above which to attempt a proposer - reorg. Default: 160% + DEPRECATED. This flag has no effect. --proposer-reorg-threshold - Percentage of head vote weight below which to attempt a proposer - reorg. Default: 20% + DEPRECATED. This flag has no effect. --prune-blobs Prune blobs from Lighthouse's database when they are older than the data data availability boundary relative to the current epoch. @@ -482,6 +476,9 @@ Flags: --disable-packet-filter Disables the discovery packet filter. Useful for testing in smaller networks + --disable-partial-columns + Disable partial messages for data columns. Use this on Hoodi or + Sepolia to opt out of the default-enabled behavior. --disable-proposer-reorgs Do not attempt to reorg late blocks from other validators when proposing. @@ -499,7 +496,8 @@ Flags: --listen-address and the UDP port will be --discovery-port. --enable-partial-columns Enable partial messages for data columns. This can reduce the amount - of data sent over the network. + of data sent over the network. Enabled by default on Hoodi and + Sepolia; use --disable-partial-columns to opt out. --enable-private-discovery Lighthouse by default does not discover private IP addresses. Set this flag to enable connection attempts to local addresses. diff --git a/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml b/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml index 02bf37cb55..ced9679142 100644 --- a/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml +++ b/common/eth2_network_config/built_in_network_configs/mainnet/config.yaml @@ -93,8 +93,8 @@ SYNC_MESSAGE_DUE_BPS: 3333 CONTRIBUTION_DUE_BPS: 6667 # Gloas -# 2**6 (= 64) epochs -MIN_BUILDER_WITHDRAWABILITY_DELAY: 64 +# 2**13 (= 8192) epochs +MIN_BUILDER_WITHDRAWABILITY_DELAY: 8192 # 2500 basis points, 25% of SLOT_DURATION_MS ATTESTATION_DUE_BPS_GLOAS: 2500 # 5000 basis points, 50% of SLOT_DURATION_MS diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index a60859585c..2de8ce7d81 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -1207,7 +1207,6 @@ where fn validate_on_payload_attestation( &self, indexed_payload_attestation: &IndexedPayloadAttestation, - is_from_block: AttestationFromBlock, ) -> Result<(), InvalidPayloadAttestation> { // This check is from `is_valid_indexed_payload_attestation`, but we do it immediately to // avoid wasting time on junk attestations. @@ -1233,25 +1232,6 @@ where }); } - // PTC votes can only change the vote for their assigned beacon block, return early otherwise - if block.slot != indexed_payload_attestation.data.slot { - return Ok(()); - } - - // Gossip payload attestations must be for the current slot. - // NOTE: signature is assumed to have been verified by caller. - // https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/fork-choice.md - if matches!(is_from_block, AttestationFromBlock::False) - && indexed_payload_attestation.data.slot != self.fc_store.get_current_slot() - { - return Err( - InvalidPayloadAttestation::PayloadAttestationNotCurrentSlot { - attestation_slot: indexed_payload_attestation.data.slot, - current_slot: self.fc_store.get_current_slot(), - }, - ); - } - Ok(()) } @@ -1339,34 +1319,69 @@ where pub fn on_payload_attestation( &mut self, system_time_current_slot: Slot, - attestation: &IndexedPayloadAttestation, + payload_attestation: &IndexedPayloadAttestation, is_from_block: AttestationFromBlock, ptc: &[usize], ) -> Result<(), Error> { self.update_time(system_time_current_slot)?; - if attestation.data.beacon_block_root.is_zero() { + if payload_attestation.data.beacon_block_root.is_zero() { return Ok(()); } // TODO(gloas): Should ignore wrong-slot payload attestations at the caller, they could // have been processed at the correct slot when received on gossip, but then have the // wrong-slot by the time they make it to here (TOCTOU). - self.validate_on_payload_attestation(attestation, is_from_block)?; + // TODO(gloas): Consider inlining validate_on_payload_attestation here to look more like the spec. + self.validate_on_payload_attestation(payload_attestation)?; - // Resolve validator indices to PTC committee positions. - let ptc_indices: Vec = attestation - .attesting_indices - .iter() - .filter_map(|validator_index| ptc.iter().position(|&p| p == *validator_index as usize)) - .collect(); + // PTC votes can only change the vote for their assigned beacon block, return early otherwise. + let block = self + .proto_array + .get_block(&payload_attestation.data.beacon_block_root) + .ok_or(InvalidPayloadAttestation::UnknownHeadBlock { + beacon_block_root: payload_attestation.data.beacon_block_root, + })?; + if block.slot != payload_attestation.data.slot { + return Ok(()); + } + + // Gossip payload attestations must be for the current slot. + if matches!(is_from_block, AttestationFromBlock::False) + && payload_attestation.data.slot != self.fc_store.get_current_slot() + { + return Err( + InvalidPayloadAttestation::PayloadAttestationNotCurrentSlot { + attestation_slot: payload_attestation.data.slot, + current_slot: self.fc_store.get_current_slot(), + } + .into(), + ); + } + + // Resolve validator indices to all PTC committee positions. A validator may + // appear multiple times in the PTC committee. + let mut ptc_indices = vec![]; + let mut validators_found = 0; + for validator_index in payload_attestation.attesting_indices.iter() { + let mut found = false; + for (ptc_index, &ptc_validator_index) in ptc.iter().enumerate() { + if ptc_validator_index == *validator_index as usize { + ptc_indices.push(ptc_index); + found = true; + } + } + if found { + validators_found += 1; + } + } // Check that all the attesters are in the PTC - if ptc_indices.len() != attestation.attesting_indices.len() { + if validators_found != payload_attestation.attesting_indices.len() { return Err( InvalidPayloadAttestation::PayloadAttestationAttestersNotInPtc { - attesting_indices_len: attestation.attesting_indices.len(), - attesting_indices_in_ptc: ptc_indices.len(), + attesting_indices_len: payload_attestation.attesting_indices.len(), + attesting_indices_in_ptc: validators_found, } .into(), ); @@ -1374,10 +1389,10 @@ where for &ptc_index in &ptc_indices { self.proto_array.process_payload_attestation( - attestation.data.beacon_block_root, + payload_attestation.data.beacon_block_root, ptc_index, - attestation.data.payload_present, - attestation.data.blob_data_available, + payload_attestation.data.payload_present, + payload_attestation.data.blob_data_available, )?; } @@ -1522,6 +1537,17 @@ where && self.is_finalized_checkpoint_or_descendant(*block_root) } + /// Called by the proposer to decide whether to build on the full or empty parent. + pub fn should_build_on_full( + &self, + block_root: &Hash256, + parent_payload_status: PayloadStatus, + ) -> Result> { + self.proto_array + .should_build_on_full::(block_root, parent_payload_status) + .map_err(Error::ProtoArrayStringError) + } + /// Returns whether the proposer should extend the execution payload chain of the given block. pub fn should_extend_payload(&self, block_root: &Hash256) -> Result> { let proposer_boost_root = self.fc_store.proposer_boost_root(); diff --git a/consensus/fork_choice/tests/tests.rs b/consensus/fork_choice/tests/tests.rs index 353893026b..848834b4d8 100644 --- a/consensus/fork_choice/tests/tests.rs +++ b/consensus/fork_choice/tests/tests.rs @@ -79,7 +79,7 @@ impl ForkChoiceTest { /// Get a value from the `ForkChoice` instantiation. fn get(&self, func: T) -> U where - T: Fn(&BeaconForkChoiceStore, MemoryStore>) -> U, + T: Fn(&BeaconForkChoiceStore) -> U, { func( self.harness diff --git a/consensus/proto_array/src/error.rs b/consensus/proto_array/src/error.rs index bb47af97d9..d185ed371c 100644 --- a/consensus/proto_array/src/error.rs +++ b/consensus/proto_array/src/error.rs @@ -1,3 +1,4 @@ +use crate::PayloadStatus; use safe_arith::ArithError; use types::{Checkpoint, Epoch, ExecutionBlockHash, Hash256, Slot}; @@ -62,6 +63,10 @@ pub enum Error { }, NoViableChildren, OnBlockRequiresProposerIndex, + InvalidPayloadStatus { + block_root: Hash256, + payload_status: PayloadStatus, + }, } impl From for Error { diff --git a/consensus/proto_array/src/fork_choice_test_definition.rs b/consensus/proto_array/src/fork_choice_test_definition.rs index d537f16bb2..43b76ec7cb 100644 --- a/consensus/proto_array/src/fork_choice_test_definition.rs +++ b/consensus/proto_array/src/fork_choice_test_definition.rs @@ -556,7 +556,11 @@ impl ForkChoiceTestDefinition { node_v29.payload_data_availability_votes = BitVector::from_bytes(smallvec::smallvec![fill; 64]) .expect("valid 512-bit bitvector"); - // Per spec, is_payload_timely/is_payload_data_available require + // Mark all PTC members as having participated. + node_v29.ptc_participation = + BitVector::from_bytes(smallvec::smallvec![0xFF; 64]) + .expect("valid 512-bit bitvector"); + // Per spec, payload_timeliness/payload_data_availability require // the payload to be in payload_states (payload_received). node_v29.payload_received = is_timely || is_data_available; } diff --git a/consensus/proto_array/src/proto_array.rs b/consensus/proto_array/src/proto_array.rs index 78f5026689..6ff5eabb04 100644 --- a/consensus/proto_array/src/proto_array.rs +++ b/consensus/proto_array/src/proto_array.rs @@ -155,6 +155,10 @@ pub struct ProtoNode { /// Tiebreak derived as: `num_set_bits() > ptc_size / 2`. #[superstruct(only(V29))] pub payload_data_availability_votes: BitVector, + /// Tracks which PTC members have cast a vote. + /// Bit i set means PTC member i has submitted a payload attestation. + #[superstruct(only(V29))] + pub ptc_participation: BitVector, /// Whether the execution payload for this block has been received and validated locally. /// Maps to `root in store.payload_states` in the spec. #[superstruct(only(V29), partial_getter(copy))] @@ -193,31 +197,60 @@ impl ProtoNode { } } - pub fn is_payload_timely(&self) -> bool { + /// Checks if `timely` matches our view of payload timeliness. + /// Returns whether the execution payload for the node is considered `timely` + /// (or not `timely` when `timely` is `false`), taking into consideration local + /// availability and PTC votes. + pub fn payload_timeliness(&self, timely: bool) -> Result { let Ok(node) = self.as_v29() else { - return false; + return Err(Error::InvalidNodeVariant { + block_root: self.root(), + }); }; - // Equivalent to `if root not in store.payload_states` in the spec. + // Equivalent to `if not is_payload_verified(store, root)` in the spec. if !node.payload_received { - return false; + return Ok(!timely); } - node.payload_timeliness_votes.num_set_bits() > E::payload_timely_threshold() + let matching_votes = if timely { + node.payload_timeliness_votes.num_set_bits() + } else { + // We take into consideration only participating ptc votes. An unset bit + // in `payload_timeliness_votes` could be an absent vote or a no vote. + node.ptc_participation + .num_set_bits() + .saturating_sub(node.payload_timeliness_votes.num_set_bits()) + }; + Ok(matching_votes > E::payload_timely_threshold()) } - pub fn is_payload_data_available(&self) -> bool { + /// Checks if `available` matches our view of payload data availability. + /// Return whether the blob data for the node is considered `available` + /// (or not, when `available` is `False`), taking into consideration local + /// availability and PTC votes. + pub fn payload_data_availability(&self, available: bool) -> Result { let Ok(node) = self.as_v29() else { - return false; + return Err(Error::InvalidNodeVariant { + block_root: self.root(), + }); }; - // Equivalent to `if root not in store.payload_states` in the spec. + // Equivalent to `if not is_payload_verified(store, root)` in the spec. if !node.payload_received { - return false; + return Ok(!available); } - node.payload_data_availability_votes.num_set_bits() - > E::data_availability_timely_threshold() + let matching_votes = if available { + node.payload_data_availability_votes.num_set_bits() + } else { + // We take into consideration only participating ptc votes. An unset bit + // in `payload_data_availability_votes` could be an absent vote or a no vote. + node.ptc_participation + .num_set_bits() + .saturating_sub(node.payload_data_availability_votes.num_set_bits()) + }; + Ok(matching_votes > E::data_availability_timely_threshold()) } } @@ -605,6 +638,7 @@ impl ProtoArray { execution_payload_parent_hash, payload_timeliness_votes: BitVector::default(), payload_data_availability_votes: BitVector::default(), + ptc_participation: BitVector::default(), payload_received: false, proposer_index, // Spec: `record_block_timeliness` + `get_forkchoice_store`. @@ -667,11 +701,9 @@ impl ProtoArray { justified_balances: &JustifiedBalances, spec: &ChainSpec, ) -> bool { - let reorg_threshold = calculate_committee_fraction::( - justified_balances, - spec.reorg_head_weight_threshold.unwrap_or(20), - ) - .unwrap_or(0); + let reorg_threshold = + calculate_committee_fraction::(justified_balances, spec.reorg_head_weight_threshold) + .unwrap_or(0); let head_weight = head_node .attestation_score(PayloadStatus::Pending) @@ -1501,12 +1533,46 @@ impl ProtoArray { } } + /// Called by the proposer to decide whether to build on the full or empty + /// parent pending node. Returns false if the PTC has voted the data as unavailable. + pub fn should_build_on_full( + &self, + fc_node: &IndexedForkChoiceNode, + proto_node: &ProtoNode, + ) -> Result { + if fc_node.payload_status == PayloadStatus::Pending { + return Err(Error::InvalidPayloadStatus { + block_root: proto_node.root(), + payload_status: fc_node.payload_status, + }); + } + + if fc_node.payload_status == PayloadStatus::Empty { + return Ok(false); + } + // Check that false votes have not achieved an absolute majority. This allows the payload to be + // considered available when either a majority have voted true or not enough votes have + // been cast either way. + Ok(!proto_node.payload_data_availability::(false)?) + } + pub fn should_extend_payload( &self, fc_node: &IndexedForkChoiceNode, proto_node: &ProtoNode, proposer_boost_root: Hash256, ) -> Result { + let Ok(node) = proto_node.as_v29() else { + return Err(Error::InvalidNodeVariant { + block_root: fc_node.root, + }); + }; + + // Spec equivalent to `if not is_payload_verified(store, root): return False` + if !node.payload_received { + return Ok(false); + } + // Per spec: `proposer_root == Root()` is one of the `or` conditions that // makes `should_extend_payload` return True. if proposer_boost_root.is_zero() { @@ -1531,11 +1597,10 @@ impl ProtoArray { .ok_or(Error::InvalidNodeIndex(parent_index))? .root(); - Ok( - (proto_node.is_payload_timely::() && proto_node.is_payload_data_available::()) - || proposer_boost_parent_root != fc_node.root - || proposer_boost_node.is_parent_node_full(), - ) + Ok((proto_node.payload_timeliness::(true)? + && proto_node.payload_data_availability::(true)?) + || proposer_boost_parent_root != fc_node.root + || proposer_boost_node.is_parent_node_full()) } /// Update the tree with new finalization information. The tree is only actually pruned if both diff --git a/consensus/proto_array/src/proto_array_fork_choice.rs b/consensus/proto_array/src/proto_array_fork_choice.rs index 7abba8a1f6..96d2302266 100644 --- a/consensus/proto_array/src/proto_array_fork_choice.rs +++ b/consensus/proto_array/src/proto_array_fork_choice.rs @@ -640,6 +640,9 @@ impl ProtoArrayForkChoice { .map_err(|e| { format!("process_payload_attestation: data availability set failed: {e:?}") })?; + v29.ptc_participation + .set(ptc_index, true) + .map_err(|e| format!("process_payload_attestation: participation set failed: {e:?}"))?; Ok(()) } @@ -1006,6 +1009,33 @@ impl ProtoArrayForkChoice { }) } + /// Called by the proposer to decide whether to build on the full or empty + /// parent. Returns false if the PTC has voted the data as unavailable. + pub fn should_build_on_full( + &self, + block_root: &Hash256, + parent_payload_status: PayloadStatus, + ) -> Result { + let block_index = self + .proto_array + .indices + .get(block_root) + .ok_or_else(|| format!("Unknown block root: {block_root:?}"))?; + let proto_node = self + .proto_array + .nodes + .get(*block_index) + .ok_or_else(|| format!("Missing node at index: {block_index}"))?; + let fc_node = IndexedForkChoiceNode { + root: proto_node.root(), + proto_node_index: *block_index, + payload_status: parent_payload_status, + }; + self.proto_array + .should_build_on_full::(&fc_node, proto_node) + .map_err(|e| format!("{e:?}")) + } + /// Returns whether the proposer should extend the parent's execution payload chain. /// /// This checks timeliness, data availability, and proposer boost conditions per the spec. 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 422e0afe06..f88a325d4e 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -916,25 +916,24 @@ pub fn process_deposit_requests_post_gloas( /// Check if there is a pending deposit for a new validator with the given pubkey. // TODO(gloas): cache the deposit signature validation or remove this loop entirely if possible, // it is `O(n * m)` where `n` is max 8192 and `m` is max 128M. -fn is_pending_validator( - state: &BeaconState, +pub fn is_pending_validator<'a>( + pending_deposits: impl IntoIterator, pubkey: &PublicKeyBytes, spec: &ChainSpec, -) -> Result { - for deposit in state.pending_deposits()?.iter() { - if deposit.pubkey == *pubkey { - 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() { - return Ok(true); - } - } - } - Ok(false) +) -> bool { + pending_deposits.into_iter().any(|deposit| { + deposit.pubkey == *pubkey + && is_valid_deposit_signature( + &DepositData { + pubkey: deposit.pubkey, + withdrawal_credentials: deposit.withdrawal_credentials, + amount: deposit.amount, + signature: deposit.signature.clone(), + }, + spec, + ) + .is_ok() + }) } pub fn process_deposit_request_post_gloas( @@ -964,7 +963,7 @@ pub fn process_deposit_request_post_gloas( if is_builder || (has_builder_prefix && !is_validator - && !is_pending_validator(state, &deposit_request.pubkey, spec)?) + && !is_pending_validator(state.pending_deposits()?, &deposit_request.pubkey, spec)) { // Apply builder deposits immediately apply_deposit_for_builder( @@ -1003,7 +1002,7 @@ pub fn apply_deposit_for_builder( signature: SignatureBytes, slot: Slot, spec: &ChainSpec, -) -> Result<(), BeaconStateError> { +) -> Result, BeaconStateError> { match builder_index_opt { None => { // Verify the deposit signature (proof of possession) which is not checked by the deposit contract @@ -1014,13 +1013,16 @@ pub fn apply_deposit_for_builder( signature, }; if is_valid_deposit_signature(&deposit_data, spec).is_ok() { - state.add_builder_to_registry( + let builder_index = state.add_builder_to_registry( pubkey, withdrawal_credentials, amount, slot, spec, )?; + Ok(Some(builder_index)) + } else { + Ok(None) } } Some(builder_index) => { @@ -1030,9 +1032,9 @@ pub fn apply_deposit_for_builder( .ok_or(BeaconStateError::UnknownBuilder(builder_index))? .balance .safe_add_assign(amount)?; + Ok(Some(builder_index)) } } - Ok(()) } // Make sure to build the pubkey cache before calling this function 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 0686c4d605..f56cb17554 100644 --- a/consensus/state_processing/src/per_block_processing/signature_sets.rs +++ b/consensus/state_processing/src/per_block_processing/signature_sets.rs @@ -363,6 +363,26 @@ pub fn indexed_payload_attestation_signature_set<'a, 'b, E, F>( indexed_payload_attestation: &'b IndexedPayloadAttestation, spec: &'a ChainSpec, ) -> Result> +where + E: EthSpec, + F: Fn(usize) -> Option>, +{ + indexed_payload_attestation_signature_set_from_pubkeys( + get_pubkey, + signature, + indexed_payload_attestation, + state.genesis_validators_root(), + spec, + ) +} + +pub fn indexed_payload_attestation_signature_set_from_pubkeys<'a, 'b, E, F>( + get_pubkey: F, + signature: &'a AggregateSignature, + indexed_payload_attestation: &'b IndexedPayloadAttestation, + genesis_validators_root: Hash256, + spec: &'a ChainSpec, +) -> Result> where E: EthSpec, F: Fn(usize) -> Option>, @@ -378,12 +398,8 @@ where .data .slot .epoch(E::slots_per_epoch()); - let domain = spec.get_domain( - epoch, - Domain::PTCAttester, - &state.fork(), - state.genesis_validators_root(), - ); + let fork = spec.fork_at_epoch(epoch); + let domain = spec.get_domain(epoch, Domain::PTCAttester, &fork, genesis_validators_root); let message = indexed_payload_attestation.data.signing_root(domain); diff --git a/consensus/state_processing/src/upgrade/gloas.rs b/consensus/state_processing/src/upgrade/gloas.rs index 84cdbf22c2..c26547e304 100644 --- a/consensus/state_processing/src/upgrade/gloas.rs +++ b/consensus/state_processing/src/upgrade/gloas.rs @@ -1,18 +1,16 @@ -use crate::per_block_processing::{ - is_valid_deposit_signature, process_operations::apply_deposit_for_builder, -}; +use crate::per_block_processing::process_operations::apply_deposit_for_builder; +use crate::per_block_processing::process_operations::is_pending_validator; use milhouse::{List, Vector}; use safe_arith::SafeArith; use ssz_types::BitVector; use ssz_types::FixedVector; -use std::collections::HashSet; +use std::collections::HashMap; use std::mem; use tree_hash::TreeHash; use typenum::Unsigned; use types::{ BeaconState, BeaconStateError as Error, BeaconStateGloas, BuilderPendingPayment, ChainSpec, - DepositData, EthSpec, ExecutionPayloadBid, ExecutionRequests, Fork, - is_builder_withdrawal_credential, + EthSpec, ExecutionPayloadBid, ExecutionRequests, Fork, is_builder_withdrawal_credential, }; /// Transform a `Fulu` state into a `Gloas` state. @@ -80,6 +78,7 @@ pub fn upgrade_state_to_gloas( // Execution Bid latest_execution_payload_bid: ExecutionPayloadBid { block_hash: pre.latest_execution_payload_header.block_hash, + gas_limit: pre.latest_execution_payload_header.gas_limit, execution_requests_root: ExecutionRequests::::default().tree_hash_root(), ..Default::default() }, @@ -167,66 +166,57 @@ 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(); + // TODO(gloas): introduce a global builder pubkey cache, see: + // https://github.com/sigp/lighthouse/issues/8783 + let mut builder_pubkey_to_index = state + .builders()? + .iter() + .enumerate() + .map(|(i, b)| (b.pubkey, i as u64)) + .collect::>(); + 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() - { + if 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); + if !builder_pubkey_to_index.contains_key(&deposit.pubkey) { + // Deposits without builder withdrawal credentials are for new validators. + if !is_builder_withdrawal_credential(deposit.withdrawal_credentials, spec) { + pending_deposits.push(deposit.clone())?; + continue; + } - 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 valid pending deposit for a new validator with this pubkey, + // keep this deposit in the pending queue to be applied to that validator later. + if is_pending_validator(&pending_deposits, &deposit.pubkey, spec) { + pending_deposits.push(deposit.clone())?; + 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())?; + let builder_index = builder_pubkey_to_index.get(&deposit.pubkey).copied(); + + if let Some(new_builder_index) = apply_deposit_for_builder( + state, + builder_index, + deposit.pubkey, + deposit.withdrawal_credentials, + deposit.amount, + deposit.signature.clone(), + deposit.slot, + spec, + )? { + builder_pubkey_to_index + .entry(deposit.pubkey) + .or_insert(new_builder_index); } } diff --git a/consensus/types/Cargo.toml b/consensus/types/Cargo.toml index 9ee827c7b9..8d991163d2 100644 --- a/consensus/types/Cargo.toml +++ b/consensus/types/Cargo.toml @@ -44,6 +44,7 @@ merkle_proof = { workspace = true } metastruct = "0.1.0" milhouse = { workspace = true } parking_lot = { workspace = true } +paste = { workspace = true } rand = { workspace = true } rand_xorshift = { workspace = true } rayon = { workspace = true } @@ -67,7 +68,6 @@ yaml_serde = { workspace = true } [dev-dependencies] beacon_chain = { workspace = true } criterion = { workspace = true } -paste = { workspace = true } state_processing = { workspace = true } tokio = { workspace = true } types = { path = ".", features = ["arbitrary"] } diff --git a/consensus/types/configs/mainnet.yaml b/consensus/types/configs/mainnet.yaml index 25bf872a7a..743384bcc9 100644 --- a/consensus/types/configs/mainnet.yaml +++ b/consensus/types/configs/mainnet.yaml @@ -91,8 +91,8 @@ SYNC_MESSAGE_DUE_BPS: 3333 CONTRIBUTION_DUE_BPS: 6667 # Gloas -# 2**6 (= 64) epochs -MIN_BUILDER_WITHDRAWABILITY_DELAY: 64 +# 2**13 (= 8192) epochs +MIN_BUILDER_WITHDRAWABILITY_DELAY: 8192 # 2500 basis points, 25% of SLOT_DURATION_MS ATTESTATION_DUE_BPS_GLOAS: 2500 # 5000 basis points, 50% of SLOT_DURATION_MS diff --git a/consensus/types/src/block/signed_beacon_block.rs b/consensus/types/src/block/signed_beacon_block.rs index 11ac17dece..1a87a519d0 100644 --- a/consensus/types/src/block/signed_beacon_block.rs +++ b/consensus/types/src/block/signed_beacon_block.rs @@ -433,285 +433,85 @@ impl From>> } // Post-Bellatrix blocks can be "unblinded" by adding the full payload. -// NOTE: It might be nice to come up with a `superstruct` pattern to abstract over this before -// the first fork after Bellatrix. -impl SignedBeaconBlockBellatrix> { - pub fn into_full_block( - self, - execution_payload: ExecutionPayloadBellatrix, - ) -> SignedBeaconBlockBellatrix> { - let SignedBeaconBlockBellatrix { - message: - BeaconBlockBellatrix { - slot, - proposer_index, - parent_root, - state_root, - body: - BeaconBlockBodyBellatrix { - randao_reveal, - eth1_data, - graffiti, - proposer_slashings, - attester_slashings, - attestations, - deposits, - voluntary_exits, - sync_aggregate, - execution_payload: BlindedPayloadBellatrix { .. }, +macro_rules! impl_into_full_block { + ($fork:ident, [ $($extra_field:ident),* $(,)? ]) => { + paste::paste! { + impl []> { + pub fn into_full_block( + self, + execution_payload: [], + ) -> []> { + let [] { + message: + [] { + slot, + proposer_index, + parent_root, + state_root, + body: + [] { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + execution_payload: [] { .. }, + $($extra_field,)* + }, + }, + signature, + } = self; + [] { + message: [] { + slot, + proposer_index, + parent_root, + state_root, + body: [] { + randao_reveal, + eth1_data, + graffiti, + proposer_slashings, + attester_slashings, + attestations, + deposits, + voluntary_exits, + sync_aggregate, + execution_payload: [] { execution_payload }, + $($extra_field,)* + }, }, - }, - signature, - } = self; - SignedBeaconBlockBellatrix { - message: BeaconBlockBellatrix { - slot, - proposer_index, - parent_root, - state_root, - body: BeaconBlockBodyBellatrix { - randao_reveal, - eth1_data, - graffiti, - proposer_slashings, - attester_slashings, - attestations, - deposits, - voluntary_exits, - sync_aggregate, - execution_payload: FullPayloadBellatrix { execution_payload }, - }, - }, - signature, + signature, + } + } + } } - } + }; } -impl SignedBeaconBlockCapella> { - pub fn into_full_block( - self, - execution_payload: ExecutionPayloadCapella, - ) -> SignedBeaconBlockCapella> { - let SignedBeaconBlockCapella { - message: - BeaconBlockCapella { - slot, - proposer_index, - parent_root, - state_root, - body: - BeaconBlockBodyCapella { - randao_reveal, - eth1_data, - graffiti, - proposer_slashings, - attester_slashings, - attestations, - deposits, - voluntary_exits, - sync_aggregate, - execution_payload: BlindedPayloadCapella { .. }, - bls_to_execution_changes, - }, - }, - signature, - } = self; - SignedBeaconBlockCapella { - message: BeaconBlockCapella { - slot, - proposer_index, - parent_root, - state_root, - body: BeaconBlockBodyCapella { - randao_reveal, - eth1_data, - graffiti, - proposer_slashings, - attester_slashings, - attestations, - deposits, - voluntary_exits, - sync_aggregate, - execution_payload: FullPayloadCapella { execution_payload }, - bls_to_execution_changes, - }, - }, - signature, - } - } -} - -impl SignedBeaconBlockDeneb> { - pub fn into_full_block( - self, - execution_payload: ExecutionPayloadDeneb, - ) -> SignedBeaconBlockDeneb> { - let SignedBeaconBlockDeneb { - message: - BeaconBlockDeneb { - slot, - proposer_index, - parent_root, - state_root, - body: - BeaconBlockBodyDeneb { - randao_reveal, - eth1_data, - graffiti, - proposer_slashings, - attester_slashings, - attestations, - deposits, - voluntary_exits, - sync_aggregate, - execution_payload: BlindedPayloadDeneb { .. }, - bls_to_execution_changes, - blob_kzg_commitments, - }, - }, - signature, - } = self; - SignedBeaconBlockDeneb { - message: BeaconBlockDeneb { - slot, - proposer_index, - parent_root, - state_root, - body: BeaconBlockBodyDeneb { - randao_reveal, - eth1_data, - graffiti, - proposer_slashings, - attester_slashings, - attestations, - deposits, - voluntary_exits, - sync_aggregate, - execution_payload: FullPayloadDeneb { execution_payload }, - bls_to_execution_changes, - blob_kzg_commitments, - }, - }, - signature, - } - } -} - -impl SignedBeaconBlockElectra> { - pub fn into_full_block( - self, - execution_payload: ExecutionPayloadElectra, - ) -> SignedBeaconBlockElectra> { - let SignedBeaconBlockElectra { - message: - BeaconBlockElectra { - slot, - proposer_index, - parent_root, - state_root, - body: - BeaconBlockBodyElectra { - randao_reveal, - eth1_data, - graffiti, - proposer_slashings, - attester_slashings, - attestations, - deposits, - voluntary_exits, - sync_aggregate, - execution_payload: BlindedPayloadElectra { .. }, - bls_to_execution_changes, - blob_kzg_commitments, - execution_requests, - }, - }, - signature, - } = self; - SignedBeaconBlockElectra { - message: BeaconBlockElectra { - slot, - proposer_index, - parent_root, - state_root, - body: BeaconBlockBodyElectra { - randao_reveal, - eth1_data, - graffiti, - proposer_slashings, - attester_slashings, - attestations, - deposits, - voluntary_exits, - sync_aggregate, - execution_payload: FullPayloadElectra { execution_payload }, - bls_to_execution_changes, - blob_kzg_commitments, - execution_requests, - }, - }, - signature, - } - } -} - -impl SignedBeaconBlockFulu> { - pub fn into_full_block( - self, - execution_payload: ExecutionPayloadFulu, - ) -> SignedBeaconBlockFulu> { - let SignedBeaconBlockFulu { - message: - BeaconBlockFulu { - slot, - proposer_index, - parent_root, - state_root, - body: - BeaconBlockBodyFulu { - randao_reveal, - eth1_data, - graffiti, - proposer_slashings, - attester_slashings, - attestations, - deposits, - voluntary_exits, - sync_aggregate, - execution_payload: BlindedPayloadFulu { .. }, - bls_to_execution_changes, - blob_kzg_commitments, - execution_requests, - }, - }, - signature, - } = self; - SignedBeaconBlockFulu { - message: BeaconBlockFulu { - slot, - proposer_index, - parent_root, - state_root, - body: BeaconBlockBodyFulu { - randao_reveal, - eth1_data, - graffiti, - proposer_slashings, - attester_slashings, - attestations, - deposits, - voluntary_exits, - sync_aggregate, - execution_payload: FullPayloadFulu { execution_payload }, - bls_to_execution_changes, - blob_kzg_commitments, - execution_requests, - }, - }, - signature, - } - } -} +impl_into_full_block!(Bellatrix, []); +impl_into_full_block!(Capella, [bls_to_execution_changes]); +impl_into_full_block!(Deneb, [bls_to_execution_changes, blob_kzg_commitments]); +impl_into_full_block!( + Electra, + [ + bls_to_execution_changes, + blob_kzg_commitments, + execution_requests + ] +); +impl_into_full_block!( + Fulu, + [ + bls_to_execution_changes, + blob_kzg_commitments, + execution_requests + ] +); // We can convert gloas blocks without payloads into blocks "with" payloads. // TODO(EIP-7732) Look into whether we can remove this in the future since no blinded blocks post-gloas diff --git a/consensus/types/src/builder/proposer_preferences.rs b/consensus/types/src/builder/proposer_preferences.rs index e3773e333d..4f27020105 100644 --- a/consensus/types/src/builder/proposer_preferences.rs +++ b/consensus/types/src/builder/proposer_preferences.rs @@ -16,7 +16,7 @@ pub struct ProposerPreferences { pub proposal_slot: Slot, pub validator_index: u64, pub fee_recipient: Address, - pub gas_limit: u64, + pub target_gas_limit: u64, } impl SignedRoot for ProposerPreferences {} diff --git a/consensus/types/src/core/chain_spec.rs b/consensus/types/src/core/chain_spec.rs index c54d032891..25dcb4ba06 100644 --- a/consensus/types/src/core/chain_spec.rs +++ b/consensus/types/src/core/chain_spec.rs @@ -108,6 +108,7 @@ pub struct ChainSpec { pub proposer_reorg_cutoff_bps: u64, pub attestation_due_bps: u64, pub attestation_due_bps_gloas: u64, + pub payload_due_bps: u64, pub payload_attestation_due_bps: u64, pub aggregate_due_bps: u64, pub sync_message_due_bps: u64, @@ -118,6 +119,7 @@ pub struct ChainSpec { */ pub unaggregated_attestation_due: Duration, pub unaggregated_attestation_due_gloas: Duration, + pub payload_due: Duration, pub payload_attestation_due: Duration, pub aggregate_attestation_due: Duration, pub sync_message_due: Duration, @@ -150,9 +152,9 @@ pub struct ChainSpec { * Fork choice */ pub proposer_score_boost: Option, - pub reorg_head_weight_threshold: Option, - pub reorg_parent_weight_threshold: Option, - pub reorg_max_epochs_since_finalization: Option, + pub reorg_head_weight_threshold: u64, + pub reorg_parent_weight_threshold: u64, + pub reorg_max_epochs_since_finalization: u64, /* * Eth1 @@ -894,6 +896,11 @@ impl ChainSpec { } } + /// Spec: `get_payload_due_ms`. + pub fn get_payload_due(&self) -> Duration { + self.payload_due + } + /// Spec: `get_payload_attestation_due_ms`. pub fn get_payload_attestation_due(&self) -> Duration { self.payload_attestation_due @@ -918,7 +925,7 @@ impl ChainSpec { } /// Calculate the duration into a slot for a given slot component - fn compute_slot_component_duration( + pub fn compute_slot_component_duration( &self, component_basis_points: u64, ) -> Result { @@ -974,6 +981,9 @@ impl ChainSpec { self.unaggregated_attestation_due_gloas = self .compute_slot_component_duration(self.attestation_due_bps_gloas) .expect("invalid chain spec: cannot compute unaggregated_attestation_due_gloas"); + self.payload_due = self + .compute_slot_component_duration(self.payload_due_bps) + .expect("invalid chain spec: cannot compute payload_due"); self.payload_attestation_due = self .compute_slot_component_duration(self.payload_attestation_due_bps) .expect("invalid chain spec: cannot compute payload_attestation_due"); @@ -1108,6 +1118,7 @@ impl ChainSpec { proposer_reorg_cutoff_bps: 1667, attestation_due_bps: 3333, attestation_due_bps_gloas: 2500, + payload_due_bps: 7500, payload_attestation_due_bps: 7500, aggregate_due_bps: 6667, sync_message_due_bps: 3333, @@ -1118,6 +1129,7 @@ impl ChainSpec { */ unaggregated_attestation_due: Duration::from_millis(3999), unaggregated_attestation_due_gloas: Duration::from_millis(3000), + payload_due: Duration::from_millis(9000), payload_attestation_due: Duration::from_millis(9000), aggregate_attestation_due: Duration::from_millis(8000), sync_message_due: Duration::from_millis(3999), @@ -1151,9 +1163,9 @@ impl ChainSpec { * Fork choice */ proposer_score_boost: Some(40), - reorg_head_weight_threshold: Some(20), - reorg_parent_weight_threshold: Some(160), - reorg_max_epochs_since_finalization: Some(2), + reorg_head_weight_threshold: 20, + reorg_parent_weight_threshold: 160, + reorg_max_epochs_since_finalization: 2, /* * Eth1 @@ -1270,7 +1282,7 @@ impl ChainSpec { gloas_fork_epoch: None, builder_payment_threshold_numerator: 6, builder_payment_threshold_denominator: 10, - min_builder_withdrawability_delay: Epoch::new(64), + min_builder_withdrawability_delay: Epoch::new(8192), churn_limit_quotient_gloas: option_wrapper(|| u64::checked_pow(2, 15)) .expect("calculation does not overflow"), consolidation_churn_limit_quotient: option_wrapper(|| u64::checked_pow(2, 16)) @@ -1440,6 +1452,7 @@ impl ChainSpec { */ unaggregated_attestation_due: Duration::from_millis(1999), unaggregated_attestation_due_gloas: Duration::from_millis(1500), + payload_due: Duration::from_millis(4500), payload_attestation_due: Duration::from_millis(4500), aggregate_attestation_due: Duration::from_millis(4000), sync_message_due: Duration::from_millis(1999), @@ -1531,6 +1544,7 @@ impl ChainSpec { proposer_reorg_cutoff_bps: 1667, attestation_due_bps: 3333, attestation_due_bps_gloas: 2500, + payload_due_bps: 7500, payload_attestation_due_bps: 7500, aggregate_due_bps: 6667, @@ -1540,6 +1554,7 @@ impl ChainSpec { */ unaggregated_attestation_due: Duration::from_millis(1666), unaggregated_attestation_due_gloas: Duration::from_millis(1250), + payload_due: Duration::from_millis(3750), payload_attestation_due: Duration::from_millis(3750), aggregate_attestation_due: Duration::from_millis(3333), sync_message_due: Duration::from_millis(1666), @@ -1573,9 +1588,9 @@ impl ChainSpec { * Fork choice */ proposer_score_boost: Some(40), - reorg_head_weight_threshold: Some(20), - reorg_parent_weight_threshold: Some(160), - reorg_max_epochs_since_finalization: Some(2), + reorg_head_weight_threshold: 20, + reorg_parent_weight_threshold: 160, + reorg_max_epochs_since_finalization: 2, /* * Eth1 @@ -1693,7 +1708,7 @@ impl ChainSpec { gloas_fork_epoch: None, builder_payment_threshold_numerator: 6, builder_payment_threshold_denominator: 10, - min_builder_withdrawability_delay: Epoch::new(64), + min_builder_withdrawability_delay: Epoch::new(8192), churn_limit_quotient_gloas: option_wrapper(|| u64::checked_pow(2, 15)) .expect("calculation does not overflow"), consolidation_churn_limit_quotient: option_wrapper(|| u64::checked_pow(2, 16)) @@ -2013,12 +2028,15 @@ pub struct Config { #[serde(skip_serializing_if = "Option::is_none")] proposer_score_boost: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - reorg_head_weight_threshold: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - reorg_parent_weight_threshold: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - reorg_max_epochs_since_finalization: Option>, + #[serde(default = "default_reorg_head_weight_threshold")] + #[serde(with = "serde_utils::quoted_u64")] + reorg_head_weight_threshold: u64, + #[serde(default = "default_reorg_parent_weight_threshold")] + #[serde(with = "serde_utils::quoted_u64")] + reorg_parent_weight_threshold: u64, + #[serde(default = "default_reorg_max_epochs_since_finalization")] + #[serde(with = "serde_utils::quoted_u64")] + reorg_max_epochs_since_finalization: u64, #[serde(with = "serde_utils::quoted_u64")] deposit_chain_id: u64, @@ -2136,6 +2154,9 @@ pub struct Config { #[serde(default = "default_attestation_due_bps_gloas")] #[serde(with = "serde_utils::quoted_u64")] attestation_due_bps_gloas: u64, + #[serde(default = "default_payload_due_bps")] + #[serde(with = "serde_utils::quoted_u64")] + payload_due_bps: u64, #[serde(default = "default_payload_attestation_due_bps")] #[serde(with = "serde_utils::quoted_u64")] payload_attestation_due_bps: u64, @@ -2379,6 +2400,10 @@ const fn default_attestation_due_bps_gloas() -> u64 { 2500 } +const fn default_payload_due_bps() -> u64 { + 7500 +} + const fn default_payload_attestation_due_bps() -> u64 { 7500 } @@ -2396,7 +2421,7 @@ const fn default_contribution_due_bps() -> u64 { } const fn default_min_builder_withdrawability_delay() -> u64 { - 64 + 8192 } const fn default_churn_limit_quotient_gloas() -> u64 { @@ -2411,6 +2436,18 @@ const fn default_max_per_epoch_activation_churn_limit_gloas() -> u64 { 256_000_000_000 } +const fn default_reorg_head_weight_threshold() -> u64 { + 20 +} + +const fn default_reorg_parent_weight_threshold() -> u64 { + 160 +} + +const fn default_reorg_max_epochs_since_finalization() -> u64 { + 2 +} + fn max_blocks_by_root_request_common(max_request_blocks: u64) -> usize { let max_request_blocks = max_request_blocks as usize; RuntimeVariableList::::new( @@ -2604,15 +2641,9 @@ impl Config { max_per_epoch_activation_churn_limit: spec.max_per_epoch_activation_churn_limit, proposer_score_boost: spec.proposer_score_boost.map(|value| MaybeQuoted { value }), - reorg_head_weight_threshold: spec - .reorg_head_weight_threshold - .map(|value| MaybeQuoted { value }), - reorg_parent_weight_threshold: spec - .reorg_parent_weight_threshold - .map(|value| MaybeQuoted { value }), - reorg_max_epochs_since_finalization: spec - .reorg_max_epochs_since_finalization - .map(|value| MaybeQuoted { value }), + reorg_head_weight_threshold: spec.reorg_head_weight_threshold, + reorg_parent_weight_threshold: spec.reorg_parent_weight_threshold, + reorg_max_epochs_since_finalization: spec.reorg_max_epochs_since_finalization, deposit_chain_id: spec.deposit_chain_id, deposit_network_id: spec.deposit_network_id, @@ -2656,6 +2687,7 @@ impl Config { proposer_reorg_cutoff_bps: spec.proposer_reorg_cutoff_bps, attestation_due_bps: spec.attestation_due_bps, attestation_due_bps_gloas: spec.attestation_due_bps_gloas, + payload_due_bps: spec.payload_due_bps, payload_attestation_due_bps: spec.payload_attestation_due_bps, aggregate_due_bps: spec.aggregate_due_bps, sync_message_due_bps: spec.sync_message_due_bps, @@ -2759,6 +2791,7 @@ impl Config { proposer_reorg_cutoff_bps, attestation_due_bps, attestation_due_bps_gloas, + payload_due_bps, payload_attestation_due_bps, aggregate_due_bps, sync_message_due_bps, @@ -2822,10 +2855,9 @@ impl Config { max_per_epoch_activation_churn_limit, churn_limit_quotient, proposer_score_boost: proposer_score_boost.map(|q| q.value), - reorg_head_weight_threshold: reorg_head_weight_threshold.map(|q| q.value), - reorg_parent_weight_threshold: reorg_parent_weight_threshold.map(|q| q.value), - reorg_max_epochs_since_finalization: reorg_max_epochs_since_finalization - .map(|q| q.value), + reorg_head_weight_threshold, + reorg_parent_weight_threshold, + reorg_max_epochs_since_finalization, deposit_chain_id, deposit_network_id, deposit_contract_address, @@ -2867,6 +2899,7 @@ impl Config { proposer_reorg_cutoff_bps, attestation_due_bps, attestation_due_bps_gloas, + payload_due_bps, payload_attestation_due_bps, aggregate_due_bps, sync_message_due_bps, @@ -3694,6 +3727,30 @@ mod yaml_tests { let custom_spec = custom_spec.compute_derived_values::(); let tiny_due = custom_spec.get_unaggregated_attestation_due(); assert_eq!(tiny_due, Duration::from_millis(1)); // 12000 * 1 / 10000 = 1.2 -> 1 + + // Test payload due (7500 bps = 75% of 12s = 9s) + let spec = ChainSpec::mainnet().compute_derived_values::(); + let payload_due = spec.get_payload_due(); + assert_eq!(payload_due, Duration::from_millis(9000)); // 12000 * 7500 / 10000 + + // Test payload attestation due (7500 bps = 75% of 12s = 9s) + let payload_att_due = spec.get_payload_attestation_due(); + assert_eq!(payload_att_due, Duration::from_millis(9000)); // 12000 * 7500 / 10000 + + // Test gloas attestation due (2500 bps = 25% of 12s = 3s) + assert_eq!( + spec.unaggregated_attestation_due_gloas, + Duration::from_millis(3000) + ); // 12000 * 2500 / 10000 + + // Test gloas with custom bps + let mut custom_spec = spec; + custom_spec.attestation_due_bps_gloas = 5000; + let custom_spec = custom_spec.compute_derived_values::(); + assert_eq!( + custom_spec.unaggregated_attestation_due_gloas, + Duration::from_millis(6000) + ); // 12000 * 5000 / 10000 } #[test] @@ -3715,6 +3772,19 @@ mod yaml_tests { Duration::from_millis(8000) ); + // Mainnet payload due: 12000ms slots, 7500 bps = 9000ms + assert_eq!(mainnet.get_payload_due(), Duration::from_millis(9000)); + assert_eq!( + mainnet.get_payload_attestation_due(), + Duration::from_millis(9000) + ); + + // Mainnet gloas: 12000ms slots, 2500 bps = 3000ms + assert_eq!( + mainnet.unaggregated_attestation_due_gloas, + Duration::from_millis(3000) + ); + // Minimal spec: 6000ms slots, 3333 bps = 1999ms, 6667 bps = 4000ms let minimal = ChainSpec::minimal(); assert_eq!( @@ -3730,6 +3800,18 @@ mod yaml_tests { minimal.get_contribution_message_due(), Duration::from_millis(4000) ); + // Minimal payload due: 6000ms slots, 7500 bps = 4500ms + assert_eq!(minimal.get_payload_due(), Duration::from_millis(4500)); + assert_eq!( + minimal.get_payload_attestation_due(), + Duration::from_millis(4500) + ); + + // Minimal gloas: 6000ms slots, 2500 bps = 1500ms + assert_eq!( + minimal.unaggregated_attestation_due_gloas, + Duration::from_millis(1500) + ); // Gnosis spec: 5000ms slots, 3333 bps = 1666ms, 6667 bps = 3333ms let gnosis = ChainSpec::gnosis(); @@ -3746,6 +3828,18 @@ mod yaml_tests { gnosis.get_contribution_message_due(), Duration::from_millis(3333) ); + // Gnosis payload due: 5000ms slots, 7500 bps = 3750ms + assert_eq!(gnosis.get_payload_due(), Duration::from_millis(3750)); + assert_eq!( + gnosis.get_payload_attestation_due(), + Duration::from_millis(3750) + ); + + // Gnosis gloas: 5000ms slots, 2500 bps = 1250ms + assert_eq!( + gnosis.unaggregated_attestation_due_gloas, + Duration::from_millis(1250) + ); } #[test] diff --git a/consensus/types/src/data/partial_data_column_sidecar.rs b/consensus/types/src/data/partial_data_column_sidecar.rs index c0e713b4b8..e70901d76e 100644 --- a/consensus/types/src/data/partial_data_column_sidecar.rs +++ b/consensus/types/src/data/partial_data_column_sidecar.rs @@ -69,7 +69,7 @@ impl PartialDataColumnSidecar { .count(); self.column .get(storage_idx) - .and_then(|cell| self.kzg_proofs.get(storage_idx).map(|proof| (cell, proof))) + .zip(self.kzg_proofs.get(storage_idx)) } /// Creates a reference to this sidecar containing only the blob indices for which the passed diff --git a/consensus/types/src/state/beacon_state.rs b/consensus/types/src/state/beacon_state.rs index 4d2c7533ca..027acfab7f 100644 --- a/consensus/types/src/state/beacon_state.rs +++ b/consensus/types/src/state/beacon_state.rs @@ -1333,6 +1333,7 @@ impl BeaconState { pub fn is_valid_proposal_slot( &self, preferences: &ProposerPreferences, + spec: &ChainSpec, ) -> Result { let current_epoch = self.current_epoch(); let proposal_epoch = preferences.proposal_slot.epoch(E::slots_per_epoch()); @@ -1341,8 +1342,7 @@ impl BeaconState { return Ok(false); } - let next_epoch = current_epoch.saturating_add(1u64); - if proposal_epoch > next_epoch { + if proposal_epoch > current_epoch.saturating_add(spec.min_seed_lookahead) { return Ok(false); } diff --git a/database_manager/src/lib.rs b/database_manager/src/lib.rs index 608400fa7e..2509b500e0 100644 --- a/database_manager/src/lib.rs +++ b/database_manager/src/lib.rs @@ -55,7 +55,7 @@ pub fn display_db_version( let blobs_path = client_config.get_blobs_db_path(); let mut version = CURRENT_SCHEMA_VERSION; - HotColdDB::, BeaconNodeBackend>::open( + HotColdDB::::open( &hot_path, &cold_path, &blobs_path, @@ -143,13 +143,13 @@ pub fn inspect_db( let mut num_keys = 0; let sub_db = if inspect_config.freezer { - BeaconNodeBackend::::open(&client_config.store, &cold_path) + BeaconNodeBackend::open(&client_config.store, &cold_path) .map_err(|e| format!("Unable to open freezer DB: {e:?}"))? } else if inspect_config.blobs_db { - BeaconNodeBackend::::open(&client_config.store, &blobs_path) + BeaconNodeBackend::open(&client_config.store, &blobs_path) .map_err(|e| format!("Unable to open blobs DB: {e:?}"))? } else { - BeaconNodeBackend::::open(&client_config.store, &hot_path) + BeaconNodeBackend::open(&client_config.store, &hot_path) .map_err(|e| format!("Unable to open hot DB: {e:?}"))? }; @@ -264,17 +264,17 @@ pub fn compact_db( let (sub_db, db_name) = if compact_config.freezer { ( - BeaconNodeBackend::::open(&client_config.store, &cold_path)?, + BeaconNodeBackend::open(&client_config.store, &cold_path)?, "freezer_db", ) } else if compact_config.blobs_db { ( - BeaconNodeBackend::::open(&client_config.store, &blobs_path)?, + BeaconNodeBackend::open(&client_config.store, &blobs_path)?, "blobs_db", ) } else { ( - BeaconNodeBackend::::open(&client_config.store, &hot_path)?, + BeaconNodeBackend::open(&client_config.store, &hot_path)?, "hot_db", ) }; @@ -309,7 +309,7 @@ pub fn migrate_db( let mut from = CURRENT_SCHEMA_VERSION; let to = migrate_config.to; - let db = HotColdDB::, BeaconNodeBackend>::open( + let db = HotColdDB::::open( &hot_path, &cold_path, &blobs_path, @@ -339,7 +339,7 @@ pub fn prune_payloads( let cold_path = client_config.get_freezer_db_path(); let blobs_path = client_config.get_blobs_db_path(); - let db = HotColdDB::, BeaconNodeBackend>::open( + let db = HotColdDB::::open( &hot_path, &cold_path, &blobs_path, @@ -363,7 +363,7 @@ pub fn prune_blobs( let cold_path = client_config.get_freezer_db_path(); let blobs_path = client_config.get_blobs_db_path(); - let db = HotColdDB::, BeaconNodeBackend>::open( + let db = HotColdDB::::open( &hot_path, &cold_path, &blobs_path, @@ -398,7 +398,7 @@ pub fn prune_states( let cold_path = client_config.get_freezer_db_path(); let blobs_path = client_config.get_blobs_db_path(); - let db = HotColdDB::, BeaconNodeBackend>::open( + let db = HotColdDB::::open( &hot_path, &cold_path, &blobs_path, diff --git a/lighthouse/Cargo.toml b/lighthouse/Cargo.toml index 3595cf04e7..09fd6d4afe 100644 --- a/lighthouse/Cargo.toml +++ b/lighthouse/Cargo.toml @@ -37,6 +37,8 @@ beacon-node-redb = ["store/redb"] console-subscriber = ["console-subscriber/default"] # Force the use of the system memory allocator rather than jemalloc. sysmalloc = ["malloc_utils/sysmalloc"] +# Enable jemalloc heap profiling support. +jemalloc-profiling = ["malloc_utils/jemalloc-profiling"] [dependencies] account_manager = { "path" = "../account_manager" } diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 0c5d9a5933..38d4275a02 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -1,8 +1,6 @@ use crate::exec::{CommandLineTestExec, CompletedTest}; use beacon_node::beacon_chain::chain_config::{ - DEFAULT_RE_ORG_CUTOFF_DENOMINATOR, DEFAULT_RE_ORG_HEAD_THRESHOLD, - DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, DEFAULT_SYNC_TOLERANCE_EPOCHS, - DisallowedReOrgOffsets, + DEFAULT_SYNC_TOLERANCE_EPOCHS, DisallowedReOrgOffsets, }; use beacon_node::beacon_chain::custody_context::NodeCustodyType; use beacon_node::{ @@ -2344,19 +2342,12 @@ fn ensure_panic_on_failed_launch() { fn enable_proposer_re_orgs_default() { CommandLineTest::new() .run_with_zero_port() - .with_config(|config| { - assert_eq!( - config.chain.re_org_head_threshold, - Some(DEFAULT_RE_ORG_HEAD_THRESHOLD) - ); - assert_eq!( - config.chain.re_org_max_epochs_since_finalization, - DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, - ); - assert_eq!( - config.chain.re_org_cutoff(Duration::from_secs(12)), - Duration::from_secs(12) / DEFAULT_RE_ORG_CUTOFF_DENOMINATOR - ); + .with_config_and_spec::(|config, spec| { + assert!(!config.chain.disable_proposer_reorg); + assert_eq!(spec.reorg_head_weight_threshold, 20); + assert_eq!(spec.reorg_parent_weight_threshold, 160); + assert_eq!(spec.reorg_max_epochs_since_finalization, 2); + assert_eq!(spec.proposer_reorg_cutoff_bps, 1667); }); } @@ -2365,52 +2356,8 @@ fn disable_proposer_re_orgs() { CommandLineTest::new() .flag("disable-proposer-reorgs", None) .run_with_zero_port() - .with_config(|config| { - assert_eq!(config.chain.re_org_head_threshold, None); - assert_eq!(config.chain.re_org_parent_threshold, None) - }); -} - -#[test] -fn proposer_re_org_parent_threshold() { - CommandLineTest::new() - .flag("proposer-reorg-parent-threshold", Some("90")) - .run_with_zero_port() - .with_config(|config| assert_eq!(config.chain.re_org_parent_threshold.unwrap().0, 90)); -} - -#[test] -fn proposer_re_org_head_threshold() { - CommandLineTest::new() - .flag("proposer-reorg-threshold", Some("90")) - .run_with_zero_port() - .with_config(|config| assert_eq!(config.chain.re_org_head_threshold.unwrap().0, 90)); -} - -#[test] -fn proposer_re_org_max_epochs_since_finalization() { - CommandLineTest::new() - .flag("proposer-reorg-epochs-since-finalization", Some("8")) - .run_with_zero_port() - .with_config(|config| { - assert_eq!( - config.chain.re_org_max_epochs_since_finalization.as_u64(), - 8 - ) - }); -} - -#[test] -fn proposer_re_org_cutoff() { - CommandLineTest::new() - .flag("proposer-reorg-cutoff", Some("500")) - .run_with_zero_port() - .with_config(|config| { - assert_eq!( - config.chain.re_org_cutoff(Duration::from_secs(12)), - Duration::from_millis(500) - ) - }); + // When --disable-proposer-reorg is used, the field in ChainConfig should become true + .with_config(|config| assert!(config.chain.disable_proposer_reorg)); } #[test] @@ -2874,7 +2821,7 @@ fn partial_columns() { assert!(config.network.enable_partial_columns); assert!(config.chain.enable_partial_columns); }); - // And disabled by default: + // And disabled by default on mainnet: CommandLineTest::new() .run_with_zero_port() .with_config(|config| { @@ -2882,3 +2829,60 @@ fn partial_columns() { assert!(!config.chain.enable_partial_columns); }) } + +#[test] +fn partial_columns_default_hoodi() { + CommandLineTest::new() + .flag("network", Some("hoodi")) + .run_with_zero_port() + .with_config(|config| { + assert!(config.network.enable_partial_columns); + assert!(config.chain.enable_partial_columns); + }); +} + +#[test] +fn partial_columns_default_sepolia() { + CommandLineTest::new() + .flag("network", Some("sepolia")) + .run_with_zero_port() + .with_config(|config| { + assert!(config.network.enable_partial_columns); + assert!(config.chain.enable_partial_columns); + }); +} + +#[test] +fn partial_columns_disable_overrides_hoodi_default() { + CommandLineTest::new() + .flag("network", Some("hoodi")) + .flag("disable-partial-columns", None) + .run_with_zero_port() + .with_config(|config| { + assert!(!config.network.enable_partial_columns); + assert!(!config.chain.enable_partial_columns); + }); +} + +#[test] +fn partial_columns_disable_on_mainnet_no_op() { + CommandLineTest::new() + .flag("disable-partial-columns", None) + .run_with_zero_port() + .with_config(|config| { + assert!(!config.network.enable_partial_columns); + assert!(!config.chain.enable_partial_columns); + }); +} + +#[test] +fn partial_columns_enable_disable_conflict() { + let mut cmd = base_cmd(); + cmd.arg("--enable-partial-columns") + .arg("--disable-partial-columns"); + let output = cmd.output().expect("should run command"); + assert!( + !output.status.success(), + "expected clap to reject --enable-partial-columns and --disable-partial-columns together", + ); +} diff --git a/lighthouse/tests/exec.rs b/lighthouse/tests/exec.rs index a25558bc2f..696cf2f40a 100644 --- a/lighthouse/tests/exec.rs +++ b/lighthouse/tests/exec.rs @@ -144,7 +144,6 @@ impl CompletedTest { func(&self.config, &self.dir); } - #[allow(dead_code)] pub fn with_config_and_spec(self, func: F) { let spec = ChainSpec::from_config::(&self.chain_config).unwrap(); func(&self.config, spec); diff --git a/testing/ef_tests/Cargo.toml b/testing/ef_tests/Cargo.toml index 9d09c3dfe6..bb7cba0b10 100644 --- a/testing/ef_tests/Cargo.toml +++ b/testing/ef_tests/Cargo.toml @@ -26,8 +26,11 @@ fork_choice = { workspace = true } fs2 = { workspace = true } hex = { workspace = true } kzg = { workspace = true } +lighthouse_network = { workspace = true } logging = { workspace = true } milhouse = { workspace = true } +network = { workspace = true } +proto_array = { workspace = true } rayon = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/testing/ef_tests/Makefile b/testing/ef_tests/Makefile index 36f6684685..f566a89ded 100644 --- a/testing/ef_tests/Makefile +++ b/testing/ef_tests/Makefile @@ -1,6 +1,6 @@ # To download/extract nightly tests, run: # CONSENSUS_SPECS_TEST_VERSION=nightly make -CONSENSUS_SPECS_TEST_VERSION ?= v1.7.0-alpha.7 +CONSENSUS_SPECS_TEST_VERSION ?= v1.7.0-alpha.8 REPO_NAME := consensus-spec-tests OUTPUT_DIR := ./$(REPO_NAME) diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index 53fb626e7e..723c5e7e9e 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -75,8 +75,15 @@ excluded_paths = [ "tests/.*/compute_challenge/.*", # We don't need these manifest files at the moment. "tests/.*/manifest.yaml", - # TODO: gossip condition tests not implemented yet - "tests/.*/.*/networking/.*", + # TODO: Remaining gossip validation topics not yet implemented + "tests/.*/.*/networking/gossip_beacon_block/.*", + "tests/.*/.*/networking/gossip_beacon_attestation/.*", + "tests/.*/.*/networking/gossip_beacon_aggregate_and_proof/.*", + "tests/.*/.*/networking/gossip_voluntary_exit/.*", + "tests/.*/.*/networking/gossip_bls_to_execution_change/.*", + "tests/.*/.*/networking/gossip_sync_committee_message/.*", + "tests/.*/.*/networking/gossip_sync_committee_contribution_and_proof/.*", + "tests/.*/.*/networking/gossip_blob_sidecar/.*", # TODO: fast confirmation rule not merged yet "tests/.*/.*/fast_confirmation", ] diff --git a/testing/ef_tests/src/cases.rs b/testing/ef_tests/src/cases.rs index b2e0276353..b2386f6fa5 100644 --- a/testing/ef_tests/src/cases.rs +++ b/testing/ef_tests/src/cases.rs @@ -20,6 +20,7 @@ mod fork_choice; mod genesis_initialization; mod genesis_validity; mod get_custody_groups; +mod gossip_validation; mod kzg_blob_to_kzg_commitment; mod kzg_compute_blob_kzg_proof; mod kzg_compute_cells; @@ -57,6 +58,7 @@ pub use fork::ForkTest; pub use genesis_initialization::*; pub use genesis_validity::*; pub use get_custody_groups::*; +pub use gossip_validation::*; pub use kzg_blob_to_kzg_commitment::*; pub use kzg_compute_blob_kzg_proof::*; pub use kzg_compute_cells::*; diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index 8b0b74d256..1736cd951f 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -1,13 +1,9 @@ use super::*; use crate::decode::{ssz_decode_file, ssz_decode_file_with, ssz_decode_state, yaml_decode_file}; -use ::fork_choice::{PayloadVerificationStatus, ProposerHeadError}; +use ::fork_choice::{AttestationFromBlock, PayloadVerificationStatus, ProposerHeadError}; use beacon_chain::beacon_proposer_cache::compute_proposer_duties_from_head; -use beacon_chain::blob_verification::GossipBlobError; use beacon_chain::block_verification_types::LookupBlock; -use beacon_chain::chain_config::{ - DEFAULT_RE_ORG_HEAD_THRESHOLD, DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, - DEFAULT_RE_ORG_PARENT_THRESHOLD, DisallowedReOrgOffsets, -}; +use beacon_chain::chain_config::DisallowedReOrgOffsets; use beacon_chain::data_column_verification::GossipVerifiedDataColumn; use beacon_chain::slot_clock::SlotClock; use beacon_chain::{ @@ -15,17 +11,21 @@ use beacon_chain::{ attestation_verification::{ VerifiedAttestation, obtain_indexed_attestation_and_committees_per_slot, }, - blob_verification::GossipVerifiedBlob, + blob_verification::KzgVerifiedBlob, custody_context::NodeCustodyType, test_utils::{BeaconChainHarness, EphemeralHarnessType}, }; +use bls::AggregateSignature; use execution_layer::{ PayloadStatusV1, PayloadStatusV1Status, json_structures::JsonPayloadStatusV1Status, }; +use proto_array::ReOrgThreshold; use serde::Deserialize; use ssz_derive::Decode; +use ssz_types::VariableList; use state_processing::VerifySignatures; use state_processing::envelope_processing::verify_execution_payload_envelope; +use state_processing::per_block_processing::is_valid_indexed_payload_attestation; use state_processing::state_advance::complete_state_advance; use std::future::Future; use std::sync::Arc; @@ -33,9 +33,9 @@ use std::time::Duration; use types::{ Attestation, AttestationRef, AttesterSlashing, AttesterSlashingRef, BeaconBlock, BeaconState, BlobSidecar, BlobsList, BlockImportSource, Checkpoint, DataColumnSidecar, - DataColumnSidecarList, DataColumnSubnetId, ExecutionBlockHash, Hash256, IndexedAttestation, - KzgProof, ProposerPreparationData, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, - Uint256, + DataColumnSidecarList, DataColumnSubnetId, Epoch, ExecutionBlockHash, Hash256, + IndexedAttestation, IndexedPayloadAttestation, KzgProof, PayloadAttestationMessage, + ProposerPreparationData, SignedBeaconBlock, SignedExecutionPayloadEnvelope, Slot, Uint256, }; // When set to true, cache any states fetched from the db. @@ -63,6 +63,13 @@ pub struct ShouldOverrideFcu { result: bool, } +#[derive(Debug, Clone, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PayloadVoteCheck { + block_root: Hash256, + votes: Vec>, +} + #[derive(Debug, Clone, Deserialize)] #[serde(deny_unknown_fields)] pub struct Checks { @@ -78,6 +85,8 @@ pub struct Checks { get_proposer_head: Option, should_override_forkchoice_update: Option, head_payload_status: Option, + payload_timeliness_vote: Option, + payload_data_availability_vote: Option, } #[derive(Debug, Clone, Deserialize)] @@ -108,6 +117,7 @@ pub enum Step< TAttesterSlashing, TPowBlock, TExecutionPayload = String, + TPayloadAttestationMessage = String, > { Tick { tick: u64, @@ -146,6 +156,15 @@ pub enum Step< execution_payload: TExecutionPayload, valid: bool, }, + PayloadAttestationMessage { + payload_attestation_message: TPayloadAttestationMessage, + #[serde(default = "default_true")] + valid: bool, + }, +} + +fn default_true() -> bool { + true } #[derive(Debug, Clone, Deserialize)] @@ -170,6 +189,7 @@ pub struct ForkChoiceTest { AttesterSlashing, PowBlock, SignedExecutionPayloadEnvelope, + PayloadAttestationMessage, >, >, } @@ -184,8 +204,12 @@ impl LoadCase for ForkChoiceTest { .expect("path must be valid OsStr") .to_string(); let spec = &testing_spec::(fork_name); - let steps: Vec, String, String, String>> = - yaml_decode_file(&path.join("steps.yaml"))?; + + #[allow(clippy::type_complexity)] + let steps: Vec< + Step, String, String, String, String, String>, + > = yaml_decode_file(&path.join("steps.yaml"))?; + // Resolve the object names in `steps.yaml` into actual decoded block/attestation objects. let steps = steps .into_iter() @@ -301,6 +325,18 @@ impl LoadCase for ForkChoiceTest { valid, }) } + Step::PayloadAttestationMessage { + payload_attestation_message, + valid, + } => { + let msg: PayloadAttestationMessage = ssz_decode_file( + &path.join(format!("{payload_attestation_message}.ssz_snappy")), + )?; + Ok(Step::PayloadAttestationMessage { + payload_attestation_message: msg, + valid, + }) + } }) .collect::>()?; let anchor_state = ssz_decode_state(&path.join("anchor_state.ssz_snappy"), spec)?; @@ -381,6 +417,8 @@ impl Case for ForkChoiceTest { get_proposer_head, should_override_forkchoice_update: should_override_fcu, head_payload_status, + payload_timeliness_vote, + payload_data_availability_vote, } = checks.as_ref(); if let Some(expected_head) = head { @@ -431,6 +469,14 @@ impl Case for ForkChoiceTest { if let Some(expected_status) = head_payload_status { tester.check_head_payload_status(*expected_status)?; } + + if let Some(expected) = payload_timeliness_vote { + tester.check_payload_timeliness_vote(expected)?; + } + + if let Some(expected) = payload_data_availability_vote { + tester.check_payload_data_availability_vote(expected)?; + } } Step::MaybeValidBlockAndColumns { @@ -446,6 +492,13 @@ impl Case for ForkChoiceTest { } => { tester.process_execution_payload(execution_payload, *valid)?; } + Step::PayloadAttestationMessage { + payload_attestation_message, + valid, + } => { + tester + .process_payload_attestation_message(payload_attestation_message, *valid)?; + } } } @@ -642,7 +695,6 @@ impl Tester { let mut blob_success = true; - // Convert blobs and kzg_proofs into sidecars, then plumb them into the availability tracker if let Some(blobs) = blobs.clone() { let proofs = kzg_proofs.unwrap(); let commitments = block @@ -655,37 +707,51 @@ impl Tester { // Zipping will stop when any of the zipped lists runs out, which is what we want. Some // of the tests don't provide enough proofs/blobs, and should fail the availability // check. - for (i, ((blob, kzg_proof), kzg_commitment)) in - blobs.into_iter().zip(proofs).zip(commitments).enumerate() - { - let blob_sidecar = Arc::new(BlobSidecar { - index: i as u64, - blob, - kzg_commitment, - kzg_proof, - signed_block_header: block.signed_block_header(), - kzg_commitment_inclusion_proof: block - .message() - .body() - .kzg_commitment_merkle_proof(i) - .unwrap(), - }); + let verified_blobs: Vec> = blobs + .into_iter() + .zip(proofs) + .zip(commitments) + .enumerate() + .filter_map(|(i, ((blob, kzg_proof), kzg_commitment))| { + let blob_sidecar = Arc::new(BlobSidecar { + index: i as u64, + blob, + kzg_commitment, + kzg_proof, + signed_block_header: block.signed_block_header(), + kzg_commitment_inclusion_proof: block + .message() + .body() + .kzg_commitment_merkle_proof(i) + .unwrap(), + }); - let chain = self.harness.chain.clone(); - let blob = - match GossipVerifiedBlob::new(blob_sidecar.clone(), blob_sidecar.index, &chain) - { - Ok(gossip_verified_blob) => gossip_verified_blob, - Err(GossipBlobError::KzgError(_)) => { + match KzgVerifiedBlob::new( + blob_sidecar.clone(), + &self.harness.chain.kzg, + Duration::default(), + ) { + Ok(verified) => Some(verified), + Err(_) => { blob_success = false; - GossipVerifiedBlob::__assumed_valid(blob_sidecar) + None } - Err(_) => GossipVerifiedBlob::__assumed_valid(blob_sidecar), - }; - let result = - self.block_on_dangerous(self.harness.chain.process_gossip_blob(blob))?; + } + }) + .collect(); + + if !verified_blobs.is_empty() { + let result = self + .harness + .chain + .data_availability_checker + .put_kzg_verified_blobs(block_root, verified_blobs); if valid { - assert!(result.is_ok()); + assert!( + result.is_ok(), + "put_kzg_verified_blobs failed: {:?}", + result + ); } } }; @@ -971,10 +1037,10 @@ impl Tester { let proposer_head_result = fc.get_proposer_head( slot, canonical_head, - DEFAULT_RE_ORG_HEAD_THRESHOLD, - DEFAULT_RE_ORG_PARENT_THRESHOLD, + ReOrgThreshold(self.spec.reorg_head_weight_threshold), + ReOrgThreshold(self.spec.reorg_parent_weight_threshold), &DisallowedReOrgOffsets::default(), - DEFAULT_RE_ORG_MAX_EPOCHS_SINCE_FINALIZATION, + Epoch::new(self.spec.reorg_max_epochs_since_finalization), ); let proposer_head = match proposer_head_result { Ok(head) => head.parent_node.root(), @@ -1149,6 +1215,173 @@ impl Tester { expected_should_override_fcu.result, ) } + + pub fn process_payload_attestation_message( + &self, + msg: &PayloadAttestationMessage, + valid: bool, + ) -> Result<(), Error> { + let slot = msg.data.slot; + let block_root = msg.data.beacon_block_root; + + // Get the state at the block to compute the PTC and verify signature. + let store = &self.harness.chain.store; + let block = store + .get_blinded_block(&block_root) + .map_err(|e| Error::InternalError(format!("Failed to load block: {e:?}")))?; + + let state_opt = block.and_then(|block| { + store + .get_hot_state(&block.state_root(), CACHE_STATE_IN_TESTS) + .ok()? + }); + + // Build IndexedPayloadAttestation from the message. + let indexed = IndexedPayloadAttestation:: { + attesting_indices: VariableList::new(vec![msg.validator_index]).unwrap(), + data: msg.data.clone(), + signature: AggregateSignature::from(&msg.signature), + }; + + let result = if let Some(ref state) = state_opt { + is_valid_indexed_payload_attestation( + state, + &indexed, + VerifySignatures::True, + &self.spec, + ) + .map_err(|e| { + Error::InternalError(format!( + "payload attestation signature verification failed for validator {}: {:?}", + msg.validator_index, e + )) + }) + .and_then(|_| { + let ptc = state.get_ptc(slot, &self.spec).map_err(|e| { + Error::InternalError(format!( + "Could not compute PTC for block root {block_root:?} at slot {slot:?}: {e:?}" + )) + })?; + + self.harness + .chain + .canonical_head + .fork_choice_write_lock() + .on_payload_attestation( + self.harness.chain.slot().unwrap(), + &indexed, + AttestationFromBlock::False, + &ptc.0, + ) + .map_err(|e| { + Error::InternalError(format!( + "on_payload_attestation for validator {} failed: {:?}", + msg.validator_index, e + )) + }) + }) + } else { + Err(Error::InternalError(format!( + "Could not get state for block root {block_root:?} at slot {slot:?}" + ))) + }; + + if valid { + result?; + } else if result.is_ok() { + return Err(Error::DidntFail(format!( + "payload_attestation_message for validator {} should have failed", + msg.validator_index + ))); + } + + Ok(()) + } + + pub fn check_payload_timeliness_vote(&self, expected: &PayloadVoteCheck) -> Result<(), Error> { + let fc = self.harness.chain.canonical_head.fork_choice_read_lock(); + let proto_array = fc.proto_array().core_proto_array(); + + let node_index = proto_array + .indices + .get(&expected.block_root) + .ok_or_else(|| { + Error::InternalError(format!( + "Block root {:?} not found in proto array", + expected.block_root + )) + })?; + let node = proto_array + .nodes + .get(*node_index) + .ok_or_else(|| Error::InternalError(format!("Node index {} not found", node_index)))?; + let v29 = node + .as_v29() + .map_err(|_| Error::InternalError("Node is not V29".to_string()))?; + + let timeliness_votes = &v29.payload_timeliness_votes; + let participation = &v29.ptc_participation; + + for (i, expected_vote) in expected.votes.iter().enumerate() { + let actual = if !participation.get(i).unwrap() { + None // not yet voted + } else { + Some(timeliness_votes.get(i).unwrap()) + }; + if actual != *expected_vote { + return Err(Error::NotEqual(format!( + "payload_timeliness_vote[{}]: Got {:?} | Expected {:?}", + i, actual, expected_vote + ))); + } + } + + Ok(()) + } + + pub fn check_payload_data_availability_vote( + &self, + expected: &PayloadVoteCheck, + ) -> Result<(), Error> { + let fc = self.harness.chain.canonical_head.fork_choice_read_lock(); + let proto_array = fc.proto_array().core_proto_array(); + + let node_index = proto_array + .indices + .get(&expected.block_root) + .ok_or_else(|| { + Error::InternalError(format!( + "Block root {:?} not found in proto array", + expected.block_root + )) + })?; + let node = proto_array + .nodes + .get(*node_index) + .ok_or_else(|| Error::InternalError(format!("Node index {} not found", node_index)))?; + let v29 = node + .as_v29() + .map_err(|_| Error::InternalError("Node is not V29".to_string()))?; + + let availability_votes = &v29.payload_data_availability_votes; + let participation = &v29.ptc_participation; + + for (i, expected_vote) in expected.votes.iter().enumerate() { + let actual = if !participation.get(i).unwrap() { + None // not yet voted + } else { + Some(availability_votes.get(i).unwrap()) + }; + if actual != *expected_vote { + return Err(Error::NotEqual(format!( + "payload_data_availability_vote[{}]: Got {:?} | Expected {:?}", + i, actual, expected_vote + ))); + } + } + + Ok(()) + } } /// Checks that the `head` checkpoint from the beacon chain head matches the `fc` checkpoint gleaned diff --git a/testing/ef_tests/src/cases/gossip_validation.rs b/testing/ef_tests/src/cases/gossip_validation.rs new file mode 100644 index 0000000000..3dbbcae5a7 --- /dev/null +++ b/testing/ef_tests/src/cases/gossip_validation.rs @@ -0,0 +1,206 @@ +use super::*; +use crate::bls_setting::BlsSetting; +use crate::decode::{ssz_decode_file, ssz_decode_state, yaml_decode_file}; +use crate::type_name::TypeName; +use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType}; +use lighthouse_network::{MessageAcceptance, MessageId, PeerId}; +use network::NetworkBeaconProcessor; +use serde::Deserialize; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use types::{AttesterSlashing, BeaconState, EthSpec, ForkName, ProposerSlashing}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "snake_case")] +enum ExpectedOutcome { + Valid, + Ignore, + Reject, +} + +impl PartialEq for ExpectedOutcome { + fn eq(&self, other: &MessageAcceptance) -> bool { + matches!( + (self, other), + (Self::Valid, MessageAcceptance::Accept) + | (Self::Ignore, MessageAcceptance::Ignore) + | (Self::Reject, MessageAcceptance::Reject) + ) + } +} + +#[derive(Debug, Clone, Deserialize)] +struct Meta { + topic: Topic, + #[serde(default)] + messages: Vec, + #[serde(default)] + bls_setting: Option, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(deny_unknown_fields)] +struct MessageMeta { + message: String, + expected: ExpectedOutcome, + #[serde(default)] + reason: Option, + #[serde(default)] + #[allow(dead_code)] + subnet_id: Option, + #[serde(default)] + #[allow(dead_code)] + offset_ms: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "snake_case")] +enum Topic { + ProposerSlashing, + AttesterSlashing, + // TODO: add support for these topics + // VoluntaryExit, + // BlsToExecutionChange, + // SyncCommittee, + // SyncCommitteeContributionAndProof, + // BeaconBlock, + // BeaconAttestation, + // BeaconAggregateAndProof, +} + +#[derive(Debug)] +pub struct GossipValidation { + path: PathBuf, + meta: Meta, + state: BeaconState, +} + +impl LoadCase for GossipValidation { + fn load_from_dir(path: &Path, fork_name: ForkName) -> Result { + let meta: Meta = yaml_decode_file(&path.join("meta.yaml"))?; + let spec = &testing_spec::(fork_name); + let state = ssz_decode_state(&path.join("state.ssz_snappy"), spec)?; + + Ok(Self { + path: path.to_path_buf(), + meta, + state, + }) + } +} + +impl Case for GossipValidation { + fn description(&self) -> String { + self.path + .iter() + .next_back() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_default() + } + + fn result(&self, _case_index: usize, fork_name: ForkName) -> Result<(), Error> { + if let Some(bls_setting) = self.meta.bls_setting { + bls_setting.check()?; + } + + let spec = testing_spec::(fork_name); + let tester = GossipTester::new(self, spec)?; + + for message_meta in &self.meta.messages { + let actual = + tester.validate_message(&self.path, &self.meta.topic, message_meta, fork_name)?; + + if message_meta.expected != actual { + return Err(Error::NotEqual(format!( + "{}: expected {:?}, got {:?}{}", + self.path.display(), + message_meta.expected, + actual, + message_meta + .reason + .as_ref() + .map(|r| format!(" ({r})")) + .unwrap_or_default() + ))); + } + } + + Ok(()) + } +} + +struct GossipTester { + network_beacon_processor: Arc>>, +} + +impl GossipTester { + fn new(case: &GossipValidation, spec: ChainSpec) -> Result { + let genesis_time = case.state.genesis_time(); + let spec = Arc::new(spec); + + let harness = BeaconChainHarness::>::builder(E::default()) + .spec(spec.clone()) + .keypairs(vec![]) + .genesis_state_ephemeral_store(case.state.clone()) + .mock_execution_layer() + .recalculate_fork_times_with_genesis(genesis_time) + .mock_execution_layer_all_payloads_valid() + .build(); + + let network_beacon_processor = NetworkBeaconProcessor::null_from_harness(&harness); + + Ok(Self { + network_beacon_processor: Arc::new(network_beacon_processor), + }) + } + + fn validate_message( + &self, + path: &Path, + topic: &Topic, + message_meta: &MessageMeta, + fork_name: ForkName, + ) -> Result { + match topic { + Topic::ProposerSlashing => self.validate_proposer_slashing(path, message_meta), + Topic::AttesterSlashing => { + self.validate_attester_slashing(path, message_meta, fork_name) + } + } + } + + fn validate_proposer_slashing( + &self, + path: &Path, + message_meta: &MessageMeta, + ) -> Result { + let slashing: ProposerSlashing = + ssz_decode_file(&path.join(format!("{}.ssz_snappy", message_meta.message)))?; + + let message_id = MessageId::new(&[]); + let peer_id = PeerId::random(); + Ok(self + .network_beacon_processor + .process_gossip_proposer_slashing(message_id, peer_id, slashing)) + } + + fn validate_attester_slashing( + &self, + path: &Path, + message_meta: &MessageMeta, + fork_name: ForkName, + ) -> Result { + let ssz_path = path.join(format!("{}.ssz_snappy", message_meta.message)); + let slashing: AttesterSlashing = if fork_name.electra_enabled() { + ssz_decode_file(&ssz_path).map(AttesterSlashing::Electra)? + } else { + ssz_decode_file(&ssz_path).map(AttesterSlashing::Base)? + }; + + let message_id = MessageId::new(&[]); + let peer_id = PeerId::random(); + Ok(self + .network_beacon_processor + .process_gossip_attester_slashing(message_id, peer_id, slashing)) + } +} diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index e380f51c0a..df1ece49dd 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -715,10 +715,8 @@ impl Handler for ForkChoiceHandler { return false; } - // Deposit tests exist only for Electra and Fulu (not Gloas). - if self.handler_name == "deposit_with_reorg" - && (!fork_name.electra_enabled() || fork_name.gloas_enabled()) - { + // Deposit tests exist only for Electra and later. + if self.handler_name == "deposit_with_reorg" && !fork_name.electra_enabled() { return false; } @@ -727,10 +725,11 @@ impl Handler for ForkChoiceHandler { return false; } - // on_execution_payload_envelope and get_parent_payload_status tests exist only for - // Gloas and later. + // on_execution_payload_envelope, get_parent_payload_status, and + // on_payload_attestation_message tests exist only for Gloas and later. if (self.handler_name == "on_execution_payload_envelope" - || self.handler_name == "get_parent_payload_status") + || self.handler_name == "get_parent_payload_status" + || self.handler_name == "on_payload_attestation_message") && !fork_name.gloas_enabled() { return false; @@ -980,6 +979,36 @@ impl Handler for ComputeColumnsForCustodyGroupHandler } } +pub struct GossipValidationHandler { + handler_name: &'static str, + _phantom: PhantomData, +} + +impl GossipValidationHandler { + pub const fn new(handler_name: &'static str) -> Self { + Self { + handler_name, + _phantom: PhantomData, + } + } +} + +impl Handler for GossipValidationHandler { + type Case = cases::GossipValidation; + + fn config_name() -> &'static str { + E::name() + } + + fn runner_name() -> &'static str { + "networking" + } + + fn handler_name(&self) -> String { + self.handler_name.into() + } +} + #[derive(Educe)] #[educe(Default)] pub struct KZGComputeCellsHandler(PhantomData); diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index ca383efdb0..6e1c4fdc10 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -1079,6 +1079,12 @@ fn fork_choice_get_parent_payload_status() { ForkChoiceHandler::::new("get_parent_payload_status").run(); } +#[test] +fn fork_choice_on_payload_attestation_message() { + ForkChoiceHandler::::new("on_payload_attestation_message").run(); + ForkChoiceHandler::::new("on_payload_attestation_message").run(); +} + #[test] fn optimistic_sync() { OptimisticSyncHandler::::default().run(); @@ -1183,3 +1189,15 @@ fn compute_columns_for_custody_group() { ComputeColumnsForCustodyGroupHandler::::default().run(); ComputeColumnsForCustodyGroupHandler::::default().run(); } + +#[test] +fn gossip_proposer_slashing() { + GossipValidationHandler::::new("gossip_proposer_slashing").run(); + GossipValidationHandler::::new("gossip_proposer_slashing").run(); +} + +#[test] +fn gossip_attester_slashing() { + GossipValidationHandler::::new("gossip_attester_slashing").run(); + GossipValidationHandler::::new("gossip_attester_slashing").run(); +} diff --git a/testing/execution_engine_integration/src/test_rig.rs b/testing/execution_engine_integration/src/test_rig.rs index ed6b5787b5..61f25e63a1 100644 --- a/testing/execution_engine_integration/src/test_rig.rs +++ b/testing/execution_engine_integration/src/test_rig.rs @@ -320,6 +320,7 @@ impl TestRig { Some(vec![]), None, None, + None, ), ) .await; @@ -366,11 +367,12 @@ impl TestRig { Some(vec![]), None, None, + None, ); let payload_parameters = PayloadParameters { parent_hash, - parent_gas_limit, + parent_gas_limit: Some(parent_gas_limit), proposer_gas_limit: None, payload_attributes: &payload_attributes, forkchoice_update_params: &forkchoice_update_params, @@ -527,11 +529,12 @@ impl TestRig { Some(vec![]), None, None, + None, ); let payload_parameters = PayloadParameters { parent_hash, - parent_gas_limit, + parent_gas_limit: Some(parent_gas_limit), proposer_gas_limit: None, payload_attributes: &payload_attributes, forkchoice_update_params: &forkchoice_update_params, @@ -588,6 +591,7 @@ impl TestRig { Some(vec![]), None, None, + None, ); let slot = Slot::new(42); let head_block_root = Hash256::repeat_byte(100); diff --git a/testing/simulator/Cargo.toml b/testing/simulator/Cargo.toml index a1b1b6f95d..29972648f3 100644 --- a/testing/simulator/Cargo.toml +++ b/testing/simulator/Cargo.toml @@ -6,6 +6,7 @@ edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +beacon_chain = { workspace = true } clap = { workspace = true } environment = { workspace = true } execution_layer = { workspace = true } diff --git a/testing/simulator/src/basic_sim.rs b/testing/simulator/src/basic_sim.rs index 79581ee529..688cfb31ec 100644 --- a/testing/simulator/src/basic_sim.rs +++ b/testing/simulator/src/basic_sim.rs @@ -30,9 +30,10 @@ const ALTAIR_FORK_EPOCH: u64 = 0; const BELLATRIX_FORK_EPOCH: u64 = 0; const CAPELLA_FORK_EPOCH: u64 = 0; const DENEB_FORK_EPOCH: u64 = 0; -const ELECTRA_FORK_EPOCH: u64 = 2; -// const FULU_FORK_EPOCH: u64 = 3; -// const GLOAS_FORK_EPOCH: u64 = 4; +const ELECTRA_FORK_EPOCH: u64 = 0; +const FULU_FORK_EPOCH: u64 = 0; +// TODO(gloas): enable Gloas in simulator, current blocker is lack of data column gossip verification +// const GLOAS_FORK_EPOCH: u64 = 2; const SUGGESTED_FEE_RECIPIENT: [u8; 20] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]; @@ -171,8 +172,8 @@ pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { let genesis_delay = GENESIS_DELAY; // Convenience variables. Update these values when adding a newer fork. - let latest_fork_version = spec.electra_fork_version; - let latest_fork_start_epoch = ELECTRA_FORK_EPOCH; + let latest_fork_version = spec.fulu_fork_version; + let latest_fork_start_epoch = FULU_FORK_EPOCH; let mut slot_duration_ms = spec.get_slot_duration().as_millis() as u64; slot_duration_ms /= speed_up_factor; @@ -187,6 +188,7 @@ pub fn run_basic_sim(matches: &ArgMatches) -> Result<(), String> { spec.capella_fork_epoch = Some(Epoch::new(CAPELLA_FORK_EPOCH)); spec.deneb_fork_epoch = Some(Epoch::new(DENEB_FORK_EPOCH)); spec.electra_fork_epoch = Some(Epoch::new(ELECTRA_FORK_EPOCH)); + spec.fulu_fork_epoch = Some(Epoch::new(FULU_FORK_EPOCH)); let spec = Arc::new(spec); env.eth2_config.spec = spec.clone(); diff --git a/testing/simulator/src/fallback_sim.rs b/testing/simulator/src/fallback_sim.rs index 06f4478c5e..aed113eca0 100644 --- a/testing/simulator/src/fallback_sim.rs +++ b/testing/simulator/src/fallback_sim.rs @@ -25,11 +25,12 @@ const END_EPOCH: u64 = 16; const GENESIS_DELAY: u64 = 38; const ALTAIR_FORK_EPOCH: u64 = 0; const BELLATRIX_FORK_EPOCH: u64 = 0; -const CAPELLA_FORK_EPOCH: u64 = 1; -const DENEB_FORK_EPOCH: u64 = 2; -// const ELECTRA_FORK_EPOCH: u64 = 3; -// const FULU_FORK_EPOCH: u64 = 4; -// const GLOAS_FORK_EPOCH: u64 = 5; +const CAPELLA_FORK_EPOCH: u64 = 0; +const DENEB_FORK_EPOCH: u64 = 0; +const ELECTRA_FORK_EPOCH: u64 = 0; +const FULU_FORK_EPOCH: u64 = 0; +// TODO(gloas): enable Gloas in simulator, current blocker is lack of data column gossip verification +// const GLOAS_FORK_EPOCH: u64 = 2; // Since simulator tests are non-deterministic and there is a non-zero chance of missed // attestations, define an acceptable network-wide attestation performance. @@ -191,8 +192,8 @@ pub fn run_fallback_sim(matches: &ArgMatches) -> Result<(), String> { spec.bellatrix_fork_epoch = Some(Epoch::new(BELLATRIX_FORK_EPOCH)); spec.capella_fork_epoch = Some(Epoch::new(CAPELLA_FORK_EPOCH)); spec.deneb_fork_epoch = Some(Epoch::new(DENEB_FORK_EPOCH)); - //spec.electra_fork_epoch = Some(Epoch::new(ELECTRA_FORK_EPOCH)); - //spec.fulu_fork_epoch = Some(Epoch::new(FULU_FORK_EPOCH)); + spec.electra_fork_epoch = Some(Epoch::new(ELECTRA_FORK_EPOCH)); + spec.fulu_fork_epoch = Some(Epoch::new(FULU_FORK_EPOCH)); let spec = Arc::new(spec); env.eth2_config.spec = spec.clone(); diff --git a/testing/simulator/src/local_network.rs b/testing/simulator/src/local_network.rs index 2beb9c0efc..780a09e543 100644 --- a/testing/simulator/src/local_network.rs +++ b/testing/simulator/src/local_network.rs @@ -1,4 +1,5 @@ use crate::checks::epoch_delay; +use beacon_chain::custody_context::NodeCustodyType; use kzg::trusted_setup::get_trusted_setup; use node_test_rig::{ ClientConfig, ClientGenesis, LocalBeaconNode, LocalExecutionNode, LocalValidatorClient, @@ -46,6 +47,7 @@ fn default_client_config(network_params: LocalNetworkParams, genesis_time: u64) beacon_config.network.discv5_config.enable_packet_filter = false; beacon_config.chain.enable_light_client_server = true; beacon_config.chain.optimistic_finalized_sync = false; + beacon_config.chain.node_custody_type = NodeCustodyType::Supernode; beacon_config.trusted_setup = get_trusted_setup(); let el_config = execution_layer::Config { @@ -103,6 +105,15 @@ fn default_mock_execution_config( ) } + if let Some(gloas_fork_epoch) = spec.gloas_fork_epoch { + mock_execution_config.amsterdam_time = Some( + genesis_time + + (spec.get_slot_duration().as_secs()) + * E::slots_per_epoch() + * gloas_fork_epoch.as_u64(), + ) + } + mock_execution_config } diff --git a/validator_client/validator_services/src/proposer_preferences_service.rs b/validator_client/validator_services/src/proposer_preferences_service.rs index 81905b9c08..ae4db18ba0 100644 --- a/validator_client/validator_services/src/proposer_preferences_service.rs +++ b/validator_client/validator_services/src/proposer_preferences_service.rs @@ -169,7 +169,7 @@ impl ProposerPreferencesSer proposal_slot: duty.slot, validator_index: duty.validator_index, fee_recipient, - gas_limit: proposal_data.gas_limit, + target_gas_limit: proposal_data.gas_limit, }, )); }