diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index 1492a3a12..f5b5926da 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -23,7 +23,7 @@ ethers-core = { workspace = true, default-features = false, optional = true } secp256k1 = { workspace = true, features = ["global-context", "recovery"] } # for eip-4844 -c-kzg = { workspace = true, features = ["serde"] } +c-kzg = { workspace = true, features = ["serde"], optional = true } # used for forkid crc = "3" @@ -88,8 +88,9 @@ criterion = "0.5" pprof = { version = "0.12", features = ["flamegraph", "frame-pointer", "criterion"] } [features] -default = [] +default = ["c-kzg"] arbitrary = ["revm-primitives/arbitrary", "reth-rpc-types/arbitrary", "dep:arbitrary", "dep:proptest", "dep:proptest-derive"] +c-kzg = ["revm-primitives/c-kzg", "dep:c-kzg"] test-utils = ["dep:plain_hasher", "dep:hash-db", "dep:ethers-core"] # value-256 controls whether transaction Value fields are DB-encoded as 256 bits instead of the # default of 128 bits. diff --git a/crates/primitives/src/constants/eip4844.rs b/crates/primitives/src/constants/eip4844.rs index 69ecc9c42..ba6ac288e 100644 --- a/crates/primitives/src/constants/eip4844.rs +++ b/crates/primitives/src/constants/eip4844.rs @@ -1,8 +1,6 @@ //! [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844#parameters) protocol constants and utils for shard Blob Transactions. - -use crate::kzg::KzgSettings; -use once_cell::sync::Lazy; -use std::{io::Write, sync::Arc}; +#[cfg(feature = "c-kzg")] +pub use trusted_setup::*; /// Size a single field element in bytes. pub const FIELD_ELEMENT_BYTES: u64 = 32; @@ -34,45 +32,55 @@ pub const BLOB_TX_MIN_BLOB_GASPRICE: u128 = 1u128; /// Commitment version of a KZG commitment pub const VERSIONED_HASH_VERSION_KZG: u8 = 0x01; -/// KZG trusted setup -pub static MAINNET_KZG_TRUSTED_SETUP: Lazy> = Lazy::new(|| { - Arc::new( - c_kzg::KzgSettings::load_trusted_setup( - &revm_primitives::kzg::G1_POINTS.0, - &revm_primitives::kzg::G2_POINTS.0, +#[cfg(feature = "c-kzg")] +mod trusted_setup { + use crate::kzg::KzgSettings; + use once_cell::sync::Lazy; + use std::{io::Write, sync::Arc}; + + /// KZG trusted setup + pub static MAINNET_KZG_TRUSTED_SETUP: Lazy> = Lazy::new(|| { + Arc::new( + c_kzg::KzgSettings::load_trusted_setup( + &revm_primitives::kzg::G1_POINTS.0, + &revm_primitives::kzg::G2_POINTS.0, + ) + .expect("failed to load trusted setup"), ) - .expect("failed to load trusted setup"), - ) -}); + }); -/// Loads the trusted setup parameters from the given bytes and returns the [KzgSettings]. -/// -/// This creates a temp file to store the bytes and then loads the [KzgSettings] from the file via -/// [KzgSettings::load_trusted_setup_file]. -pub fn load_trusted_setup_from_bytes(bytes: &[u8]) -> Result { - let mut file = tempfile::NamedTempFile::new().map_err(LoadKzgSettingsError::TempFileErr)?; - file.write_all(bytes).map_err(LoadKzgSettingsError::TempFileErr)?; - KzgSettings::load_trusted_setup_file(file.path()).map_err(LoadKzgSettingsError::KzgError) -} + /// Loads the trusted setup parameters from the given bytes and returns the [KzgSettings]. + /// + /// This creates a temp file to store the bytes and then loads the [KzgSettings] from the file + /// via [KzgSettings::load_trusted_setup_file]. + pub fn load_trusted_setup_from_bytes( + bytes: &[u8], + ) -> Result { + let mut file = tempfile::NamedTempFile::new().map_err(LoadKzgSettingsError::TempFileErr)?; + file.write_all(bytes).map_err(LoadKzgSettingsError::TempFileErr)?; + KzgSettings::load_trusted_setup_file(file.path()).map_err(LoadKzgSettingsError::KzgError) + } -/// Error type for loading the trusted setup. -#[derive(Debug, thiserror::Error)] -pub enum LoadKzgSettingsError { - /// Failed to create temp file to store bytes for loading [KzgSettings] via - /// [KzgSettings::load_trusted_setup_file]. - #[error("failed to setup temp file: {0}")] - TempFileErr(#[from] std::io::Error), - /// Kzg error - #[error("KZG error: {0:?}")] - KzgError(#[from] c_kzg::Error), -} + /// Error type for loading the trusted setup. + #[derive(Debug, thiserror::Error)] + pub enum LoadKzgSettingsError { + /// Failed to create temp file to store bytes for loading [KzgSettings] via + /// [KzgSettings::load_trusted_setup_file]. + #[error("failed to setup temp file: {0}")] + TempFileErr(#[from] std::io::Error), + /// Kzg error + #[error("KZG error: {0:?}")] + KzgError(#[from] c_kzg::Error), + } -#[cfg(test)] -mod tests { - use super::*; + #[cfg(test)] + mod tests { + use super::*; + use std::sync::Arc; - #[test] - fn ensure_load_kzg_settings() { - let _settings = Arc::clone(&MAINNET_KZG_TRUSTED_SETUP); + #[test] + fn ensure_load_kzg_settings() { + let _settings = Arc::clone(&MAINNET_KZG_TRUSTED_SETUP); + } } } diff --git a/crates/primitives/src/eip4844.rs b/crates/primitives/src/eip4844.rs index bf84da3f7..6d7668b59 100644 --- a/crates/primitives/src/eip4844.rs +++ b/crates/primitives/src/eip4844.rs @@ -1,9 +1,8 @@ //! Helpers for working with EIP-4844 blob fee -use crate::{ - constants::eip4844::{TARGET_DATA_GAS_PER_BLOCK, VERSIONED_HASH_VERSION_KZG}, - kzg::KzgCommitment, - B256, -}; +use crate::constants::eip4844::TARGET_DATA_GAS_PER_BLOCK; +#[cfg(feature = "c-kzg")] +use crate::{constants::eip4844::VERSIONED_HASH_VERSION_KZG, kzg::KzgCommitment, B256}; +#[cfg(feature = "c-kzg")] use sha2::{Digest, Sha256}; // re-exports from revm for calculating blob fee @@ -12,6 +11,7 @@ pub use revm_primitives::calc_blob_gasprice; /// Calculates the versioned hash for a KzgCommitment /// /// Specified in [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844#header-extension) +#[cfg(feature = "c-kzg")] pub fn kzg_to_versioned_hash(commitment: KzgCommitment) -> B256 { let mut res = Sha256::digest(commitment.as_slice()); res[0] = VERSIONED_HASH_VERSION_KZG; diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 957d31065..379ace2dc 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -64,6 +64,7 @@ pub use constants::{ DEV_GENESIS_HASH, EMPTY_OMMER_ROOT_HASH, GOERLI_GENESIS_HASH, HOLESKY_GENESIS_HASH, KECCAK_EMPTY, MAINNET_GENESIS_HASH, SEPOLIA_GENESIS_HASH, }; +#[cfg(feature = "c-kzg")] pub use eip4844::{calculate_excess_blob_gas, kzg_to_versioned_hash}; pub use forkid::{ForkFilter, ForkHash, ForkId, ForkTransition, ValidationError}; pub use genesis::{Genesis, GenesisAccount}; @@ -84,12 +85,18 @@ pub use receipt::{Receipt, ReceiptWithBloom, ReceiptWithBloomRef, Receipts}; pub use serde_helper::JsonU256; pub use snapshot::SnapshotSegment; pub use storage::StorageEntry; + +#[cfg(feature = "c-kzg")] +pub use transaction::{ + BlobTransaction, BlobTransactionSidecar, BlobTransactionValidationError, + FromRecoveredPooledTransaction, PooledTransactionsElement, + PooledTransactionsElementEcRecovered, +}; + pub use transaction::{ util::secp256k1::{public_key_to_address, recover_signer, sign_message}, - AccessList, AccessListItem, BlobTransaction, BlobTransactionSidecar, - BlobTransactionValidationError, FromRecoveredPooledTransaction, FromRecoveredTransaction, - IntoRecoveredTransaction, InvalidTransactionError, PooledTransactionsElement, - PooledTransactionsElementEcRecovered, Signature, Transaction, TransactionKind, TransactionMeta, + AccessList, AccessListItem, FromRecoveredTransaction, IntoRecoveredTransaction, + InvalidTransactionError, Signature, Transaction, TransactionKind, TransactionMeta, TransactionSigned, TransactionSignedEcRecovered, TransactionSignedNoHash, TxEip1559, TxEip2930, TxEip4844, TxHashOrNumber, TxLegacy, TxType, TxValue, EIP1559_TX_TYPE_ID, EIP2930_TX_TYPE_ID, EIP4844_TX_TYPE_ID, LEGACY_TX_TYPE_ID, @@ -125,6 +132,7 @@ pub type H64 = B64; pub use arbitrary; /// EIP-4844 + KZG helpers +#[cfg(feature = "c-kzg")] pub mod kzg { pub use c_kzg::*; } diff --git a/crates/primitives/src/transaction/eip4844.rs b/crates/primitives/src/transaction/eip4844.rs index fccd756dc..1f074f6aa 100644 --- a/crates/primitives/src/transaction/eip4844.rs +++ b/crates/primitives/src/transaction/eip4844.rs @@ -1,32 +1,23 @@ use super::access_list::AccessList; use crate::{ - constants::eip4844::DATA_GAS_PER_BLOB, - keccak256, - kzg::{ - self, Blob, Bytes48, KzgCommitment, KzgProof, KzgSettings, BYTES_PER_BLOB, - BYTES_PER_COMMITMENT, BYTES_PER_PROOF, - }, - kzg_to_versioned_hash, Bytes, ChainId, Signature, Transaction, TransactionKind, - TransactionSigned, TxHash, TxType, TxValue, B256, EIP4844_TX_TYPE_ID, + constants::eip4844::DATA_GAS_PER_BLOB, keccak256, Bytes, ChainId, Signature, TransactionKind, + TxType, TxValue, B256, }; -use alloy_rlp::{length_of_length, Decodable, Encodable, Error as RlpError, Header}; + +#[cfg(feature = "c-kzg")] +use crate::transaction::sidecar::*; + +#[cfg(feature = "c-kzg")] +use crate::kzg_to_versioned_hash; + +#[cfg(feature = "c-kzg")] +use crate::kzg::{self, KzgCommitment, KzgProof, KzgSettings}; +use alloy_rlp::{length_of_length, Decodable, Encodable, Header}; use bytes::BytesMut; use reth_codecs::{main_codec, Compact}; -use serde::{Deserialize, Serialize}; -use std::{mem, ops::Deref}; - -#[cfg(any(test, feature = "arbitrary"))] -use proptest::{ - arbitrary::{any as proptest_any, ParamsFor}, - collection::vec as proptest_vec, - strategy::{BoxedStrategy, Strategy}, -}; - -#[cfg(any(test, feature = "arbitrary"))] -use crate::{ - constants::eip4844::{FIELD_ELEMENTS_PER_BLOB, MAINNET_KZG_TRUSTED_SETUP}, - kzg::BYTES_PER_FIELD_ELEMENT, -}; +use std::mem; +#[cfg(feature = "c-kzg")] +use std::ops::Deref; /// [EIP-4844 Blob Transaction](https://eips.ethereum.org/EIPS/eip-4844#blob-transaction) /// @@ -127,6 +118,7 @@ impl TxEip4844 { /// Returns [BlobTransactionValidationError::InvalidProof] if any blob KZG proof in the response /// fails to verify, or if the versioned hashes in the transaction do not match the actual /// commitment versioned hashes. + #[cfg(feature = "c-kzg")] pub fn validate_blob( &self, sidecar: &BlobTransactionSidecar, @@ -326,431 +318,3 @@ impl TxEip4844 { keccak256(&buf) } } - -/// An error that can occur when validating a [BlobTransaction]. -#[derive(Debug, thiserror::Error)] -pub enum BlobTransactionValidationError { - /// Proof validation failed. - #[error("invalid KZG proof")] - InvalidProof, - /// An error returned by [`kzg`]. - #[error("KZG error: {0:?}")] - KZGError(#[from] kzg::Error), - /// The inner transaction is not a blob transaction. - #[error("unable to verify proof for non blob transaction: {0}")] - NotBlobTransaction(u8), -} - -/// A response to `GetPooledTransactions` that includes blob data, their commitments, and their -/// corresponding proofs. -/// -/// This is defined in [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844#networking) as an element -/// of a `PooledTransactions` response. -#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] -pub struct BlobTransaction { - /// The transaction hash. - pub hash: TxHash, - /// The transaction payload. - pub transaction: TxEip4844, - /// The transaction signature. - pub signature: Signature, - /// The transaction's blob sidecar. - pub sidecar: BlobTransactionSidecar, -} - -impl BlobTransaction { - /// Constructs a new [BlobTransaction] from a [TransactionSigned] and a - /// [BlobTransactionSidecar]. - /// - /// Returns an error if the signed transaction is not [TxEip4844] - pub fn try_from_signed( - tx: TransactionSigned, - sidecar: BlobTransactionSidecar, - ) -> Result { - let TransactionSigned { transaction, signature, hash } = tx; - match transaction { - Transaction::Eip4844(transaction) => Ok(Self { hash, transaction, signature, sidecar }), - transaction => { - let tx = TransactionSigned { transaction, signature, hash }; - Err((tx, sidecar)) - } - } - } - - /// Verifies that the transaction's blob data, commitments, and proofs are all valid. - /// - /// See also [TxEip4844::validate_blob] - pub fn validate( - &self, - proof_settings: &KzgSettings, - ) -> Result<(), BlobTransactionValidationError> { - self.transaction.validate_blob(&self.sidecar, proof_settings) - } - - /// Splits the [BlobTransaction] into its [TransactionSigned] and [BlobTransactionSidecar] - /// components. - pub fn into_parts(self) -> (TransactionSigned, BlobTransactionSidecar) { - let transaction = TransactionSigned { - transaction: Transaction::Eip4844(self.transaction), - hash: self.hash, - signature: self.signature, - }; - - (transaction, self.sidecar) - } - - /// Encodes the [BlobTransaction] fields as RLP, with a tx type. If `with_header` is `false`, - /// the following will be encoded: - /// `tx_type (0x03) || rlp([transaction_payload_body, blobs, commitments, proofs])` - /// - /// If `with_header` is `true`, the following will be encoded: - /// `rlp(tx_type (0x03) || rlp([transaction_payload_body, blobs, commitments, proofs]))` - /// - /// NOTE: The header will be a byte string header, not a list header. - pub(crate) fn encode_with_type_inner(&self, out: &mut dyn bytes::BufMut, with_header: bool) { - // Calculate the length of: - // `tx_type || rlp([transaction_payload_body, blobs, commitments, proofs])` - // - // to construct and encode the string header - if with_header { - Header { - list: false, - // add one for the tx type - payload_length: 1 + self.payload_len(), - } - .encode(out); - } - - out.put_u8(EIP4844_TX_TYPE_ID); - - // Now we encode the inner blob transaction: - self.encode_inner(out); - } - - /// Encodes the [BlobTransaction] fields as RLP, with the following format: - /// `rlp([transaction_payload_body, blobs, commitments, proofs])` - /// - /// where `transaction_payload_body` is a list: - /// `[chain_id, nonce, max_priority_fee_per_gas, ..., y_parity, r, s]` - /// - /// Note: this should be used only when implementing other RLP encoding methods, and does not - /// represent the full RLP encoding of the blob transaction. - pub(crate) fn encode_inner(&self, out: &mut dyn bytes::BufMut) { - // First we construct both required list headers. - // - // The `transaction_payload_body` length is the length of the fields, plus the length of - // its list header. - let tx_header = Header { - list: true, - payload_length: self.transaction.fields_len() + self.signature.payload_len(), - }; - - let tx_length = tx_header.length() + tx_header.payload_length; - - // The payload length is the length of the `tranascation_payload_body` list, plus the - // length of the blobs, commitments, and proofs. - let payload_length = tx_length + self.sidecar.fields_len(); - - // First we use the payload len to construct the first list header - let blob_tx_header = Header { list: true, payload_length }; - - // Encode the blob tx header first - blob_tx_header.encode(out); - - // Encode the inner tx list header, then its fields - tx_header.encode(out); - self.transaction.encode_fields(out); - - // Encode the signature - self.signature.encode(out); - - // Encode the blobs, commitments, and proofs - self.sidecar.encode_inner(out); - } - - /// Ouputs the length of the RLP encoding of the blob transaction, including the tx type byte, - /// optionally including the length of a wrapping string header. If `with_header` is `false`, - /// the length of the following will be calculated: - /// `tx_type (0x03) || rlp([transaction_payload_body, blobs, commitments, proofs])` - /// - /// If `with_header` is `true`, the length of the following will be calculated: - /// `rlp(tx_type (0x03) || rlp([transaction_payload_body, blobs, commitments, proofs]))` - pub(crate) fn payload_len_with_type(&self, with_header: bool) -> usize { - if with_header { - // Construct a header and use that to calculate the total length - let wrapped_header = Header { - list: false, - // add one for the tx type byte - payload_length: 1 + self.payload_len(), - }; - - // The total length is now the length of the header plus the length of the payload - // (which includes the tx type byte) - wrapped_header.length() + wrapped_header.payload_length - } else { - // Just add the length of the tx type to the payload length - 1 + self.payload_len() - } - } - - /// Outputs the length of the RLP encoding of the blob transaction with the following format: - /// `rlp([transaction_payload_body, blobs, commitments, proofs])` - /// - /// where `transaction_payload_body` is a list: - /// `[chain_id, nonce, max_priority_fee_per_gas, ..., y_parity, r, s]` - /// - /// Note: this should be used only when implementing other RLP encoding length methods, and - /// does not represent the full RLP encoding of the blob transaction. - pub(crate) fn payload_len(&self) -> usize { - // The `transaction_payload_body` length is the length of the fields, plus the length of - // its list header. - let tx_header = Header { - list: true, - payload_length: self.transaction.fields_len() + self.signature.payload_len(), - }; - - let tx_length = tx_header.length() + tx_header.payload_length; - - // The payload length is the length of the `tranascation_payload_body` list, plus the - // length of the blobs, commitments, and proofs. - let payload_length = tx_length + self.sidecar.fields_len(); - - // We use the calculated payload len to construct the first list header, which encompasses - // everything in the tx - the length of the second, inner list header is part of - // payload_length - let blob_tx_header = Header { list: true, payload_length }; - - // The final length is the length of: - // * the outer blob tx header + - // * the inner tx header + - // * the inner tx fields + - // * the signature fields + - // * the sidecar fields - blob_tx_header.length() + blob_tx_header.payload_length - } - - /// Decodes a [BlobTransaction] from RLP. This expects the encoding to be: - /// `rlp([transaction_payload_body, blobs, commitments, proofs])` - /// - /// where `transaction_payload_body` is a list: - /// `[chain_id, nonce, max_priority_fee_per_gas, ..., y_parity, r, s]` - /// - /// Note: this should be used only when implementing other RLP decoding methods, and does not - /// represent the full RLP decoding of the `PooledTransactionsElement` type. - pub(crate) fn decode_inner(data: &mut &[u8]) -> alloy_rlp::Result { - // decode the _first_ list header for the rest of the transaction - let header = Header::decode(data)?; - if !header.list { - return Err(RlpError::Custom("PooledTransactions blob tx must be encoded as a list")) - } - - // Now we need to decode the inner 4844 transaction and its signature: - // - // `[chain_id, nonce, max_priority_fee_per_gas, ..., y_parity, r, s]` - let header = Header::decode(data)?; - if !header.list { - return Err(RlpError::Custom( - "PooledTransactions inner blob tx must be encoded as a list", - )) - } - - // inner transaction - let transaction = TxEip4844::decode_inner(data)?; - - // signature - let signature = Signature::decode(data)?; - - // All that's left are the blobs, commitments, and proofs - let sidecar = BlobTransactionSidecar::decode_inner(data)?; - - // # Calculating the hash - // - // The full encoding of the `PooledTransaction` response is: - // `tx_type (0x03) || rlp([tx_payload_body, blobs, commitments, proofs])` - // - // The transaction hash however, is: - // `keccak256(tx_type (0x03) || rlp(tx_payload_body))` - // - // Note that this is `tx_payload_body`, not `[tx_payload_body]`, which would be - // `[[chain_id, nonce, max_priority_fee_per_gas, ...]]`, i.e. a list within a list. - // - // Because the pooled transaction encoding is different than the hash encoding for - // EIP-4844 transactions, we do not use the original buffer to calculate the hash. - // - // Instead, we use `encode_with_signature`, which RLP encodes the transaction with a - // signature for hashing without a header. We then hash the result. - let mut buf = Vec::new(); - transaction.encode_with_signature(&signature, &mut buf, false); - let hash = keccak256(&buf); - - Ok(Self { transaction, hash, signature, sidecar }) - } -} - -/// This represents a set of blobs, and its corresponding commitments and proofs. -#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] -#[repr(C)] -pub struct BlobTransactionSidecar { - /// The blob data. - pub blobs: Vec, - /// The blob commitments. - pub commitments: Vec, - /// The blob proofs. - pub proofs: Vec, -} - -impl BlobTransactionSidecar { - /// Creates a new [BlobTransactionSidecar] using the given blobs, commitments, and proofs. - pub fn new(blobs: Vec, commitments: Vec, proofs: Vec) -> Self { - Self { blobs, commitments, proofs } - } - - /// Encodes the inner [BlobTransactionSidecar] fields as RLP bytes, without a RLP header. - /// - /// This encodes the fields in the following order: - /// - `blobs` - /// - `commitments` - /// - `proofs` - pub(crate) fn encode_inner(&self, out: &mut dyn bytes::BufMut) { - BlobTransactionSidecarRlp::wrap_ref(self).encode(out); - } - - /// Outputs the RLP length of the [BlobTransactionSidecar] fields, without a RLP header. - pub fn fields_len(&self) -> usize { - BlobTransactionSidecarRlp::wrap_ref(self).fields_len() - } - - /// 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()) - } - - /// 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 - } -} - -// Wrapper for c-kzg rlp -#[repr(C)] -struct BlobTransactionSidecarRlp { - blobs: Vec<[u8; c_kzg::BYTES_PER_BLOB]>, - commitments: Vec<[u8; 48]>, - proofs: Vec<[u8; 48]>, -} - -const _: [(); std::mem::size_of::()] = - [(); std::mem::size_of::()]; - -impl BlobTransactionSidecarRlp { - 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 { std::mem::transmute(self) } - } - - fn encode(&self, out: &mut dyn bytes::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)?, - }) - } -} - -#[cfg(any(test, feature = "arbitrary"))] -impl<'a> arbitrary::Arbitrary<'a> for BlobTransactionSidecar { - fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { - let mut arr = [0u8; BYTES_PER_BLOB]; - let blobs: Vec = (0..u.int_in_range(1..=16)?) - .map(|_| { - arr = arbitrary::Arbitrary::arbitrary(u).unwrap(); - - // Ensure that each blob is cacnonical by ensuring each field element contained in - // the blob is < BLS_MODULUS - for i in 0..(FIELD_ELEMENTS_PER_BLOB as usize) { - arr[i * BYTES_PER_FIELD_ELEMENT] = 0; - } - - Blob::from(arr) - }) - .collect(); - - Ok(generate_blob_sidecar(blobs)) - } -} - -#[cfg(any(test, feature = "arbitrary"))] -impl proptest::arbitrary::Arbitrary for BlobTransactionSidecar { - type Parameters = ParamsFor; - type Strategy = BoxedStrategy; - - fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { - proptest_vec(proptest_vec(proptest_any::(), BYTES_PER_BLOB), 1..=5) - .prop_map(move |blobs| { - let blobs = blobs - .into_iter() - .map(|mut blob| { - let mut arr = [0u8; BYTES_PER_BLOB]; - - // Ensure that each blob is cacnonical by ensuring each field element - // contained in the blob is < BLS_MODULUS - for i in 0..(FIELD_ELEMENTS_PER_BLOB as usize) { - blob[i * BYTES_PER_FIELD_ELEMENT] = 0; - } - - arr.copy_from_slice(blob.as_slice()); - arr.into() - }) - .collect(); - - generate_blob_sidecar(blobs) - }) - .boxed() - } -} - -#[cfg(any(test, feature = "arbitrary"))] -fn generate_blob_sidecar(blobs: Vec) -> BlobTransactionSidecar { - let kzg_settings = MAINNET_KZG_TRUSTED_SETUP.clone(); - - let commitments: Vec = blobs - .iter() - .map(|blob| KzgCommitment::blob_to_kzg_commitment(&blob.clone(), &kzg_settings).unwrap()) - .map(|commitment| commitment.to_bytes()) - .collect(); - - let proofs: Vec = blobs - .iter() - .zip(commitments.iter()) - .map(|(blob, commitment)| { - KzgProof::compute_blob_kzg_proof(blob, commitment, &kzg_settings).unwrap() - }) - .map(|proof| proof.to_bytes()) - .collect(); - - BlobTransactionSidecar { blobs, commitments, proofs } -} diff --git a/crates/primitives/src/transaction/mod.rs b/crates/primitives/src/transaction/mod.rs index 1d1dc1228..7f542e80d 100644 --- a/crates/primitives/src/transaction/mod.rs +++ b/crates/primitives/src/transaction/mod.rs @@ -16,13 +16,15 @@ use std::mem; pub use access_list::{AccessList, AccessListItem}; pub use eip1559::TxEip1559; pub use eip2930::TxEip2930; -pub use eip4844::{ - BlobTransaction, BlobTransactionSidecar, BlobTransactionValidationError, TxEip4844, -}; +pub use eip4844::TxEip4844; + pub use error::InvalidTransactionError; pub use legacy::TxLegacy; pub use meta::TransactionMeta; +#[cfg(feature = "c-kzg")] pub use pooled::{PooledTransactionsElement, PooledTransactionsElementEcRecovered}; +#[cfg(feature = "c-kzg")] +pub use sidecar::{BlobTransaction, BlobTransactionSidecar, BlobTransactionValidationError}; pub use signature::Signature; pub use tx_type::{ TxType, EIP1559_TX_TYPE_ID, EIP2930_TX_TYPE_ID, EIP4844_TX_TYPE_ID, LEGACY_TX_TYPE_ID, @@ -37,7 +39,10 @@ mod eip4844; mod error; mod legacy; mod meta; +#[cfg(feature = "c-kzg")] mod pooled; +#[cfg(feature = "c-kzg")] +mod sidecar; mod signature; mod tx_type; mod tx_value; @@ -1286,6 +1291,7 @@ impl FromRecoveredTransaction for TransactionSignedEcRecovered { /// /// This is a conversion trait that'll ensure transactions received via P2P can be converted to the /// transaction type that the transaction pool uses. +#[cfg(feature = "c-kzg")] pub trait FromRecoveredPooledTransaction { /// Converts to this type from the given [`PooledTransactionsElementEcRecovered`]. fn from_recovered_transaction(tx: PooledTransactionsElementEcRecovered) -> Self; diff --git a/crates/primitives/src/transaction/pooled.rs b/crates/primitives/src/transaction/pooled.rs index 005f36a05..1331c59c2 100644 --- a/crates/primitives/src/transaction/pooled.rs +++ b/crates/primitives/src/transaction/pooled.rs @@ -1,5 +1,8 @@ //! Defines the types for blob transactions, legacy, and other EIP-2718 transactions included in a //! response to `GetPooledTransactions`. +#![cfg(feature = "c-kzg")] +#![cfg_attr(docsrs, doc(cfg(feature = "c-kzg")))] + use crate::{ Address, BlobTransaction, Bytes, Signature, Transaction, TransactionSigned, TransactionSignedEcRecovered, TxEip1559, TxEip2930, TxHash, TxLegacy, B256, EIP4844_TX_TYPE_ID, diff --git a/crates/primitives/src/transaction/sidecar.rs b/crates/primitives/src/transaction/sidecar.rs new file mode 100644 index 000000000..1ab6bc117 --- /dev/null +++ b/crates/primitives/src/transaction/sidecar.rs @@ -0,0 +1,454 @@ +#![cfg(feature = "c-kzg")] +#![cfg_attr(docsrs, doc(cfg(feature = "c-kzg")))] + +use crate::{ + keccak256, Signature, Transaction, TransactionSigned, TxEip4844, TxHash, EIP4844_TX_TYPE_ID, +}; + +use crate::kzg::{ + self, Blob, Bytes48, KzgSettings, BYTES_PER_BLOB, BYTES_PER_COMMITMENT, BYTES_PER_PROOF, +}; +use alloy_rlp::{Decodable, Encodable, Error as RlpError, Header}; + +use serde::{Deserialize, Serialize}; + +#[cfg(any(test, feature = "arbitrary"))] +use proptest::{ + arbitrary::{any as proptest_any, ParamsFor}, + collection::vec as proptest_vec, + strategy::{BoxedStrategy, Strategy}, +}; + +#[cfg(any(test, feature = "arbitrary"))] +use crate::{ + constants::eip4844::{FIELD_ELEMENTS_PER_BLOB, MAINNET_KZG_TRUSTED_SETUP}, + kzg::{KzgCommitment, KzgProof, BYTES_PER_FIELD_ELEMENT}, +}; + +/// An error that can occur when validating a [BlobTransaction]. +#[derive(Debug, thiserror::Error)] +pub enum BlobTransactionValidationError { + /// Proof validation failed. + #[error("invalid KZG proof")] + InvalidProof, + /// An error returned by [`kzg`]. + #[error("KZG error: {0:?}")] + KZGError(#[from] kzg::Error), + /// The inner transaction is not a blob transaction. + #[error("unable to verify proof for non blob transaction: {0}")] + NotBlobTransaction(u8), +} + +/// A response to `GetPooledTransactions` that includes blob data, their commitments, and their +/// corresponding proofs. +/// +/// This is defined in [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844#networking) as an element +/// of a `PooledTransactions` response. +#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] +pub struct BlobTransaction { + /// The transaction hash. + pub hash: TxHash, + /// The transaction payload. + pub transaction: TxEip4844, + /// The transaction signature. + pub signature: Signature, + /// The transaction's blob sidecar. + pub sidecar: BlobTransactionSidecar, +} + +impl BlobTransaction { + /// Constructs a new [BlobTransaction] from a [TransactionSigned] and a + /// [BlobTransactionSidecar]. + /// + /// Returns an error if the signed transaction is not [TxEip4844] + pub fn try_from_signed( + tx: TransactionSigned, + sidecar: BlobTransactionSidecar, + ) -> Result { + let TransactionSigned { transaction, signature, hash } = tx; + match transaction { + Transaction::Eip4844(transaction) => Ok(Self { hash, transaction, signature, sidecar }), + transaction => { + let tx = TransactionSigned { transaction, signature, hash }; + Err((tx, sidecar)) + } + } + } + + /// Verifies that the transaction's blob data, commitments, and proofs are all valid. + /// + /// See also [TxEip4844::validate_blob] + pub fn validate( + &self, + proof_settings: &KzgSettings, + ) -> Result<(), BlobTransactionValidationError> { + self.transaction.validate_blob(&self.sidecar, proof_settings) + } + + /// Splits the [BlobTransaction] into its [TransactionSigned] and [BlobTransactionSidecar] + /// components. + pub fn into_parts(self) -> (TransactionSigned, BlobTransactionSidecar) { + let transaction = TransactionSigned { + transaction: Transaction::Eip4844(self.transaction), + hash: self.hash, + signature: self.signature, + }; + + (transaction, self.sidecar) + } + + /// Encodes the [BlobTransaction] fields as RLP, with a tx type. If `with_header` is `false`, + /// the following will be encoded: + /// `tx_type (0x03) || rlp([transaction_payload_body, blobs, commitments, proofs])` + /// + /// If `with_header` is `true`, the following will be encoded: + /// `rlp(tx_type (0x03) || rlp([transaction_payload_body, blobs, commitments, proofs]))` + /// + /// NOTE: The header will be a byte string header, not a list header. + pub(crate) fn encode_with_type_inner(&self, out: &mut dyn bytes::BufMut, with_header: bool) { + // Calculate the length of: + // `tx_type || rlp([transaction_payload_body, blobs, commitments, proofs])` + // + // to construct and encode the string header + if with_header { + Header { + list: false, + // add one for the tx type + payload_length: 1 + self.payload_len(), + } + .encode(out); + } + + out.put_u8(EIP4844_TX_TYPE_ID); + + // Now we encode the inner blob transaction: + self.encode_inner(out); + } + + /// Encodes the [BlobTransaction] fields as RLP, with the following format: + /// `rlp([transaction_payload_body, blobs, commitments, proofs])` + /// + /// where `transaction_payload_body` is a list: + /// `[chain_id, nonce, max_priority_fee_per_gas, ..., y_parity, r, s]` + /// + /// Note: this should be used only when implementing other RLP encoding methods, and does not + /// represent the full RLP encoding of the blob transaction. + pub(crate) fn encode_inner(&self, out: &mut dyn bytes::BufMut) { + // First we construct both required list headers. + // + // The `transaction_payload_body` length is the length of the fields, plus the length of + // its list header. + let tx_header = Header { + list: true, + payload_length: self.transaction.fields_len() + self.signature.payload_len(), + }; + + let tx_length = tx_header.length() + tx_header.payload_length; + + // The payload length is the length of the `tranascation_payload_body` list, plus the + // length of the blobs, commitments, and proofs. + let payload_length = tx_length + self.sidecar.fields_len(); + + // First we use the payload len to construct the first list header + let blob_tx_header = Header { list: true, payload_length }; + + // Encode the blob tx header first + blob_tx_header.encode(out); + + // Encode the inner tx list header, then its fields + tx_header.encode(out); + self.transaction.encode_fields(out); + + // Encode the signature + self.signature.encode(out); + + // Encode the blobs, commitments, and proofs + self.sidecar.encode_inner(out); + } + + /// Ouputs the length of the RLP encoding of the blob transaction, including the tx type byte, + /// optionally including the length of a wrapping string header. If `with_header` is `false`, + /// the length of the following will be calculated: + /// `tx_type (0x03) || rlp([transaction_payload_body, blobs, commitments, proofs])` + /// + /// If `with_header` is `true`, the length of the following will be calculated: + /// `rlp(tx_type (0x03) || rlp([transaction_payload_body, blobs, commitments, proofs]))` + pub(crate) fn payload_len_with_type(&self, with_header: bool) -> usize { + if with_header { + // Construct a header and use that to calculate the total length + let wrapped_header = Header { + list: false, + // add one for the tx type byte + payload_length: 1 + self.payload_len(), + }; + + // The total length is now the length of the header plus the length of the payload + // (which includes the tx type byte) + wrapped_header.length() + wrapped_header.payload_length + } else { + // Just add the length of the tx type to the payload length + 1 + self.payload_len() + } + } + + /// Outputs the length of the RLP encoding of the blob transaction with the following format: + /// `rlp([transaction_payload_body, blobs, commitments, proofs])` + /// + /// where `transaction_payload_body` is a list: + /// `[chain_id, nonce, max_priority_fee_per_gas, ..., y_parity, r, s]` + /// + /// Note: this should be used only when implementing other RLP encoding length methods, and + /// does not represent the full RLP encoding of the blob transaction. + pub(crate) fn payload_len(&self) -> usize { + // The `transaction_payload_body` length is the length of the fields, plus the length of + // its list header. + let tx_header = Header { + list: true, + payload_length: self.transaction.fields_len() + self.signature.payload_len(), + }; + + let tx_length = tx_header.length() + tx_header.payload_length; + + // The payload length is the length of the `tranascation_payload_body` list, plus the + // length of the blobs, commitments, and proofs. + let payload_length = tx_length + self.sidecar.fields_len(); + + // We use the calculated payload len to construct the first list header, which encompasses + // everything in the tx - the length of the second, inner list header is part of + // payload_length + let blob_tx_header = Header { list: true, payload_length }; + + // The final length is the length of: + // * the outer blob tx header + + // * the inner tx header + + // * the inner tx fields + + // * the signature fields + + // * the sidecar fields + blob_tx_header.length() + blob_tx_header.payload_length + } + + /// Decodes a [BlobTransaction] from RLP. This expects the encoding to be: + /// `rlp([transaction_payload_body, blobs, commitments, proofs])` + /// + /// where `transaction_payload_body` is a list: + /// `[chain_id, nonce, max_priority_fee_per_gas, ..., y_parity, r, s]` + /// + /// Note: this should be used only when implementing other RLP decoding methods, and does not + /// represent the full RLP decoding of the `PooledTransactionsElement` type. + pub(crate) fn decode_inner(data: &mut &[u8]) -> alloy_rlp::Result { + // decode the _first_ list header for the rest of the transaction + let header = Header::decode(data)?; + if !header.list { + return Err(RlpError::Custom("PooledTransactions blob tx must be encoded as a list")) + } + + // Now we need to decode the inner 4844 transaction and its signature: + // + // `[chain_id, nonce, max_priority_fee_per_gas, ..., y_parity, r, s]` + let header = Header::decode(data)?; + if !header.list { + return Err(RlpError::Custom( + "PooledTransactions inner blob tx must be encoded as a list", + )) + } + + // inner transaction + let transaction = TxEip4844::decode_inner(data)?; + + // signature + let signature = Signature::decode(data)?; + + // All that's left are the blobs, commitments, and proofs + let sidecar = BlobTransactionSidecar::decode_inner(data)?; + + // # Calculating the hash + // + // The full encoding of the `PooledTransaction` response is: + // `tx_type (0x03) || rlp([tx_payload_body, blobs, commitments, proofs])` + // + // The transaction hash however, is: + // `keccak256(tx_type (0x03) || rlp(tx_payload_body))` + // + // Note that this is `tx_payload_body`, not `[tx_payload_body]`, which would be + // `[[chain_id, nonce, max_priority_fee_per_gas, ...]]`, i.e. a list within a list. + // + // Because the pooled transaction encoding is different than the hash encoding for + // EIP-4844 transactions, we do not use the original buffer to calculate the hash. + // + // Instead, we use `encode_with_signature`, which RLP encodes the transaction with a + // signature for hashing without a header. We then hash the result. + let mut buf = Vec::new(); + transaction.encode_with_signature(&signature, &mut buf, false); + let hash = keccak256(&buf); + + Ok(Self { transaction, hash, signature, sidecar }) + } +} + +/// This represents a set of blobs, and its corresponding commitments and proofs. +#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)] +#[repr(C)] +pub struct BlobTransactionSidecar { + /// The blob data. + pub blobs: Vec, + /// The blob commitments. + pub commitments: Vec, + /// The blob proofs. + pub proofs: Vec, +} + +impl BlobTransactionSidecar { + /// Creates a new [BlobTransactionSidecar] using the given blobs, commitments, and proofs. + pub fn new(blobs: Vec, commitments: Vec, proofs: Vec) -> Self { + Self { blobs, commitments, proofs } + } + + /// Encodes the inner [BlobTransactionSidecar] fields as RLP bytes, without a RLP header. + /// + /// This encodes the fields in the following order: + /// - `blobs` + /// - `commitments` + /// - `proofs` + pub(crate) fn encode_inner(&self, out: &mut dyn bytes::BufMut) { + BlobTransactionSidecarRlp::wrap_ref(self).encode(out); + } + + /// Outputs the RLP length of the [BlobTransactionSidecar] fields, without a RLP header. + pub fn fields_len(&self) -> usize { + BlobTransactionSidecarRlp::wrap_ref(self).fields_len() + } + + /// 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()) + } + + /// 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 + } +} + +// Wrapper for c-kzg rlp +#[repr(C)] +struct BlobTransactionSidecarRlp { + blobs: Vec<[u8; c_kzg::BYTES_PER_BLOB]>, + commitments: Vec<[u8; 48]>, + proofs: Vec<[u8; 48]>, +} + +const _: [(); std::mem::size_of::()] = + [(); std::mem::size_of::()]; + +impl BlobTransactionSidecarRlp { + 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 { std::mem::transmute(self) } + } + + fn encode(&self, out: &mut dyn bytes::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)?, + }) + } +} + +#[cfg(any(test, feature = "arbitrary"))] +impl<'a> arbitrary::Arbitrary<'a> for BlobTransactionSidecar { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let mut arr = [0u8; BYTES_PER_BLOB]; + let blobs: Vec = (0..u.int_in_range(1..=16)?) + .map(|_| { + arr = arbitrary::Arbitrary::arbitrary(u).unwrap(); + + // Ensure that each blob is cacnonical by ensuring each field element contained in + // the blob is < BLS_MODULUS + for i in 0..(FIELD_ELEMENTS_PER_BLOB as usize) { + arr[i * BYTES_PER_FIELD_ELEMENT] = 0; + } + + Blob::from(arr) + }) + .collect(); + + Ok(generate_blob_sidecar(blobs)) + } +} + +#[cfg(any(test, feature = "arbitrary"))] +impl proptest::arbitrary::Arbitrary for BlobTransactionSidecar { + type Parameters = ParamsFor; + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + proptest_vec(proptest_vec(proptest_any::(), BYTES_PER_BLOB), 1..=5) + .prop_map(move |blobs| { + let blobs = blobs + .into_iter() + .map(|mut blob| { + let mut arr = [0u8; BYTES_PER_BLOB]; + + // Ensure that each blob is cacnonical by ensuring each field element + // contained in the blob is < BLS_MODULUS + for i in 0..(FIELD_ELEMENTS_PER_BLOB as usize) { + blob[i * BYTES_PER_FIELD_ELEMENT] = 0; + } + + arr.copy_from_slice(blob.as_slice()); + arr.into() + }) + .collect(); + + generate_blob_sidecar(blobs) + }) + .boxed() + } +} + +#[cfg(any(test, feature = "arbitrary"))] +fn generate_blob_sidecar(blobs: Vec) -> BlobTransactionSidecar { + let kzg_settings = MAINNET_KZG_TRUSTED_SETUP.clone(); + + let commitments: Vec = blobs + .iter() + .map(|blob| KzgCommitment::blob_to_kzg_commitment(&blob.clone(), &kzg_settings).unwrap()) + .map(|commitment| commitment.to_bytes()) + .collect(); + + let proofs: Vec = blobs + .iter() + .zip(commitments.iter()) + .map(|(blob, commitment)| { + KzgProof::compute_blob_kzg_proof(blob, commitment, &kzg_settings).unwrap() + }) + .map(|proof| proof.to_bytes()) + .collect(); + + BlobTransactionSidecar { blobs, commitments, proofs } +}