use super::{error::TransactionConversionError, signature::recover_signer, TxEip7702};
use crate::{BlobTransaction, Transaction, TransactionSigned, TransactionSignedEcRecovered};
use alloy_consensus::{
constants::EIP4844_TX_TYPE_ID,
transaction::{TxEip1559, TxEip2930, TxEip4844, TxLegacy},
Signed, TxEip4844WithSidecar,
};
use alloy_eips::{
eip2718::{Decodable2718, Eip2718Result, Encodable2718},
eip2930::AccessList,
eip4844::BlobTransactionSidecar,
eip7702::SignedAuthorization,
};
use alloy_primitives::{
Address, Bytes, ChainId, PrimitiveSignature as Signature, TxHash, TxKind, B256, U256,
};
use alloy_rlp::{Decodable, Encodable, Error as RlpError, Header};
use bytes::Buf;
use derive_more::{AsRef, Deref};
use serde::{Deserialize, Serialize};
#[cfg_attr(any(test, feature = "reth-codec"), reth_codecs::add_arbitrary_tests)]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum PooledTransactionsElement {
Legacy(Signed<TxLegacy>),
Eip2930(Signed<TxEip2930>),
Eip1559(Signed<TxEip1559>),
Eip7702(Signed<TxEip7702>),
BlobTransaction(BlobTransaction),
}
impl PooledTransactionsElement {
pub fn try_from_broadcast(tx: TransactionSigned) -> Result<Self, TransactionSigned> {
let hash = tx.hash();
match tx {
TransactionSigned { transaction: Transaction::Legacy(tx), signature, .. } => {
Ok(Self::Legacy(Signed::new_unchecked(tx, signature, hash)))
}
TransactionSigned { transaction: Transaction::Eip2930(tx), signature, .. } => {
Ok(Self::Eip2930(Signed::new_unchecked(tx, signature, hash)))
}
TransactionSigned { transaction: Transaction::Eip1559(tx), signature, .. } => {
Ok(Self::Eip1559(Signed::new_unchecked(tx, signature, hash)))
}
TransactionSigned { transaction: Transaction::Eip7702(tx), signature, .. } => {
Ok(Self::Eip7702(Signed::new_unchecked(tx, signature, hash)))
}
tx @ TransactionSigned { transaction: Transaction::Eip4844(_), .. } => Err(tx),
#[cfg(feature = "optimism")]
tx @ TransactionSigned { transaction: Transaction::Deposit(_), .. } => Err(tx),
}
}
pub fn try_from_blob_transaction(
tx: TransactionSigned,
sidecar: BlobTransactionSidecar,
) -> Result<Self, TransactionSigned> {
let hash = tx.hash();
Ok(match tx {
TransactionSigned { transaction: Transaction::Eip4844(tx), signature, .. } => {
Self::BlobTransaction(BlobTransaction(Signed::new_unchecked(
TxEip4844WithSidecar { tx, sidecar },
signature,
hash,
)))
}
_ => return Err(tx),
})
}
pub fn signature_hash(&self) -> B256 {
match self {
Self::Legacy(tx) => tx.signature_hash(),
Self::Eip2930(tx) => tx.signature_hash(),
Self::Eip1559(tx) => tx.signature_hash(),
Self::Eip7702(tx) => tx.signature_hash(),
Self::BlobTransaction(tx) => tx.signature_hash(),
}
}
pub const fn hash(&self) -> &TxHash {
match self {
Self::Legacy(tx) => tx.hash(),
Self::Eip2930(tx) => tx.hash(),
Self::Eip1559(tx) => tx.hash(),
Self::Eip7702(tx) => tx.hash(),
Self::BlobTransaction(tx) => tx.0.hash(),
}
}
pub const fn signature(&self) -> &Signature {
match self {
Self::Legacy(tx) => tx.signature(),
Self::Eip2930(tx) => tx.signature(),
Self::Eip1559(tx) => tx.signature(),
Self::Eip7702(tx) => tx.signature(),
Self::BlobTransaction(tx) => tx.0.signature(),
}
}
pub fn recover_signer(&self) -> Option<Address> {
recover_signer(self.signature(), self.signature_hash())
}
pub fn try_into_ecrecovered(self) -> Result<PooledTransactionsElementEcRecovered, Self> {
match self.recover_signer() {
None => Err(self),
Some(signer) => Ok(PooledTransactionsElementEcRecovered { transaction: self, signer }),
}
}
pub fn into_ecrecovered_transaction(self, signer: Address) -> TransactionSignedEcRecovered {
TransactionSignedEcRecovered::from_signed_transaction(self.into_transaction(), signer)
}
pub fn into_transaction(self) -> TransactionSigned {
match self {
Self::Legacy(tx) => tx.into(),
Self::Eip2930(tx) => tx.into(),
Self::Eip1559(tx) => tx.into(),
Self::Eip7702(tx) => tx.into(),
Self::BlobTransaction(blob_tx) => blob_tx.into_parts().0,
}
}
#[inline]
pub const fn is_eip4844(&self) -> bool {
matches!(self, Self::BlobTransaction(_))
}
pub const fn as_legacy(&self) -> Option<&TxLegacy> {
match self {
Self::Legacy(tx) => Some(tx.tx()),
_ => None,
}
}
pub const fn as_eip2930(&self) -> Option<&TxEip2930> {
match self {
Self::Eip2930(tx) => Some(tx.tx()),
_ => None,
}
}
pub const fn as_eip1559(&self) -> Option<&TxEip1559> {
match self {
Self::Eip1559(tx) => Some(tx.tx()),
_ => None,
}
}
pub const fn as_eip4844(&self) -> Option<&TxEip4844> {
match self {
Self::BlobTransaction(tx) => Some(tx.0.tx().tx()),
_ => None,
}
}
pub const fn as_eip7702(&self) -> Option<&TxEip7702> {
match self {
Self::Eip7702(tx) => Some(tx.tx()),
_ => None,
}
}
pub fn blob_gas_used(&self) -> Option<u64> {
self.as_eip4844().map(TxEip4844::blob_gas)
}
}
impl Encodable for PooledTransactionsElement {
fn encode(&self, out: &mut dyn bytes::BufMut) {
self.network_encode(out);
}
fn length(&self) -> usize {
let mut payload_length = self.encode_2718_len();
if !self.is_legacy() {
payload_length += Header { list: false, payload_length }.length();
}
payload_length
}
}
impl Decodable for PooledTransactionsElement {
fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
if buf.is_empty() {
return Err(RlpError::InputTooShort)
}
let mut original_encoding = *buf;
let header = Header::decode(buf)?;
if header.list {
let tx = Self::fallback_decode(&mut original_encoding)?;
*buf = original_encoding;
Ok(tx)
} else {
let tx_type = *buf.first().ok_or(RlpError::InputTooShort)?;
let remaining_len = buf.len();
buf.advance(1);
let tx = Self::typed_decode(tx_type, buf).map_err(RlpError::from)?;
let bytes_consumed = remaining_len - buf.len();
if bytes_consumed != header.payload_length {
return Err(RlpError::UnexpectedLength)
}
Ok(tx)
}
}
}
impl Encodable2718 for PooledTransactionsElement {
fn type_flag(&self) -> Option<u8> {
match self {
Self::Legacy(_) => None,
Self::Eip2930(_) => Some(0x01),
Self::Eip1559(_) => Some(0x02),
Self::BlobTransaction(_) => Some(0x03),
Self::Eip7702(_) => Some(0x04),
}
}
fn encode_2718_len(&self) -> usize {
match self {
Self::Legacy(tx) => tx.eip2718_encoded_length(),
Self::Eip2930(tx) => tx.eip2718_encoded_length(),
Self::Eip1559(tx) => tx.eip2718_encoded_length(),
Self::Eip7702(tx) => tx.eip2718_encoded_length(),
Self::BlobTransaction(tx) => tx.eip2718_encoded_length(),
}
}
fn encode_2718(&self, out: &mut dyn alloy_rlp::BufMut) {
match self {
Self::Legacy(tx) => tx.eip2718_encode(out),
Self::Eip2930(tx) => tx.eip2718_encode(out),
Self::Eip1559(tx) => tx.eip2718_encode(out),
Self::Eip7702(tx) => tx.eip2718_encode(out),
Self::BlobTransaction(tx) => tx.eip2718_encode(out),
}
}
fn trie_hash(&self) -> B256 {
*self.hash()
}
}
impl Decodable2718 for PooledTransactionsElement {
fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self> {
match ty {
EIP4844_TX_TYPE_ID => {
let blob_tx = BlobTransaction::decode_inner(buf)?;
Ok(Self::BlobTransaction(blob_tx))
}
tx_type => {
let typed_tx = TransactionSigned::typed_decode(tx_type, buf)?;
let hash = typed_tx.hash();
match typed_tx.transaction {
Transaction::Legacy(_) => Err(RlpError::Custom(
"legacy transactions should not be a result of typed decoding",
).into()),
Transaction::Eip4844(_) => Err(RlpError::Custom(
"EIP-4844 transactions can only be decoded with transaction type 0x03",
).into()),
Transaction::Eip2930(tx) => Ok(Self::Eip2930 (
Signed::new_unchecked(tx, typed_tx.signature, hash)
)),
Transaction::Eip1559(tx) => Ok(Self::Eip1559( Signed::new_unchecked(tx, typed_tx.signature, hash))),
Transaction::Eip7702(tx) => Ok(Self::Eip7702( Signed::new_unchecked(tx, typed_tx.signature, hash))),
#[cfg(feature = "optimism")]
Transaction::Deposit(_) => Err(RlpError::Custom("Optimism deposit transaction cannot be decoded to PooledTransactionsElement").into())
}
}
}
}
fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result<Self> {
let (transaction, hash, signature) =
TransactionSigned::decode_rlp_legacy_transaction_tuple(buf)?;
Ok(Self::Legacy(Signed::new_unchecked(transaction, signature, hash)))
}
}
impl alloy_consensus::Transaction for PooledTransactionsElement {
fn chain_id(&self) -> Option<ChainId> {
match self {
Self::Legacy(tx) => tx.tx().chain_id(),
Self::Eip2930(tx) => tx.tx().chain_id(),
Self::Eip1559(tx) => tx.tx().chain_id(),
Self::Eip7702(tx) => tx.tx().chain_id(),
Self::BlobTransaction(tx) => tx.tx().chain_id(),
}
}
fn nonce(&self) -> u64 {
match self {
Self::Legacy(tx) => tx.tx().nonce(),
Self::Eip2930(tx) => tx.tx().nonce(),
Self::Eip1559(tx) => tx.tx().nonce(),
Self::Eip7702(tx) => tx.tx().nonce(),
Self::BlobTransaction(tx) => tx.tx().nonce(),
}
}
fn gas_limit(&self) -> u64 {
match self {
Self::Legacy(tx) => tx.tx().gas_limit(),
Self::Eip2930(tx) => tx.tx().gas_limit(),
Self::Eip1559(tx) => tx.tx().gas_limit(),
Self::Eip7702(tx) => tx.tx().gas_limit(),
Self::BlobTransaction(tx) => tx.tx().gas_limit(),
}
}
fn gas_price(&self) -> Option<u128> {
match self {
Self::Legacy(tx) => tx.tx().gas_price(),
Self::Eip2930(tx) => tx.tx().gas_price(),
Self::Eip1559(tx) => tx.tx().gas_price(),
Self::Eip7702(tx) => tx.tx().gas_price(),
Self::BlobTransaction(tx) => tx.tx().gas_price(),
}
}
fn max_fee_per_gas(&self) -> u128 {
match self {
Self::Legacy(tx) => tx.tx().max_fee_per_gas(),
Self::Eip2930(tx) => tx.tx().max_fee_per_gas(),
Self::Eip1559(tx) => tx.tx().max_fee_per_gas(),
Self::Eip7702(tx) => tx.tx().max_fee_per_gas(),
Self::BlobTransaction(tx) => tx.tx().max_fee_per_gas(),
}
}
fn max_priority_fee_per_gas(&self) -> Option<u128> {
match self {
Self::Legacy(tx) => tx.tx().max_priority_fee_per_gas(),
Self::Eip2930(tx) => tx.tx().max_priority_fee_per_gas(),
Self::Eip1559(tx) => tx.tx().max_priority_fee_per_gas(),
Self::Eip7702(tx) => tx.tx().max_priority_fee_per_gas(),
Self::BlobTransaction(tx) => tx.tx().max_priority_fee_per_gas(),
}
}
fn max_fee_per_blob_gas(&self) -> Option<u128> {
match self {
Self::Legacy(tx) => tx.tx().max_fee_per_blob_gas(),
Self::Eip2930(tx) => tx.tx().max_fee_per_blob_gas(),
Self::Eip1559(tx) => tx.tx().max_fee_per_blob_gas(),
Self::Eip7702(tx) => tx.tx().max_fee_per_blob_gas(),
Self::BlobTransaction(tx) => tx.tx().max_fee_per_blob_gas(),
}
}
fn priority_fee_or_price(&self) -> u128 {
match self {
Self::Legacy(tx) => tx.tx().priority_fee_or_price(),
Self::Eip2930(tx) => tx.tx().priority_fee_or_price(),
Self::Eip1559(tx) => tx.tx().priority_fee_or_price(),
Self::Eip7702(tx) => tx.tx().priority_fee_or_price(),
Self::BlobTransaction(tx) => tx.tx().priority_fee_or_price(),
}
}
fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
match self {
Self::Legacy(tx) => tx.tx().effective_gas_price(base_fee),
Self::Eip2930(tx) => tx.tx().effective_gas_price(base_fee),
Self::Eip1559(tx) => tx.tx().effective_gas_price(base_fee),
Self::Eip7702(tx) => tx.tx().effective_gas_price(base_fee),
Self::BlobTransaction(tx) => tx.tx().effective_gas_price(base_fee),
}
}
fn is_dynamic_fee(&self) -> bool {
match self {
Self::Legacy(tx) => tx.tx().is_dynamic_fee(),
Self::Eip2930(tx) => tx.tx().is_dynamic_fee(),
Self::Eip1559(tx) => tx.tx().is_dynamic_fee(),
Self::Eip7702(tx) => tx.tx().is_dynamic_fee(),
Self::BlobTransaction(tx) => tx.tx().is_dynamic_fee(),
}
}
fn kind(&self) -> TxKind {
match self {
Self::Legacy(tx) => tx.tx().kind(),
Self::Eip2930(tx) => tx.tx().kind(),
Self::Eip1559(tx) => tx.tx().kind(),
Self::Eip7702(tx) => tx.tx().kind(),
Self::BlobTransaction(tx) => tx.tx().kind(),
}
}
fn value(&self) -> U256 {
match self {
Self::Legacy(tx) => tx.tx().value(),
Self::Eip2930(tx) => tx.tx().value(),
Self::Eip1559(tx) => tx.tx().value(),
Self::Eip7702(tx) => tx.tx().value(),
Self::BlobTransaction(tx) => tx.tx().value(),
}
}
fn input(&self) -> &Bytes {
match self {
Self::Legacy(tx) => tx.tx().input(),
Self::Eip2930(tx) => tx.tx().input(),
Self::Eip1559(tx) => tx.tx().input(),
Self::Eip7702(tx) => tx.tx().input(),
Self::BlobTransaction(tx) => tx.tx().input(),
}
}
fn ty(&self) -> u8 {
match self {
Self::Legacy(tx) => tx.tx().ty(),
Self::Eip2930(tx) => tx.tx().ty(),
Self::Eip1559(tx) => tx.tx().ty(),
Self::Eip7702(tx) => tx.tx().ty(),
Self::BlobTransaction(tx) => tx.tx().ty(),
}
}
fn access_list(&self) -> Option<&AccessList> {
match self {
Self::Legacy(tx) => tx.tx().access_list(),
Self::Eip2930(tx) => tx.tx().access_list(),
Self::Eip1559(tx) => tx.tx().access_list(),
Self::Eip7702(tx) => tx.tx().access_list(),
Self::BlobTransaction(tx) => tx.tx().access_list(),
}
}
fn blob_versioned_hashes(&self) -> Option<&[B256]> {
match self {
Self::Legacy(tx) => tx.tx().blob_versioned_hashes(),
Self::Eip2930(tx) => tx.tx().blob_versioned_hashes(),
Self::Eip1559(tx) => tx.tx().blob_versioned_hashes(),
Self::Eip7702(tx) => tx.tx().blob_versioned_hashes(),
Self::BlobTransaction(tx) => tx.tx().blob_versioned_hashes(),
}
}
fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
match self {
Self::Legacy(tx) => tx.tx().authorization_list(),
Self::Eip2930(tx) => tx.tx().authorization_list(),
Self::Eip1559(tx) => tx.tx().authorization_list(),
Self::Eip7702(tx) => tx.tx().authorization_list(),
Self::BlobTransaction(tx) => tx.tx().authorization_list(),
}
}
}
impl TryFrom<TransactionSigned> for PooledTransactionsElement {
type Error = TransactionConversionError;
fn try_from(tx: TransactionSigned) -> Result<Self, Self::Error> {
Self::try_from_broadcast(tx).map_err(|_| TransactionConversionError::UnsupportedForP2P)
}
}
#[cfg(any(test, feature = "arbitrary"))]
impl<'a> arbitrary::Arbitrary<'a> for PooledTransactionsElement {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
let tx_signed = TransactionSigned::arbitrary(u)?;
match Self::try_from_broadcast(tx_signed) {
Ok(tx) => Ok(tx),
Err(tx) => {
let (tx, sig, hash) = tx.into_parts();
match tx {
Transaction::Eip4844(tx) => {
let sidecar = BlobTransactionSidecar::arbitrary(u)?;
Ok(Self::BlobTransaction(BlobTransaction(Signed::new_unchecked(
TxEip4844WithSidecar { tx, sidecar },
sig,
hash,
))))
}
_ => Err(arbitrary::Error::IncorrectFormat),
}
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, AsRef, Deref)]
pub struct PooledTransactionsElementEcRecovered {
signer: Address,
#[deref]
#[as_ref]
transaction: PooledTransactionsElement,
}
impl PooledTransactionsElementEcRecovered {
pub const fn signer(&self) -> Address {
self.signer
}
pub fn into_transaction(self) -> PooledTransactionsElement {
self.transaction
}
pub fn into_ecrecovered_transaction(self) -> TransactionSignedEcRecovered {
let (tx, signer) = self.into_components();
tx.into_ecrecovered_transaction(signer)
}
pub fn into_components(self) -> (PooledTransactionsElement, Address) {
(self.transaction, self.signer)
}
pub const fn from_signed_transaction(
transaction: PooledTransactionsElement,
signer: Address,
) -> Self {
Self { transaction, signer }
}
pub fn try_from_blob_transaction(
tx: TransactionSignedEcRecovered,
sidecar: BlobTransactionSidecar,
) -> Result<Self, TransactionSignedEcRecovered> {
let TransactionSignedEcRecovered { signer, signed_transaction } = tx;
let transaction =
PooledTransactionsElement::try_from_blob_transaction(signed_transaction, sidecar)
.map_err(|tx| TransactionSignedEcRecovered { signer, signed_transaction: tx })?;
Ok(Self { transaction, signer })
}
}
impl TryFrom<TransactionSignedEcRecovered> for PooledTransactionsElementEcRecovered {
type Error = TransactionConversionError;
fn try_from(tx: TransactionSignedEcRecovered) -> Result<Self, Self::Error> {
match PooledTransactionsElement::try_from(tx.signed_transaction) {
Ok(pooled_transaction) => {
Ok(Self { transaction: pooled_transaction, signer: tx.signer })
}
Err(_) => Err(TransactionConversionError::UnsupportedForP2P),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::{address, hex};
use assert_matches::assert_matches;
use bytes::Bytes;
#[test]
fn invalid_legacy_pooled_decoding_input_too_short() {
let input_too_short = [
&hex!("d90b0280808bc5cd028083c5cdfd9e407c56565656")[..],
&hex!("c10b02808083c5cd028883c5cdfd9e407c56565656"),
&hex!("c10b0280808bc5cd028083c5cdfd9e407c56565656"),
&hex!("d40b02808083c5cdeb8783c5acfd9e407c5656565656"),
&hex!("d30102808083c5cd02887dc5cdfd9e64fd9e407c56"),
];
for hex_data in &input_too_short {
let input_rlp = &mut &hex_data[..];
let res = PooledTransactionsElement::decode(input_rlp);
assert!(
res.is_err(),
"expected err after decoding rlp input: {:x?}",
Bytes::copy_from_slice(hex_data)
);
let input_rlp = &mut &hex_data[..];
let res = PooledTransactionsElement::decode_2718(input_rlp);
assert!(
res.is_err(),
"expected err after decoding enveloped rlp input: {:x?}",
Bytes::copy_from_slice(hex_data)
);
}
}
#[test]
fn decode_eip1559_enveloped() {
let data = hex!("02f903d382426882ba09832dc6c0848674742682ed9694714b6a4ea9b94a8a7d9fd362ed72630688c8898c80b90364492d24749189822d8512430d3f3ff7a2ede675ac08265c08e2c56ff6fdaa66dae1cdbe4a5d1d7809f3e99272d067364e597542ac0c369d69e22a6399c3e9bee5da4b07e3f3fdc34c32c3d88aa2268785f3e3f8086df0934b10ef92cfffc2e7f3d90f5e83302e31382e302d64657600000000000000000000000000000000000000000000569e75fc77c1a856f6daaf9e69d8a9566ca34aa47f9133711ce065a571af0cfd000000000000000000000000e1e210594771824dad216568b91c9cb4ceed361c00000000000000000000000000000000000000000000000000000000000546e00000000000000000000000000000000000000000000000000000000000e4e1c00000000000000000000000000000000000000000000000000000000065d6750c00000000000000000000000000000000000000000000000000000000000f288000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002cf600000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000000f1628e56fa6d8c50e5b984a58c0df14de31c7b857ce7ba499945b99252976a93d06dcda6776fc42167fbe71cb59f978f5ef5b12577a90b132d14d9c6efa528076f0161d7bf03643cfc5490ec5084f4a041db7f06c50bd97efa08907ba79ddcac8b890f24d12d8db31abbaaf18985d54f400449ee0559a4452afe53de5853ce090000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000064ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000c080a01428023fc54a27544abc421d5d017b9a7c5936ad501cbdecd0d9d12d04c1a033a0753104bbf1c87634d6ff3f0ffa0982710612306003eb022363b57994bdef445a"
);
let res = PooledTransactionsElement::decode_2718(&mut &data[..]).unwrap();
assert_eq!(
res.into_transaction().to(),
Some(address!("714b6a4ea9b94a8a7d9fd362ed72630688c8898c"))
);
}
#[test]
fn legacy_valid_pooled_decoding() {
let data = &hex!("d30b02808083c5cdeb8783c5acfd9e407c565656")[..];
let input_rlp = &mut &data[..];
let res = PooledTransactionsElement::decode(input_rlp);
assert_matches!(res, Ok(_tx));
assert!(input_rlp.is_empty());
let input_rlp = &mut &data[..];
let res = TransactionSigned::decode_rlp_legacy_transaction_tuple(input_rlp);
assert_matches!(res, Ok(_tx));
assert!(input_rlp.is_empty());
let res = PooledTransactionsElement::decode_2718(&mut &data[..]);
assert_matches!(res, Ok(_tx));
}
}