mirror of
https://github.com/sigp/lighthouse.git
synced 2026-05-29 20:27:14 +00:00
Improving blob propagation post-PeerDAS with Decentralized Blob Building (#6268)
* Get blobs from EL. Co-authored-by: Michael Sproul <michael@sigmaprime.io> * Avoid cloning blobs after fetching blobs. * Address review comments and refactor code. * Fix lint. * Move blob computation metric to the right spot. * Merge branch 'unstable' into das-fetch-blobs * Merge branch 'unstable' into das-fetch-blobs # Conflicts: # beacon_node/beacon_chain/src/beacon_chain.rs # beacon_node/beacon_chain/src/block_verification.rs # beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs * Merge branch 'unstable' into das-fetch-blobs # Conflicts: # beacon_node/beacon_chain/src/beacon_chain.rs * Gradual publication of data columns for supernodes. * Recompute head after importing block with blobs from the EL. * Fix lint * Merge branch 'unstable' into das-fetch-blobs * Use blocking task instead of async when computing cells. * Merge branch 'das-fetch-blobs' of github.com:jimmygchen/lighthouse into das-fetch-blobs * Merge remote-tracking branch 'origin/unstable' into das-fetch-blobs * Fix semantic conflicts * Downgrade error log. * Merge branch 'unstable' into das-fetch-blobs # Conflicts: # beacon_node/beacon_chain/src/data_availability_checker.rs # beacon_node/beacon_chain/src/data_availability_checker/overflow_lru_cache.rs # beacon_node/execution_layer/src/engine_api.rs # beacon_node/execution_layer/src/engine_api/json_structures.rs # beacon_node/network/src/network_beacon_processor/gossip_methods.rs # beacon_node/network/src/network_beacon_processor/mod.rs # beacon_node/network/src/network_beacon_processor/sync_methods.rs * Merge branch 'unstable' into das-fetch-blobs * Publish block without waiting for blob and column proof computation. * Address review comments and refactor. * Merge branch 'unstable' into das-fetch-blobs * Fix test and docs. * Comment cleanups. * Merge branch 'unstable' into das-fetch-blobs * Address review comments and cleanup * Address review comments and cleanup * Refactor to de-duplicate gradual publication logic. * Add more logging. * Merge remote-tracking branch 'origin/unstable' into das-fetch-blobs # Conflicts: # Cargo.lock * Fix incorrect comparison on `num_fetched_blobs`. * Implement gradual blob publication. * Merge branch 'unstable' into das-fetch-blobs * Inline `publish_fn`. * Merge branch 'das-fetch-blobs' of github.com:jimmygchen/lighthouse into das-fetch-blobs * Gossip verify blobs before publishing * Avoid queries for 0 blobs and error for duplicates * Gossip verified engine blob before processing them, and use observe cache to detect duplicates before publishing. * Merge branch 'das-fetch-blobs' of github.com:jimmygchen/lighthouse into das-fetch-blobs # Conflicts: # beacon_node/network/src/network_beacon_processor/mod.rs * Merge branch 'unstable' into das-fetch-blobs * Fix invalid commitment inclusion proofs in blob sidecars created from EL blobs. * Only publish EL blobs triggered from gossip block, and not RPC block. * Downgrade gossip blob log to `debug`. * Merge branch 'unstable' into das-fetch-blobs * Merge branch 'unstable' into das-fetch-blobs * Grammar
This commit is contained in:
@@ -88,7 +88,7 @@ use kzg::Kzg;
|
||||
use operation_pool::{
|
||||
CompactAttestationRef, OperationPool, PersistedOperationPool, ReceivedPreCapella,
|
||||
};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use parking_lot::{Mutex, RwLock, RwLockWriteGuard};
|
||||
use proto_array::{DoNotReOrg, ProposerHeadError};
|
||||
use safe_arith::SafeArith;
|
||||
use slasher::Slasher;
|
||||
@@ -120,6 +120,7 @@ use store::{
|
||||
DatabaseBlock, Error as DBError, HotColdDB, KeyValueStore, KeyValueStoreOp, StoreItem, StoreOp,
|
||||
};
|
||||
use task_executor::{ShutdownReason, TaskExecutor};
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tokio_stream::Stream;
|
||||
use tree_hash::TreeHash;
|
||||
use types::blob_sidecar::FixedBlobSidecarList;
|
||||
@@ -2971,7 +2972,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
pub async fn process_gossip_blob(
|
||||
self: &Arc<Self>,
|
||||
blob: GossipVerifiedBlob<T>,
|
||||
publish_fn: impl FnOnce() -> Result<(), BlockError>,
|
||||
) -> Result<AvailabilityProcessingStatus, BlockError> {
|
||||
let block_root = blob.block_root();
|
||||
|
||||
@@ -2990,17 +2990,9 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
return Err(BlockError::BlobNotRequired(blob.slot()));
|
||||
}
|
||||
|
||||
if let Some(event_handler) = self.event_handler.as_ref() {
|
||||
if event_handler.has_blob_sidecar_subscribers() {
|
||||
event_handler.register(EventKind::BlobSidecar(SseBlobSidecar::from_blob_sidecar(
|
||||
blob.as_blob(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
self.emit_sse_blob_sidecar_events(&block_root, std::iter::once(blob.as_blob()));
|
||||
|
||||
let r = self
|
||||
.check_gossip_blob_availability_and_import(blob, publish_fn)
|
||||
.await;
|
||||
let r = self.check_gossip_blob_availability_and_import(blob).await;
|
||||
self.remove_notified(&block_root, r)
|
||||
}
|
||||
|
||||
@@ -3078,20 +3070,63 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
}
|
||||
}
|
||||
|
||||
self.emit_sse_blob_sidecar_events(&block_root, blobs.iter().flatten().map(Arc::as_ref));
|
||||
|
||||
let r = self
|
||||
.check_rpc_blob_availability_and_import(slot, block_root, blobs)
|
||||
.await;
|
||||
self.remove_notified(&block_root, r)
|
||||
}
|
||||
|
||||
/// Process blobs retrieved from the EL and returns the `AvailabilityProcessingStatus`.
|
||||
///
|
||||
/// `data_column_recv`: An optional receiver for `DataColumnSidecarList`.
|
||||
/// If PeerDAS is enabled, this receiver will be provided and used to send
|
||||
/// the `DataColumnSidecar`s once they have been successfully computed.
|
||||
pub async fn process_engine_blobs(
|
||||
self: &Arc<Self>,
|
||||
slot: Slot,
|
||||
block_root: Hash256,
|
||||
blobs: FixedBlobSidecarList<T::EthSpec>,
|
||||
data_column_recv: Option<Receiver<DataColumnSidecarList<T::EthSpec>>>,
|
||||
) -> Result<AvailabilityProcessingStatus, BlockError> {
|
||||
// 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(block_root));
|
||||
}
|
||||
|
||||
self.emit_sse_blob_sidecar_events(&block_root, blobs.iter().flatten().map(Arc::as_ref));
|
||||
|
||||
let r = self
|
||||
.check_engine_blob_availability_and_import(slot, block_root, blobs, data_column_recv)
|
||||
.await;
|
||||
self.remove_notified(&block_root, r)
|
||||
}
|
||||
|
||||
fn emit_sse_blob_sidecar_events<'a, I>(self: &Arc<Self>, block_root: &Hash256, blobs_iter: I)
|
||||
where
|
||||
I: Iterator<Item = &'a BlobSidecar<T::EthSpec>>,
|
||||
{
|
||||
if let Some(event_handler) = self.event_handler.as_ref() {
|
||||
if event_handler.has_blob_sidecar_subscribers() {
|
||||
for blob in blobs.iter().filter_map(|maybe_blob| maybe_blob.as_ref()) {
|
||||
let imported_blobs = self
|
||||
.data_availability_checker
|
||||
.cached_blob_indexes(block_root)
|
||||
.unwrap_or_default();
|
||||
let new_blobs = blobs_iter.filter(|b| !imported_blobs.contains(&b.index));
|
||||
|
||||
for blob in new_blobs {
|
||||
event_handler.register(EventKind::BlobSidecar(
|
||||
SseBlobSidecar::from_blob_sidecar(blob),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let r = self
|
||||
.check_rpc_blob_availability_and_import(slot, block_root, blobs)
|
||||
.await;
|
||||
self.remove_notified(&block_root, r)
|
||||
}
|
||||
|
||||
/// Cache the columns in the processing cache, process it, then evict it from the cache if it was
|
||||
@@ -3181,7 +3216,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
};
|
||||
|
||||
let r = self
|
||||
.process_availability(slot, availability, || Ok(()))
|
||||
.process_availability(slot, availability, None, || Ok(()))
|
||||
.await;
|
||||
self.remove_notified(&block_root, r)
|
||||
.map(|availability_processing_status| {
|
||||
@@ -3309,7 +3344,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
|
||||
match executed_block {
|
||||
ExecutedBlock::Available(block) => {
|
||||
self.import_available_block(Box::new(block)).await
|
||||
self.import_available_block(Box::new(block), None).await
|
||||
}
|
||||
ExecutedBlock::AvailabilityPending(block) => {
|
||||
self.check_block_availability_and_import(block).await
|
||||
@@ -3441,7 +3476,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
let availability = self
|
||||
.data_availability_checker
|
||||
.put_pending_executed_block(block)?;
|
||||
self.process_availability(slot, availability, || Ok(()))
|
||||
self.process_availability(slot, availability, None, || Ok(()))
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -3450,7 +3485,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
async fn check_gossip_blob_availability_and_import(
|
||||
self: &Arc<Self>,
|
||||
blob: GossipVerifiedBlob<T>,
|
||||
publish_fn: impl FnOnce() -> Result<(), BlockError>,
|
||||
) -> Result<AvailabilityProcessingStatus, BlockError> {
|
||||
let slot = blob.slot();
|
||||
if let Some(slasher) = self.slasher.as_ref() {
|
||||
@@ -3458,7 +3492,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
}
|
||||
let availability = self.data_availability_checker.put_gossip_blob(blob)?;
|
||||
|
||||
self.process_availability(slot, availability, publish_fn)
|
||||
self.process_availability(slot, availability, None, || Ok(()))
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -3477,16 +3511,41 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
}
|
||||
}
|
||||
|
||||
let availability = self.data_availability_checker.put_gossip_data_columns(
|
||||
slot,
|
||||
block_root,
|
||||
data_columns,
|
||||
)?;
|
||||
let availability = self
|
||||
.data_availability_checker
|
||||
.put_gossip_data_columns(block_root, data_columns)?;
|
||||
|
||||
self.process_availability(slot, availability, publish_fn)
|
||||
self.process_availability(slot, availability, None, publish_fn)
|
||||
.await
|
||||
}
|
||||
|
||||
fn check_blobs_for_slashability(
|
||||
self: &Arc<Self>,
|
||||
block_root: Hash256,
|
||||
blobs: &FixedBlobSidecarList<T::EthSpec>,
|
||||
) -> Result<(), BlockError> {
|
||||
let mut slashable_cache = self.observed_slashable.write();
|
||||
for header in blobs
|
||||
.iter()
|
||||
.filter_map(|b| b.as_ref().map(|b| b.signed_block_header.clone()))
|
||||
.unique()
|
||||
{
|
||||
if verify_header_signature::<T, BlockError>(self, &header).is_ok() {
|
||||
slashable_cache
|
||||
.observe_slashable(
|
||||
header.message.slot,
|
||||
header.message.proposer_index,
|
||||
block_root,
|
||||
)
|
||||
.map_err(|e| BlockError::BeaconChainError(e.into()))?;
|
||||
if let Some(slasher) = self.slasher.as_ref() {
|
||||
slasher.accept_block_header(header);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks if the provided blobs can make any cached blocks available, and imports immediately
|
||||
/// if so, otherwise caches the blob in the data availability checker.
|
||||
async fn check_rpc_blob_availability_and_import(
|
||||
@@ -3495,35 +3554,28 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
block_root: Hash256,
|
||||
blobs: FixedBlobSidecarList<T::EthSpec>,
|
||||
) -> Result<AvailabilityProcessingStatus, BlockError> {
|
||||
// Need to scope this to ensure the lock is dropped before calling `process_availability`
|
||||
// Even an explicit drop is not enough to convince the borrow checker.
|
||||
{
|
||||
let mut slashable_cache = self.observed_slashable.write();
|
||||
for header in blobs
|
||||
.iter()
|
||||
.filter_map(|b| b.as_ref().map(|b| b.signed_block_header.clone()))
|
||||
.unique()
|
||||
{
|
||||
if verify_header_signature::<T, BlockError>(self, &header).is_ok() {
|
||||
slashable_cache
|
||||
.observe_slashable(
|
||||
header.message.slot,
|
||||
header.message.proposer_index,
|
||||
block_root,
|
||||
)
|
||||
.map_err(|e| BlockError::BeaconChainError(e.into()))?;
|
||||
if let Some(slasher) = self.slasher.as_ref() {
|
||||
slasher.accept_block_header(header);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let epoch = slot.epoch(T::EthSpec::slots_per_epoch());
|
||||
self.check_blobs_for_slashability(block_root, &blobs)?;
|
||||
let availability = self
|
||||
.data_availability_checker
|
||||
.put_rpc_blobs(block_root, epoch, blobs)?;
|
||||
.put_rpc_blobs(block_root, blobs)?;
|
||||
|
||||
self.process_availability(slot, availability, || Ok(()))
|
||||
self.process_availability(slot, availability, None, || Ok(()))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn check_engine_blob_availability_and_import(
|
||||
self: &Arc<Self>,
|
||||
slot: Slot,
|
||||
block_root: Hash256,
|
||||
blobs: FixedBlobSidecarList<T::EthSpec>,
|
||||
data_column_recv: Option<Receiver<DataColumnSidecarList<T::EthSpec>>>,
|
||||
) -> Result<AvailabilityProcessingStatus, BlockError> {
|
||||
self.check_blobs_for_slashability(block_root, &blobs)?;
|
||||
let availability = self
|
||||
.data_availability_checker
|
||||
.put_engine_blobs(block_root, blobs)?;
|
||||
|
||||
self.process_availability(slot, availability, data_column_recv, || Ok(()))
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -3559,13 +3611,11 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
|
||||
// This slot value is purely informative for the consumers of
|
||||
// `AvailabilityProcessingStatus::MissingComponents` to log an error with a slot.
|
||||
let availability = self.data_availability_checker.put_rpc_custody_columns(
|
||||
block_root,
|
||||
slot.epoch(T::EthSpec::slots_per_epoch()),
|
||||
custody_columns,
|
||||
)?;
|
||||
let availability = self
|
||||
.data_availability_checker
|
||||
.put_rpc_custody_columns(block_root, custody_columns)?;
|
||||
|
||||
self.process_availability(slot, availability, || Ok(()))
|
||||
self.process_availability(slot, availability, None, || Ok(()))
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -3577,13 +3627,14 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
self: &Arc<Self>,
|
||||
slot: Slot,
|
||||
availability: Availability<T::EthSpec>,
|
||||
recv: Option<Receiver<DataColumnSidecarList<T::EthSpec>>>,
|
||||
publish_fn: impl FnOnce() -> Result<(), BlockError>,
|
||||
) -> Result<AvailabilityProcessingStatus, BlockError> {
|
||||
match availability {
|
||||
Availability::Available(block) => {
|
||||
publish_fn()?;
|
||||
// Block is fully available, import into fork choice
|
||||
self.import_available_block(block).await
|
||||
self.import_available_block(block, recv).await
|
||||
}
|
||||
Availability::MissingComponents(block_root) => Ok(
|
||||
AvailabilityProcessingStatus::MissingComponents(slot, block_root),
|
||||
@@ -3594,6 +3645,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
pub async fn import_available_block(
|
||||
self: &Arc<Self>,
|
||||
block: Box<AvailableExecutedBlock<T::EthSpec>>,
|
||||
data_column_recv: Option<Receiver<DataColumnSidecarList<T::EthSpec>>>,
|
||||
) -> Result<AvailabilityProcessingStatus, BlockError> {
|
||||
let AvailableExecutedBlock {
|
||||
block,
|
||||
@@ -3635,6 +3687,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
parent_block,
|
||||
parent_eth1_finalization_data,
|
||||
consensus_context,
|
||||
data_column_recv,
|
||||
)
|
||||
},
|
||||
"payload_verification_handle",
|
||||
@@ -3673,6 +3726,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
parent_block: SignedBlindedBeaconBlock<T::EthSpec>,
|
||||
parent_eth1_finalization_data: Eth1FinalizationData,
|
||||
mut consensus_context: ConsensusContext<T::EthSpec>,
|
||||
data_column_recv: Option<Receiver<DataColumnSidecarList<T::EthSpec>>>,
|
||||
) -> Result<Hash256, BlockError> {
|
||||
// ----------------------------- BLOCK NOT YET ATTESTABLE ----------------------------------
|
||||
// Everything in this initial section is on the hot path between processing the block and
|
||||
@@ -3818,7 +3872,6 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
// state if we returned early without committing. In other words, an error here would
|
||||
// corrupt the node's database permanently.
|
||||
// -----------------------------------------------------------------------------------------
|
||||
|
||||
self.import_block_update_shuffling_cache(block_root, &mut state);
|
||||
self.import_block_observe_attestations(
|
||||
block,
|
||||
@@ -3835,15 +3888,53 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
);
|
||||
self.import_block_update_slasher(block, &state, &mut consensus_context);
|
||||
|
||||
let db_write_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_DB_WRITE);
|
||||
|
||||
// Store the block and its state, and execute the confirmation batch for the intermediate
|
||||
// states, which will delete their temporary flags.
|
||||
// If the write fails, revert fork choice to the version from disk, else we can
|
||||
// end up with blocks in fork choice that are missing from disk.
|
||||
// See https://github.com/sigp/lighthouse/issues/2028
|
||||
let (_, signed_block, blobs, data_columns) = signed_block.deconstruct();
|
||||
// TODO(das) we currently store all subnet sampled columns. Tracking issue to exclude non
|
||||
// custody columns: https://github.com/sigp/lighthouse/issues/6465
|
||||
let custody_columns_count = self.data_availability_checker.get_sampling_column_count();
|
||||
// if block is made available via blobs, dropped the data columns.
|
||||
let data_columns = data_columns.filter(|columns| columns.len() == custody_columns_count);
|
||||
|
||||
let data_columns = match (data_columns, data_column_recv) {
|
||||
// If the block was made available via custody columns received from gossip / rpc, use them
|
||||
// since we already have them.
|
||||
(Some(columns), _) => Some(columns),
|
||||
// Otherwise, it means blobs were likely available via fetching from EL, in this case we
|
||||
// wait for the data columns to be computed (blocking).
|
||||
(None, Some(mut data_column_recv)) => {
|
||||
let _column_recv_timer =
|
||||
metrics::start_timer(&metrics::BLOCK_PROCESSING_DATA_COLUMNS_WAIT);
|
||||
// Unable to receive data columns from sender, sender is either dropped or
|
||||
// failed to compute data columns from blobs. We restore fork choice here and
|
||||
// return to avoid inconsistency in database.
|
||||
if let Some(columns) = data_column_recv.blocking_recv() {
|
||||
Some(columns)
|
||||
} else {
|
||||
let err_msg = "Did not receive data columns from sender";
|
||||
error!(
|
||||
self.log,
|
||||
"Failed to store data columns into the database";
|
||||
"msg" => "Restoring fork choice from disk",
|
||||
"error" => err_msg,
|
||||
);
|
||||
return Err(self
|
||||
.handle_import_block_db_write_error(fork_choice)
|
||||
.err()
|
||||
.unwrap_or(BlockError::InternalError(err_msg.to_string())));
|
||||
}
|
||||
}
|
||||
// No data columns present and compute data columns task was not spawned.
|
||||
// Could either be no blobs in the block or before PeerDAS activation.
|
||||
(None, None) => None,
|
||||
};
|
||||
|
||||
let block = signed_block.message();
|
||||
let db_write_timer = metrics::start_timer(&metrics::BLOCK_PROCESSING_DB_WRITE);
|
||||
ops.extend(
|
||||
confirmed_state_roots
|
||||
.into_iter()
|
||||
@@ -3885,33 +3976,10 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
"msg" => "Restoring fork choice from disk",
|
||||
"error" => ?e,
|
||||
);
|
||||
|
||||
// Clear the early attester cache to prevent attestations which we would later be unable
|
||||
// to verify due to the failure.
|
||||
self.early_attester_cache.clear();
|
||||
|
||||
// Since the write failed, try to revert the canonical head back to what was stored
|
||||
// in the database. This attempts to prevent inconsistency between the database and
|
||||
// fork choice.
|
||||
if let Err(e) = self.canonical_head.restore_from_store(
|
||||
fork_choice,
|
||||
ResetPayloadStatuses::always_reset_conditionally(
|
||||
self.config.always_reset_payload_statuses,
|
||||
),
|
||||
&self.store,
|
||||
&self.spec,
|
||||
&self.log,
|
||||
) {
|
||||
crit!(
|
||||
self.log,
|
||||
"No stored fork choice found to restore from";
|
||||
"error" => ?e,
|
||||
"warning" => "The database is likely corrupt now, consider --purge-db"
|
||||
);
|
||||
return Err(BlockError::BeaconChainError(e));
|
||||
}
|
||||
|
||||
return Err(e.into());
|
||||
return Err(self
|
||||
.handle_import_block_db_write_error(fork_choice)
|
||||
.err()
|
||||
.unwrap_or(e.into()));
|
||||
}
|
||||
drop(txn_lock);
|
||||
|
||||
@@ -3979,6 +4047,41 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
Ok(block_root)
|
||||
}
|
||||
|
||||
fn handle_import_block_db_write_error(
|
||||
&self,
|
||||
// We don't actually need this value, however it's always present when we call this function
|
||||
// and it needs to be dropped to prevent a dead-lock. Requiring it to be passed here is
|
||||
// defensive programming.
|
||||
fork_choice_write_lock: RwLockWriteGuard<BeaconForkChoice<T>>,
|
||||
) -> Result<(), BlockError> {
|
||||
// Clear the early attester cache to prevent attestations which we would later be unable
|
||||
// to verify due to the failure.
|
||||
self.early_attester_cache.clear();
|
||||
|
||||
// Since the write failed, try to revert the canonical head back to what was stored
|
||||
// in the database. This attempts to prevent inconsistency between the database and
|
||||
// fork choice.
|
||||
if let Err(e) = self.canonical_head.restore_from_store(
|
||||
fork_choice_write_lock,
|
||||
ResetPayloadStatuses::always_reset_conditionally(
|
||||
self.config.always_reset_payload_statuses,
|
||||
),
|
||||
&self.store,
|
||||
&self.spec,
|
||||
&self.log,
|
||||
) {
|
||||
crit!(
|
||||
self.log,
|
||||
"No stored fork choice found to restore from";
|
||||
"error" => ?e,
|
||||
"warning" => "The database is likely corrupt now, consider --purge-db"
|
||||
);
|
||||
Err(BlockError::BeaconChainError(e))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Check block's consistentency with any configured weak subjectivity checkpoint.
|
||||
fn check_block_against_weak_subjectivity_checkpoint(
|
||||
&self,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use derivative::Derivative;
|
||||
use slot_clock::SlotClock;
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::beacon_chain::{BeaconChain, BeaconChainTypes};
|
||||
@@ -8,11 +9,11 @@ use crate::block_verification::{
|
||||
BlockSlashInfo,
|
||||
};
|
||||
use crate::kzg_utils::{validate_blob, validate_blobs};
|
||||
use crate::observed_data_sidecars::{DoNotObserve, ObservationStrategy, Observe};
|
||||
use crate::{metrics, BeaconChainError};
|
||||
use kzg::{Error as KzgError, Kzg, KzgCommitment};
|
||||
use slog::debug;
|
||||
use ssz_derive::{Decode, Encode};
|
||||
use ssz_types::VariableList;
|
||||
use std::time::Duration;
|
||||
use tree_hash::TreeHash;
|
||||
use types::blob_sidecar::BlobIdentifier;
|
||||
@@ -156,20 +157,16 @@ impl From<BeaconStateError> for GossipBlobError {
|
||||
}
|
||||
}
|
||||
|
||||
pub type GossipVerifiedBlobList<T> = VariableList<
|
||||
GossipVerifiedBlob<T>,
|
||||
<<T as BeaconChainTypes>::EthSpec as EthSpec>::MaxBlobsPerBlock,
|
||||
>;
|
||||
|
||||
/// A wrapper around a `BlobSidecar` that indicates it has been approved for re-gossiping on
|
||||
/// the p2p network.
|
||||
#[derive(Debug)]
|
||||
pub struct GossipVerifiedBlob<T: BeaconChainTypes> {
|
||||
pub struct GossipVerifiedBlob<T: BeaconChainTypes, O: ObservationStrategy = Observe> {
|
||||
block_root: Hash256,
|
||||
blob: KzgVerifiedBlob<T::EthSpec>,
|
||||
_phantom: PhantomData<O>,
|
||||
}
|
||||
|
||||
impl<T: BeaconChainTypes> GossipVerifiedBlob<T> {
|
||||
impl<T: BeaconChainTypes, O: ObservationStrategy> GossipVerifiedBlob<T, O> {
|
||||
pub fn new(
|
||||
blob: Arc<BlobSidecar<T::EthSpec>>,
|
||||
subnet_id: u64,
|
||||
@@ -178,7 +175,7 @@ impl<T: BeaconChainTypes> GossipVerifiedBlob<T> {
|
||||
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| {
|
||||
validate_blob_sidecar_for_gossip::<T, O>(blob, subnet_id, chain).map_err(|e| {
|
||||
process_block_slash_info::<_, GossipBlobError>(
|
||||
chain,
|
||||
BlockSlashInfo::from_early_error_blob(header, e),
|
||||
@@ -195,6 +192,7 @@ impl<T: BeaconChainTypes> GossipVerifiedBlob<T> {
|
||||
blob,
|
||||
seen_timestamp: Duration::from_secs(0),
|
||||
},
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
pub fn id(&self) -> BlobIdentifier {
|
||||
@@ -335,6 +333,25 @@ impl<E: EthSpec> KzgVerifiedBlobList<E> {
|
||||
verified_blobs: blobs,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a `KzgVerifiedBlobList` from `blobs` that are already KZG verified.
|
||||
///
|
||||
/// This should be used with caution, as used incorrectly it could result in KZG verification
|
||||
/// being skipped and invalid blobs being deemed valid.
|
||||
pub fn from_verified<I: IntoIterator<Item = Arc<BlobSidecar<E>>>>(
|
||||
blobs: I,
|
||||
seen_timestamp: Duration,
|
||||
) -> Self {
|
||||
Self {
|
||||
verified_blobs: blobs
|
||||
.into_iter()
|
||||
.map(|blob| KzgVerifiedBlob {
|
||||
blob,
|
||||
seen_timestamp,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: EthSpec> IntoIterator for KzgVerifiedBlobList<E> {
|
||||
@@ -364,11 +381,11 @@ where
|
||||
validate_blobs::<E>(kzg, commitments.as_slice(), blobs, proofs.as_slice())
|
||||
}
|
||||
|
||||
pub fn validate_blob_sidecar_for_gossip<T: BeaconChainTypes>(
|
||||
pub fn validate_blob_sidecar_for_gossip<T: BeaconChainTypes, O: ObservationStrategy>(
|
||||
blob_sidecar: Arc<BlobSidecar<T::EthSpec>>,
|
||||
subnet: u64,
|
||||
chain: &BeaconChain<T>,
|
||||
) -> Result<GossipVerifiedBlob<T>, GossipBlobError> {
|
||||
) -> Result<GossipVerifiedBlob<T, O>, GossipBlobError> {
|
||||
let blob_slot = blob_sidecar.slot();
|
||||
let blob_index = blob_sidecar.index;
|
||||
let block_parent_root = blob_sidecar.block_parent_root();
|
||||
@@ -568,16 +585,45 @@ pub fn validate_blob_sidecar_for_gossip<T: BeaconChainTypes>(
|
||||
)
|
||||
.map_err(|e| GossipBlobError::BeaconChainError(e.into()))?;
|
||||
|
||||
if O::observe() {
|
||||
observe_gossip_blob(&kzg_verified_blob.blob, chain)?;
|
||||
}
|
||||
|
||||
Ok(GossipVerifiedBlob {
|
||||
block_root,
|
||||
blob: kzg_verified_blob,
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
impl<T: BeaconChainTypes> GossipVerifiedBlob<T, DoNotObserve> {
|
||||
pub fn observe(
|
||||
self,
|
||||
chain: &BeaconChain<T>,
|
||||
) -> Result<GossipVerifiedBlob<T, Observe>, GossipBlobError> {
|
||||
observe_gossip_blob(&self.blob.blob, chain)?;
|
||||
Ok(GossipVerifiedBlob {
|
||||
block_root: self.block_root,
|
||||
blob: self.blob,
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn observe_gossip_blob<T: BeaconChainTypes>(
|
||||
blob_sidecar: &BlobSidecar<T::EthSpec>,
|
||||
chain: &BeaconChain<T>,
|
||||
) -> 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.
|
||||
// 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
|
||||
// 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
|
||||
@@ -586,16 +632,12 @@ pub fn validate_blob_sidecar_for_gossip<T: BeaconChainTypes>(
|
||||
.map_err(|e| GossipBlobError::BeaconChainError(e.into()))?
|
||||
{
|
||||
return Err(GossipBlobError::RepeatBlob {
|
||||
proposer: proposer_index as u64,
|
||||
slot: blob_slot,
|
||||
index: blob_index,
|
||||
proposer: blob_sidecar.block_proposer_index(),
|
||||
slot: blob_sidecar.slot(),
|
||||
index: blob_sidecar.index,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(GossipVerifiedBlob {
|
||||
block_root,
|
||||
blob: kzg_verified_blob,
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the canonical root of the given `blob`.
|
||||
|
||||
@@ -683,7 +683,7 @@ pub struct SignatureVerifiedBlock<T: BeaconChainTypes> {
|
||||
consensus_context: ConsensusContext<T::EthSpec>,
|
||||
}
|
||||
|
||||
/// Used to await the result of executing payload with a remote EE.
|
||||
/// Used to await the result of executing payload with an EE.
|
||||
type PayloadVerificationHandle = JoinHandle<Option<Result<PayloadVerificationOutcome, BlockError>>>;
|
||||
|
||||
/// A wrapper around a `SignedBeaconBlock` that indicates that this block is fully verified and
|
||||
@@ -750,7 +750,8 @@ pub fn build_blob_data_column_sidecars<T: BeaconChainTypes>(
|
||||
&metrics::DATA_COLUMN_SIDECAR_COMPUTATION,
|
||||
&[&blobs.len().to_string()],
|
||||
);
|
||||
let sidecars = blobs_to_data_column_sidecars(&blobs, block, &chain.kzg, &chain.spec)
|
||||
let blob_refs = blobs.iter().collect::<Vec<_>>();
|
||||
let sidecars = blobs_to_data_column_sidecars(&blob_refs, block, &chain.kzg, &chain.spec)
|
||||
.discard_timer_on_break(&mut timer)?;
|
||||
drop(timer);
|
||||
Ok(sidecars)
|
||||
@@ -1343,7 +1344,6 @@ impl<T: BeaconChainTypes> ExecutionPendingBlock<T> {
|
||||
/*
|
||||
* Perform cursory checks to see if the block is even worth processing.
|
||||
*/
|
||||
|
||||
check_block_relevancy(block.as_block(), block_root, chain)?;
|
||||
|
||||
// Define a future that will verify the execution payload with an execution engine.
|
||||
|
||||
@@ -88,6 +88,12 @@ pub struct ChainConfig {
|
||||
pub malicious_withhold_count: usize,
|
||||
/// Enable peer sampling on blocks.
|
||||
pub enable_sampling: bool,
|
||||
/// Number of batches that the node splits blobs or data columns into during publication.
|
||||
/// This doesn't apply if the node is the block proposer. For PeerDAS only.
|
||||
pub blob_publication_batches: usize,
|
||||
/// The delay in milliseconds applied by the node between sending each blob or data column batch.
|
||||
/// This doesn't apply if the node is the block proposer.
|
||||
pub blob_publication_batch_interval: Duration,
|
||||
}
|
||||
|
||||
impl Default for ChainConfig {
|
||||
@@ -121,6 +127,8 @@ impl Default for ChainConfig {
|
||||
enable_light_client_server: false,
|
||||
malicious_withhold_count: 0,
|
||||
enable_sampling: false,
|
||||
blob_publication_batches: 4,
|
||||
blob_publication_batch_interval: Duration::from_millis(300),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use task_executor::TaskExecutor;
|
||||
use types::blob_sidecar::{BlobIdentifier, BlobSidecar, FixedBlobSidecarList};
|
||||
use types::{
|
||||
BlobSidecarList, ChainSpec, DataColumnIdentifier, DataColumnSidecar, DataColumnSidecarList,
|
||||
Epoch, EthSpec, Hash256, RuntimeVariableList, SignedBeaconBlock, Slot,
|
||||
Epoch, EthSpec, Hash256, RuntimeVariableList, SignedBeaconBlock,
|
||||
};
|
||||
|
||||
mod error;
|
||||
@@ -146,6 +146,10 @@ impl<T: BeaconChainTypes> DataAvailabilityChecker<T> {
|
||||
self.availability_cache.sampling_column_count()
|
||||
}
|
||||
|
||||
pub(crate) fn is_supernode(&self) -> bool {
|
||||
self.get_sampling_column_count() == self.spec.number_of_columns
|
||||
}
|
||||
|
||||
/// Checks if the block root is currenlty in the availability cache awaiting import because
|
||||
/// of missing components.
|
||||
pub fn get_execution_valid_block(
|
||||
@@ -201,7 +205,6 @@ impl<T: BeaconChainTypes> DataAvailabilityChecker<T> {
|
||||
pub fn put_rpc_blobs(
|
||||
&self,
|
||||
block_root: Hash256,
|
||||
epoch: Epoch,
|
||||
blobs: FixedBlobSidecarList<T::EthSpec>,
|
||||
) -> Result<Availability<T::EthSpec>, AvailabilityCheckError> {
|
||||
let seen_timestamp = self
|
||||
@@ -212,15 +215,12 @@ impl<T: BeaconChainTypes> DataAvailabilityChecker<T> {
|
||||
// Note: currently not reporting which specific blob is invalid because we fetch all blobs
|
||||
// from the same peer for both lookup and range sync.
|
||||
|
||||
let verified_blobs = KzgVerifiedBlobList::new(
|
||||
Vec::from(blobs).into_iter().flatten(),
|
||||
&self.kzg,
|
||||
seen_timestamp,
|
||||
)
|
||||
.map_err(AvailabilityCheckError::InvalidBlobs)?;
|
||||
let verified_blobs =
|
||||
KzgVerifiedBlobList::new(blobs.iter().flatten().cloned(), &self.kzg, seen_timestamp)
|
||||
.map_err(AvailabilityCheckError::InvalidBlobs)?;
|
||||
|
||||
self.availability_cache
|
||||
.put_kzg_verified_blobs(block_root, epoch, verified_blobs, &self.log)
|
||||
.put_kzg_verified_blobs(block_root, verified_blobs, &self.log)
|
||||
}
|
||||
|
||||
/// Put a list of custody columns received via RPC into the availability cache. This performs KZG
|
||||
@@ -229,7 +229,6 @@ impl<T: BeaconChainTypes> DataAvailabilityChecker<T> {
|
||||
pub fn put_rpc_custody_columns(
|
||||
&self,
|
||||
block_root: Hash256,
|
||||
epoch: Epoch,
|
||||
custody_columns: DataColumnSidecarList<T::EthSpec>,
|
||||
) -> Result<Availability<T::EthSpec>, AvailabilityCheckError> {
|
||||
// TODO(das): report which column is invalid for proper peer scoring
|
||||
@@ -248,12 +247,32 @@ impl<T: BeaconChainTypes> DataAvailabilityChecker<T> {
|
||||
|
||||
self.availability_cache.put_kzg_verified_data_columns(
|
||||
block_root,
|
||||
epoch,
|
||||
verified_custody_columns,
|
||||
&self.log,
|
||||
)
|
||||
}
|
||||
|
||||
/// Put a list of blobs received from the EL pool into the availability cache.
|
||||
///
|
||||
/// This DOES NOT perform KZG verification because the KZG proofs should have been constructed
|
||||
/// immediately prior to calling this function so they are assumed to be valid.
|
||||
pub fn put_engine_blobs(
|
||||
&self,
|
||||
block_root: Hash256,
|
||||
blobs: FixedBlobSidecarList<T::EthSpec>,
|
||||
) -> Result<Availability<T::EthSpec>, AvailabilityCheckError> {
|
||||
let seen_timestamp = self
|
||||
.slot_clock
|
||||
.now_duration()
|
||||
.ok_or(AvailabilityCheckError::SlotClockError)?;
|
||||
|
||||
let verified_blobs =
|
||||
KzgVerifiedBlobList::from_verified(blobs.iter().flatten().cloned(), seen_timestamp);
|
||||
|
||||
self.availability_cache
|
||||
.put_kzg_verified_blobs(block_root, verified_blobs, &self.log)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -265,7 +284,6 @@ impl<T: BeaconChainTypes> DataAvailabilityChecker<T> {
|
||||
) -> Result<Availability<T::EthSpec>, AvailabilityCheckError> {
|
||||
self.availability_cache.put_kzg_verified_blobs(
|
||||
gossip_blob.block_root(),
|
||||
gossip_blob.epoch(),
|
||||
vec![gossip_blob.into_inner()],
|
||||
&self.log,
|
||||
)
|
||||
@@ -279,12 +297,9 @@ impl<T: BeaconChainTypes> DataAvailabilityChecker<T> {
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn put_gossip_data_columns(
|
||||
&self,
|
||||
slot: Slot,
|
||||
block_root: Hash256,
|
||||
gossip_data_columns: Vec<GossipVerifiedDataColumn<T>>,
|
||||
) -> Result<Availability<T::EthSpec>, AvailabilityCheckError> {
|
||||
let epoch = slot.epoch(T::EthSpec::slots_per_epoch());
|
||||
|
||||
let custody_columns = gossip_data_columns
|
||||
.into_iter()
|
||||
.map(|c| KzgVerifiedCustodyDataColumn::from_asserted_custody(c.into_inner()))
|
||||
@@ -292,7 +307,6 @@ impl<T: BeaconChainTypes> DataAvailabilityChecker<T> {
|
||||
|
||||
self.availability_cache.put_kzg_verified_data_columns(
|
||||
block_root,
|
||||
epoch,
|
||||
custody_columns,
|
||||
&self.log,
|
||||
)
|
||||
@@ -595,12 +609,7 @@ impl<T: BeaconChainTypes> DataAvailabilityChecker<T> {
|
||||
);
|
||||
|
||||
self.availability_cache
|
||||
.put_kzg_verified_data_columns(
|
||||
*block_root,
|
||||
slot.epoch(T::EthSpec::slots_per_epoch()),
|
||||
data_columns_to_publish.clone(),
|
||||
&self.log,
|
||||
)
|
||||
.put_kzg_verified_data_columns(*block_root, data_columns_to_publish.clone(), &self.log)
|
||||
.map(|availability| {
|
||||
DataColumnReconstructionResult::Success((
|
||||
availability,
|
||||
|
||||
@@ -10,7 +10,6 @@ pub enum Error {
|
||||
blob_commitment: KzgCommitment,
|
||||
block_commitment: KzgCommitment,
|
||||
},
|
||||
UnableToDetermineImportRequirement,
|
||||
Unexpected,
|
||||
SszTypes(ssz_types::Error),
|
||||
MissingBlobs,
|
||||
@@ -44,7 +43,6 @@ impl Error {
|
||||
| Error::Unexpected
|
||||
| Error::ParentStateMissing(_)
|
||||
| Error::BlockReplayError(_)
|
||||
| Error::UnableToDetermineImportRequirement
|
||||
| Error::RebuildingStateCaches(_)
|
||||
| Error::SlotClockError => ErrorCategory::Internal,
|
||||
Error::InvalidBlobs { .. }
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::BeaconChainTypes;
|
||||
use lru::LruCache;
|
||||
use parking_lot::RwLock;
|
||||
use slog::{debug, Logger};
|
||||
use ssz_types::{FixedVector, VariableList};
|
||||
use ssz_types::FixedVector;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
use types::blob_sidecar::BlobIdentifier;
|
||||
@@ -34,11 +34,6 @@ pub struct PendingComponents<E: EthSpec> {
|
||||
pub reconstruction_started: bool,
|
||||
}
|
||||
|
||||
pub enum BlockImportRequirement {
|
||||
AllBlobs,
|
||||
ColumnSampling(usize),
|
||||
}
|
||||
|
||||
impl<E: EthSpec> PendingComponents<E> {
|
||||
/// Returns an immutable reference to the cached block.
|
||||
pub fn get_cached_block(&self) -> &Option<DietAvailabilityPendingExecutedBlock<E>> {
|
||||
@@ -199,63 +194,49 @@ impl<E: EthSpec> PendingComponents<E> {
|
||||
///
|
||||
/// Returns `true` if both the block exists and the number of received blobs / custody columns
|
||||
/// matches the number of expected blobs / custody columns.
|
||||
pub fn is_available(
|
||||
&self,
|
||||
block_import_requirement: &BlockImportRequirement,
|
||||
log: &Logger,
|
||||
) -> bool {
|
||||
pub fn is_available(&self, custody_column_count: usize, log: &Logger) -> bool {
|
||||
let block_kzg_commitments_count_opt = self.block_kzg_commitments_count();
|
||||
let expected_blobs_msg = block_kzg_commitments_count_opt
|
||||
.as_ref()
|
||||
.map(|num| num.to_string())
|
||||
.unwrap_or("unknown".to_string());
|
||||
|
||||
match block_import_requirement {
|
||||
BlockImportRequirement::AllBlobs => {
|
||||
let received_blobs = self.num_received_blobs();
|
||||
let expected_blobs_msg = block_kzg_commitments_count_opt
|
||||
.as_ref()
|
||||
.map(|num| num.to_string())
|
||||
.unwrap_or("unknown".to_string());
|
||||
|
||||
debug!(log,
|
||||
"Component(s) added to data availability checker";
|
||||
"block_root" => ?self.block_root,
|
||||
"received_block" => block_kzg_commitments_count_opt.is_some(),
|
||||
"received_blobs" => received_blobs,
|
||||
"expected_blobs" => expected_blobs_msg,
|
||||
);
|
||||
|
||||
block_kzg_commitments_count_opt.map_or(false, |num_expected_blobs| {
|
||||
num_expected_blobs == received_blobs
|
||||
})
|
||||
// No data columns when there are 0 blobs
|
||||
let expected_columns_opt = block_kzg_commitments_count_opt.map(|blob_count| {
|
||||
if blob_count > 0 {
|
||||
custody_column_count
|
||||
} else {
|
||||
0
|
||||
}
|
||||
BlockImportRequirement::ColumnSampling(num_expected_columns) => {
|
||||
// No data columns when there are 0 blobs
|
||||
let expected_columns_opt = block_kzg_commitments_count_opt.map(|blob_count| {
|
||||
if blob_count > 0 {
|
||||
*num_expected_columns
|
||||
} else {
|
||||
0
|
||||
}
|
||||
});
|
||||
});
|
||||
let expected_columns_msg = expected_columns_opt
|
||||
.as_ref()
|
||||
.map(|num| num.to_string())
|
||||
.unwrap_or("unknown".to_string());
|
||||
|
||||
let expected_columns_msg = expected_columns_opt
|
||||
.as_ref()
|
||||
.map(|num| num.to_string())
|
||||
.unwrap_or("unknown".to_string());
|
||||
let num_received_blobs = self.num_received_blobs();
|
||||
let num_received_columns = self.num_received_data_columns();
|
||||
|
||||
let num_received_columns = self.num_received_data_columns();
|
||||
debug!(
|
||||
log,
|
||||
"Component(s) added to data availability checker";
|
||||
"block_root" => ?self.block_root,
|
||||
"received_blobs" => num_received_blobs,
|
||||
"expected_blobs" => expected_blobs_msg,
|
||||
"received_columns" => num_received_columns,
|
||||
"expected_columns" => expected_columns_msg,
|
||||
);
|
||||
|
||||
debug!(log,
|
||||
"Component(s) added to data availability checker";
|
||||
"block_root" => ?self.block_root,
|
||||
"received_block" => block_kzg_commitments_count_opt.is_some(),
|
||||
"received_columns" => num_received_columns,
|
||||
"expected_columns" => expected_columns_msg,
|
||||
);
|
||||
let all_blobs_received = block_kzg_commitments_count_opt
|
||||
.map_or(false, |num_expected_blobs| {
|
||||
num_expected_blobs == num_received_blobs
|
||||
});
|
||||
|
||||
expected_columns_opt.map_or(false, |num_expected_columns| {
|
||||
num_expected_columns == num_received_columns
|
||||
})
|
||||
}
|
||||
}
|
||||
let all_columns_received = expected_columns_opt.map_or(false, |num_expected_columns| {
|
||||
num_expected_columns == num_received_columns
|
||||
});
|
||||
|
||||
all_blobs_received || all_columns_received
|
||||
}
|
||||
|
||||
/// Returns an empty `PendingComponents` object with the given block root.
|
||||
@@ -277,7 +258,6 @@ impl<E: EthSpec> PendingComponents<E> {
|
||||
/// reconstructed from disk. Ensure you are not holding any write locks while calling this.
|
||||
pub fn make_available<R>(
|
||||
self,
|
||||
block_import_requirement: BlockImportRequirement,
|
||||
spec: &Arc<ChainSpec>,
|
||||
recover: R,
|
||||
) -> Result<Availability<E>, AvailabilityCheckError>
|
||||
@@ -304,26 +284,25 @@ impl<E: EthSpec> PendingComponents<E> {
|
||||
return Err(AvailabilityCheckError::Unexpected);
|
||||
};
|
||||
|
||||
let (blobs, data_columns) = match block_import_requirement {
|
||||
BlockImportRequirement::AllBlobs => {
|
||||
let num_blobs_expected = diet_executed_block.num_blobs_expected();
|
||||
let Some(verified_blobs) = verified_blobs
|
||||
.into_iter()
|
||||
.map(|b| b.map(|b| b.to_blob()))
|
||||
.take(num_blobs_expected)
|
||||
.collect::<Option<Vec<_>>>()
|
||||
else {
|
||||
return Err(AvailabilityCheckError::Unexpected);
|
||||
};
|
||||
(Some(VariableList::new(verified_blobs)?), None)
|
||||
}
|
||||
BlockImportRequirement::ColumnSampling(_) => {
|
||||
let verified_data_columns = verified_data_columns
|
||||
.into_iter()
|
||||
.map(|d| d.into_inner())
|
||||
.collect();
|
||||
(None, Some(verified_data_columns))
|
||||
}
|
||||
let is_peer_das_enabled = spec.is_peer_das_enabled_for_epoch(diet_executed_block.epoch());
|
||||
let (blobs, data_columns) = if is_peer_das_enabled {
|
||||
let data_columns = verified_data_columns
|
||||
.into_iter()
|
||||
.map(|d| d.into_inner())
|
||||
.collect::<Vec<_>>();
|
||||
(None, Some(data_columns))
|
||||
} else {
|
||||
let num_blobs_expected = diet_executed_block.num_blobs_expected();
|
||||
let Some(verified_blobs) = verified_blobs
|
||||
.into_iter()
|
||||
.map(|b| b.map(|b| b.to_blob()))
|
||||
.take(num_blobs_expected)
|
||||
.collect::<Option<Vec<_>>>()
|
||||
.map(Into::into)
|
||||
else {
|
||||
return Err(AvailabilityCheckError::Unexpected);
|
||||
};
|
||||
(Some(verified_blobs), None)
|
||||
};
|
||||
|
||||
let executed_block = recover(diet_executed_block)?;
|
||||
@@ -475,24 +454,9 @@ impl<T: BeaconChainTypes> DataAvailabilityCheckerInner<T> {
|
||||
f(self.critical.read().peek(block_root))
|
||||
}
|
||||
|
||||
fn block_import_requirement(
|
||||
&self,
|
||||
epoch: Epoch,
|
||||
) -> Result<BlockImportRequirement, AvailabilityCheckError> {
|
||||
let peer_das_enabled = self.spec.is_peer_das_enabled_for_epoch(epoch);
|
||||
if peer_das_enabled {
|
||||
Ok(BlockImportRequirement::ColumnSampling(
|
||||
self.sampling_column_count,
|
||||
))
|
||||
} else {
|
||||
Ok(BlockImportRequirement::AllBlobs)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn put_kzg_verified_blobs<I: IntoIterator<Item = KzgVerifiedBlob<T::EthSpec>>>(
|
||||
&self,
|
||||
block_root: Hash256,
|
||||
epoch: Epoch,
|
||||
kzg_verified_blobs: I,
|
||||
log: &Logger,
|
||||
) -> Result<Availability<T::EthSpec>, AvailabilityCheckError> {
|
||||
@@ -515,12 +479,11 @@ impl<T: BeaconChainTypes> DataAvailabilityCheckerInner<T> {
|
||||
// Merge in the blobs.
|
||||
pending_components.merge_blobs(fixed_blobs);
|
||||
|
||||
let block_import_requirement = self.block_import_requirement(epoch)?;
|
||||
if pending_components.is_available(&block_import_requirement, log) {
|
||||
if pending_components.is_available(self.sampling_column_count, log) {
|
||||
write_lock.put(block_root, pending_components.clone());
|
||||
// No need to hold the write lock anymore
|
||||
drop(write_lock);
|
||||
pending_components.make_available(block_import_requirement, &self.spec, |diet_block| {
|
||||
pending_components.make_available(&self.spec, |diet_block| {
|
||||
self.state_cache.recover_pending_executed_block(diet_block)
|
||||
})
|
||||
} else {
|
||||
@@ -535,7 +498,6 @@ impl<T: BeaconChainTypes> DataAvailabilityCheckerInner<T> {
|
||||
>(
|
||||
&self,
|
||||
block_root: Hash256,
|
||||
epoch: Epoch,
|
||||
kzg_verified_data_columns: I,
|
||||
log: &Logger,
|
||||
) -> Result<Availability<T::EthSpec>, AvailabilityCheckError> {
|
||||
@@ -550,13 +512,11 @@ impl<T: BeaconChainTypes> DataAvailabilityCheckerInner<T> {
|
||||
// Merge in the data columns.
|
||||
pending_components.merge_data_columns(kzg_verified_data_columns)?;
|
||||
|
||||
let block_import_requirement = self.block_import_requirement(epoch)?;
|
||||
|
||||
if pending_components.is_available(&block_import_requirement, log) {
|
||||
if pending_components.is_available(self.sampling_column_count, log) {
|
||||
write_lock.put(block_root, pending_components.clone());
|
||||
// No need to hold the write lock anymore
|
||||
drop(write_lock);
|
||||
pending_components.make_available(block_import_requirement, &self.spec, |diet_block| {
|
||||
pending_components.make_available(&self.spec, |diet_block| {
|
||||
self.state_cache.recover_pending_executed_block(diet_block)
|
||||
})
|
||||
} else {
|
||||
@@ -625,7 +585,6 @@ impl<T: BeaconChainTypes> DataAvailabilityCheckerInner<T> {
|
||||
) -> Result<Availability<T::EthSpec>, AvailabilityCheckError> {
|
||||
let mut write_lock = self.critical.write();
|
||||
let block_root = executed_block.import_data.block_root;
|
||||
let epoch = executed_block.block.epoch();
|
||||
|
||||
// register the block to get the diet block
|
||||
let diet_executed_block = self
|
||||
@@ -642,12 +601,11 @@ impl<T: BeaconChainTypes> DataAvailabilityCheckerInner<T> {
|
||||
pending_components.merge_block(diet_executed_block);
|
||||
|
||||
// Check if we have all components and entire set is consistent.
|
||||
let block_import_requirement = self.block_import_requirement(epoch)?;
|
||||
if pending_components.is_available(&block_import_requirement, log) {
|
||||
if pending_components.is_available(self.sampling_column_count, log) {
|
||||
write_lock.put(block_root, pending_components.clone());
|
||||
// No need to hold the write lock anymore
|
||||
drop(write_lock);
|
||||
pending_components.make_available(block_import_requirement, &self.spec, |diet_block| {
|
||||
pending_components.make_available(&self.spec, |diet_block| {
|
||||
self.state_cache.recover_pending_executed_block(diet_block)
|
||||
})
|
||||
} else {
|
||||
@@ -703,6 +661,7 @@ impl<T: BeaconChainTypes> DataAvailabilityCheckerInner<T> {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use crate::{
|
||||
blob_verification::GossipVerifiedBlob,
|
||||
block_verification::PayloadVerificationOutcome,
|
||||
@@ -712,6 +671,7 @@ mod test {
|
||||
test_utils::{BaseHarnessType, BeaconChainHarness, DiskHarnessType},
|
||||
};
|
||||
use fork_choice::PayloadVerificationStatus;
|
||||
|
||||
use logging::test_logger;
|
||||
use slog::{info, Logger};
|
||||
use state_processing::ConsensusContext;
|
||||
@@ -931,7 +891,6 @@ mod test {
|
||||
|
||||
let (pending_block, blobs) = 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();
|
||||
assert_eq!(
|
||||
@@ -980,7 +939,7 @@ mod test {
|
||||
for (blob_index, gossip_blob) in blobs.into_iter().enumerate() {
|
||||
kzg_verified_blobs.push(gossip_blob.into_inner());
|
||||
let availability = cache
|
||||
.put_kzg_verified_blobs(root, epoch, kzg_verified_blobs.clone(), harness.logger())
|
||||
.put_kzg_verified_blobs(root, kzg_verified_blobs.clone(), harness.logger())
|
||||
.expect("should put blob");
|
||||
if blob_index == blobs_expected - 1 {
|
||||
assert!(matches!(availability, Availability::Available(_)));
|
||||
@@ -1002,12 +961,11 @@ mod test {
|
||||
"should have expected number of blobs"
|
||||
);
|
||||
let root = pending_block.import_data.block_root;
|
||||
let epoch = pending_block.block.epoch();
|
||||
let mut kzg_verified_blobs = vec![];
|
||||
for gossip_blob in blobs {
|
||||
kzg_verified_blobs.push(gossip_blob.into_inner());
|
||||
let availability = cache
|
||||
.put_kzg_verified_blobs(root, epoch, kzg_verified_blobs.clone(), harness.logger())
|
||||
.put_kzg_verified_blobs(root, kzg_verified_blobs.clone(), harness.logger())
|
||||
.expect("should put blob");
|
||||
assert_eq!(
|
||||
availability,
|
||||
|
||||
@@ -57,6 +57,11 @@ impl<E: EthSpec> DietAvailabilityPendingExecutedBlock<E> {
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns the epoch corresponding to `self.slot()`.
|
||||
pub fn epoch(&self) -> Epoch {
|
||||
self.block.slot().epoch(E::slots_per_epoch())
|
||||
}
|
||||
}
|
||||
|
||||
/// This LRU cache holds BeaconStates used for block import. If the cache overflows,
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::block_verification::{
|
||||
BlockSlashInfo,
|
||||
};
|
||||
use crate::kzg_utils::{reconstruct_data_columns, validate_data_columns};
|
||||
use crate::observed_data_sidecars::{ObservationStrategy, Observe};
|
||||
use crate::{metrics, BeaconChain, BeaconChainError, BeaconChainTypes};
|
||||
use derivative::Derivative;
|
||||
use fork_choice::ProtoBlock;
|
||||
@@ -13,6 +14,7 @@ use slog::debug;
|
||||
use slot_clock::SlotClock;
|
||||
use ssz_derive::{Decode, Encode};
|
||||
use std::iter;
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
use types::data_column_sidecar::{ColumnIndex, DataColumnIdentifier};
|
||||
use types::{
|
||||
@@ -160,17 +162,16 @@ impl From<BeaconStateError> for GossipDataColumnError {
|
||||
}
|
||||
}
|
||||
|
||||
pub type GossipVerifiedDataColumnList<T> = RuntimeVariableList<GossipVerifiedDataColumn<T>>;
|
||||
|
||||
/// A wrapper around a `DataColumnSidecar` that indicates it has been approved for re-gossiping on
|
||||
/// the p2p network.
|
||||
#[derive(Debug)]
|
||||
pub struct GossipVerifiedDataColumn<T: BeaconChainTypes> {
|
||||
pub struct GossipVerifiedDataColumn<T: BeaconChainTypes, O: ObservationStrategy = Observe> {
|
||||
block_root: Hash256,
|
||||
data_column: KzgVerifiedDataColumn<T::EthSpec>,
|
||||
_phantom: PhantomData<O>,
|
||||
}
|
||||
|
||||
impl<T: BeaconChainTypes> GossipVerifiedDataColumn<T> {
|
||||
impl<T: BeaconChainTypes, O: ObservationStrategy> GossipVerifiedDataColumn<T, O> {
|
||||
pub fn new(
|
||||
column_sidecar: Arc<DataColumnSidecar<T::EthSpec>>,
|
||||
subnet_id: u64,
|
||||
@@ -179,12 +180,14 @@ impl<T: BeaconChainTypes> GossipVerifiedDataColumn<T> {
|
||||
let header = column_sidecar.signed_block_header.clone();
|
||||
// We only process slashing info if the gossip verification failed
|
||||
// since we do not process the data column any further in that case.
|
||||
validate_data_column_sidecar_for_gossip(column_sidecar, subnet_id, chain).map_err(|e| {
|
||||
process_block_slash_info::<_, GossipDataColumnError>(
|
||||
chain,
|
||||
BlockSlashInfo::from_early_error_data_column(header, e),
|
||||
)
|
||||
})
|
||||
validate_data_column_sidecar_for_gossip::<T, O>(column_sidecar, subnet_id, chain).map_err(
|
||||
|e| {
|
||||
process_block_slash_info::<_, GossipDataColumnError>(
|
||||
chain,
|
||||
BlockSlashInfo::from_early_error_data_column(header, e),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn id(&self) -> DataColumnIdentifier {
|
||||
@@ -375,11 +378,11 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_data_column_sidecar_for_gossip<T: BeaconChainTypes>(
|
||||
pub fn validate_data_column_sidecar_for_gossip<T: BeaconChainTypes, O: ObservationStrategy>(
|
||||
data_column: Arc<DataColumnSidecar<T::EthSpec>>,
|
||||
subnet: u64,
|
||||
chain: &BeaconChain<T>,
|
||||
) -> Result<GossipVerifiedDataColumn<T>, GossipDataColumnError> {
|
||||
) -> Result<GossipVerifiedDataColumn<T, O>, GossipDataColumnError> {
|
||||
let column_slot = data_column.slot();
|
||||
verify_data_column_sidecar(&data_column, &chain.spec)?;
|
||||
verify_index_matches_subnet(&data_column, subnet, &chain.spec)?;
|
||||
@@ -404,9 +407,14 @@ pub fn validate_data_column_sidecar_for_gossip<T: BeaconChainTypes>(
|
||||
)
|
||||
.map_err(|e| GossipDataColumnError::BeaconChainError(e.into()))?;
|
||||
|
||||
if O::observe() {
|
||||
observe_gossip_data_column(&kzg_verified_data_column.data, chain)?;
|
||||
}
|
||||
|
||||
Ok(GossipVerifiedDataColumn {
|
||||
block_root: data_column.block_root(),
|
||||
data_column: kzg_verified_data_column,
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -648,11 +656,42 @@ fn verify_sidecar_not_from_future_slot<T: BeaconChainTypes>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn observe_gossip_data_column<T: BeaconChainTypes>(
|
||||
data_column_sidecar: &DataColumnSidecar<T::EthSpec>,
|
||||
chain: &BeaconChain<T>,
|
||||
) -> Result<(), GossipDataColumnError> {
|
||||
// Now the signature is valid, store the proposal so we don't accept another data column sidecar
|
||||
// with the same `DataColumnIdentifier`. 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 DataColumnSidecar goes on to fail full verification, we do not evict it from the
|
||||
// seen_cache as alternate data_column_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_column_sidecars
|
||||
.write()
|
||||
.observe_sidecar(data_column_sidecar)
|
||||
.map_err(|e| GossipDataColumnError::BeaconChainError(e.into()))?
|
||||
{
|
||||
return Err(GossipDataColumnError::PriorKnown {
|
||||
proposer: data_column_sidecar.block_proposer_index(),
|
||||
slot: data_column_sidecar.slot(),
|
||||
index: data_column_sidecar.index,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::data_column_verification::{
|
||||
validate_data_column_sidecar_for_gossip, GossipDataColumnError,
|
||||
};
|
||||
use crate::observed_data_sidecars::Observe;
|
||||
use crate::test_utils::BeaconChainHarness;
|
||||
use types::{DataColumnSidecar, EthSpec, ForkName, MainnetEthSpec};
|
||||
|
||||
@@ -691,8 +730,11 @@ mod test {
|
||||
.unwrap(),
|
||||
};
|
||||
|
||||
let result =
|
||||
validate_data_column_sidecar_for_gossip(column_sidecar.into(), index, &harness.chain);
|
||||
let result = validate_data_column_sidecar_for_gossip::<_, Observe>(
|
||||
column_sidecar.into(),
|
||||
index,
|
||||
&harness.chain,
|
||||
);
|
||||
assert!(matches!(
|
||||
result.err(),
|
||||
Some(GossipDataColumnError::UnexpectedDataColumn)
|
||||
|
||||
308
beacon_node/beacon_chain/src/fetch_blobs.rs
Normal file
308
beacon_node/beacon_chain/src/fetch_blobs.rs
Normal file
@@ -0,0 +1,308 @@
|
||||
//! This module implements an optimisation to fetch blobs via JSON-RPC from the EL.
|
||||
//! If a blob has already been seen in the public mempool, then it is often unnecessary to wait for
|
||||
//! it to arrive on P2P gossip. This PR uses a new JSON-RPC method (`engine_getBlobsV1`) which
|
||||
//! allows the CL to load the blobs quickly from the EL's blob pool.
|
||||
//!
|
||||
//! Once the node fetches the blobs from EL, it then publishes the remaining blobs that it hasn't seen
|
||||
//! on P2P gossip to the network. From PeerDAS onwards, together with the increase in blob count,
|
||||
//! broadcasting blobs requires a much higher bandwidth, and is only done by high capacity
|
||||
//! supernodes.
|
||||
use crate::blob_verification::{GossipBlobError, GossipVerifiedBlob};
|
||||
use crate::kzg_utils::blobs_to_data_column_sidecars;
|
||||
use crate::observed_data_sidecars::DoNotObserve;
|
||||
use crate::{metrics, AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, BlockError};
|
||||
use execution_layer::json_structures::BlobAndProofV1;
|
||||
use execution_layer::Error as ExecutionLayerError;
|
||||
use metrics::{inc_counter, inc_counter_by, TryExt};
|
||||
use slog::{debug, error, o, Logger};
|
||||
use ssz_types::FixedVector;
|
||||
use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use types::blob_sidecar::{BlobSidecarError, FixedBlobSidecarList};
|
||||
use types::{
|
||||
BeaconStateError, BlobSidecar, DataColumnSidecar, DataColumnSidecarList, EthSpec, FullPayload,
|
||||
Hash256, SignedBeaconBlock, SignedBeaconBlockHeader,
|
||||
};
|
||||
|
||||
pub enum BlobsOrDataColumns<T: BeaconChainTypes> {
|
||||
Blobs(Vec<GossipVerifiedBlob<T, DoNotObserve>>),
|
||||
DataColumns(DataColumnSidecarList<T::EthSpec>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FetchEngineBlobError {
|
||||
BeaconStateError(BeaconStateError),
|
||||
BlobProcessingError(BlockError),
|
||||
BlobSidecarError(BlobSidecarError),
|
||||
ExecutionLayerMissing,
|
||||
InternalError(String),
|
||||
GossipBlob(GossipBlobError),
|
||||
RequestFailed(ExecutionLayerError),
|
||||
RuntimeShutdown,
|
||||
}
|
||||
|
||||
/// 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`.
|
||||
pub async fn fetch_and_process_engine_blobs<T: BeaconChainTypes>(
|
||||
chain: Arc<BeaconChain<T>>,
|
||||
block_root: Hash256,
|
||||
block: Arc<SignedBeaconBlock<T::EthSpec, FullPayload<T::EthSpec>>>,
|
||||
publish_fn: impl Fn(BlobsOrDataColumns<T>) + Send + 'static,
|
||||
) -> Result<Option<AvailabilityProcessingStatus>, FetchEngineBlobError> {
|
||||
let block_root_str = format!("{:?}", block_root);
|
||||
let log = chain
|
||||
.log
|
||||
.new(o!("service" => "fetch_engine_blobs", "block_root" => block_root_str));
|
||||
|
||||
let versioned_hashes = if let Some(kzg_commitments) = block
|
||||
.message()
|
||||
.body()
|
||||
.blob_kzg_commitments()
|
||||
.ok()
|
||||
.filter(|blobs| !blobs.is_empty())
|
||||
{
|
||||
kzg_commitments
|
||||
.iter()
|
||||
.map(kzg_commitment_to_versioned_hash)
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
debug!(
|
||||
log,
|
||||
"Fetch blobs not triggered - none required";
|
||||
);
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let num_expected_blobs = versioned_hashes.len();
|
||||
|
||||
let execution_layer = chain
|
||||
.execution_layer
|
||||
.as_ref()
|
||||
.ok_or(FetchEngineBlobError::ExecutionLayerMissing)?;
|
||||
|
||||
debug!(
|
||||
log,
|
||||
"Fetching blobs from the EL";
|
||||
"num_expected_blobs" => num_expected_blobs,
|
||||
);
|
||||
let response = execution_layer
|
||||
.get_blobs(versioned_hashes)
|
||||
.await
|
||||
.map_err(FetchEngineBlobError::RequestFailed)?;
|
||||
|
||||
if response.is_empty() {
|
||||
debug!(
|
||||
log,
|
||||
"No blobs fetched from the EL";
|
||||
"num_expected_blobs" => num_expected_blobs,
|
||||
);
|
||||
inc_counter(&metrics::BLOBS_FROM_EL_MISS_TOTAL);
|
||||
return Ok(None);
|
||||
} else {
|
||||
inc_counter(&metrics::BLOBS_FROM_EL_HIT_TOTAL);
|
||||
}
|
||||
|
||||
let (signed_block_header, kzg_commitments_proof) = block
|
||||
.signed_block_header_and_kzg_commitments_proof()
|
||||
.map_err(FetchEngineBlobError::BeaconStateError)?;
|
||||
|
||||
let fixed_blob_sidecar_list = build_blob_sidecars(
|
||||
&block,
|
||||
response,
|
||||
signed_block_header,
|
||||
&kzg_commitments_proof,
|
||||
)?;
|
||||
|
||||
let num_fetched_blobs = fixed_blob_sidecar_list
|
||||
.iter()
|
||||
.filter(|b| b.is_some())
|
||||
.count();
|
||||
|
||||
inc_counter_by(
|
||||
&metrics::BLOBS_FROM_EL_EXPECTED_TOTAL,
|
||||
num_expected_blobs as u64,
|
||||
);
|
||||
inc_counter_by(
|
||||
&metrics::BLOBS_FROM_EL_RECEIVED_TOTAL,
|
||||
num_fetched_blobs as u64,
|
||||
);
|
||||
|
||||
// Gossip verify blobs before publishing. This prevents blobs with invalid KZG proofs from
|
||||
// the EL making it into the data availability checker. We do not immediately add these
|
||||
// blobs to the observed blobs/columns cache because we want to allow blobs/columns to arrive on gossip
|
||||
// and be accepted (and propagated) while we are waiting to publish. Just before publishing
|
||||
// we will observe the blobs/columns and only proceed with publishing if they are not yet seen.
|
||||
let blobs_to_import_and_publish = fixed_blob_sidecar_list
|
||||
.iter()
|
||||
.filter_map(|opt_blob| {
|
||||
let blob = opt_blob.as_ref()?;
|
||||
match GossipVerifiedBlob::<T, DoNotObserve>::new(blob.clone(), blob.index, &chain) {
|
||||
Ok(verified) => Some(Ok(verified)),
|
||||
// Ignore already seen blobs.
|
||||
Err(GossipBlobError::RepeatBlob { .. }) => None,
|
||||
Err(e) => Some(Err(e)),
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(FetchEngineBlobError::GossipBlob)?;
|
||||
|
||||
let peer_das_enabled = chain.spec.is_peer_das_enabled_for_epoch(block.epoch());
|
||||
|
||||
let data_columns_receiver_opt = if peer_das_enabled {
|
||||
// Partial blobs response isn't useful for PeerDAS, so we don't bother building and publishing data columns.
|
||||
if num_fetched_blobs != num_expected_blobs {
|
||||
debug!(
|
||||
log,
|
||||
"Not all blobs fetched from the EL";
|
||||
"info" => "Unable to compute data columns",
|
||||
"num_fetched_blobs" => num_fetched_blobs,
|
||||
"num_expected_blobs" => num_expected_blobs,
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let data_columns_receiver = spawn_compute_and_publish_data_columns_task(
|
||||
&chain,
|
||||
block.clone(),
|
||||
fixed_blob_sidecar_list.clone(),
|
||||
publish_fn,
|
||||
log.clone(),
|
||||
);
|
||||
|
||||
Some(data_columns_receiver)
|
||||
} else {
|
||||
if !blobs_to_import_and_publish.is_empty() {
|
||||
publish_fn(BlobsOrDataColumns::Blobs(blobs_to_import_and_publish));
|
||||
}
|
||||
|
||||
None
|
||||
};
|
||||
|
||||
debug!(
|
||||
log,
|
||||
"Processing engine blobs";
|
||||
"num_fetched_blobs" => num_fetched_blobs,
|
||||
);
|
||||
|
||||
let availability_processing_status = chain
|
||||
.process_engine_blobs(
|
||||
block.slot(),
|
||||
block_root,
|
||||
fixed_blob_sidecar_list.clone(),
|
||||
data_columns_receiver_opt,
|
||||
)
|
||||
.await
|
||||
.map_err(FetchEngineBlobError::BlobProcessingError)?;
|
||||
|
||||
Ok(Some(availability_processing_status))
|
||||
}
|
||||
|
||||
/// Spawn a blocking task here for long computation tasks, so it doesn't block processing, and it
|
||||
/// allows blobs / data columns to propagate without waiting for processing.
|
||||
///
|
||||
/// An `mpsc::Sender` is then used to send the produced data columns to the `beacon_chain` for it
|
||||
/// to be persisted, **after** the block is made attestable.
|
||||
///
|
||||
/// The reason for doing this is to make the block available and attestable as soon as possible,
|
||||
/// while maintaining the invariant that block and data columns are persisted atomically.
|
||||
fn spawn_compute_and_publish_data_columns_task<T: BeaconChainTypes>(
|
||||
chain: &Arc<BeaconChain<T>>,
|
||||
block: Arc<SignedBeaconBlock<T::EthSpec, FullPayload<T::EthSpec>>>,
|
||||
blobs: FixedBlobSidecarList<T::EthSpec>,
|
||||
publish_fn: impl Fn(BlobsOrDataColumns<T>) + Send + 'static,
|
||||
log: Logger,
|
||||
) -> Receiver<Vec<Arc<DataColumnSidecar<T::EthSpec>>>> {
|
||||
let chain_cloned = chain.clone();
|
||||
let (data_columns_sender, data_columns_receiver) = tokio::sync::mpsc::channel(1);
|
||||
|
||||
chain.task_executor.spawn_blocking(
|
||||
move || {
|
||||
let mut timer = metrics::start_timer_vec(
|
||||
&metrics::DATA_COLUMN_SIDECAR_COMPUTATION,
|
||||
&[&blobs.len().to_string()],
|
||||
);
|
||||
let blob_refs = blobs
|
||||
.iter()
|
||||
.filter_map(|b| b.as_ref().map(|b| &b.blob))
|
||||
.collect::<Vec<_>>();
|
||||
let data_columns_result = blobs_to_data_column_sidecars(
|
||||
&blob_refs,
|
||||
&block,
|
||||
&chain_cloned.kzg,
|
||||
&chain_cloned.spec,
|
||||
)
|
||||
.discard_timer_on_break(&mut timer);
|
||||
drop(timer);
|
||||
|
||||
let all_data_columns = match data_columns_result {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
error!(
|
||||
log,
|
||||
"Failed to build data column sidecars from blobs";
|
||||
"error" => ?e
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = data_columns_sender.try_send(all_data_columns.clone()) {
|
||||
error!(log, "Failed to send computed data columns"; "error" => ?e);
|
||||
};
|
||||
|
||||
// Check indices from cache before sending the columns, to make sure we don't
|
||||
// publish components already seen on gossip.
|
||||
let is_supernode = chain_cloned.data_availability_checker.is_supernode();
|
||||
|
||||
// At the moment non supernodes are not required to publish any columns.
|
||||
// TODO(das): we could experiment with having full nodes publish their custodied
|
||||
// columns here.
|
||||
if !is_supernode {
|
||||
return;
|
||||
}
|
||||
|
||||
publish_fn(BlobsOrDataColumns::DataColumns(all_data_columns));
|
||||
},
|
||||
"compute_and_publish_data_columns",
|
||||
);
|
||||
|
||||
data_columns_receiver
|
||||
}
|
||||
|
||||
fn build_blob_sidecars<E: EthSpec>(
|
||||
block: &Arc<SignedBeaconBlock<E, FullPayload<E>>>,
|
||||
response: Vec<Option<BlobAndProofV1<E>>>,
|
||||
signed_block_header: SignedBeaconBlockHeader,
|
||||
kzg_commitments_inclusion_proof: &FixedVector<Hash256, E::KzgCommitmentsInclusionProofDepth>,
|
||||
) -> Result<FixedBlobSidecarList<E>, FetchEngineBlobError> {
|
||||
let mut fixed_blob_sidecar_list = FixedBlobSidecarList::default();
|
||||
for (index, blob_and_proof) in response
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, opt_blob)| Some((i, opt_blob?)))
|
||||
{
|
||||
match BlobSidecar::new_with_existing_proof(
|
||||
index,
|
||||
blob_and_proof.blob,
|
||||
block,
|
||||
signed_block_header.clone(),
|
||||
kzg_commitments_inclusion_proof,
|
||||
blob_and_proof.proof,
|
||||
) {
|
||||
Ok(blob) => {
|
||||
if let Some(blob_mut) = fixed_blob_sidecar_list.get_mut(index) {
|
||||
*blob_mut = Some(Arc::new(blob));
|
||||
} else {
|
||||
return Err(FetchEngineBlobError::InternalError(format!(
|
||||
"Blobs from EL contains blob with invalid index {index}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(FetchEngineBlobError::BlobSidecarError(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(fixed_blob_sidecar_list)
|
||||
}
|
||||
@@ -7,8 +7,8 @@ use std::sync::Arc;
|
||||
use types::beacon_block_body::KzgCommitments;
|
||||
use types::data_column_sidecar::{Cell, DataColumn, DataColumnSidecarError};
|
||||
use types::{
|
||||
Blob, BlobsList, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec,
|
||||
Hash256, KzgCommitment, KzgProof, KzgProofs, SignedBeaconBlock, SignedBeaconBlockHeader,
|
||||
Blob, ChainSpec, ColumnIndex, DataColumnSidecar, DataColumnSidecarList, EthSpec, Hash256,
|
||||
KzgCommitment, KzgProof, KzgProofs, SignedBeaconBlock, SignedBeaconBlockHeader,
|
||||
};
|
||||
|
||||
/// Converts a blob ssz List object to an array to be used with the kzg
|
||||
@@ -146,7 +146,7 @@ pub fn verify_kzg_proof<E: EthSpec>(
|
||||
|
||||
/// Build data column sidecars from a signed beacon block and its blobs.
|
||||
pub fn blobs_to_data_column_sidecars<E: EthSpec>(
|
||||
blobs: &BlobsList<E>,
|
||||
blobs: &[&Blob<E>],
|
||||
block: &SignedBeaconBlock<E>,
|
||||
kzg: &Kzg,
|
||||
spec: &ChainSpec,
|
||||
@@ -154,6 +154,7 @@ pub fn blobs_to_data_column_sidecars<E: EthSpec>(
|
||||
if blobs.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let kzg_commitments = block
|
||||
.message()
|
||||
.body()
|
||||
@@ -312,19 +313,21 @@ mod test {
|
||||
#[track_caller]
|
||||
fn test_build_data_columns_empty(kzg: &Kzg, spec: &ChainSpec) {
|
||||
let num_of_blobs = 0;
|
||||
let (signed_block, blob_sidecars) = create_test_block_and_blobs::<E>(num_of_blobs, spec);
|
||||
let (signed_block, blobs) = create_test_block_and_blobs::<E>(num_of_blobs, spec);
|
||||
let blob_refs = blobs.iter().collect::<Vec<_>>();
|
||||
let column_sidecars =
|
||||
blobs_to_data_column_sidecars(&blob_sidecars, &signed_block, kzg, spec).unwrap();
|
||||
blobs_to_data_column_sidecars(&blob_refs, &signed_block, kzg, spec).unwrap();
|
||||
assert!(column_sidecars.is_empty());
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn test_build_data_columns(kzg: &Kzg, spec: &ChainSpec) {
|
||||
let num_of_blobs = 6;
|
||||
let (signed_block, blob_sidecars) = create_test_block_and_blobs::<E>(num_of_blobs, spec);
|
||||
let (signed_block, blobs) = create_test_block_and_blobs::<E>(num_of_blobs, spec);
|
||||
|
||||
let blob_refs = blobs.iter().collect::<Vec<_>>();
|
||||
let column_sidecars =
|
||||
blobs_to_data_column_sidecars(&blob_sidecars, &signed_block, kzg, spec).unwrap();
|
||||
blobs_to_data_column_sidecars(&blob_refs, &signed_block, kzg, spec).unwrap();
|
||||
|
||||
let block_kzg_commitments = signed_block
|
||||
.message()
|
||||
@@ -358,9 +361,10 @@ mod test {
|
||||
#[track_caller]
|
||||
fn test_reconstruct_data_columns(kzg: &Kzg, spec: &ChainSpec) {
|
||||
let num_of_blobs = 6;
|
||||
let (signed_block, blob_sidecars) = create_test_block_and_blobs::<E>(num_of_blobs, spec);
|
||||
let (signed_block, blobs) = create_test_block_and_blobs::<E>(num_of_blobs, spec);
|
||||
let blob_refs = blobs.iter().collect::<Vec<_>>();
|
||||
let column_sidecars =
|
||||
blobs_to_data_column_sidecars(&blob_sidecars, &signed_block, kzg, spec).unwrap();
|
||||
blobs_to_data_column_sidecars(&blob_refs, &signed_block, kzg, spec).unwrap();
|
||||
|
||||
// Now reconstruct
|
||||
let reconstructed_columns = reconstruct_data_columns(
|
||||
|
||||
@@ -28,6 +28,7 @@ pub mod eth1_chain;
|
||||
mod eth1_finalization_cache;
|
||||
pub mod events;
|
||||
pub mod execution_payload;
|
||||
pub mod fetch_blobs;
|
||||
pub mod fork_choice_signal;
|
||||
pub mod fork_revert;
|
||||
pub mod graffiti_calculator;
|
||||
@@ -43,7 +44,7 @@ mod naive_aggregation_pool;
|
||||
pub mod observed_aggregates;
|
||||
mod observed_attesters;
|
||||
pub mod observed_block_producers;
|
||||
mod observed_data_sidecars;
|
||||
pub mod observed_data_sidecars;
|
||||
pub mod observed_operations;
|
||||
mod observed_slashable;
|
||||
pub mod otb_verification_service;
|
||||
|
||||
@@ -111,6 +111,13 @@ pub static BLOCK_PROCESSING_POST_EXEC_PROCESSING: LazyLock<Result<Histogram>> =
|
||||
linear_buckets(5e-3, 5e-3, 10),
|
||||
)
|
||||
});
|
||||
pub static BLOCK_PROCESSING_DATA_COLUMNS_WAIT: LazyLock<Result<Histogram>> = LazyLock::new(|| {
|
||||
try_create_histogram_with_buckets(
|
||||
"beacon_block_processing_data_columns_wait_seconds",
|
||||
"Time spent waiting for data columns to be computed before starting database write",
|
||||
exponential_buckets(0.01, 2.0, 10),
|
||||
)
|
||||
});
|
||||
pub static BLOCK_PROCESSING_DB_WRITE: LazyLock<Result<Histogram>> = LazyLock::new(|| {
|
||||
try_create_histogram(
|
||||
"beacon_block_processing_db_write_seconds",
|
||||
@@ -1691,6 +1698,34 @@ pub static DATA_COLUMNS_SIDECAR_PROCESSING_SUCCESSES: LazyLock<Result<IntCounter
|
||||
)
|
||||
});
|
||||
|
||||
pub static BLOBS_FROM_EL_HIT_TOTAL: LazyLock<Result<IntCounter>> = LazyLock::new(|| {
|
||||
try_create_int_counter(
|
||||
"beacon_blobs_from_el_hit_total",
|
||||
"Number of blob batches fetched from the execution layer",
|
||||
)
|
||||
});
|
||||
|
||||
pub static BLOBS_FROM_EL_MISS_TOTAL: LazyLock<Result<IntCounter>> = LazyLock::new(|| {
|
||||
try_create_int_counter(
|
||||
"beacon_blobs_from_el_miss_total",
|
||||
"Number of blob batches failed to fetch from the execution layer",
|
||||
)
|
||||
});
|
||||
|
||||
pub static BLOBS_FROM_EL_EXPECTED_TOTAL: LazyLock<Result<IntCounter>> = LazyLock::new(|| {
|
||||
try_create_int_counter(
|
||||
"beacon_blobs_from_el_expected_total",
|
||||
"Number of blobs expected from the execution layer",
|
||||
)
|
||||
});
|
||||
|
||||
pub static BLOBS_FROM_EL_RECEIVED_TOTAL: LazyLock<Result<IntCounter>> = LazyLock::new(|| {
|
||||
try_create_int_counter(
|
||||
"beacon_blobs_from_el_received_total",
|
||||
"Number of blobs fetched from the execution layer",
|
||||
)
|
||||
});
|
||||
|
||||
/*
|
||||
* Light server message verification
|
||||
*/
|
||||
|
||||
@@ -148,6 +148,31 @@ impl<T: ObservableDataSidecar> ObservedDataSidecars<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Abstraction to control "observation" of gossip messages (currently just blobs and data columns).
|
||||
///
|
||||
/// If a type returns `false` for `observe` then the message will not be immediately added to its
|
||||
/// respective gossip observation cache. Unobserved messages should usually be observed later.
|
||||
pub trait ObservationStrategy {
|
||||
fn observe() -> bool;
|
||||
}
|
||||
|
||||
/// Type for messages that are observed immediately.
|
||||
pub struct Observe;
|
||||
/// Type for messages that have not been observed.
|
||||
pub struct DoNotObserve;
|
||||
|
||||
impl ObservationStrategy for Observe {
|
||||
fn observe() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl ObservationStrategy for DoNotObserve {
|
||||
fn observe() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -2894,7 +2894,6 @@ pub fn generate_rand_block_and_blobs<E: EthSpec>(
|
||||
(block, blob_sidecars)
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn generate_rand_block_and_data_columns<E: EthSpec>(
|
||||
fork_name: ForkName,
|
||||
num_blobs: NumBlobs,
|
||||
@@ -2902,12 +2901,12 @@ pub fn generate_rand_block_and_data_columns<E: EthSpec>(
|
||||
spec: &ChainSpec,
|
||||
) -> (
|
||||
SignedBeaconBlock<E, FullPayload<E>>,
|
||||
Vec<Arc<DataColumnSidecar<E>>>,
|
||||
DataColumnSidecarList<E>,
|
||||
) {
|
||||
let kzg = get_kzg(spec);
|
||||
let (block, blobs) = generate_rand_block_and_blobs(fork_name, num_blobs, rng);
|
||||
let blob: BlobsList<E> = blobs.into_iter().map(|b| b.blob).collect::<Vec<_>>().into();
|
||||
let data_columns = blobs_to_data_column_sidecars(&blob, &block, &kzg, spec).unwrap();
|
||||
let blob_refs = blobs.iter().map(|b| &b.blob).collect::<Vec<_>>();
|
||||
let data_columns = blobs_to_data_column_sidecars(&blob_refs, &block, &kzg, spec).unwrap();
|
||||
|
||||
(block, data_columns)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user