feat: feature gate c-kzg in reth-primitives (#5240)

This commit is contained in:
Dan Cline
2023-11-02 10:26:03 -04:00
committed by GitHub
parent c0e70ba8b6
commit 24fade37ec
8 changed files with 549 additions and 505 deletions

View File

@ -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.

View File

@ -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<Arc<KzgSettings>> = 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<Arc<KzgSettings>> = 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<KzgSettings, LoadKzgSettingsError> {
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<KzgSettings, LoadKzgSettingsError> {
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);
}
}
}

View File

@ -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;

View File

@ -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::*;
}

View File

@ -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<Self, (TransactionSigned, BlobTransactionSidecar)> {
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<Self> {
// 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<Blob>,
/// The blob commitments.
pub commitments: Vec<Bytes48>,
/// The blob proofs.
pub proofs: Vec<Bytes48>,
}
impl BlobTransactionSidecar {
/// Creates a new [BlobTransactionSidecar] using the given blobs, commitments, and proofs.
pub fn new(blobs: Vec<Blob>, commitments: Vec<Bytes48>, proofs: Vec<Bytes48>) -> 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<Self> {
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::<BlobTransactionSidecar>()] =
[(); std::mem::size_of::<BlobTransactionSidecarRlp>()];
impl BlobTransactionSidecarRlp {
fn wrap_ref(other: &BlobTransactionSidecar) -> &Self {
// SAFETY: Same repr and size
unsafe { &*(other as *const BlobTransactionSidecar).cast::<Self>() }
}
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<Self> {
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<Self> {
let mut arr = [0u8; BYTES_PER_BLOB];
let blobs: Vec<Blob> = (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<String>;
type Strategy = BoxedStrategy<BlobTransactionSidecar>;
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
proptest_vec(proptest_vec(proptest_any::<u8>(), 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<Blob>) -> BlobTransactionSidecar {
let kzg_settings = MAINNET_KZG_TRUSTED_SETUP.clone();
let commitments: Vec<Bytes48> = blobs
.iter()
.map(|blob| KzgCommitment::blob_to_kzg_commitment(&blob.clone(), &kzg_settings).unwrap())
.map(|commitment| commitment.to_bytes())
.collect();
let proofs: Vec<Bytes48> = 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 }
}

View File

@ -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;

View File

@ -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,

View File

@ -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<Self, (TransactionSigned, BlobTransactionSidecar)> {
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<Self> {
// 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<Blob>,
/// The blob commitments.
pub commitments: Vec<Bytes48>,
/// The blob proofs.
pub proofs: Vec<Bytes48>,
}
impl BlobTransactionSidecar {
/// Creates a new [BlobTransactionSidecar] using the given blobs, commitments, and proofs.
pub fn new(blobs: Vec<Blob>, commitments: Vec<Bytes48>, proofs: Vec<Bytes48>) -> 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<Self> {
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::<BlobTransactionSidecar>()] =
[(); std::mem::size_of::<BlobTransactionSidecarRlp>()];
impl BlobTransactionSidecarRlp {
fn wrap_ref(other: &BlobTransactionSidecar) -> &Self {
// SAFETY: Same repr and size
unsafe { &*(other as *const BlobTransactionSidecar).cast::<Self>() }
}
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<Self> {
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<Self> {
let mut arr = [0u8; BYTES_PER_BLOB];
let blobs: Vec<Blob> = (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<String>;
type Strategy = BoxedStrategy<BlobTransactionSidecar>;
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
proptest_vec(proptest_vec(proptest_any::<u8>(), 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<Blob>) -> BlobTransactionSidecar {
let kzg_settings = MAINNET_KZG_TRUSTED_SETUP.clone();
let commitments: Vec<Bytes48> = blobs
.iter()
.map(|blob| KzgCommitment::blob_to_kzg_commitment(&blob.clone(), &kzg_settings).unwrap())
.map(|commitment| commitment.to_bytes())
.collect();
let proofs: Vec<Bytes48> = 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 }
}