diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 13fd7e910..84039e422 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -1981,7 +1981,7 @@ impl BeaconChain { self: &Arc, blob_sidecar: SignedBlobSidecar, subnet_id: u64, - ) -> Result, BlobError> { + ) -> Result, BlobError> { blob_verification::validate_blob_sidecar_for_gossip(blob_sidecar, subnet_id, self) } @@ -2711,7 +2711,7 @@ impl BeaconChain { /// Returns an `Err` if the given block was invalid, or an error was encountered during pub async fn verify_block_for_gossip( self: &Arc, - block: BlockWrapper, + block: Arc>, ) -> Result, BlockError> { let chain = self.clone(); self.task_executor @@ -2755,7 +2755,7 @@ impl BeaconChain { pub async fn process_blob( self: &Arc, - blob: GossipVerifiedBlob, + blob: GossipVerifiedBlob, ) -> Result> { self.check_availability_and_maybe_import(blob.slot(), |chain| { chain.data_availability_checker.put_gossip_blob(blob) diff --git a/beacon_node/beacon_chain/src/blob_verification.rs b/beacon_node/beacon_chain/src/blob_verification.rs index 501e0ae59..da3ae2c93 100644 --- a/beacon_node/beacon_chain/src/blob_verification.rs +++ b/beacon_node/beacon_chain/src/blob_verification.rs @@ -16,7 +16,7 @@ use eth2::types::BlockContentsTuple; use kzg::Kzg; use slog::{debug, warn}; use ssz_derive::{Decode, Encode}; -use ssz_types::FixedVector; +use ssz_types::{FixedVector, VariableList}; use std::borrow::Cow; use types::blob_sidecar::{BlobIdentifier, FixedBlobSidecarList}; use types::{ @@ -125,25 +125,40 @@ impl From for BlobError { } } +pub type GossipVerifiedBlobList = VariableList< + GossipVerifiedBlob, + <::EthSpec as EthSpec>::MaxBlobsPerBlock, +>; + /// A wrapper around a `BlobSidecar` that indicates it has been approved for re-gossiping on /// the p2p network. -#[derive(Debug, Clone)] -pub struct GossipVerifiedBlob { - blob: Arc>, +#[derive(Debug)] +pub struct GossipVerifiedBlob { + blob: SignedBlobSidecar, } -impl GossipVerifiedBlob { +impl GossipVerifiedBlob { + pub fn new( + blob: SignedBlobSidecar, + chain: &BeaconChain, + ) -> Result> { + let blob_index = blob.message.index; + validate_blob_sidecar_for_gossip(blob, blob_index, chain) + } pub fn id(&self) -> BlobIdentifier { - self.blob.id() + self.blob.message.id() } pub fn block_root(&self) -> Hash256 { - self.blob.block_root + self.blob.message.block_root } - pub fn to_blob(self) -> Arc> { - self.blob + pub fn to_blob(self) -> Arc> { + self.blob.message + } + pub fn signed_blob(&self) -> SignedBlobSidecar { + self.blob.clone() } pub fn slot(&self) -> Slot { - self.blob.slot + self.blob.message.slot } } @@ -151,7 +166,7 @@ pub fn validate_blob_sidecar_for_gossip( signed_blob_sidecar: SignedBlobSidecar, subnet: u64, chain: &BeaconChain, -) -> Result, BlobError> { +) -> Result, BlobError> { let blob_slot = signed_blob_sidecar.message.slot; let blob_index = signed_blob_sidecar.message.index; let block_parent_root = signed_blob_sidecar.message.block_parent_root; @@ -366,7 +381,7 @@ pub fn validate_blob_sidecar_for_gossip( // 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 - // retreieval of potentially valid blocks over rpc, but try to punish the proposer for signing + // 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 @@ -383,7 +398,7 @@ pub fn validate_blob_sidecar_for_gossip( } Ok(GossipVerifiedBlob { - blob: signed_blob_sidecar.message, + blob: signed_blob_sidecar, }) } diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index f82fd7e28..796712ed2 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -48,7 +48,10 @@ // returned alongside. #![allow(clippy::result_large_err)] -use crate::blob_verification::{AsBlock, BlobError, BlockWrapper, MaybeAvailableBlock}; +use crate::blob_verification::{ + AsBlock, BlobError, BlockWrapper, GossipVerifiedBlob, GossipVerifiedBlobList, + MaybeAvailableBlock, +}; use crate::data_availability_checker::{ AvailabilityCheckError, AvailabilityPendingBlock, AvailableBlock, }; @@ -79,6 +82,7 @@ use slog::{debug, error, warn, Logger}; use slot_clock::SlotClock; use ssz::Encode; use ssz_derive::{Decode, Encode}; +use ssz_types::VariableList; use state_processing::per_block_processing::{errors::IntoWithIndex, is_merge_transition_block}; use state_processing::{ block_signature_verifier::{BlockSignatureVerifier, Error as BlockSignatureVerifierError}, @@ -803,48 +807,65 @@ pub struct BlockImportData { pub consensus_context: ConsensusContext, } -pub trait IntoGossipVerifiedBlock: Sized { +pub type GossipVerifiedBlockContents = + (GossipVerifiedBlock, Option>); + +pub trait IntoGossipVerifiedBlockContents: Sized { fn into_gossip_verified_block( self, chain: &BeaconChain, - ) -> Result, BlockError>; - fn inner(&self) -> &SignedBeaconBlock; - fn blobs(&self) -> Option>; + ) -> Result, BlockError>; + fn inner_block(&self) -> &SignedBeaconBlock; + fn inner_blobs(&self) -> Option>; } -impl IntoGossipVerifiedBlock - for ( - GossipVerifiedBlock, - Option>, - ) -{ +impl IntoGossipVerifiedBlockContents for GossipVerifiedBlockContents { fn into_gossip_verified_block( self, _chain: &BeaconChain, - ) -> Result, BlockError> { - Ok(self.0) + ) -> Result, BlockError> { + Ok(self) } - fn inner(&self) -> &SignedBeaconBlock { + fn inner_block(&self) -> &SignedBeaconBlock { self.0.block.as_block() } - fn blobs(&self) -> Option> { - self.1.clone() + fn inner_blobs(&self) -> Option> { + self.1.as_ref().map(|blobs| { + VariableList::from( + blobs + .into_iter() + .map(GossipVerifiedBlob::signed_blob) + .collect::>(), + ) + }) } } -impl IntoGossipVerifiedBlock for SignedBlockContents { +impl IntoGossipVerifiedBlockContents for SignedBlockContents { fn into_gossip_verified_block( self, chain: &BeaconChain, - ) -> Result, BlockError> { - GossipVerifiedBlock::new(self.deconstruct().into(), chain) + ) -> Result, BlockError> { + let (block, blobs) = self.deconstruct(); + let gossip_verified_block = GossipVerifiedBlock::new(Arc::new(block), chain)?; + let gossip_verified_blobs = blobs + .map(|blobs| { + Ok::<_, BlobError>(VariableList::from( + blobs + .into_iter() + .map(|blob| GossipVerifiedBlob::new(blob, chain)) + .collect::, BlobError>>()?, + )) + }) + .transpose()?; + Ok((gossip_verified_block, gossip_verified_blobs)) } - fn inner(&self) -> &SignedBeaconBlock { + fn inner_block(&self) -> &SignedBeaconBlock { self.signed_block() } - fn blobs(&self) -> Option> { + fn inner_blobs(&self) -> Option> { self.blobs_cloned() } } @@ -887,10 +908,12 @@ impl GossipVerifiedBlock { /// /// Returns an error if the block is invalid, or i8f the block was unable to be verified. pub fn new( - block: BlockWrapper, + block: Arc>, chain: &BeaconChain, ) -> Result> { - let maybe_available = chain.data_availability_checker.check_availability(block)?; + let maybe_available = chain + .data_availability_checker + .check_availability(block.into())?; // If the block is valid for gossip we don't supply it to the slasher here because // we assume it will be transformed into a fully verified block. We *do* need to supply // it to the slasher if an error occurs, because that's the end of this block's journey, diff --git a/beacon_node/beacon_chain/src/data_availability_checker.rs b/beacon_node/beacon_chain/src/data_availability_checker.rs index ed5e04e07..fc48ba47e 100644 --- a/beacon_node/beacon_chain/src/data_availability_checker.rs +++ b/beacon_node/beacon_chain/src/data_availability_checker.rs @@ -205,7 +205,7 @@ impl DataAvailabilityChecker { /// This should only accept gossip verified blobs, so we should not have to worry about dupes. pub fn put_gossip_blob( &self, - gossip_blob: GossipVerifiedBlob, + gossip_blob: GossipVerifiedBlob, ) -> Result, AvailabilityCheckError> { // Verify the KZG commitments. let kzg_verified_blob = if let Some(kzg) = self.kzg.as_ref() { 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 e969c283d..6b99e62de 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 @@ -1075,7 +1075,7 @@ mod test { log: Logger, ) -> ( AvailabilityPendingExecutedBlock, - Vec>, + Vec>>, ) where E: EthSpec, diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index 09981de8d..f394cabe0 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -71,7 +71,7 @@ pub use beacon_fork_choice_store::{BeaconForkChoiceStore, Error as ForkChoiceSto pub use block_verification::{ get_block_root, AvailabilityPendingExecutedBlock, BlockError, ExecutedBlock, ExecutionPayloadError, ExecutionPendingBlock, GossipVerifiedBlock, IntoExecutionPendingBlock, - IntoGossipVerifiedBlock, PayloadVerificationOutcome, PayloadVerificationStatus, + IntoGossipVerifiedBlockContents, PayloadVerificationOutcome, PayloadVerificationStatus, }; pub use canonical_head::{CachedHead, CanonicalHead, CanonicalHeadRwLock}; pub use eth1_chain::{Eth1Chain, Eth1ChainBackend}; diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 48bd8f2dd..374792364 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -851,7 +851,7 @@ async fn block_gossip_verification() { { let gossip_verified = harness .chain - .verify_block_for_gossip(snapshot.beacon_block.clone().into()) + .verify_block_for_gossip(snapshot.beacon_block.clone()) .await .expect("should obtain gossip verified block"); @@ -1079,11 +1079,7 @@ async fn block_gossip_verification() { let block = chain_segment[block_index].beacon_block.clone(); assert!( - harness - .chain - .verify_block_for_gossip(block.into()) - .await - .is_ok(), + harness.chain.verify_block_for_gossip(block).await.is_ok(), "the valid block should be processed" ); @@ -1134,7 +1130,7 @@ async fn verify_block_for_gossip_slashing_detection() { let verified_block = harness .chain - .verify_block_for_gossip(Arc::new(block1).into()) + .verify_block_for_gossip(Arc::new(block1)) .await .unwrap(); @@ -1161,7 +1157,7 @@ async fn verify_block_for_gossip_slashing_detection() { unwrap_err( harness .chain - .verify_block_for_gossip(Arc::new(block2).into()) + .verify_block_for_gossip(Arc::new(block2)) .await, ); @@ -1184,7 +1180,7 @@ async fn verify_block_for_gossip_doppelganger_detection() { let verified_block = harness .chain - .verify_block_for_gossip(Arc::new(block).into()) + .verify_block_for_gossip(Arc::new(block)) .await .unwrap(); let attestations = verified_block.block.message().body().attestations().clone(); diff --git a/beacon_node/http_api/src/publish_blocks.rs b/beacon_node/http_api/src/publish_blocks.rs index 719c1ebcb..542516081 100644 --- a/beacon_node/http_api/src/publish_blocks.rs +++ b/beacon_node/http_api/src/publish_blocks.rs @@ -4,7 +4,7 @@ use beacon_chain::blob_verification::AsBlock; use beacon_chain::validator_monitor::{get_block_delay_ms, timestamp_now}; use beacon_chain::{ AvailabilityProcessingStatus, BeaconChain, BeaconChainError, BeaconChainTypes, BlockError, - IntoGossipVerifiedBlock, NotifyExecutionLayer, + IntoGossipVerifiedBlockContents, NotifyExecutionLayer, }; use eth2::types::BroadcastValidation; use eth2::types::SignedBlockContents; @@ -24,7 +24,7 @@ use types::{ }; use warp::Rejection; -pub enum ProvenancedBlock> { +pub enum ProvenancedBlock> { /// The payload was built using a local EE. Local(B, PhantomData), /// The payload was build using a remote builder (e.g., via a mev-boost @@ -32,7 +32,7 @@ pub enum ProvenancedBlock> { Builder(B, PhantomData), } -impl> ProvenancedBlock { +impl> ProvenancedBlock { pub fn local(block: B) -> Self { Self::Local(block, PhantomData) } @@ -43,7 +43,7 @@ impl> ProvenancedBlock } /// Handles a request from the HTTP API for full blocks. -pub async fn publish_block>( +pub async fn publish_block>( block_root: Option, provenanced_block: ProvenancedBlock, chain: Arc>, @@ -57,7 +57,7 @@ pub async fn publish_block>( ProvenancedBlock::Local(block_contents, _) => (block_contents, true), ProvenancedBlock::Builder(block_contents, _) => (block_contents, false), }; - let block = block_contents.inner(); + let block = block_contents.inner_block(); let delay = get_block_delay_ms(seen_timestamp, block.message(), &chain.slot_clock); debug!(log, "Signed block received in HTTP API"; "slot" => block.slot()); @@ -111,10 +111,10 @@ pub async fn publish_block>( // We can clone this because the blobs are `Arc`'d in `BlockContents`, but the block is not, // so we avoid cloning the block at this point. - let blobs_opt = block_contents.blobs(); + let blobs_opt = block_contents.inner_blobs(); /* if we can form a `GossipVerifiedBlock`, we've passed our basic gossip checks */ - let gossip_verified_block = block_contents + let (gossip_verified_block, gossip_verified_blobs) = block_contents .into_gossip_verified_block(&chain) .map_err(|e| { warn!(log, "Not publishing block, not gossip verified"; "slot" => slot, "error" => ?e); @@ -175,6 +175,16 @@ pub async fn publish_block>( } }; + if let Some(gossip_verified_blobs) = gossip_verified_blobs { + for blob in gossip_verified_blobs { + if let Err(e) = chain.process_blob(blob).await { + return Err(warp_utils::reject::custom_bad_request(format!( + "Invalid blob: {e}" + ))); + } + } + } + match chain .process_block( block_root, diff --git a/beacon_node/http_api/tests/broadcast_validation_tests.rs b/beacon_node/http_api/tests/broadcast_validation_tests.rs index e8c97a77a..b525c23a5 100644 --- a/beacon_node/http_api/tests/broadcast_validation_tests.rs +++ b/beacon_node/http_api/tests/broadcast_validation_tests.rs @@ -1,12 +1,13 @@ use beacon_chain::{ test_utils::{AttestationStrategy, BlockStrategy}, - GossipVerifiedBlock, + GossipVerifiedBlock, IntoGossipVerifiedBlockContents, }; use eth2::types::{ BroadcastValidation, SignedBeaconBlock, SignedBlindedBeaconBlock, SignedBlockContents, }; use http_api::test_utils::InteractiveTester; use http_api::{publish_blinded_block, publish_block, reconstruct_block, ProvenancedBlock}; +use std::sync::Arc; use tree_hash::TreeHash; use types::{Hash256, MainnetEthSpec, Slot}; use warp::Rejection; @@ -314,14 +315,16 @@ pub async fn consensus_partial_pass_only_consensus() { tester.harness.make_block(state_a.clone(), slot_b).await; let ((block_b, blobs_b), state_after_b): ((SignedBeaconBlock, _), _) = tester.harness.make_block(state_a, slot_b).await; + let block_b_root = block_b.canonical_root(); /* check for `make_block` curios */ assert_eq!(block_a.state_root(), state_after_a.tree_hash_root()); assert_eq!(block_b.state_root(), state_after_b.tree_hash_root()); assert_ne!(block_a.state_root(), block_b.state_root()); - let gossip_block_b = GossipVerifiedBlock::new(block_b.clone().into(), &tester.harness.chain); - assert!(gossip_block_b.is_ok()); + let gossip_block_contents_b = SignedBlockContents::new(block_b, blobs_b) + .into_gossip_verified_block(&tester.harness.chain); + assert!(gossip_block_contents_b.is_ok()); let gossip_block_a = GossipVerifiedBlock::new(block_a.clone().into(), &tester.harness.chain); assert!(gossip_block_a.is_err()); @@ -330,7 +333,7 @@ pub async fn consensus_partial_pass_only_consensus() { let publication_result: Result<(), Rejection> = publish_block( None, - ProvenancedBlock::local((gossip_block_b.unwrap(), blobs_b)), + ProvenancedBlock::local(gossip_block_contents_b.unwrap()), tester.harness.chain.clone(), &channel.0, test_logger, @@ -342,7 +345,7 @@ pub async fn consensus_partial_pass_only_consensus() { assert!(tester .harness .chain - .block_is_known_to_fork_choice(&block_b.canonical_root())); + .block_is_known_to_fork_choice(&block_b_root)); } /// This test checks that a block that is valid from both a gossip and consensus perspective is accepted when using `broadcast_validation=consensus`. @@ -600,7 +603,7 @@ pub async fn equivocation_consensus_late_equivocation() { let slot_b = slot_a + 1; let state_a = tester.harness.get_current_state(); - let ((block_a, _), state_after_a): ((SignedBeaconBlock, _), _) = + let ((block_a, blobs_a), state_after_a): ((SignedBeaconBlock, _), _) = tester.harness.make_block(state_a.clone(), slot_b).await; let ((block_b, blobs_b), state_after_b): ((SignedBeaconBlock, _), _) = tester.harness.make_block(state_a, slot_b).await; @@ -610,16 +613,18 @@ pub async fn equivocation_consensus_late_equivocation() { assert_eq!(block_b.state_root(), state_after_b.tree_hash_root()); assert_ne!(block_a.state_root(), block_b.state_root()); - let gossip_block_b = GossipVerifiedBlock::new(block_b.clone().into(), &tester.harness.chain); - assert!(gossip_block_b.is_ok()); - let gossip_block_a = GossipVerifiedBlock::new(block_a.clone().into(), &tester.harness.chain); - assert!(gossip_block_a.is_err()); + let gossip_block_contents_b = SignedBlockContents::new(block_b, blobs_b) + .into_gossip_verified_block(&tester.harness.chain); + assert!(gossip_block_contents_b.is_ok()); + let gossip_block_contents_a = SignedBlockContents::new(block_a, blobs_a) + .into_gossip_verified_block(&tester.harness.chain); + assert!(gossip_block_contents_a.is_err()); let channel = tokio::sync::mpsc::unbounded_channel(); let publication_result: Result<(), Rejection> = publish_block( None, - ProvenancedBlock::local((gossip_block_b.unwrap(), blobs_b)), + ProvenancedBlock::local(gossip_block_contents_b.unwrap()), tester.harness.chain, &channel.0, test_logger, @@ -1224,11 +1229,15 @@ pub async fn blinded_equivocation_consensus_late_equivocation() { ProvenancedBlock::Builder(b, _) => b, }; - let gossip_block_b = - GossipVerifiedBlock::new(inner_block_b.deconstruct().into(), &tester.harness.chain); + let gossip_block_b = GossipVerifiedBlock::new( + Arc::new(inner_block_b.clone().deconstruct().0), + &tester.harness.chain, + ); assert!(gossip_block_b.is_ok()); - let gossip_block_a = - GossipVerifiedBlock::new(inner_block_a.deconstruct().into(), &tester.harness.chain); + let gossip_block_a = GossipVerifiedBlock::new( + Arc::new(inner_block_a.clone().deconstruct().0), + &tester.harness.chain, + ); assert!(gossip_block_a.is_err()); let channel = tokio::sync::mpsc::unbounded_channel(); diff --git a/beacon_node/network/src/beacon_processor/mod.rs b/beacon_node/network/src/beacon_processor/mod.rs index e051b1330..a01265db4 100644 --- a/beacon_node/network/src/beacon_processor/mod.rs +++ b/beacon_node/network/src/beacon_processor/mod.rs @@ -1852,7 +1852,7 @@ impl BeaconProcessor { message_id, peer_id, peer_client, - block.into(), + block, work_reprocessing_tx, duplicate_cache, invalid_block_storage, diff --git a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs index bd663be96..3d5f3f4f4 100644 --- a/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs +++ b/beacon_node/network/src/beacon_processor/worker/gossip_methods.rs @@ -1,6 +1,6 @@ use crate::{metrics, service::NetworkMessage, sync::SyncMessage}; -use beacon_chain::blob_verification::{AsBlock, BlobError, BlockWrapper, GossipVerifiedBlob}; +use beacon_chain::blob_verification::{AsBlock, BlobError, GossipVerifiedBlob}; use beacon_chain::store::Error; use beacon_chain::{ attestation_verification::{self, Error as AttnError, VerifiedAttestation}, @@ -20,6 +20,7 @@ use ssz::Encode; use std::fs; use std::io::Write; use std::path::PathBuf; +use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use store::hot_cold_store::HotColdDBError; use tokio::sync::mpsc; @@ -754,13 +755,13 @@ impl Worker { pub async fn process_gossip_verified_blob( self, peer_id: PeerId, - verified_blob: GossipVerifiedBlob, + verified_blob: GossipVerifiedBlob, // This value is not used presently, but it might come in handy for debugging. _seen_duration: Duration, ) { let blob_root = verified_blob.block_root(); let blob_slot = verified_blob.slot(); - let blob_clone = verified_blob.clone().to_blob(); + let blob_index = verified_blob.id().index; match self.chain.process_blob(verified_blob).await { Ok(AvailabilityProcessingStatus::Imported(_hash)) => { //TODO(sean) add metrics and logging @@ -778,7 +779,7 @@ impl Worker { "outcome" => ?err, "block root" => ?blob_root, "block slot" => blob_slot, - "blob index" => blob_clone.index, + "blob index" => blob_index, ); self.gossip_penalize_peer( peer_id, @@ -788,7 +789,6 @@ impl Worker { trace!( self.log, "Invalid gossip blob ssz"; - "ssz" => format_args!("0x{}", hex::encode(blob_clone.as_ssz_bytes())), ); } } @@ -807,7 +807,7 @@ impl Worker { message_id: MessageId, peer_id: PeerId, peer_client: Client, - block: BlockWrapper, + block: Arc>, reprocess_tx: mpsc::Sender>, duplicate_cache: DuplicateCache, invalid_block_storage: InvalidBlockStorage, @@ -856,7 +856,7 @@ impl Worker { message_id: MessageId, peer_id: PeerId, peer_client: Client, - block: BlockWrapper, + block: Arc>, reprocess_tx: mpsc::Sender>, seen_duration: Duration, ) -> Option> { @@ -881,7 +881,7 @@ impl Worker { let block_root = if let Ok(verified_block) = &verification_result { verified_block.block_root } else { - block.as_block().canonical_root() + block.canonical_root() }; // Write the time the block was observed into delay cache.