From 38f50fb055a5c9bfd9f563357f7499f7611b58f0 Mon Sep 17 00:00:00 2001 From: panos Date: Sun, 8 Feb 2026 16:23:00 +0800 Subject: [PATCH 1/8] refactor: add receipt buulder in block executor --- crates/evm/Cargo.toml | 2 +- crates/evm/src/assemble.rs | 4 +- crates/evm/src/block/mod.rs | 207 ++++++++++++++++++++++++++------ crates/evm/src/block/receipt.rs | 148 ++++++++++++++++++++--- 4 files changed, 304 insertions(+), 57 deletions(-) diff --git a/crates/evm/Cargo.toml b/crates/evm/Cargo.toml index 97d6884..ce6353f 100644 --- a/crates/evm/Cargo.toml +++ b/crates/evm/Cargo.toml @@ -13,7 +13,7 @@ workspace = true [dependencies] morph-chainspec.workspace = true -morph-primitives.workspace = true +morph-primitives = { workspace = true, features = ["serde-bincode-compat"] } morph-revm.workspace = true reth-chainspec.workspace = true diff --git a/crates/evm/src/assemble.rs b/crates/evm/src/assemble.rs index f2142ac..ab0cb93 100644 --- a/crates/evm/src/assemble.rs +++ b/crates/evm/src/assemble.rs @@ -1,5 +1,5 @@ use crate::{ - MorphEvmConfig, MorphEvmFactory, block::MorphReceiptBuilder, context::MorphBlockExecutionCtx, + MorphEvmConfig, MorphEvmFactory, block::DefaultMorphReceiptBuilder, context::MorphBlockExecutionCtx, }; use alloy_evm::{block::BlockExecutionError, eth::EthBlockExecutorFactory}; use morph_chainspec::MorphChainSpec; @@ -48,7 +48,7 @@ impl BlockAssembler for MorphBlockAssembler { // Delegate block building to the inner assembler let block = self.inner.assemble_block(BlockAssemblerInput::< - EthBlockExecutorFactory, + EthBlockExecutorFactory, >::new( evm_env, inner, diff --git a/crates/evm/src/block/mod.rs b/crates/evm/src/block/mod.rs index f6e6839..96668b8 100644 --- a/crates/evm/src/block/mod.rs +++ b/crates/evm/src/block/mod.rs @@ -8,29 +8,52 @@ pub(crate) mod curie; mod receipt; -pub(crate) use receipt::MorphReceiptBuilder; +pub(crate) use receipt::{ + DefaultMorphReceiptBuilder, MorphReceiptBuilder, MorphReceiptBuilderCtx, MorphTxTokenFeeInfo, +}; use crate::{MorphBlockExecutionCtx, evm::MorphEvm}; +use alloy_consensus::Transaction; +use alloy_consensus::transaction::TxHashRef; use alloy_evm::{ Database, Evm, block::{BlockExecutionError, BlockExecutionResult, BlockExecutor, ExecutableTx, OnStateHook}, - eth::EthBlockExecutor, }; +use alloy_primitives::U256; use curie::apply_curie_hard_fork; use morph_chainspec::{MorphChainSpec, MorphHardfork, MorphHardforks}; use morph_primitives::{MorphReceipt, MorphTxEnvelope}; -use morph_revm::{L1_GAS_PRICE_ORACLE_ADDRESS, MorphHaltReason, evm::MorphContext}; -use reth_revm::{Inspector, State, context::result::ResultAndState}; +use morph_revm::{ + L1BlockInfo, L1_GAS_PRICE_ORACLE_ADDRESS, MorphHaltReason, TokenFeeInfo, + evm::MorphContext, +}; +use reth_chainspec::EthereumHardforks; +use reth_revm::{DatabaseCommit, Inspector, State, context::result::ResultAndState}; +use revm::context::Block; +use std::marker::PhantomData; -/// Block executor for Morph. Wraps an inner [`EthBlockExecutor`]. +/// Block executor for Morph. +/// +/// This executor handles Morph-specific logic including: +/// - L1 fee calculation for transactions +/// - Token fee information extraction for MorphTx (0x7F) transactions +/// - Curie hardfork application pub(crate) struct MorphBlockExecutor<'a, DB: Database, I> { - /// Inner Ethereum block executor. - pub(crate) inner: EthBlockExecutor< - 'a, - MorphEvm<&'a mut State, I>, - &'a MorphChainSpec, - MorphReceiptBuilder, - >, + /// The EVM used by executor + evm: MorphEvm<&'a mut State, I>, + /// Chain specification + spec: &'a MorphChainSpec, + /// Receipt builder + receipt_builder: DefaultMorphReceiptBuilder, + /// Context for block execution + #[allow(dead_code)] + ctx: MorphBlockExecutionCtx<'a>, + /// Receipts of executed transactions + receipts: Vec, + /// Total gas used by executed transactions + gas_used: u64, + /// Phantom data for inspector type + _phantom: PhantomData, } impl<'a, DB, I> MorphBlockExecutor<'a, DB, I> @@ -41,16 +64,85 @@ where pub(crate) fn new( evm: MorphEvm<&'a mut State, I>, ctx: MorphBlockExecutionCtx<'a>, - chain_spec: &'a MorphChainSpec, + spec: &'a MorphChainSpec, ) -> Self { Self { - inner: EthBlockExecutor::new( - evm, - ctx.inner, - chain_spec, - MorphReceiptBuilder::default(), - ), + evm, + spec, + receipt_builder: DefaultMorphReceiptBuilder, + ctx, + receipts: Vec::new(), + gas_used: 0, + _phantom: PhantomData, + } + } + + /// Calculate the L1 data fee for a transaction. + /// + /// Returns `U256::ZERO` for L1 message transactions, or the calculated L1 fee + /// based on the L1 Gas Price Oracle state. + fn calculate_l1_fee( + &mut self, + tx: &MorphTxEnvelope, + ) -> Result { + // L1 message transactions don't pay L1 fees + if tx.is_l1_msg() { + return Ok(U256::ZERO); } + + // Get the RLP-encoded transaction bytes + let rlp_bytes = tx.rlp(); + + // Determine the current hardfork based on block number and timestamp + let block = self.evm.block(); + let block_number: u64 = block.number.to(); + let timestamp: u64 = block.timestamp.to(); + let hardfork = self.spec.morph_hardfork_at(block_number, timestamp); + + // Fetch L1 block info from the L1 Gas Price Oracle contract + let l1_block_info = L1BlockInfo::try_fetch(self.evm.db_mut(), hardfork) + .map_err(|e| BlockExecutionError::msg(format!("Failed to fetch L1 block info: {e:?}")))?; + + // Calculate L1 data fee + Ok(l1_block_info.calculate_tx_l1_cost(rlp_bytes.as_ref(), hardfork)) + } + + /// Extract token fee information for MorphTx (0x7F) transactions. + /// + /// This queries the L2TokenRegistry contract to get the exchange rate and scale + /// for the specified token ID. + fn get_token_fee_info( + &mut self, + tx: &MorphTxEnvelope, + ) -> Result, BlockExecutionError> { + // Only MorphTx transactions have token fee information + if !tx.is_morph_tx() { + return Ok(None); + } + + let fee_token_id = tx.fee_token_id().unwrap(); // Safe because we checked is_morph_tx() + let fee_limit = tx.fee_limit().unwrap(); // Safe because we checked is_morph_tx() + + // Determine the current hardfork based on block number and timestamp + let block = self.evm.block(); + let block_number: u64 = block.number.to(); + let timestamp: u64 = block.timestamp.to(); + let hardfork = self.spec.morph_hardfork_at(block_number, timestamp); + + // Fetch token fee info from L2TokenRegistry contract + // Note: We use the transaction sender as the caller address + // This is needed to check token balance when validating MorphTx + let sender = tx.signer_unchecked().unwrap_or_default(); + + let token_info = TokenFeeInfo::fetch(self.evm.db_mut(), fee_token_id, sender, hardfork) + .map_err(|e| BlockExecutionError::msg(format!("Failed to fetch token fee info: {e:?}")))?; + + Ok(token_info.map(|info| MorphTxTokenFeeInfo { + fee_token_id, + fee_rate: info.price_ratio, + token_scale: info.scale, + fee_limit, + })) } } @@ -64,32 +156,33 @@ where type Evm = MorphEvm<&'a mut State, I>; fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> { - // 1. Apply base Ethereum pre-execution changes (state clear flag, EIP-2935, EIP-4788) - self.inner.apply_pre_execution_changes()?; + // 1. Set state clear flag if the block is after the Spurious Dragon hardfork + let block_number: u64 = self.evm.block().number.to(); + let state_clear_flag = self.spec.is_spurious_dragon_active_at_block(block_number); + self.evm.db_mut().set_state_clear_flag(state_clear_flag); - // 2. Load L1 gas oracle contract into cache for Curie hardfork upgrade + // 2. Load L1 gas oracle contract into cache let _ = self - .inner - .evm_mut() + .evm .db_mut() .load_cache_account(L1_GAS_PRICE_ORACLE_ADDRESS) .map_err(BlockExecutionError::other)?; // 3. Apply Curie hardfork at the transition block // Only executes once at the exact block where Curie activates - let block_number = self.inner.evm.block().number.saturating_to(); if self - .inner .spec .morph_fork_activation(MorphHardfork::Curie) .transitions_at_block(block_number) - && let Err(err) = apply_curie_hard_fork(self.inner.evm_mut().db_mut()) + && let Err(err) = apply_curie_hard_fork(self.evm.db_mut()) { return Err(BlockExecutionError::msg(format!( "error occurred at Curie fork: {err:?}" ))); } + // TODO: Apply EIP-2935 blockhashes contract call when needed + Ok(()) } @@ -97,7 +190,19 @@ where &mut self, tx: impl ExecutableTx, ) -> Result, BlockExecutionError> { - self.inner.execute_transaction_without_commit(tx) + // The sum of the transaction's gas limit and the gas utilized in this block prior, + // must be no greater than the block's gasLimit. + let block_available_gas = self.evm.block().gas_limit() - self.gas_used; + if tx.tx().gas_limit() > block_available_gas { + return Err(BlockExecutionError::msg(format!( + "transaction gas limit {} exceeds block available gas {}", + tx.tx().gas_limit(), + block_available_gas + ))); + } + + // Execute the transaction + self.evm.transact(&tx).map_err(|err| BlockExecutionError::evm(err, *tx.tx().tx_hash())) } fn commit_transaction( @@ -105,24 +210,58 @@ where output: ResultAndState, tx: impl ExecutableTx, ) -> Result { - self.inner.commit_transaction(output, tx) + + let ResultAndState { result, state } = output; + + // Calculate L1 fee for the transaction + let l1_fee = self.calculate_l1_fee(tx.tx())?; + + // Get token fee information for MorphTx transactions + let token_fee_info = self.get_token_fee_info(tx.tx())?; + + // Update cumulative gas used + let gas_used = result.gas_used(); + self.gas_used += gas_used; + + // Build receipt + let ctx: MorphReceiptBuilderCtx<'_, Self::Evm> = MorphReceiptBuilderCtx { + tx: tx.tx(), + result, + cumulative_gas_used: self.gas_used, + l1_fee, + token_fee_info, + }; + self.receipts.push(self.receipt_builder.build_receipt(ctx)); + + // Commit state changes + self.evm.db_mut().commit(state); + + Ok(gas_used) } fn finish( self, ) -> Result<(Self::Evm, BlockExecutionResult), BlockExecutionError> { - self.inner.finish() + Ok(( + self.evm, + BlockExecutionResult { + receipts: self.receipts, + requests: Default::default(), + gas_used: self.gas_used, + blob_gas_used: 0, + }, + )) } - fn set_state_hook(&mut self, hook: Option>) { - self.inner.set_state_hook(hook) + fn set_state_hook(&mut self, _hook: Option>) { + // State hooks are not yet supported for Morph block executor } fn evm_mut(&mut self) -> &mut Self::Evm { - self.inner.evm_mut() + &mut self.evm } fn evm(&self) -> &Self::Evm { - self.inner.evm() + &self.evm } } diff --git a/crates/evm/src/block/receipt.rs b/crates/evm/src/block/receipt.rs index 590b564..5395af0 100644 --- a/crates/evm/src/block/receipt.rs +++ b/crates/evm/src/block/receipt.rs @@ -1,36 +1,133 @@ //! Receipt builder for Morph block execution. //! //! This module provides the [`MorphReceiptBuilder`] which constructs receipts -//! for executed transactions based on their type. +//! for executed transactions based on their type, including L1 fee calculation. use alloy_consensus::Receipt; use alloy_evm::{ Evm, - eth::receipt_builder::{ReceiptBuilder, ReceiptBuilderCtx}, + eth::receipt_builder::{ReceiptBuilder, ReceiptBuilderCtx as AlloyReceiptBuilderCtx}, }; +use alloy_primitives::U256; use morph_primitives::{MorphReceipt, MorphTransactionReceipt, MorphTxEnvelope, MorphTxType}; +use revm::context::result::ExecutionResult; -/// Builder for [`MorphReceipt`]. +/// Context for building a Morph receipt. /// -/// Creates the appropriate receipt variant based on transaction type. +/// Contains all the information needed to construct a receipt for an executed transaction. +#[derive(Debug)] +pub(crate) struct MorphReceiptBuilderCtx<'a, E: Evm> { + /// The executed transaction + pub tx: &'a MorphTxEnvelope, + /// Result of transaction execution + pub result: ExecutionResult, + /// Cumulative gas used in the block up to and including this transaction + pub cumulative_gas_used: u64, + /// L1 data fee for this transaction + pub l1_fee: U256, + /// Token fee information for MorphTx (0x7F) transactions + pub token_fee_info: Option, +} + +/// Token fee information for MorphTx transactions. +#[derive(Debug, Clone, Copy)] +pub(crate) struct MorphTxTokenFeeInfo { + /// Token ID for fee payment + pub fee_token_id: u16, + /// Exchange rate for the fee token + pub fee_rate: U256, + /// Scale factor for the token + pub token_scale: U256, + /// Fee limit specified in the transaction + pub fee_limit: U256, +} + +/// Trait for building Morph receipts. +pub(crate) trait MorphReceiptBuilder: Send + Sync { + /// Build a receipt from the execution context. + fn build_receipt(&self, ctx: MorphReceiptBuilderCtx<'_, E>) -> MorphReceipt; +} + +/// Default builder for [`MorphReceipt`]. +/// +/// Creates the appropriate receipt variant based on transaction type and includes: +/// - L1 data fee for all non-L1Message transactions +/// - Token fee information for MorphTx (0x7F) transactions #[derive(Debug, Clone, Copy, Default)] #[non_exhaustive] -pub(crate) struct MorphReceiptBuilder; +pub(crate) struct DefaultMorphReceiptBuilder; + +impl MorphReceiptBuilder for DefaultMorphReceiptBuilder { + fn build_receipt(&self, ctx: MorphReceiptBuilderCtx<'_, E>) -> MorphReceipt { + let MorphReceiptBuilderCtx { + tx, + result, + cumulative_gas_used, + l1_fee, + token_fee_info, + } = ctx; -impl ReceiptBuilder for MorphReceiptBuilder { + let inner = Receipt { + status: result.is_success().into(), + cumulative_gas_used, + logs: result.into_logs(), + }; + + // Create the appropriate receipt variant based on transaction type + match tx.tx_type() { + MorphTxType::Legacy => { + MorphReceipt::Legacy(MorphTransactionReceipt::with_l1_fee(inner, l1_fee)) + } + MorphTxType::Eip2930 => { + MorphReceipt::Eip2930(MorphTransactionReceipt::with_l1_fee(inner, l1_fee)) + } + MorphTxType::Eip1559 => { + MorphReceipt::Eip1559(MorphTransactionReceipt::with_l1_fee(inner, l1_fee)) + } + MorphTxType::Eip7702 => { + MorphReceipt::Eip7702(MorphTransactionReceipt::with_l1_fee(inner, l1_fee)) + } + MorphTxType::L1Msg => { + // L1 messages don't pay L1 fees + MorphReceipt::L1Msg(inner) + } + MorphTxType::Morph => { + // MorphTx transactions include token fee information + if let Some(token_info) = token_fee_info { + MorphReceipt::Morph(MorphTransactionReceipt::with_morph_tx( + inner, + l1_fee, + token_info.fee_token_id, + token_info.fee_rate, + token_info.token_scale, + token_info.fee_limit, + )) + } else { + // Fallback: just include L1 fee if token info is missing + MorphReceipt::Morph(MorphTransactionReceipt::with_l1_fee(inner, l1_fee)) + } + } + } + } +} + +/// Implementation of alloy-evm's `ReceiptBuilder` trait. +/// +/// This implementation is primarily needed to satisfy type bounds in `EthBlockExecutorFactory`. +/// The actual receipt building during block execution is done via `MorphReceiptBuilder` +/// in `MorphBlockExecutor::commit_transaction`. +/// +/// Note: This implementation builds receipts without L1 fee and token fee information, +/// since the alloy-evm `ReceiptBuilderCtx` doesn't provide these fields. +impl ReceiptBuilder for DefaultMorphReceiptBuilder { type Transaction = MorphTxEnvelope; type Receipt = MorphReceipt; fn build_receipt( &self, - ctx: ReceiptBuilderCtx<'_, Self::Transaction, E>, + ctx: AlloyReceiptBuilderCtx<'_, Self::Transaction, E>, ) -> Self::Receipt { - let ReceiptBuilderCtx { - tx, - result, - cumulative_gas_used, - .. - } = ctx; + let AlloyReceiptBuilderCtx { tx, result, cumulative_gas_used, .. } = ctx; let inner = Receipt { status: result.is_success().into(), @@ -38,15 +135,26 @@ impl ReceiptBuilder for MorphReceiptBuilder { logs: result.into_logs(), }; - // Create the appropriate receipt variant based on transaction type - // TODO: Add L1 fee calculation from execution context + // Build receipts without L1 fee since it's not available in the context. + // In practice, this code path is not used during execution - receipts are built + // via MorphBlockExecutor which has full context including L1 fees. match tx.tx_type() { - MorphTxType::Legacy => MorphReceipt::Legacy(MorphTransactionReceipt::new(inner)), - MorphTxType::Eip2930 => MorphReceipt::Eip2930(MorphTransactionReceipt::new(inner)), - MorphTxType::Eip1559 => MorphReceipt::Eip1559(MorphTransactionReceipt::new(inner)), - MorphTxType::Eip7702 => MorphReceipt::Eip7702(MorphTransactionReceipt::new(inner)), + MorphTxType::Legacy => { + MorphReceipt::Legacy(MorphTransactionReceipt::with_l1_fee(inner, U256::ZERO)) + } + MorphTxType::Eip2930 => { + MorphReceipt::Eip2930(MorphTransactionReceipt::with_l1_fee(inner, U256::ZERO)) + } + MorphTxType::Eip1559 => { + MorphReceipt::Eip1559(MorphTransactionReceipt::with_l1_fee(inner, U256::ZERO)) + } + MorphTxType::Eip7702 => { + MorphReceipt::Eip7702(MorphTransactionReceipt::with_l1_fee(inner, U256::ZERO)) + } MorphTxType::L1Msg => MorphReceipt::L1Msg(inner), - MorphTxType::Morph => MorphReceipt::Morph(MorphTransactionReceipt::new(inner)), + MorphTxType::Morph => { + MorphReceipt::Morph(MorphTransactionReceipt::with_l1_fee(inner, U256::ZERO)) + } } } } From 1336459e0f89afb2e20d2d529ef619c5efec2651 Mon Sep 17 00:00:00 2001 From: panos Date: Mon, 9 Feb 2026 09:56:23 +0800 Subject: [PATCH 2/8] refactor: add ethereum primitives features for evm crate tests --- Cargo.lock | 1 + crates/evm/Cargo.toml | 1 + crates/evm/src/assemble.rs | 3 ++- crates/evm/src/block/mod.rs | 22 +++++++++++----------- crates/evm/src/block/receipt.rs | 7 ++++++- crates/evm/src/lib.rs | 9 +++++---- crates/primitives/Cargo.toml | 3 --- crates/primitives/src/lib.rs | 6 ++---- 8 files changed, 28 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f6897d8..2a5dedc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4514,6 +4514,7 @@ dependencies = [ "morph-primitives", "morph-revm", "reth-chainspec", + "reth-ethereum-primitives", "reth-evm", "reth-evm-ethereum", "reth-primitives-traits", diff --git a/crates/evm/Cargo.toml b/crates/evm/Cargo.toml index ce6353f..b93adee 100644 --- a/crates/evm/Cargo.toml +++ b/crates/evm/Cargo.toml @@ -19,6 +19,7 @@ morph-revm.workspace = true reth-chainspec.workspace = true reth-evm.workspace = true reth-evm-ethereum.workspace = true +reth-ethereum-primitives = { workspace = true, features = ["serde", "serde-bincode-compat"] } reth-revm.workspace = true reth-primitives-traits.workspace = true reth-rpc-eth-api = { workspace = true, optional = true } diff --git a/crates/evm/src/assemble.rs b/crates/evm/src/assemble.rs index ab0cb93..31f2d40 100644 --- a/crates/evm/src/assemble.rs +++ b/crates/evm/src/assemble.rs @@ -1,5 +1,6 @@ use crate::{ - MorphEvmConfig, MorphEvmFactory, block::DefaultMorphReceiptBuilder, context::MorphBlockExecutionCtx, + MorphEvmConfig, MorphEvmFactory, block::DefaultMorphReceiptBuilder, + context::MorphBlockExecutionCtx, }; use alloy_evm::{block::BlockExecutionError, eth::EthBlockExecutorFactory}; use morph_chainspec::MorphChainSpec; diff --git a/crates/evm/src/block/mod.rs b/crates/evm/src/block/mod.rs index 96668b8..389c999 100644 --- a/crates/evm/src/block/mod.rs +++ b/crates/evm/src/block/mod.rs @@ -24,8 +24,7 @@ use curie::apply_curie_hard_fork; use morph_chainspec::{MorphChainSpec, MorphHardfork, MorphHardforks}; use morph_primitives::{MorphReceipt, MorphTxEnvelope}; use morph_revm::{ - L1BlockInfo, L1_GAS_PRICE_ORACLE_ADDRESS, MorphHaltReason, TokenFeeInfo, - evm::MorphContext, + L1_GAS_PRICE_ORACLE_ADDRESS, L1BlockInfo, MorphHaltReason, TokenFeeInfo, evm::MorphContext, }; use reth_chainspec::EthereumHardforks; use reth_revm::{DatabaseCommit, Inspector, State, context::result::ResultAndState}; @@ -81,10 +80,7 @@ where /// /// Returns `U256::ZERO` for L1 message transactions, or the calculated L1 fee /// based on the L1 Gas Price Oracle state. - fn calculate_l1_fee( - &mut self, - tx: &MorphTxEnvelope, - ) -> Result { + fn calculate_l1_fee(&mut self, tx: &MorphTxEnvelope) -> Result { // L1 message transactions don't pay L1 fees if tx.is_l1_msg() { return Ok(U256::ZERO); @@ -100,8 +96,9 @@ where let hardfork = self.spec.morph_hardfork_at(block_number, timestamp); // Fetch L1 block info from the L1 Gas Price Oracle contract - let l1_block_info = L1BlockInfo::try_fetch(self.evm.db_mut(), hardfork) - .map_err(|e| BlockExecutionError::msg(format!("Failed to fetch L1 block info: {e:?}")))?; + let l1_block_info = L1BlockInfo::try_fetch(self.evm.db_mut(), hardfork).map_err(|e| { + BlockExecutionError::msg(format!("Failed to fetch L1 block info: {e:?}")) + })?; // Calculate L1 data fee Ok(l1_block_info.calculate_tx_l1_cost(rlp_bytes.as_ref(), hardfork)) @@ -135,7 +132,9 @@ where let sender = tx.signer_unchecked().unwrap_or_default(); let token_info = TokenFeeInfo::fetch(self.evm.db_mut(), fee_token_id, sender, hardfork) - .map_err(|e| BlockExecutionError::msg(format!("Failed to fetch token fee info: {e:?}")))?; + .map_err(|e| { + BlockExecutionError::msg(format!("Failed to fetch token fee info: {e:?}")) + })?; Ok(token_info.map(|info| MorphTxTokenFeeInfo { fee_token_id, @@ -202,7 +201,9 @@ where } // Execute the transaction - self.evm.transact(&tx).map_err(|err| BlockExecutionError::evm(err, *tx.tx().tx_hash())) + self.evm + .transact(&tx) + .map_err(|err| BlockExecutionError::evm(err, *tx.tx().tx_hash())) } fn commit_transaction( @@ -210,7 +211,6 @@ where output: ResultAndState, tx: impl ExecutableTx, ) -> Result { - let ResultAndState { result, state } = output; // Calculate L1 fee for the transaction diff --git a/crates/evm/src/block/receipt.rs b/crates/evm/src/block/receipt.rs index 5395af0..c403093 100644 --- a/crates/evm/src/block/receipt.rs +++ b/crates/evm/src/block/receipt.rs @@ -127,7 +127,12 @@ impl ReceiptBuilder for DefaultMorphReceiptBuilder { &self, ctx: AlloyReceiptBuilderCtx<'_, Self::Transaction, E>, ) -> Self::Receipt { - let AlloyReceiptBuilderCtx { tx, result, cumulative_gas_used, .. } = ctx; + let AlloyReceiptBuilderCtx { + tx, + result, + cumulative_gas_used, + .. + } = ctx; let inner = Receipt { status: result.is_success().into(), diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index ae73572..047453b 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -29,14 +29,15 @@ //! └─────────────────────────────────────────────────────────────────┘ //! ``` //! -//! # Features -//! -//! - `serde-bincode-compat`: Enable `ConfigureEvm` implementation for reth integration -//! - `engine`: Enable engine API types #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg))] +// reth_ethereum_primitives is explicitly depended on to ensure feature unification +// with reth-evm-ethereum (which transitively depends on it). We need serde features +// for NodePrimitives trait bounds. +use reth_ethereum_primitives as _; + mod config; mod assemble; diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index 62d2232..edfba2e 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -50,9 +50,6 @@ serde-bincode-compat = [ "alloy-consensus/serde-bincode-compat", "alloy-eips/serde-bincode-compat", "reth-primitives-traits/serde-bincode-compat", - "dep:reth-ethereum-primitives", - "reth-ethereum-primitives/serde", - "reth-ethereum-primitives/serde-bincode-compat", ] reth-codec = [ "serde", diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 7b0b95c..f20bdfa 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -30,7 +30,8 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg), allow(unexpected_cfgs))] -#[cfg(feature = "serde-bincode-compat")] +// reth-ethereum-primitives is only used when the reth-codec feature is enabled +#[cfg(feature = "reth-codec")] use reth_ethereum_primitives as _; pub mod header; @@ -57,13 +58,10 @@ pub use transaction::{ }; /// A [`NodePrimitives`] implementation for Morph. -/// -/// This implementation is only available when the `serde-bincode-compat` feature is enabled. #[derive(Debug, Clone, Default, Eq, PartialEq)] #[non_exhaustive] pub struct MorphPrimitives; -#[cfg(feature = "serde-bincode-compat")] impl reth_primitives_traits::NodePrimitives for MorphPrimitives { type Block = Block; type BlockHeader = MorphHeader; From a33b4fccec62faec2672e917c40e19c4254d29ce Mon Sep 17 00:00:00 2001 From: panos Date: Mon, 9 Feb 2026 15:10:51 +0800 Subject: [PATCH 3/8] refactor: add morph block executor factory --- Cargo.lock | 2 - crates/evm/Cargo.toml | 1 - crates/evm/src/assemble.rs | 108 ++++++++++++++++++++++---------- crates/evm/src/block/factory.rs | 86 +++++++++++++++++++++++++ crates/evm/src/block/mod.rs | 17 +++-- crates/evm/src/block/receipt.rs | 58 +---------------- crates/evm/src/config.rs | 37 +++++------ crates/evm/src/context.rs | 9 --- crates/evm/src/lib.rs | 41 ++++++------ 9 files changed, 203 insertions(+), 156 deletions(-) create mode 100644 crates/evm/src/block/factory.rs diff --git a/Cargo.lock b/Cargo.lock index 2a5dedc..a2a1d81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4516,7 +4516,6 @@ dependencies = [ "reth-chainspec", "reth-ethereum-primitives", "reth-evm", - "reth-evm-ethereum", "reth-primitives-traits", "reth-revm", "reth-rpc-eth-api", @@ -6773,7 +6772,6 @@ dependencies = [ "alloy-evm", "alloy-primitives", "alloy-rpc-types-engine", - "derive_more", "reth-chainspec", "reth-ethereum-forks", "reth-ethereum-primitives", diff --git a/crates/evm/Cargo.toml b/crates/evm/Cargo.toml index b93adee..b5027c7 100644 --- a/crates/evm/Cargo.toml +++ b/crates/evm/Cargo.toml @@ -18,7 +18,6 @@ morph-revm.workspace = true reth-chainspec.workspace = true reth-evm.workspace = true -reth-evm-ethereum.workspace = true reth-ethereum-primitives = { workspace = true, features = ["serde", "serde-bincode-compat"] } reth-revm.workspace = true reth-primitives-traits.workspace = true diff --git a/crates/evm/src/assemble.rs b/crates/evm/src/assemble.rs index 31f2d40..1c07187 100644 --- a/crates/evm/src/assemble.rs +++ b/crates/evm/src/assemble.rs @@ -1,26 +1,34 @@ -use crate::{ - MorphEvmConfig, MorphEvmFactory, block::DefaultMorphReceiptBuilder, - context::MorphBlockExecutionCtx, -}; -use alloy_evm::{block::BlockExecutionError, eth::EthBlockExecutorFactory}; +use crate::MorphEvmConfig; +use alloy_consensus::{BlockBody, EMPTY_OMMER_ROOT_HASH, Header, TxReceipt, proofs}; +use alloy_evm::block::{BlockExecutionError, BlockExecutionResult}; +use alloy_primitives::{Address, B64, logs_bloom}; use morph_chainspec::MorphChainSpec; -use morph_primitives::MorphHeader; +use morph_primitives::{MorphHeader, receipt::calculate_receipt_root_no_memo}; +use reth_chainspec::EthereumHardforks; use reth_evm::execute::{BlockAssembler, BlockAssemblerInput}; -use reth_evm_ethereum::EthBlockAssembler; -use reth_primitives_traits::SealedHeader; +use revm::context::Block; use std::sync::Arc; /// Assembler for Morph blocks. +/// +/// This assembler builds Morph blocks from the execution output. +/// Unlike `EthBlockAssembler`, it produces `MorphHeader` with proper +/// L2-specific fields (next_l1_msg_index, batch_hash). #[derive(Debug, Clone)] pub struct MorphBlockAssembler { - pub(crate) inner: EthBlockAssembler, + /// Chain specification + chain_spec: Arc, } impl MorphBlockAssembler { + /// Creates a new [`MorphBlockAssembler`]. pub fn new(chain_spec: Arc) -> Self { - Self { - inner: EthBlockAssembler::new(chain_spec), - } + Self { chain_spec } + } + + /// Returns the chain spec. + pub const fn chain_spec(&self) -> &Arc { + &self.chain_spec } } @@ -33,36 +41,68 @@ impl BlockAssembler for MorphBlockAssembler { ) -> Result { let BlockAssemblerInput { evm_env, - execution_ctx: MorphBlockExecutionCtx { inner }, + execution_ctx, parent, transactions, - output, - bundle_state, - state_provider, + output: BlockExecutionResult { + receipts, gas_used, .. + }, state_root, .. } = input; - // Convert MorphHeader parent to standard Header for the inner assembler. - // We extract the inner Header since EthBlockAssembler works with standard Headers. - let inner_parent = SealedHeader::new_unhashed(parent.header().inner.clone()); + let timestamp = evm_env.block_env.timestamp(); + let block_number: u64 = evm_env.block_env.number().to(); - // Delegate block building to the inner assembler - let block = self.inner.assemble_block(BlockAssemblerInput::< - EthBlockExecutorFactory, - >::new( - evm_env, - inner, - &inner_parent, - transactions, - output, - bundle_state, - state_provider, + // Calculate roots and bloom + let transactions_root = proofs::calculate_transaction_root(&transactions); + let receipts_root = calculate_receipt_root_no_memo(receipts); + let logs_bloom = logs_bloom(receipts.iter().flat_map(|r| r.logs())); + + // Determine if EIP-1559 is active + let is_london_active = self.chain_spec.is_london_active_at_block(block_number); + + // Build standard Ethereum header + let inner = Header { + parent_hash: execution_ctx.parent_hash, + ommers_hash: EMPTY_OMMER_ROOT_HASH, + beneficiary: Address::ZERO, state_root, - ))?; + transactions_root, + receipts_root, + withdrawals_root: None, + logs_bloom, + timestamp: timestamp.to(), + mix_hash: evm_env.block_env.prevrandao().unwrap_or_default(), + nonce: B64::ZERO, + // Only include base_fee_per_gas after London (EIP-1559) + base_fee_per_gas: is_london_active.then_some(evm_env.block_env.basefee()), + number: block_number, + gas_limit: evm_env.block_env.gas_limit(), + difficulty: evm_env.block_env.difficulty(), + gas_used: *gas_used, + extra_data: Default::default(), + parent_beacon_block_root: None, + blob_gas_used: None, + excess_blob_gas: None, + requests_hash: None, + }; + + // Wrap in MorphHeader with L2-specific fields + // Note: next_l1_msg_index and batch_hash will be set by the payload builder + let header = MorphHeader { + inner, + next_l1_msg_index: parent.header().next_l1_msg_index, + batch_hash: Default::default(), + }; - // Convert the standard Header back to MorphHeader. - // The next_l1_msg_index and batch_hash will be set by the payload builder. - Ok(block.map_header(MorphHeader::from)) + Ok(alloy_consensus::Block::new( + header, + BlockBody { + transactions, + ommers: Default::default(), + withdrawals: None, + }, + )) } } diff --git a/crates/evm/src/block/factory.rs b/crates/evm/src/block/factory.rs new file mode 100644 index 0000000..b4ae1df --- /dev/null +++ b/crates/evm/src/block/factory.rs @@ -0,0 +1,86 @@ +//! Block executor factory for Morph. +//! +//! This module provides the [`MorphBlockExecutorFactory`] which creates block executors +//! that can execute Morph L2 blocks with proper L1 fee calculation and receipt building. + +use crate::{ + block::{DefaultMorphReceiptBuilder, MorphBlockExecutor}, + evm::MorphEvm, +}; +use alloy_evm::{ + Database, + block::{BlockExecutorFactory, BlockExecutorFor}, + eth::EthBlockExecutionCtx, + revm::{Inspector, database::State}, +}; +use morph_chainspec::MorphChainSpec; +use morph_primitives::{MorphReceipt, MorphTxEnvelope}; +use morph_revm::evm::MorphContext; +use std::sync::Arc; + +use crate::evm::MorphEvmFactory; + +/// Block executor factory for Morph. +/// +/// This factory creates [`MorphBlockExecutor`] instances that handle Morph-specific +/// block execution logic including: +/// - L1 fee calculation for transactions +/// - Token fee information extraction for MorphTx (0x7F) transactions +/// - Curie hardfork application +/// +/// Unlike using `EthBlockExecutorFactory`, this factory uses the custom +/// [`MorphReceiptBuilder`] trait which includes `l1_fee` in its context, +/// ensuring receipts are built with complete information. +#[derive(Debug, Clone)] +pub(crate) struct MorphBlockExecutorFactory { + /// Receipt builder + receipt_builder: DefaultMorphReceiptBuilder, + /// Chain specification + spec: Arc, + /// EVM factory + evm_factory: MorphEvmFactory, +} + +impl MorphBlockExecutorFactory { + /// Creates a new [`MorphBlockExecutorFactory`]. + pub(crate) fn new(spec: Arc, evm_factory: MorphEvmFactory) -> Self { + Self { + receipt_builder: DefaultMorphReceiptBuilder, + spec, + evm_factory, + } + } + + /// Returns the chain spec. + pub(crate) const fn spec(&self) -> &Arc { + &self.spec + } + + /// Returns the EVM factory. + pub(crate) const fn evm_factory(&self) -> &MorphEvmFactory { + &self.evm_factory + } +} + +impl BlockExecutorFactory for MorphBlockExecutorFactory { + type EvmFactory = MorphEvmFactory; + type ExecutionCtx<'a> = EthBlockExecutionCtx<'a>; + type Transaction = MorphTxEnvelope; + type Receipt = MorphReceipt; + + fn evm_factory(&self) -> &Self::EvmFactory { + &self.evm_factory + } + + fn create_executor<'a, DB, I>( + &'a self, + evm: MorphEvm<&'a mut State, I>, + _ctx: Self::ExecutionCtx<'a>, + ) -> impl BlockExecutorFor<'a, Self, DB, I> + where + DB: Database + 'a, + I: Inspector>> + 'a, + { + MorphBlockExecutor::new(evm, &self.spec, &self.receipt_builder) + } +} diff --git a/crates/evm/src/block/mod.rs b/crates/evm/src/block/mod.rs index 389c999..efafc8a 100644 --- a/crates/evm/src/block/mod.rs +++ b/crates/evm/src/block/mod.rs @@ -2,17 +2,20 @@ //! //! This module provides block execution functionality for Morph, including: //! - [`MorphBlockExecutor`]: The main block executor +//! - [`MorphBlockExecutorFactory`]: Factory for creating block executors //! - [`MorphReceiptBuilder`]: Receipt construction for transactions //! - Hardfork application logic (Curie, etc.) pub(crate) mod curie; +mod factory; mod receipt; +pub(crate) use factory::MorphBlockExecutorFactory; pub(crate) use receipt::{ DefaultMorphReceiptBuilder, MorphReceiptBuilder, MorphReceiptBuilderCtx, MorphTxTokenFeeInfo, }; -use crate::{MorphBlockExecutionCtx, evm::MorphEvm}; +use crate::evm::MorphEvm; use alloy_consensus::Transaction; use alloy_consensus::transaction::TxHashRef; use alloy_evm::{ @@ -43,10 +46,7 @@ pub(crate) struct MorphBlockExecutor<'a, DB: Database, I> { /// Chain specification spec: &'a MorphChainSpec, /// Receipt builder - receipt_builder: DefaultMorphReceiptBuilder, - /// Context for block execution - #[allow(dead_code)] - ctx: MorphBlockExecutionCtx<'a>, + receipt_builder: &'a DefaultMorphReceiptBuilder, /// Receipts of executed transactions receipts: Vec, /// Total gas used by executed transactions @@ -62,14 +62,13 @@ where { pub(crate) fn new( evm: MorphEvm<&'a mut State, I>, - ctx: MorphBlockExecutionCtx<'a>, spec: &'a MorphChainSpec, + receipt_builder: &'a DefaultMorphReceiptBuilder, ) -> Self { Self { evm, spec, - receipt_builder: DefaultMorphReceiptBuilder, - ctx, + receipt_builder, receipts: Vec::new(), gas_used: 0, _phantom: PhantomData, @@ -180,8 +179,6 @@ where ))); } - // TODO: Apply EIP-2935 blockhashes contract call when needed - Ok(()) } diff --git a/crates/evm/src/block/receipt.rs b/crates/evm/src/block/receipt.rs index c403093..e08cfe5 100644 --- a/crates/evm/src/block/receipt.rs +++ b/crates/evm/src/block/receipt.rs @@ -4,10 +4,7 @@ //! for executed transactions based on their type, including L1 fee calculation. use alloy_consensus::Receipt; -use alloy_evm::{ - Evm, - eth::receipt_builder::{ReceiptBuilder, ReceiptBuilderCtx as AlloyReceiptBuilderCtx}, -}; +use alloy_evm::Evm; use alloy_primitives::U256; use morph_primitives::{MorphReceipt, MorphTransactionReceipt, MorphTxEnvelope, MorphTxType}; use revm::context::result::ExecutionResult; @@ -110,56 +107,3 @@ impl MorphReceiptBuilder for DefaultMorphReceiptBuilder { } } } - -/// Implementation of alloy-evm's `ReceiptBuilder` trait. -/// -/// This implementation is primarily needed to satisfy type bounds in `EthBlockExecutorFactory`. -/// The actual receipt building during block execution is done via `MorphReceiptBuilder` -/// in `MorphBlockExecutor::commit_transaction`. -/// -/// Note: This implementation builds receipts without L1 fee and token fee information, -/// since the alloy-evm `ReceiptBuilderCtx` doesn't provide these fields. -impl ReceiptBuilder for DefaultMorphReceiptBuilder { - type Transaction = MorphTxEnvelope; - type Receipt = MorphReceipt; - - fn build_receipt( - &self, - ctx: AlloyReceiptBuilderCtx<'_, Self::Transaction, E>, - ) -> Self::Receipt { - let AlloyReceiptBuilderCtx { - tx, - result, - cumulative_gas_used, - .. - } = ctx; - - let inner = Receipt { - status: result.is_success().into(), - cumulative_gas_used, - logs: result.into_logs(), - }; - - // Build receipts without L1 fee since it's not available in the context. - // In practice, this code path is not used during execution - receipts are built - // via MorphBlockExecutor which has full context including L1 fees. - match tx.tx_type() { - MorphTxType::Legacy => { - MorphReceipt::Legacy(MorphTransactionReceipt::with_l1_fee(inner, U256::ZERO)) - } - MorphTxType::Eip2930 => { - MorphReceipt::Eip2930(MorphTransactionReceipt::with_l1_fee(inner, U256::ZERO)) - } - MorphTxType::Eip1559 => { - MorphReceipt::Eip1559(MorphTransactionReceipt::with_l1_fee(inner, U256::ZERO)) - } - MorphTxType::Eip7702 => { - MorphReceipt::Eip7702(MorphTransactionReceipt::with_l1_fee(inner, U256::ZERO)) - } - MorphTxType::L1Msg => MorphReceipt::L1Msg(inner), - MorphTxType::Morph => { - MorphReceipt::Morph(MorphTransactionReceipt::with_l1_fee(inner, U256::ZERO)) - } - } - } -} diff --git a/crates/evm/src/config.rs b/crates/evm/src/config.rs index f26ed54..785ca17 100644 --- a/crates/evm/src/config.rs +++ b/crates/evm/src/config.rs @@ -1,7 +1,4 @@ -use crate::{ - MorphBlockAssembler, MorphBlockExecutionCtx, MorphEvmConfig, MorphEvmError, - MorphNextBlockEnvAttributes, -}; +use crate::{MorphBlockAssembler, MorphEvmConfig, MorphEvmError, MorphNextBlockEnvAttributes}; use alloy_consensus::BlockHeader; use morph_chainspec::hardfork::MorphHardforks; use morph_primitives::Block; @@ -85,15 +82,13 @@ impl ConfigureEvm for MorphEvmConfig { fn context_for_block<'a>( &self, block: &'a SealedBlock, - ) -> Result, Self::Error> { - Ok(MorphBlockExecutionCtx { - inner: EthBlockExecutionCtx { - parent_hash: block.header().parent_hash(), - parent_beacon_block_root: block.header().parent_beacon_block_root(), - ommers: &[], - withdrawals: block.body().withdrawals.as_ref().map(Cow::Borrowed), - extra_data: block.extra_data().clone(), - }, + ) -> Result, Self::Error> { + Ok(EthBlockExecutionCtx { + parent_hash: block.header().parent_hash(), + parent_beacon_block_root: block.header().parent_beacon_block_root(), + ommers: &[], + withdrawals: block.body().withdrawals.as_ref().map(Cow::Borrowed), + extra_data: block.extra_data().clone(), }) } @@ -101,15 +96,13 @@ impl ConfigureEvm for MorphEvmConfig { &self, parent: &SealedHeader, attributes: Self::NextBlockEnvCtx, - ) -> Result, Self::Error> { - Ok(MorphBlockExecutionCtx { - inner: EthBlockExecutionCtx { - parent_hash: parent.hash(), - parent_beacon_block_root: attributes.parent_beacon_block_root, - ommers: &[], - withdrawals: attributes.inner.withdrawals.map(Cow::Owned), - extra_data: attributes.inner.extra_data, - }, + ) -> Result, Self::Error> { + Ok(EthBlockExecutionCtx { + parent_hash: parent.hash(), + parent_beacon_block_root: attributes.parent_beacon_block_root, + ommers: &[], + withdrawals: attributes.inner.withdrawals.map(Cow::Owned), + extra_data: attributes.inner.extra_data, }) } } diff --git a/crates/evm/src/context.rs b/crates/evm/src/context.rs index 534eefa..98dd45a 100644 --- a/crates/evm/src/context.rs +++ b/crates/evm/src/context.rs @@ -1,16 +1,7 @@ -use alloy_evm::eth::EthBlockExecutionCtx; use reth_evm::NextBlockEnvAttributes; #[cfg(feature = "rpc")] use reth_primitives_traits::SealedHeader; -/// Execution context for Morph block. -#[derive(Debug, Clone, derive_more::Deref)] -pub struct MorphBlockExecutionCtx<'a> { - /// Inner [`EthBlockExecutionCtx`]. - #[deref] - pub inner: EthBlockExecutionCtx<'a>, -} - /// Context required for next block environment. #[derive(Debug, Clone, derive_more::Deref)] pub struct MorphNextBlockEnvAttributes { diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index 047453b..d2ff71e 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -7,17 +7,16 @@ //! - [`MorphEvmConfig`]: Main EVM configuration that implements `BlockExecutorFactory` //! - [`MorphEvmFactory`]: Factory for creating Morph EVM instances //! - [`MorphBlockAssembler`]: Block assembly logic for payload building -//! - [`MorphBlockExecutionCtx`]: Execution context for block processing //! //! # Architecture //! //! ```text //! ┌─────────────────────────────────────────────────────────────────┐ //! │ MorphEvmConfig │ -//! │ ┌─────────────────────┐ ┌─────────────────────────────────┐ │ -//! │ │ EthEvmConfig │ │ MorphBlockAssembler │ │ -//! │ │ (inner config) │ │ (block building) │ │ -//! │ └─────────────────────┘ └─────────────────────────────────┘ │ +//! │ ┌─────────────────────────┐ ┌─────────────────────────────┐ │ +//! │ │ MorphBlockExecutorFactory│ │ MorphBlockAssembler │ │ +//! │ │ (creates executors) │ │ (block building) │ │ +//! │ └─────────────────────────┘ └─────────────────────────────┘ │ //! │ │ │ //! │ ▼ │ //! │ ┌─────────────────────────────────────────────────────────┐ │ @@ -25,6 +24,7 @@ //! │ │ - Executes transactions │ │ //! │ │ - Handles L1 messages │ │ //! │ │ - Calculates L1 data fee │ │ +//! │ │ - Builds receipts with full context │ │ //! │ └─────────────────────────────────────────────────────────┘ │ //! └─────────────────────────────────────────────────────────────────┘ //! ``` @@ -34,8 +34,7 @@ #![cfg_attr(docsrs, feature(doc_cfg))] // reth_ethereum_primitives is explicitly depended on to ensure feature unification -// with reth-evm-ethereum (which transitively depends on it). We need serde features -// for NodePrimitives trait bounds. +// for NodePrimitives trait bounds that require serde features. use reth_ethereum_primitives as _; mod config; @@ -44,7 +43,7 @@ mod assemble; pub use assemble::MorphBlockAssembler; mod block; mod context; -pub use context::{MorphBlockExecutionCtx, MorphNextBlockEnvAttributes}; +pub use context::MorphNextBlockEnvAttributes; mod error; pub use error::MorphEvmError; @@ -54,23 +53,23 @@ use std::sync::Arc; use alloy_evm::{ Database, block::{BlockExecutorFactory, BlockExecutorFor}, + eth::EthBlockExecutionCtx, revm::{Inspector, database::State}, }; pub use evm::MorphEvmFactory; use morph_primitives::{MorphReceipt, MorphTxEnvelope}; -use crate::{block::MorphBlockExecutor, evm::MorphEvm}; +use crate::{block::MorphBlockExecutorFactory, evm::MorphEvm}; use morph_chainspec::MorphChainSpec; use morph_revm::evm::MorphContext; -use reth_evm_ethereum::EthEvmConfig; pub use morph_revm::{MorphBlockEnv, MorphHaltReason}; /// Morph-related EVM configuration. #[derive(Debug, Clone)] pub struct MorphEvmConfig { - /// Inner evm config - pub inner: EthEvmConfig, + /// Block executor factory + executor_factory: MorphBlockExecutorFactory, /// Block assembler pub block_assembler: MorphBlockAssembler, @@ -79,9 +78,9 @@ pub struct MorphEvmConfig { impl MorphEvmConfig { /// Create a new [`MorphEvmConfig`] with the given chain spec and EVM factory. pub fn new(chain_spec: Arc, evm_factory: MorphEvmFactory) -> Self { - let inner = EthEvmConfig::new_with_evm_factory(chain_spec.clone(), evm_factory); + let executor_factory = MorphBlockExecutorFactory::new(chain_spec.clone(), evm_factory); Self { - inner, + executor_factory, block_assembler: MorphBlockAssembler::new(chain_spec), } } @@ -93,23 +92,23 @@ impl MorphEvmConfig { /// Returns the chain spec pub const fn chain_spec(&self) -> &Arc { - self.inner.chain_spec() + self.executor_factory.spec() } - /// Returns the inner EVM config - pub const fn inner(&self) -> &EthEvmConfig { - &self.inner + /// Returns the EVM factory + pub const fn evm_factory(&self) -> &MorphEvmFactory { + self.executor_factory.evm_factory() } } impl BlockExecutorFactory for MorphEvmConfig { type EvmFactory = MorphEvmFactory; - type ExecutionCtx<'a> = MorphBlockExecutionCtx<'a>; + type ExecutionCtx<'a> = EthBlockExecutionCtx<'a>; type Transaction = MorphTxEnvelope; type Receipt = MorphReceipt; fn evm_factory(&self) -> &Self::EvmFactory { - self.inner.executor_factory.evm_factory() + self.executor_factory.evm_factory() } fn create_executor<'a, DB, I>( @@ -121,7 +120,7 @@ impl BlockExecutorFactory for MorphEvmConfig { DB: Database + 'a, I: Inspector>> + 'a, { - MorphBlockExecutor::new(evm, ctx, self.chain_spec()) + self.executor_factory.create_executor(evm, ctx) } } From 6b30d5d0ee9189ac2ee9f2ec8d36efc4561803f1 Mon Sep 17 00:00:00 2001 From: panos Date: Mon, 9 Feb 2026 15:25:26 +0800 Subject: [PATCH 4/8] refactor: get token fee error when tx sender invaild --- crates/evm/src/block/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/evm/src/block/mod.rs b/crates/evm/src/block/mod.rs index efafc8a..739c8fb 100644 --- a/crates/evm/src/block/mod.rs +++ b/crates/evm/src/block/mod.rs @@ -128,7 +128,9 @@ where // Fetch token fee info from L2TokenRegistry contract // Note: We use the transaction sender as the caller address // This is needed to check token balance when validating MorphTx - let sender = tx.signer_unchecked().unwrap_or_default(); + let sender = tx + .signer_unchecked() + .map_err(|_| BlockExecutionError::msg("Failed to extract signer from MorphTx"))?; let token_info = TokenFeeInfo::fetch(self.evm.db_mut(), fee_token_id, sender, hardfork) .map_err(|e| { From 261e856cbcf48103d8307f1458213bd4d2ff9aeb Mon Sep 17 00:00:00 2001 From: panos Date: Mon, 9 Feb 2026 15:46:47 +0800 Subject: [PATCH 5/8] refactor: return error when fetch token id and fee limit error --- crates/evm/src/block/mod.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/evm/src/block/mod.rs b/crates/evm/src/block/mod.rs index 739c8fb..95df702 100644 --- a/crates/evm/src/block/mod.rs +++ b/crates/evm/src/block/mod.rs @@ -116,8 +116,12 @@ where return Ok(None); } - let fee_token_id = tx.fee_token_id().unwrap(); // Safe because we checked is_morph_tx() - let fee_limit = tx.fee_limit().unwrap(); // Safe because we checked is_morph_tx() + let fee_token_id = tx + .fee_token_id() + .ok_or_else(|| BlockExecutionError::msg("MorphTx missing fee_token_id"))?; + let fee_limit = tx + .fee_limit() + .ok_or_else(|| BlockExecutionError::msg("MorphTx missing fee_limit"))?; // Determine the current hardfork based on block number and timestamp let block = self.evm.block(); From 91736d876327ec236abcbed1445eeca82cbad8ce Mon Sep 17 00:00:00 2001 From: panos Date: Mon, 9 Feb 2026 16:07:56 +0800 Subject: [PATCH 6/8] docs: add some comments --- crates/evm/src/assemble.rs | 30 ++++++- crates/evm/src/block/factory.rs | 2 +- crates/evm/src/block/mod.rs | 153 ++++++++++++++++++++++++++++++-- crates/evm/src/block/receipt.rs | 92 +++++++++++++++++-- crates/evm/src/lib.rs | 78 +++++++++++++--- crates/primitives/src/lib.rs | 2 +- 6 files changed, 323 insertions(+), 34 deletions(-) diff --git a/crates/evm/src/assemble.rs b/crates/evm/src/assemble.rs index 1c07187..31cd2c7 100644 --- a/crates/evm/src/assemble.rs +++ b/crates/evm/src/assemble.rs @@ -21,7 +21,10 @@ pub struct MorphBlockAssembler { } impl MorphBlockAssembler { - /// Creates a new [`MorphBlockAssembler`]. + /// Creates a new [`MorphBlockAssembler`] with the given chain specification. + /// + /// # Arguments + /// * `chain_spec` - The Morph chain specification used for hardfork detection pub fn new(chain_spec: Arc) -> Self { Self { chain_spec } } @@ -35,6 +38,31 @@ impl MorphBlockAssembler { impl BlockAssembler for MorphBlockAssembler { type Block = morph_primitives::Block; + /// Assembles a Morph block from execution results. + /// + /// This method constructs a complete [`Block`] from the execution output, + /// including building the proper [`MorphHeader`] with L2-specific fields. + /// + /// # Block Assembly Process + /// 1. **Calculate Merkle Roots**: Computes transaction root and receipt root + /// 2. **Build Logs Bloom**: Aggregates logs from all receipts + /// 3. **Check Hardforks**: Determines if EIP-1559 (London) is active for base fee + /// 4. **Build Header**: Creates standard Ethereum header fields + /// 5. **Wrap in MorphHeader**: Adds L2-specific fields (next_l1_msg_index, batch_hash) + /// 6. **Build Block Body**: Combines transactions and empty ommers + /// + /// # L2-Specific Fields + /// - `next_l1_msg_index`: Inherited from parent block + /// - `batch_hash`: Set to default (will be filled by payload builder) + /// + /// # Arguments + /// * `input` - Contains execution context, transactions, receipts, and state root + /// + /// # Returns + /// A fully assembled Morph block ready for sealing. + /// + /// # Errors + /// Returns error if block assembly fails (should not occur in normal operation). fn assemble_block( &self, input: BlockAssemblerInput<'_, '_, MorphEvmConfig, MorphHeader>, diff --git a/crates/evm/src/block/factory.rs b/crates/evm/src/block/factory.rs index b4ae1df..969a35b 100644 --- a/crates/evm/src/block/factory.rs +++ b/crates/evm/src/block/factory.rs @@ -29,7 +29,7 @@ use crate::evm::MorphEvmFactory; /// - Curie hardfork application /// /// Unlike using `EthBlockExecutorFactory`, this factory uses the custom -/// [`MorphReceiptBuilder`] trait which includes `l1_fee` in its context, +/// `MorphReceiptBuilder` trait which includes `l1_fee` in its context, /// ensuring receipts are built with complete information. #[derive(Debug, Clone)] pub(crate) struct MorphBlockExecutorFactory { diff --git a/crates/evm/src/block/mod.rs b/crates/evm/src/block/mod.rs index 95df702..0bfe8a7 100644 --- a/crates/evm/src/block/mod.rs +++ b/crates/evm/src/block/mod.rs @@ -34,12 +34,32 @@ use reth_revm::{DatabaseCommit, Inspector, State, context::result::ResultAndStat use revm::context::Block; use std::marker::PhantomData; -/// Block executor for Morph. +/// Block executor for Morph L2 blocks. /// -/// This executor handles Morph-specific logic including: -/// - L1 fee calculation for transactions -/// - Token fee information extraction for MorphTx (0x7F) transactions -/// - Curie hardfork application +/// This executor handles Morph-specific block execution logic, differing from +/// standard Ethereum execution in several key ways: +/// +/// ## L1 Fee Calculation +/// All L2 transactions (except L1 messages) must pay an L1 data fee for posting +/// transaction data to L1. This fee is calculated based on: +/// - The RLP-encoded transaction size +/// - Current L1 gas price from the L1 Gas Price Oracle contract +/// - Hardfork-specific fee calculation logic (pre-Curie vs post-Curie) +/// +/// ## Token Fee Support (MorphTx 0x7F) +/// MorphTx transactions allow users to pay gas fees using ERC20 tokens. +/// The executor extracts token fee information from the L2TokenRegistry contract, +/// including exchange rate and scale factor. +/// +/// ## Hardfork Application +/// The executor applies hardfork-specific state changes at transition blocks, +/// such as the Curie hardfork which updates the L1 Gas Price Oracle contract. +/// +/// ## Execution Flow +/// 1. `apply_pre_execution_changes`: Set up state, load contracts, apply hardforks +/// 2. `execute_transaction_without_commit`: Execute transaction in EVM +/// 3. `commit_transaction`: Calculate fees, build receipt, commit state +/// 4. `finish`: Return final execution result with all receipts pub(crate) struct MorphBlockExecutor<'a, DB: Database, I> { /// The EVM used by executor evm: MorphEvm<&'a mut State, I>, @@ -60,6 +80,12 @@ where DB: Database, I: Inspector>>, { + /// Creates a new [`MorphBlockExecutor`]. + /// + /// # Arguments + /// * `evm` - The EVM instance configured for Morph execution + /// * `spec` - Chain specification containing hardfork information + /// * `receipt_builder` - Builder for constructing transaction receipts pub(crate) fn new( evm: MorphEvm<&'a mut State, I>, spec: &'a MorphChainSpec, @@ -77,8 +103,23 @@ where /// Calculate the L1 data fee for a transaction. /// - /// Returns `U256::ZERO` for L1 message transactions, or the calculated L1 fee - /// based on the L1 Gas Price Oracle state. + /// The L1 fee compensates for the cost of posting transaction data to Ethereum L1. + /// This is a key component of L2 transaction costs on Morph. + /// + /// # Calculation Steps + /// 1. Check if transaction is an L1 message (which don't pay L1 fees) + /// 2. Get RLP-encoded transaction bytes + /// 3. Determine current hardfork (affects fee calculation formula) + /// 4. Fetch L1 block info from L1 Gas Price Oracle contract + /// 5. Calculate fee based on transaction size and L1 gas price + /// + /// # Returns + /// - `Ok(U256::ZERO)` for L1 message transactions + /// - `Ok(fee)` for regular transactions, where fee = f(tx_size, l1_gas_price, hardfork) + /// - `Err` if L1 block info cannot be fetched + /// + /// # Errors + /// Returns error if the L1 Gas Price Oracle contract state cannot be read. fn calculate_l1_fee(&mut self, tx: &MorphTxEnvelope) -> Result { // L1 message transactions don't pay L1 fees if tx.is_l1_msg() { @@ -105,8 +146,30 @@ where /// Extract token fee information for MorphTx (0x7F) transactions. /// - /// This queries the L2TokenRegistry contract to get the exchange rate and scale - /// for the specified token ID. + /// MorphTx allows users to pay gas fees using ERC20 tokens instead of native ETH. + /// This method queries the L2TokenRegistry contract to get the current exchange + /// rate and scale factor for the specified token. + /// + /// # How MorphTx Token Fees Work + /// 1. User specifies a `fee_token_id` (registered ERC20 token) + /// 2. User specifies a `fee_limit` (max tokens willing to pay) + /// 3. System fetches token exchange rate from L2TokenRegistry + /// 4. System converts ETH fee to token amount using: `token_fee = eth_fee * fee_rate / token_scale` + /// 5. System validates user has sufficient token balance + /// + /// # Arguments + /// * `tx` - The transaction to extract fee info from + /// + /// # Returns + /// - `Ok(None)` for non-MorphTx transactions + /// - `Ok(Some(info))` for MorphTx with valid token fee info + /// - `Err` if MorphTx is missing required fields or token info cannot be fetched + /// + /// # Errors + /// Returns error if: + /// - MorphTx is missing `fee_token_id` or `fee_limit` + /// - Transaction sender cannot be extracted + /// - L2TokenRegistry contract cannot be queried fn get_token_fee_info( &mut self, tx: &MorphTxEnvelope, @@ -159,6 +222,23 @@ where type Receipt = MorphReceipt; type Evm = MorphEvm<&'a mut State, I>; + /// Applies pre-execution state changes before processing transactions. + /// + /// This method performs initialization required before executing any transactions: + /// + /// 1. **State Clear Flag**: Sets the flag that enables EIP-161 state trie clearing + /// if the Spurious Dragon hardfork is active + /// + /// 2. **L1 Gas Oracle Cache**: Loads the L1 Gas Price Oracle contract into the + /// account cache to optimize L1 fee calculations for all transactions + /// + /// 3. **Curie Hardfork**: At the exact Curie activation block, applies the + /// hardfork state changes (updates to L1 Gas Price Oracle contract) + /// + /// # Errors + /// Returns error if: + /// - L1 Gas Price Oracle account cannot be loaded + /// - Curie hardfork application fails at transition block fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> { // 1. Set state clear flag if the block is after the Spurious Dragon hardfork let block_number: u64 = self.evm.block().number.to(); @@ -188,6 +268,25 @@ where Ok(()) } + /// Executes a transaction without committing state changes. + /// + /// This method validates the transaction can fit in the remaining block gas, + /// then executes it in the EVM. The state changes are returned but not yet + /// committed to the database. + /// + /// # Gas Validation + /// Before execution, validates that: + /// ```text + /// tx.gas_limit + cumulative_gas_used <= block.gas_limit + /// ``` + /// + /// # Returns + /// Returns the execution result and state changes that can be committed later. + /// + /// # Errors + /// Returns error if: + /// - Transaction gas limit exceeds available block gas + /// - EVM execution fails (reverts, halts, out of gas, etc.) fn execute_transaction_without_commit( &mut self, tx: impl ExecutableTx, @@ -209,6 +308,25 @@ where .map_err(|err| BlockExecutionError::evm(err, *tx.tx().tx_hash())) } + /// Commits a transaction's execution result and builds its receipt. + /// + /// This method performs post-execution processing for a transaction: + /// + /// 1. **L1 Fee Calculation**: Calculates the L1 data fee for the transaction + /// 2. **Token Fee Info**: For MorphTx, extracts token fee information + /// 3. **Gas Accounting**: Updates cumulative gas used for the block + /// 4. **Receipt Building**: Constructs receipt with all Morph-specific fields + /// 5. **State Commit**: Commits the EVM state changes to the database + /// + /// # Arguments + /// * `output` - The execution result from `execute_transaction_without_commit` + /// * `tx` - The original transaction + /// + /// # Returns + /// The gas used by this transaction. + /// + /// # Errors + /// Returns error if L1 fee calculation or token fee info extraction fails. fn commit_transaction( &mut self, output: ResultAndState, @@ -242,6 +360,17 @@ where Ok(gas_used) } + /// Finalizes block execution and returns the results. + /// + /// Consumes the executor and returns the EVM instance along with the + /// complete execution results including all transaction receipts. + /// + /// # Returns + /// A tuple containing: + /// - The EVM instance (for potential reuse or state access) + /// - Block execution result with receipts, gas used, and empty requests + /// + /// Note: `blob_gas_used` is always 0 as Morph doesn't support EIP-4844 blobs. fn finish( self, ) -> Result<(Self::Evm, BlockExecutionResult), BlockExecutionError> { @@ -256,14 +385,20 @@ where )) } + /// Sets a state hook for observing state changes. + /// + /// Note: State hooks are not yet implemented for the Morph block executor. + /// This is a no-op placeholder for future implementation. fn set_state_hook(&mut self, _hook: Option>) { // State hooks are not yet supported for Morph block executor } + /// Returns a mutable reference to the EVM instance. fn evm_mut(&mut self) -> &mut Self::Evm { &mut self.evm } + /// Returns a reference to the EVM instance. fn evm(&self) -> &Self::Evm { &self.evm } diff --git a/crates/evm/src/block/receipt.rs b/crates/evm/src/block/receipt.rs index e08cfe5..42348e2 100644 --- a/crates/evm/src/block/receipt.rs +++ b/crates/evm/src/block/receipt.rs @@ -1,7 +1,29 @@ //! Receipt builder for Morph block execution. //! -//! This module provides the [`MorphReceiptBuilder`] which constructs receipts -//! for executed transactions based on their type, including L1 fee calculation. +//! This module provides the receipt building infrastructure for Morph L2 blocks. +//! Receipts contain essential information about transaction execution results, +//! including Morph-specific fields like L1 data fees and token fee information. +//! +//! # Why Custom Receipt Builder? +//! +//! Unlike standard Ethereum receipts, Morph receipts include: +//! - **L1 Data Fee**: The cost charged for posting transaction data to L1 +//! - **Token Fee Info**: For MorphTx (0x7F), includes exchange rate and fee limit +//! +//! The standard `EthBlockExecutor` doesn't have access to L1 fee information +//! during receipt building. This module provides a custom builder that receives +//! the pre-calculated L1 fee as part of its context. +//! +//! # Receipt Types +//! +//! | Transaction Type | Receipt Content | +//! |-----------------|-----------------| +//! | Legacy (0x00) | inner + l1_fee | +//! | EIP-2930 (0x01) | inner + l1_fee | +//! | EIP-1559 (0x02) | inner + l1_fee | +//! | EIP-7702 (0x04) | inner + l1_fee | +//! | L1Message (0x7E) | inner only (no L1 fee) | +//! | MorphTx (0x7F) | inner + l1_fee + token_fee_info | use alloy_consensus::Receipt; use alloy_evm::Evm; @@ -11,7 +33,16 @@ use revm::context::result::ExecutionResult; /// Context for building a Morph receipt. /// -/// Contains all the information needed to construct a receipt for an executed transaction. +/// This struct aggregates all information needed to construct a receipt for +/// an executed transaction. It is populated by the block executor after +/// transaction execution and L1 fee calculation. +/// +/// # Fields +/// - `tx`: The original transaction (needed for determining receipt type) +/// - `result`: EVM execution result (success/failure, logs, gas used) +/// - `cumulative_gas_used`: Running total of gas used in the block +/// - `l1_fee`: Pre-calculated L1 data fee for this transaction +/// - `token_fee_info`: Token fee details for MorphTx transactions #[derive(Debug)] pub(crate) struct MorphReceiptBuilderCtx<'a, E: Evm> { /// The executed transaction @@ -26,7 +57,22 @@ pub(crate) struct MorphReceiptBuilderCtx<'a, E: Evm> { pub token_fee_info: Option, } -/// Token fee information for MorphTx transactions. +/// Token fee information for MorphTx (0x7F) transactions. +/// +/// When a user pays gas fees with ERC20 tokens (MorphTx), these fields +/// record the exchange rate and limits used for the fee calculation. +/// This information is stored in receipts for transparency and debugging. +/// +/// # Fee Calculation Formula +/// ```text +/// token_fee = eth_fee * fee_rate / token_scale +/// ``` +/// +/// # Fields +/// - `fee_token_id`: ID of the ERC20 token registered in L2TokenRegistry +/// - `fee_rate`: Exchange rate from L2TokenRegistry (token per ETH) +/// - `token_scale`: Decimal scale factor for the token (e.g., 10^18) +/// - `fee_limit`: Maximum tokens the user agreed to pay #[derive(Debug, Clone, Copy)] pub(crate) struct MorphTxTokenFeeInfo { /// Token ID for fee payment @@ -39,17 +85,45 @@ pub(crate) struct MorphTxTokenFeeInfo { pub fee_limit: U256, } -/// Trait for building Morph receipts. +/// Trait for building Morph receipts from execution context. +/// +/// This trait abstracts receipt construction to allow different implementations +/// (e.g., for testing or custom receipt formats). The default implementation +/// is [`DefaultMorphReceiptBuilder`]. +/// +/// # Thread Safety +/// Implementations must be `Send + Sync` as the builder is shared across +/// the block executor and may be accessed concurrently. pub(crate) trait MorphReceiptBuilder: Send + Sync { - /// Build a receipt from the execution context. + /// Builds a receipt from the execution context. + /// + /// # Arguments + /// * `ctx` - Context containing transaction, execution result, and fee info + /// + /// # Returns + /// A [`MorphReceipt`] variant appropriate for the transaction type. fn build_receipt(&self, ctx: MorphReceiptBuilderCtx<'_, E>) -> MorphReceipt; } /// Default builder for [`MorphReceipt`]. /// -/// Creates the appropriate receipt variant based on transaction type and includes: -/// - L1 data fee for all non-L1Message transactions -/// - Token fee information for MorphTx (0x7F) transactions +/// This builder creates the appropriate receipt variant based on transaction type: +/// +/// ## Standard Transactions (Legacy, EIP-2930, EIP-1559, EIP-7702) +/// - Wraps the base receipt with L1 fee using `with_l1_fee()` +/// - L1 fee is non-zero for all L2-originated transactions +/// +/// ## L1 Message Transactions (0x7E) +/// - Uses base receipt without L1 fee +/// - These transactions originate from L1 and don't pay L1 data fees +/// +/// ## MorphTx Transactions (0x7F) +/// - Includes L1 fee plus token fee information +/// - Uses `with_morph_tx()` to populate all ERC20 token fee fields +/// - Falls back to `with_l1_fee()` if token info is unexpectedly missing +/// +/// # Note +/// The builder is stateless and can be reused across multiple receipts. #[derive(Debug, Clone, Copy, Default)] #[non_exhaustive] pub(crate) struct DefaultMorphReceiptBuilder; diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index d2ff71e..be1fed7 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -1,15 +1,20 @@ //! Morph EVM implementation. //! //! This crate provides the EVM configuration and block execution logic for Morph L2. +//! It implements reth's trait system to integrate Morph-specific behavior into the +//! standard reth node architecture. //! //! # Main Components //! //! - [`MorphEvmConfig`]: Main EVM configuration that implements `BlockExecutorFactory` -//! - [`MorphEvmFactory`]: Factory for creating Morph EVM instances -//! - [`MorphBlockAssembler`]: Block assembly logic for payload building +//! - [`MorphEvmFactory`]: Factory for creating Morph EVM instances with custom context +//! - [`MorphBlockAssembler`]: Block assembly logic for building `MorphHeader` blocks +//! - [`MorphNextBlockEnvAttributes`]: Attributes for configuring the next block environment //! //! # Architecture //! +//! The crate is structured around reth's EVM trait hierarchy: +//! //! ```text //! ┌─────────────────────────────────────────────────────────────────┐ //! │ MorphEvmConfig │ @@ -21,14 +26,31 @@ //! │ ▼ │ //! │ ┌─────────────────────────────────────────────────────────┐ │ //! │ │ MorphBlockExecutor │ │ -//! │ │ - Executes transactions │ │ -//! │ │ - Handles L1 messages │ │ -//! │ │ - Calculates L1 data fee │ │ -//! │ │ - Builds receipts with full context │ │ +//! │ │ - Executes transactions with Morph EVM │ │ +//! │ │ - Handles L1 message transactions (0x7E) │ │ +//! │ │ - Calculates L1 data fee for all L2 transactions │ │ +//! │ │ - Extracts token fee info for MorphTx (0x7F) │ │ +//! │ │ - Builds receipts with full Morph-specific context │ │ +//! │ │ - Applies hardfork state changes (Curie, etc.) │ │ //! │ └─────────────────────────────────────────────────────────┘ │ //! └─────────────────────────────────────────────────────────────────┘ //! ``` //! +//! # Key Differences from Standard Ethereum +//! +//! 1. **L1 Data Fee**: Every L2 transaction (except L1 messages) pays an L1 fee +//! calculated from the L1 Gas Price Oracle contract state. +//! +//! 2. **Token Gas Payment**: MorphTx (0x7F) allows users to pay gas with ERC20 tokens. +//! The executor queries L2TokenRegistry for exchange rates. +//! +//! 3. **Custom Receipts**: Receipts include L1 fee and optional token fee info, +//! requiring a custom receipt builder. +//! +//! 4. **L2-Specific Header**: `MorphHeader` extends standard header with +//! `next_l1_msg_index` and `batch_hash` fields. +//! +//! 5. **No Blob Transactions**: EIP-4844 blob transactions are not supported. #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg))] @@ -65,18 +87,41 @@ use morph_revm::evm::MorphContext; pub use morph_revm::{MorphBlockEnv, MorphHaltReason}; -/// Morph-related EVM configuration. +/// Morph EVM configuration and block executor factory. +/// +/// This is the main entry point for Morph EVM integration with reth. It implements +/// both [`ConfigureEvm`] and [`BlockExecutorFactory`] traits, providing: +/// +/// - EVM environment configuration for block execution and building +/// - Block executor creation with Morph-specific execution logic +/// - Block assembler for constructing `MorphHeader` blocks +/// +/// # Usage +/// +/// Create with a chain specification: +/// # Trait Implementations +/// +/// - `ConfigureEvm`: Provides EVM environment setup and block context creation +/// - `BlockExecutorFactory`: Creates `MorphBlockExecutor` instances for block execution +/// +/// # Thread Safety +/// +/// `MorphEvmConfig` is `Clone` and can be safely shared across threads. +/// The internal executor factory is lightweight and stateless. #[derive(Debug, Clone)] pub struct MorphEvmConfig { - /// Block executor factory + /// Internal block executor factory that creates `MorphBlockExecutor` instances. executor_factory: MorphBlockExecutorFactory, - /// Block assembler + /// Block assembler for building `MorphHeader` blocks from execution results. pub block_assembler: MorphBlockAssembler, } impl MorphEvmConfig { - /// Create a new [`MorphEvmConfig`] with the given chain spec and EVM factory. + /// Creates a new [`MorphEvmConfig`] with the given chain spec and EVM factory. + /// + /// This is the primary constructor that allows specifying a custom EVM factory + /// for advanced use cases (e.g., custom inspector or precompile configuration). pub fn new(chain_spec: Arc, evm_factory: MorphEvmFactory) -> Self { let executor_factory = MorphBlockExecutorFactory::new(chain_spec.clone(), evm_factory); Self { @@ -85,17 +130,24 @@ impl MorphEvmConfig { } } - /// Create a new [`MorphEvmConfig`] with the given chain spec and default EVM factory. + /// Creates a new [`MorphEvmConfig`] with the given chain spec and default EVM factory. + /// + /// This is the recommended constructor for most use cases. It uses the default + /// Morph EVM factory with standard configuration. pub fn new_with_default_factory(chain_spec: Arc) -> Self { Self::new(chain_spec, MorphEvmFactory::default()) } - /// Returns the chain spec + /// Returns a reference to the Morph chain specification. + /// + /// The chain spec contains hardfork configuration and network parameters. pub const fn chain_spec(&self) -> &Arc { self.executor_factory.spec() } - /// Returns the EVM factory + /// Returns a reference to the Morph EVM factory. + /// + /// The factory is used to create EVM instances for block execution. pub const fn evm_factory(&self) -> &MorphEvmFactory { self.executor_factory.evm_factory() } diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index f20bdfa..589a73e 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -57,7 +57,7 @@ pub use transaction::{ L1_TX_TYPE_ID, MORPH_TX_TYPE_ID, MorphTxEnvelope, MorphTxType, TxL1Msg, TxMorph, TxMorphExt, }; -/// A [`NodePrimitives`] implementation for Morph. +/// A [`reth_primitives_traits::NodePrimitives`] implementation for Morph. #[derive(Debug, Clone, Default, Eq, PartialEq)] #[non_exhaustive] pub struct MorphPrimitives; From 4e7facdfb2188a125c09637aabee78a69d2c620e Mon Sep 17 00:00:00 2001 From: panos Date: Wed, 11 Feb 2026 16:48:09 +0800 Subject: [PATCH 7/8] refactor: add all morph tx fields in receipt builder --- crates/evm/src/block/mod.rs | 36 +++++++++++-------- crates/evm/src/block/receipt.rs | 61 ++++++++++++++++++++------------- 2 files changed, 59 insertions(+), 38 deletions(-) diff --git a/crates/evm/src/block/mod.rs b/crates/evm/src/block/mod.rs index 0bfe8a7..f325b75 100644 --- a/crates/evm/src/block/mod.rs +++ b/crates/evm/src/block/mod.rs @@ -12,7 +12,7 @@ mod receipt; pub(crate) use factory::MorphBlockExecutorFactory; pub(crate) use receipt::{ - DefaultMorphReceiptBuilder, MorphReceiptBuilder, MorphReceiptBuilderCtx, MorphTxTokenFeeInfo, + DefaultMorphReceiptBuilder, MorphReceiptBuilder, MorphReceiptBuilderCtx, MorphTxFields, }; use crate::evm::MorphEvm; @@ -144,11 +144,11 @@ where Ok(l1_block_info.calculate_tx_l1_cost(rlp_bytes.as_ref(), hardfork)) } - /// Extract token fee information for MorphTx (0x7F) transactions. + /// Extract MorphTx-specific fields for MorphTx (0x7F) transactions. /// - /// MorphTx allows users to pay gas fees using ERC20 tokens instead of native ETH. - /// This method queries the L2TokenRegistry contract to get the current exchange - /// rate and scale factor for the specified token. + /// MorphTx transactions include: + /// - Token fee information (when using ERC20 for gas payment) + /// - Transaction metadata (version, reference, memo) /// /// # How MorphTx Token Fees Work /// 1. User specifies a `fee_token_id` (registered ERC20 token) @@ -158,11 +158,11 @@ where /// 5. System validates user has sufficient token balance /// /// # Arguments - /// * `tx` - The transaction to extract fee info from + /// * `tx` - The transaction to extract fields from /// /// # Returns /// - `Ok(None)` for non-MorphTx transactions - /// - `Ok(Some(info))` for MorphTx with valid token fee info + /// - `Ok(Some(fields))` for MorphTx with valid fields /// - `Err` if MorphTx is missing required fields or token info cannot be fetched /// /// # Errors @@ -170,11 +170,11 @@ where /// - MorphTx is missing `fee_token_id` or `fee_limit` /// - Transaction sender cannot be extracted /// - L2TokenRegistry contract cannot be queried - fn get_token_fee_info( + fn get_morph_tx_fields( &mut self, tx: &MorphTxEnvelope, - ) -> Result, BlockExecutionError> { - // Only MorphTx transactions have token fee information + ) -> Result, BlockExecutionError> { + // Only MorphTx transactions have these fields if !tx.is_morph_tx() { return Ok(None); } @@ -186,6 +186,11 @@ where .fee_limit() .ok_or_else(|| BlockExecutionError::msg("MorphTx missing fee_limit"))?; + // Extract version, reference, and memo from the transaction + let version = tx.version().unwrap_or(0); + let reference = tx.reference(); + let memo = tx.memo(); + // Determine the current hardfork based on block number and timestamp let block = self.evm.block(); let block_number: u64 = block.number.to(); @@ -204,11 +209,14 @@ where BlockExecutionError::msg(format!("Failed to fetch token fee info: {e:?}")) })?; - Ok(token_info.map(|info| MorphTxTokenFeeInfo { + Ok(token_info.map(|info| MorphTxFields { + version, fee_token_id, fee_rate: info.price_ratio, token_scale: info.scale, fee_limit, + reference, + memo, })) } } @@ -337,8 +345,8 @@ where // Calculate L1 fee for the transaction let l1_fee = self.calculate_l1_fee(tx.tx())?; - // Get token fee information for MorphTx transactions - let token_fee_info = self.get_token_fee_info(tx.tx())?; + // Get MorphTx-specific fields for MorphTx transactions + let morph_tx_fields = self.get_morph_tx_fields(tx.tx())?; // Update cumulative gas used let gas_used = result.gas_used(); @@ -350,7 +358,7 @@ where result, cumulative_gas_used: self.gas_used, l1_fee, - token_fee_info, + morph_tx_fields, }; self.receipts.push(self.receipt_builder.build_receipt(ctx)); diff --git a/crates/evm/src/block/receipt.rs b/crates/evm/src/block/receipt.rs index 42348e2..77de25f 100644 --- a/crates/evm/src/block/receipt.rs +++ b/crates/evm/src/block/receipt.rs @@ -8,7 +8,7 @@ //! //! Unlike standard Ethereum receipts, Morph receipts include: //! - **L1 Data Fee**: The cost charged for posting transaction data to L1 -//! - **Token Fee Info**: For MorphTx (0x7F), includes exchange rate and fee limit +//! - **Token Fee Info**: For MorphTx (0x7F), includes exchange rate, fee limit, reference, and memo //! //! The standard `EthBlockExecutor` doesn't have access to L1 fee information //! during receipt building. This module provides a custom builder that receives @@ -23,11 +23,11 @@ //! | EIP-1559 (0x02) | inner + l1_fee | //! | EIP-7702 (0x04) | inner + l1_fee | //! | L1Message (0x7E) | inner only (no L1 fee) | -//! | MorphTx (0x7F) | inner + l1_fee + token_fee_info | +//! | MorphTx (0x7F) | inner + l1_fee + token_fee_info + reference + memo | use alloy_consensus::Receipt; use alloy_evm::Evm; -use alloy_primitives::U256; +use alloy_primitives::{B256, Bytes, U256}; use morph_primitives::{MorphReceipt, MorphTransactionReceipt, MorphTxEnvelope, MorphTxType}; use revm::context::result::ExecutionResult; @@ -53,28 +53,34 @@ pub(crate) struct MorphReceiptBuilderCtx<'a, E: Evm> { pub cumulative_gas_used: u64, /// L1 data fee for this transaction pub l1_fee: U256, - /// Token fee information for MorphTx (0x7F) transactions - pub token_fee_info: Option, + /// MorphTx-specific fields (token fee info, version, reference, memo) + pub morph_tx_fields: Option, } -/// Token fee information for MorphTx (0x7F) transactions. +/// MorphTx (0x7F) specific fields for receipts. /// -/// When a user pays gas fees with ERC20 tokens (MorphTx), these fields -/// record the exchange rate and limits used for the fee calculation. -/// This information is stored in receipts for transparency and debugging. +/// This struct aggregates all Morph-specific transaction fields that need to be +/// included in the receipt, including: +/// - Token fee information (when using ERC20 for gas payment) +/// - Transaction metadata (version, reference, memo) /// -/// # Fee Calculation Formula +/// # Token Fee Calculation Formula /// ```text /// token_fee = eth_fee * fee_rate / token_scale /// ``` /// /// # Fields +/// - `version`: The version of the Morph transaction format (0 = legacy, 1 = with reference/memo) /// - `fee_token_id`: ID of the ERC20 token registered in L2TokenRegistry /// - `fee_rate`: Exchange rate from L2TokenRegistry (token per ETH) /// - `token_scale`: Decimal scale factor for the token (e.g., 10^18) /// - `fee_limit`: Maximum tokens the user agreed to pay -#[derive(Debug, Clone, Copy)] -pub(crate) struct MorphTxTokenFeeInfo { +/// - `reference`: 32-byte key for transaction indexing by external systems +/// - `memo`: Arbitrary data field (up to 64 bytes) +#[derive(Debug, Clone)] +pub(crate) struct MorphTxFields { + /// Version of the Morph transaction format + pub version: u8, /// Token ID for fee payment pub fee_token_id: u16, /// Exchange rate for the fee token @@ -83,6 +89,10 @@ pub(crate) struct MorphTxTokenFeeInfo { pub token_scale: U256, /// Fee limit specified in the transaction pub fee_limit: U256, + /// Reference key for transaction indexing + pub reference: Option, + /// Memo field for arbitrary data + pub memo: Option, } /// Trait for building Morph receipts from execution context. @@ -118,9 +128,9 @@ pub(crate) trait MorphReceiptBuilder: Send + Sync { /// - These transactions originate from L1 and don't pay L1 data fees /// /// ## MorphTx Transactions (0x7F) -/// - Includes L1 fee plus token fee information -/// - Uses `with_morph_tx()` to populate all ERC20 token fee fields -/// - Falls back to `with_l1_fee()` if token info is unexpectedly missing +/// - Includes L1 fee plus MorphTx-specific fields +/// - Uses `with_morph_tx_v1()` to populate all MorphTx fields +/// - Falls back to `with_l1_fee()` if MorphTx fields are unexpectedly missing /// /// # Note /// The builder is stateless and can be reused across multiple receipts. @@ -135,7 +145,7 @@ impl MorphReceiptBuilder for DefaultMorphReceiptBuilder { result, cumulative_gas_used, l1_fee, - token_fee_info, + morph_tx_fields, } = ctx; let inner = Receipt { @@ -163,18 +173,21 @@ impl MorphReceiptBuilder for DefaultMorphReceiptBuilder { MorphReceipt::L1Msg(inner) } MorphTxType::Morph => { - // MorphTx transactions include token fee information - if let Some(token_info) = token_fee_info { - MorphReceipt::Morph(MorphTransactionReceipt::with_morph_tx( + // MorphTx transactions include MorphTx-specific fields + if let Some(fields) = morph_tx_fields { + MorphReceipt::Morph(MorphTransactionReceipt::with_morph_tx_v1( inner, l1_fee, - token_info.fee_token_id, - token_info.fee_rate, - token_info.token_scale, - token_info.fee_limit, + fields.version, + fields.fee_token_id, + fields.fee_rate, + fields.token_scale, + fields.fee_limit, + fields.reference, + fields.memo, )) } else { - // Fallback: just include L1 fee if token info is missing + // Fallback: just include L1 fee if MorphTx fields are missing MorphReceipt::Morph(MorphTransactionReceipt::with_l1_fee(inner, l1_fee)) } } From fa5e4da905fdbbce6be9ba2d68059b23ad54dffd Mon Sep 17 00:00:00 2001 From: panos Date: Wed, 11 Feb 2026 20:01:20 +0800 Subject: [PATCH 8/8] refactor: reduced commit_transaction hardfork calculate --- crates/evm/src/block/mod.rs | 39 ++++++++++++++++++--------------- crates/evm/src/block/receipt.rs | 18 +++++++++++++-- 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/crates/evm/src/block/mod.rs b/crates/evm/src/block/mod.rs index f325b75..ee3573b 100644 --- a/crates/evm/src/block/mod.rs +++ b/crates/evm/src/block/mod.rs @@ -109,9 +109,12 @@ where /// # Calculation Steps /// 1. Check if transaction is an L1 message (which don't pay L1 fees) /// 2. Get RLP-encoded transaction bytes - /// 3. Determine current hardfork (affects fee calculation formula) - /// 4. Fetch L1 block info from L1 Gas Price Oracle contract - /// 5. Calculate fee based on transaction size and L1 gas price + /// 3. Fetch L1 block info from L1 Gas Price Oracle contract + /// 4. Calculate fee based on transaction size and L1 gas price + /// + /// # Arguments + /// * `tx` - The transaction to calculate L1 fee for + /// * `hardfork` - The current Morph hardfork (affects fee calculation formula) /// /// # Returns /// - `Ok(U256::ZERO)` for L1 message transactions @@ -120,7 +123,11 @@ where /// /// # Errors /// Returns error if the L1 Gas Price Oracle contract state cannot be read. - fn calculate_l1_fee(&mut self, tx: &MorphTxEnvelope) -> Result { + fn calculate_l1_fee( + &mut self, + tx: &MorphTxEnvelope, + hardfork: MorphHardfork, + ) -> Result { // L1 message transactions don't pay L1 fees if tx.is_l1_msg() { return Ok(U256::ZERO); @@ -129,12 +136,6 @@ where // Get the RLP-encoded transaction bytes let rlp_bytes = tx.rlp(); - // Determine the current hardfork based on block number and timestamp - let block = self.evm.block(); - let block_number: u64 = block.number.to(); - let timestamp: u64 = block.timestamp.to(); - let hardfork = self.spec.morph_hardfork_at(block_number, timestamp); - // Fetch L1 block info from the L1 Gas Price Oracle contract let l1_block_info = L1BlockInfo::try_fetch(self.evm.db_mut(), hardfork).map_err(|e| { BlockExecutionError::msg(format!("Failed to fetch L1 block info: {e:?}")) @@ -159,6 +160,7 @@ where /// /// # Arguments /// * `tx` - The transaction to extract fields from + /// * `hardfork` - The current Morph hardfork (affects token registry behavior) /// /// # Returns /// - `Ok(None)` for non-MorphTx transactions @@ -173,6 +175,7 @@ where fn get_morph_tx_fields( &mut self, tx: &MorphTxEnvelope, + hardfork: MorphHardfork, ) -> Result, BlockExecutionError> { // Only MorphTx transactions have these fields if !tx.is_morph_tx() { @@ -191,12 +194,6 @@ where let reference = tx.reference(); let memo = tx.memo(); - // Determine the current hardfork based on block number and timestamp - let block = self.evm.block(); - let block_number: u64 = block.number.to(); - let timestamp: u64 = block.timestamp.to(); - let hardfork = self.spec.morph_hardfork_at(block_number, timestamp); - // Fetch token fee info from L2TokenRegistry contract // Note: We use the transaction sender as the caller address // This is needed to check token balance when validating MorphTx @@ -342,11 +339,17 @@ where ) -> Result { let ResultAndState { result, state } = output; + // Determine hardfork once and reuse for both L1 fee and token fee calculations + let block = self.evm.block(); + let block_number: u64 = block.number.to(); + let timestamp: u64 = block.timestamp.to(); + let hardfork = self.spec.morph_hardfork_at(block_number, timestamp); + // Calculate L1 fee for the transaction - let l1_fee = self.calculate_l1_fee(tx.tx())?; + let l1_fee = self.calculate_l1_fee(tx.tx(), hardfork)?; // Get MorphTx-specific fields for MorphTx transactions - let morph_tx_fields = self.get_morph_tx_fields(tx.tx())?; + let morph_tx_fields = self.get_morph_tx_fields(tx.tx(), hardfork)?; // Update cumulative gas used let gas_used = result.gas_used(); diff --git a/crates/evm/src/block/receipt.rs b/crates/evm/src/block/receipt.rs index 77de25f..d078459 100644 --- a/crates/evm/src/block/receipt.rs +++ b/crates/evm/src/block/receipt.rs @@ -26,10 +26,12 @@ //! | MorphTx (0x7F) | inner + l1_fee + token_fee_info + reference + memo | use alloy_consensus::Receipt; +use alloy_consensus::transaction::TxHashRef; use alloy_evm::Evm; use alloy_primitives::{B256, Bytes, U256}; use morph_primitives::{MorphReceipt, MorphTransactionReceipt, MorphTxEnvelope, MorphTxType}; use revm::context::result::ExecutionResult; +use tracing::warn; /// Context for building a Morph receipt. /// @@ -173,7 +175,14 @@ impl MorphReceiptBuilder for DefaultMorphReceiptBuilder { MorphReceipt::L1Msg(inner) } MorphTxType::Morph => { - // MorphTx transactions include MorphTx-specific fields + // MorphTx transactions should always have MorphTx-specific fields. + // If fields are missing, it indicates one of the following: + // 1. The fee token is not registered in L2TokenRegistry + // 2. TokenFeeInfo::fetch returned None (token inactive or query failed) + // 3. A bug in get_morph_tx_fields logic + // + // We log a warning and fallback to L1-fee-only receipt to avoid + // blocking block execution, but this should be investigated. if let Some(fields) = morph_tx_fields { MorphReceipt::Morph(MorphTransactionReceipt::with_morph_tx_v1( inner, @@ -187,7 +196,12 @@ impl MorphReceiptBuilder for DefaultMorphReceiptBuilder { fields.memo, )) } else { - // Fallback: just include L1 fee if MorphTx fields are missing + warn!( + target: "morph::receipt", + tx_hash = ?tx.tx_hash(), + "MorphTx missing token fee fields - receipt will not include fee token info. \ + This may indicate an unregistered/inactive token or a bug." + ); MorphReceipt::Morph(MorphTransactionReceipt::with_l1_fee(inner, l1_fee)) } }