diff --git a/crates/consensus/Cargo.toml b/crates/consensus/Cargo.toml index 3924d5dbd39..44a49e32002 100644 --- a/crates/consensus/Cargo.toml +++ b/crates/consensus/Cargo.toml @@ -17,8 +17,6 @@ alloy-rlp.workspace = true alloy-eips.workspace = true alloy-serde = { workspace = true, optional = true } -sha2 = { version = "0.10", default-features = false } - # kzg thiserror = { workspace = true, optional = true } c-kzg = { workspace = true, features = ["serde"], optional = true } @@ -30,7 +28,9 @@ arbitrary = { workspace = true, features = ["derive"], optional = true } serde = { workspace = true, features = ["derive"], optional = true } [dev-dependencies] +alloy-primitives = { workspace = true, features = ["arbitrary", "rand"] } alloy-signer.workspace = true + arbitrary = { workspace = true, features = ["derive"] } k256.workspace = true tokio = { workspace = true, features = ["macros"] } @@ -38,7 +38,7 @@ serde_json.workspace = true [features] default = ["std"] -std = ["alloy-eips/std", "sha2/std", "c-kzg?/std"] +std = ["alloy-eips/std", "c-kzg?/std"] k256 = ["alloy-primitives/k256"] kzg = ["dep:c-kzg", "dep:thiserror", "alloy-eips/kzg", "std"] arbitrary = ["std", "dep:arbitrary", "alloy-eips/arbitrary"] diff --git a/crates/consensus/src/transaction/eip4844.rs b/crates/consensus/src/transaction/eip4844.rs index 0c5a0c22dfb..1867290c5c7 100644 --- a/crates/consensus/src/transaction/eip4844.rs +++ b/crates/consensus/src/transaction/eip4844.rs @@ -5,21 +5,17 @@ pub mod utils; use crate::{SignableTransaction, Signed, Transaction, TxType}; -use alloy_eips::{ - eip2930::AccessList, - eip4844::{BYTES_PER_BLOB, BYTES_PER_COMMITMENT, BYTES_PER_PROOF, DATA_GAS_PER_BLOB}, -}; -use alloy_primitives::{ - keccak256, Address, Bytes, ChainId, FixedBytes, Signature, TxKind, B256, U256, -}; +use alloy_eips::{eip2930::AccessList, eip4844::DATA_GAS_PER_BLOB}; +use alloy_primitives::{keccak256, Address, Bytes, ChainId, Signature, TxKind, B256, U256}; use alloy_rlp::{length_of_length, BufMut, Decodable, Encodable, Header}; use core::mem; -use sha2::{Digest, Sha256}; + +#[doc(inline)] +pub use alloy_eips::eip4844::BlobTransactionSidecar; #[cfg(not(feature = "std"))] use alloc::vec::Vec; -#[cfg(not(feature = "kzg"))] pub use alloy_eips::eip4844::{Blob, Bytes48}; #[cfg(feature = "kzg")] @@ -27,9 +23,6 @@ use c_kzg::{KzgCommitment, KzgProof, KzgSettings}; #[cfg(feature = "kzg")] use std::ops::Deref; -#[cfg(feature = "kzg")] -pub use c_kzg::{Blob, Bytes48}; - /// An error that can occur when validating a [TxEip4844Variant]. #[derive(Debug, thiserror::Error)] #[cfg(feature = "kzg")] @@ -462,7 +455,8 @@ impl TxEip4844 { let commitment = KzgCommitment::from(*commitment.deref()); // calculate & verify versioned hash - let calculated_versioned_hash = kzg_to_versioned_hash(commitment.as_slice()); + let calculated_versioned_hash = + alloy_eips::eip4844::kzg_to_versioned_hash(commitment.as_slice()); if *versioned_hash != calculated_versioned_hash { return Err(BlobTransactionValidationError::WrongVersionedHash { have: *versioned_hash, @@ -471,12 +465,20 @@ impl TxEip4844 { } } - let res = KzgProof::verify_blob_kzg_proof_batch( - sidecar.blobs.as_slice(), - sidecar.commitments.as_slice(), - sidecar.proofs.as_slice(), - proof_settings, - ) + // SAFETY: ALL types have the same size + let res = unsafe { + KzgProof::verify_blob_kzg_proof_batch( + // blobs + std::mem::transmute::<&[Blob], &[c_kzg::Blob]>(sidecar.blobs.as_slice()), + // commitments + std::mem::transmute::<&[Bytes48], &[c_kzg::Bytes48]>( + sidecar.commitments.as_slice(), + ), + // proofs + std::mem::transmute::<&[Bytes48], &[c_kzg::Bytes48]>(sidecar.proofs.as_slice()), + proof_settings, + ) + } .map_err(BlobTransactionValidationError::KZGError)?; if res { @@ -859,7 +861,7 @@ impl TxEip4844WithSidecar { // now write the fields self.tx.encode_fields(out); signature.write_rlp_vrs(out); - self.sidecar.encode_inner(out); + self.sidecar.encode(out); } /// Decodes the transaction from RLP bytes, including the signature. @@ -884,7 +886,7 @@ impl TxEip4844WithSidecar { let inner_tx = TxEip4844::decode_signed_fields(buf)?; // decode the sidecar - let sidecar = BlobTransactionSidecar::decode_inner(buf)?; + let sidecar = BlobTransactionSidecar::decode(buf)?; if buf.len() + header.payload_length != original_len { return Err(alloy_rlp::Error::ListLengthMismatch { @@ -972,155 +974,17 @@ impl Transaction for TxEip4844WithSidecar { } } -/// This represents a set of blobs, and its corresponding commitments and proofs. -#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] -#[repr(C)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct BlobTransactionSidecar { - /// The blob data. - pub blobs: Vec, - /// The blob commitments. - pub commitments: Vec, - /// The blob proofs. - pub proofs: Vec, -} - -impl BlobTransactionSidecar { - /// Constructs a new [BlobTransactionSidecar] from a set of blobs, commitments, and proofs. - pub fn new(blobs: Vec, commitments: Vec, proofs: Vec) -> Self { - Self { blobs, commitments, proofs } - } - - /// Returns an iterator over the versioned hashes of the commitments. - pub fn versioned_hashes(&self) -> impl Iterator + '_ { - self.commitments.iter().map(|c| kzg_to_versioned_hash(c.as_slice())) - } - - /// Returns the versioned hash for the blob at the given index, if it - /// exists. - pub fn versioned_hash_for_blob(&self, blob_index: usize) -> Option { - self.commitments.get(blob_index).map(|c| kzg_to_versioned_hash(c.as_slice())) - } - - /// Encodes the inner [BlobTransactionSidecar] fields as RLP bytes, without a RLP header. - /// - /// This encodes the fields in the following order: - /// - `blobs` - /// - `commitments` - /// - `proofs` - #[inline] - pub(crate) fn encode_inner(&self, out: &mut dyn BufMut) { - BlobTransactionSidecarRlp::wrap_ref(self).encode(out); - } - - /// Decodes the inner [BlobTransactionSidecar] fields from RLP bytes, without a RLP header. - /// - /// This decodes the fields in the following order: - /// - `blobs` - /// - `commitments` - /// - `proofs` - pub(crate) fn decode_inner(buf: &mut &[u8]) -> alloy_rlp::Result { - Ok(BlobTransactionSidecarRlp::decode(buf)?.unwrap()) - } - - /// Outputs the RLP length of the [BlobTransactionSidecar] fields, without a RLP header. - pub fn fields_len(&self) -> usize { - BlobTransactionSidecarRlp::wrap_ref(self).fields_len() - } - - /// Calculates a size heuristic for the in-memory size of the [BlobTransactionSidecar]. - #[inline] - pub fn size(&self) -> usize { - self.blobs.len() * BYTES_PER_BLOB + // blobs - self.commitments.len() * BYTES_PER_COMMITMENT + // commitments - self.proofs.len() * BYTES_PER_PROOF // proofs - } -} - -impl Encodable for BlobTransactionSidecar { - /// Encodes the inner [BlobTransactionSidecar] fields as RLP bytes, without a RLP header. - fn encode(&self, s: &mut dyn BufMut) { - self.encode_inner(s); - } - - fn length(&self) -> usize { - self.fields_len() - } -} - -impl Decodable for BlobTransactionSidecar { - /// Decodes the inner [BlobTransactionSidecar] fields from RLP bytes, without a RLP header. - fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { - Self::decode_inner(buf) - } -} - -// Wrapper for c-kzg rlp -#[repr(C)] -struct BlobTransactionSidecarRlp { - blobs: Vec>, - commitments: Vec>, - proofs: Vec>, -} - -const _: [(); mem::size_of::()] = - [(); mem::size_of::()]; - -impl BlobTransactionSidecarRlp { - const fn wrap_ref(other: &BlobTransactionSidecar) -> &Self { - // SAFETY: Same repr and size - unsafe { &*(other as *const BlobTransactionSidecar).cast::() } - } - - fn unwrap(self) -> BlobTransactionSidecar { - // SAFETY: Same repr and size - unsafe { mem::transmute(self) } - } - - fn encode(&self, out: &mut dyn BufMut) { - // Encode the blobs, commitments, and proofs - self.blobs.encode(out); - self.commitments.encode(out); - self.proofs.encode(out); - } - - fn fields_len(&self) -> usize { - self.blobs.length() + self.commitments.length() + self.proofs.length() - } - - fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { - Ok(Self { - blobs: Decodable::decode(buf)?, - commitments: Decodable::decode(buf)?, - proofs: Decodable::decode(buf)?, - }) - } -} - -/// Calculates the versioned hash for a KzgCommitment -/// -/// Specified in [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844#header-extension) -pub(crate) fn kzg_to_versioned_hash(commitment: &[u8]) -> B256 { - debug_assert_eq!(commitment.len(), 48, "commitment length is not 48"); - let mut res = Sha256::digest(commitment); - res[0] = alloy_eips::eip4844::VERSIONED_HASH_VERSION_KZG; - B256::new(res.into()) -} - #[cfg(test)] mod tests { use super::{BlobTransactionSidecar, TxEip4844, TxEip4844WithSidecar}; use crate::{SignableTransaction, TxEip4844Variant, TxEnvelope}; - use alloy_eips::eip2930::AccessList; + use alloy_eips::{ + eip2930::AccessList, + eip4844::{Blob, Bytes48}, + }; use alloy_primitives::{address, b256, bytes, Signature, U256}; use alloy_rlp::{Decodable, Encodable}; - #[cfg(not(feature = "kzg"))] - use alloy_eips::eip4844::{Blob, Bytes48}; - - #[cfg(feature = "kzg")] - use c_kzg::{Blob, Bytes48}; - #[test] fn different_sidecar_same_hash() { // this should make sure that the hash calculated for the `into_signed` conversion does not diff --git a/crates/consensus/src/transaction/eip4844/builder.rs b/crates/consensus/src/transaction/eip4844/builder.rs index 51df9a79c04..bb5967d9f3b 100644 --- a/crates/consensus/src/transaction/eip4844/builder.rs +++ b/crates/consensus/src/transaction/eip4844/builder.rs @@ -1,7 +1,6 @@ -#[cfg(not(feature = "kzg"))] use alloy_eips::eip4844::Blob; #[cfg(feature = "kzg")] -use c_kzg::{Blob, KzgCommitment, KzgProof}; +use c_kzg::{KzgCommitment, KzgProof}; #[cfg(not(feature = "std"))] use alloc::vec::Vec; @@ -351,13 +350,25 @@ impl SidecarBuilder { let mut commitments = Vec::with_capacity(self.inner.blobs.len()); let mut proofs = Vec::with_capacity(self.inner.blobs.len()); for blob in self.inner.blobs.iter() { + // SAFETY: same size + let blob = unsafe { std::mem::transmute::<&Blob, &c_kzg::Blob>(blob) }; let commitment = KzgCommitment::blob_to_kzg_commitment(blob, settings)?; let proof = KzgProof::compute_blob_kzg_proof(blob, &commitment.to_bytes(), settings)?; - commitments.push(commitment.to_bytes()); - proofs.push(proof.to_bytes()); + + // SAFETY: same size + unsafe { + commitments.push( + std::mem::transmute::( + commitment.to_bytes(), + ), + ); + proofs.push(std::mem::transmute::( + proof.to_bytes(), + )); + } } - Ok(crate::BlobTransactionSidecar { blobs: self.inner.blobs, commitments, proofs }) + Ok(crate::BlobTransactionSidecar::new(self.inner.blobs, commitments, proofs)) } /// Build the sidecar from the data, with default (Ethereum Mainnet) diff --git a/crates/consensus/src/transaction/envelope.rs b/crates/consensus/src/transaction/envelope.rs index 6a31011ec97..b70e09d588f 100644 --- a/crates/consensus/src/transaction/envelope.rs +++ b/crates/consensus/src/transaction/envelope.rs @@ -305,7 +305,7 @@ mod tests { use super::*; use crate::transaction::SignableTransaction; use alloy_eips::eip2930::{AccessList, AccessListItem}; - use alloy_primitives::{hex, Address, Bytes, Signature, TxKind, B256, U256}; + use alloy_primitives::{hex, Address, Bytes, Signature, TxKind, U256}; use std::{fs, path::PathBuf, vec}; #[cfg(not(feature = "std"))] diff --git a/crates/eips/Cargo.toml b/crates/eips/Cargo.toml index cb186d0fb36..849cb74032c 100644 --- a/crates/eips/Cargo.toml +++ b/crates/eips/Cargo.toml @@ -16,6 +16,8 @@ alloy-primitives = { workspace = true, features = ["rlp"], default-features = fa alloy-rlp = { workspace = true, features = ["derive"], default-features = false } alloy-serde.workspace = true +sha2 = { workspace = true, default-features = false } + # serde serde = { workspace = true, default-features = false, optional = true } diff --git a/crates/eips/src/eip4844/mod.rs b/crates/eips/src/eip4844/mod.rs index 0bd1097ab67..71c2a739aae 100644 --- a/crates/eips/src/eip4844/mod.rs +++ b/crates/eips/src/eip4844/mod.rs @@ -9,7 +9,11 @@ pub mod env_settings; #[cfg(feature = "kzg")] pub mod trusted_setup_points; -use alloy_primitives::{b256, FixedBytes, U256}; +/// Contains sidecar related types +mod sidecar; +pub use sidecar::BlobTransactionSidecar; + +use alloy_primitives::{b256, FixedBytes, B256, U256}; /// The modulus of the BLS group used in the KZG commitment scheme. All field /// elements contained in a blob MUST be STRICTLY LESS than this value. @@ -75,6 +79,22 @@ pub type Blob = FixedBytes; /// A commitment/proof serialized as 0x-prefixed hex string pub type Bytes48 = FixedBytes<48>; +/// Calculates the versioned hash for a KzgCommitment of 48 bytes. +/// +/// Specified in [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844#header-extension) +/// +/// # Panics +/// +/// If the given commitment is not 48 bytes long. +pub fn kzg_to_versioned_hash(commitment: &[u8]) -> B256 { + use sha2::Digest; + + debug_assert_eq!(commitment.len(), 48, "commitment length is not 48"); + let mut res = sha2::Sha256::digest(commitment); + res[0] = VERSIONED_HASH_VERSION_KZG; + B256::new(res.into()) +} + /// Calculates the `excess_blob_gas` from the parent header's `blob_gas_used` and `excess_blob_gas`. /// /// See also [the EIP-4844 helpers](https://eips.ethereum.org/EIPS/eip-4844#helpers) diff --git a/crates/eips/src/eip4844/sidecar.rs b/crates/eips/src/eip4844/sidecar.rs new file mode 100644 index 00000000000..03e5315a140 --- /dev/null +++ b/crates/eips/src/eip4844/sidecar.rs @@ -0,0 +1,114 @@ +//! EIP-4844 sidecar type + +use crate::eip4844::{ + kzg_to_versioned_hash, Blob, Bytes48, BYTES_PER_BLOB, BYTES_PER_COMMITMENT, BYTES_PER_PROOF, +}; +use alloy_primitives::{bytes::BufMut, B256}; +use alloy_rlp::{Decodable, Encodable}; + +#[cfg(not(feature = "std"))] +use alloc::vec::Vec; + +/// This represents a set of blobs, and its corresponding commitments and proofs. +/// +/// This type encodes and decodes the fields without an rlp header. +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +#[repr(C)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct BlobTransactionSidecar { + /// The blob data. + pub blobs: Vec, + /// The blob commitments. + pub commitments: Vec, + /// The blob proofs. + pub proofs: Vec, +} + +impl BlobTransactionSidecar { + /// Constructs a new [BlobTransactionSidecar] from a set of blobs, commitments, and proofs. + pub const fn new(blobs: Vec, commitments: Vec, proofs: Vec) -> Self { + Self { blobs, commitments, proofs } + } + + /// Creates a new instance from the given KZG types. + #[cfg(feature = "kzg")] + pub fn from_kzg( + blobs: Vec, + commitments: Vec, + proofs: Vec, + ) -> Self { + // transmutes the vec of items, see also [std::mem::transmute](https://doc.rust-lang.org/std/mem/fn.transmute.html) + unsafe fn transmute_vec(input: Vec) -> Vec { + let mut v = core::mem::ManuallyDrop::new(input); + Vec::from_raw_parts(v.as_mut_ptr() as *mut U, v.len(), v.capacity()) + } + + // SAFETY: all types have the same size and alignment + unsafe { + let blobs = transmute_vec::(blobs); + let commitments = transmute_vec::(commitments); + let proofs = transmute_vec::(proofs); + Self { blobs, commitments, proofs } + } + } + + /// Returns an iterator over the versioned hashes of the commitments. + pub fn versioned_hashes(&self) -> impl Iterator + '_ { + self.commitments.iter().map(|c| kzg_to_versioned_hash(c.as_slice())) + } + + /// Returns the versioned hash for the blob at the given index, if it + /// exists. + pub fn versioned_hash_for_blob(&self, blob_index: usize) -> Option { + self.commitments.get(blob_index).map(|c| kzg_to_versioned_hash(c.as_slice())) + } + + /// Encodes the inner [BlobTransactionSidecar] fields as RLP bytes, __without__ a RLP header. + /// + /// This encodes the fields in the following order: + /// - `blobs` + /// - `commitments` + /// - `proofs` + #[inline] + pub(crate) fn encode_inner(&self, out: &mut dyn BufMut) { + // Encode the blobs, commitments, and proofs + self.blobs.encode(out); + self.commitments.encode(out); + self.proofs.encode(out); + } + + /// Outputs the RLP length of the [BlobTransactionSidecar] fields, without a RLP header. + pub fn fields_len(&self) -> usize { + self.blobs.length() + self.commitments.length() + self.proofs.length() + } + + /// Calculates a size heuristic for the in-memory size of the [BlobTransactionSidecar]. + #[inline] + pub fn size(&self) -> usize { + self.blobs.len() * BYTES_PER_BLOB + // blobs + self.commitments.len() * BYTES_PER_COMMITMENT + // commitments + self.proofs.len() * BYTES_PER_PROOF // proofs + } +} + +impl Encodable for BlobTransactionSidecar { + /// Encodes the inner [BlobTransactionSidecar] fields as RLP bytes, without a RLP header. + fn encode(&self, s: &mut dyn BufMut) { + self.encode_inner(s); + } + + fn length(&self) -> usize { + self.fields_len() + } +} + +impl Decodable for BlobTransactionSidecar { + /// Decodes the inner [BlobTransactionSidecar] fields from RLP bytes, without a RLP header. + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + Ok(Self { + blobs: Decodable::decode(buf)?, + commitments: Decodable::decode(buf)?, + proofs: Decodable::decode(buf)?, + }) + } +}