diff --git a/Cargo.lock b/Cargo.lock index 7dd35bf..f930b8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3686,7 +3686,12 @@ dependencies = [ "alloy-primitives", "alloy-rlp", "alloy-serde", + "modular-bitfield", + "reth-codecs", + "reth-db-api", "reth-ethereum-primitives", + "reth-primitives-traits", + "serde", ] [[package]] @@ -3694,8 +3699,10 @@ name = "morph-revm" version = "0.7.5" dependencies = [ "alloy-consensus", + "alloy-eips", "alloy-evm", "alloy-primitives", + "alloy-rlp", "alloy-sol-types", "auto_impl", "derive_more", diff --git a/Cargo.toml b/Cargo.toml index def9f2e..60ba080 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,12 +6,7 @@ license = "MIT OR Apache-2.0" publish = false [workspace] -members = [ - "crates/evm", - "crates/revm", - "crates/chainspec", - "crates/primitives" -] +members = ["crates/evm", "crates/revm", "crates/chainspec", "crates/primitives"] [workspace.lints] [workspace.lints.clippy] @@ -36,7 +31,10 @@ all = "warn" morph-chainspec = { path = "crates/chainspec", default-features = false } morph-evm = { path = "crates/evm", default-features = false } morph-revm = { path = "crates/revm", default-features = false } -morph-primitives = { path = "crates/primitives", default-features = false } +morph-primitives = { path = "crates/primitives", default-features = false, features = [ + "serde", + "reth", +] } reth-basic-payload-builder = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } reth-chainspec = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } @@ -159,4 +157,4 @@ criterion = "0.7.0" test-case = "3" secp256k1 = "0.30.0" pyroscope = "0.5.8" -pyroscope_pprofrs = "0.2.10" \ No newline at end of file +pyroscope_pprofrs = "0.2.10" diff --git a/crates/chainspec/src/hardfork.rs b/crates/chainspec/src/hardfork.rs index c05ee10..3df5e58 100644 --- a/crates/chainspec/src/hardfork.rs +++ b/crates/chainspec/src/hardfork.rs @@ -44,9 +44,9 @@ hardfork!( /// Curie hardfork. Curie, /// Morph203 hardfork. - #[default] Morph203, /// Viridian hardfork. + #[default] Viridian, } ); diff --git a/crates/chainspec/src/spec.rs b/crates/chainspec/src/spec.rs index 94dea17..1a4c0b3 100644 --- a/crates/chainspec/src/spec.rs +++ b/crates/chainspec/src/spec.rs @@ -10,7 +10,7 @@ use reth_chainspec::{ Head, }; use reth_network_peers::NodeRecord; -use std::sync::{Arc}; +use std::sync::Arc; pub const MORPH_BASE_FEE: u64 = 10_000_000_000; @@ -289,7 +289,10 @@ mod tests { let has_bernoulli = chainspec .forks_iter() .any(|(fork, _)| fork.name() == "Bernoulli"); - assert!(has_bernoulli, "Bernoulli hardfork should be in inner.hardforks"); + assert!( + has_bernoulli, + "Bernoulli hardfork should be in inner.hardforks" + ); } #[test] diff --git a/crates/evm/Cargo.toml b/crates/evm/Cargo.toml index c93f8da..a17afcd 100644 --- a/crates/evm/Cargo.toml +++ b/crates/evm/Cargo.toml @@ -36,3 +36,4 @@ revm.workspace = true [features] default = ["rpc"] rpc = ["dep:reth-rpc-eth-api", "morph-revm/rpc"] +engine = [] diff --git a/crates/evm/src/assemble.rs b/crates/evm/src/assemble.rs index 0d0755c..4221edd 100644 --- a/crates/evm/src/assemble.rs +++ b/crates/evm/src/assemble.rs @@ -2,11 +2,11 @@ use crate::{ MorphEvmConfig, MorphEvmFactory, block::MorphReceiptBuilder, context::MorphBlockExecutionCtx, }; use alloy_evm::{block::BlockExecutionError, eth::EthBlockExecutorFactory}; +use morph_chainspec::MorphChainSpec; +use morph_primitives::MorphHeader; use reth_evm::execute::{BlockAssembler, BlockAssemblerInput}; use reth_evm_ethereum::EthBlockAssembler; use std::sync::Arc; -use morph_chainspec::MorphChainSpec; -use morph_primitives::MorphHeader; /// Assembler for Morph blocks. #[derive(Debug, Clone)] diff --git a/crates/evm/src/block.rs b/crates/evm/src/block.rs index c219720..a88f265 100644 --- a/crates/evm/src/block.rs +++ b/crates/evm/src/block.rs @@ -7,11 +7,10 @@ use alloy_evm::{ receipt_builder::{ReceiptBuilder, ReceiptBuilderCtx}, }, }; -use reth_revm::{Inspector, State, context::result::ResultAndState}; use morph_chainspec::MorphChainSpec; use morph_primitives::{MorphReceipt, MorphTxEnvelope}; use morph_revm::{MorphHaltReason, evm::MorphContext}; - +use reth_revm::{Inspector, State, context::result::ResultAndState}; /// Builder for [`MorphReceipt`]. #[derive(Debug, Clone, Copy, Default)] @@ -64,7 +63,12 @@ where chain_spec: &'a MorphChainSpec, ) -> Self { Self { - inner: EthBlockExecutor::new(evm, ctx.inner, chain_spec, MorphReceiptBuilder::default()), + inner: EthBlockExecutor::new( + evm, + ctx.inner, + chain_spec, + MorphReceiptBuilder::default(), + ), } } } diff --git a/crates/evm/src/engine.rs b/crates/evm/src/engine.rs index f7b01d3..0e7bde4 100644 --- a/crates/evm/src/engine.rs +++ b/crates/evm/src/engine.rs @@ -1,15 +1,15 @@ use crate::MorphEvmConfig; use alloy_consensus::crypto::RecoveryError; use alloy_primitives::Address; +use morph_payload_types::MorphExecutionData; +use morph_primitives::{Block, MorphTxEnvelope}; +use morph_revm::MorphTxEnv; use reth_evm::{ ConfigureEngineEvm, ConfigureEvm, EvmEnvFor, ExecutableTxIterator, ExecutionCtxFor, FromRecoveredTx, RecoveredTx, ToTxEnv, }; use reth_primitives_traits::{SealedBlock, SignedTransaction}; use std::sync::Arc; -use morph_payload_types::MorphExecutionData; -use morph_primitives::{Block, MorphTxEnvelope}; -use morph_revm::MorphTxEnv; impl ConfigureEngineEvm for MorphEvmConfig { fn evm_env_for_payload( diff --git a/crates/evm/src/evm.rs b/crates/evm/src/evm.rs index b04a1fd..8896880 100644 --- a/crates/evm/src/evm.rs +++ b/crates/evm/src/evm.rs @@ -8,10 +8,10 @@ use alloy_evm::{ }, }; use alloy_primitives::{Address, Bytes, Log}; -use reth_revm::MainContext; -use std::ops::{Deref, DerefMut}; use morph_chainspec::hardfork::MorphHardfork; use morph_revm::{MorphHaltReason, MorphInvalidTransaction, MorphTxEnv, evm::MorphContext}; +use reth_revm::MainContext; +use std::ops::{Deref, DerefMut}; use crate::MorphBlockEnv; @@ -208,7 +208,7 @@ where #[cfg(test)] mod tests { use reth_revm::context::BlockEnv; - use revm::database::EmptyDB; + use revm::{context::TxEnv, database::EmptyDB}; use super::*; @@ -228,9 +228,12 @@ mod tests { ); let result = evm .transact(MorphTxEnv { - caller: Address::ZERO, - gas_price: 0, - gas_limit: 21000, + inner: TxEnv { + caller: Address::ZERO, + gas_price: 0, + gas_limit: 21000, + ..Default::default() + }, ..Default::default() }) .unwrap(); diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index 4acbbd1..b59cff6 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -3,14 +3,15 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg))] +#[cfg(feature = "engine")] +mod engine; mod assemble; use alloy_consensus::BlockHeader as _; pub use assemble::MorphBlockAssembler; mod block; mod context; pub use context::{MorphBlockExecutionCtx, MorphNextBlockEnvAttributes}; -#[cfg(feature = "engine")] -mod engine; + mod error; pub use error::MorphEvmError; pub mod evm; @@ -23,15 +24,15 @@ use alloy_evm::{ revm::{Inspector, database::State}, }; pub use evm::MorphEvmFactory; +use morph_primitives::{Block, MorphHeader, MorphPrimitives, MorphReceipt, MorphTxEnvelope}; use reth_chainspec::EthChainSpec; use reth_evm::{self, ConfigureEvm, EvmEnvFor}; use reth_primitives_traits::{SealedBlock, SealedHeader}; -use morph_primitives::{Block, MorphHeader, MorphPrimitives, MorphReceipt, MorphTxEnvelope}; use crate::{block::MorphBlockExecutor, evm::MorphEvm}; -use reth_evm_ethereum::EthEvmConfig; use morph_chainspec::{MorphChainSpec, hardfork::MorphHardforks}; use morph_revm::evm::MorphContext; +use reth_evm_ethereum::EthEvmConfig; pub use morph_revm::{MorphBlockEnv, MorphHaltReason}; @@ -197,14 +198,18 @@ mod tests { fn test_evm_config_can_query_morph_hardforks() { // Create a test chainspec with Bernoulli at genesis let chainspec = Arc::new(morph_chainspec::MorphChainSpec::from_genesis( - morph_chainspec::spec::ANDANTINO.genesis().clone(), + Default::default(), )); let evm_config = MorphEvmConfig::new_with_default_factory(chainspec); // Should be able to query Morph hardforks through the chainspec assert!(evm_config.chain_spec().is_bernoulli_active_at_timestamp(0)); - assert!(evm_config.chain_spec().is_bernoulli_active_at_timestamp(1000)); + assert!( + evm_config + .chain_spec() + .is_bernoulli_active_at_timestamp(1000) + ); // Should be able to query activation condition let activation = evm_config diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index f7cf699..4b36796 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -12,7 +12,10 @@ workspace = true [dependencies] # Reth -reth-ethereum-primitives.workspace = true +reth-db-api = { workspace = true, optional = true } +reth-ethereum-primitives = { workspace = true, optional = true } +reth-primitives-traits = { workspace = true, optional = true } +reth-codecs = { workspace = true, optional = true } # Alloy alloy-consensus.workspace = true @@ -21,8 +24,32 @@ alloy-primitives.workspace = true alloy-rlp.workspace = true alloy-serde = { workspace = true, optional = true } +# Utils +serde = { workspace = true, features = ["derive"], optional = true } +modular-bitfield = { version = "0.11.2", optional = true } + + [dev-dependencies] [features] -default = [] -serde = ["dep:alloy-serde", "alloy-primitives/serde", "alloy-eips/serde"] +default = ["serde", "reth-codec"] +serde = [ + "dep:serde", + "dep:alloy-serde", + "alloy-primitives/serde", + "alloy-eips/serde", +] +reth = [ + "dep:reth-ethereum-primitives", + "dep:reth-primitives-traits", +] +reth-codec = [ + "reth", + "serde", + "dep:reth-codecs", + "dep:reth-db-api", + "dep:modular-bitfield", + "reth-ethereum-primitives/reth-codec", + "reth-codecs/alloy", + "reth-primitives-traits/reth-codec", +] diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 7487665..d38fc37 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -12,16 +12,40 @@ use alloy_primitives as _; use alloy_rlp as _; pub mod transaction; +use crate::transaction::envelope::MorphTxType; +use alloy_primitives::Log; // Re-export standard Ethereum types pub use alloy_consensus::Header; -pub use reth_ethereum_primitives::{ - Block, EthPrimitives as MorphPrimitives, Receipt as MorphReceipt, - TransactionSigned as MorphTxEnvelope, -}; +/// Header alias for backwards compatibility. +pub type MorphHeader = Header; + +use reth_ethereum_primitives::EthereumReceipt; +use reth_primitives_traits::NodePrimitives; + +/// Morph block. +pub type Block = alloy_consensus::Block; + +/// Morph block body. +pub type BlockBody = alloy_consensus::BlockBody; + +/// Morph receipt. +pub type MorphReceipt = EthereumReceipt; // Re-export transaction types -pub use transaction::{L1Transaction, L1_TX_TYPE_ID}; +pub use transaction::{ + ALT_FEE_TX_TYPE_ID, L1_TX_TYPE_ID, MorphTxEnvelope, TxAltFee, TxAltFeeExt, TxL1Msg, +}; -/// Header alias for backwards compatibility. -pub type MorphHeader = Header; +/// A [`NodePrimitives`] implementation for Morph. +#[derive(Debug, Clone, Default, Eq, PartialEq)] +#[non_exhaustive] +pub struct MorphPrimitives; + +impl NodePrimitives for MorphPrimitives { + type Block = Block; + type BlockHeader = MorphHeader; + type BlockBody = BlockBody; + type SignedTx = MorphTxEnvelope; + type Receipt = MorphReceipt; +} diff --git a/crates/primitives/src/transaction/alt_fee.rs b/crates/primitives/src/transaction/alt_fee.rs new file mode 100644 index 0000000..bba7c26 --- /dev/null +++ b/crates/primitives/src/transaction/alt_fee.rs @@ -0,0 +1,815 @@ +//! Altfee Transaction type for Morph L2. +//! +//! This module defines the TxAltFee type which represents transactions that +//! use ERC20 tokens for gas payment instead of native ETH. +//! +//! Reference: + +use alloy_consensus::{ + SignableTransaction, Transaction, + transaction::{RlpEcdsaDecodableTx, RlpEcdsaEncodableTx}, +}; +use alloy_eips::{ + Typed2718, eip2718::Encodable2718, eip2930::AccessList, eip7702::SignedAuthorization, +}; +use alloy_primitives::{B256, Bytes, ChainId, Signature, TxKind, U256, keccak256}; +use alloy_rlp::{BufMut, Decodable, Encodable, Header}; +use core::mem; + +/// Altfee Transaction type ID (0x7F). +pub const ALT_FEE_TX_TYPE_ID: u8 = 0x7F; + +/// Altfee Transaction for Morph L2. +/// +/// This transaction type allows users to pay gas fees using ERC20 tokens +/// instead of native ETH. It extends EIP-1559 style transactions with +/// additional fields for token-based fee payment. +/// +/// Reference: +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct TxAltFee { + /// EIP-155: Simple replay attack protection. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] + pub chain_id: ChainId, + + /// A scalar value equal to the number of transactions sent by the sender. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] + pub nonce: 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. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] + pub gas_limit: u128, + + /// 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. + /// + /// This is also known as `GasFeeCap`. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] + pub max_fee_per_gas: u128, + + /// Max Priority fee that transaction is paying. + /// + /// This is also known as `GasTipCap`. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] + pub max_priority_fee_per_gas: u128, + + /// The 160-bit address of the message call's recipient or, for a contract + /// creation transaction, empty. + 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. + 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, + + + /// Maximum amount of tokens the sender is willing to pay as fee. + pub fee_limit: U256, + + /// Input has two uses depending if transaction is Create or Call (if `to` + /// field is None or Some). + /// - init: An unlimited size byte array specifying the EVM-code for the + /// account initialisation procedure CREATE. + /// - data: An unlimited size byte array specifying the input data of the + /// message call. + #[cfg_attr(feature = "serde", serde(default, alias = "data"))] + pub input: Bytes, + + /// Token ID for alternative fee payment. + /// This corresponds to the token registered in the L2 Token Registry. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] + pub fee_token_id: u16, +} + +impl TxAltFee { + /// Get the transaction type. + #[doc(alias = "transaction_type")] + pub const fn tx_type() -> u8 { + ALT_FEE_TX_TYPE_ID + } + + /// 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 + } + } + } + } + + /// Validates the transaction according to the spec rules. + pub fn validate(&self) -> Result<(), &'static str> { + if self.max_priority_fee_per_gas > self.max_fee_per_gas { + return Err("max priority fee per gas exceeds max fee per gas"); + } + Ok(()) + } + + /// Calculate the in-memory size of this transaction. + pub fn size(&self) -> usize { + mem::size_of::() + // chain_id + mem::size_of::() + // nonce + mem::size_of::() + // gas_limit + mem::size_of::() + // max_fee_per_gas + mem::size_of::() + // max_priority_fee_per_gas + self.to.size() + // to + mem::size_of::() + // value + self.access_list.size() + // access_list + self.input.len() + // input + mem::size_of::() + // fee_token_id + mem::size_of::() // fee_limit + } + + /// Outputs the length of the transaction's fields, without a RLP header. + #[doc(hidden)] + pub fn fields_len(&self) -> usize { + let mut len = 0; + len += self.chain_id.length(); + len += self.nonce.length(); + len += self.max_priority_fee_per_gas.length(); + len += self.max_fee_per_gas.length(); + len += self.gas_limit.length(); + len += self.to.length(); + len += self.value.length(); + len += self.input.0.length(); + len += self.access_list.length(); + len += self.fee_token_id.length(); + len += self.fee_limit.length(); + len + } + + /// Encodes only the transaction's fields into the desired buffer, without a RLP header. + pub fn encode_fields(&self, out: &mut dyn 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.fee_token_id.encode(out); + self.fee_limit.encode(out); + } + + /// Decodes the inner 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` + /// - `max_priority_fee_per_gas` + /// - `max_fee_per_gas` + /// - `gas_limit` + /// - `to` + /// - `value` + /// - `data` (`input`) + /// - `access_list` + /// - `fee_token_id` + /// - `fee_limit` + pub fn decode_fields(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)?, + fee_token_id: Decodable::decode(buf)?, + fee_limit: Decodable::decode(buf)?, + }) + } + + /// Computes the hash used for signing the transaction. + pub fn signature_hash(&self) -> B256 { + let mut buf = Vec::with_capacity(self.encode_2718_len()); + self.encode_2718(&mut buf); + keccak256(&buf) + } + + /// Returns the RLP header for this transaction. + fn rlp_header(&self) -> Header { + Header { + list: true, + payload_length: self.fields_len(), + } + } +} + +impl Typed2718 for TxAltFee { + fn ty(&self) -> u8 { + ALT_FEE_TX_TYPE_ID + } +} + +impl Transaction for TxAltFee { + fn chain_id(&self) -> Option { + Some(self.chain_id) + } + + fn nonce(&self) -> u64 { + self.nonce + } + + fn gas_limit(&self) -> u64 { + self.gas_limit as u64 + } + + fn gas_price(&self) -> Option { + None + } + + fn max_fee_per_gas(&self) -> u128 { + self.max_fee_per_gas + } + + fn max_priority_fee_per_gas(&self) -> Option { + Some(self.max_priority_fee_per_gas) + } + + fn max_fee_per_blob_gas(&self) -> Option { + None + } + + fn priority_fee_or_price(&self) -> u128 { + self.max_priority_fee_per_gas + } + + fn effective_gas_price(&self, base_fee: Option) -> u128 { + self.effective_gas_price(base_fee) + } + + fn is_dynamic_fee(&self) -> bool { + true + } + + fn kind(&self) -> TxKind { + self.to + } + + fn is_create(&self) -> bool { + self.to.is_create() + } + + fn value(&self) -> U256 { + self.value + } + + fn input(&self) -> &Bytes { + &self.input + } + + fn access_list(&self) -> Option<&AccessList> { + Some(&self.access_list) + } + + fn blob_versioned_hashes(&self) -> Option<&[B256]> { + None + } + + fn authorization_list(&self) -> Option<&[SignedAuthorization]> { + None + } +} + +impl RlpEcdsaEncodableTx for TxAltFee { + fn rlp_encoded_fields_length(&self) -> usize { + self.fields_len() + } + + fn rlp_encode_fields(&self, out: &mut dyn BufMut) { + self.encode_fields(out); + } +} + +impl RlpEcdsaDecodableTx for TxAltFee { + const DEFAULT_TX_TYPE: u8 = { Self::tx_type() as u8 }; + + /// Decodes the inner [TxEip1559] fields from RLP bytes. + fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result { + Self::decode_fields(buf) + } +} + +impl SignableTransaction for TxAltFee { + fn set_chain_id(&mut self, chain_id: ChainId) { + self.chain_id = chain_id; + } + + fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) { + out.put_u8(Self::tx_type() as u8); + self.encode(out) + } + + fn payload_len_for_signature(&self) -> usize { + self.length() + 1 + } +} + +impl Encodable for TxAltFee { + fn encode(&self, out: &mut dyn BufMut) { + self.rlp_header().encode(out); + self.encode_fields(out); + } + + fn length(&self) -> usize { + self.rlp_header().length_with_payload() + } +} + +impl Decodable for TxAltFee { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + let header = Header::decode(buf)?; + if !header.list { + return Err(alloy_rlp::Error::UnexpectedString); + } + + let remaining = buf.len(); + if header.payload_length > remaining { + return Err(alloy_rlp::Error::InputTooShort); + } + + Self::decode_fields(buf) + } +} + +impl Encodable2718 for TxAltFee { + fn type_flag(&self) -> Option { + Some(ALT_FEE_TX_TYPE_ID) + } + + fn encode_2718_len(&self) -> usize { + let payload_length = self.fields_len(); + 1 + Header { + list: true, + payload_length, + } + .length() + + payload_length + } + + fn encode_2718(&self, out: &mut dyn BufMut) { + ALT_FEE_TX_TYPE_ID.encode(out); + let header = Header { + list: true, + payload_length: self.fields_len(), + }; + header.encode(out); + self.encode_fields(out); + } +} + +impl reth_primitives_traits::InMemorySize for TxAltFee { + fn size(&self) -> usize { + Self::size(self) + } +} + +#[cfg(feature = "reth-codec")] +impl reth_codecs::Compact for TxAltFee { + fn to_compact(&self, buf: &mut B) -> usize + where + B: BufMut + AsMut<[u8]>, + { + let mut len = 0; + len += self.chain_id.to_compact(buf); + len += self.nonce.to_compact(buf); + len += self.gas_limit.to_compact(buf); + len += self.max_fee_per_gas.to_compact(buf); + len += self.max_priority_fee_per_gas.to_compact(buf); + len += self.to.to_compact(buf); + len += self.value.to_compact(buf); + len += self.access_list.to_compact(buf); + len += self.fee_limit.to_compact(buf); + len += self.input.to_compact(buf); + len += (self.fee_token_id as u64).to_compact(buf); + len + } + + fn from_compact(buf: &[u8], len: usize) -> (Self, &[u8]) { + let (chain_id, buf) = ChainId::from_compact(buf, len); + let (nonce, buf) = u64::from_compact(buf, len); + let (gas_limit, buf) = u128::from_compact(buf, len); + let (max_fee_per_gas, buf) = u128::from_compact(buf, len); + let (max_priority_fee_per_gas, buf) = u128::from_compact(buf, len); + let (to, buf) = TxKind::from_compact(buf, len); + let (value, buf) = U256::from_compact(buf, len); + let (access_list, buf) = AccessList::from_compact(buf, len); + let (fee_limit, buf) = U256::from_compact(buf, len); + let (input, buf) = Bytes::from_compact(buf, len); + let (fee_token_id, buf) = u64::from_compact(buf, len); + + ( + Self { + chain_id, + nonce, + gas_limit, + max_fee_per_gas, + max_priority_fee_per_gas, + to, + value, + access_list, + fee_limit, + input, + fee_token_id: fee_token_id as u16, + }, + buf, + ) + } +} + +/// Extension trait for [`TxAltFee`] to access token fee fields. +pub trait TxAltFeeExt { + /// Returns the token ID used for fee payment. + fn fee_token_id(&self) -> u16; + + /// Returns the maximum token amount for fee payment. + fn fee_limit(&self) -> U256; + + /// Returns true if this transaction uses token-based fee payment. + fn uses_token_fee(&self) -> bool { + self.fee_token_id() > 0 + } +} + +impl TxAltFeeExt for TxAltFee { + fn fee_token_id(&self) -> u16 { + self.fee_token_id + } + + fn fee_limit(&self) -> U256 { + self.fee_limit + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::address; + + #[test] + fn test_tx_alt_fee_default() { + let tx = TxAltFee::default(); + assert_eq!(tx.chain_id, 0); + assert_eq!(tx.nonce, 0); + assert_eq!(tx.gas_limit, 0); + assert_eq!(tx.max_fee_per_gas, 0); + assert_eq!(tx.max_priority_fee_per_gas, 0); + assert_eq!(tx.value, U256::ZERO); + assert_eq!(tx.fee_token_id, 0); + assert_eq!(tx.fee_limit, U256::ZERO); + } + + #[test] + fn test_tx_alt_fee_tx_type() { + assert_eq!(TxAltFee::tx_type(), ALT_FEE_TX_TYPE_ID); + assert_eq!(TxAltFee::tx_type(), 0x7F); + } + + #[test] + fn test_tx_alt_fee_validate() { + let valid_tx = TxAltFee { + max_fee_per_gas: 100, + max_priority_fee_per_gas: 50, + ..Default::default() + }; + assert!(valid_tx.validate().is_ok()); + + let invalid_tx = TxAltFee { + max_fee_per_gas: 50, + max_priority_fee_per_gas: 100, + ..Default::default() + }; + assert!(invalid_tx.validate().is_err()); + } + + #[test] + fn test_tx_alt_fee_effective_gas_price() { + let tx = TxAltFee { + max_fee_per_gas: 100, + max_priority_fee_per_gas: 20, + ..Default::default() + }; + + // Without base fee + assert_eq!(tx.effective_gas_price(None), 100); + + // With base fee (tip > max_priority_fee_per_gas) + assert_eq!(tx.effective_gas_price(Some(50)), 70); // 20 + 50 + + // With base fee (tip <= max_priority_fee_per_gas) + assert_eq!(tx.effective_gas_price(Some(90)), 100); // max_fee_per_gas + } + + #[test] + fn test_tx_alt_fee_trait_methods() { + let tx = TxAltFee { + chain_id: 1, + nonce: 42, + gas_limit: 21_000, + max_fee_per_gas: 100, + max_priority_fee_per_gas: 20, + to: TxKind::Call(address!("0000000000000000000000000000000000000002")), + value: U256::from(100u64), + access_list: AccessList::default(), + input: Bytes::from(vec![1, 2, 3, 4]), + fee_token_id: 1, + fee_limit: U256::from(1000u64), + }; + + // Test Transaction trait methods + assert_eq!(tx.chain_id(), Some(1)); + assert_eq!(Transaction::nonce(&tx), 42); + assert_eq!(Transaction::gas_limit(&tx), 21_000); + assert_eq!(tx.gas_price(), None); + assert_eq!(tx.max_fee_per_gas(), 100); + assert_eq!(tx.max_priority_fee_per_gas(), Some(20)); + assert_eq!(tx.max_fee_per_blob_gas(), None); + assert_eq!(tx.priority_fee_or_price(), 20); + assert!(tx.is_dynamic_fee()); + assert!(!tx.is_create()); + assert_eq!( + tx.kind(), + TxKind::Call(address!("0000000000000000000000000000000000000002")) + ); + assert_eq!(Transaction::value(&tx), U256::from(100u64)); + assert_eq!(Transaction::input(&tx), &Bytes::from(vec![1, 2, 3, 4])); + assert_eq!(Typed2718::ty(&tx), ALT_FEE_TX_TYPE_ID); + assert!(tx.access_list().is_some()); + assert!(tx.blob_versioned_hashes().is_none()); + assert!(tx.authorization_list().is_none()); + + // Test TxAltFeeExt trait methods + assert_eq!(tx.fee_token_id(), 1); + assert_eq!(tx.fee_limit(), U256::from(1000u64)); + assert!(tx.uses_token_fee()); + } + + #[test] + fn test_tx_alt_fee_is_create() { + let create_tx = TxAltFee { + to: TxKind::Create, + ..Default::default() + }; + assert!(create_tx.is_create()); + + let call_tx = TxAltFee { + to: TxKind::Call(address!("0000000000000000000000000000000000000001")), + ..Default::default() + }; + assert!(!call_tx.is_create()); + } + + #[test] + fn test_tx_alt_fee_rlp_roundtrip() { + let tx = TxAltFee { + chain_id: 1, + nonce: 42, + gas_limit: 21_000, + max_fee_per_gas: 100_000_000_000, + max_priority_fee_per_gas: 2_000_000_000, + to: TxKind::Call(address!("0000000000000000000000000000000000000002")), + value: U256::from(1_000_000_000_000_000_000u128), + access_list: AccessList::default(), + input: Bytes::from(vec![0x12, 0x34]), + fee_token_id: 1, + fee_limit: U256::from(1000u64), + }; + + // Encode + let mut buf = Vec::new(); + tx.encode(&mut buf); + + // Decode + let decoded = TxAltFee::decode(&mut buf.as_slice()).expect("Should decode"); + + assert_eq!(tx.chain_id, decoded.chain_id); + assert_eq!(tx.nonce, decoded.nonce); + assert_eq!(tx.gas_limit, decoded.gas_limit); + assert_eq!(tx.max_fee_per_gas, decoded.max_fee_per_gas); + assert_eq!( + tx.max_priority_fee_per_gas, + decoded.max_priority_fee_per_gas + ); + assert_eq!(tx.to, decoded.to); + assert_eq!(tx.value, decoded.value); + assert_eq!(tx.input, decoded.input); + assert_eq!(tx.fee_token_id, decoded.fee_token_id); + assert_eq!(tx.fee_limit, decoded.fee_limit); + } + + #[test] + fn test_tx_alt_fee_create() { + let tx = TxAltFee { + chain_id: 1, + nonce: 0, + gas_limit: 100_000, + max_fee_per_gas: 100_000_000_000, + max_priority_fee_per_gas: 2_000_000_000, + to: TxKind::Create, + value: U256::ZERO, + access_list: AccessList::default(), + input: Bytes::from(vec![0x60, 0x80, 0x60, 0x40]), + fee_token_id: 1, + fee_limit: U256::from(1000u64), + }; + + // Encode + let mut buf = Vec::new(); + tx.encode(&mut buf); + + // Decode + let decoded = TxAltFee::decode(&mut buf.as_slice()).expect("Should decode"); + + assert_eq!(decoded.to, TxKind::Create); + } + + #[test] + fn test_tx_alt_fee_encode_2718() { + let tx = TxAltFee { + chain_id: 1, + nonce: 1, + gas_limit: 21_000, + max_fee_per_gas: 100_000_000_000, + max_priority_fee_per_gas: 2_000_000_000, + to: TxKind::Call(address!("0000000000000000000000000000000000000002")), + value: U256::from(100u64), + access_list: AccessList::default(), + input: Bytes::new(), + fee_token_id: 1, + fee_limit: U256::from(1000u64), + }; + + let mut buf = Vec::new(); + tx.encode_2718(&mut buf); + + // First byte should be the type ID + assert_eq!(buf[0], ALT_FEE_TX_TYPE_ID); + + // Verify type_flag + assert_eq!(tx.type_flag(), Some(ALT_FEE_TX_TYPE_ID)); + + // Verify length consistency + assert_eq!(buf.len(), tx.encode_2718_len()); + } + + #[test] + fn test_tx_alt_fee_decode_rejects_malformed_rlp() { + let tx = TxAltFee { + chain_id: 1, + nonce: 42, + gas_limit: 21_000, + max_fee_per_gas: 100_000_000_000, + max_priority_fee_per_gas: 2_000_000_000, + to: TxKind::Call(address!("0000000000000000000000000000000000000002")), + value: U256::from(1_000_000_000_000_000_000u128), + access_list: AccessList::default(), + input: Bytes::from(vec![0x12, 0x34]), + fee_token_id: 1, + fee_limit: U256::from(1000u64), + }; + + // Encode the transaction + let mut buf = Vec::new(); + tx.encode(&mut buf); + + // Corrupt by truncating + let original_len = buf.len(); + buf.truncate(original_len - 5); + + let result = TxAltFee::decode(&mut buf.as_slice()); + assert!( + result.is_err(), + "Decoding should fail when data is truncated" + ); + } + + #[test] + fn test_tx_alt_fee_size() { + let tx = TxAltFee { + chain_id: 1, + nonce: 0, + gas_limit: 21_000, + max_fee_per_gas: 100, + max_priority_fee_per_gas: 20, + to: TxKind::Create, + value: U256::ZERO, + access_list: AccessList::default(), + input: Bytes::new(), + fee_token_id: 0, + fee_limit: U256::ZERO, + }; + + let size = tx.size(); + assert!(size > 0); + } + + #[test] + fn test_tx_alt_fee_fields_len() { + let tx = TxAltFee { + chain_id: 1, + nonce: 1, + gas_limit: 21_000, + max_fee_per_gas: 100_000_000_000, + max_priority_fee_per_gas: 2_000_000_000, + to: TxKind::Call(address!("0000000000000000000000000000000000000002")), + value: U256::from(100u64), + access_list: AccessList::default(), + input: Bytes::from(vec![1, 2, 3, 4]), + fee_token_id: 1, + fee_limit: U256::from(1000u64), + }; + + let fields_len = tx.fields_len(); + assert!(fields_len > 0); + + // Verify encode_2718_len is consistent + let encode_2718_len = tx.encode_2718_len(); + assert!(encode_2718_len > fields_len); + } + + #[test] + fn test_tx_alt_fee_encode_fields() { + let tx = TxAltFee { + chain_id: 1, + nonce: 1, + gas_limit: 21_000, + max_fee_per_gas: 100_000_000_000, + max_priority_fee_per_gas: 2_000_000_000, + to: TxKind::Call(address!("0000000000000000000000000000000000000002")), + value: U256::from(100u64), + access_list: AccessList::default(), + input: Bytes::new(), + fee_token_id: 1, + fee_limit: U256::from(1000u64), + }; + + let mut buf = Vec::new(); + tx.encode_fields(&mut buf); + + // Should have encoded fields + assert!(!buf.is_empty()); + assert_eq!(buf.len(), tx.fields_len()); + } + + #[test] + fn test_tx_alt_fee_uses_token_fee() { + let tx_with_token = TxAltFee { + fee_token_id: 1, + ..Default::default() + }; + assert!(tx_with_token.uses_token_fee()); + + let tx_without_token = TxAltFee { + fee_token_id: 0, + ..Default::default() + }; + assert!(!tx_without_token.uses_token_fee()); + } + + #[test] + fn test_tx_alt_fee_signature_hash() { + let tx = TxAltFee { + chain_id: 1, + nonce: 1, + gas_limit: 21_000, + max_fee_per_gas: 100_000_000_000, + max_priority_fee_per_gas: 2_000_000_000, + to: TxKind::Call(address!("0000000000000000000000000000000000000002")), + value: U256::from(100u64), + access_list: AccessList::default(), + input: Bytes::new(), + fee_token_id: 1, + fee_limit: U256::from(1000u64), + }; + + let hash = tx.signature_hash(); + assert_ne!(hash, B256::ZERO); + } +} diff --git a/crates/primitives/src/transaction/envelope.rs b/crates/primitives/src/transaction/envelope.rs new file mode 100644 index 0000000..826000e --- /dev/null +++ b/crates/primitives/src/transaction/envelope.rs @@ -0,0 +1,331 @@ +use alloy_consensus::{Signed, TransactionEnvelope, TxEip1559, TxEip2930, TxEip7702, TxLegacy}; +use alloy_primitives::{B256, Bytes}; +use alloy_rlp::BytesMut; + +use crate::{TxAltFee, TxL1Msg}; + +#[derive(Debug, Clone, TransactionEnvelope)] +#[envelope(tx_type_name = MorphTxType)] +#[expect(clippy::large_enum_variant)] +pub enum MorphTxEnvelope { + /// Legacy transaction (type 0x00) + #[envelope(ty = 0)] + Legacy(Signed), + + /// EIP-2930 access list transaction (type 0x01) + #[envelope(ty = 1)] + Eip2930(Signed), + + /// EIP-1559 dynamic fee transaction (type 0x02) + #[envelope(ty = 2)] + Eip1559(Signed), + + /// EIP-7702 authorization list transaction (type 0x04) + #[envelope(ty = 4)] + Eip7702(Signed), + + /// Layer1 Message Transaction + #[envelope(ty = 0x7e)] + L1Msg(Signed), + + /// Alt Fee Transaction + #[envelope(ty = 0x7f)] + AltFee(Signed), +} + +impl MorphTxEnvelope { + /// Return the [`MorphTxType`] of the inner txn. + pub const fn tx_type(&self) -> MorphTxType { + match self { + Self::Legacy(_) => MorphTxType::Legacy, + Self::Eip2930(_) => MorphTxType::Eip2930, + Self::Eip1559(_) => MorphTxType::Eip1559, + Self::Eip7702(_) => MorphTxType::Eip7702, + Self::L1Msg(_) => MorphTxType::L1Msg, + Self::AltFee(_) => MorphTxType::AltFee, + } + } + + /// Same as [`Self::signer`], but skips signature validation checks. + pub fn signer_unchecked( + &self, + ) -> Result { + alloy_consensus::transaction::SignerRecoverable::recover_signer_unchecked(self) + } + + pub fn is_l1_msg(&self) -> bool { + self.tx_type() == MorphTxType::L1Msg + } + + /// Encode the transaction according to [EIP-2718] rules. First a 1-byte + /// type flag in the range 0x0-0x7f, then the body of the transaction. + pub fn rlp(&self) -> Bytes { + let mut bytes = BytesMut::new(); + match self { + Self::Legacy(tx) => tx.encode_2718(&mut bytes), + Self::Eip2930(tx) => tx.encode_2718(&mut bytes), + Self::Eip1559(tx) => tx.encode_2718(&mut bytes), + Self::Eip7702(tx) => tx.encode_2718(&mut bytes), + Self::L1Msg(tx) => tx.encode_2718(&mut bytes), + Self::AltFee(tx) => tx.encode_2718(&mut bytes), + } + Bytes(bytes.freeze()) + } +} + +impl reth_primitives_traits::InMemorySize for MorphTxEnvelope { + fn size(&self) -> usize { + match self { + Self::Legacy(tx) => tx.size(), + Self::Eip2930(tx) => tx.size(), + Self::Eip1559(tx) => tx.size(), + Self::Eip7702(tx) => tx.size(), + Self::L1Msg(tx) => tx.size(), + Self::AltFee(tx) => tx.size(), + } + } +} + +impl reth_primitives_traits::InMemorySize for MorphTxType { + fn size(&self) -> usize { + core::mem::size_of::() + } +} + +impl alloy_consensus::transaction::TxHashRef for MorphTxEnvelope { + fn tx_hash(&self) -> &B256 { + match self { + Self::Legacy(tx) => tx.hash(), + Self::Eip2930(tx) => tx.hash(), + Self::Eip1559(tx) => tx.hash(), + Self::Eip7702(tx) => tx.hash(), + Self::L1Msg(tx) => tx.hash(), + Self::AltFee(tx) => tx.hash(), + } + } +} + +impl alloy_consensus::transaction::SignerRecoverable for MorphTxEnvelope { + fn recover_signer( + &self, + ) -> Result { + match self { + Self::Legacy(tx) => alloy_consensus::transaction::SignerRecoverable::recover_signer(tx), + Self::Eip2930(tx) => { + alloy_consensus::transaction::SignerRecoverable::recover_signer(tx) + } + Self::Eip1559(tx) => { + alloy_consensus::transaction::SignerRecoverable::recover_signer(tx) + } + Self::Eip7702(tx) => { + alloy_consensus::transaction::SignerRecoverable::recover_signer(tx) + } + Self::L1Msg(tx) => alloy_consensus::transaction::SignerRecoverable::recover_signer(tx), + Self::AltFee(tx) => alloy_consensus::transaction::SignerRecoverable::recover_signer(tx), + } + } + + fn recover_signer_unchecked( + &self, + ) -> Result { + match self { + Self::Legacy(tx) => { + alloy_consensus::transaction::SignerRecoverable::recover_signer_unchecked(tx) + } + Self::Eip2930(tx) => { + alloy_consensus::transaction::SignerRecoverable::recover_signer_unchecked(tx) + } + Self::Eip1559(tx) => { + alloy_consensus::transaction::SignerRecoverable::recover_signer_unchecked(tx) + } + Self::Eip7702(tx) => { + alloy_consensus::transaction::SignerRecoverable::recover_signer_unchecked(tx) + } + Self::L1Msg(tx) => { + alloy_consensus::transaction::SignerRecoverable::recover_signer_unchecked(tx) + } + Self::AltFee(tx) => { + alloy_consensus::transaction::SignerRecoverable::recover_signer_unchecked(tx) + } + } + } +} + +impl reth_primitives_traits::SignedTransaction for MorphTxEnvelope {} + +#[cfg(feature = "reth-codec")] +mod codec { + use crate::ALT_FEE_TX_TYPE_ID; + use crate::L1_TX_TYPE_ID; + use crate::TxAltFee; + use crate::TxL1Msg; + + use super::*; + use alloy_eips::eip2718::EIP7702_TX_TYPE_ID; + use alloy_primitives::{ + Signature, + bytes::{self, BufMut}, + }; + use reth_codecs::{ + Compact, + alloy::transaction::{CompactEnvelope, Envelope}, + txtype::{ + COMPACT_EXTENDED_IDENTIFIER_FLAG, COMPACT_IDENTIFIER_EIP1559, + COMPACT_IDENTIFIER_EIP2930, COMPACT_IDENTIFIER_LEGACY, + }, + }; + + impl reth_codecs::alloy::transaction::FromTxCompact for MorphTxEnvelope { + type TxType = MorphTxType; + + fn from_tx_compact( + buf: &[u8], + tx_type: Self::TxType, + signature: Signature, + ) -> (Self, &[u8]) { + use alloy_consensus::Signed; + use reth_codecs::Compact; + + match tx_type { + MorphTxType::Legacy => { + let (tx, buf) = TxLegacy::from_compact(buf, buf.len()); + let tx = Signed::new_unhashed(tx, signature); + (Self::Legacy(tx), buf) + } + MorphTxType::Eip2930 => { + let (tx, buf) = TxEip2930::from_compact(buf, buf.len()); + let tx = Signed::new_unhashed(tx, signature); + (Self::Eip2930(tx), buf) + } + MorphTxType::Eip1559 => { + let (tx, buf) = TxEip1559::from_compact(buf, buf.len()); + let tx = Signed::new_unhashed(tx, signature); + (Self::Eip1559(tx), buf) + } + MorphTxType::Eip7702 => { + let (tx, buf) = TxEip7702::from_compact(buf, buf.len()); + let tx = Signed::new_unhashed(tx, signature); + (Self::Eip7702(tx), buf) + } + MorphTxType::L1Msg => { + let (tx, buf) = TxL1Msg::from_compact(buf, buf.len()); + let tx = Signed::new_unhashed(tx, signature); + (Self::L1Msg(tx), buf) + } + MorphTxType::AltFee => { + let (tx, buf) = TxAltFee::from_compact(buf, buf.len()); + let tx = Signed::new_unhashed(tx, signature); + (Self::AltFee(tx), buf) + } + } + } + } + + impl reth_codecs::alloy::transaction::ToTxCompact for MorphTxEnvelope { + 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::Eip7702(tx) => tx.tx().to_compact(buf), + Self::L1Msg(tx) => tx.tx().to_compact(buf), + Self::AltFee(tx) => tx.tx().to_compact(buf), + }; + } + } + + impl Envelope for MorphTxEnvelope { + 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::L1Msg(tx) => tx.signature(), + Self::AltFee(tx) => tx.signature(), + } + } + + fn tx_type(&self) -> Self::TxType { + Self::tx_type(self) + } + } + + impl Compact for MorphTxType { + fn to_compact(&self, buf: &mut B) -> usize + where + B: BufMut + AsMut<[u8]>, + { + match self { + Self::Legacy => COMPACT_IDENTIFIER_LEGACY, + Self::Eip2930 => COMPACT_IDENTIFIER_EIP2930, + Self::Eip1559 => COMPACT_IDENTIFIER_EIP1559, + Self::Eip7702 => { + buf.put_u8(EIP7702_TX_TYPE_ID); + COMPACT_EXTENDED_IDENTIFIER_FLAG + } + Self::L1Msg => { + buf.put_u8(L1_TX_TYPE_ID); + COMPACT_EXTENDED_IDENTIFIER_FLAG + } + Self::AltFee => { + buf.put_u8(ALT_FEE_TX_TYPE_ID); + COMPACT_EXTENDED_IDENTIFIER_FLAG + } + } + } + + // For backwards compatibility purposes only 2 bits of the type are encoded in the identifier + // parameter. In the case of a [`COMPACT_EXTENDED_IDENTIFIER_FLAG`], the full transaction type + // is read from the buffer as a single byte. + fn from_compact(mut buf: &[u8], identifier: usize) -> (Self, &[u8]) { + use bytes::Buf; + ( + match identifier { + COMPACT_IDENTIFIER_LEGACY => Self::Legacy, + COMPACT_IDENTIFIER_EIP2930 => Self::Eip2930, + COMPACT_IDENTIFIER_EIP1559 => Self::Eip1559, + COMPACT_EXTENDED_IDENTIFIER_FLAG => { + let extended_identifier = buf.get_u8(); + match extended_identifier { + EIP7702_TX_TYPE_ID => Self::Eip7702, + crate::transaction::L1_TX_TYPE_ID => Self::L1Msg, + crate::transaction::ALT_FEE_TX_TYPE_ID => Self::AltFee, + _ => panic!("Unsupported TxType identifier: {extended_identifier}"), + } + } + _ => panic!("Unknown identifier for TxType: {identifier}"), + }, + buf, + ) + } + } + + impl Compact for MorphTxEnvelope { + fn to_compact(&self, buf: &mut B) -> usize + where + B: BufMut + AsMut<[u8]>, + { + CompactEnvelope::to_compact(self, buf) + } + + fn from_compact(buf: &[u8], len: usize) -> (Self, &[u8]) { + CompactEnvelope::from_compact(buf, len) + } + } + + impl reth_db_api::table::Compress for MorphTxEnvelope { + type Compressed = Vec; + + fn compress_to_buf>(&self, buf: &mut B) { + let _ = Compact::to_compact(self, buf); + } + } + + impl reth_db_api::table::Decompress for MorphTxEnvelope { + fn decompress(value: &[u8]) -> Result { + let (obj, _) = Compact::from_compact(value, value.len()); + Ok(obj) + } + } +} diff --git a/crates/primitives/src/transaction/l1_transaction.rs b/crates/primitives/src/transaction/l1_transaction.rs index f68128d..79b215a 100644 --- a/crates/primitives/src/transaction/l1_transaction.rs +++ b/crates/primitives/src/transaction/l1_transaction.rs @@ -1,13 +1,16 @@ //! L1 Message Transaction type for Morph L2. //! -//! This module defines the L1Transaction type which represents L1 message +//! This module defines the TxL1Msg type which represents L1 message //! transactions that are processed on Morph L2. //! //! Reference: -use alloy_consensus::Transaction; -use alloy_eips::{eip2718::Encodable2718, Typed2718}; -use alloy_primitives::{Address, Bytes, ChainId, TxKind, B256, U256, keccak256}; +use alloy_consensus::{ + SignableTransaction, Transaction, + transaction::{RlpEcdsaDecodableTx, RlpEcdsaEncodableTx}, +}; +use alloy_eips::{Typed2718, eip2718::Encodable2718}; +use alloy_primitives::{Address, B256, Bytes, ChainId, Signature, TxKind, U256, keccak256}; use alloy_rlp::{BufMut, Decodable, Encodable, Header}; use core::mem; @@ -23,7 +26,8 @@ pub const L1_TX_TYPE_ID: u8 = 0x7E; #[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] -pub struct L1Transaction { +#[cfg_attr(feature = "reth-codec", derive(reth_codecs::Compact))] +pub struct TxL1Msg { /// The 32-byte hash of the transaction. pub tx_hash: B256, @@ -59,7 +63,7 @@ pub struct L1Transaction { pub input: Bytes, } -impl L1Transaction { +impl TxL1Msg { /// Get the transaction type #[doc(alias = "transaction_type")] pub const fn tx_type() -> u8 { @@ -120,6 +124,18 @@ impl L1Transaction { self.from.encode(out); } + pub fn decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result { + Ok(Self { + tx_hash: Decodable::decode(buf)?, + nonce: Decodable::decode(buf)?, + gas_limit: Decodable::decode(buf)?, + to: Decodable::decode(buf)?, + value: Decodable::decode(buf)?, + input: Decodable::decode(buf)?, + from: Decodable::decode(buf)?, + }) + } + /// Computes the hash used for the transaction. /// /// For L1 messages, this computes the keccak256 hash of the RLP encoding. @@ -138,13 +154,13 @@ impl L1Transaction { } } -impl Typed2718 for L1Transaction { +impl Typed2718 for TxL1Msg { fn ty(&self) -> u8 { L1_TX_TYPE_ID } } -impl Transaction for L1Transaction { +impl Transaction for TxL1Msg { fn chain_id(&self) -> Option { None } @@ -214,7 +230,39 @@ impl Transaction for L1Transaction { } } -impl Encodable for L1Transaction { +impl RlpEcdsaEncodableTx for TxL1Msg { + fn rlp_encoded_fields_length(&self) -> usize { + self.fields_len() + } + + fn rlp_encode_fields(&self, out: &mut dyn BufMut) { + self.encode_fields(out); + } +} + +impl RlpEcdsaDecodableTx for TxL1Msg { + const DEFAULT_TX_TYPE: u8 = { Self::tx_type() as u8 }; + + /// Decodes the inner [TxEip1559] fields from RLP bytes. + fn rlp_decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result { + Self::decode_fields(buf) + } +} + +impl SignableTransaction for TxL1Msg { + fn set_chain_id(&mut self, _chain_id: ChainId) {} + + fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) { + out.put_u8(Self::tx_type() as u8); + self.encode(out) + } + + fn payload_len_for_signature(&self) -> usize { + self.length() + 1 + } +} + +impl Encodable for TxL1Msg { fn encode(&self, out: &mut dyn BufMut) { self.rlp_header().encode(out); self.encode_fields(out); @@ -225,7 +273,7 @@ impl Encodable for L1Transaction { } } -impl Decodable for L1Transaction { +impl Decodable for TxL1Msg { fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { let header = Header::decode(buf)?; if !header.list { @@ -263,7 +311,7 @@ impl Decodable for L1Transaction { } } -impl Encodable2718 for L1Transaction { +impl Encodable2718 for TxL1Msg { fn type_flag(&self) -> Option { Some(L1_TX_TYPE_ID) } @@ -289,6 +337,12 @@ impl Encodable2718 for L1Transaction { } } +impl reth_primitives_traits::InMemorySize for TxL1Msg { + fn size(&self) -> usize { + Self::size(self) + } +} + #[cfg(test)] mod tests { use super::*; @@ -296,7 +350,7 @@ mod tests { #[test] fn test_l1_transaction_default() { - let tx = L1Transaction::default(); + let tx = TxL1Msg::default(); assert_eq!(tx.nonce, 0); assert_eq!(tx.gas_limit, 0); assert_eq!(tx.value, U256::ZERO); @@ -305,19 +359,19 @@ mod tests { #[test] fn test_l1_transaction_tx_type() { - assert_eq!(L1Transaction::tx_type(), L1_TX_TYPE_ID); - assert_eq!(L1Transaction::tx_type(), 0x7E); + assert_eq!(TxL1Msg::tx_type(), L1_TX_TYPE_ID); + assert_eq!(TxL1Msg::tx_type(), 0x7E); } #[test] fn test_l1_transaction_validate() { - let tx = L1Transaction::default(); + let tx = TxL1Msg::default(); assert!(tx.validate().is_ok()); } #[test] fn test_l1_transaction_trait_methods() { - let tx = L1Transaction { + let tx = TxL1Msg { tx_hash: B256::ZERO, from: address!("0000000000000000000000000000000000000001"), nonce: 42, @@ -353,13 +407,13 @@ mod tests { #[test] fn test_l1_transaction_is_create() { - let create_tx = L1Transaction { + let create_tx = TxL1Msg { to: TxKind::Create, ..Default::default() }; assert!(create_tx.is_create()); - let call_tx = L1Transaction { + let call_tx = TxL1Msg { to: TxKind::Call(address!("0000000000000000000000000000000000000001")), ..Default::default() }; @@ -368,16 +422,19 @@ mod tests { #[test] fn test_l1_transaction_sender() { - let tx = L1Transaction { + let tx = TxL1Msg { from: address!("0000000000000000000000000000000000000001"), ..Default::default() }; - assert_eq!(tx.sender(), address!("0000000000000000000000000000000000000001")); + assert_eq!( + tx.sender(), + address!("0000000000000000000000000000000000000001") + ); } #[test] fn test_l1_transaction_signature_hash() { - let tx = L1Transaction { + let tx = TxL1Msg { tx_hash: B256::ZERO, from: address!("0000000000000000000000000000000000000001"), nonce: 1, @@ -393,7 +450,7 @@ mod tests { #[test] fn test_l1_transaction_rlp_roundtrip() { - let tx = L1Transaction { + let tx = TxL1Msg { tx_hash: B256::ZERO, from: address!("0000000000000000000000000000000000000001"), nonce: 42, @@ -408,7 +465,7 @@ mod tests { tx.encode(&mut buf); // Decode - let decoded = L1Transaction::decode(&mut buf.as_slice()).expect("Should decode"); + let decoded = TxL1Msg::decode(&mut buf.as_slice()).expect("Should decode"); assert_eq!(tx.from, decoded.from); assert_eq!(tx.nonce, decoded.nonce); @@ -420,7 +477,7 @@ mod tests { #[test] fn test_l1_transaction_create() { - let tx = L1Transaction { + let tx = TxL1Msg { tx_hash: B256::ZERO, from: address!("0000000000000000000000000000000000000001"), nonce: 0, @@ -435,14 +492,14 @@ mod tests { tx.encode(&mut buf); // Decode - let decoded = L1Transaction::decode(&mut buf.as_slice()).expect("Should decode"); + let decoded = TxL1Msg::decode(&mut buf.as_slice()).expect("Should decode"); assert_eq!(decoded.to, TxKind::Create); } #[test] fn test_l1_transaction_encode_2718() { - let tx = L1Transaction { + let tx = TxL1Msg { tx_hash: B256::ZERO, from: address!("0000000000000000000000000000000000000001"), nonce: 1, @@ -467,7 +524,7 @@ mod tests { #[test] fn test_l1_transaction_decode_rejects_malformed_rlp() { - let tx = L1Transaction { + let tx = TxL1Msg { tx_hash: B256::ZERO, from: address!("0000000000000000000000000000000000000001"), nonce: 42, @@ -485,8 +542,11 @@ mod tests { let original_len = buf.len(); buf.truncate(original_len - 5); - let result = L1Transaction::decode(&mut buf.as_slice()); - assert!(result.is_err(), "Decoding should fail when data is truncated"); + let result = TxL1Msg::decode(&mut buf.as_slice()); + assert!( + result.is_err(), + "Decoding should fail when data is truncated" + ); assert!(matches!( result.unwrap_err(), alloy_rlp::Error::InputTooShort | alloy_rlp::Error::UnexpectedLength @@ -495,7 +555,7 @@ mod tests { #[test] fn test_l1_transaction_size() { - let tx = L1Transaction { + let tx = TxL1Msg { tx_hash: B256::ZERO, from: Address::ZERO, nonce: 0, @@ -518,7 +578,7 @@ mod tests { #[test] fn test_l1_transaction_fields_len() { - let tx = L1Transaction { + let tx = TxL1Msg { tx_hash: B256::ZERO, from: address!("0000000000000000000000000000000000000001"), nonce: 1, @@ -538,7 +598,7 @@ mod tests { #[test] fn test_l1_transaction_encode_fields() { - let tx = L1Transaction { + let tx = TxL1Msg { tx_hash: B256::ZERO, from: address!("0000000000000000000000000000000000000001"), nonce: 1, diff --git a/crates/primitives/src/transaction/mod.rs b/crates/primitives/src/transaction/mod.rs index 107b268..b27a8d1 100644 --- a/crates/primitives/src/transaction/mod.rs +++ b/crates/primitives/src/transaction/mod.rs @@ -1,4 +1,9 @@ //! Morph transaction types. -mod l1_transaction; -pub use l1_transaction::{L1Transaction, L1_TX_TYPE_ID}; +pub mod alt_fee; +pub mod envelope; +pub mod l1_transaction; + +pub use alt_fee::{ALT_FEE_TX_TYPE_ID, TxAltFee, TxAltFeeExt}; +pub use envelope::MorphTxEnvelope; +pub use l1_transaction::{L1_TX_TYPE_ID, TxL1Msg}; diff --git a/crates/revm/Cargo.toml b/crates/revm/Cargo.toml index 6bed471..6bfceb7 100644 --- a/crates/revm/Cargo.toml +++ b/crates/revm/Cargo.toml @@ -24,6 +24,8 @@ alloy-evm.workspace = true alloy-primitives.workspace = true alloy-consensus.workspace = true alloy-sol-types.workspace = true +alloy-rlp.workspace = true +alloy-eips.workspace = true auto_impl.workspace = true derive_more.workspace = true diff --git a/crates/revm/src/error.rs b/crates/revm/src/error.rs index ef03133..41fd53c 100644 --- a/crates/revm/src/error.rs +++ b/crates/revm/src/error.rs @@ -1,6 +1,7 @@ //! Morph-specific transaction validation errors. use alloy_evm::error::InvalidTxError; +use alloy_primitives::U256; use revm::context::result::{EVMError, HaltReason, InvalidTransaction}; /// Morph-specific invalid transaction errors. @@ -9,18 +10,39 @@ pub enum MorphInvalidTransaction { /// Standard Ethereum transaction validation error. #[error(transparent)] EthInvalidTransaction(#[from] InvalidTransaction), + + /// Token is not registered in the Token Registry. + #[error("Token with ID {0} is not registered")] + TokenNotRegistered(u16), + + /// Token is not active for gas payment. + #[error("Token with ID {0} is not active for gas payment")] + TokenNotActive(u16), + + /// Insufficient token balance for gas payment. + #[error( + "Insufficient token balance for gas payment: required {required}, available {available}" + )] + InsufficientTokenBalance { + /// Required token amount. + required: U256, + /// Available token balance. + available: U256, + }, } impl InvalidTxError for MorphInvalidTransaction { fn is_nonce_too_low(&self) -> bool { match self { Self::EthInvalidTransaction(err) => err.is_nonce_too_low(), + _ => false, } } fn as_invalid_tx_err(&self) -> Option<&InvalidTransaction> { match self { Self::EthInvalidTransaction(err) => Some(err), + _ => None, } } } diff --git a/crates/revm/src/evm.rs b/crates/revm/src/evm.rs index 1e794b9..72d0778 100644 --- a/crates/revm/src/evm.rs +++ b/crates/revm/src/evm.rs @@ -1,6 +1,7 @@ use crate::{MorphBlockEnv, MorphTxEnv}; use alloy_evm::{Database, precompiles::PrecompilesMap}; use alloy_primitives::Log; +use morph_chainspec::hardfork::MorphHardfork; use revm::{ Context, Inspector, context::{CfgEnv, ContextError, Evm, FrameStack}, @@ -11,7 +12,6 @@ use revm::{ inspector::InspectorEvmTr, interpreter::interpreter::EthInterpreter, }; -use morph_chainspec::hardfork::MorphHardfork; /// The Morph EVM context type. pub type MorphContext = Context, DB>; diff --git a/crates/revm/src/handler.rs b/crates/revm/src/handler.rs index 1d828a4..1083117 100644 --- a/crates/revm/src/handler.rs +++ b/crates/revm/src/handler.rs @@ -9,16 +9,9 @@ use revm::{ result::{EVMError, ExecutionResult, InvalidTransaction}, }, context_interface::Block, - handler::{ - EvmTr, FrameTr, Handler, MainnetHandler, - pre_execution, - validation, - }, + handler::{EvmTr, FrameTr, Handler, MainnetHandler, pre_execution, validation}, inspector::{Inspector, InspectorHandler}, - interpreter::{ - InitialAndFloorGas, - interpreter::EthInterpreter, - }, + interpreter::{InitialAndFloorGas, interpreter::EthInterpreter}, }; use crate::{ @@ -26,6 +19,7 @@ use crate::{ error::MorphHaltReason, evm::MorphContext, l1block::L1BlockInfo, + token_fee::{TokenFeeInfo, get_mapping_account_slot}, tx::MorphTxExt, }; @@ -107,46 +101,15 @@ where return Ok(()); } - // Get the current hardfork for L1 fee calculation - let hardfork = evm.ctx_ref().cfg().spec(); - - // Fetch L1 block info from the L1 Gas Price Oracle contract - let l1_block_info = L1BlockInfo::try_fetch(evm.ctx_mut().db_mut(), hardfork)?; - - // Get transaction input data for L1 fee calculation - let tx_input = evm.ctx_ref().tx().input(); - - // Calculate L1 data fee - let l1_data_fee = l1_block_info.calculate_tx_l1_cost(tx_input, hardfork); - - // Get mutable access to context components - let (block, tx, cfg, journal, _, _) = evm.ctx().all_mut(); - - // Load caller's account - let mut caller = journal.load_account_with_code_mut(tx.caller())?.data; - - // Validate account nonce and code (EIP-3607) - pre_execution::validate_account_nonce_and_code( - &caller.info, - tx.nonce(), - cfg.is_eip3607_disabled(), - cfg.is_nonce_check_disabled(), - )?; - - // Calculate L2 fee and validate balance - // This includes: gas_limit * gas_price + value + blob_fee - let new_balance_after_l2_fee = - calculate_caller_fee_with_l1_cost(*caller.balance(), tx, block, cfg, l1_data_fee)?; - - // Set the new balance (deducting L2 fee + L1 data fee) - caller.set_balance(new_balance_after_l2_fee); - - // Bump nonce for calls (CREATE nonce is bumped in make_create_frame) - if tx.kind().is_call() { - caller.bump_nonce(); + // Check if transaction is AltFeeTx (tx_type 0x7F) which uses token fee + if evm.ctx_ref().tx().is_alt_fee_tx() { + // Get fee_token_id directly from MorphTxEnv + let token_id = evm.ctx_ref().tx().fee_token_id.unwrap_or_default(); + return self.validate_and_deduct_token_fee(evm, token_id); } - Ok(()) + // Standard ETH-based fee handling + self.validate_and_deduct_eth_fee(evm) } fn reimburse_caller( @@ -168,11 +131,56 @@ where #[inline] fn reward_beneficiary( &self, - _evm: &mut Self::Evm, - _exec_result: &mut <::Frame as FrameTr>::FrameResult, + evm: &mut Self::Evm, + exec_result: &mut <::Frame as FrameTr>::FrameResult, ) -> Result<(), Self::Error> { - // For Morph L2, beneficiary reward is handled differently - // The sequencer collects fees through the L1 fee vault mechanism + // L1 message transactions skip all validation - everything is handled on L1 side + if evm.ctx_ref().tx().is_l1_msg() { + return Ok(()); + } + // AltFeeTx rewards are already applied when gasFee is deducted. + if evm.ctx_ref().tx().is_alt_fee_tx() { + return Ok(()); + } + + let beneficiary = evm.ctx_ref().block().beneficiary(); + + let basefee = evm.ctx_ref().block().basefee() as u128; + let effective_gas_price = evm.ctx_ref().tx().effective_gas_price(basefee); + + // Get the current hardfork for L1 fee calculation + let hardfork = evm.ctx_ref().cfg().spec(); + + // Fetch L1 block info from the L1 Gas Price Oracle contract + let l1_block_info = L1BlockInfo::try_fetch(evm.ctx_mut().db_mut(), hardfork)?; + + // Get RLP-encoded transaction bytes for L1 fee calculation + // This represents the full transaction data posted to L1 for data availability + let rlp_bytes = evm + .ctx_ref() + .tx() + .rlp_bytes + .as_ref() + .map(|b| b.as_ref()) + .unwrap_or_default(); + + // Calculate L1 data fee based on full RLP-encoded transaction + let l1_data_fee = l1_block_info.calculate_tx_l1_cost(rlp_bytes, hardfork); + + // Get mutable access to context components + let journal = evm.ctx().journal_mut(); + + let gas_spent = exec_result.gas().spent(); + let gas_refunded = exec_result.gas().refunded() as u64; + let gas_used = gas_spent - gas_refunded; + + let execution_fee = U256::from(effective_gas_price).saturating_mul(U256::from(gas_used)); + + // reward beneficiary + journal + .load_account_mut(beneficiary)? + .incr_balance(execution_fee.saturating_add(l1_data_fee)); + Ok(()) } @@ -228,6 +236,174 @@ where } } +// Helper methods for MorphEvmHandler +impl MorphEvmHandler +where + DB: alloy_evm::Database, +{ + /// Validate and deduct ETH-based gas fees. + fn validate_and_deduct_eth_fee( + &self, + evm: &mut MorphEvm, + ) -> Result<(), EVMError> { + // Get the current hardfork for L1 fee calculation + let hardfork = evm.ctx_ref().cfg().spec(); + + // Fetch L1 block info from the L1 Gas Price Oracle contract + let l1_block_info = L1BlockInfo::try_fetch(evm.ctx_mut().db_mut(), hardfork)?; + + // Get RLP-encoded transaction bytes for L1 fee calculation + // This represents the full transaction data posted to L1 for data availability + let rlp_bytes = evm + .ctx_ref() + .tx() + .rlp_bytes + .as_ref() + .map(|b| b.as_ref()) + .unwrap_or_default(); + + // Calculate L1 data fee based on full RLP-encoded transaction + let l1_data_fee = l1_block_info.calculate_tx_l1_cost(rlp_bytes, hardfork); + + // Get mutable access to context components + let (block, tx, cfg, journal, _, _) = evm.ctx().all_mut(); + + // Load caller's account + let mut caller = journal.load_account_with_code_mut(tx.caller())?.data; + + // Validate account nonce and code (EIP-3607) + pre_execution::validate_account_nonce_and_code( + &caller.info, + tx.nonce(), + cfg.is_eip3607_disabled(), + cfg.is_nonce_check_disabled(), + )?; + + // Calculate L2 fee and validate balance + // This includes: gas_limit * gas_price + value + blob_fee + let new_balance_after_l2_fee = + calculate_caller_fee_with_l1_cost(*caller.balance(), tx, block, cfg, l1_data_fee)?; + + // Set the new balance (deducting L2 fee + L1 data fee) + caller.set_balance(new_balance_after_l2_fee); + + // Bump nonce for calls (CREATE nonce is bumped in make_create_frame) + if tx.kind().is_call() { + caller.bump_nonce(); + } + + Ok(()) + } + + /// Validate and deduct token-based gas fees. + /// + /// This handles gas payment using ERC20 tokens instead of ETH. + fn validate_and_deduct_token_fee( + &self, + evm: &mut MorphEvm, + token_id: u16, + ) -> Result<(), EVMError> { + // Get caller address + let caller_addr = evm.ctx_ref().tx().caller(); + // Get coinbase address + let beneficiary = evm.ctx_ref().block().beneficiary(); + + // Fetch token fee info from Token Registry + let token_fee_info = + TokenFeeInfo::try_fetch(evm.ctx_mut().db_mut(), token_id, caller_addr)? + .ok_or(MorphInvalidTransaction::TokenNotRegistered(token_id))?; + + // Check if token is active + if !token_fee_info.is_active { + return Err(MorphInvalidTransaction::TokenNotActive(token_id).into()); + } + + // Get the current hardfork for L1 fee calculation + let hardfork = evm.ctx_ref().cfg().spec(); + + // Fetch L1 block info from the L1 Gas Price Oracle contract + let l1_block_info = L1BlockInfo::try_fetch(evm.ctx_mut().db_mut(), hardfork)?; + + // Get RLP-encoded transaction bytes for L1 fee calculation + // This represents the full transaction data posted to L1 for data availability + let rlp_bytes = evm + .ctx_ref() + .tx() + .rlp_bytes + .as_ref() + .map(|b| b.as_ref()) + .unwrap_or_default(); + + // Calculate L1 data fee (in ETH) based on full RLP-encoded transaction + let l1_data_fee = l1_block_info.calculate_tx_l1_cost(rlp_bytes, hardfork); + + // Calculate L2 gas fee (in ETH) + let gas_limit = evm.ctx_ref().tx().gas_limit(); + let gas_price = evm.ctx_ref().tx().gas_price(); + let l2_gas_fee = U256::from(gas_limit).saturating_mul(U256::from(gas_price)); + + // Total fee in ETH + let total_eth_fee = l2_gas_fee.saturating_add(l1_data_fee); + + // Calculate token amount required for total fee + let token_amount_required = token_fee_info.calculate_token_amount(total_eth_fee); + + // Check if caller has sufficient token balance + if token_fee_info.balance < token_amount_required { + return Err(MorphInvalidTransaction::InsufficientTokenBalance { + required: token_amount_required, + available: token_fee_info.balance, + } + .into()); + } + + // Get mutable access to context components + let (_, tx, cfg, journal, _, _) = evm.ctx().all_mut(); + + // First, deduct token fee from caller's ERC20 balance + // This updates the ERC20 token's storage directly + if let Some(balance_slot) = token_fee_info.balance_slot { + // Sub amount + let token_storage_slot = get_mapping_account_slot(balance_slot, caller_addr); + let new_token_balance = token_fee_info.balance.saturating_sub(token_amount_required); + journal.sstore( + token_fee_info.token_address, + token_storage_slot, + new_token_balance, + )?; + + // Add amount + let token_storage_slot = get_mapping_account_slot(balance_slot, beneficiary); + let balance = journal + .sload(token_fee_info.token_address, token_storage_slot) + .unwrap_or_default(); + journal.sstore( + beneficiary, + token_storage_slot, + balance.saturating_sub(token_amount_required), + )?; + } + + // Load caller's account for nonce/code validation + let mut caller = journal.load_account_with_code_mut(tx.caller())?.data; + + // Validate account nonce and code (EIP-3607) + pre_execution::validate_account_nonce_and_code( + &caller.info, + tx.nonce(), + cfg.is_eip3607_disabled(), + cfg.is_nonce_check_disabled(), + )?; + + // Bump nonce for calls (CREATE nonce is bumped in make_create_frame) + if tx.kind().is_call() { + caller.bump_nonce(); + } + + Ok(()) + } +} + /// Calculate the new balance after deducting L2 fees and L1 data fee. /// /// This is a Morph-specific version of `pre_execution::calculate_caller_fee` that @@ -260,7 +436,6 @@ fn calculate_caller_fee_with_l1_cost( // Total spending = L2 fees + L1 data fee let total_spending = effective_balance_spending.saturating_add(l1_data_fee); - // Check if caller has enough balance for total spending if !is_balance_check_disabled { if balance < total_spending { diff --git a/crates/revm/src/l1block.rs b/crates/revm/src/l1block.rs index 01b2061..c438860 100644 --- a/crates/revm/src/l1block.rs +++ b/crates/revm/src/l1block.rs @@ -217,4 +217,3 @@ mod tests { assert_eq!(cost, U256::from(80_000_000_000u64)); } } - diff --git a/crates/revm/src/lib.rs b/crates/revm/src/lib.rs index 11be614..fd95de4 100644 --- a/crates/revm/src/lib.rs +++ b/crates/revm/src/lib.rs @@ -6,11 +6,11 @@ mod block; // Suppress unused_crate_dependencies warnings #[cfg(not(test))] -use tracing as _; -#[cfg(not(test))] use alloy_consensus as _; #[cfg(not(test))] use alloy_sol_types as _; +#[cfg(not(test))] +use tracing as _; mod common; pub use common::{MorphStateAccess, MorphTx}; @@ -19,10 +19,12 @@ pub mod evm; pub mod exec; pub mod handler; pub mod l1block; +pub mod token_fee; mod tx; pub use block::MorphBlockEnv; pub use error::{MorphHaltReason, MorphInvalidTransaction}; pub use evm::MorphEvm; -pub use l1block::{L1BlockInfo, L1_GAS_PRICE_ORACLE_ADDRESS}; +pub use l1block::{L1_GAS_PRICE_ORACLE_ADDRESS, L1BlockInfo}; +pub use token_fee::{L2_TOKEN_REGISTRY_ADDRESS, TokenFeeInfo, get_erc20_balance_with_evm}; pub use tx::{MorphTxEnv, MorphTxExt}; diff --git a/crates/revm/src/token_fee.rs b/crates/revm/src/token_fee.rs new file mode 100644 index 0000000..6efa461 --- /dev/null +++ b/crates/revm/src/token_fee.rs @@ -0,0 +1,393 @@ +//! Token Fee Info for Morph L2 ERC20 gas payment. +//! +//! This module provides the infrastructure for fetching ERC20 token prices +//! and calculating gas fees in alternative tokens. +//! +//! Reference: + +use alloy_evm::Database; +use alloy_primitives::{Address, Bytes, TxKind, U256, address, keccak256}; +use morph_primitives::L1_TX_TYPE_ID; +use revm::{ + ExecuteEvm, Inspector, + context::TxEnv, + context_interface::{ContextTr, result::EVMError}, + handler::EvmTr, +}; + +use crate::evm::MorphContext; +use crate::{MorphEvm, MorphInvalidTransaction}; + +/// L2 Token Registry contract address on Morph L2. +/// Reference: +pub const L2_TOKEN_REGISTRY_ADDRESS: Address = address!("5300000000000000000000000000000000000021"); + +/// TokenRegistry storage slot for mapping(uint16 => TokenInfo) - slot 151. +const TOKEN_REGISTRY_SLOT: U256 = U256::from_limbs([151u64, 0, 0, 0]); +/// PriceRatio storage slot for mapping(uint16 => uint256) - slot 153. +const PRICE_RATIO_SLOT: U256 = U256::from_limbs([153u64, 0, 0, 0]); + +/// Token fee information for ERC20 gas payment. +/// +/// Contains the token parameters fetched from the L2 Token Registry contract. +/// These parameters are used to calculate gas fees in alternative ERC20 tokens. +#[derive(Clone, Debug, Default)] +pub struct TokenFeeInfo { + /// The fee token address. + pub token_address: Address, + /// Whether the token is active for gas payment. + pub is_active: bool, + /// Token decimals. + pub decimals: u8, + /// The price ratio of the token (relative to ETH). + pub price_ratio: U256, + /// The scale of the token for price calculation. + pub scale: U256, + /// The caller address. + pub caller: Address, + /// The token balance of the caller. + pub balance: U256, + /// The user's ERC20 balance storage slot (if known). + pub balance_slot: Option, +} + +impl TokenFeeInfo { + /// Try to fetch the token fee information from the database. + /// + /// This reads the token parameters from the L2 Token Registry contract storage. + /// Returns `None` if the token is not registered. + pub fn try_fetch( + db: &mut DB, + token_id: u16, + caller: Address, + ) -> Result, DB::Error> { + // Get the base slot for this token_id in tokenRegistry mapping + let mut token_id_bytes = [0u8; 32]; + token_id_bytes[30..32].copy_from_slice(&token_id.to_be_bytes()); + let token_registry_base = get_mapping_slot(TOKEN_REGISTRY_SLOT, token_id_bytes.to_vec()); + + // TokenInfo struct layout in storage (following Solidity storage packing rules): + // slot + 0: tokenAddress (address, 20 bytes) + 12 bytes padding + // slot + 1: balanceSlot (bytes32, 32 bytes) + // slot + 2: isActive (bool, 1 byte) + decimals (uint8, 1 byte) + 30 bytes padding + // slot + 3: scale (uint256, 32 bytes) + + // Read tokenAddress from slot + 0 + let slot_0 = db.storage(L2_TOKEN_REGISTRY_ADDRESS, token_registry_base)?; + let token_address = Address::from_word(slot_0.into()); + if token_address == Address::default() { + return Ok(None); + } + + // Read balanceSlot from slot + 1 + let balance_slot_value = db.storage( + L2_TOKEN_REGISTRY_ADDRESS, + token_registry_base + U256::from(1), + )?; + let token_balance_slot = if !balance_slot_value.is_zero() { + Some(balance_slot_value.saturating_sub(U256::from(1u64))) + } else { + None + }; + + // Read isActive and decimals from slot + 2 + // In big-endian representation, rightmost byte is the lowest position + // isActive is at the rightmost (byte 31), decimals is to its left (byte 30) + let slot_2 = db.storage( + L2_TOKEN_REGISTRY_ADDRESS, + token_registry_base + U256::from(2), + )?; + let slot_2_bytes = slot_2.to_be_bytes::<32>(); + let is_active = slot_2_bytes[31] != 0; + let decimals = slot_2_bytes[30]; + + // Read scale from slot + 3 + let scale = db.storage( + L2_TOKEN_REGISTRY_ADDRESS, + token_registry_base + U256::from(3), + )?; + + // Get price ratio from priceRatio mapping + let price_ratio = load_mapping_value( + db, + L2_TOKEN_REGISTRY_ADDRESS, + PRICE_RATIO_SLOT, + token_id_bytes.to_vec(), + )?; + + // Get caller's token balance + let caller_token_balance = + get_erc20_balance(db, token_address, caller, token_balance_slot)?; + + let token_fee = TokenFeeInfo { + token_address, + is_active, + decimals, + price_ratio, + scale, + caller, + balance: caller_token_balance, + balance_slot: token_balance_slot, + }; + + Ok(Some(token_fee)) + } + + /// Calculate the token amount required for a given ETH amount. + /// + /// Uses the price ratio and scale to convert ETH value to token amount. + pub fn calculate_token_amount(&self, eth_amount: U256) -> U256 { + if self.price_ratio.is_zero() { + return U256::ZERO; + } + + // token_amount = eth_amount * scale / price_ratio + eth_amount + .saturating_mul(self.scale) + .wrapping_div(self.price_ratio) + } + + /// Check if the caller has sufficient token balance for the given ETH amount. + pub fn has_sufficient_balance(&self, eth_amount: U256) -> bool { + let required = self.calculate_token_amount(eth_amount); + self.balance >= required + } +} + +/// Calculate the storage slot for a mapping value. +/// +/// For a mapping `mapping(keyType => valueType)` at storage slot `slot_index`, +/// the value for `key` is stored at `keccak256(key || slot_index)`. +pub fn get_mapping_slot(slot_index: U256, mut key: Vec) -> U256 { + let mut pre_image = slot_index.to_be_bytes_vec(); + key.append(&mut pre_image); + let storage_key = keccak256(key); + U256::from_be_bytes(storage_key.0) +} + +/// Calculate the account's storage slot for a mapping value. +/// +/// For address-keyed mappings, the address is left-padded to 32 bytes. +#[inline] +pub fn get_mapping_account_slot(slot_index: U256, account: Address) -> U256 { + let mut key = [0u8; 32]; + key[12..32].copy_from_slice(account.as_slice()); + get_mapping_slot(slot_index, key.to_vec()) +} + +/// Load a value from a mapping in contract storage. +fn load_mapping_value( + db: &mut DB, + account: Address, + slot_index: U256, + key: Vec, +) -> Result { + let storage_slot = get_mapping_slot(slot_index, key); + let storage_value = db.storage(account, storage_slot)?; + Ok(storage_value) +} + +/// Gas limit for ERC20 balance query calls. +const BALANCE_OF_GAS_LIMIT: u64 = 1_000_000; + +/// Get ERC20 token balance for an account (storage-only version). +/// +/// First tries to read directly from storage if the balance slot is known. +/// If the balance slot is not available, returns zero. +/// +/// Use [`get_erc20_balance_with_evm`] if you have access to a `MorphEvm` instance +/// and need the EVM call fallback. +pub fn get_erc20_balance( + db: &mut DB, + token: Address, + account: Address, + token_balance_slot: Option, +) -> Result { + // If balance slot is provided, read directly from storage + if let Some(slot) = token_balance_slot { + let mut data = [0u8; 32]; + data[12..32].copy_from_slice(account.as_slice()); + if let Ok(balance) = load_mapping_value(db, token, slot, data.to_vec()) { + return Ok(balance); + } + } + + // If balance slot is not available, return zero. + // Use get_erc20_balance_with_evm for EVM call fallback. + Ok(U256::ZERO) +} + +/// Get ERC20 token balance for an account with EVM call fallback. +/// +/// First tries to read directly from storage if the balance slot is known. +/// If the balance slot is not available, falls back to executing an EVM call +/// to `balanceOf(address)` using the provided `MorphEvm` instance. +pub fn get_erc20_balance_with_evm( + evm: &mut MorphEvm, + token: Address, + account: Address, + token_balance_slot: Option, +) -> Result> +where + DB: Database, + I: Inspector>, +{ + // First try storage-based lookup + if let Some(slot) = token_balance_slot { + let mut data = [0u8; 32]; + data[12..32].copy_from_slice(account.as_slice()); + let storage_slot = get_mapping_slot(slot, data.to_vec()); + if let Ok(balance) = evm.ctx_mut().db_mut().storage(token, storage_slot) { + return Ok(balance); + } + } + + // Fallback: Execute EVM call to balanceOf(address) + let calldata = build_balance_of_calldata(account); + + // Create a minimal transaction environment for the call + let mut tx = TxEnv::default(); + tx.caller = Address::ZERO; + tx.gas_limit = BALANCE_OF_GAS_LIMIT; + tx.kind = TxKind::Call(token); + tx.value = U256::ZERO; + tx.data = calldata; + tx.nonce = 0; + tx.tx_type = L1_TX_TYPE_ID; // Mark as L1 message to skip gas validation + + // Convert to MorphTxEnv + let morph_tx = crate::MorphTxEnv::new(tx); + + // Execute using transact_one + match evm.transact_one(morph_tx) { + Ok(result) => { + if result.is_success() { + // Parse the returned balance (32 bytes) + if let Some(output) = result.output() { + if output.len() >= 32 { + return Ok(U256::from_be_slice(&output[..32])); + } + } + } + Ok(U256::ZERO) + } + Err(_) => { + // On error, return zero (matches original behavior) + Ok(U256::ZERO) + } + } +} + +/// Build the calldata for ERC20 balanceOf(address) call. +/// +/// Method signature: `balanceOf(address) -> 0x70a08231` +pub fn build_balance_of_calldata(account: Address) -> Bytes { + let method_id = [0x70u8, 0xa0, 0x82, 0x31]; + let mut calldata = Vec::with_capacity(36); + calldata.extend_from_slice(&method_id); + calldata.extend_from_slice(&[0u8; 12]); // Pad address to 32 bytes + calldata.extend_from_slice(account.as_slice()); + Bytes::from(calldata) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_token_fee_info_default() { + let info = TokenFeeInfo::default(); + assert_eq!(info.token_address, Address::default()); + assert!(!info.is_active); + assert_eq!(info.decimals, 0); + assert_eq!(info.price_ratio, U256::ZERO); + assert_eq!(info.scale, U256::ZERO); + assert_eq!(info.balance, U256::ZERO); + assert!(info.balance_slot.is_none()); + } + + #[test] + fn test_get_mapping_slot() { + // Test that mapping slot calculation produces deterministic results + let slot = U256::from(151); + let key = vec![0u8; 32]; + let result1 = get_mapping_slot(slot, key.clone()); + let result2 = get_mapping_slot(slot, key); + assert_eq!(result1, result2); + } + + #[test] + fn test_get_mapping_account_slot() { + let slot = U256::from(1); + let account = address!("1234567890123456789012345678901234567890"); + let result = get_mapping_account_slot(slot, account); + // Result should be non-zero + assert!(!result.is_zero()); + } + + #[test] + fn test_calculate_token_amount() { + let info = TokenFeeInfo { + price_ratio: U256::from(2_000_000_000_000_000_000u128), // 2 ETH per token + scale: U256::from(1_000_000_000_000_000_000u128), // 1e18 + ..Default::default() + }; + + // 1 ETH should give 0.5 tokens + let eth_amount = U256::from(1_000_000_000_000_000_000u128); // 1 ETH + let token_amount = info.calculate_token_amount(eth_amount); + assert_eq!(token_amount, U256::from(500_000_000_000_000_000u128)); // 0.5 tokens + } + + #[test] + fn test_calculate_token_amount_zero_ratio() { + let info = TokenFeeInfo { + price_ratio: U256::ZERO, + scale: U256::from(1_000_000_000_000_000_000u128), + ..Default::default() + }; + + let eth_amount = U256::from(1_000_000_000_000_000_000u128); + let token_amount = info.calculate_token_amount(eth_amount); + assert_eq!(token_amount, U256::ZERO); + } + + #[test] + fn test_has_sufficient_balance() { + let info = TokenFeeInfo { + price_ratio: U256::from(1_000_000_000_000_000_000u128), // 1:1 + scale: U256::from(1_000_000_000_000_000_000u128), + balance: U256::from(1_000_000_000_000_000_000u128), // 1 token + ..Default::default() + }; + + // Has exactly enough + assert!(info.has_sufficient_balance(U256::from(1_000_000_000_000_000_000u128))); + // Has more than enough + assert!(info.has_sufficient_balance(U256::from(500_000_000_000_000_000u128))); + // Not enough + assert!(!info.has_sufficient_balance(U256::from(2_000_000_000_000_000_000u128))); + } + + #[test] + fn test_build_balance_of_calldata() { + let account = address!("1234567890123456789012345678901234567890"); + let calldata = build_balance_of_calldata(account); + + // Should be 4 bytes method id + 32 bytes address = 36 bytes + assert_eq!(calldata.len(), 36); + // First 4 bytes should be the method id + assert_eq!(&calldata[0..4], &[0x70, 0xa0, 0x82, 0x31]); + // Last 20 bytes should be the address + assert_eq!(&calldata[16..36], account.as_slice()); + } + + #[test] + fn test_l2_token_registry_address() { + // Verify the constant is set correctly + assert_eq!( + L2_TOKEN_REGISTRY_ADDRESS, + address!("5300000000000000000000000000000000000021") + ); + } +} diff --git a/crates/revm/src/tx.rs b/crates/revm/src/tx.rs index d0bbded..93be407 100644 --- a/crates/revm/src/tx.rs +++ b/crates/revm/src/tx.rs @@ -1,19 +1,346 @@ //! Morph transaction environment. //! -//! This module defines the Morph-specific transaction environment. +//! This module defines the Morph-specific transaction environment with token fee support. -use morph_primitives::L1_TX_TYPE_ID; -use revm::context::TxEnv; +use alloy_consensus::{EthereumTxEnvelope, Transaction as AlloyTransaction, TxEip4844}; +use alloy_eips::eip2718::Encodable2718; +use alloy_eips::eip2930::AccessList; +use alloy_primitives::{Address, B256, Bytes, TxKind, U256}; +use alloy_rlp::Decodable; +use morph_primitives::{ALT_FEE_TX_TYPE_ID, L1_TX_TYPE_ID, MorphTxEnvelope, TxAltFee}; +use reth_evm::{FromRecoveredTx, FromTxWithEncoded, ToTxEnv, TransactionEnv}; +use revm::context::{Transaction, TxEnv}; +use revm::context_interface::transaction::{ + AccessListItem, RecoveredAuthorization, SignedAuthorization, +}; +use std::ops::{Deref, DerefMut}; -/// Morph transaction environment. +/// Re-export Either for authorization list +use alloy_consensus::transaction::Either; + +/// Morph transaction environment with token fee support. /// -/// An alias for [`TxEnv`] for backwards compatibility. -pub type MorphTxEnv = TxEnv; +/// This wraps the standard [`TxEnv`] and adds Morph-specific fields for: +/// - L1 message detection (tx_type 0x7E) +/// - Morph transaction with token-based gas payment (tx_type 0x7F) +/// - RLP encoded transaction bytes for L1 data fee calculation +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MorphTxEnv { + /// Inner transaction environment. + pub inner: TxEnv, + /// RLP encoded transaction bytes. + /// Used only for L1 data fee calculation. + pub rlp_bytes: Option, + /// Maximum amount of tokens the sender is willing to pay as fee. + pub fee_limit: Option, + /// Token ID for fee payment (only for AltFeeTx type 0x7F). + /// 0 means ETH payment, > 0 means ERC20 token payment. + pub fee_token_id: Option, +} + +impl Default for MorphTxEnv { + fn default() -> Self { + Self { + inner: TxEnv::default(), + rlp_bytes: None, + fee_limit: None, + fee_token_id: None, + } + } +} + +impl MorphTxEnv { + /// Create a new Morph transaction environment from a standard TxEnv. + pub fn new(inner: TxEnv) -> Self { + Self { + inner, + rlp_bytes: None, + fee_limit: None, + fee_token_id: None, + } + } + + /// Create a new Morph transaction environment with RLP bytes. + pub fn with_rlp_bytes(mut self, rlp_bytes: Bytes) -> Self { + self.rlp_bytes = Some(rlp_bytes); + self + } + + /// Set the fee limit. + pub fn with_fee_limit(mut self, fee_limit: U256) -> Self { + self.fee_limit = Some(fee_limit); + self + } + + /// Set the fee token ID. + pub fn with_fee_token_id(mut self, fee_token_id: u16) -> Self { + self.fee_token_id = Some(fee_token_id); + self + } + + /// Create a new Morph transaction environment from a recovered transaction. + /// + /// This method: + /// - Converts the transaction to `TxEnv` + /// - Extracts the RLP-encoded transaction bytes for L1 data fee calculation + /// - Extracts fee_token_id for AltFeeTx (type 0x7F) + pub fn from_recovered_tx(tx: &MorphTxEnvelope, signer: Address) -> Self { + // Encode the transaction to RLP bytes for L1 data fee calculation + let rlp_bytes = tx.encoded_2718(); + Self::from_tx_with_rlp_bytes(tx, signer, Bytes::from(rlp_bytes)) + } + + /// Create a new Morph transaction environment from a transaction with pre-encoded RLP bytes. + /// + /// This is the core implementation used by both `from_recovered_tx` and `FromTxWithEncoded`. + fn from_tx_with_rlp_bytes(tx: &MorphTxEnvelope, signer: Address, rlp_bytes: Bytes) -> Self { + let tx_type: u8 = tx.tx_type().into(); + + // Extract fee_token_id for AltFeeTx (type 0x7F) + let fee_token_id = if tx_type == ALT_FEE_TX_TYPE_ID { + extract_fee_token_id_from_rlp(&rlp_bytes) + } else { + 0 + }; + + // Build TxEnv from the transaction + let inner = TxEnv { + tx_type, + caller: signer, + gas_limit: AlloyTransaction::gas_limit(tx) as u64, + gas_price: tx.effective_gas_price(None), + kind: AlloyTransaction::kind(tx), + value: AlloyTransaction::value(tx), + data: AlloyTransaction::input(tx).clone(), + nonce: AlloyTransaction::nonce(tx), + chain_id: AlloyTransaction::chain_id(tx), + access_list: tx.access_list().cloned().unwrap_or_default(), + gas_priority_fee: AlloyTransaction::max_priority_fee_per_gas(tx), + blob_hashes: tx + .blob_versioned_hashes() + .map(|h| h.to_vec()) + .unwrap_or_default(), + max_fee_per_blob_gas: AlloyTransaction::max_fee_per_blob_gas(tx).unwrap_or(0), + authorization_list: Default::default(), + }; + + // Use builder pattern to set Morph-specific fields + Self::new(inner) + .with_rlp_bytes(rlp_bytes) + .with_fee_token_id(fee_token_id) + } +} + +/// Extract fee_token_id from RLP-encoded AltFeeTx bytes. +/// +/// The bytes should be EIP-2718 encoded (type byte + RLP payload). +/// Returns 0 if decoding fails. +fn extract_fee_token_id_from_rlp(rlp_bytes: &Bytes) -> u16 { + if rlp_bytes.is_empty() { + return 0; + } + + // Skip the type byte (0x7F) and decode the AltFeeTx + let payload = &rlp_bytes[1..]; + TxAltFee::decode(&mut &payload[..]) + .map(|tx| tx.fee_token_id) + .unwrap_or(0) +} + +impl Deref for MorphTxEnv { + type Target = TxEnv; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for MorphTxEnv { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl From for MorphTxEnv { + fn from(inner: TxEnv) -> Self { + Self::new(inner) + } +} + +impl From for TxEnv { + fn from(morph_tx: MorphTxEnv) -> Self { + morph_tx.inner + } +} + +// Implement ToTxEnv for MorphTxEnv (identity conversion) +impl ToTxEnv for MorphTxEnv { + fn to_tx_env(&self) -> MorphTxEnv { + self.clone() + } +} + +// Implement FromRecoveredTx for MorphTxEnv +impl FromRecoveredTx for MorphTxEnv { + fn from_recovered_tx(tx: &MorphTxEnvelope, sender: Address) -> Self { + MorphTxEnv::from_recovered_tx(tx, sender) + } +} + +impl FromRecoveredTx> for MorphTxEnv { + fn from_recovered_tx(tx: &EthereumTxEnvelope, sender: Address) -> Self { + TxEnv::from_recovered_tx(tx, sender).into() + } +} + +impl FromTxWithEncoded> for MorphTxEnv { + fn from_encoded_tx( + tx: &EthereumTxEnvelope, + sender: Address, + _encoded: Bytes, + ) -> Self { + >>::from_recovered_tx(tx, sender) + } +} + +// Implement FromTxWithEncoded for MorphTxEnv +impl FromTxWithEncoded for MorphTxEnv { + fn from_encoded_tx(tx: &MorphTxEnvelope, sender: Address, encoded: Bytes) -> Self { + MorphTxEnv::from_tx_with_rlp_bytes(tx, sender, encoded) + } +} + +// Implement TransactionEnv for MorphTxEnv +impl TransactionEnv for MorphTxEnv { + fn set_gas_limit(&mut self, gas_limit: u64) { + self.inner.gas_limit = gas_limit; + } + + fn nonce(&self) -> u64 { + self.inner.nonce + } + + fn set_nonce(&mut self, nonce: u64) { + self.inner.nonce = nonce; + } + + fn set_access_list(&mut self, access_list: AccessList) { + self.inner.access_list = access_list; + } +} + +// Implement the Transaction trait for MorphTxEnv by delegating to inner TxEnv +impl Transaction for MorphTxEnv { + type AccessListItem<'a> + = &'a AccessListItem + where + Self: 'a; + type Authorization<'a> + = &'a Either + where + Self: 'a; + + #[inline] + fn tx_type(&self) -> u8 { + self.inner.tx_type + } + + #[inline] + fn kind(&self) -> TxKind { + self.inner.kind + } + + #[inline] + fn caller(&self) -> Address { + self.inner.caller + } -/// Extension trait for [`TxEnv`] to support Morph-specific functionality. + #[inline] + fn gas_limit(&self) -> u64 { + self.inner.gas_limit + } + + #[inline] + fn gas_price(&self) -> u128 { + self.inner.gas_price + } + + #[inline] + fn value(&self) -> U256 { + self.inner.value + } + + #[inline] + fn nonce(&self) -> u64 { + self.inner.nonce + } + + #[inline] + fn chain_id(&self) -> Option { + self.inner.chain_id + } + + #[inline] + fn access_list(&self) -> Option>> { + Some(self.inner.access_list.0.iter()) + } + + #[inline] + fn max_fee_per_gas(&self) -> u128 { + self.inner.gas_price + } + + #[inline] + fn max_fee_per_blob_gas(&self) -> u128 { + self.inner.max_fee_per_blob_gas + } + + #[inline] + fn authorization_list_len(&self) -> usize { + self.inner.authorization_list.len() + } + + #[inline] + fn authorization_list(&self) -> impl Iterator> { + self.inner.authorization_list.iter() + } + + #[inline] + fn input(&self) -> &Bytes { + &self.inner.data + } + + #[inline] + fn blob_versioned_hashes(&self) -> &[B256] { + &self.inner.blob_hashes + } + + #[inline] + fn max_priority_fee_per_gas(&self) -> Option { + self.inner.gas_priority_fee + } +} + +/// Extension trait for transaction types to support Morph-specific functionality. pub trait MorphTxExt { /// Returns whether this transaction is an L1 message transaction (type 0x7E). fn is_l1_msg(&self) -> bool; + + /// Returns whether this transaction is a Morph transaction (type 0x7F). + /// Morph transactions support ERC20 token-based gas payment. + fn is_alt_fee_tx(&self) -> bool; +} + +impl MorphTxExt for MorphTxEnv { + #[inline] + fn is_l1_msg(&self) -> bool { + self.inner.tx_type == L1_TX_TYPE_ID + } + + #[inline] + fn is_alt_fee_tx(&self) -> bool { + self.inner.tx_type == ALT_FEE_TX_TYPE_ID + } } impl MorphTxExt for TxEnv { @@ -21,6 +348,11 @@ impl MorphTxExt for TxEnv { fn is_l1_msg(&self) -> bool { self.tx_type == L1_TX_TYPE_ID } + + #[inline] + fn is_alt_fee_tx(&self) -> bool { + self.tx_type == ALT_FEE_TX_TYPE_ID + } } #[cfg(test)] @@ -29,11 +361,56 @@ mod tests { #[test] fn test_l1_msg_detection() { - let mut tx = TxEnv::default(); - tx.tx_type = L1_TX_TYPE_ID; + let mut tx = MorphTxEnv::default(); + tx.inner.tx_type = L1_TX_TYPE_ID; assert!(tx.is_l1_msg()); + assert!(!tx.is_alt_fee_tx()); - let regular_tx = TxEnv::default(); + let regular_tx = MorphTxEnv::default(); assert!(!regular_tx.is_l1_msg()); } + + #[test] + fn test_morph_tx_detection() { + let mut tx = MorphTxEnv::default(); + tx.inner.tx_type = ALT_FEE_TX_TYPE_ID; + assert!(tx.is_alt_fee_tx()); + assert!(!tx.is_l1_msg()); + } + + #[test] + fn test_txenv_morph_tx_detection() { + let mut tx = TxEnv::default(); + tx.tx_type = ALT_FEE_TX_TYPE_ID; + assert!(tx.is_alt_fee_tx()); + + let regular_tx = TxEnv::default(); + assert!(!regular_tx.is_alt_fee_tx()); + } + + #[test] + fn test_deref() { + let mut tx = MorphTxEnv::default(); + tx.gas_limit = 21000; + assert_eq!(tx.gas_limit, 21000); + assert_eq!(tx.inner.gas_limit, 21000); + } + + #[test] + fn test_transaction_trait() { + let tx = MorphTxEnv::default(); + // Test that Transaction trait methods work + assert_eq!(Transaction::gas_limit(&tx), tx.inner.gas_limit); + assert_eq!(Transaction::caller(&tx), tx.inner.caller); + assert_eq!(Transaction::value(&tx), tx.inner.value); + } + + #[test] + fn test_rlp_bytes() { + let tx = MorphTxEnv::default(); + assert!(tx.rlp_bytes.is_none()); + + let tx_with_rlp = MorphTxEnv::default().with_rlp_bytes(Bytes::from(vec![1, 2, 3])); + assert_eq!(tx_with_rlp.rlp_bytes, Some(Bytes::from(vec![1, 2, 3]))); + } }