From fc4c037e60b623b81b296fe9242fa905ff36b89a Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 11 Jul 2024 07:45:47 +0200 Subject: [PATCH] feat: eip-7702 (#9214) Co-authored-by: Matthew Smith Co-authored-by: Matthias Seitz Co-authored-by: joshieDo <93316087+joshieDo@users.noreply.github.com> Co-authored-by: Dan Cline <6798349+Rjected@users.noreply.github.com> --- Cargo.toml | 2 +- crates/chainspec/src/spec.rs | 7 + crates/net/network/src/metrics.rs | 8 + .../network/src/transactions/validation.rs | 1 + crates/primitives/src/alloy_compat.rs | 28 ++ crates/primitives/src/lib.rs | 4 +- crates/primitives/src/receipt.rs | 7 + crates/primitives/src/transaction/compat.rs | 17 +- crates/primitives/src/transaction/eip7702.rs | 388 ++++++++++++++++++ crates/primitives/src/transaction/error.rs | 3 + crates/primitives/src/transaction/mod.rs | 114 ++++- crates/primitives/src/transaction/pooled.rs | 68 ++- .../primitives/src/transaction/signature.rs | 3 + crates/primitives/src/transaction/tx_type.rs | 28 +- crates/rpc/rpc-eth-api/Cargo.toml | 2 +- crates/rpc/rpc-eth-api/src/helpers/call.rs | 3 + crates/rpc/rpc-eth-types/src/error.rs | 1 + .../rpc-types-compat/src/transaction/mod.rs | 2 + crates/storage/codecs/Cargo.toml | 1 + .../codecs/src/alloy/authorization_list.rs | 96 +++++ crates/storage/codecs/src/alloy/mod.rs | 1 + crates/transaction-pool/src/error.rs | 3 +- .../transaction-pool/src/test_utils/mock.rs | 4 + crates/transaction-pool/src/traits.rs | 24 +- crates/transaction-pool/src/validate/eth.rs | 134 ++++-- 25 files changed, 877 insertions(+), 72 deletions(-) create mode 100644 crates/primitives/src/transaction/eip7702.rs create mode 100644 crates/storage/codecs/src/alloy/authorization_list.rs diff --git a/Cargo.toml b/Cargo.toml index 615ace70424e..f109a59c1c2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -386,7 +386,7 @@ revm-primitives = { version = "6.0.0", features = [ revm-inspectors = "0.4" # eth -alloy-chains = "0.1.15" +alloy-chains = "0.1.18" alloy-primitives = "0.7.2" alloy-dyn-abi = "0.7.2" alloy-sol-types = "0.7.2" diff --git a/crates/chainspec/src/spec.rs b/crates/chainspec/src/spec.rs index d8c52839fa87..96eaeda537a6 100644 --- a/crates/chainspec/src/spec.rs +++ b/crates/chainspec/src/spec.rs @@ -948,6 +948,13 @@ impl ChainSpecBuilder { self } + /// Enable Prague at genesis. + pub fn prague_activated(mut self) -> Self { + self = self.cancun_activated(); + self.hardforks.insert(EthereumHardfork::Prague, ForkCondition::Timestamp(0)); + self + } + /// Enable Bedrock at genesis #[cfg(feature = "optimism")] pub fn bedrock_activated(mut self) -> Self { diff --git a/crates/net/network/src/metrics.rs b/crates/net/network/src/metrics.rs index 6e2d11a3a176..71cd9f0795e8 100644 --- a/crates/net/network/src/metrics.rs +++ b/crates/net/network/src/metrics.rs @@ -342,6 +342,9 @@ pub struct AnnouncedTxTypesMetrics { /// Histogram for tracking frequency of EIP-4844 transaction type pub(crate) eip4844: Histogram, + + /// Histogram for tracking frequency of EIP-7702 transaction type + pub(crate) eip7702: Histogram, } #[derive(Debug, Default)] @@ -350,6 +353,7 @@ pub struct TxTypesCounter { pub(crate) eip2930: usize, pub(crate) eip1559: usize, pub(crate) eip4844: usize, + pub(crate) eip7702: usize, } impl TxTypesCounter { @@ -368,6 +372,9 @@ impl TxTypesCounter { TxType::Eip4844 => { self.eip4844 += 1; } + TxType::Eip7702 => { + self.eip7702 += 1; + } _ => {} } } @@ -381,5 +388,6 @@ impl AnnouncedTxTypesMetrics { self.eip2930.record(tx_types_counter.eip2930 as f64); self.eip1559.record(tx_types_counter.eip1559 as f64); self.eip4844.record(tx_types_counter.eip4844 as f64); + self.eip7702.record(tx_types_counter.eip7702 as f64); } } diff --git a/crates/net/network/src/transactions/validation.rs b/crates/net/network/src/transactions/validation.rs index 931cfdb9e514..f16d914fb0c0 100644 --- a/crates/net/network/src/transactions/validation.rs +++ b/crates/net/network/src/transactions/validation.rs @@ -331,6 +331,7 @@ impl FilterAnnouncement for EthMessageFilter { } } +// TODO(eip7702): update tests as needed #[cfg(test)] mod test { use super::*; diff --git a/crates/primitives/src/alloy_compat.rs b/crates/primitives/src/alloy_compat.rs index f257ee8bb89a..781327466d4a 100644 --- a/crates/primitives/src/alloy_compat.rs +++ b/crates/primitives/src/alloy_compat.rs @@ -181,6 +181,34 @@ impl TryFrom for Transaction { .ok_or(ConversionError::MissingMaxFeePerBlobGas)?, })) } + Some(TxType::Eip7702) => { + // this is currently unsupported as it is not present in alloy due to missing rpc + // specs + Err(ConversionError::Custom("Unimplemented".to_string())) + /* + // EIP-7702 + Ok(Transaction::Eip7702(TxEip7702 { + chain_id: tx.chain_id.ok_or(ConversionError::MissingChainId)?, + nonce: tx.nonce, + max_priority_fee_per_gas: tx + .max_priority_fee_per_gas + .ok_or(ConversionError::MissingMaxPriorityFeePerGas)?, + max_fee_per_gas: tx + .max_fee_per_gas + .ok_or(ConversionError::MissingMaxFeePerGas)?, + gas_limit: tx + .gas + .try_into() + .map_err(|_| ConversionError::Eip2718Error(RlpError::Overflow.into()))?, + to: tx.to.map_or(TxKind::Create, TxKind::Call), + value: tx.value, + access_list: tx.access_list.ok_or(ConversionError::MissingAccessList)?, + authorization_list: tx + .authorization_list + .ok_or(ConversionError::MissingAuthorizationList)?, + input: tx.input, + }))*/ + } #[cfg(feature = "optimism")] Some(TxType::Deposit) => { let fields = tx diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 29154d591e8e..eae739965662 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -68,8 +68,8 @@ pub use transaction::{ AccessList, AccessListItem, IntoRecoveredTransaction, InvalidTransactionError, Signature, Transaction, TransactionMeta, TransactionSigned, TransactionSignedEcRecovered, TransactionSignedNoHash, TryFromRecoveredTransaction, TxEip1559, TxEip2930, TxEip4844, - TxHashOrNumber, TxLegacy, TxType, EIP1559_TX_TYPE_ID, EIP2930_TX_TYPE_ID, EIP4844_TX_TYPE_ID, - LEGACY_TX_TYPE_ID, + TxEip7702, TxHashOrNumber, TxLegacy, TxType, EIP1559_TX_TYPE_ID, EIP2930_TX_TYPE_ID, + EIP4844_TX_TYPE_ID, EIP7702_TX_TYPE_ID, LEGACY_TX_TYPE_ID, }; // Re-exports diff --git a/crates/primitives/src/receipt.rs b/crates/primitives/src/receipt.rs index 1a9d917d62f7..90707e3430de 100644 --- a/crates/primitives/src/receipt.rs +++ b/crates/primitives/src/receipt.rs @@ -339,6 +339,10 @@ impl Decodable for ReceiptWithBloom { buf.advance(1); Self::decode_receipt(buf, TxType::Eip4844) } + 0x04 => { + buf.advance(1); + Self::decode_receipt(buf, TxType::Eip7702) + } #[cfg(feature = "optimism")] 0x7E => { buf.advance(1); @@ -471,6 +475,9 @@ impl<'a> ReceiptWithBloomEncoder<'a> { TxType::Eip4844 => { out.put_u8(0x03); } + TxType::Eip7702 => { + out.put_u8(0x04); + } #[cfg(feature = "optimism")] TxType::Deposit => { out.put_u8(0x7E); diff --git a/crates/primitives/src/transaction/compat.rs b/crates/primitives/src/transaction/compat.rs index 1f9c72be43d7..f249d369e03c 100644 --- a/crates/primitives/src/transaction/compat.rs +++ b/crates/primitives/src/transaction/compat.rs @@ -1,5 +1,5 @@ use crate::{Address, Transaction, TransactionSigned, TxKind, U256}; -use revm_primitives::TxEnv; +use revm_primitives::{AuthorizationList, TxEnv}; /// Implements behaviour to fill a [`TxEnv`] from another transaction. pub trait FillTxEnv { @@ -70,6 +70,21 @@ impl FillTxEnv for TransactionSigned { tx_env.blob_hashes.clone_from(&tx.blob_versioned_hashes); tx_env.max_fee_per_blob_gas = Some(U256::from(tx.max_fee_per_blob_gas)); } + Transaction::Eip7702(tx) => { + tx_env.gas_limit = tx.gas_limit; + tx_env.gas_price = U256::from(tx.max_fee_per_gas); + tx_env.gas_priority_fee = Some(U256::from(tx.max_priority_fee_per_gas)); + tx_env.transact_to = tx.to; + tx_env.value = tx.value; + tx_env.data = tx.input.clone(); + tx_env.chain_id = Some(tx.chain_id); + tx_env.nonce = Some(tx.nonce); + tx_env.access_list = tx.access_list.0.clone(); + tx_env.blob_hashes.clear(); + tx_env.max_fee_per_blob_gas.take(); + tx_env.authorization_list = + Some(AuthorizationList::Signed(tx.authorization_list.clone())); + } #[cfg(feature = "optimism")] Transaction::Deposit(tx) => { tx_env.access_list.clear(); diff --git a/crates/primitives/src/transaction/eip7702.rs b/crates/primitives/src/transaction/eip7702.rs new file mode 100644 index 000000000000..cd2070014e3c --- /dev/null +++ b/crates/primitives/src/transaction/eip7702.rs @@ -0,0 +1,388 @@ +use super::access_list::AccessList; +use crate::{keccak256, Bytes, ChainId, Signature, TxKind, TxType, B256, U256}; +use alloy_eips::eip7702::SignedAuthorization; +use alloy_rlp::{length_of_length, Decodable, Encodable, Header}; +use bytes::BytesMut; +use reth_codecs::{main_codec, Compact}; +use std::mem; + +/// [EIP-7702 Set Code Transaction](https://eips.ethereum.org/EIPS/eip-7702) +/// +/// Set EOA account code for one transaction +#[main_codec(no_arbitrary, add_arbitrary_tests)] +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct TxEip7702 { + /// Added as EIP-155: Simple replay attack protection + pub chain_id: ChainId, + /// A scalar value equal to the number of transactions sent by the sender; formally Tn. + pub nonce: u64, + /// A scalar value equal to the number of + /// Wei to be paid per unit of gas for all computation + /// costs incurred as a result of the execution of this transaction; formally Tp. + /// + /// As ethereum circulation is around 120mil eth as of 2022 that is around + /// 120000000000000000000000000 wei we are safe to use u128 as its max number is: + /// 340282366920938463463374607431768211455 + pub gas_limit: u64, + /// A scalar value equal to the maximum + /// amount of gas that should be used in executing + /// this transaction. This is paid up-front, before any + /// computation is done and may not be increased + /// later; formally Tg. + /// + /// As ethereum circulation is around 120mil eth as of 2022 that is around + /// 120000000000000000000000000 wei we are safe to use u128 as its max number is: + /// 340282366920938463463374607431768211455 + /// + /// This is also known as `GasFeeCap` + pub max_fee_per_gas: u128, + /// Max Priority fee that transaction is paying + /// + /// As ethereum circulation is around 120mil eth as of 2022 that is around + /// 120000000000000000000000000 wei we are safe to use u128 as its max number is: + /// 340282366920938463463374607431768211455 + /// + /// This is also known as `GasTipCap` + pub max_priority_fee_per_gas: u128, + /// The 160-bit address of the message call’s recipient or, for a contract creation + /// transaction, ∅, used here to denote the only member of B0 ; formally Tt. + pub to: TxKind, + /// A scalar value equal to the number of Wei to + /// be transferred to the message call’s recipient or, + /// in the case of contract creation, as an endowment + /// to the newly created account; formally Tv. + pub value: U256, + /// The accessList specifies a list of addresses and storage keys; + /// these addresses and storage keys are added into the `accessed_addresses` + /// and `accessed_storage_keys` global sets (introduced in EIP-2929). + /// A gas cost is charged, though at a discount relative to the cost of + /// accessing outside the list. + pub access_list: AccessList, + /// Authorizations are used to temporarily set the code of its signer to + /// the code referenced by `address`. These also include a `chain_id` (which + /// can be set to zero and not evaluated) as well as an optional `nonce`. + pub authorization_list: Vec>, + /// Input has two uses depending if the transaction `to` field is [`TxKind::Create`] or + /// [`TxKind::Call`]. + /// + /// Input as init code, or if `to` is [`TxKind::Create`]: An unlimited size byte array + /// specifying the EVM-code for the account initialisation procedure `CREATE` + /// + /// Input as data, or if `to` is [`TxKind::Call`]: An unlimited size byte array specifying the + /// input data of the message call, formally Td. + pub input: Bytes, +} + +impl TxEip7702 { + /// Returns the effective gas price for the given `base_fee`. + pub const fn effective_gas_price(&self, base_fee: Option) -> u128 { + match base_fee { + None => self.max_fee_per_gas, + Some(base_fee) => { + // if the tip is greater than the max priority fee per gas, set it to the max + // priority fee per gas + base fee + let tip = self.max_fee_per_gas.saturating_sub(base_fee as u128); + if tip > self.max_priority_fee_per_gas { + self.max_priority_fee_per_gas + base_fee as u128 + } else { + // otherwise return the max fee per gas + self.max_fee_per_gas + } + } + } + } + + /// Calculates a heuristic for the in-memory size of the [`TxEip7702`] transaction. + #[inline] + pub fn size(&self) -> usize { + mem::size_of::() + // chain_id + mem::size_of::() + // nonce + mem::size_of::() + // gas_price + mem::size_of::() + // gas_limit + self.to.size() + // to + mem::size_of::() + // value + self.access_list.size() + // access_list + mem::size_of::>() + * self.authorization_list.capacity() + // authorization_list + self.input.len() // input + } + + /// Decodes the inner [`TxEip7702`] fields from RLP bytes. + /// + /// NOTE: This assumes a RLP header has already been decoded, and _just_ decodes the following + /// RLP fields in the following order: + /// + /// - `chain_id` + /// - `nonce` + /// - `gas_price` + /// - `gas_limit` + /// - `to` + /// - `value` + /// - `data` (`input`) + /// - `access_list` + /// - `authorization_list` + pub(crate) fn decode_inner(buf: &mut &[u8]) -> alloy_rlp::Result { + Ok(Self { + chain_id: Decodable::decode(buf)?, + nonce: Decodable::decode(buf)?, + max_priority_fee_per_gas: Decodable::decode(buf)?, + max_fee_per_gas: Decodable::decode(buf)?, + gas_limit: Decodable::decode(buf)?, + to: Decodable::decode(buf)?, + value: Decodable::decode(buf)?, + input: Decodable::decode(buf)?, + access_list: Decodable::decode(buf)?, + authorization_list: Decodable::decode(buf)?, + }) + } + + /// Outputs the length of the transaction's fields, without a RLP header. + pub(crate) fn fields_len(&self) -> usize { + self.chain_id.length() + + self.nonce.length() + + self.max_priority_fee_per_gas.length() + + self.max_fee_per_gas.length() + + self.gas_limit.length() + + self.to.length() + + self.value.length() + + self.input.0.length() + + self.access_list.length() + + self.authorization_list.length() + } + + /// Encodes only the transaction's fields into the desired buffer, without a RLP header. + pub(crate) fn encode_fields(&self, out: &mut dyn bytes::BufMut) { + self.chain_id.encode(out); + self.nonce.encode(out); + self.max_priority_fee_per_gas.encode(out); + self.max_fee_per_gas.encode(out); + self.gas_limit.encode(out); + self.to.encode(out); + self.value.encode(out); + self.input.0.encode(out); + self.access_list.encode(out); + self.authorization_list.encode(out); + } + + /// Inner encoding function that is used for both rlp [`Encodable`] trait and for calculating + /// hash that for eip2718 does not require rlp header + /// + /// This encodes the transaction as: + /// `rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, + /// value, data, access_list, authorization_list, signature_y_parity, signature_r, + /// signature_s])` + pub(crate) fn encode_with_signature( + &self, + signature: &Signature, + out: &mut dyn bytes::BufMut, + with_header: bool, + ) { + let payload_length = self.fields_len() + signature.payload_len(); + if with_header { + Header { + list: false, + payload_length: 1 + length_of_length(payload_length) + payload_length, + } + .encode(out); + } + out.put_u8(self.tx_type() as u8); + let header = Header { list: true, payload_length }; + header.encode(out); + self.encode_fields(out); + signature.encode(out); + } + + /// Output the length of the RLP signed transaction encoding, _without_ a RLP string header. + pub(crate) fn payload_len_with_signature_without_header(&self, signature: &Signature) -> usize { + let payload_length = self.fields_len() + signature.payload_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } + + /// Output the length of the RLP signed transaction encoding. This encodes with a RLP header. + pub(crate) fn payload_len_with_signature(&self, signature: &Signature) -> usize { + let len = self.payload_len_with_signature_without_header(signature); + length_of_length(len) + len + } + + /// Get transaction type + pub(crate) const fn tx_type(&self) -> TxType { + TxType::Eip7702 + } + + /// Encodes the EIP-7702 transaction in RLP for signing. + /// + /// This encodes the transaction as: + /// `tx_type || rlp(chain_id, nonce, gas_price, gas_limit, to, value, input, access_list, + /// authorization_list)` + /// + /// Note that there is no rlp header before the transaction type byte. + pub(crate) fn encode_for_signing(&self, out: &mut dyn bytes::BufMut) { + out.put_u8(self.tx_type() as u8); + Header { list: true, payload_length: self.fields_len() }.encode(out); + self.encode_fields(out); + } + + /// Outputs the length of the signature RLP encoding for the transaction. + pub(crate) fn payload_len_for_signature(&self) -> usize { + let payload_length = self.fields_len(); + // 'transaction type byte length' + 'header length' + 'payload length' + 1 + length_of_length(payload_length) + payload_length + } + + /// Outputs the signature hash of the transaction by first encoding without a signature, then + /// hashing. + pub(crate) fn signature_hash(&self) -> B256 { + let mut buf = BytesMut::with_capacity(self.payload_len_for_signature()); + self.encode_for_signing(&mut buf); + keccak256(&buf) + } +} + +// TODO(onbjerg): This is temporary until we upstream `Arbitrary` to EIP-7702 types and `Signature` +// in alloy +#[cfg(any(test, feature = "arbitrary"))] +impl<'a> arbitrary::Arbitrary<'a> for TxEip7702 { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + use arbitrary::Arbitrary; + #[derive(Arbitrary)] + struct ArbitrarySignedAuth { + chain_id: ChainId, + address: alloy_primitives::Address, + nonce: Option, + parity: bool, + r: U256, + s: U256, + } + + let iter = u.arbitrary_iter::()?; + let mut authorization_list = Vec::new(); + for auth in iter { + let auth = auth?; + + let sig = alloy_primitives::Signature::from_rs_and_parity( + auth.r, + auth.s, + alloy_primitives::Parity::Parity(auth.parity), + ) + .unwrap_or_else(|_| { + // Give a default one if the randomly generated one failed + alloy_primitives::Signature::from_rs_and_parity( + alloy_primitives::b256!( + "1fd474b1f9404c0c5df43b7620119ffbc3a1c3f942c73b6e14e9f55255ed9b1d" + ) + .into(), + alloy_primitives::b256!( + "29aca24813279a901ec13b5f7bb53385fa1fc627b946592221417ff74a49600d" + ) + .into(), + false, + ) + .unwrap() + }); + + authorization_list.push( + alloy_eips::eip7702::Authorization { + chain_id: auth.chain_id, + address: auth.address, + nonce: auth.nonce.into(), + } + .into_signed(sig), + ); + } + + Ok(Self { + chain_id: Arbitrary::arbitrary(u)?, + nonce: Arbitrary::arbitrary(u)?, + gas_limit: Arbitrary::arbitrary(u)?, + max_fee_per_gas: Arbitrary::arbitrary(u)?, + max_priority_fee_per_gas: Arbitrary::arbitrary(u)?, + to: Arbitrary::arbitrary(u)?, + value: Arbitrary::arbitrary(u)?, + access_list: Arbitrary::arbitrary(u)?, + authorization_list, + input: Arbitrary::arbitrary(u)?, + }) + } +} + +// TODO(onbjerg): This is temporary until we upstream `Hash` for EIP-7702 types in alloy +impl std::hash::Hash for TxEip7702 { + fn hash(&self, state: &mut H) { + self.chain_id.hash(state); + self.nonce.hash(state); + self.gas_limit.hash(state); + self.max_fee_per_gas.hash(state); + self.max_priority_fee_per_gas.hash(state); + self.to.hash(state); + self.value.hash(state); + self.access_list.hash(state); + for auth in &self.authorization_list { + auth.signature_hash().hash(state); + } + self.input.hash(state); + } +} + +#[cfg(test)] +mod tests { + use super::TxEip7702; + use crate::{ + transaction::{signature::Signature, TxKind}, + Address, Bytes, Transaction, TransactionSigned, U256, + }; + use alloy_rlp::{Decodable, Encodable}; + + #[test] + fn test_decode_create() { + // tests that a contract creation tx encodes and decodes properly + let request = Transaction::Eip7702(TxEip7702 { + chain_id: 1u64, + nonce: 0, + max_fee_per_gas: 0x4a817c800, + max_priority_fee_per_gas: 0x3b9aca00, + gas_limit: 2, + to: TxKind::Create, + value: U256::ZERO, + input: Bytes::from(vec![1, 2]), + access_list: Default::default(), + authorization_list: Default::default(), + }); + let signature = Signature { odd_y_parity: true, r: U256::default(), s: U256::default() }; + let tx = TransactionSigned::from_transaction_and_signature(request, signature); + + let mut encoded = Vec::new(); + tx.encode(&mut encoded); + assert_eq!(encoded.len(), tx.length()); + + let decoded = TransactionSigned::decode(&mut &*encoded).unwrap(); + assert_eq!(decoded, tx); + } + + #[test] + fn test_decode_call() { + let request = Transaction::Eip7702(TxEip7702 { + chain_id: 1u64, + nonce: 0, + max_fee_per_gas: 0x4a817c800, + max_priority_fee_per_gas: 0x3b9aca00, + gas_limit: 2, + to: Address::default().into(), + value: U256::ZERO, + input: Bytes::from(vec![1, 2]), + access_list: Default::default(), + authorization_list: Default::default(), + }); + + let signature = Signature { odd_y_parity: true, r: U256::default(), s: U256::default() }; + + let tx = TransactionSigned::from_transaction_and_signature(request, signature); + + let mut encoded = Vec::new(); + tx.encode(&mut encoded); + assert_eq!(encoded.len(), tx.length()); + + let decoded = TransactionSigned::decode(&mut &*encoded).unwrap(); + assert_eq!(decoded, tx); + } +} diff --git a/crates/primitives/src/transaction/error.rs b/crates/primitives/src/transaction/error.rs index c5199dda5d9a..a75704cd2db2 100644 --- a/crates/primitives/src/transaction/error.rs +++ b/crates/primitives/src/transaction/error.rs @@ -29,6 +29,9 @@ pub enum InvalidTransactionError { /// The transaction requires EIP-4844 which is not enabled currently. #[error("EIP-4844 transactions are disabled")] Eip4844Disabled, + /// The transaction requires EIP-7702 which is not enabled currently. + #[error("EIP-7702 transactions are disabled")] + Eip7702Disabled, /// Thrown if a transaction is not supported in the current network configuration. #[error("transaction type not supported")] TxTypeNotSupported, diff --git a/crates/primitives/src/transaction/mod.rs b/crates/primitives/src/transaction/mod.rs index b6f8ba72c71d..b5f1f1f7c472 100644 --- a/crates/primitives/src/transaction/mod.rs +++ b/crates/primitives/src/transaction/mod.rs @@ -4,6 +4,7 @@ use crate::compression::{TRANSACTION_COMPRESSOR, TRANSACTION_DECOMPRESSOR}; use crate::{keccak256, Address, BlockHashOrNumber, Bytes, TxHash, TxKind, B256, U256}; +use alloy_eips::eip7702::SignedAuthorization; use alloy_rlp::{ Decodable, Encodable, Error as RlpError, Header, EMPTY_LIST_CODE, EMPTY_STRING_CODE, }; @@ -19,6 +20,7 @@ pub use access_list::{AccessList, AccessListItem}; pub use eip1559::TxEip1559; pub use eip2930::TxEip2930; pub use eip4844::TxEip4844; +pub use eip7702::TxEip7702; pub use error::{ InvalidTransactionError, TransactionConversionError, TryFromRecoveredTransactionError, @@ -35,7 +37,8 @@ pub use sidecar::{BlobTransaction, BlobTransactionSidecar}; pub use compat::FillTxEnv; pub use signature::{extract_chain_id, Signature}; pub use tx_type::{ - TxType, EIP1559_TX_TYPE_ID, EIP2930_TX_TYPE_ID, EIP4844_TX_TYPE_ID, LEGACY_TX_TYPE_ID, + TxType, EIP1559_TX_TYPE_ID, EIP2930_TX_TYPE_ID, EIP4844_TX_TYPE_ID, EIP7702_TX_TYPE_ID, + LEGACY_TX_TYPE_ID, }; pub use variant::TransactionSignedVariant; @@ -44,6 +47,7 @@ mod compat; mod eip1559; mod eip2930; mod eip4844; +mod eip7702; mod error; mod legacy; mod meta; @@ -122,6 +126,12 @@ pub enum Transaction { /// EIP-4844, also known as proto-danksharding, implements the framework and logic of /// danksharding, introducing new transaction formats and verification rules. Eip4844(TxEip4844), + /// EOA Set Code Transactions ([EIP-7702](https://eips.ethereum.org/EIPS/eip-7702)), type `0x4`. + /// + /// EOA Set Code Transactions give the ability to temporarily set contract code for an + /// EOA for a single transaction. This allows for temporarily adding smart contract + /// functionality to the EOA. + Eip7702(TxEip7702), /// Optimism deposit transaction. #[cfg(feature = "optimism")] Deposit(TxDeposit), @@ -138,6 +148,7 @@ impl Transaction { Self::Eip2930(tx) => tx.signature_hash(), Self::Eip1559(tx) => tx.signature_hash(), Self::Eip4844(tx) => tx.signature_hash(), + Self::Eip7702(tx) => tx.signature_hash(), #[cfg(feature = "optimism")] Self::Deposit(_) => B256::ZERO, } @@ -149,7 +160,8 @@ impl Transaction { Self::Legacy(TxLegacy { chain_id, .. }) => *chain_id, Self::Eip2930(TxEip2930 { chain_id, .. }) | Self::Eip1559(TxEip1559 { chain_id, .. }) | - Self::Eip4844(TxEip4844 { chain_id, .. }) => Some(*chain_id), + Self::Eip4844(TxEip4844 { chain_id, .. }) | + Self::Eip7702(TxEip7702 { chain_id, .. }) => Some(*chain_id), #[cfg(feature = "optimism")] Self::Deposit(_) => None, } @@ -161,7 +173,8 @@ impl Transaction { Self::Legacy(TxLegacy { chain_id: ref mut c, .. }) => *c = Some(chain_id), Self::Eip2930(TxEip2930 { chain_id: ref mut c, .. }) | Self::Eip1559(TxEip1559 { chain_id: ref mut c, .. }) | - Self::Eip4844(TxEip4844 { chain_id: ref mut c, .. }) => *c = chain_id, + Self::Eip4844(TxEip4844 { chain_id: ref mut c, .. }) | + Self::Eip7702(TxEip7702 { chain_id: ref mut c, .. }) => *c = chain_id, #[cfg(feature = "optimism")] Self::Deposit(_) => { /* noop */ } } @@ -173,7 +186,8 @@ impl Transaction { match self { Self::Legacy(TxLegacy { to, .. }) | Self::Eip2930(TxEip2930 { to, .. }) | - Self::Eip1559(TxEip1559 { to, .. }) => *to, + Self::Eip1559(TxEip1559 { to, .. }) | + Self::Eip7702(TxEip7702 { to, .. }) => *to, Self::Eip4844(TxEip4844 { to, .. }) => TxKind::Call(*to), #[cfg(feature = "optimism")] Self::Deposit(TxDeposit { to, .. }) => *to, @@ -195,6 +209,7 @@ impl Transaction { Self::Eip2930(access_list_tx) => access_list_tx.tx_type(), Self::Eip1559(dynamic_fee_tx) => dynamic_fee_tx.tx_type(), Self::Eip4844(blob_tx) => blob_tx.tx_type(), + Self::Eip7702(set_code_tx) => set_code_tx.tx_type(), #[cfg(feature = "optimism")] Self::Deposit(deposit_tx) => deposit_tx.tx_type(), } @@ -206,7 +221,8 @@ impl Transaction { Self::Legacy(TxLegacy { value, .. }) | Self::Eip2930(TxEip2930 { value, .. }) | Self::Eip1559(TxEip1559 { value, .. }) | - Self::Eip4844(TxEip4844 { value, .. }) => value, + Self::Eip4844(TxEip4844 { value, .. }) | + Self::Eip7702(TxEip7702 { value, .. }) => value, #[cfg(feature = "optimism")] Self::Deposit(TxDeposit { value, .. }) => value, } @@ -218,7 +234,8 @@ impl Transaction { Self::Legacy(TxLegacy { nonce, .. }) | Self::Eip2930(TxEip2930 { nonce, .. }) | Self::Eip1559(TxEip1559 { nonce, .. }) | - Self::Eip4844(TxEip4844 { nonce, .. }) => *nonce, + Self::Eip4844(TxEip4844 { nonce, .. }) | + Self::Eip7702(TxEip7702 { nonce, .. }) => *nonce, // Deposit transactions do not have nonces. #[cfg(feature = "optimism")] Self::Deposit(_) => 0, @@ -234,18 +251,32 @@ impl Transaction { Self::Eip2930(tx) => Some(&tx.access_list), Self::Eip1559(tx) => Some(&tx.access_list), Self::Eip4844(tx) => Some(&tx.access_list), + Self::Eip7702(tx) => Some(&tx.access_list), #[cfg(feature = "optimism")] Self::Deposit(_) => None, } } + /// Returns the [`SignedAuthorization`] list of the transaction. + /// + /// Returns `None` if this transaction is not EIP-7702. + pub fn authorization_list( + &self, + ) -> Option<&[SignedAuthorization]> { + match self { + Self::Eip7702(tx) => Some(&tx.authorization_list), + _ => None, + } + } + /// Get the gas limit of the transaction. pub const fn gas_limit(&self) -> u64 { match self { Self::Legacy(TxLegacy { gas_limit, .. }) | Self::Eip2930(TxEip2930 { gas_limit, .. }) | Self::Eip1559(TxEip1559 { gas_limit, .. }) | - Self::Eip4844(TxEip4844 { gas_limit, .. }) => *gas_limit, + Self::Eip4844(TxEip4844 { gas_limit, .. }) | + Self::Eip7702(TxEip7702 { gas_limit, .. }) => *gas_limit, #[cfg(feature = "optimism")] Self::Deposit(TxDeposit { gas_limit, .. }) => *gas_limit, } @@ -255,7 +286,7 @@ impl Transaction { pub const fn is_dynamic_fee(&self) -> bool { match self { Self::Legacy(_) | Self::Eip2930(_) => false, - Self::Eip1559(_) | Self::Eip4844(_) => true, + Self::Eip1559(_) | Self::Eip4844(_) | Self::Eip7702(_) => true, #[cfg(feature = "optimism")] Self::Deposit(_) => false, } @@ -269,7 +300,8 @@ impl Transaction { Self::Legacy(TxLegacy { gas_price, .. }) | Self::Eip2930(TxEip2930 { gas_price, .. }) => *gas_price, Self::Eip1559(TxEip1559 { max_fee_per_gas, .. }) | - Self::Eip4844(TxEip4844 { max_fee_per_gas, .. }) => *max_fee_per_gas, + Self::Eip4844(TxEip4844 { max_fee_per_gas, .. }) | + Self::Eip7702(TxEip7702 { max_fee_per_gas, .. }) => *max_fee_per_gas, // Deposit transactions buy their L2 gas on L1 and, as such, the L2 gas is not // refundable. #[cfg(feature = "optimism")] @@ -285,7 +317,8 @@ impl Transaction { match self { Self::Legacy(_) | Self::Eip2930(_) => None, Self::Eip1559(TxEip1559 { max_priority_fee_per_gas, .. }) | - Self::Eip4844(TxEip4844 { max_priority_fee_per_gas, .. }) => { + Self::Eip4844(TxEip4844 { max_priority_fee_per_gas, .. }) | + Self::Eip7702(TxEip7702 { max_priority_fee_per_gas, .. }) => { Some(*max_priority_fee_per_gas) } #[cfg(feature = "optimism")] @@ -293,13 +326,13 @@ impl Transaction { } } - /// Blob versioned hashes for eip4844 transaction, for legacy,eip1559 and eip2930 transactions - /// this is `None` + /// Blob versioned hashes for eip4844 transaction, for legacy, eip1559, eip2930 and eip7702 + /// transactions this is `None` /// /// This is also commonly referred to as the "blob versioned hashes" (`BlobVersionedHashes`). pub fn blob_versioned_hashes(&self) -> Option> { match self { - Self::Legacy(_) | Self::Eip2930(_) | Self::Eip1559(_) => None, + Self::Legacy(_) | Self::Eip2930(_) | Self::Eip1559(_) | Self::Eip7702(_) => None, Self::Eip4844(TxEip4844 { blob_versioned_hashes, .. }) => { Some(blob_versioned_hashes.to_vec()) } @@ -341,7 +374,8 @@ impl Transaction { Self::Legacy(TxLegacy { gas_price, .. }) | Self::Eip2930(TxEip2930 { gas_price, .. }) => *gas_price, Self::Eip1559(TxEip1559 { max_priority_fee_per_gas, .. }) | - Self::Eip4844(TxEip4844 { max_priority_fee_per_gas, .. }) => *max_priority_fee_per_gas, + Self::Eip4844(TxEip4844 { max_priority_fee_per_gas, .. }) | + Self::Eip7702(TxEip7702 { max_priority_fee_per_gas, .. }) => *max_priority_fee_per_gas, #[cfg(feature = "optimism")] Self::Deposit(_) => 0, } @@ -356,6 +390,7 @@ impl Transaction { Self::Eip2930(tx) => tx.gas_price, Self::Eip1559(dynamic_tx) => dynamic_tx.effective_gas_price(base_fee), Self::Eip4844(dynamic_tx) => dynamic_tx.effective_gas_price(base_fee), + Self::Eip7702(dynamic_tx) => dynamic_tx.effective_gas_price(base_fee), #[cfg(feature = "optimism")] Self::Deposit(_) => 0, } @@ -398,7 +433,8 @@ impl Transaction { Self::Legacy(TxLegacy { input, .. }) | Self::Eip2930(TxEip2930 { input, .. }) | Self::Eip1559(TxEip1559 { input, .. }) | - Self::Eip4844(TxEip4844 { input, .. }) => input, + Self::Eip4844(TxEip4844 { input, .. }) | + Self::Eip7702(TxEip7702 { input, .. }) => input, #[cfg(feature = "optimism")] Self::Deposit(TxDeposit { input, .. }) => input, } @@ -466,6 +502,9 @@ impl Transaction { dynamic_fee_tx.encode_with_signature(signature, out, with_header) } Self::Eip4844(blob_tx) => blob_tx.encode_with_signature(signature, out, with_header), + Self::Eip7702(set_code_tx) => { + set_code_tx.encode_with_signature(signature, out, with_header) + } #[cfg(feature = "optimism")] Self::Deposit(deposit_tx) => deposit_tx.encode(out, with_header), } @@ -478,6 +517,7 @@ impl Transaction { Self::Eip2930(tx) => tx.gas_limit = gas_limit, Self::Eip1559(tx) => tx.gas_limit = gas_limit, Self::Eip4844(tx) => tx.gas_limit = gas_limit, + Self::Eip7702(tx) => tx.gas_limit = gas_limit, #[cfg(feature = "optimism")] Self::Deposit(tx) => tx.gas_limit = gas_limit, } @@ -490,6 +530,7 @@ impl Transaction { Self::Eip2930(tx) => tx.nonce = nonce, Self::Eip1559(tx) => tx.nonce = nonce, Self::Eip4844(tx) => tx.nonce = nonce, + Self::Eip7702(tx) => tx.nonce = nonce, #[cfg(feature = "optimism")] Self::Deposit(_) => { /* noop */ } } @@ -502,6 +543,7 @@ impl Transaction { Self::Eip2930(tx) => tx.value = value, Self::Eip1559(tx) => tx.value = value, Self::Eip4844(tx) => tx.value = value, + Self::Eip7702(tx) => tx.value = value, #[cfg(feature = "optimism")] Self::Deposit(tx) => tx.value = value, } @@ -514,6 +556,7 @@ impl Transaction { Self::Eip2930(tx) => tx.input = input, Self::Eip1559(tx) => tx.input = input, Self::Eip4844(tx) => tx.input = input, + Self::Eip7702(tx) => tx.input = input, #[cfg(feature = "optimism")] Self::Deposit(tx) => tx.input = input, } @@ -527,6 +570,7 @@ impl Transaction { Self::Eip2930(tx) => tx.size(), Self::Eip1559(tx) => tx.size(), Self::Eip4844(tx) => tx.size(), + Self::Eip7702(tx) => tx.size(), #[cfg(feature = "optimism")] Self::Deposit(tx) => tx.size(), } @@ -556,6 +600,12 @@ impl Transaction { matches!(self, Self::Eip4844(_)) } + /// Returns true if the transaction is an EIP-7702 transaction. + #[inline] + pub const fn is_eip7702(&self) -> bool { + matches!(self, Self::Eip7702(_)) + } + /// Returns the [`TxLegacy`] variant if the transaction is a legacy transaction. pub const fn as_legacy(&self) -> Option<&TxLegacy> { match self { @@ -587,6 +637,14 @@ impl Transaction { _ => None, } } + + /// Returns the [`TxEip7702`] variant if the transaction is an EIP-7702 transaction. + pub const fn as_eip7702(&self) -> Option<&TxEip7702> { + match self { + Self::Eip7702(tx) => Some(tx), + _ => None, + } + } } impl From for Transaction { @@ -613,6 +671,12 @@ impl From for Transaction { } } +impl From for Transaction { + fn from(tx: TxEip7702) -> Self { + Self::Eip7702(tx) + } +} + impl Compact for Transaction { // Serializes the TxType to the buffer if necessary, returning 2 bits of the type as an // identifier instead of the length. @@ -634,6 +698,9 @@ impl Compact for Transaction { Self::Eip4844(tx) => { tx.to_compact(buf); } + Self::Eip7702(tx) => { + tx.to_compact(buf); + } #[cfg(feature = "optimism")] Self::Deposit(tx) => { tx.to_compact(buf); @@ -676,6 +743,10 @@ impl Compact for Transaction { let (tx, buf) = TxEip4844::from_compact(buf, buf.len()); (Self::Eip4844(tx), buf) } + 4 => { + let (tx, buf) = TxEip7702::from_compact(buf, buf.len()); + (Self::Eip7702(tx), buf) + } #[cfg(feature = "optimism")] 126 => { let (tx, buf) = TxDeposit::from_compact(buf, buf.len()); @@ -712,6 +783,9 @@ impl Encodable for Transaction { Self::Eip4844(blob_tx) => { blob_tx.encode_for_signing(out); } + Self::Eip7702(set_code_tx) => { + set_code_tx.encode_for_signing(out); + } #[cfg(feature = "optimism")] Self::Deposit(deposit_tx) => { deposit_tx.encode(out, true); @@ -725,6 +799,7 @@ impl Encodable for Transaction { Self::Eip2930(access_list_tx) => access_list_tx.payload_len_for_signature(), Self::Eip1559(dynamic_fee_tx) => dynamic_fee_tx.payload_len_for_signature(), Self::Eip4844(blob_tx) => blob_tx.payload_len_for_signature(), + Self::Eip7702(set_code_tx) => set_code_tx.payload_len_for_signature(), #[cfg(feature = "optimism")] Self::Deposit(deposit_tx) => deposit_tx.payload_len(), } @@ -1161,6 +1236,9 @@ impl TransactionSigned { dynamic_fee_tx.payload_len_with_signature(&self.signature) } Transaction::Eip4844(blob_tx) => blob_tx.payload_len_with_signature(&self.signature), + Transaction::Eip7702(set_code_tx) => { + set_code_tx.payload_len_with_signature(&self.signature) + } #[cfg(feature = "optimism")] Transaction::Deposit(deposit_tx) => deposit_tx.payload_len(), } @@ -1250,7 +1328,7 @@ impl TransactionSigned { Ok(signed) } - /// Decodes en enveloped EIP-2718 typed transaction. + /// Decodes an enveloped EIP-2718 typed transaction. /// /// This should _only_ be used internally in general transaction decoding methods, /// which have already ensured that the input is a typed transaction with the following format: @@ -1287,6 +1365,7 @@ impl TransactionSigned { TxType::Eip2930 => Transaction::Eip2930(TxEip2930::decode_inner(data)?), TxType::Eip1559 => Transaction::Eip1559(TxEip1559::decode_inner(data)?), TxType::Eip4844 => Transaction::Eip4844(TxEip4844::decode_inner(data)?), + TxType::Eip7702 => Transaction::Eip7702(TxEip7702::decode_inner(data)?), #[cfg(feature = "optimism")] TxType::Deposit => Transaction::Deposit(TxDeposit::decode_inner(data)?), TxType::Legacy => return Err(RlpError::Custom("unexpected legacy tx type")), @@ -1363,6 +1442,9 @@ impl TransactionSigned { Transaction::Eip4844(blob_tx) => { blob_tx.payload_len_with_signature_without_header(&self.signature) } + Transaction::Eip7702(set_code_tx) => { + set_code_tx.payload_len_with_signature_without_header(&self.signature) + } #[cfg(feature = "optimism")] Transaction::Deposit(deposit_tx) => deposit_tx.payload_len_without_header(), } diff --git a/crates/primitives/src/transaction/pooled.rs b/crates/primitives/src/transaction/pooled.rs index a72d22d9f907..7114f131cef7 100644 --- a/crates/primitives/src/transaction/pooled.rs +++ b/crates/primitives/src/transaction/pooled.rs @@ -1,7 +1,7 @@ //! Defines the types for blob transactions, legacy, and other EIP-2718 transactions included in a //! response to `GetPooledTransactions`. -use super::error::TransactionConversionError; +use super::{error::TransactionConversionError, TxEip7702}; use crate::{ Address, BlobTransaction, BlobTransactionSidecar, Bytes, Signature, Transaction, TransactionSigned, TransactionSignedEcRecovered, TxEip1559, TxEip2930, TxEip4844, TxHash, @@ -48,6 +48,15 @@ pub enum PooledTransactionsElement { /// The hash of the transaction hash: TxHash, }, + /// An EIP-7702 typed transaction + Eip7702 { + /// The inner transaction + transaction: TxEip7702, + /// The signature + signature: Signature, + /// The hash of the transaction + hash: TxHash, + }, /// A blob transaction, which includes the transaction, blob data, commitments, and proofs. BlobTransaction(BlobTransaction), } @@ -69,6 +78,9 @@ impl PooledTransactionsElement { TransactionSigned { transaction: Transaction::Eip1559(tx), signature, hash } => { Ok(Self::Eip1559 { transaction: tx, signature, hash }) } + TransactionSigned { transaction: Transaction::Eip7702(tx), signature, hash } => { + Ok(Self::Eip7702 { transaction: tx, signature, hash }) + } // Not supported because missing blob sidecar tx @ TransactionSigned { transaction: Transaction::Eip4844(_), .. } => Err(tx), #[cfg(feature = "optimism")] @@ -105,6 +117,7 @@ impl PooledTransactionsElement { Self::Legacy { transaction, .. } => transaction.signature_hash(), Self::Eip2930 { transaction, .. } => transaction.signature_hash(), Self::Eip1559 { transaction, .. } => transaction.signature_hash(), + Self::Eip7702 { transaction, .. } => transaction.signature_hash(), Self::BlobTransaction(blob_tx) => blob_tx.transaction.signature_hash(), } } @@ -112,9 +125,10 @@ impl PooledTransactionsElement { /// Reference to transaction hash. Used to identify transaction. pub const fn hash(&self) -> &TxHash { match self { - Self::Legacy { hash, .. } | Self::Eip2930 { hash, .. } | Self::Eip1559 { hash, .. } => { - hash - } + Self::Legacy { hash, .. } | + Self::Eip2930 { hash, .. } | + Self::Eip1559 { hash, .. } | + Self::Eip7702 { hash, .. } => hash, Self::BlobTransaction(tx) => &tx.hash, } } @@ -124,7 +138,8 @@ impl PooledTransactionsElement { match self { Self::Legacy { signature, .. } | Self::Eip2930 { signature, .. } | - Self::Eip1559 { signature, .. } => signature, + Self::Eip1559 { signature, .. } | + Self::Eip7702 { signature, .. } => signature, Self::BlobTransaction(blob_tx) => &blob_tx.signature, } } @@ -135,6 +150,7 @@ impl PooledTransactionsElement { Self::Legacy { transaction, .. } => transaction.nonce, Self::Eip2930 { transaction, .. } => transaction.nonce, Self::Eip1559 { transaction, .. } => transaction.nonce, + Self::Eip7702 { transaction, .. } => transaction.nonce, Self::BlobTransaction(blob_tx) => blob_tx.transaction.nonce, } } @@ -238,6 +254,11 @@ impl PooledTransactionsElement { signature: typed_tx.signature, hash: typed_tx.hash, }), + Transaction::Eip7702(tx) => {Ok(Self::Eip7702 { + transaction: tx, + signature: typed_tx.signature, + hash: typed_tx.hash, + })}, #[cfg(feature = "optimism")] Transaction::Deposit(_) => Err(RlpError::Custom("Optimism deposit transaction cannot be decoded to PooledTransactionsElement")) } @@ -267,6 +288,11 @@ impl PooledTransactionsElement { signature, hash, }, + Self::Eip7702 { transaction, signature, hash } => TransactionSigned { + transaction: Transaction::Eip7702(transaction), + signature, + hash, + }, Self::BlobTransaction(blob_tx) => blob_tx.into_parts().0, } } @@ -286,6 +312,10 @@ impl PooledTransactionsElement { // method computes the payload len without a RLP header transaction.payload_len_with_signature_without_header(signature) } + Self::Eip7702 { transaction, signature, .. } => { + // method computes the payload len without a RLP header + transaction.payload_len_with_signature_without_header(signature) + } Self::BlobTransaction(blob_tx) => { // the encoding does not use a header, so we set `with_header` to false blob_tx.payload_len_with_type(false) @@ -316,6 +346,7 @@ impl PooledTransactionsElement { // - EIP-2930: TxEip2930::encode_with_signature // - EIP-1559: TxEip1559::encode_with_signature // - EIP-4844: BlobTransaction::encode_with_type_inner + // - EIP-7702: TxEip7702::encode_with_signature match self { Self::Legacy { transaction, signature, .. } => { transaction.encode_with_signature(signature, out) @@ -326,6 +357,9 @@ impl PooledTransactionsElement { Self::Eip1559 { transaction, signature, .. } => { transaction.encode_with_signature(signature, out, false) } + Self::Eip7702 { transaction, signature, .. } => { + transaction.encode_with_signature(signature, out, false) + } Self::BlobTransaction(blob_tx) => { // The inner encoding is used with `with_header` set to true, making the final // encoding: @@ -373,6 +407,14 @@ impl PooledTransactionsElement { } } + /// Returns the [`TxEip7702`] variant if the transaction is an EIP-7702 transaction. + pub const fn as_eip7702(&self) -> Option<&TxEip7702> { + match self { + Self::Eip7702 { transaction, .. } => Some(transaction), + _ => None, + } + } + /// Returns the blob gas used for all blobs of the EIP-4844 transaction if it is an EIP-4844 /// transaction. /// @@ -402,6 +444,7 @@ impl PooledTransactionsElement { match self { Self::Legacy { .. } | Self::Eip2930 { .. } => None, Self::Eip1559 { transaction, .. } => Some(transaction.max_priority_fee_per_gas), + Self::Eip7702 { transaction, .. } => Some(transaction.max_priority_fee_per_gas), Self::BlobTransaction(tx) => Some(tx.transaction.max_priority_fee_per_gas), } } @@ -414,6 +457,7 @@ impl PooledTransactionsElement { Self::Legacy { transaction, .. } => transaction.gas_price, Self::Eip2930 { transaction, .. } => transaction.gas_price, Self::Eip1559 { transaction, .. } => transaction.max_fee_per_gas, + Self::Eip7702 { transaction, .. } => transaction.max_fee_per_gas, Self::BlobTransaction(tx) => tx.transaction.max_fee_per_gas, } } @@ -433,6 +477,7 @@ impl Encodable for PooledTransactionsElement { // - EIP-2930: TxEip2930::encode_with_signature // - EIP-1559: TxEip1559::encode_with_signature // - EIP-4844: BlobTransaction::encode_with_type_inner + // - EIP-7702: TxEip7702::encode_with_signature match self { Self::Legacy { transaction, signature, .. } => { transaction.encode_with_signature(signature, out) @@ -445,6 +490,10 @@ impl Encodable for PooledTransactionsElement { // encodes with string header transaction.encode_with_signature(signature, out, true) } + Self::Eip7702 { transaction, signature, .. } => { + // encodes with string header + transaction.encode_with_signature(signature, out, true) + } Self::BlobTransaction(blob_tx) => { // The inner encoding is used with `with_header` set to true, making the final // encoding: @@ -468,6 +517,10 @@ impl Encodable for PooledTransactionsElement { // method computes the payload len with a RLP header transaction.payload_len_with_signature(signature) } + Self::Eip7702 { transaction, signature, .. } => { + // method computes the payload len with a RLP header + transaction.payload_len_with_signature(signature) + } Self::BlobTransaction(blob_tx) => { // the encoding uses a header, so we set `with_header` to true blob_tx.payload_len_with_type(true) @@ -573,6 +626,11 @@ impl Decodable for PooledTransactionsElement { signature: typed_tx.signature, hash: typed_tx.hash, }), + Transaction::Eip7702(tx) => Ok(Self::Eip7702 { + transaction: tx, + signature: typed_tx.signature, + hash: typed_tx.hash, + }), #[cfg(feature = "optimism")] Transaction::Deposit(_) => Err(RlpError::Custom("Optimism deposit transaction cannot be decoded to PooledTransactionsElement")) } diff --git a/crates/primitives/src/transaction/signature.rs b/crates/primitives/src/transaction/signature.rs index 077858a3c579..b74c41f55a32 100644 --- a/crates/primitives/src/transaction/signature.rs +++ b/crates/primitives/src/transaction/signature.rs @@ -25,6 +25,9 @@ pub struct Signature { /// The S field of the signature; the point on the curve. pub s: U256, /// yParity: Signature Y parity; formally Ty + /// + /// WARNING: if it's deprecated in favor of `alloy_primitives::Signature` be sure that parity + /// storage deser matches. pub odd_y_parity: bool, } diff --git a/crates/primitives/src/transaction/tx_type.rs b/crates/primitives/src/transaction/tx_type.rs index 7530fda086f8..a482212aec06 100644 --- a/crates/primitives/src/transaction/tx_type.rs +++ b/crates/primitives/src/transaction/tx_type.rs @@ -17,6 +17,9 @@ pub const EIP1559_TX_TYPE_ID: u8 = 2; /// Identifier for [`TxEip4844`](crate::TxEip4844) transaction. pub const EIP4844_TX_TYPE_ID: u8 = 3; +/// Identifier for [`TxEip7702`](crate::TxEip7702) transaction. +pub const EIP7702_TX_TYPE_ID: u8 = 4; + /// Identifier for [`TxDeposit`](crate::TxDeposit) transaction. #[cfg(feature = "optimism")] pub const DEPOSIT_TX_TYPE_ID: u8 = 126; @@ -42,6 +45,8 @@ pub enum TxType { Eip1559 = 2_isize, /// Shard Blob Transactions - EIP-4844 Eip4844 = 3_isize, + /// EOA Contract Code Transactions - EIP-7702 + Eip7702 = 4_isize, /// Optimism Deposit transaction. #[cfg(feature = "optimism")] Deposit = 126_isize, @@ -49,13 +54,13 @@ pub enum TxType { impl TxType { /// The max type reserved by an EIP. - pub const MAX_RESERVED_EIP: Self = Self::Eip4844; + pub const MAX_RESERVED_EIP: Self = Self::Eip7702; /// Check if the transaction type has an access list. pub const fn has_access_list(&self) -> bool { match self { Self::Legacy => false, - Self::Eip2930 | Self::Eip1559 | Self::Eip4844 => true, + Self::Eip2930 | Self::Eip1559 | Self::Eip4844 | Self::Eip7702 => true, #[cfg(feature = "optimism")] Self::Deposit => false, } @@ -69,6 +74,7 @@ impl From for u8 { TxType::Eip2930 => EIP2930_TX_TYPE_ID, TxType::Eip1559 => EIP1559_TX_TYPE_ID, TxType::Eip4844 => EIP4844_TX_TYPE_ID, + TxType::Eip7702 => EIP7702_TX_TYPE_ID, #[cfg(feature = "optimism")] TxType::Deposit => DEPOSIT_TX_TYPE_ID, } @@ -98,6 +104,8 @@ impl TryFrom for TxType { return Ok(Self::Eip1559) } else if value == Self::Eip4844 { return Ok(Self::Eip4844) + } else if value == Self::Eip7702 { + return Ok(Self::Eip7702) } Err("invalid tx type") @@ -137,6 +145,10 @@ impl Compact for TxType { buf.put_u8(self as u8); 3 } + Self::Eip7702 => { + buf.put_u8(self as u8); + 3 + } #[cfg(feature = "optimism")] Self::Deposit => { buf.put_u8(self as u8); @@ -158,6 +170,7 @@ impl Compact for TxType { let extended_identifier = buf.get_u8(); match extended_identifier { EIP4844_TX_TYPE_ID => Self::Eip4844, + EIP7702_TX_TYPE_ID => Self::Eip7702, #[cfg(feature = "optimism")] DEPOSIT_TX_TYPE_ID => Self::Deposit, _ => panic!("Unsupported TxType identifier: {extended_identifier}"), @@ -222,12 +235,15 @@ mod tests { // Test for EIP4844 transaction assert_eq!(TxType::try_from(U64::from(3)).unwrap(), TxType::Eip4844); + // Test for EIP7702 transaction + assert_eq!(TxType::try_from(U64::from(4)).unwrap(), TxType::Eip7702); + // Test for Deposit transaction #[cfg(feature = "optimism")] assert_eq!(TxType::try_from(U64::from(126)).unwrap(), TxType::Deposit); // For transactions with unsupported values - assert!(TxType::try_from(U64::from(4)).is_err()); + assert!(TxType::try_from(U64::from(5)).is_err()); } #[test] @@ -237,6 +253,7 @@ mod tests { (TxType::Eip2930, 1, vec![]), (TxType::Eip1559, 2, vec![]), (TxType::Eip4844, 3, vec![EIP4844_TX_TYPE_ID]), + (TxType::Eip7702, 3, vec![EIP7702_TX_TYPE_ID]), #[cfg(feature = "optimism")] (TxType::Deposit, 3, vec![DEPOSIT_TX_TYPE_ID]), ]; @@ -259,6 +276,7 @@ mod tests { (TxType::Eip2930, 1, vec![]), (TxType::Eip1559, 2, vec![]), (TxType::Eip4844, 3, vec![EIP4844_TX_TYPE_ID]), + (TxType::Eip7702, 3, vec![EIP7702_TX_TYPE_ID]), #[cfg(feature = "optimism")] (TxType::Deposit, 3, vec![DEPOSIT_TX_TYPE_ID]), ]; @@ -291,6 +309,10 @@ mod tests { let tx_type = TxType::decode(&mut &[3u8][..]).unwrap(); assert_eq!(tx_type, TxType::Eip4844); + // Test for EIP7702 transaction + let tx_type = TxType::decode(&mut &[4u8][..]).unwrap(); + assert_eq!(tx_type, TxType::Eip7702); + // Test random byte not in range let buf = [rand::thread_rng().gen_range(4..=u8::MAX)]; println!("{buf:?}"); diff --git a/crates/rpc/rpc-eth-api/Cargo.toml b/crates/rpc/rpc-eth-api/Cargo.toml index fb8d84b3f4b7..b1295d69e5f6 100644 --- a/crates/rpc/rpc-eth-api/Cargo.toml +++ b/crates/rpc/rpc-eth-api/Cargo.toml @@ -54,4 +54,4 @@ optimism = [ "revm/optimism", "reth-provider/optimism", "reth-rpc-eth-types/optimism" -] \ No newline at end of file +] diff --git a/crates/rpc/rpc-eth-api/src/helpers/call.rs b/crates/rpc/rpc-eth-api/src/helpers/call.rs index 932138f3929d..4ee669691be8 100644 --- a/crates/rpc/rpc-eth-api/src/helpers/call.rs +++ b/crates/rpc/rpc-eth-api/src/helpers/call.rs @@ -807,6 +807,7 @@ pub trait Call: LoadState + SpawnBlocking { chain_id, blob_versioned_hashes, max_fee_per_blob_gas, + // authorization_list, .. } = request; @@ -838,7 +839,9 @@ pub trait Call: LoadState + SpawnBlocking { // EIP-4844 fields blob_hashes: blob_versioned_hashes.unwrap_or_default(), max_fee_per_blob_gas, + // EIP-7702 fields authorization_list: None, + // authorization_list: TODO #[cfg(feature = "optimism")] optimism: OptimismFields { enveloped_tx: Some(Bytes::new()), ..Default::default() }, }; diff --git a/crates/rpc/rpc-eth-types/src/error.rs b/crates/rpc/rpc-eth-types/src/error.rs index 95d989a19a81..d635a2e8ff04 100644 --- a/crates/rpc/rpc-eth-types/src/error.rs +++ b/crates/rpc/rpc-eth-types/src/error.rs @@ -498,6 +498,7 @@ impl From for RpcInvalidTransactionErr InvalidTransactionError::Eip2930Disabled | InvalidTransactionError::Eip1559Disabled | InvalidTransactionError::Eip4844Disabled | + InvalidTransactionError::Eip7702Disabled | InvalidTransactionError::TxTypeNotSupported => Self::TxTypeNotSupported, InvalidTransactionError::GasUintOverflow => Self::GasUintOverflow, InvalidTransactionError::GasTooLow => Self::GasTooLow, diff --git a/crates/rpc/rpc-types-compat/src/transaction/mod.rs b/crates/rpc/rpc-types-compat/src/transaction/mod.rs index ed768db4c71c..62eb2b8e49d2 100644 --- a/crates/rpc/rpc-types-compat/src/transaction/mod.rs +++ b/crates/rpc/rpc-types-compat/src/transaction/mod.rs @@ -73,6 +73,7 @@ fn fill( let chain_id = signed_tx.chain_id(); let blob_versioned_hashes = signed_tx.blob_versioned_hashes(); let access_list = signed_tx.access_list().cloned(); + let _authorization_list = signed_tx.authorization_list(); let signature = from_primitive_signature(*signed_tx.signature(), signed_tx.tx_type(), signed_tx.chain_id()); @@ -124,6 +125,7 @@ pub fn transaction_to_call_request(tx: TransactionSignedEcRecovered) -> Transact let chain_id = tx.transaction.chain_id(); let access_list = tx.transaction.access_list().cloned(); let max_fee_per_blob_gas = tx.transaction.max_fee_per_blob_gas(); + let _authorization_list = tx.transaction.authorization_list(); let blob_versioned_hashes = tx.transaction.blob_versioned_hashes(); let tx_type = tx.transaction.tx_type(); diff --git a/crates/storage/codecs/Cargo.toml b/crates/storage/codecs/Cargo.toml index 6f286a95b411..43efd45d6600 100644 --- a/crates/storage/codecs/Cargo.toml +++ b/crates/storage/codecs/Cargo.toml @@ -31,6 +31,7 @@ alloy-eips = { workspace = true, default-features = false, features = [ "serde", ] } alloy-primitives = { workspace = true, features = ["arbitrary", "serde"] } +alloy-consensus = { workspace = true, features = ["arbitrary"] } test-fuzz.workspace = true serde_json.workspace = true diff --git a/crates/storage/codecs/src/alloy/authorization_list.rs b/crates/storage/codecs/src/alloy/authorization_list.rs new file mode 100644 index 000000000000..308c597a1b3d --- /dev/null +++ b/crates/storage/codecs/src/alloy/authorization_list.rs @@ -0,0 +1,96 @@ +use crate::Compact; +use alloy_eips::eip7702::{Authorization as AlloyAuthorization, SignedAuthorization}; +use alloy_primitives::{Address, ChainId, U256}; +use bytes::Buf; +use reth_codecs_derive::main_codec; + +/// Authorization acts as bridge which simplifies Compact implementation for AlloyAuthorization. +/// +/// Notice: Make sure this struct is 1:1 with `alloy_eips::eip7702::Authorization` +#[main_codec] +#[derive(Debug, Clone, PartialEq, Eq, Default)] +struct Authorization { + chain_id: ChainId, + address: Address, + nonce: Option, +} + +impl Compact for AlloyAuthorization { + fn to_compact(self, buf: &mut B) -> usize + where + B: bytes::BufMut + AsMut<[u8]>, + { + let authorization = + Authorization { chain_id: self.chain_id, address: self.address, nonce: self.nonce() }; + authorization.to_compact(buf) + } + + fn from_compact(buf: &[u8], len: usize) -> (Self, &[u8]) { + let (authorization, buf) = Authorization::from_compact(buf, len); + let alloy_authorization = Self { + chain_id: authorization.chain_id, + address: authorization.address, + nonce: authorization.nonce.into(), + }; + (alloy_authorization, buf) + } +} + +impl Compact for SignedAuthorization { + fn to_compact(self, buf: &mut B) -> usize + where + B: bytes::BufMut + AsMut<[u8]>, + { + let (auth, signature) = self.into_parts(); + let (v, r, s) = (signature.v(), signature.r(), signature.s()); + buf.put_u8(v.y_parity_byte()); + buf.put_slice(r.as_le_slice()); + buf.put_slice(s.as_le_slice()); + + // to_compact doesn't write the len to buffer. + // By placing it as last, we don't need to store it either. + 1 + 32 + 32 + auth.to_compact(buf) + } + + fn from_compact(mut buf: &[u8], len: usize) -> (Self, &[u8]) { + let y = alloy_primitives::Parity::Parity(buf.get_u8() == 1); + let r = U256::from_le_slice(&buf[0..32]); + buf.advance(32); + let s = U256::from_le_slice(&buf[0..32]); + buf.advance(32); + + let signature = alloy_primitives::Signature::from_rs_and_parity(r, s, y) + .expect("invalid authorization signature"); + let (auth, buf) = AlloyAuthorization::from_compact(buf, len); + + (auth.into_signed(signature), buf) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::{address, b256}; + + #[test] + fn test_roundtrip_compact_authorization_list_item() { + let authorization = AlloyAuthorization { + chain_id: 1, + address: address!("dac17f958d2ee523a2206206994597c13d831ec7"), + nonce: None.into(), + } + .into_signed( + alloy_primitives::Signature::from_rs_and_parity( + b256!("1fd474b1f9404c0c5df43b7620119ffbc3a1c3f942c73b6e14e9f55255ed9b1d").into(), + b256!("29aca24813279a901ec13b5f7bb53385fa1fc627b946592221417ff74a49600d").into(), + false, + ) + .unwrap(), + ); + let mut compacted_authorization = Vec::::new(); + let len = authorization.clone().to_compact(&mut compacted_authorization); + let (decoded_authorization, _) = + SignedAuthorization::from_compact(&compacted_authorization, len); + assert_eq!(authorization, decoded_authorization); + } +} diff --git a/crates/storage/codecs/src/alloy/mod.rs b/crates/storage/codecs/src/alloy/mod.rs index b36f4c94312b..bb0a9cd42e3b 100644 --- a/crates/storage/codecs/src/alloy/mod.rs +++ b/crates/storage/codecs/src/alloy/mod.rs @@ -1,4 +1,5 @@ mod access_list; +mod authorization_list; mod genesis_account; mod log; mod request; diff --git a/crates/transaction-pool/src/error.rs b/crates/transaction-pool/src/error.rs index c6ed4b2e0700..606b50559f8c 100644 --- a/crates/transaction-pool/src/error.rs +++ b/crates/transaction-pool/src/error.rs @@ -241,7 +241,8 @@ impl InvalidPoolTransactionError { } InvalidTransactionError::Eip2930Disabled | InvalidTransactionError::Eip1559Disabled | - InvalidTransactionError::Eip4844Disabled => { + InvalidTransactionError::Eip4844Disabled | + InvalidTransactionError::Eip7702Disabled => { // settings false } diff --git a/crates/transaction-pool/src/test_utils/mock.rs b/crates/transaction-pool/src/test_utils/mock.rs index 72c7a4121730..ad811217a1d2 100644 --- a/crates/transaction-pool/src/test_utils/mock.rs +++ b/crates/transaction-pool/src/test_utils/mock.rs @@ -749,6 +749,10 @@ impl EthPoolTransaction for MockTransaction { _ => Err(BlobTransactionValidationError::NotBlobTransaction(self.tx_type())), } } + + fn authorization_count(&self) -> usize { + 0 + } } impl TryFromRecoveredTransaction for MockTransaction { diff --git a/crates/transaction-pool/src/traits.rs b/crates/transaction-pool/src/traits.rs index e43c45e17e82..b53c31358b19 100644 --- a/crates/transaction-pool/src/traits.rs +++ b/crates/transaction-pool/src/traits.rs @@ -14,7 +14,7 @@ use reth_primitives::{ BlobTransactionSidecar, BlobTransactionValidationError, FromRecoveredPooledTransaction, IntoRecoveredTransaction, PooledTransactionsElement, PooledTransactionsElementEcRecovered, SealedBlock, Transaction, TransactionSignedEcRecovered, TryFromRecoveredTransaction, TxHash, - TxKind, B256, EIP1559_TX_TYPE_ID, EIP4844_TX_TYPE_ID, U256, + TxKind, B256, EIP1559_TX_TYPE_ID, EIP4844_TX_TYPE_ID, EIP7702_TX_TYPE_ID, U256, }; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -842,6 +842,11 @@ pub trait PoolTransaction: self.tx_type() == EIP4844_TX_TYPE_ID } + /// Returns true if the transaction is an EIP-7702 transaction. + fn is_eip7702(&self) -> bool { + self.tx_type() == EIP7702_TX_TYPE_ID + } + /// Returns the length of the rlp encoded transaction object /// /// Note: Implementations should cache this value. @@ -866,6 +871,9 @@ pub trait EthPoolTransaction: PoolTransaction { blob: &BlobTransactionSidecar, settings: &KzgSettings, ) -> Result<(), BlobTransactionValidationError>; + + /// Returns the number of authorizations this transaction has. + fn authorization_count(&self) -> usize; } /// The default [`PoolTransaction`] for the [Pool](crate::Pool) for Ethereum. @@ -938,6 +946,9 @@ impl EthPooledTransaction { blob_sidecar = EthBlobTransactionSidecar::Missing; U256::from(t.max_fee_per_gas).saturating_mul(U256::from(t.gas_limit)) } + Transaction::Eip7702(t) => { + U256::from(t.max_fee_per_gas).saturating_mul(U256::from(t.gas_limit)) + } _ => U256::ZERO, }; let mut cost = transaction.value(); @@ -1024,6 +1035,7 @@ impl PoolTransaction for EthPooledTransaction { Transaction::Eip2930(tx) => tx.gas_price, Transaction::Eip1559(tx) => tx.max_fee_per_gas, Transaction::Eip4844(tx) => tx.max_fee_per_gas, + Transaction::Eip7702(tx) => tx.max_fee_per_gas, _ => 0, } } @@ -1041,6 +1053,7 @@ impl PoolTransaction for EthPooledTransaction { Transaction::Legacy(_) | Transaction::Eip2930(_) => None, Transaction::Eip1559(tx) => Some(tx.max_priority_fee_per_gas), Transaction::Eip4844(tx) => Some(tx.max_priority_fee_per_gas), + Transaction::Eip7702(tx) => Some(tx.max_priority_fee_per_gas), _ => None, } } @@ -1120,6 +1133,13 @@ impl EthPoolTransaction for EthPooledTransaction { _ => Err(BlobTransactionValidationError::NotBlobTransaction(self.tx_type())), } } + + fn authorization_count(&self) -> usize { + match &self.transaction.transaction { + Transaction::Eip7702(tx) => tx.authorization_list.len(), + _ => 0, + } + } } impl TryFromRecoveredTransaction for EthPooledTransaction { @@ -1130,7 +1150,7 @@ impl TryFromRecoveredTransaction for EthPooledTransaction { ) -> Result { // ensure we can handle the transaction type and its format match tx.tx_type() as u8 { - 0..=EIP1559_TX_TYPE_ID => { + 0..=EIP1559_TX_TYPE_ID | EIP7702_TX_TYPE_ID => { // supported } EIP4844_TX_TYPE_ID => { diff --git a/crates/transaction-pool/src/validate/eth.rs b/crates/transaction-pool/src/validate/eth.rs index eef090bcddb0..f393519b2103 100644 --- a/crates/transaction-pool/src/validate/eth.rs +++ b/crates/transaction-pool/src/validate/eth.rs @@ -12,14 +12,14 @@ use crate::{ use reth_chainspec::{ChainSpec, EthereumHardforks}; use reth_primitives::{ constants::{eip4844::MAX_BLOBS_PER_BLOCK, ETHEREUM_BLOCK_GAS_LIMIT}, - GotExpected, InvalidTransactionError, SealedBlock, TxKind, EIP1559_TX_TYPE_ID, - EIP2930_TX_TYPE_ID, EIP4844_TX_TYPE_ID, LEGACY_TX_TYPE_ID, + GotExpected, InvalidTransactionError, SealedBlock, EIP1559_TX_TYPE_ID, EIP2930_TX_TYPE_ID, + EIP4844_TX_TYPE_ID, EIP7702_TX_TYPE_ID, LEGACY_TX_TYPE_ID, }; use reth_provider::{AccountReader, BlockReaderIdExt, StateProviderFactory}; use reth_tasks::TaskSpawner; use revm::{ interpreter::gas::validate_initial_tx_gas, - primitives::{AccessListItem, EnvKzgSettings, SpecId}, + primitives::{EnvKzgSettings, SpecId}, }; use std::{ marker::PhantomData, @@ -119,6 +119,8 @@ pub(crate) struct EthTransactionValidatorInner { eip1559: bool, /// Fork indicator whether we are using EIP-4844 blob transactions. eip4844: bool, + /// Fork indicator whether we are using EIP-7702 type transactions. + eip7702: bool, /// The current max gas limit block_gas_limit: u64, /// Minimum priority fee to enforce for acceptance into the pool. @@ -185,6 +187,15 @@ where ) } } + EIP7702_TX_TYPE_ID => { + // Reject EIP-7702 transactions. + if !self.eip7702 { + return TransactionValidationOutcome::Invalid( + transaction, + InvalidTransactionError::Eip7702Disabled.into(), + ) + } + } _ => { return TransactionValidationOutcome::Invalid( @@ -255,9 +266,17 @@ where } } - // intrinsic gas checks - let is_shanghai = self.fork_tracker.is_shanghai_activated(); - if let Err(err) = ensure_intrinsic_gas(&transaction, is_shanghai) { + if transaction.is_eip7702() { + // Cancun fork is required for 7702 txs + if !self.fork_tracker.is_prague_activated() { + return TransactionValidationOutcome::Invalid( + transaction, + InvalidTransactionError::TxTypeNotSupported.into(), + ) + } + } + + if let Err(err) = ensure_intrinsic_gas(&transaction, &self.fork_tracker) { return TransactionValidationOutcome::Invalid(transaction, err) } @@ -407,6 +426,10 @@ where if self.chain_spec.is_shanghai_active_at_timestamp(new_tip_block.timestamp) { self.fork_tracker.shanghai.store(true, std::sync::atomic::Ordering::Relaxed); } + + if self.chain_spec.is_prague_active_at_timestamp(new_tip_block.timestamp) { + self.fork_tracker.prague.store(true, std::sync::atomic::Ordering::Relaxed); + } } } @@ -418,12 +441,16 @@ pub struct EthTransactionValidatorBuilder { shanghai: bool, /// Fork indicator whether we are in the Cancun hardfork. cancun: bool, + /// Fork indicator whether we are in the Cancun hardfork. + prague: bool, /// Whether using EIP-2718 type transactions is allowed eip2718: bool, /// Whether using EIP-1559 type transactions is allowed eip1559: bool, /// Whether using EIP-4844 type transactions is allowed eip4844: bool, + /// Whether using EIP-7702 type transactions is allowed + eip7702: bool, /// The current max gas limit block_gas_limit: u64, /// Minimum priority fee to enforce for acceptance into the pool. @@ -464,12 +491,16 @@ impl EthTransactionValidatorBuilder { eip2718: true, eip1559: true, eip4844: true, + eip7702: true, // shanghai is activated by default shanghai: true, // cancun is activated by default cancun: true, + + // prague not yet activated + prague: false, } } @@ -504,6 +535,17 @@ impl EthTransactionValidatorBuilder { self } + /// Disables the Prague fork. + pub const fn no_prague(self) -> Self { + self.set_prague(false) + } + + /// Set the Prague fork. + pub const fn set_prague(mut self, prague: bool) -> Self { + self.prague = prague; + self + } + /// Disables the support for EIP-2718 transactions. pub const fn no_eip2718(self) -> Self { self.set_eip2718(false) @@ -591,9 +633,11 @@ impl EthTransactionValidatorBuilder { chain_spec, shanghai, cancun, + prague, eip2718, eip1559, eip4844, + eip7702, block_gas_limit, minimum_priority_fee, kzg_settings, @@ -602,8 +646,11 @@ impl EthTransactionValidatorBuilder { .. } = self; - let fork_tracker = - ForkTracker { shanghai: AtomicBool::new(shanghai), cancun: AtomicBool::new(cancun) }; + let fork_tracker = ForkTracker { + shanghai: AtomicBool::new(shanghai), + cancun: AtomicBool::new(cancun), + prague: AtomicBool::new(prague), + }; let inner = EthTransactionValidatorInner { chain_spec, @@ -612,6 +659,7 @@ impl EthTransactionValidatorBuilder { eip1559, fork_tracker, eip4844, + eip7702, block_gas_limit, minimum_priority_fee, blob_store: Box::new(blob_store), @@ -670,23 +718,30 @@ impl EthTransactionValidatorBuilder { /// Keeps track of whether certain forks are activated #[derive(Debug)] -pub(crate) struct ForkTracker { +pub struct ForkTracker { /// Tracks if shanghai is activated at the block's timestamp. - pub(crate) shanghai: AtomicBool, + pub shanghai: AtomicBool, /// Tracks if cancun is activated at the block's timestamp. - pub(crate) cancun: AtomicBool, + pub cancun: AtomicBool, + /// Tracks if prague is activated at the block's timestamp. + pub prague: AtomicBool, } impl ForkTracker { /// Returns `true` if Shanghai fork is activated. - pub(crate) fn is_shanghai_activated(&self) -> bool { + pub fn is_shanghai_activated(&self) -> bool { self.shanghai.load(std::sync::atomic::Ordering::Relaxed) } /// Returns `true` if Cancun fork is activated. - pub(crate) fn is_cancun_activated(&self) -> bool { + pub fn is_cancun_activated(&self) -> bool { self.cancun.load(std::sync::atomic::Ordering::Relaxed) } + + /// Returns `true` if Prague fork is activated. + pub fn is_prague_activated(&self) -> bool { + self.prague.load(std::sync::atomic::Ordering::Relaxed) + } } /// Ensure that the code size is not greater than `max_init_code_size`. @@ -707,39 +762,34 @@ pub fn ensure_max_init_code_size( /// Ensures that gas limit of the transaction exceeds the intrinsic gas of the transaction. /// -/// See also [`calculate_intrinsic_gas_after_merge`] -pub fn ensure_intrinsic_gas( +/// Caution: This only checks past the Merge hardfork. +pub fn ensure_intrinsic_gas( transaction: &T, - is_shanghai: bool, + fork_tracker: &ForkTracker, ) -> Result<(), InvalidPoolTransactionError> { - if transaction.gas_limit() < - calculate_intrinsic_gas_after_merge( - transaction.input(), - &transaction.kind(), - transaction.access_list().map(|list| list.0.as_slice()).unwrap_or(&[]), - is_shanghai, - ) - { + let spec_id = if fork_tracker.is_prague_activated() { + SpecId::PRAGUE + } else if fork_tracker.is_shanghai_activated() { + SpecId::SHANGHAI + } else { + SpecId::MERGE + }; + + let gas_after_merge = validate_initial_tx_gas( + spec_id, + transaction.input(), + transaction.kind().is_create(), + transaction.access_list().map(|list| list.0.as_slice()).unwrap_or(&[]), + transaction.authorization_count() as u64, + ); + + if transaction.gas_limit() < gas_after_merge { Err(InvalidPoolTransactionError::IntrinsicGasTooLow) } else { Ok(()) } } -/// Calculates the Intrinsic Gas usage for a Transaction -/// -/// Caution: This only checks past the Merge hardfork. -#[inline] -pub fn calculate_intrinsic_gas_after_merge( - input: &[u8], - kind: &TxKind, - access_list: &[AccessListItem], - is_shanghai: bool, -) -> u64 { - let spec_id = if is_shanghai { SpecId::SHANGHAI } else { SpecId::MERGE }; - validate_initial_tx_gas(spec_id, input, kind.is_create(), access_list, 0) -} - #[cfg(test)] mod tests { use super::*; @@ -764,10 +814,14 @@ mod tests { #[tokio::test] async fn validate_transaction() { let transaction = get_transaction(); + let mut fork_tracker = + ForkTracker { shanghai: false.into(), cancun: false.into(), prague: false.into() }; - let res = ensure_intrinsic_gas(&transaction, false); + let res = ensure_intrinsic_gas(&transaction, &fork_tracker); assert!(res.is_ok()); - let res = ensure_intrinsic_gas(&transaction, true); + + fork_tracker.shanghai = true.into(); + let res = ensure_intrinsic_gas(&transaction, &fork_tracker); assert!(res.is_ok()); let provider = MockEthProvider::default();