diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index 84005183f2..602851add0 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -12,6 +12,7 @@ pub use self::verify_attester_slashing::{ pub use self::verify_proposer_slashing::verify_proposer_slashing; pub use altair::sync_committee::process_sync_aggregate; pub use block_signature_verifier::{BlockSignatureVerifier, ParallelSignatureSets}; +pub use eip4844::eip4844::process_blob_kzg_commitments; pub use is_valid_indexed_attestation::is_valid_indexed_attestation; pub use process_operations::process_operations; pub use verify_attestation::{ @@ -24,6 +25,7 @@ pub use verify_exit::verify_exit; pub mod altair; pub mod block_signature_verifier; +pub mod eip4844; pub mod errors; mod is_valid_indexed_attestation; pub mod process_operations; @@ -171,6 +173,8 @@ pub fn per_block_processing>( )?; } + process_blob_kzg_commitments(block.body())?; + Ok(()) } diff --git a/consensus/state_processing/src/per_block_processing/eip4844.rs b/consensus/state_processing/src/per_block_processing/eip4844.rs new file mode 100644 index 0000000000..120ba304d0 --- /dev/null +++ b/consensus/state_processing/src/per_block_processing/eip4844.rs @@ -0,0 +1 @@ +pub mod eip4844; diff --git a/consensus/state_processing/src/per_block_processing/eip4844/eip4844.rs b/consensus/state_processing/src/per_block_processing/eip4844/eip4844.rs new file mode 100644 index 0000000000..56b3ed58a6 --- /dev/null +++ b/consensus/state_processing/src/per_block_processing/eip4844/eip4844.rs @@ -0,0 +1,122 @@ +use crate::BlockProcessingError; +use eth2_hashing::hash_fixed; +use itertools::{EitherOrBoth, Itertools}; +use safe_arith::SafeArith; +use ssz::Decode; +use ssz_types::VariableList; +use types::consts::eip4844::{BLOB_TX_TYPE, VERSIONED_HASH_VERSION_KZG}; +use types::{ + AbstractExecPayload, BeaconBlockBodyRef, EthSpec, ExecPayload, FullPayload, FullPayloadRef, + KzgCommitment, Transaction, Transactions, VersionedHash, +}; + +pub fn process_blob_kzg_commitments>( + block_body: BeaconBlockBodyRef, +) -> Result<(), BlockProcessingError> { + if let (Ok(payload), Ok(kzg_commitments)) = ( + block_body.execution_payload(), + block_body.blob_kzg_commitments(), + ) { + if let Some(transactions) = payload.transactions() { + if !verify_kzg_commitments_against_transactions::(transactions, kzg_commitments)? { + return Err(BlockProcessingError::BlobVersionHashMismatch); + } + } + } + + Ok(()) +} + +pub fn verify_kzg_commitments_against_transactions( + transactions: &Transactions, + kzg_commitments: &VariableList, +) -> Result { + let nested_iter = transactions + .into_iter() + .filter(|tx| { + tx.get(0) + .map(|tx_type| *tx_type == BLOB_TX_TYPE) + .unwrap_or(false) + }) + .map(|tx| tx_peek_blob_versioned_hashes::(tx)); + + itertools::process_results(nested_iter, |iter| { + let zipped_iter = iter + .flatten() + // Need to use `itertools::zip_longest` here because just zipping hides if one iter is shorter + // and `itertools::zip_eq` panics. + .zip_longest(kzg_commitments.into_iter()) + .enumerate() + .map(|(index, next)| match next { + EitherOrBoth::Both(hash, commitment) => Ok((hash?, commitment)), + // The number of versioned hashes from the blob transactions exceeds the number of + // commitments in the block. + EitherOrBoth::Left(_) => Err(BlockProcessingError::BlobNumCommitmentsMismatch { + commitments_processed_in_block: index, + commitments_processed_in_transactions: index.safe_add(1)?, + }), + // The number of commitments in the block exceeds the number of versioned hashes + // in the blob transactions. + EitherOrBoth::Right(_) => Err(BlockProcessingError::BlobNumCommitmentsMismatch { + commitments_processed_in_block: index.safe_add(1)?, + commitments_processed_in_transactions: index, + }), + }); + + itertools::process_results(zipped_iter, |mut iter| { + iter.all(|(tx_versioned_hash, commitment)| { + tx_versioned_hash == kzg_commitment_to_versioned_hash(commitment) + }) + }) + })? +} + +/// Only transactions of type `BLOB_TX_TYPE` should be passed into this function. +fn tx_peek_blob_versioned_hashes( + opaque_tx: &Transaction, +) -> Result< + impl IntoIterator> + '_, + BlockProcessingError, +> { + let tx_len = opaque_tx.len(); + let message_offset = 1.safe_add(u32::from_ssz_bytes(opaque_tx.get(1..5).ok_or( + BlockProcessingError::BlobVersionHashIndexOutOfBounds { + length: tx_len, + index: 5, + }, + )?)?)?; + + let message_offset_usize = message_offset as usize; + + // field offset: 32 + 8 + 32 + 32 + 8 + 4 + 32 + 4 + 4 + 32 = 188 + let blob_versioned_hashes_offset = message_offset.safe_add(u32::from_ssz_bytes( + opaque_tx + .get(message_offset_usize.safe_add(188)?..message_offset_usize.safe_add(192)?) + .ok_or(BlockProcessingError::BlobVersionHashIndexOutOfBounds { + length: tx_len, + index: message_offset_usize.safe_add(192)?, + })?, + )?)?; + + let num_hashes = tx_len + .safe_sub(blob_versioned_hashes_offset as usize)? + .safe_div(32)?; + + Ok((0..num_hashes).into_iter().map(move |i| { + let next_version_hash_index = + (blob_versioned_hashes_offset as usize).safe_add(i.safe_mul(32)?)?; + let bytes = opaque_tx + .get(next_version_hash_index..next_version_hash_index.safe_add(32)?) + .ok_or(BlockProcessingError::BlobVersionHashIndexOutOfBounds { + length: tx_len, + index: (next_version_hash_index as usize).safe_add(32)?, + })?; + Ok(VersionedHash::from_slice(bytes)) + })) +} + +fn kzg_commitment_to_versioned_hash(kzg_commitment: &KzgCommitment) -> VersionedHash { + let mut hashed_commitment = hash_fixed(&kzg_commitment.0); + hashed_commitment[0] = VERSIONED_HASH_VERSION_KZG; + VersionedHash::from(hashed_commitment) +} diff --git a/consensus/state_processing/src/per_block_processing/errors.rs b/consensus/state_processing/src/per_block_processing/errors.rs index e214b6e63d..bb006dc25c 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -1,6 +1,7 @@ use super::signature_sets::Error as SignatureSetError; use merkle_proof::MerkleTreeError; use safe_arith::ArithError; +use ssz::DecodeError; use types::*; /// The error returned from the `per_block_processing` function. Indicates that a block is either @@ -53,6 +54,7 @@ pub enum BlockProcessingError { BeaconStateError(BeaconStateError), SignatureSetError(SignatureSetError), SszTypesError(ssz_types::Error), + SszDecodeError(DecodeError), MerkleTreeError(MerkleTreeError), ArithError(ArithError), InconsistentBlockFork(InconsistentFork), @@ -70,6 +72,18 @@ pub enum BlockProcessingError { found: u64, }, ExecutionInvalid, + BlobVersionHashMismatch, + /// The number of commitments in blob transactions in the payload does not match the number + /// of commitments in the block. + BlobNumCommitmentsMismatch { + commitments_processed_in_block: usize, + /// This number depic + commitments_processed_in_transactions: usize, + }, + BlobVersionHashIndexOutOfBounds { + index: usize, + length: usize, + }, } impl From for BlockProcessingError { @@ -90,6 +104,12 @@ impl From for BlockProcessingError { } } +impl From for BlockProcessingError { + fn from(error: DecodeError) -> Self { + BlockProcessingError::SszDecodeError(error) + } +} + impl From for BlockProcessingError { fn from(e: ArithError) -> Self { BlockProcessingError::ArithError(e) diff --git a/consensus/types/src/consts.rs b/consensus/types/src/consts.rs index 2469f5f9cf..b13e3aa9c3 100644 --- a/consensus/types/src/consts.rs +++ b/consensus/types/src/consts.rs @@ -34,4 +34,5 @@ pub mod eip4844 { .expect("should initialize BLS_MODULUS"); } pub const BLOB_TX_TYPE: u8 = 5; + pub const VERSIONED_HASH_VERSION_KZG: u8 = 1; } diff --git a/consensus/types/src/kzg_commitment.rs b/consensus/types/src/kzg_commitment.rs index 3b9570cd26..64ed24d9ac 100644 --- a/consensus/types/src/kzg_commitment.rs +++ b/consensus/types/src/kzg_commitment.rs @@ -9,7 +9,7 @@ use tree_hash::{PackedEncoding, TreeHash}; #[derive(Derivative, Debug, Clone, Serialize, Deserialize)] #[derivative(PartialEq, Eq, Hash)] -pub struct KzgCommitment(#[serde(with = "BigArray")] [u8; 48]); +pub struct KzgCommitment(#[serde(with = "BigArray")] pub [u8; 48]); impl Display for KzgCommitment { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { diff --git a/consensus/types/src/kzg_proof.rs b/consensus/types/src/kzg_proof.rs index 879620bd6f..92a994a85c 100644 --- a/consensus/types/src/kzg_proof.rs +++ b/consensus/types/src/kzg_proof.rs @@ -1,9 +1,9 @@ use crate::test_utils::{RngCore, TestRandom}; use serde::{Deserialize, Serialize}; +use serde_big_array::BigArray; use ssz::{Decode, DecodeError, Encode}; use std::fmt; use tree_hash::{PackedEncoding, TreeHash}; -use serde_big_array::BigArray; const KZG_PROOF_BYTES_LEN: usize = 48; diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index cc839001dd..506bb0f26d 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -193,6 +193,7 @@ pub type Address = H160; pub type ForkVersion = [u8; 4]; pub type BLSFieldElement = Uint256; pub type Blob = FixedVector::FieldElementsPerBlob>; +pub type VersionedHash = Hash256; pub use bls::{ AggregatePublicKey, AggregateSignature, Keypair, PublicKey, PublicKeyBytes, SecretKey, diff --git a/consensus/types/src/payload.rs b/consensus/types/src/payload.rs index 736f06e22b..ea483efaf6 100644 --- a/consensus/types/src/payload.rs +++ b/consensus/types/src/payload.rs @@ -35,6 +35,9 @@ pub trait ExecPayload: Debug + Clone + PartialEq + Hash + TreeHash + fn fee_recipient(&self) -> Address; fn gas_limit(&self) -> u64; + /// This will return `None` on blinded blocks or pre-merge blocks. + fn transactions(&self) -> Option<&Transactions>; + // Is this a default payload? (pre-merge) fn is_default(&self) -> bool; } @@ -191,6 +194,10 @@ impl ExecPayload for FullPayloadMerge { self.execution_payload.gas_limit } + fn transactions(&self) -> Option<&Transactions> { + Some(&self.execution_payload.transactions) + } + // TODO: can this function be optimized? fn is_default(&self) -> bool { self.execution_payload == ExecutionPayloadMerge::default() @@ -235,6 +242,10 @@ impl ExecPayload for FullPayloadCapella { self.execution_payload.gas_limit } + fn transactions(&self) -> Option<&Transactions> { + Some(&self.execution_payload.transactions) + } + // TODO: can this function be optimized? fn is_default(&self) -> bool { self.execution_payload == ExecutionPayloadCapella::default() @@ -279,6 +290,10 @@ impl ExecPayload for FullPayloadEip4844 { self.execution_payload.gas_limit } + fn transactions(&self) -> Option<&Transactions> { + Some(&self.execution_payload.transactions) + } + // TODO: can this function be optimized? fn is_default(&self) -> bool { self.execution_payload == ExecutionPayloadEip4844::default() @@ -347,6 +362,13 @@ impl ExecPayload for FullPayload { }) } + fn transactions<'a>(&'a self) -> Option<&'a Transactions> { + map_full_payload_ref!(&'a _, self.to_ref(), move |payload, cons| { + cons(payload); + Some(&payload.execution_payload.transactions) + }) + } + fn is_default(&self) -> bool { match self { Self::Merge(payload) => payload.is_default(), @@ -428,6 +450,13 @@ impl<'b, T: EthSpec> ExecPayload for FullPayloadRef<'b, T> { }) } + fn transactions<'a>(&'a self) -> Option<&'a Transactions> { + map_full_payload_ref!(&'a _, self, move |payload, cons| { + cons(payload); + Some(&payload.execution_payload.transactions) + }) + } + // TODO: can this function be optimized? fn is_default<'a>(&'a self) -> bool { match self { @@ -687,6 +716,10 @@ impl ExecPayload for BlindedPayload { } } + fn transactions(&self) -> Option<&Transactions> { + None + } + // TODO: can this function be optimized? fn is_default(&self) -> bool { match self { @@ -773,6 +806,10 @@ impl<'b, T: EthSpec> ExecPayload for BlindedPayloadRef<'b, T> { } } + fn transactions(&self) -> Option<&Transactions> { + None + } + // TODO: can this function be optimized? fn is_default<'a>(&'a self) -> bool { match self { @@ -828,6 +865,10 @@ impl ExecPayload for BlindedPayloadMerge { self.execution_payload_header.gas_limit } + fn transactions(&self) -> Option<&Transactions> { + None + } + fn is_default(&self) -> bool { self.execution_payload_header == ExecutionPayloadHeaderMerge::default() } @@ -871,6 +912,10 @@ impl ExecPayload for BlindedPayloadCapella { self.execution_payload_header.gas_limit } + fn transactions(&self) -> Option<&Transactions> { + None + } + fn is_default(&self) -> bool { self.execution_payload_header == ExecutionPayloadHeaderCapella::default() } @@ -914,6 +959,10 @@ impl ExecPayload for BlindedPayloadEip4844 { self.execution_payload_header.gas_limit } + fn transactions(&self) -> Option<&Transactions> { + None + } + fn is_default(&self) -> bool { self.execution_payload_header == ExecutionPayloadHeaderEip4844::default() }