use slot_clock::SlotClock; use std::sync::Arc; use crate::beacon_chain::{ BeaconChain, BeaconChainTypes, MAXIMUM_GOSSIP_CLOCK_DISPARITY, VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT, }; use crate::gossip_blob_cache::AvailabilityCheckError; use crate::BeaconChainError; use derivative::Derivative; use state_processing::per_block_processing::eip4844::eip4844::verify_kzg_commitments_against_transactions; use types::blob_sidecar::BlobSidecarList; use types::{ BeaconBlockRef, BeaconStateError, BlobSidecar, BlobSidecarList, Epoch, EthSpec, Hash256, KzgCommitment, SignedBeaconBlock, SignedBeaconBlockHeader, SignedBlobSidecar, Slot, Transactions, }; #[derive(Debug)] pub enum BlobError { /// The blob sidecar is from a slot that is later than the current slot (with respect to the /// gossip clock disparity). /// /// ## Peer scoring /// /// Assuming the local clock is correct, the peer has sent an invalid message. FutureSlot { message_slot: Slot, latest_permissible_slot: Slot, }, /// The blob sidecar has a different slot than the block. /// /// ## Peer scoring /// /// Assuming the local clock is correct, the peer has sent an invalid message. SlotMismatch { blob_slot: Slot, block_slot: Slot, }, /// No kzg ccommitment associated with blob sidecar. KzgCommitmentMissing, /// No transactions in block TransactionsMissing, /// Blob transactions in the block do not correspond to the kzg commitments. TransactionCommitmentMismatch, TrustedSetupNotInitialized, InvalidKzgProof, KzgError(kzg::Error), /// There was an error whilst processing the sync contribution. It is not known if it is valid or invalid. /// /// ## Peer scoring /// /// We were unable to process this sync committee message due to an internal error. It's unclear if the /// sync committee message is valid. BeaconChainError(BeaconChainError), /// No blobs for the specified block where we would expect blobs. UnavailableBlobs, /// Blobs provided for a pre-Eip4844 fork. InconsistentFork, /// The `blobs_sidecar.message.beacon_block_root` block is unknown. /// /// ## Peer scoring /// /// The blob points to a block we have not yet imported. The blob cannot be imported /// into fork choice yet UnknownHeadBlock { beacon_block_root: Hash256, }, /// The `BlobSidecar` was gossiped over an incorrect subnet. InvalidSubnet { expected: u64, received: u64, }, /// The sidecar corresponds to a slot older than the finalized head slot. PastFinalizedSlot { blob_slot: Slot, finalized_slot: Slot, }, /// The proposer index specified in the sidecar does not match the locally computed /// proposer index. ProposerIndexMismatch { sidecar: usize, local: usize, }, ProposerSignatureInvalid, /// A sidecar with same slot, beacon_block_root and proposer_index but different blob is received for /// the same blob index. RepeatSidecar { proposer: usize, slot: Slot, blob_index: usize, }, /// The proposal_index corresponding to blob.beacon_block_root is not known. /// /// ## Peer scoring /// /// The block is invalid and the peer is faulty. UnknownValidator(u64), BlobCacheError(AvailabilityCheckError), } impl From for BlobError { fn from(e: BeaconChainError) -> Self { BlobError::BeaconChainError(e) } } impl From for BlobError { fn from(e: BeaconStateError) -> Self { BlobError::BeaconChainError(BeaconChainError::BeaconStateError(e)) } } /// A wrapper around a `BlobSidecar` that indicates it has been approved for re-gossiping on /// the p2p network. #[derive(Debug)] pub struct GossipVerifiedBlob { blob: BlobSidecar, } impl GossipVerifiedBlob { pub fn to_blob(self) -> BlobSidecar { self.blob } } pub fn validate_blob_sidecar_for_gossip( signed_blob_sidecar: SignedBlobSidecar, subnet: u64, chain: &BeaconChain, ) -> Result, BlobError> { let blob_slot = signed_blob_sidecar.message.slot; let blob_index = signed_blob_sidecar.message.index; let block_root = signed_blob_sidecar.message.block_root; // Verify that the blob_sidecar was received on the correct subnet. if blob_index != subnet { return Err(BlobError::InvalidSubnet { expected: blob_index, received: subnet, }); } // Verify that the sidecar is not from a future slot. let latest_permissible_slot = chain .slot_clock .now_with_future_tolerance(MAXIMUM_GOSSIP_CLOCK_DISPARITY) .ok_or(BeaconChainError::UnableToReadSlot)?; if blob_slot > latest_permissible_slot { return Err(BlobError::FutureSlot { message_slot: blob_slot, latest_permissible_slot, }); } // TODO(pawan): Verify not from a past slot? // Verify that the sidecar slot is greater than the latest finalized slot let latest_finalized_slot = chain .head() .finalized_checkpoint() .epoch .start_slot(T::EthSpec::slots_per_epoch()); if blob_slot <= latest_finalized_slot { return Err(BlobError::PastFinalizedSlot { blob_slot, finalized_slot: latest_finalized_slot, }); } // TODO(pawan): should we verify locally that the parent root is correct // or just use whatever the proposer gives us? let proposer_shuffling_root = signed_blob_sidecar.message.block_parent_root; let (proposer_index, fork) = match chain .beacon_proposer_cache .lock() .get_slot::(proposer_shuffling_root, blob_slot) { Some(proposer) => (proposer.index, proposer.fork), None => { let state = &chain.canonical_head.cached_head().snapshot.beacon_state; ( state.get_beacon_proposer_index(blob_slot, &chain.spec)?, state.fork(), ) } }; let blob_proposer_index = signed_blob_sidecar.message.proposer_index; if proposer_index != blob_proposer_index as usize { return Err(BlobError::ProposerIndexMismatch { sidecar: blob_proposer_index as usize, local: proposer_index, }); } let signature_is_valid = { let pubkey_cache = chain .validator_pubkey_cache .try_read_for(VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT) .ok_or(BeaconChainError::ValidatorPubkeyCacheLockTimeout) .map_err(BlobError::BeaconChainError)?; let pubkey = pubkey_cache .get(proposer_index) .ok_or_else(|| BlobError::UnknownValidator(proposer_index as u64))?; signed_blob_sidecar.verify_signature( None, pubkey, &fork, chain.genesis_validators_root, &chain.spec, ) }; if !signature_is_valid { return Err(BlobError::ProposerSignatureInvalid); } // TODO(pawan): kzg validations. // TODO(pawan): Check if other blobs for the same proposer index and blob index have been // received and drop if required. // Verify if the corresponding block for this blob has been received. // Note: this should be the last gossip check so that we can forward the blob // over the gossip network even if we haven't received the corresponding block yet // as all other validations have passed. let block_opt = chain .canonical_head .fork_choice_read_lock() .get_block(&block_root) .or_else(|| chain.early_attester_cache.get_proto_block(block_root)); // TODO(pawan): should we be checking this cache? // TODO(pawan): this may be redundant with the new `AvailabilityProcessingStatus::PendingBlock variant` if block_opt.is_none() { return Err(BlobError::UnknownHeadBlock { beacon_block_root: block_root, }); } Ok(GossipVerifiedBlob { blob: signed_blob_sidecar.message, }) } pub fn verify_data_availability( _blob_sidecar: &BlobSidecarList, kzg_commitments: &[KzgCommitment], transactions: &Transactions, _block_slot: Slot, _block_root: Hash256, chain: &BeaconChain, ) -> Result<(), BlobError> { if verify_kzg_commitments_against_transactions::(transactions, kzg_commitments) .is_err() { return Err(BlobError::TransactionCommitmentMismatch); } // Validatate that the kzg proof is valid against the commitments and blobs let _kzg = chain .kzg .as_ref() .ok_or(BlobError::TrustedSetupNotInitialized)?; todo!("use `kzg_utils::validate_blobs` once the function is updated") // if !kzg_utils::validate_blobs_sidecar( // kzg, // block_slot, // block_root, // kzg_commitments, // blob_sidecar, // ) // .map_err(BlobError::KzgError)? // { // return Err(BlobError::InvalidKzgProof); // } // Ok(()) } #[derive(Copy, Clone)] pub enum DataAvailabilityCheckRequired { Yes, No, } pub trait IntoAvailableBlock { fn into_available_block( self, block_root: Hash256, chain: &BeaconChain, ) -> Result, BlobError>; } impl IntoAvailableBlock for BlockWrapper { fn into_available_block( self, _block_root: Hash256, _chain: &BeaconChain, ) -> Result, BlobError> { todo!() } } #[derive(Clone, Debug, PartialEq, Derivative)] #[derivative(Hash(bound = "E: EthSpec"))] pub struct AvailableBlock(AvailableBlockInner); #[derive(Clone, Debug, PartialEq, Derivative)] #[derivative(Hash(bound = "E: EthSpec"))] struct AvailableBlockInner { block: Arc>, blobs: VerifiedBlobs, } impl AvailableBlock { pub fn new( block: Arc>, blobs: Vec>>, da_check: impl FnOnce(Epoch) -> bool, ) -> Result { if let Ok(block_kzg_commitments) = block.message().body().blob_kzg_commitments() { if blobs.is_empty() && block_kzg_commitments.is_empty() { return Ok(Self(AvailableBlockInner { block, blobs: VerifiedBlobs::EmptyBlobs, })); } if blobs.is_empty() { if da_check(block.epoch()) { return Ok(Self(AvailableBlockInner { block, blobs: VerifiedBlobs::NotRequired, })); } else { return Err("Block within DA boundary but no blobs provided".to_string()); } } if blobs.len() != block_kzg_commitments.len() { return Err(format!( "Block commitments and blobs length must be the same. Block commitments len: {}, blobs length: {}", block_kzg_commitments.len(), blobs.len() )); } for (block_commitment, blob) in block_kzg_commitments.iter().zip(blobs.iter()) { if *block_commitment != blob.kzg_commitment { return Err(format!( "Invalid input. Blob and block commitment mismatch at index {}", blob.index )); } } Ok(Self(AvailableBlockInner { block, blobs: VerifiedBlobs::Available(blobs.into()), })) } // This is a pre 4844 block else { Ok(Self(AvailableBlockInner { block, blobs: VerifiedBlobs::PreEip4844, })) } } pub fn block(&self) -> &SignedBeaconBlock { &self.0.block } pub fn blobs(&self) -> Option> { match &self.0.blobs { VerifiedBlobs::EmptyBlobs | VerifiedBlobs::NotRequired | VerifiedBlobs::PreEip4844 => { None } VerifiedBlobs::Available(blobs) => Some(blobs.clone()), } } pub fn deconstruct(self) -> (Arc>, Option>) { match self.0.blobs { VerifiedBlobs::EmptyBlobs | VerifiedBlobs::NotRequired | VerifiedBlobs::PreEip4844 => { (self.0.block, None) } VerifiedBlobs::Available(blobs) => (self.0.block, Some(blobs)), } } } #[derive(Clone, Debug, PartialEq, Derivative)] #[derivative(Hash(bound = "E: EthSpec"))] pub enum VerifiedBlobs { /// These blobs are available. Available(BlobSidecarList), /// This block is from outside the data availability boundary so doesn't require /// a data availability check. NotRequired, /// The block's `kzg_commitments` field is empty so it does not contain any blobs. EmptyBlobs, /// This is a block prior to the 4844 fork, so doesn't require any blobs PreEip4844, } pub trait AsBlock { fn slot(&self) -> Slot; fn epoch(&self) -> Epoch; fn parent_root(&self) -> Hash256; fn state_root(&self) -> Hash256; fn signed_block_header(&self) -> SignedBeaconBlockHeader; fn message(&self) -> BeaconBlockRef; fn as_block(&self) -> &SignedBeaconBlock; fn block_cloned(&self) -> Arc>; fn canonical_root(&self) -> Hash256; } #[derive(Debug, Clone, Derivative)] #[derivative(Hash(bound = "E: EthSpec"))] pub enum BlockWrapper { /// This variant is fully available. /// i.e. for pre-4844 blocks, it contains a (`SignedBeaconBlock`, `Blobs::None`) and for /// post-4844 blocks, it contains a `SignedBeaconBlock` and a Blobs variant other than `Blobs::None`. Available(AvailableBlock), /// This variant is not fully available and requires blobs to become fully available. AvailabilityPending(Arc>), } impl BlockWrapper { pub fn into_available_block(self) -> Option> { match self { BlockWrapper::AvailabilityPending(_) => None, BlockWrapper::Available(block) => Some(block), } } } impl AsBlock for AvailableBlock { fn slot(&self) -> Slot { self.0.block.slot() } fn epoch(&self) -> Epoch { self.0.block.epoch() } fn parent_root(&self) -> Hash256 { self.0.block.parent_root() } fn state_root(&self) -> Hash256 { self.0.block.state_root() } fn signed_block_header(&self) -> SignedBeaconBlockHeader { self.0.block.signed_block_header() } fn message(&self) -> BeaconBlockRef { self.0.block.message() } fn as_block(&self) -> &SignedBeaconBlock { &self.0.block } fn block_cloned(&self) -> Arc> { self.0.block.clone() } fn canonical_root(&self) -> Hash256 { self.0.block.canonical_root() } } impl AsBlock for BlockWrapper { fn slot(&self) -> Slot { self.as_block().slot() } fn epoch(&self) -> Epoch { self.as_block().epoch() } fn parent_root(&self) -> Hash256 { self.as_block().parent_root() } fn state_root(&self) -> Hash256 { self.as_block().state_root() } fn signed_block_header(&self) -> SignedBeaconBlockHeader { self.as_block().signed_block_header() } fn message(&self) -> BeaconBlockRef { self.as_block().message() } fn as_block(&self) -> &SignedBeaconBlock { match &self { BlockWrapper::Available(block) => &block.0.block, BlockWrapper::AvailabilityPending(block) => block, } } fn block_cloned(&self) -> Arc> { match &self { BlockWrapper::Available(block) => block.0.block.clone(), BlockWrapper::AvailabilityPending(block) => block.clone(), } } fn canonical_root(&self) -> Hash256 { self.as_block().canonical_root() } } impl AsBlock for &BlockWrapper { fn slot(&self) -> Slot { self.as_block().slot() } fn epoch(&self) -> Epoch { self.as_block().epoch() } fn parent_root(&self) -> Hash256 { self.as_block().parent_root() } fn state_root(&self) -> Hash256 { self.as_block().state_root() } fn signed_block_header(&self) -> SignedBeaconBlockHeader { self.as_block().signed_block_header() } fn message(&self) -> BeaconBlockRef { self.as_block().message() } fn as_block(&self) -> &SignedBeaconBlock { match &self { BlockWrapper::Available(block) => &block.0.block, BlockWrapper::AvailabilityPending(block) => block, } } fn block_cloned(&self) -> Arc> { match &self { BlockWrapper::Available(block) => block.0.block.clone(), BlockWrapper::AvailabilityPending(block) => block.clone(), } } fn canonical_root(&self) -> Hash256 { self.as_block().canonical_root() } } impl From> for BlockWrapper { fn from(block: SignedBeaconBlock) -> Self { BlockWrapper::AvailabilityPending(Arc::new(block)) } } impl From>> for BlockWrapper { fn from(block: Arc>) -> Self { BlockWrapper::AvailabilityPending(block) } } impl From> for BlockWrapper { fn from(block: AvailableBlock) -> Self { BlockWrapper::Available(block) } }