mirror of
https://github.com/sigp/lighthouse.git
synced 2026-05-30 20:57:10 +00:00
- pending_column.rs: split try_to_sidecar into is_complete-checked to_sidecar with typed PendingColumnError so 'incomplete column' is no longer conflated with VariableList size-bound failures (jimmygchen, dapplion). - pending_components.rs: get_cached_data_columns filters by is_complete first, then logs an error if a complete column fails to assemble (dknopik's sanity check on filter_map silent drops). - data_column_verification.rs: add the missing column.slot == bid.slot consistency check in validate_data_column_sidecar_for_gossip_gloas, using the previously-defined-but-unused BlockSlotMismatch error variant (jimmygchen).
2116 lines
74 KiB
Rust
2116 lines
74 KiB
Rust
use crate::block_verification::{
|
|
BlockSlashInfo, get_validator_pubkey_cache, process_block_slash_info,
|
|
};
|
|
use crate::data_availability_checker::MissingCellsError;
|
|
use crate::kzg_utils::{
|
|
reconstruct_data_columns, validate_full_data_columns,
|
|
validate_full_data_columns_with_commitments, validate_partial_data_columns,
|
|
};
|
|
use crate::observed_data_sidecars::{
|
|
Error as ObservedDataSidecarsError, ObservationKey, ObservationStrategy, Observe,
|
|
};
|
|
use crate::pending_payload_cache::signed_payload_bid_from_block;
|
|
use crate::{BeaconChain, BeaconChainError, BeaconChainTypes, metrics};
|
|
use educe::Educe;
|
|
use fork_choice::ProtoBlock;
|
|
use kzg::{Error as KzgError, Kzg};
|
|
use proto_array::Block;
|
|
use slot_clock::{SlotClock, timestamp_now};
|
|
use ssz_derive::Encode;
|
|
use ssz_types::VariableList;
|
|
use std::iter;
|
|
use std::marker::PhantomData;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
use store::DatabaseBlock;
|
|
use tracing::{debug, instrument};
|
|
use tree_hash::TreeHash;
|
|
use types::data::{
|
|
ColumnIndex, PartialDataColumn, PartialDataColumnHeader, PartialDataColumnSidecar,
|
|
PartialDataColumnSidecarError,
|
|
};
|
|
use types::{
|
|
BeaconStateError, ChainSpec, DataColumnSidecar, DataColumnSidecarFulu, DataColumnSidecarGloas,
|
|
DataColumnSubnetId, EthSpec, Hash256, KzgCommitment, PartialDataColumnSidecarRef,
|
|
SignedBeaconBlockHeader, SignedExecutionPayloadBid, Slot,
|
|
};
|
|
|
|
/// An error occurred while validating a gossip data column.
|
|
#[derive(Debug)]
|
|
pub enum GossipDataColumnError {
|
|
InvalidVariant,
|
|
/// There was an error whilst processing the data column. It is not known if it is
|
|
/// valid or invalid.
|
|
///
|
|
/// ## Peer scoring
|
|
///
|
|
/// We were unable to process this data column due to an internal error. It's
|
|
/// unclear if the data column is valid.
|
|
BeaconChainError(Box<BeaconChainError>),
|
|
/// The proposal signature in invalid.
|
|
///
|
|
/// ## Peer scoring
|
|
///
|
|
/// The data column is invalid and the peer is faulty.
|
|
ProposalSignatureInvalid,
|
|
/// The proposal_index corresponding to data column.beacon_block_root is not known.
|
|
///
|
|
/// ## Peer scoring
|
|
///
|
|
/// The data column is invalid and the peer is faulty.
|
|
UnknownValidator(u64),
|
|
/// The provided data column is not from a later slot than its parent.
|
|
///
|
|
/// ## Peer scoring
|
|
///
|
|
/// The data column is invalid and the peer is faulty.
|
|
IsNotLaterThanParent {
|
|
data_column_slot: Slot,
|
|
parent_slot: Slot,
|
|
},
|
|
/// The kzg verification failed.
|
|
///
|
|
/// ## Peer scoring
|
|
///
|
|
/// The data column sidecar is invalid and the peer is faulty.
|
|
InvalidKzgProof(kzg::Error),
|
|
/// The column mismatches the cached (possibly partial) column.
|
|
/// This is equivalent to failed kzg verification.
|
|
///
|
|
/// ## Peer scoring
|
|
///
|
|
/// The data column sidecar is invalid and the peer is faulty.
|
|
MismatchesCachedColumn,
|
|
/// The column was gossiped over an incorrect subnet.
|
|
///
|
|
/// ## Peer scoring
|
|
///
|
|
/// The column is invalid or the peer is faulty.
|
|
InvalidSubnetId {
|
|
received: u64,
|
|
expected: u64,
|
|
},
|
|
/// The column 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 sidecar corresponds to a slot older than the finalized head slot.
|
|
///
|
|
/// ## Peer scoring
|
|
///
|
|
/// It's unclear if this column is valid, but this column is for a finalized slot and is
|
|
/// therefore useless to us.
|
|
PastFinalizedSlot {
|
|
column_slot: Slot,
|
|
finalized_slot: Slot,
|
|
},
|
|
/// The pubkey cache timed out.
|
|
///
|
|
/// ## Peer scoring
|
|
///
|
|
/// The column sidecar may be valid, this is an internal error.
|
|
PubkeyCacheTimeout,
|
|
/// The proposer index specified in the sidecar does not match the locally computed
|
|
/// proposer index.
|
|
///
|
|
/// ## Peer scoring
|
|
///
|
|
/// The column is invalid and the peer is faulty.
|
|
ProposerIndexMismatch {
|
|
sidecar: usize,
|
|
local: usize,
|
|
},
|
|
/// The provided columns's parent block is unknown.
|
|
///
|
|
/// ## Peer scoring
|
|
///
|
|
/// We cannot process the columns without validating its parent, the peer isn't necessarily faulty.
|
|
ParentUnknown {
|
|
parent_root: Hash256,
|
|
slot: Slot,
|
|
},
|
|
/// The block referenced by the data column is unknown.
|
|
///
|
|
/// ## Peer scoring
|
|
///
|
|
/// We cannot process the column without the referenced block, the peer isn't necessarily faulty.
|
|
BlockRootUnknown {
|
|
block_root: Hash256,
|
|
slot: Slot,
|
|
},
|
|
/// The data column slot does not match its referenced block slot.
|
|
///
|
|
/// ## Peer scoring
|
|
///
|
|
/// The column sidecar is invalid and the peer is faulty.
|
|
BlockSlotMismatch {
|
|
block_slot: Slot,
|
|
data_column_slot: Slot,
|
|
},
|
|
/// The column conflicts with finalization, no need to propagate.
|
|
///
|
|
/// ## Peer scoring
|
|
///
|
|
/// It's unclear if this column is valid, but it conflicts with finality and shouldn't be
|
|
/// imported.
|
|
NotFinalizedDescendant {
|
|
block_parent_root: Hash256,
|
|
},
|
|
/// Invalid kzg commitment inclusion proof
|
|
///
|
|
/// ## Peer scoring
|
|
///
|
|
/// The column sidecar is invalid and the peer is faulty
|
|
InvalidInclusionProof,
|
|
/// A column has already been seen for the given observation key and index.
|
|
///
|
|
/// ## Peer scoring
|
|
///
|
|
/// The peer isn't faulty, but we do not forward it over gossip.
|
|
PriorKnown {
|
|
observation_key: ObservationKey,
|
|
index: ColumnIndex,
|
|
},
|
|
/// A column has already been processed from non-gossip source and have not yet been seen on
|
|
/// the gossip network.
|
|
/// This column should be accepted and forwarded over gossip.
|
|
PriorKnownUnpublished,
|
|
/// Data column index must be between 0 and `NUMBER_OF_COLUMNS` (exclusive).
|
|
///
|
|
/// ## Peer scoring
|
|
///
|
|
/// The column sidecar is invalid and the peer is faulty
|
|
InvalidColumnIndex(u64),
|
|
/// Data column not expected for a block with empty kzg commitments.
|
|
///
|
|
/// ## Peer scoring
|
|
///
|
|
/// The column sidecar is invalid and the peer is faulty
|
|
UnexpectedDataColumn,
|
|
/// The data column length must be equal to the number of commitments, otherwise the
|
|
/// sidecar is invalid.
|
|
///
|
|
/// ## Peer scoring
|
|
///
|
|
/// The column sidecar is invalid and the peer is faulty
|
|
InconsistentCommitmentsLength {
|
|
cells_len: usize,
|
|
commitments_len: usize,
|
|
},
|
|
/// The data column length must be equal to the number of proofs, otherwise the
|
|
/// sidecar is invalid.
|
|
///
|
|
/// ## Peer scoring
|
|
///
|
|
/// The column sidecar is invalid and the peer is faulty
|
|
InconsistentProofsLength {
|
|
cells_len: usize,
|
|
proofs_len: usize,
|
|
},
|
|
/// The number of KZG commitments exceeds the maximum number of blobs allowed for the fork. The
|
|
/// sidecar is invalid.
|
|
///
|
|
/// ## Peer scoring
|
|
/// The column sidecar is invalid and the peer is faulty
|
|
MaxBlobsPerBlockExceeded {
|
|
max_blobs_per_block: usize,
|
|
commitments_len: usize,
|
|
},
|
|
}
|
|
|
|
impl From<BeaconChainError> for GossipDataColumnError {
|
|
fn from(e: BeaconChainError) -> Self {
|
|
GossipDataColumnError::BeaconChainError(e.into())
|
|
}
|
|
}
|
|
|
|
impl From<BeaconStateError> for GossipDataColumnError {
|
|
fn from(e: BeaconStateError) -> Self {
|
|
GossipDataColumnError::BeaconChainError(BeaconChainError::BeaconStateError(e).into())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum GossipPartialDataColumnError {
|
|
GossipDataColumnError(GossipDataColumnError),
|
|
/// Partial messages are disabled and we can not validate them.
|
|
///
|
|
/// ## Peer scoring
|
|
/// A peer sent us a partial message even though we did not advertize support for it, penalize
|
|
/// it
|
|
PartialColumnsDisabled,
|
|
/// There was an unexpected error while performing an operation on the partial data column.
|
|
InternalError(PartialDataColumnSidecarError),
|
|
/// The partial data column does not contain a header, and we do not have it cached.
|
|
///
|
|
/// ## Peer scoring
|
|
/// The peer SHOULD send us the header on the first partial message, but is not required to.
|
|
/// Still, the peer incorrectly assumed that we have the header, and sent us data we can not
|
|
/// process due to that. Penalize it slightly.
|
|
MissingHeader,
|
|
/// The partial data column header does not match the valid one we have already cached.
|
|
///
|
|
/// ## Peer scoring
|
|
/// The column sidecar is invalid and the peer is faulty
|
|
HeaderMismatches,
|
|
/// The partial data column header block root does not match the group id.
|
|
///
|
|
/// ## Peer scoring
|
|
/// The column sidecar is invalid and the peer is faulty
|
|
HeaderIncorrectRoot {
|
|
group_id: Hash256,
|
|
header_hash: Hash256,
|
|
},
|
|
/// The partial message has neither a header nor cells.
|
|
///
|
|
/// ## Peer scoring
|
|
/// The column sidecar is invalid and the peer is faulty
|
|
EmptyMessage,
|
|
/// The partial message has a count of proofs anc/or cells that is inconsistent with the bitmap.
|
|
///
|
|
/// ## Peer scoring
|
|
/// The column sidecar is invalid and the peer is faulty
|
|
InconsistentPresentCount {
|
|
bitmap_popcount: usize,
|
|
cells_len: usize,
|
|
proofs_len: usize,
|
|
},
|
|
/// The partial message has a bitmap length that is inconsistent with the number of commitments.
|
|
///
|
|
/// ## Peer scoring
|
|
/// The column sidecar is invalid and the peer is faulty
|
|
InconsistentCommitmentsLength {
|
|
bitmap_len: usize,
|
|
commitments_len: usize,
|
|
},
|
|
}
|
|
|
|
impl From<GossipDataColumnError> for GossipPartialDataColumnError {
|
|
fn from(e: GossipDataColumnError) -> Self {
|
|
GossipPartialDataColumnError::GossipDataColumnError(e)
|
|
}
|
|
}
|
|
|
|
impl From<BeaconChainError> for GossipPartialDataColumnError {
|
|
fn from(e: BeaconChainError) -> Self {
|
|
GossipDataColumnError::from(e).into()
|
|
}
|
|
}
|
|
|
|
impl From<BeaconStateError> for GossipPartialDataColumnError {
|
|
fn from(e: BeaconStateError) -> Self {
|
|
GossipDataColumnError::from(e).into()
|
|
}
|
|
}
|
|
|
|
/// A wrapper around a `DataColumnSidecar` that indicates it has been approved for re-gossiping on
|
|
/// the p2p network.
|
|
#[derive(Debug, Clone)]
|
|
pub struct GossipVerifiedDataColumn<T: BeaconChainTypes, O: ObservationStrategy = Observe> {
|
|
block_root: Hash256,
|
|
data_column: KzgVerifiedDataColumn<T::EthSpec>,
|
|
_phantom: PhantomData<O>,
|
|
}
|
|
|
|
impl<T: BeaconChainTypes, O: ObservationStrategy> GossipVerifiedDataColumn<T, O> {
|
|
pub fn new(
|
|
column_sidecar: Arc<DataColumnSidecar<T::EthSpec>>,
|
|
subnet_id: DataColumnSubnetId,
|
|
chain: &BeaconChain<T>,
|
|
) -> Result<Self, GossipDataColumnError> {
|
|
match column_sidecar.as_ref() {
|
|
DataColumnSidecar::Fulu(c) => {
|
|
let header = c.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_fulu::<T, O>(c, subnet_id, chain).map_err(
|
|
|e| {
|
|
process_block_slash_info::<_, GossipDataColumnError>(
|
|
chain,
|
|
BlockSlashInfo::from_early_error_data_column(header, e),
|
|
)
|
|
},
|
|
)?;
|
|
}
|
|
DataColumnSidecar::Gloas(data_column_gloas) => {
|
|
validate_data_column_sidecar_for_gossip_gloas::<T, O>(
|
|
data_column_gloas,
|
|
subnet_id,
|
|
chain,
|
|
)?;
|
|
}
|
|
}
|
|
|
|
Ok(GossipVerifiedDataColumn {
|
|
block_root: column_sidecar.block_root(),
|
|
data_column: KzgVerifiedDataColumn {
|
|
data: column_sidecar,
|
|
seen_timestamp: chain.slot_clock.now_duration().unwrap_or_default(),
|
|
},
|
|
_phantom: PhantomData,
|
|
})
|
|
}
|
|
|
|
/// Create a `GossipVerifiedDataColumn` from `DataColumnSidecar` for block production ONLY.
|
|
/// When publishing a block constructed locally, the EL will have already verified the cell proofs.
|
|
/// When publishing a block constructed externally, there will be no columns here.
|
|
pub fn new_for_block_publishing(
|
|
column_sidecar: Arc<DataColumnSidecar<T::EthSpec>>,
|
|
chain: &BeaconChain<T>,
|
|
) -> Result<Self, GossipDataColumnError> {
|
|
match column_sidecar.as_ref() {
|
|
DataColumnSidecar::Fulu(data_column_fulu) => {
|
|
verify_data_column_sidecar_with_commitments_len(
|
|
&column_sidecar,
|
|
data_column_fulu.kzg_commitments.len(),
|
|
&chain.spec,
|
|
)?;
|
|
}
|
|
DataColumnSidecar::Gloas(_) => {
|
|
let bid = load_gloas_payload_bid(column_sidecar.block_root(), chain)?.ok_or(
|
|
GossipDataColumnError::BlockRootUnknown {
|
|
block_root: column_sidecar.block_root(),
|
|
slot: column_sidecar.slot(),
|
|
},
|
|
)?;
|
|
verify_data_column_sidecar_with_commitments_len(
|
|
&column_sidecar,
|
|
bid.message.blob_kzg_commitments.len(),
|
|
&chain.spec,
|
|
)?;
|
|
}
|
|
}
|
|
|
|
// Check if the data column is already in the DA checker cache. This happens when data columns
|
|
// are made available through the `engine_getBlobs` method. If it exists in the cache, we know
|
|
// it has already passed the gossip checks, even though this particular instance hasn't been
|
|
// seen / published on the gossip network yet (passed the `verify_is_unknown_sidecar` check above).
|
|
// In this case, we should accept it for gossip propagation.
|
|
verify_is_unknown_sidecar(chain, &column_sidecar)?;
|
|
|
|
match missing_cells_for_column_sidecar(chain, &column_sidecar)? {
|
|
Some(_) => Ok(Self {
|
|
block_root: column_sidecar.block_root(),
|
|
data_column: KzgVerifiedDataColumn::from_execution_verified(column_sidecar),
|
|
_phantom: Default::default(),
|
|
}),
|
|
None => {
|
|
// Observe this data column so we don't process it again.
|
|
if O::observe() {
|
|
observe_gossip_data_column(&column_sidecar, chain)?;
|
|
}
|
|
Err(GossipDataColumnError::PriorKnownUnpublished)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Create a `GossipVerifiedDataColumn` from `DataColumnSidecar` for testing ONLY.
|
|
pub fn __new_for_testing(column_sidecar: Arc<DataColumnSidecar<T::EthSpec>>) -> Self {
|
|
Self {
|
|
block_root: column_sidecar.block_root(),
|
|
data_column: KzgVerifiedDataColumn::__new_for_testing(column_sidecar),
|
|
_phantom: Default::default(),
|
|
}
|
|
}
|
|
|
|
pub fn as_data_column(&self) -> &DataColumnSidecar<T::EthSpec> {
|
|
self.data_column.as_data_column()
|
|
}
|
|
|
|
/// This is cheap as we're calling clone on an Arc
|
|
pub fn clone_data_column(&self) -> Arc<DataColumnSidecar<T::EthSpec>> {
|
|
self.data_column.clone_data_column()
|
|
}
|
|
|
|
pub fn block_root(&self) -> Hash256 {
|
|
self.block_root
|
|
}
|
|
|
|
pub fn slot(&self) -> Slot {
|
|
self.data_column.data.slot()
|
|
}
|
|
|
|
pub fn index(&self) -> ColumnIndex {
|
|
*self.data_column.data.index()
|
|
}
|
|
|
|
pub fn into_inner(self) -> KzgVerifiedDataColumn<T::EthSpec> {
|
|
self.data_column
|
|
}
|
|
}
|
|
|
|
/// Wrapper over a `DataColumnSidecar` for which we have completed kzg verification.
|
|
#[derive(Debug, Educe, Clone)]
|
|
#[educe(PartialEq, Eq)]
|
|
pub struct KzgVerifiedDataColumn<E: EthSpec> {
|
|
data: Arc<DataColumnSidecar<E>>,
|
|
seen_timestamp: Duration,
|
|
}
|
|
|
|
impl<E: EthSpec> KzgVerifiedDataColumn<E> {
|
|
/// Mark a data column as KZG verified. Caller must ONLY use this on columns constructed
|
|
/// from EL blobs.
|
|
pub fn from_execution_verified(data_column: Arc<DataColumnSidecar<E>>) -> Self {
|
|
Self {
|
|
data: data_column,
|
|
seen_timestamp: timestamp_now(),
|
|
}
|
|
}
|
|
|
|
/// Create a `KzgVerifiedDataColumn` from `DataColumnSidecar` for testing ONLY.
|
|
pub(crate) fn __new_for_testing(data_column: Arc<DataColumnSidecar<E>>) -> Self {
|
|
Self {
|
|
data: data_column,
|
|
seen_timestamp: timestamp_now(),
|
|
}
|
|
}
|
|
|
|
pub fn from_batch_with_scoring(
|
|
data_columns: Vec<Arc<DataColumnSidecar<E>>>,
|
|
kzg: &Kzg,
|
|
) -> Result<Vec<Self>, (Option<ColumnIndex>, KzgError)> {
|
|
verify_kzg_for_data_column_list(data_columns.iter(), kzg)?;
|
|
Ok(data_columns
|
|
.into_iter()
|
|
.map(|column| Self {
|
|
data: column,
|
|
seen_timestamp: timestamp_now(),
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
pub fn from_batch_with_scoring_and_commitments(
|
|
data_columns: Vec<Arc<DataColumnSidecar<E>>>,
|
|
kzg_commitments: &[KzgCommitment],
|
|
kzg: &Kzg,
|
|
) -> Result<Vec<Self>, (Option<ColumnIndex>, KzgError)> {
|
|
let _timer = metrics::start_timer(&metrics::KZG_VERIFICATION_DATA_COLUMN_BATCH_TIMES);
|
|
validate_full_data_columns_with_commitments(kzg, data_columns.iter(), kzg_commitments)?;
|
|
Ok(data_columns
|
|
.into_iter()
|
|
.map(|column| Self {
|
|
data: column,
|
|
seen_timestamp: timestamp_now(),
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
pub fn to_data_column(self) -> Arc<DataColumnSidecar<E>> {
|
|
self.data
|
|
}
|
|
pub fn as_data_column(&self) -> &DataColumnSidecar<E> {
|
|
&self.data
|
|
}
|
|
/// This is cheap as we're calling clone on an Arc
|
|
pub fn clone_data_column(&self) -> Arc<DataColumnSidecar<E>> {
|
|
self.data.clone()
|
|
}
|
|
|
|
pub fn index(&self) -> ColumnIndex {
|
|
*self.data.index()
|
|
}
|
|
}
|
|
|
|
/// Wrapper over a `VerifiablePartialDataColumn` for which we have completed kzg verification.
|
|
#[derive(Debug, Educe, Clone)]
|
|
#[educe(PartialEq, Eq)]
|
|
pub struct KzgVerifiedPartialDataColumn<E: EthSpec> {
|
|
data: Arc<PartialDataColumn<E>>,
|
|
latest_cell_timestamp: Duration,
|
|
}
|
|
|
|
impl<E: EthSpec> KzgVerifiedPartialDataColumn<E> {
|
|
/// Create a `KzgVerifiedPartialDataColumn` for testing ONLY.
|
|
pub(crate) fn __new_for_testing(data_column: Arc<PartialDataColumn<E>>) -> Self {
|
|
Self {
|
|
data: data_column,
|
|
latest_cell_timestamp: timestamp_now(),
|
|
}
|
|
}
|
|
|
|
/// Mark a partial data column as KZG verified. Caller must ONLY use this on columns constructed
|
|
/// from EL blobs.
|
|
pub fn from_execution_verified(data_column: Arc<PartialDataColumn<E>>) -> Self {
|
|
Self {
|
|
data: data_column,
|
|
latest_cell_timestamp: timestamp_now(),
|
|
}
|
|
}
|
|
|
|
pub fn to_data_column(self) -> Arc<PartialDataColumn<E>> {
|
|
self.data
|
|
}
|
|
|
|
pub fn as_data_column(&self) -> &PartialDataColumn<E> {
|
|
&self.data
|
|
}
|
|
|
|
pub fn index(&self) -> ColumnIndex {
|
|
self.data.index
|
|
}
|
|
|
|
pub fn block_root(&self) -> Hash256 {
|
|
self.data.block_root
|
|
}
|
|
}
|
|
|
|
/// Wrapper over a `PartialDataColumnHeader` for which we have completed gossip verification.
|
|
#[derive(Debug, Educe, Clone)]
|
|
#[educe(PartialEq, Eq)]
|
|
pub struct GossipVerifiedPartialDataColumnHeader<E: EthSpec> {
|
|
header: Arc<PartialDataColumnHeader<E>>,
|
|
previously_cached: bool,
|
|
}
|
|
|
|
impl<E: EthSpec> GossipVerifiedPartialDataColumnHeader<E> {
|
|
pub fn new<T: BeaconChainTypes<EthSpec = E>>(
|
|
group_id: Hash256,
|
|
header: PartialDataColumnHeader<E>,
|
|
chain: &BeaconChain<T>,
|
|
) -> Result<Self, GossipPartialDataColumnError> {
|
|
let column_slot = header.slot();
|
|
if header.kzg_commitments.is_empty() {
|
|
return Err(GossipDataColumnError::UnexpectedDataColumn.into());
|
|
}
|
|
|
|
let header_hash = header.signed_block_header.message.canonical_root();
|
|
if group_id != header_hash {
|
|
return Err(GossipPartialDataColumnError::HeaderIncorrectRoot {
|
|
group_id,
|
|
header_hash,
|
|
});
|
|
}
|
|
|
|
verify_sidecar_not_from_future_slot(chain, column_slot)?;
|
|
verify_slot_greater_than_latest_finalized_slot(chain, column_slot)?;
|
|
verify_partial_column_header_inclusion_proof(&header)?;
|
|
let parent_block = verify_parent_block_and_finalized_descendant(
|
|
header.signed_block_header.message.parent_root,
|
|
column_slot,
|
|
chain,
|
|
)?;
|
|
verify_slot_higher_than_parent(&parent_block, column_slot)?;
|
|
verify_proposer_and_signature(&header.signed_block_header, &parent_block, chain)?;
|
|
|
|
let header = Arc::new(header);
|
|
|
|
// Cache the valid header
|
|
let Some(assembler) = chain.data_availability_checker.partial_assembler() else {
|
|
return Err(GossipPartialDataColumnError::PartialColumnsDisabled);
|
|
};
|
|
let newly_cached = assembler.init(group_id, header.clone());
|
|
|
|
chain
|
|
.observed_slashable
|
|
.write()
|
|
.observe_slashable(
|
|
column_slot,
|
|
header.signed_block_header.message.proposer_index,
|
|
header_hash,
|
|
)
|
|
.map_err(BeaconChainError::from)?;
|
|
|
|
Ok(Self {
|
|
header,
|
|
previously_cached: !newly_cached,
|
|
})
|
|
}
|
|
|
|
pub fn new_from_cached(header: Arc<PartialDataColumnHeader<E>>) -> Self {
|
|
Self {
|
|
header,
|
|
previously_cached: true,
|
|
}
|
|
}
|
|
|
|
pub fn was_cached(&self) -> bool {
|
|
self.previously_cached
|
|
}
|
|
|
|
pub fn as_header(&self) -> &PartialDataColumnHeader<E> {
|
|
&self.header
|
|
}
|
|
|
|
pub fn into_header(self) -> Arc<PartialDataColumnHeader<E>> {
|
|
self.header
|
|
}
|
|
}
|
|
|
|
pub type CustodyDataColumnList<E> =
|
|
VariableList<CustodyDataColumn<E>, <E as EthSpec>::NumberOfColumns>;
|
|
|
|
/// Data column that we must custody
|
|
#[derive(Debug, Educe, Clone, Encode)]
|
|
#[educe(PartialEq, Eq, Hash(bound(E: EthSpec)))]
|
|
#[ssz(struct_behaviour = "transparent")]
|
|
pub struct CustodyDataColumn<E: EthSpec> {
|
|
data: Arc<DataColumnSidecar<E>>,
|
|
}
|
|
|
|
impl<E: EthSpec> CustodyDataColumn<E> {
|
|
/// Mark a column as custody column. Caller must ensure that our current custody requirements
|
|
/// include this column
|
|
pub fn from_asserted_custody(data: Arc<DataColumnSidecar<E>>) -> Self {
|
|
Self { data }
|
|
}
|
|
|
|
pub fn into_inner(self) -> Arc<DataColumnSidecar<E>> {
|
|
self.data
|
|
}
|
|
pub fn as_data_column(&self) -> &Arc<DataColumnSidecar<E>> {
|
|
&self.data
|
|
}
|
|
/// This is cheap as we're calling clone on an Arc
|
|
pub fn clone_arc(&self) -> Arc<DataColumnSidecar<E>> {
|
|
self.data.clone()
|
|
}
|
|
pub fn index(&self) -> u64 {
|
|
*self.data.index()
|
|
}
|
|
}
|
|
|
|
/// Data column that we must custody and has completed kzg verification.
|
|
/// Wraps a full `DataColumnSidecar`.
|
|
#[derive(Debug, Educe, Clone)]
|
|
#[educe(PartialEq, Eq)]
|
|
pub struct KzgVerifiedCustodyDataColumn<E: EthSpec> {
|
|
data: Arc<DataColumnSidecar<E>>,
|
|
seen_timestamp: Duration,
|
|
}
|
|
|
|
impl<E: EthSpec> KzgVerifiedCustodyDataColumn<E> {
|
|
/// Mark a column as custody column. Caller must ensure that our current custody requirements
|
|
/// include this column
|
|
pub fn from_asserted_custody(kzg_verified: KzgVerifiedDataColumn<E>) -> Self {
|
|
Self {
|
|
seen_timestamp: kzg_verified.seen_timestamp,
|
|
data: kzg_verified.to_data_column(),
|
|
}
|
|
}
|
|
|
|
pub fn reconstruct_columns(
|
|
kzg: &Kzg,
|
|
partial_set_of_columns: Vec<Arc<DataColumnSidecar<E>>>,
|
|
spec: &ChainSpec,
|
|
) -> Result<Vec<KzgVerifiedCustodyDataColumn<E>>, KzgError> {
|
|
let all_data_columns =
|
|
reconstruct_data_columns(kzg, partial_set_of_columns.to_vec(), spec)?;
|
|
|
|
let seen_timestamp = timestamp_now();
|
|
|
|
Ok(all_data_columns
|
|
.into_iter()
|
|
.map(|data| {
|
|
KzgVerifiedCustodyDataColumn::from_asserted_custody(KzgVerifiedDataColumn {
|
|
data,
|
|
seen_timestamp,
|
|
})
|
|
})
|
|
.collect::<Vec<_>>())
|
|
}
|
|
|
|
pub fn into_inner(self) -> Arc<DataColumnSidecar<E>> {
|
|
self.data
|
|
}
|
|
|
|
pub fn as_data_column(&self) -> &DataColumnSidecar<E> {
|
|
&self.data
|
|
}
|
|
pub fn clone_arc(&self) -> Arc<DataColumnSidecar<E>> {
|
|
self.data.clone()
|
|
}
|
|
pub fn index(&self) -> ColumnIndex {
|
|
*self.data.index()
|
|
}
|
|
|
|
pub fn seen_timestamp(&self) -> Duration {
|
|
self.seen_timestamp
|
|
}
|
|
}
|
|
|
|
/// Partial data column that we must custody and has completed kzg verification.
|
|
/// Wraps a `VerifiablePartialDataColumn`.
|
|
#[derive(Debug, Educe, Clone)]
|
|
#[educe(PartialEq, Eq)]
|
|
pub struct KzgVerifiedCustodyPartialDataColumn<E: EthSpec> {
|
|
data: Arc<PartialDataColumn<E>>,
|
|
latest_cell_timestamp: Duration,
|
|
}
|
|
|
|
impl<E: EthSpec> KzgVerifiedCustodyPartialDataColumn<E> {
|
|
/// Mark a partial column as custody column. Caller must ensure that our current custody requirements
|
|
/// include this column
|
|
pub fn from_asserted_custody(kzg_verified: KzgVerifiedPartialDataColumn<E>) -> Self {
|
|
Self {
|
|
latest_cell_timestamp: kzg_verified.latest_cell_timestamp,
|
|
data: kzg_verified.to_data_column(),
|
|
}
|
|
}
|
|
|
|
pub fn into_inner(self) -> Arc<PartialDataColumn<E>> {
|
|
self.data
|
|
}
|
|
|
|
pub fn as_data_column(&self) -> &PartialDataColumn<E> {
|
|
&self.data
|
|
}
|
|
|
|
pub fn index(&self) -> ColumnIndex {
|
|
self.data.index
|
|
}
|
|
|
|
/// Merge two verified partial data columns.
|
|
///
|
|
/// Each column must be internally consistent. Additionally, the columns to be merged must have
|
|
/// the same block root and index.
|
|
/// An error is returned if the columns are internally inconsistent or incompatible for merging.
|
|
///
|
|
/// If both columns contain the same cell, the cell from `self` is used - however, as they are
|
|
/// KZG verified, they will be the same.
|
|
pub fn merge(&self, other: &Self) -> Result<Self, PartialDataColumnSidecarError> {
|
|
let self_sidecar = &self.data.sidecar;
|
|
let other_sidecar = &other.data.sidecar;
|
|
|
|
// Check that each sidecar is internally consistent by checking the lengths.
|
|
self_sidecar.verify_len()?;
|
|
other_sidecar.verify_len()?;
|
|
if self.data.block_root != other.data.block_root || self.data.index != other.data.index {
|
|
return Err(PartialDataColumnSidecarError::ConflictingData);
|
|
}
|
|
if self_sidecar.cells_present_bitmap.len() != other_sidecar.cells_present_bitmap.len() {
|
|
return Err(PartialDataColumnSidecarError::DifferingLengths {
|
|
lhs_len: self_sidecar.cells_present_bitmap.len(),
|
|
rhs_len: other_sidecar.cells_present_bitmap.len(),
|
|
});
|
|
}
|
|
|
|
let new_bitmap = self_sidecar
|
|
.cells_present_bitmap
|
|
.union(&other_sidecar.cells_present_bitmap);
|
|
let len = new_bitmap.num_set_bits();
|
|
let mut new_column = Vec::with_capacity(len);
|
|
let mut new_proofs = Vec::with_capacity(len);
|
|
let mut self_iter = self_sidecar
|
|
.column
|
|
.iter()
|
|
.zip(self_sidecar.kzg_proofs.iter());
|
|
let mut other_iter = other_sidecar
|
|
.column
|
|
.iter()
|
|
.zip(other_sidecar.kzg_proofs.iter());
|
|
|
|
for presence_bits in self_sidecar
|
|
.cells_present_bitmap
|
|
.iter()
|
|
.zip(other_sidecar.cells_present_bitmap.iter())
|
|
{
|
|
match presence_bits {
|
|
(false, false) => {}
|
|
(true, other) => {
|
|
let (cell, proof) = self_iter
|
|
.next()
|
|
.ok_or(PartialDataColumnSidecarError::UnexpectedBounds)?;
|
|
new_column.push(cell.clone());
|
|
new_proofs.push(*proof);
|
|
if other {
|
|
other_iter
|
|
.next()
|
|
.ok_or(PartialDataColumnSidecarError::UnexpectedBounds)?;
|
|
}
|
|
}
|
|
(false, true) => {
|
|
let (cell, proof) = other_iter
|
|
.next()
|
|
.ok_or(PartialDataColumnSidecarError::UnexpectedBounds)?;
|
|
new_column.push(cell.clone());
|
|
new_proofs.push(*proof);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(Self {
|
|
data: Arc::new(PartialDataColumn {
|
|
block_root: self.data.block_root,
|
|
index: self.data.index,
|
|
sidecar: PartialDataColumnSidecar {
|
|
cells_present_bitmap: new_bitmap,
|
|
column: new_column
|
|
.try_into()
|
|
.map_err(|_| PartialDataColumnSidecarError::UnexpectedBounds)?,
|
|
kzg_proofs: new_proofs
|
|
.try_into()
|
|
.map_err(|_| PartialDataColumnSidecarError::UnexpectedBounds)?,
|
|
header: if self_sidecar.header.is_some() {
|
|
self_sidecar.header.clone()
|
|
} else {
|
|
other_sidecar.header.clone()
|
|
},
|
|
},
|
|
}),
|
|
latest_cell_timestamp: self.latest_cell_timestamp.max(other.latest_cell_timestamp),
|
|
})
|
|
}
|
|
|
|
pub fn try_clone_full(
|
|
&self,
|
|
header: &PartialDataColumnHeader<E>,
|
|
) -> Option<KzgVerifiedCustodyDataColumn<E>> {
|
|
self.data
|
|
.try_clone_full(header)
|
|
.map(|data| KzgVerifiedCustodyDataColumn {
|
|
data: Arc::new(data),
|
|
seen_timestamp: self.latest_cell_timestamp,
|
|
})
|
|
}
|
|
|
|
/// Try to convert the partial data column into a full one, returning None if the conversion
|
|
/// fails.
|
|
/// May clone the column if the Arc cannot be unwrapped.
|
|
pub fn try_into_full(
|
|
self,
|
|
header: &PartialDataColumnHeader<E>,
|
|
) -> Option<KzgVerifiedCustodyDataColumn<E>> {
|
|
match Arc::try_unwrap(self.data) {
|
|
Ok(data) => data.try_into_full(header),
|
|
Err(data) => data.try_clone_full(header),
|
|
}
|
|
.map(|data| KzgVerifiedCustodyDataColumn {
|
|
data: Arc::new(data),
|
|
seen_timestamp: self.latest_cell_timestamp,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Complete kzg verification for a `DataColumnSidecar`.
|
|
///
|
|
/// Returns an error if the kzg verification check fails.
|
|
#[instrument(skip_all, level = "debug")]
|
|
pub fn verify_kzg_for_data_column<E: EthSpec>(
|
|
data_column: Arc<DataColumnSidecar<E>>,
|
|
cells_to_verify: PartialDataColumnSidecarRef<E>,
|
|
kzg: &Kzg,
|
|
seen_timestamp: Duration,
|
|
) -> Result<KzgVerifiedDataColumn<E>, (Option<ColumnIndex>, KzgError)> {
|
|
let _timer = metrics::start_timer(&metrics::KZG_VERIFICATION_DATA_COLUMN_SINGLE_TIMES);
|
|
let Ok(kzg_commitments) = data_column.kzg_commitments() else {
|
|
return Err((
|
|
Some(*data_column.index()),
|
|
KzgError::InconsistentArrayLength("todo(gloas)".to_string()),
|
|
));
|
|
};
|
|
validate_partial_data_columns(
|
|
kzg,
|
|
iter::once((*data_column.index(), cells_to_verify)),
|
|
kzg_commitments,
|
|
)?;
|
|
Ok(KzgVerifiedDataColumn {
|
|
data: data_column,
|
|
seen_timestamp,
|
|
})
|
|
}
|
|
|
|
#[instrument(skip_all, level = "debug")]
|
|
pub fn verify_kzg_for_data_column_with_commitments<E: EthSpec>(
|
|
data_column: Arc<DataColumnSidecar<E>>,
|
|
cells_to_verify: PartialDataColumnSidecarRef<E>,
|
|
kzg_commitments: &[KzgCommitment],
|
|
kzg: &Kzg,
|
|
seen_timestamp: Duration,
|
|
) -> Result<KzgVerifiedDataColumn<E>, (Option<ColumnIndex>, KzgError)> {
|
|
let _timer = metrics::start_timer(&metrics::KZG_VERIFICATION_DATA_COLUMN_SINGLE_TIMES);
|
|
validate_partial_data_columns(
|
|
kzg,
|
|
iter::once((*data_column.index(), cells_to_verify)),
|
|
kzg_commitments,
|
|
)?;
|
|
Ok(KzgVerifiedDataColumn {
|
|
data: data_column,
|
|
seen_timestamp,
|
|
})
|
|
}
|
|
|
|
/// Complete kzg verification for a `VerifiablePartialDataColumn`.
|
|
///
|
|
/// Returns an error if the kzg verification check fails.
|
|
#[instrument(skip_all, level = "debug")]
|
|
pub fn verify_kzg_for_partial_data_column<E: EthSpec>(
|
|
data_column: Arc<PartialDataColumn<E>>,
|
|
cells_to_verify: PartialDataColumnSidecarRef<E>,
|
|
header: &GossipVerifiedPartialDataColumnHeader<E>,
|
|
kzg: &Kzg,
|
|
seen_timestamp: Duration,
|
|
) -> Result<KzgVerifiedPartialDataColumn<E>, GossipPartialDataColumnError> {
|
|
let _timer = metrics::start_timer(&metrics::KZG_VERIFICATION_DATA_COLUMN_SINGLE_TIMES);
|
|
validate_partial_data_columns(
|
|
kzg,
|
|
iter::once((data_column.index, cells_to_verify)),
|
|
header.header.kzg_commitments.as_ref(),
|
|
)
|
|
.map_err(|(_, e)| GossipDataColumnError::InvalidKzgProof(e))?;
|
|
Ok(KzgVerifiedPartialDataColumn {
|
|
data: data_column,
|
|
latest_cell_timestamp: seen_timestamp,
|
|
})
|
|
}
|
|
|
|
/// Complete kzg verification for a list of `DataColumnSidecar`s.
|
|
/// Returns an error for the first `DataColumnSidecar`s that fails kzg verification.
|
|
///
|
|
/// Note: This function should be preferred over calling `verify_kzg_for_data_column`
|
|
/// in a loop since this function kzg verifies a list of data columns more efficiently.
|
|
pub fn verify_kzg_for_data_column_list<'a, E: EthSpec, I>(
|
|
data_column_iter: I,
|
|
kzg: &'a Kzg,
|
|
) -> Result<(), (Option<ColumnIndex>, KzgError)>
|
|
where
|
|
I: Iterator<Item = &'a Arc<DataColumnSidecar<E>>> + Clone,
|
|
{
|
|
let _timer = metrics::start_timer(&metrics::KZG_VERIFICATION_DATA_COLUMN_BATCH_TIMES);
|
|
validate_full_data_columns(kzg, data_column_iter)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[instrument(
|
|
skip_all,
|
|
name = "validate_data_column_sidecar_for_gossip",
|
|
level = "debug"
|
|
)]
|
|
pub fn validate_data_column_sidecar_for_gossip_fulu<T: BeaconChainTypes, O: ObservationStrategy>(
|
|
data_column_fulu: &DataColumnSidecarFulu<T::EthSpec>,
|
|
subnet: DataColumnSubnetId,
|
|
chain: &BeaconChain<T>,
|
|
) -> Result<(), GossipDataColumnError> {
|
|
let data_column = Arc::new(DataColumnSidecar::Fulu(data_column_fulu.clone()));
|
|
let column_slot = data_column.slot();
|
|
|
|
verify_data_column_sidecar_with_commitments_len(
|
|
&data_column,
|
|
data_column_fulu.kzg_commitments.len(),
|
|
&chain.spec,
|
|
)?;
|
|
verify_index_matches_subnet(&data_column, subnet, &chain.spec)?;
|
|
verify_sidecar_not_from_future_slot(chain, column_slot)?;
|
|
verify_slot_greater_than_latest_finalized_slot(chain, column_slot)?;
|
|
verify_is_unknown_sidecar(chain, &data_column)?;
|
|
|
|
// Check if the data column is already in the DA checker cache. This happens when data columns
|
|
// are made available through the `engine_getBlobs` method and/or partial messages have arrived.
|
|
// If it exists in the cache, we know it has already passed the gossip checks, even though this
|
|
// particular instance hasn't been seen / published on the gossip network yet (passed the
|
|
// `verify_is_unknown_sidecar` check above). In this case, we should accept it for gossip
|
|
// propagation.
|
|
let Some(cells_to_kzg_verify) = chain
|
|
.data_availability_checker
|
|
.missing_cells_for_column_sidecar(&data_column)
|
|
.map_err(|err| match err {
|
|
MissingCellsError::MismatchesCachedColumn => {
|
|
GossipDataColumnError::MismatchesCachedColumn
|
|
}
|
|
MissingCellsError::UnexpectedError(_) => todo!("handle unexpected error"),
|
|
})?
|
|
else {
|
|
// Observe this data column so we don't process it again.
|
|
if O::observe() {
|
|
observe_gossip_data_column(&data_column, chain)?;
|
|
}
|
|
return Err(GossipDataColumnError::PriorKnownUnpublished);
|
|
};
|
|
|
|
verify_column_inclusion_proof(data_column_fulu)?;
|
|
let parent_block = verify_parent_block_and_finalized_descendant(
|
|
data_column_fulu.block_parent_root(),
|
|
column_slot,
|
|
chain,
|
|
)?;
|
|
verify_slot_higher_than_parent(&parent_block, column_slot)?;
|
|
verify_proposer_and_signature(&data_column_fulu.signed_block_header, &parent_block, chain)?;
|
|
|
|
verify_kzg_for_data_column(
|
|
data_column.clone(),
|
|
cells_to_kzg_verify,
|
|
&chain.kzg,
|
|
chain.slot_clock.now_duration().unwrap_or_default(),
|
|
)
|
|
.map_err(|(_, e)| GossipDataColumnError::InvalidKzgProof(e))?;
|
|
|
|
chain
|
|
.observed_slashable
|
|
.write()
|
|
.observe_slashable(
|
|
column_slot,
|
|
data_column_fulu.block_proposer_index(),
|
|
data_column.block_root(),
|
|
)
|
|
.map_err(|e| GossipDataColumnError::BeaconChainError(Box::new(e.into())))?;
|
|
|
|
if O::observe() {
|
|
observe_gossip_data_column(&data_column, chain)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[instrument(
|
|
skip_all,
|
|
name = "validate_data_column_sidecar_for_gossip_gloas",
|
|
level = "debug"
|
|
)]
|
|
pub fn validate_data_column_sidecar_for_gossip_gloas<
|
|
T: BeaconChainTypes,
|
|
O: ObservationStrategy,
|
|
>(
|
|
data_column_gloas: &DataColumnSidecarGloas<T::EthSpec>,
|
|
subnet: DataColumnSubnetId,
|
|
chain: &BeaconChain<T>,
|
|
) -> Result<(), GossipDataColumnError> {
|
|
let data_column = Arc::new(DataColumnSidecar::Gloas(data_column_gloas.clone()));
|
|
let column_slot = data_column.slot();
|
|
|
|
if *data_column.index() >= T::EthSpec::number_of_columns() as u64 {
|
|
return Err(GossipDataColumnError::InvalidColumnIndex(
|
|
*data_column.index(),
|
|
));
|
|
}
|
|
|
|
if !chain
|
|
.spec
|
|
.fork_name_at_slot::<T::EthSpec>(column_slot)
|
|
.gloas_enabled()
|
|
{
|
|
return Err(GossipDataColumnError::InvalidVariant);
|
|
}
|
|
|
|
verify_index_matches_subnet(&data_column, subnet, &chain.spec)?;
|
|
verify_sidecar_not_from_future_slot(chain, column_slot)?;
|
|
verify_slot_greater_than_latest_finalized_slot(chain, column_slot)?;
|
|
verify_is_unknown_sidecar(chain, &data_column)?;
|
|
|
|
let bid = load_gloas_payload_bid(data_column.block_root(), chain)?.ok_or(
|
|
GossipDataColumnError::BlockRootUnknown {
|
|
block_root: data_column.block_root(),
|
|
slot: column_slot,
|
|
},
|
|
)?;
|
|
if bid.message.slot != column_slot {
|
|
return Err(GossipDataColumnError::BlockSlotMismatch {
|
|
block_slot: bid.message.slot,
|
|
data_column_slot: column_slot,
|
|
});
|
|
}
|
|
let kzg_commitments = &bid.message.blob_kzg_commitments;
|
|
verify_data_column_sidecar_with_commitments_len(
|
|
&data_column,
|
|
kzg_commitments.len(),
|
|
&chain.spec,
|
|
)?;
|
|
|
|
let Some(cells_to_kzg_verify) = missing_cells_for_column_sidecar(chain, &data_column)? else {
|
|
// Observe this data column so we don't process it again.
|
|
if O::observe() {
|
|
observe_gossip_data_column(&data_column, chain)?;
|
|
}
|
|
return Err(GossipDataColumnError::PriorKnownUnpublished);
|
|
};
|
|
|
|
let kzg = &chain.kzg;
|
|
let seen_timestamp = chain.slot_clock.now_duration().unwrap_or_default();
|
|
verify_kzg_for_data_column_with_commitments(
|
|
data_column.clone(),
|
|
cells_to_kzg_verify,
|
|
kzg_commitments.as_ref(),
|
|
kzg,
|
|
seen_timestamp,
|
|
)
|
|
.map_err(|(_, e)| GossipDataColumnError::InvalidKzgProof(e))?;
|
|
|
|
if O::observe() {
|
|
observe_gossip_data_column(&data_column, chain)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[instrument(skip_all, level = "debug")]
|
|
pub fn validate_partial_data_column_sidecar_for_gossip<T: BeaconChainTypes>(
|
|
mut column: Box<PartialDataColumn<T::EthSpec>>,
|
|
chain: &BeaconChain<T>,
|
|
seen_timestamp: Duration,
|
|
) -> PartialColumnVerificationResult<T::EthSpec> {
|
|
let block_root = column.block_root;
|
|
|
|
// Remove the header (if any) to avoid wasted memory.
|
|
let header = column.sidecar.header.take();
|
|
|
|
let header = if let Some(header) = header {
|
|
// Header was sent, so it is required to be valid
|
|
match chain.verify_partial_data_column_header_for_gossip(block_root, header) {
|
|
Ok(verified) => verified,
|
|
Err(err) => {
|
|
return PartialColumnVerificationResult::Err(err);
|
|
}
|
|
}
|
|
} else {
|
|
let Some(assembler) = chain.data_availability_checker.partial_assembler() else {
|
|
return PartialColumnVerificationResult::Err(
|
|
GossipPartialDataColumnError::PartialColumnsDisabled,
|
|
);
|
|
};
|
|
|
|
// There is no header, so we check if we have a cached one to use
|
|
let Some(header) = assembler
|
|
.get_header(&column.block_root)
|
|
.map(GossipVerifiedPartialDataColumnHeader::new_from_cached)
|
|
else {
|
|
return PartialColumnVerificationResult::Err(
|
|
GossipPartialDataColumnError::MissingHeader,
|
|
);
|
|
};
|
|
|
|
// If there was no header, there must be at least one cell.
|
|
if column.sidecar.column.is_empty() {
|
|
return PartialColumnVerificationResult::ErrWithValidHeader {
|
|
err: GossipPartialDataColumnError::EmptyMessage,
|
|
header,
|
|
};
|
|
}
|
|
|
|
header
|
|
};
|
|
|
|
// The number of cells nad proofs must match the population count of the bitmap.
|
|
let bitmap_popcount = column.sidecar.cells_present_bitmap.num_set_bits();
|
|
let cells_len = column.sidecar.column.len();
|
|
let proofs_len = column.sidecar.kzg_proofs.len();
|
|
if bitmap_popcount != cells_len || bitmap_popcount != proofs_len {
|
|
return PartialColumnVerificationResult::ErrWithValidHeader {
|
|
err: GossipPartialDataColumnError::InconsistentPresentCount {
|
|
bitmap_popcount,
|
|
cells_len,
|
|
proofs_len,
|
|
},
|
|
header,
|
|
};
|
|
}
|
|
|
|
let bitmap_len = column.sidecar.cells_present_bitmap.len();
|
|
let commitments_len = header.as_header().kzg_commitments.len();
|
|
if bitmap_len != commitments_len {
|
|
return PartialColumnVerificationResult::ErrWithValidHeader {
|
|
err: GossipPartialDataColumnError::InconsistentCommitmentsLength {
|
|
bitmap_len,
|
|
commitments_len,
|
|
},
|
|
header,
|
|
};
|
|
}
|
|
|
|
let column = Arc::from(column);
|
|
let cells_to_kzg_verify = match chain
|
|
.data_availability_checker
|
|
.missing_cells_for_partial_column_sidecar(&column)
|
|
{
|
|
Ok(Some(cells_to_kzg_verify)) => cells_to_kzg_verify,
|
|
Ok(None) => {
|
|
return PartialColumnVerificationResult::ErrWithValidHeader {
|
|
err: GossipDataColumnError::PriorKnownUnpublished.into(),
|
|
header,
|
|
};
|
|
}
|
|
Err(MissingCellsError::MismatchesCachedColumn) => {
|
|
return PartialColumnVerificationResult::ErrWithValidHeader {
|
|
err: GossipDataColumnError::MismatchesCachedColumn.into(),
|
|
header,
|
|
};
|
|
}
|
|
Err(MissingCellsError::UnexpectedError(e)) => todo!("handle unexpected error {:?}", e),
|
|
};
|
|
|
|
// We do not have to check block related data here, as we create the verifiable column from
|
|
// gossip accepted block
|
|
let kzg = &chain.kzg;
|
|
let column = match verify_kzg_for_partial_data_column(
|
|
column.clone(),
|
|
cells_to_kzg_verify,
|
|
&header,
|
|
kzg,
|
|
seen_timestamp,
|
|
) {
|
|
Ok(column) => column,
|
|
Err(err) => {
|
|
return PartialColumnVerificationResult::ErrWithValidHeader { err, header };
|
|
}
|
|
};
|
|
|
|
PartialColumnVerificationResult::Ok { column, header }
|
|
}
|
|
|
|
/// The result of a `validate_partial_data_column_sidecar_for_gossip` call. Any headers returned
|
|
/// herein were cached during this call or previously cached.
|
|
pub enum PartialColumnVerificationResult<E: EthSpec> {
|
|
/// Verification succeeded fully.
|
|
Ok {
|
|
column: KzgVerifiedPartialDataColumn<E>,
|
|
header: GossipVerifiedPartialDataColumnHeader<E>,
|
|
},
|
|
/// Verification of the column failed, but the header is valid.
|
|
ErrWithValidHeader {
|
|
err: GossipPartialDataColumnError,
|
|
header: GossipVerifiedPartialDataColumnHeader<E>,
|
|
},
|
|
/// Verification of the column or header failed, and no valid header was cached previously.
|
|
Err(GossipPartialDataColumnError),
|
|
}
|
|
|
|
fn verify_data_column_sidecar_with_commitments_len<E: EthSpec>(
|
|
data_column: &DataColumnSidecar<E>,
|
|
commitments_len: usize,
|
|
spec: &ChainSpec,
|
|
) -> Result<(), GossipDataColumnError> {
|
|
if *data_column.index() >= E::number_of_columns() as u64 {
|
|
return Err(GossipDataColumnError::InvalidColumnIndex(
|
|
*data_column.index(),
|
|
));
|
|
}
|
|
|
|
if commitments_len == 0 {
|
|
return Err(GossipDataColumnError::UnexpectedDataColumn);
|
|
}
|
|
|
|
let cells_len = data_column.column().len();
|
|
let proofs_len = data_column.kzg_proofs().len();
|
|
let max_blobs_per_block = spec.max_blobs_per_block(data_column.epoch()) as usize;
|
|
|
|
if commitments_len > max_blobs_per_block {
|
|
return Err(GossipDataColumnError::MaxBlobsPerBlockExceeded {
|
|
max_blobs_per_block,
|
|
commitments_len,
|
|
});
|
|
}
|
|
|
|
if cells_len != commitments_len {
|
|
return Err(GossipDataColumnError::InconsistentCommitmentsLength {
|
|
cells_len,
|
|
commitments_len,
|
|
});
|
|
}
|
|
|
|
if cells_len != proofs_len {
|
|
return Err(GossipDataColumnError::InconsistentProofsLength {
|
|
cells_len,
|
|
proofs_len,
|
|
});
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn load_gloas_payload_bid<T: BeaconChainTypes>(
|
|
block_root: Hash256,
|
|
chain: &BeaconChain<T>,
|
|
) -> Result<Option<Arc<SignedExecutionPayloadBid<T::EthSpec>>>, BeaconChainError> {
|
|
if let Some(bid) = chain.pending_payload_cache.get_bid(&block_root) {
|
|
return Ok(Some(bid));
|
|
}
|
|
|
|
let bid = if let Some(block) = chain.early_attester_cache.get_block(block_root) {
|
|
signed_payload_bid_from_block(block.as_ref()).map_err(BeaconChainError::BeaconStateError)?
|
|
} else {
|
|
match chain
|
|
.store
|
|
.try_get_full_block(&block_root)
|
|
.map_err(BeaconChainError::DBError)?
|
|
{
|
|
Some(DatabaseBlock::Full(block)) => {
|
|
signed_payload_bid_from_block(&block).map_err(BeaconChainError::BeaconStateError)?
|
|
}
|
|
Some(DatabaseBlock::Blinded(block)) => {
|
|
signed_payload_bid_from_block(&block).map_err(BeaconChainError::BeaconStateError)?
|
|
}
|
|
None => {
|
|
return Ok(None);
|
|
}
|
|
}
|
|
};
|
|
|
|
chain
|
|
.pending_payload_cache
|
|
.insert_bid(block_root, bid.clone());
|
|
|
|
Ok(Some(bid))
|
|
}
|
|
|
|
fn missing_cells_for_column_sidecar<'a, T: BeaconChainTypes>(
|
|
chain: &'_ BeaconChain<T>,
|
|
data_column: &'a DataColumnSidecar<T::EthSpec>,
|
|
) -> Result<Option<PartialDataColumnSidecarRef<'a, T::EthSpec>>, GossipDataColumnError> {
|
|
let result = if chain
|
|
.spec
|
|
.fork_name_at_slot::<T::EthSpec>(data_column.slot())
|
|
.gloas_enabled()
|
|
{
|
|
chain
|
|
.pending_payload_cache
|
|
.missing_cells_for_column_sidecar(data_column)
|
|
} else {
|
|
chain
|
|
.data_availability_checker
|
|
.missing_cells_for_column_sidecar(data_column)
|
|
};
|
|
|
|
result.map_err(|err| match err {
|
|
MissingCellsError::MismatchesCachedColumn => GossipDataColumnError::MismatchesCachedColumn,
|
|
MissingCellsError::UnexpectedError(_) => todo!("handle unexpected error"),
|
|
})
|
|
}
|
|
|
|
/// Verify that `column_sidecar` is not yet known, i.e. this is the first time `column_sidecar` has been received for the tuple:
|
|
/// `(block_header.slot, block_header.proposer_index, column_sidecar.index)`
|
|
fn verify_is_unknown_sidecar<T: BeaconChainTypes>(
|
|
chain: &BeaconChain<T>,
|
|
column_sidecar: &DataColumnSidecar<T::EthSpec>,
|
|
) -> Result<(), GossipDataColumnError> {
|
|
if let Some(observation_key) = chain
|
|
.observed_column_sidecars
|
|
.read()
|
|
.observation_key_is_known(column_sidecar)
|
|
.map_err(|e: ObservedDataSidecarsError| {
|
|
GossipDataColumnError::BeaconChainError(Box::new(e.into()))
|
|
})?
|
|
{
|
|
return Err(GossipDataColumnError::PriorKnown {
|
|
observation_key,
|
|
index: *column_sidecar.index(),
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn verify_column_inclusion_proof<E: EthSpec>(
|
|
data_column: &DataColumnSidecarFulu<E>,
|
|
) -> Result<(), GossipDataColumnError> {
|
|
let _timer = metrics::start_timer(&metrics::DATA_COLUMN_SIDECAR_INCLUSION_PROOF_VERIFICATION);
|
|
if !data_column.verify_inclusion_proof() {
|
|
return Err(GossipDataColumnError::InvalidInclusionProof);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn verify_partial_column_header_inclusion_proof<E: EthSpec>(
|
|
header: &PartialDataColumnHeader<E>,
|
|
) -> Result<(), GossipDataColumnError> {
|
|
let _timer = metrics::start_timer(&metrics::DATA_COLUMN_SIDECAR_INCLUSION_PROOF_VERIFICATION);
|
|
if !header.verify_inclusion_proof() {
|
|
return Err(GossipDataColumnError::InvalidInclusionProof);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn verify_slot_higher_than_parent(
|
|
parent_block: &Block,
|
|
data_column_slot: Slot,
|
|
) -> Result<(), GossipDataColumnError> {
|
|
if parent_block.slot >= data_column_slot {
|
|
return Err(GossipDataColumnError::IsNotLaterThanParent {
|
|
data_column_slot,
|
|
parent_slot: parent_block.slot,
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn verify_parent_block_and_finalized_descendant<T: BeaconChainTypes>(
|
|
block_parent_root: Hash256,
|
|
slot: Slot,
|
|
chain: &BeaconChain<T>,
|
|
) -> Result<ProtoBlock, GossipDataColumnError> {
|
|
let fork_choice = chain.canonical_head.fork_choice_read_lock();
|
|
|
|
// We have already verified that the column is past finalization, so we can
|
|
// just check fork choice for the block's parent.
|
|
let Some(parent_block) = fork_choice.get_block(&block_parent_root) else {
|
|
return Err(GossipDataColumnError::ParentUnknown {
|
|
parent_root: block_parent_root,
|
|
slot,
|
|
});
|
|
};
|
|
|
|
// Do not process a column that does not descend from the finalized root.
|
|
// We just loaded the parent_block, so we can be sure that it exists in fork choice.
|
|
if !fork_choice.is_finalized_checkpoint_or_descendant(block_parent_root) {
|
|
return Err(GossipDataColumnError::NotFinalizedDescendant { block_parent_root });
|
|
}
|
|
|
|
Ok(parent_block)
|
|
}
|
|
|
|
fn verify_proposer_and_signature<T: BeaconChainTypes>(
|
|
signed_block_header: &SignedBeaconBlockHeader,
|
|
parent_block: &ProtoBlock,
|
|
chain: &BeaconChain<T>,
|
|
) -> Result<(), GossipDataColumnError> {
|
|
let column_slot = signed_block_header.message.slot;
|
|
let slots_per_epoch = T::EthSpec::slots_per_epoch();
|
|
let column_epoch = column_slot.epoch(slots_per_epoch);
|
|
let block_root = signed_block_header.message.tree_hash_root();
|
|
let block_parent_root = signed_block_header.message.parent_root;
|
|
|
|
let proposer_shuffling_root =
|
|
parent_block.proposer_shuffling_root_for_child_block(column_epoch, &chain.spec);
|
|
|
|
let proposer = chain.with_proposer_cache(
|
|
proposer_shuffling_root,
|
|
column_epoch,
|
|
|proposers| proposers.get_slot::<T::EthSpec>(column_slot),
|
|
|| {
|
|
debug!(
|
|
%block_root,
|
|
"Proposer shuffling cache miss for column verification"
|
|
);
|
|
// We assume that the `Pending` state has the same shufflings as a `Full` state
|
|
// for the same block. Analysis: https://hackmd.io/@dapplion/gloas_dependant_root
|
|
chain
|
|
.store
|
|
.get_advanced_hot_state(block_parent_root, column_slot, parent_block.state_root)
|
|
.map_err(|e| GossipDataColumnError::BeaconChainError(Box::new(e.into())))?
|
|
.ok_or_else(|| {
|
|
GossipDataColumnError::BeaconChainError(Box::new(
|
|
BeaconChainError::DBInconsistent(format!(
|
|
"Missing state for parent block {block_parent_root:?}",
|
|
)),
|
|
))
|
|
})
|
|
},
|
|
)?;
|
|
let proposer_index = proposer.index;
|
|
let fork = proposer.fork;
|
|
|
|
// Signature verify the signed block header.
|
|
let signature_is_valid = {
|
|
let pubkey_cache = get_validator_pubkey_cache(chain)
|
|
.map_err(|_| GossipDataColumnError::PubkeyCacheTimeout)?;
|
|
|
|
let pubkey = pubkey_cache
|
|
.get(proposer_index)
|
|
.ok_or_else(|| GossipDataColumnError::UnknownValidator(proposer_index as u64))?;
|
|
signed_block_header.verify_signature::<T::EthSpec>(
|
|
pubkey,
|
|
&fork,
|
|
chain.genesis_validators_root,
|
|
&chain.spec,
|
|
)
|
|
};
|
|
|
|
if !signature_is_valid {
|
|
return Err(GossipDataColumnError::ProposalSignatureInvalid);
|
|
}
|
|
|
|
let column_proposer_index = signed_block_header.message.proposer_index;
|
|
if proposer_index != column_proposer_index as usize {
|
|
return Err(GossipDataColumnError::ProposerIndexMismatch {
|
|
sidecar: column_proposer_index as usize,
|
|
local: proposer_index,
|
|
});
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn verify_index_matches_subnet<E: EthSpec>(
|
|
data_column: &DataColumnSidecar<E>,
|
|
subnet: DataColumnSubnetId,
|
|
spec: &ChainSpec,
|
|
) -> Result<(), GossipDataColumnError> {
|
|
let expected_subnet = DataColumnSubnetId::from_column_index(*data_column.index(), spec);
|
|
if expected_subnet != subnet {
|
|
return Err(GossipDataColumnError::InvalidSubnetId {
|
|
received: subnet.into(),
|
|
expected: expected_subnet.into(),
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn verify_slot_greater_than_latest_finalized_slot<T: BeaconChainTypes>(
|
|
chain: &BeaconChain<T>,
|
|
column_slot: Slot,
|
|
) -> Result<(), GossipDataColumnError> {
|
|
let latest_finalized_slot = chain
|
|
.head()
|
|
.finalized_checkpoint()
|
|
.epoch
|
|
.start_slot(T::EthSpec::slots_per_epoch());
|
|
if column_slot <= latest_finalized_slot {
|
|
return Err(GossipDataColumnError::PastFinalizedSlot {
|
|
column_slot,
|
|
finalized_slot: latest_finalized_slot,
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn verify_sidecar_not_from_future_slot<T: BeaconChainTypes>(
|
|
chain: &BeaconChain<T>,
|
|
column_slot: Slot,
|
|
) -> Result<(), GossipDataColumnError> {
|
|
let latest_permissible_slot = chain
|
|
.slot_clock
|
|
.now_with_future_tolerance(chain.spec.maximum_gossip_clock_disparity())
|
|
.ok_or(BeaconChainError::UnableToReadSlot)?;
|
|
if column_slot > latest_permissible_slot {
|
|
return Err(GossipDataColumnError::FutureSlot {
|
|
message_slot: column_slot,
|
|
latest_permissible_slot,
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn observe_gossip_data_column<T: BeaconChainTypes>(
|
|
data_column_sidecar: &DataColumnSidecar<T::EthSpec>,
|
|
chain: &BeaconChain<T>,
|
|
) -> Result<(), GossipDataColumnError> {
|
|
// Pre-gloas: Now the signature is valid, store the proposal so we don't accept another data column sidecar
|
|
// with the same `ColumnIndex`.
|
|
// Post-gloas: The block associated with the sidecar has already been imported into fork choice. Store the
|
|
// columns `beacon_block_root` so we don't accept another data column sidecar with the same `ColumnIndex`.
|
|
// It's important to double-check that the `Observationkey` still
|
|
// hasn't been observed so we don't have a race-condition when verifying two sidecars
|
|
// 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 sidecars 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 let Some(observation_key) = chain
|
|
.observed_column_sidecars
|
|
.write()
|
|
.observe_sidecar(data_column_sidecar)
|
|
.map_err(|e: ObservedDataSidecarsError| {
|
|
GossipDataColumnError::BeaconChainError(Box::new(e.into()))
|
|
})?
|
|
{
|
|
return Err(GossipDataColumnError::PriorKnown {
|
|
observation_key,
|
|
index: *data_column_sidecar.index(),
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use crate::ChainConfig;
|
|
use crate::data_column_verification::{
|
|
GossipDataColumnError, GossipPartialDataColumnError, GossipVerifiedDataColumn,
|
|
GossipVerifiedPartialDataColumnHeader, KzgVerifiedCustodyPartialDataColumn,
|
|
PartialColumnVerificationResult, validate_data_column_sidecar_for_gossip_fulu,
|
|
validate_partial_data_column_sidecar_for_gossip,
|
|
};
|
|
use crate::observed_data_sidecars::Observe;
|
|
use crate::test_utils::{
|
|
BeaconChainHarness, EphemeralHarnessType, fork_name_from_env,
|
|
generate_data_column_sidecars_from_block, test_spec,
|
|
};
|
|
use eth2::types::BlobsBundle;
|
|
use execution_layer::test_utils::generate_blobs;
|
|
use kzg::KzgProof;
|
|
use ssz::BitList;
|
|
use ssz_types::VariableList;
|
|
use std::sync::Arc;
|
|
use std::time::UNIX_EPOCH;
|
|
use types::{
|
|
Cell, CellBitmap, DataColumnSidecar, DataColumnSidecarFulu, DataColumnSubnetId, EthSpec,
|
|
ForkName, MainnetEthSpec, PartialDataColumn, PartialDataColumnHeader,
|
|
PartialDataColumnSidecar,
|
|
};
|
|
|
|
type E = MainnetEthSpec;
|
|
|
|
// TODO(gloas) make this generic over gloas/fulu
|
|
#[tokio::test]
|
|
async fn test_validate_data_column_sidecar_for_gossip_fulu() {
|
|
// Setting up harness is slow, we initialise once and use it for all gossip validation tests.
|
|
let spec = ForkName::Fulu.make_genesis_spec(E::default_spec());
|
|
let harness = BeaconChainHarness::builder(E::default())
|
|
.spec(spec.into())
|
|
.deterministic_keypairs(64)
|
|
.fresh_ephemeral_store()
|
|
.mock_execution_layer()
|
|
.build();
|
|
harness.advance_slot();
|
|
|
|
let verify_fn = |column_sidecar: DataColumnSidecar<E>| {
|
|
let col_index = *column_sidecar.index();
|
|
validate_data_column_sidecar_for_gossip_fulu::<_, Observe>(
|
|
column_sidecar.as_fulu().unwrap(),
|
|
DataColumnSubnetId::from_column_index(col_index, &harness.spec),
|
|
&harness.chain,
|
|
)
|
|
};
|
|
empty_data_column_sidecars_fails_validation_fulu(&harness, &verify_fn).await;
|
|
data_column_sidecar_commitments_exceed_max_blobs_per_block(&harness, &verify_fn).await;
|
|
}
|
|
|
|
// TODO(gloas) make this generic over gloas/fulu
|
|
#[tokio::test]
|
|
async fn test_new_for_block_publishing_fulu() {
|
|
// Setting up harness is slow, we initialise once and use it for all gossip validation tests.
|
|
let spec = ForkName::Fulu.make_genesis_spec(E::default_spec());
|
|
let harness = BeaconChainHarness::builder(E::default())
|
|
.spec(spec.into())
|
|
.deterministic_keypairs(64)
|
|
.fresh_ephemeral_store()
|
|
.mock_execution_layer()
|
|
.build();
|
|
harness.advance_slot();
|
|
|
|
let verify_fn = |column_sidecar: DataColumnSidecar<E>| {
|
|
GossipVerifiedDataColumn::<_>::new_for_block_publishing(
|
|
column_sidecar.into(),
|
|
&harness.chain,
|
|
)
|
|
};
|
|
empty_data_column_sidecars_fails_validation_fulu(&harness, &verify_fn).await;
|
|
data_column_sidecar_commitments_exceed_max_blobs_per_block(&harness, &verify_fn).await;
|
|
}
|
|
|
|
// TODO(gloas) make this generic over gloas/fulu
|
|
async fn empty_data_column_sidecars_fails_validation_fulu<D>(
|
|
harness: &BeaconChainHarness<EphemeralHarnessType<E>>,
|
|
verify_fn: &impl Fn(DataColumnSidecar<E>) -> Result<D, GossipDataColumnError>,
|
|
) {
|
|
let slot = harness.get_current_slot();
|
|
let state = harness.get_current_state();
|
|
let ((block, _blobs_opt), _state) = harness
|
|
.make_block_with_modifier(state, slot, |block| {
|
|
*block.body_mut().blob_kzg_commitments_mut().unwrap() = vec![].try_into().unwrap();
|
|
})
|
|
.await;
|
|
|
|
let index = 0;
|
|
let column_sidecar: DataColumnSidecar<E> = DataColumnSidecar::Fulu(DataColumnSidecarFulu {
|
|
index,
|
|
column: vec![].try_into().unwrap(),
|
|
kzg_commitments: vec![].try_into().unwrap(),
|
|
kzg_proofs: vec![].try_into().unwrap(),
|
|
signed_block_header: block.signed_block_header(),
|
|
kzg_commitments_inclusion_proof: block
|
|
.message()
|
|
.body()
|
|
.kzg_commitments_merkle_proof()
|
|
.unwrap(),
|
|
});
|
|
|
|
let result = verify_fn(column_sidecar);
|
|
assert!(matches!(
|
|
result.err(),
|
|
Some(GossipDataColumnError::UnexpectedDataColumn)
|
|
));
|
|
}
|
|
|
|
async fn data_column_sidecar_commitments_exceed_max_blobs_per_block<D>(
|
|
harness: &BeaconChainHarness<EphemeralHarnessType<E>>,
|
|
verify_fn: &impl Fn(DataColumnSidecar<E>) -> Result<D, GossipDataColumnError>,
|
|
) {
|
|
let slot = harness.get_current_slot();
|
|
let epoch = slot.epoch(E::slots_per_epoch());
|
|
let state = harness.get_current_state();
|
|
let max_blobs_per_block = harness.spec.max_blobs_per_block(epoch) as usize;
|
|
let fork = harness.spec.fork_name_at_epoch(epoch);
|
|
|
|
// Generate data column sidecar with blob count exceeding max_blobs_per_block.
|
|
let blob_count = max_blobs_per_block + 1;
|
|
let BlobsBundle::<E> {
|
|
commitments: preloaded_commitments_single,
|
|
proofs: _,
|
|
blobs: _,
|
|
} = generate_blobs(1, fork).unwrap().0;
|
|
|
|
let ((block, _blobs_opt), _state) = harness
|
|
.make_block_with_modifier(state, slot, |block| {
|
|
*block.body_mut().blob_kzg_commitments_mut().unwrap() =
|
|
vec![preloaded_commitments_single[0]; blob_count]
|
|
.try_into()
|
|
.unwrap();
|
|
})
|
|
.await;
|
|
|
|
let column_sidecar = generate_data_column_sidecars_from_block(&block, &harness.spec)
|
|
.into_iter()
|
|
.next()
|
|
.unwrap();
|
|
|
|
let result = verify_fn(Arc::try_unwrap(column_sidecar).unwrap());
|
|
assert!(matches!(
|
|
result.err(),
|
|
Some(GossipDataColumnError::MaxBlobsPerBlockExceeded { .. })
|
|
));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_partial_message_verification_fulu() {
|
|
let spec = if fork_name_from_env().is_some() {
|
|
Arc::new(test_spec::<E>())
|
|
} else {
|
|
Arc::new(ForkName::Fulu.make_genesis_spec(E::default_spec()))
|
|
};
|
|
|
|
// Only run these tests if columns are enabled.
|
|
if !spec.is_fulu_scheduled() {
|
|
return;
|
|
}
|
|
// Gloas is not supported yet.
|
|
if spec.is_gloas_scheduled() {
|
|
return;
|
|
}
|
|
|
|
let chain_config = ChainConfig {
|
|
enable_partial_columns: true,
|
|
..Default::default()
|
|
};
|
|
let harness = BeaconChainHarness::builder(E::default())
|
|
.spec(spec)
|
|
.deterministic_keypairs(64)
|
|
.fresh_ephemeral_store()
|
|
.mock_execution_layer()
|
|
.chain_config(chain_config)
|
|
.build();
|
|
|
|
partial_empty_message_without_cells_returns_error(&harness).await;
|
|
partial_inconsistent_present_count_returns_error(&harness).await;
|
|
partial_inconsistent_max_count_returns_error(&harness).await;
|
|
partial_header_with_empty_commitments_fails(&harness).await;
|
|
partial_header_root_mismatch_fails(&harness).await;
|
|
partial_header_with_invalid_inclusion_proof_fails(&harness).await;
|
|
}
|
|
|
|
/// Build a block containing 1 blob and pre-cache the header in the partial assembler.
|
|
async fn add_block_and_header(
|
|
harness: &BeaconChainHarness<EphemeralHarnessType<E>>,
|
|
) -> (types::Hash256, Arc<PartialDataColumnHeader<E>>) {
|
|
harness.advance_slot();
|
|
// Generate a block with 1 blob so we have valid data columns.
|
|
let fork = harness
|
|
.spec
|
|
.fork_name_at_epoch(harness.get_current_slot().epoch(E::slots_per_epoch()));
|
|
let BlobsBundle::<E> {
|
|
commitments,
|
|
proofs: _,
|
|
blobs: _,
|
|
} = generate_blobs(1, fork).unwrap().0;
|
|
|
|
let slot = harness.get_current_slot();
|
|
let state = harness.get_current_state();
|
|
let ((block, _blobs_opt), _state) = harness
|
|
.make_block_with_modifier(state, slot, |block| {
|
|
*block.body_mut().blob_kzg_commitments_mut().unwrap() =
|
|
vec![commitments[0]].try_into().unwrap();
|
|
})
|
|
.await;
|
|
|
|
let block_root = block.canonical_root();
|
|
let header: PartialDataColumnHeader<E> = block.as_ref().try_into().unwrap();
|
|
let header = Arc::new(header);
|
|
|
|
// Pre-cache the header in the partial assembler so headerless partials can be verified.
|
|
harness
|
|
.chain
|
|
.data_availability_checker
|
|
.partial_assembler()
|
|
.unwrap()
|
|
.init(block_root, header.clone());
|
|
|
|
(block_root, header)
|
|
}
|
|
|
|
async fn partial_empty_message_without_cells_returns_error(
|
|
harness: &BeaconChainHarness<EphemeralHarnessType<E>>,
|
|
) {
|
|
let (block_root, header) = add_block_and_header(harness).await;
|
|
|
|
// Create a headerless partial with no cells — should trigger EmptyMessage.
|
|
let num_commitments = header.kzg_commitments.len();
|
|
let empty_bitmap =
|
|
BitList::<<E as EthSpec>::MaxBlobCommitmentsPerBlock>::with_capacity(num_commitments)
|
|
.unwrap();
|
|
|
|
let column = PartialDataColumn {
|
|
block_root,
|
|
index: 0,
|
|
sidecar: PartialDataColumnSidecar {
|
|
cells_present_bitmap: empty_bitmap,
|
|
column: vec![].try_into().unwrap(),
|
|
kzg_proofs: vec![].try_into().unwrap(),
|
|
header: None.into(),
|
|
},
|
|
};
|
|
|
|
let result = validate_partial_data_column_sidecar_for_gossip(
|
|
Box::new(column),
|
|
&harness.chain,
|
|
UNIX_EPOCH.elapsed().unwrap(),
|
|
);
|
|
assert!(
|
|
matches!(
|
|
result,
|
|
PartialColumnVerificationResult::ErrWithValidHeader {
|
|
err: GossipPartialDataColumnError::EmptyMessage,
|
|
..
|
|
}
|
|
),
|
|
"Expected EmptyMessage"
|
|
);
|
|
}
|
|
|
|
async fn partial_inconsistent_present_count_returns_error(
|
|
harness: &BeaconChainHarness<EphemeralHarnessType<E>>,
|
|
) {
|
|
let (block_root, header) = add_block_and_header(harness).await;
|
|
|
|
// Create a bitmap that says 2 bits are set, but only provide 1 cell/proof.
|
|
let num_commitments = header.kzg_commitments.len();
|
|
let mut bitmap =
|
|
BitList::<<E as EthSpec>::MaxBlobCommitmentsPerBlock>::with_capacity(num_commitments)
|
|
.unwrap();
|
|
bitmap.set(0, true).unwrap();
|
|
|
|
let column = PartialDataColumn {
|
|
block_root,
|
|
index: 0,
|
|
sidecar: PartialDataColumnSidecar {
|
|
cells_present_bitmap: bitmap,
|
|
column: vec![types::Cell::<E>::default()].try_into().unwrap(),
|
|
// Provide 2 proofs but only 1 cell ← mismatch with popcount=1
|
|
kzg_proofs: vec![types::KzgProof::empty(), types::KzgProof::empty()]
|
|
.try_into()
|
|
.unwrap(),
|
|
header: None.into(),
|
|
},
|
|
};
|
|
|
|
let result = validate_partial_data_column_sidecar_for_gossip(
|
|
Box::new(column),
|
|
&harness.chain,
|
|
UNIX_EPOCH.elapsed().unwrap(),
|
|
);
|
|
assert!(
|
|
matches!(
|
|
result,
|
|
PartialColumnVerificationResult::ErrWithValidHeader {
|
|
err: GossipPartialDataColumnError::InconsistentPresentCount { .. },
|
|
..
|
|
}
|
|
),
|
|
"Expected InconsistentPresentCount"
|
|
);
|
|
}
|
|
|
|
async fn partial_inconsistent_max_count_returns_error(
|
|
harness: &BeaconChainHarness<EphemeralHarnessType<E>>,
|
|
) {
|
|
let (block_root, _header) = add_block_and_header(harness).await;
|
|
|
|
// Create a bitmap with length different from the number of commitments in the header.
|
|
// Header has 1 commitment, but we use a bitmap with capacity 3.
|
|
let mut bitmap =
|
|
BitList::<<E as EthSpec>::MaxBlobCommitmentsPerBlock>::with_capacity(3).unwrap();
|
|
bitmap.set(0, true).unwrap();
|
|
|
|
let column = PartialDataColumn {
|
|
block_root,
|
|
index: 0,
|
|
sidecar: PartialDataColumnSidecar {
|
|
cells_present_bitmap: bitmap,
|
|
column: vec![types::Cell::<E>::default()].try_into().unwrap(),
|
|
kzg_proofs: vec![types::KzgProof::empty()].try_into().unwrap(),
|
|
header: None.into(),
|
|
},
|
|
};
|
|
|
|
let result = validate_partial_data_column_sidecar_for_gossip(
|
|
Box::new(column),
|
|
&harness.chain,
|
|
UNIX_EPOCH.elapsed().unwrap(),
|
|
);
|
|
assert!(
|
|
matches!(
|
|
result,
|
|
PartialColumnVerificationResult::ErrWithValidHeader {
|
|
err: GossipPartialDataColumnError::InconsistentCommitmentsLength { .. },
|
|
..
|
|
}
|
|
),
|
|
"Expected InconsistentMaxCount"
|
|
);
|
|
}
|
|
|
|
async fn partial_header_with_empty_commitments_fails(
|
|
harness: &BeaconChainHarness<EphemeralHarnessType<E>>,
|
|
) {
|
|
let slot = harness.get_current_slot();
|
|
let state = harness.get_current_state();
|
|
let ((block, _), _) = harness
|
|
.make_block_with_modifier(state, slot, |block| {
|
|
*block.body_mut().blob_kzg_commitments_mut().unwrap() = vec![].try_into().unwrap();
|
|
})
|
|
.await;
|
|
|
|
let block_root = block.canonical_root();
|
|
let header: PartialDataColumnHeader<E> = block.as_ref().try_into().unwrap();
|
|
assert!(header.kzg_commitments.is_empty());
|
|
|
|
let result =
|
|
GossipVerifiedPartialDataColumnHeader::new(block_root, header, &*harness.chain);
|
|
assert!(
|
|
matches!(
|
|
result,
|
|
Err(GossipPartialDataColumnError::GossipDataColumnError(
|
|
GossipDataColumnError::UnexpectedDataColumn
|
|
))
|
|
),
|
|
"Expected UnexpectedDataColumn, got: {result:?}"
|
|
);
|
|
}
|
|
|
|
async fn partial_header_root_mismatch_fails(
|
|
harness: &BeaconChainHarness<EphemeralHarnessType<E>>,
|
|
) {
|
|
let (_block_root, header) = add_block_and_header(harness).await;
|
|
|
|
// Use a wrong group_id (not matching the header's block root)
|
|
let wrong_root = types::Hash256::repeat_byte(0xff);
|
|
let header = PartialDataColumnHeader::clone(&header);
|
|
|
|
let result =
|
|
GossipVerifiedPartialDataColumnHeader::new(wrong_root, header, &*harness.chain);
|
|
assert!(
|
|
matches!(
|
|
result,
|
|
Err(GossipPartialDataColumnError::HeaderIncorrectRoot { .. })
|
|
),
|
|
"Expected HeaderIncorrectRoot, got: {result:?}"
|
|
);
|
|
}
|
|
|
|
async fn partial_header_with_invalid_inclusion_proof_fails(
|
|
harness: &BeaconChainHarness<EphemeralHarnessType<E>>,
|
|
) {
|
|
let (block_root, header) = add_block_and_header(harness).await;
|
|
|
|
// Corrupt the inclusion proof
|
|
let mut header = PartialDataColumnHeader::clone(&header);
|
|
header.kzg_commitments_inclusion_proof[0] = types::Hash256::repeat_byte(0xaa);
|
|
|
|
let result =
|
|
GossipVerifiedPartialDataColumnHeader::new(block_root, header, &*harness.chain);
|
|
assert!(
|
|
matches!(
|
|
result,
|
|
Err(GossipPartialDataColumnError::GossipDataColumnError(
|
|
GossipDataColumnError::InvalidInclusionProof
|
|
))
|
|
),
|
|
"Expected InvalidInclusionProof, got: {result:?}"
|
|
);
|
|
}
|
|
|
|
// -- merge tests --
|
|
|
|
fn make_cell(marker: u8) -> Cell<E> {
|
|
let mut cell = Cell::<E>::default();
|
|
cell[0] = marker;
|
|
cell
|
|
}
|
|
|
|
fn make_partial_with_marker(
|
|
total_blobs: usize,
|
|
present_indices: &[usize],
|
|
marker_base: u8,
|
|
) -> KzgVerifiedCustodyPartialDataColumn<E> {
|
|
let mut bitmap = CellBitmap::<E>::with_capacity(total_blobs).unwrap();
|
|
for &idx in present_indices {
|
|
bitmap.set(idx, true).unwrap();
|
|
}
|
|
|
|
let column: VariableList<_, _> = present_indices
|
|
.iter()
|
|
.map(|&idx| make_cell(marker_base.wrapping_add(idx as u8)))
|
|
.collect::<Vec<_>>()
|
|
.try_into()
|
|
.unwrap();
|
|
let proofs: VariableList<_, _> = present_indices
|
|
.iter()
|
|
.map(|_| KzgProof::empty())
|
|
.collect::<Vec<_>>()
|
|
.try_into()
|
|
.unwrap();
|
|
|
|
KzgVerifiedCustodyPartialDataColumn {
|
|
data: Arc::new(PartialDataColumn {
|
|
block_root: Default::default(),
|
|
index: 0,
|
|
sidecar: PartialDataColumnSidecar {
|
|
cells_present_bitmap: bitmap,
|
|
column,
|
|
kzg_proofs: proofs,
|
|
header: None.into(),
|
|
},
|
|
}),
|
|
latest_cell_timestamp: Default::default(),
|
|
}
|
|
}
|
|
|
|
fn make_partial(
|
|
total_blobs: usize,
|
|
present_indices: &[usize],
|
|
) -> KzgVerifiedCustodyPartialDataColumn<E> {
|
|
make_partial_with_marker(total_blobs, present_indices, 0)
|
|
}
|
|
|
|
#[test]
|
|
fn merge_disjoint_partials() {
|
|
let a = make_partial(6, &[0, 2]);
|
|
let b = make_partial(6, &[1, 3]);
|
|
let merged = a.merge(&b).unwrap();
|
|
assert_eq!(merged.data.sidecar.column.len(), 4);
|
|
assert_eq!(merged.data.sidecar.kzg_proofs.len(), 4);
|
|
for i in 0..4 {
|
|
assert!(merged.data.sidecar.cells_present_bitmap.get(i).unwrap());
|
|
}
|
|
assert!(!merged.data.sidecar.cells_present_bitmap.get(4).unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn merge_overlapping_partials_prefers_self() {
|
|
let a = make_partial_with_marker(4, &[0, 1], 0);
|
|
let b = make_partial_with_marker(4, &[1, 2], 100);
|
|
let merged = a.merge(&b).unwrap();
|
|
assert_eq!(merged.data.sidecar.column.len(), 3);
|
|
// Cell at bitmap index 1 is the second cell in the merged column.
|
|
// It should come from `a` (marker_base=0, so marker=0+1=1), not `b` (marker=100+1=101).
|
|
assert_eq!(merged.data.sidecar.column[1][0], 1);
|
|
}
|
|
|
|
#[test]
|
|
fn merge_with_empty_other() {
|
|
let a = make_partial(4, &[0, 2]);
|
|
let b = make_partial(4, &[]);
|
|
let merged = a.merge(&b).unwrap();
|
|
assert_eq!(merged.data.sidecar.column.len(), 2);
|
|
assert_eq!(
|
|
merged.data.sidecar.cells_present_bitmap,
|
|
a.data.sidecar.cells_present_bitmap
|
|
);
|
|
}
|
|
}
|