mirror of
https://github.com/sigp/lighthouse.git
synced 2026-04-18 21:38:31 +00:00
587 lines
18 KiB
Rust
587 lines
18 KiB
Rust
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<BeaconChainError> for BlobError {
|
|
fn from(e: BeaconChainError) -> Self {
|
|
BlobError::BeaconChainError(e)
|
|
}
|
|
}
|
|
|
|
impl From<BeaconStateError> 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<T: EthSpec> {
|
|
blob: BlobSidecar<T>,
|
|
}
|
|
|
|
impl<T: EthSpec> GossipVerifiedBlob<T> {
|
|
pub fn to_blob(self) -> BlobSidecar<T> {
|
|
self.blob
|
|
}
|
|
}
|
|
|
|
pub fn validate_blob_sidecar_for_gossip<T: BeaconChainTypes>(
|
|
signed_blob_sidecar: SignedBlobSidecar<T::EthSpec>,
|
|
subnet: u64,
|
|
chain: &BeaconChain<T>,
|
|
) -> Result<GossipVerifiedBlob<T::EthSpec>, 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::<T::EthSpec>(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<T: BeaconChainTypes>(
|
|
_blob_sidecar: &BlobSidecarList<T::EthSpec>,
|
|
kzg_commitments: &[KzgCommitment],
|
|
transactions: &Transactions<T::EthSpec>,
|
|
_block_slot: Slot,
|
|
_block_root: Hash256,
|
|
chain: &BeaconChain<T>,
|
|
) -> Result<(), BlobError> {
|
|
if verify_kzg_commitments_against_transactions::<T::EthSpec>(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<T: BeaconChainTypes> {
|
|
fn into_available_block(
|
|
self,
|
|
block_root: Hash256,
|
|
chain: &BeaconChain<T>,
|
|
) -> Result<AvailableBlock<T::EthSpec>, BlobError>;
|
|
}
|
|
|
|
impl<T: BeaconChainTypes> IntoAvailableBlock<T> for BlockWrapper<T::EthSpec> {
|
|
fn into_available_block(
|
|
self,
|
|
_block_root: Hash256,
|
|
_chain: &BeaconChain<T>,
|
|
) -> Result<AvailableBlock<T::EthSpec>, BlobError> {
|
|
todo!()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Derivative)]
|
|
#[derivative(Hash(bound = "E: EthSpec"))]
|
|
pub struct AvailableBlock<E: EthSpec>(AvailableBlockInner<E>);
|
|
|
|
#[derive(Clone, Debug, PartialEq, Derivative)]
|
|
#[derivative(Hash(bound = "E: EthSpec"))]
|
|
struct AvailableBlockInner<E: EthSpec> {
|
|
block: Arc<SignedBeaconBlock<E>>,
|
|
blobs: VerifiedBlobs<E>,
|
|
}
|
|
|
|
impl<E: EthSpec> AvailableBlock<E> {
|
|
pub fn new(
|
|
block: Arc<SignedBeaconBlock<E>>,
|
|
blobs: Vec<Arc<BlobSidecar<E>>>,
|
|
da_check: impl FnOnce(Epoch) -> bool,
|
|
) -> Result<Self, String> {
|
|
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<E> {
|
|
&self.0.block
|
|
}
|
|
|
|
pub fn blobs(&self) -> Option<BlobSidecarList<E>> {
|
|
match &self.0.blobs {
|
|
VerifiedBlobs::EmptyBlobs | VerifiedBlobs::NotRequired | VerifiedBlobs::PreEip4844 => {
|
|
None
|
|
}
|
|
VerifiedBlobs::Available(blobs) => Some(blobs.clone()),
|
|
}
|
|
}
|
|
|
|
pub fn deconstruct(self) -> (Arc<SignedBeaconBlock<E>>, Option<BlobSidecarList<E>>) {
|
|
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<E: EthSpec> {
|
|
/// These blobs are available.
|
|
Available(BlobSidecarList<E>),
|
|
/// 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<E: EthSpec> {
|
|
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<E>;
|
|
fn as_block(&self) -> &SignedBeaconBlock<E>;
|
|
fn block_cloned(&self) -> Arc<SignedBeaconBlock<E>>;
|
|
fn canonical_root(&self) -> Hash256;
|
|
}
|
|
|
|
#[derive(Debug, Clone, Derivative)]
|
|
#[derivative(Hash(bound = "E: EthSpec"))]
|
|
pub enum BlockWrapper<E: EthSpec> {
|
|
/// 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<E>),
|
|
/// This variant is not fully available and requires blobs to become fully available.
|
|
AvailabilityPending(Arc<SignedBeaconBlock<E>>),
|
|
}
|
|
|
|
impl<E: EthSpec> BlockWrapper<E> {
|
|
pub fn into_available_block(self) -> Option<AvailableBlock<E>> {
|
|
match self {
|
|
BlockWrapper::AvailabilityPending(_) => None,
|
|
BlockWrapper::Available(block) => Some(block),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<E: EthSpec> AsBlock<E> for AvailableBlock<E> {
|
|
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<E> {
|
|
self.0.block.message()
|
|
}
|
|
|
|
fn as_block(&self) -> &SignedBeaconBlock<E> {
|
|
&self.0.block
|
|
}
|
|
|
|
fn block_cloned(&self) -> Arc<SignedBeaconBlock<E>> {
|
|
self.0.block.clone()
|
|
}
|
|
|
|
fn canonical_root(&self) -> Hash256 {
|
|
self.0.block.canonical_root()
|
|
}
|
|
}
|
|
|
|
impl<E: EthSpec> AsBlock<E> for BlockWrapper<E> {
|
|
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<E> {
|
|
self.as_block().message()
|
|
}
|
|
fn as_block(&self) -> &SignedBeaconBlock<E> {
|
|
match &self {
|
|
BlockWrapper::Available(block) => &block.0.block,
|
|
BlockWrapper::AvailabilityPending(block) => block,
|
|
}
|
|
}
|
|
fn block_cloned(&self) -> Arc<SignedBeaconBlock<E>> {
|
|
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<E: EthSpec> AsBlock<E> for &BlockWrapper<E> {
|
|
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<E> {
|
|
self.as_block().message()
|
|
}
|
|
fn as_block(&self) -> &SignedBeaconBlock<E> {
|
|
match &self {
|
|
BlockWrapper::Available(block) => &block.0.block,
|
|
BlockWrapper::AvailabilityPending(block) => block,
|
|
}
|
|
}
|
|
fn block_cloned(&self) -> Arc<SignedBeaconBlock<E>> {
|
|
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<E: EthSpec> From<SignedBeaconBlock<E>> for BlockWrapper<E> {
|
|
fn from(block: SignedBeaconBlock<E>) -> Self {
|
|
BlockWrapper::AvailabilityPending(Arc::new(block))
|
|
}
|
|
}
|
|
|
|
impl<E: EthSpec> From<Arc<SignedBeaconBlock<E>>> for BlockWrapper<E> {
|
|
fn from(block: Arc<SignedBeaconBlock<E>>) -> Self {
|
|
BlockWrapper::AvailabilityPending(block)
|
|
}
|
|
}
|
|
|
|
impl<E: EthSpec> From<AvailableBlock<E>> for BlockWrapper<E> {
|
|
fn from(block: AvailableBlock<E>) -> Self {
|
|
BlockWrapper::Available(block)
|
|
}
|
|
}
|