diff --git a/Cargo.lock b/Cargo.lock index 9f85ac1fb4..996086a630 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7120,6 +7120,7 @@ dependencies = [ "proptest", "proptest-arbitrary-interop", "reth-codecs-derive", + "reth-zstd-compressors", "rstest", "serde", "serde_json", diff --git a/crates/ethereum/primitives/src/transaction.rs b/crates/ethereum/primitives/src/transaction.rs index 2c70264402..e3b4c8f2bd 100644 --- a/crates/ethereum/primitives/src/transaction.rs +++ b/crates/ethereum/primitives/src/transaction.rs @@ -2,8 +2,8 @@ use alloc::vec::Vec; pub use alloy_consensus::{transaction::PooledTransaction, TxType}; use alloy_consensus::{ transaction::{RlpEcdsaDecodableTx, RlpEcdsaEncodableTx}, - BlobTransactionSidecar, SignableTransaction, Signed, TxEip1559, TxEip2930, TxEip4844, - TxEip4844Variant, TxEip4844WithSidecar, TxEip7702, TxEnvelope, TxLegacy, Typed2718, + BlobTransactionSidecar, EthereumTxEnvelope, SignableTransaction, Signed, TxEip1559, TxEip2930, + TxEip4844, TxEip4844Variant, TxEip4844WithSidecar, TxEip7702, TxEnvelope, TxLegacy, Typed2718, TypedTransaction, }; use alloy_eips::{ @@ -594,6 +594,19 @@ impl From for TxEnvelope { } } +impl From for EthereumTxEnvelope { + fn from(value: TransactionSigned) -> Self { + let (tx, signature, hash) = value.into_parts(); + match tx { + Transaction::Legacy(tx) => Signed::new_unchecked(tx, signature, hash).into(), + Transaction::Eip2930(tx) => Signed::new_unchecked(tx, signature, hash).into(), + Transaction::Eip1559(tx) => Signed::new_unchecked(tx, signature, hash).into(), + Transaction::Eip4844(tx) => Signed::new_unchecked(tx, signature, hash).into(), + Transaction::Eip7702(tx) => Signed::new_unchecked(tx, signature, hash).into(), + } + } +} + impl From for Signed { fn from(value: TransactionSigned) -> Self { let (tx, sig, hash) = value.into_parts(); @@ -1074,7 +1087,8 @@ pub(super) mod serde_bincode_compat { mod tests { use super::*; use alloy_consensus::{ - constants::LEGACY_TX_TYPE_ID, Block, Transaction as _, TxEip1559, TxLegacy, + constants::LEGACY_TX_TYPE_ID, Block, EthereumTxEnvelope, Transaction as _, TxEip1559, + TxLegacy, }; use alloy_eips::{ eip2718::{Decodable2718, Encodable2718}, @@ -1085,10 +1099,38 @@ mod tests { U256, }; use alloy_rlp::{Decodable, Encodable, Error as RlpError}; + use proptest::proptest; + use proptest_arbitrary_interop::arb; use reth_codecs::Compact; use reth_primitives_traits::SignedTransaction; use std::str::FromStr; + proptest! { + #[test] + fn test_roundtrip_compact_encode_envelope(reth_tx in arb::()) { + let mut expected_buf = Vec::::new(); + let expected_len = reth_tx.to_compact(&mut expected_buf); + + let mut actual_but = Vec::::new(); + let alloy_tx = EthereumTxEnvelope::::from(reth_tx); + let actual_len = alloy_tx.to_compact(&mut actual_but); + + assert_eq!(actual_but, expected_buf); + assert_eq!(actual_len, expected_len); + } + + #[test] + fn test_roundtrip_compact_decode_envelope(reth_tx in arb::()) { + let mut buf = Vec::::new(); + let len = reth_tx.to_compact(&mut buf); + + let (actual_tx, _) = EthereumTxEnvelope::::from_compact(&buf, len); + let expected_tx = EthereumTxEnvelope::::from(reth_tx); + + assert_eq!(actual_tx, expected_tx); + } + } + #[test] fn eip_2_reject_high_s_value() { // This pre-homestead transaction has a high `s` value and should be rejected by the diff --git a/crates/storage/codecs/Cargo.toml b/crates/storage/codecs/Cargo.toml index 7e35f40056..98bc23c353 100644 --- a/crates/storage/codecs/Cargo.toml +++ b/crates/storage/codecs/Cargo.toml @@ -13,6 +13,7 @@ workspace = true [dependencies] # reth reth-codecs-derive.workspace = true +reth-zstd-compressors = { workspace = true, optional = true, default-features = false } # eth alloy-consensus = { workspace = true, optional = true } @@ -55,12 +56,14 @@ std = [ "serde/std", "op-alloy-consensus?/std", "serde_json/std", + "reth-zstd-compressors?/std", ] alloy = [ "dep:alloy-consensus", "dep:alloy-eips", "dep:alloy-genesis", "dep:alloy-trie", + "dep:reth-zstd-compressors", ] op = ["alloy", "dep:op-alloy-consensus"] test-utils = [ diff --git a/crates/storage/codecs/src/alloy/transaction/ethereum.rs b/crates/storage/codecs/src/alloy/transaction/ethereum.rs new file mode 100644 index 0000000000..265e289c76 --- /dev/null +++ b/crates/storage/codecs/src/alloy/transaction/ethereum.rs @@ -0,0 +1,179 @@ +use crate::{Compact, Vec}; +use alloy_consensus::{ + transaction::RlpEcdsaEncodableTx, EthereumTxEnvelope, Signed, Transaction, TxEip1559, + TxEip2930, TxEip7702, TxLegacy, TxType, +}; +use alloy_primitives::PrimitiveSignature; +use bytes::{Buf, BufMut}; + +/// A trait for extracting transaction without type and signature and serializing it using +/// [`Compact`] encoding. +/// +/// It is not a responsibility of this trait to encode transaction type and signature. Likely this +/// will be a part of a serialization scenario with a greater scope where these values are +/// serialized separately. +/// +/// See [`ToTxCompact::to_tx_compact`]. +trait ToTxCompact { + /// Serializes inner transaction using [`Compact`] encoding. Writes the result into `buf`. + /// + /// The written bytes do not contain signature and transaction type. This information be needs + /// to be serialized extra if needed. + fn to_tx_compact(&self, buf: &mut (impl BufMut + AsMut<[u8]>)); +} + +/// A trait for deserializing transaction without type and signature using [`Compact`] encoding. +/// +/// It is not a responsibility of this trait to extract transaction type and signature, but both +/// are needed to create the value. While these values can come from anywhere, likely this will be +/// a part of a deserialization scenario with a greater scope where these values are deserialized +/// separately. +/// +/// See [`FromTxCompact::from_tx_compact`]. +trait FromTxCompact { + /// Deserializes inner transaction using [`Compact`] encoding. The concrete type is determined + /// by `tx_type`. The `signature` is added to create typed and signed transaction. + /// + /// Returns a tuple of 2 elements. The first element is the deserialized value and the second + /// is a byte slice created from `buf` with a starting position advanced by the exact amount + /// of bytes consumed for this process. + fn from_tx_compact(buf: &[u8], tx_type: TxType, signature: PrimitiveSignature) -> (Self, &[u8]) + where + Self: Sized; +} + +impl ToTxCompact for EthereumTxEnvelope { + fn to_tx_compact(&self, buf: &mut (impl BufMut + AsMut<[u8]>)) { + match self { + Self::Legacy(tx) => tx.tx().to_compact(buf), + Self::Eip2930(tx) => tx.tx().to_compact(buf), + Self::Eip1559(tx) => tx.tx().to_compact(buf), + Self::Eip4844(tx) => tx.tx().to_compact(buf), + Self::Eip7702(tx) => tx.tx().to_compact(buf), + }; + } +} + +impl FromTxCompact for EthereumTxEnvelope { + fn from_tx_compact( + buf: &[u8], + tx_type: TxType, + signature: PrimitiveSignature, + ) -> (Self, &[u8]) { + match tx_type { + TxType::Legacy => { + let (tx, buf) = TxLegacy::from_compact(buf, buf.len()); + let tx = Signed::new_unhashed(tx, signature); + (Self::Legacy(tx), buf) + } + TxType::Eip2930 => { + let (tx, buf) = TxEip2930::from_compact(buf, buf.len()); + let tx = Signed::new_unhashed(tx, signature); + (Self::Eip2930(tx), buf) + } + TxType::Eip1559 => { + let (tx, buf) = TxEip1559::from_compact(buf, buf.len()); + let tx = Signed::new_unhashed(tx, signature); + (Self::Eip1559(tx), buf) + } + TxType::Eip4844 => { + let (tx, buf) = Eip4844::from_compact(buf, buf.len()); + let tx = Signed::new_unhashed(tx, signature); + (Self::Eip4844(tx), buf) + } + TxType::Eip7702 => { + let (tx, buf) = TxEip7702::from_compact(buf, buf.len()); + let tx = Signed::new_unhashed(tx, signature); + (Self::Eip7702(tx), buf) + } + } + } +} + +impl Compact + for EthereumTxEnvelope +{ + fn to_compact(&self, buf: &mut B) -> usize + where + B: BufMut + AsMut<[u8]>, + { + let start = buf.as_mut().len(); + + // Placeholder for bitflags. + // The first byte uses 4 bits as flags: IsCompressed[1bit], TxType[2bits], Signature[1bit] + buf.put_u8(0); + + let sig_bit = self.signature().to_compact(buf) as u8; + let zstd_bit = self.input().len() >= 32; + let tx_bits = self.tx_type().to_compact(buf) as u8; + let flags = sig_bit | (tx_bits << 1) | ((zstd_bit as u8) << 3); + + buf.as_mut()[start] = flags; + + if zstd_bit { + let mut tx_buf = Vec::with_capacity(256); + + self.to_tx_compact(&mut tx_buf); + + buf.put_slice( + &{ + #[cfg(feature = "std")] + { + reth_zstd_compressors::TRANSACTION_COMPRESSOR.with(|compressor| { + let mut compressor = compressor.borrow_mut(); + compressor.compress(&tx_buf) + }) + } + #[cfg(not(feature = "std"))] + { + let mut compressor = reth_zstd_compressors::create_tx_compressor(); + compressor.compress(&tx_buf) + } + } + .expect("Failed to compress"), + ); + } else { + self.to_tx_compact(buf); + }; + + buf.as_mut().len() - start + } + + fn from_compact(mut buf: &[u8], _len: usize) -> (Self, &[u8]) { + let flags = buf.get_u8() as usize; + + let sig_bit = flags & 1; + let tx_bits = (flags & 0b110) >> 1; + let zstd_bit = flags >> 3; + + let (signature, buf) = PrimitiveSignature::from_compact(buf, sig_bit); + let (tx_type, buf) = TxType::from_compact(buf, tx_bits); + + let (transaction, buf) = if zstd_bit != 0 { + #[cfg(feature = "std")] + { + reth_zstd_compressors::TRANSACTION_DECOMPRESSOR.with(|decompressor| { + let mut decompressor = decompressor.borrow_mut(); + + let (tx, _) = + Self::from_tx_compact(decompressor.decompress(buf), tx_type, signature); + + (tx, buf) + }) + } + #[cfg(not(feature = "std"))] + { + let mut decompressor = reth_zstd_compressors::create_tx_decompressor(); + + let (tx, _) = + Self::from_tx_compact(decompressor.decompress(buf), tx_type, signature); + + (tx, buf) + } + } else { + Self::from_tx_compact(buf, tx_type, signature) + }; + + (transaction, buf) + } +} diff --git a/crates/storage/codecs/src/alloy/transaction/mod.rs b/crates/storage/codecs/src/alloy/transaction/mod.rs index d8a0843248..d21e97f4c4 100644 --- a/crates/storage/codecs/src/alloy/transaction/mod.rs +++ b/crates/storage/codecs/src/alloy/transaction/mod.rs @@ -1,8 +1,10 @@ //! Compact implementation for transaction types use crate::Compact; -use alloy_consensus::{EthereumTypedTransaction, TxType, transaction::{TxEip7702, TxEip1559, TxEip2930, TxLegacy}}; +use alloy_consensus::{ + transaction::{RlpEcdsaEncodableTx, TxEip1559, TxEip2930, TxEip7702, TxLegacy}, + EthereumTypedTransaction, TxType, +}; use alloy_primitives::bytes::BufMut; -use alloy_consensus::transaction::RlpEcdsaEncodableTx; impl Compact for EthereumTypedTransaction where @@ -25,7 +27,7 @@ where fn from_compact(buf: &[u8], identifier: usize) -> (Self, &[u8]) { let (tx_type, buf) = TxType::from_compact(buf, identifier); - + match tx_type { TxType::Legacy => { let (tx, buf) = TxLegacy::from_compact(buf, buf.len()); @@ -51,16 +53,9 @@ where } } -cond_mod!( - eip1559, - eip2930, - eip4844, - eip7702, - legacy, - txtype -); - +cond_mod!(eip1559, eip2930, eip4844, eip7702, legacy, txtype); +mod ethereum; #[cfg(all(feature = "test-utils", feature = "op"))] pub mod optimism; #[cfg(all(not(feature = "test-utils"), feature = "op"))] @@ -75,14 +70,17 @@ mod tests { // this check is to ensure we do not inadvertently add too many fields to a struct which would // expand the flags field and break backwards compatibility - use alloy_primitives::hex; use crate::{ - alloy::{header::Header, transaction::{ - eip1559::TxEip1559, eip2930::TxEip2930, eip4844::TxEip4844, eip7702::TxEip7702, - legacy::TxLegacy, - }}, + alloy::{ + header::Header, + transaction::{ + eip1559::TxEip1559, eip2930::TxEip2930, eip4844::TxEip4844, eip7702::TxEip7702, + legacy::TxLegacy, + }, + }, test_utils::test_decode, }; + use alloy_primitives::hex; #[test] fn test_ensure_backwards_compatibility() {