feat(storage): Implement Compact for EthereumTxEnvelope from alloy (#15122)

This commit is contained in:
Roman Hodulák
2025-03-22 08:30:54 +01:00
committed by GitHub
parent a34f45641a
commit 88f8be48b7
5 changed files with 243 additions and 20 deletions

View File

@@ -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<TransactionSigned> for TxEnvelope {
}
}
impl From<TransactionSigned> for EthereumTxEnvelope<TxEip4844> {
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<TransactionSigned> for Signed<Transaction> {
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::<TransactionSigned>()) {
let mut expected_buf = Vec::<u8>::new();
let expected_len = reth_tx.to_compact(&mut expected_buf);
let mut actual_but = Vec::<u8>::new();
let alloy_tx = EthereumTxEnvelope::<TxEip4844>::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::<TransactionSigned>()) {
let mut buf = Vec::<u8>::new();
let len = reth_tx.to_compact(&mut buf);
let (actual_tx, _) = EthereumTxEnvelope::<TxEip4844>::from_compact(&buf, len);
let expected_tx = EthereumTxEnvelope::<TxEip4844>::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

View File

@@ -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 = [

View File

@@ -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<Eip4844: Compact + Transaction> ToTxCompact for EthereumTxEnvelope<Eip4844> {
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<Eip4844: Compact + Transaction> FromTxCompact for EthereumTxEnvelope<Eip4844> {
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<Eip4844: Compact + RlpEcdsaEncodableTx + Transaction + Send + Sync> Compact
for EthereumTxEnvelope<Eip4844>
{
fn to_compact<B>(&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)
}
}

View File

@@ -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<Eip4844> Compact for EthereumTypedTransaction<Eip4844>
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() {