diff --git a/Cargo.lock b/Cargo.lock index 525b82e..bf7bf37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3652,6 +3652,7 @@ dependencies = [ "alloy-serde", "auto_impl", "eyre", + "morph-primitives", "reth-chainspec", "reth-cli", "reth-network-peers", @@ -3698,11 +3699,35 @@ dependencies = [ "revm", "serde_json", "thiserror 2.0.17", + "tracing", ] [[package]] name = "morph-payload-builder" version = "0.7.5" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "morph-chainspec", + "morph-evm", + "morph-payload-types", + "morph-primitives", + "reth-basic-payload-builder", + "reth-chainspec", + "reth-evm", + "reth-payload-builder", + "reth-payload-primitives", + "reth-payload-util", + "reth-primitives-traits", + "reth-revm", + "reth-storage-api", + "reth-transaction-pool", + "revm", + "thiserror 2.0.17", + "tracing", +] [[package]] name = "morph-payload-types" @@ -3716,7 +3741,7 @@ dependencies = [ "alloy-serde", "morph-primitives", "rand 0.8.5", - "reth-engine-primitives", + "reth-payload-builder", "reth-payload-primitives", "reth-primitives-traits", "serde", @@ -3741,6 +3766,7 @@ dependencies = [ "reth-primitives-traits", "reth-zstd-compressors", "serde", + "serde_json", ] [[package]] @@ -5827,6 +5853,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "reth-payload-util" +version = "1.9.3" +source = "git+https://github.com/paradigmxyz/reth?rev=64909d3#64909d33e6b7ab60774e37f5508fb5ad17f41897" +dependencies = [ + "alloy-consensus", + "alloy-primitives", + "reth-transaction-pool", +] + [[package]] name = "reth-primitives-traits" version = "1.9.3" diff --git a/Cargo.toml b/Cargo.toml index d04230f..e54fe00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ morph-primitives = { path = "crates/primitives", default-features = false, featu morph-revm = { path = "crates/revm", default-features = false } reth-basic-payload-builder = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } +reth-chain-state = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } reth-chainspec = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } reth-cli = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } reth-cli-commands = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } @@ -72,6 +73,7 @@ reth-ethereum-engine-primitives = { git = "https://github.com/paradigmxyz/reth", reth-ethereum-primitives = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3", default-features = false } reth-evm = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } reth-evm-ethereum = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } +reth-execution-types = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } reth-metrics = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } reth-network-peers = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } reth-node-api = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } @@ -81,6 +83,7 @@ reth-node-ethereum = { git = "https://github.com/paradigmxyz/reth", rev = "64909 reth-node-metrics = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } reth-payload-builder = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } reth-payload-primitives = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } +reth-payload-util = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } reth-primitives-traits = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3", default-features = false } reth-provider = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } reth-rpc = { git = "https://github.com/paradigmxyz/reth", rev = "64909d3" } diff --git a/crates/chainspec/Cargo.toml b/crates/chainspec/Cargo.toml index 59da654..a7fccd9 100644 --- a/crates/chainspec/Cargo.toml +++ b/crates/chainspec/Cargo.toml @@ -12,6 +12,8 @@ publish.workspace = true workspace = true [dependencies] +morph-primitives.workspace = true + reth-cli = { workspace = true, optional = true } reth-chainspec.workspace = true reth-network-peers.workspace = true diff --git a/crates/chainspec/src/genesis.rs b/crates/chainspec/src/genesis.rs index 1058244..c38ac6b 100644 --- a/crates/chainspec/src/genesis.rs +++ b/crates/chainspec/src/genesis.rs @@ -40,7 +40,7 @@ impl TryFrom<&OtherFields> for MorphGenesisInfo { /// the Morph hardforks were activated. /// /// Note: Bernoulli and Curie use block-based activation, while Morph203, Viridian, -/// and Emerald use timestamp-based activation (matching go-ethereum behavior). +/// Emerald, and MPTFork use timestamp-based activation (matching go-ethereum behavior). #[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MorphHardforkInfo { @@ -59,6 +59,9 @@ pub struct MorphHardforkInfo { /// Emerald hardfork timestamp. #[serde(skip_serializing_if = "Option::is_none")] pub emerald_time: Option, + /// MPTFork hardfork timestamp. + #[serde(skip_serializing_if = "Option::is_none")] + pub mpt_fork_time: Option, } impl MorphHardforkInfo { @@ -132,7 +135,8 @@ mod tests { "curieBlock": 100, "morph203Time": 3000, "viridianTime": 4000, - "emeraldTime": 5000 + "emeraldTime": 5000, + "mptForkTime": 6000 } "#; @@ -147,6 +151,7 @@ mod tests { morph203_time: Some(3000), viridian_time: Some(4000), emerald_time: Some(5000), + mpt_fork_time: Some(6000), } ); } diff --git a/crates/chainspec/src/hardfork.rs b/crates/chainspec/src/hardfork.rs index 9613a50..0b882a9 100644 --- a/crates/chainspec/src/hardfork.rs +++ b/crates/chainspec/src/hardfork.rs @@ -28,7 +28,8 @@ //! //! ## Current State //! -//! The `Bernoulli` variant is a placeholder representing the pre-hardfork baseline. +//! Bernoulli and Curie use block-based activation, while Morph203, Viridian, +//! Emerald, and MPTFork use timestamp-based activation. use alloy_evm::revm::primitives::hardfork::SpecId; use alloy_hardforks::hardfork; @@ -36,24 +37,35 @@ use reth_chainspec::{EthereumHardforks, ForkCondition}; hardfork!( /// Morph-specific hardforks for network upgrades. + /// + /// Note: Bernoulli and Curie use block-based activation, while Morph203, Viridian, + /// Emerald, and MPTFork use timestamp-based activation (matching go-ethereum behavior). #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Default)] MorphHardfork { - /// Bernoulli. + /// Bernoulli hardfork (block-based). Bernoulli, - /// Curie hardfork. + /// Curie hardfork (block-based). Curie, - /// Morph203 hardfork. + /// Morph203 hardfork (timestamp-based). Morph203, - /// Viridian hardfork. + /// Viridian hardfork (timestamp-based). Viridian, - /// Emerald hardfork. - #[default] + /// Emerald hardfork (timestamp-based). Emerald, + /// MPTFork hardfork (timestamp-based). + #[default] + MPTFork, } ); impl MorphHardfork { + /// Returns `true` if this hardfork is Bernoulli or later. + #[inline] + pub fn is_bernoulli(self) -> bool { + self >= Self::Bernoulli + } + /// Returns `true` if this hardfork is Curie or later. #[inline] pub fn is_curie(self) -> bool { @@ -61,19 +73,28 @@ impl MorphHardfork { } /// Returns `true` if this hardfork is Morph203 or later. + #[inline] pub fn is_morph203(self) -> bool { self >= Self::Morph203 } /// Returns `true` if this hardfork is Viridian or later. + #[inline] pub fn is_viridian(self) -> bool { self >= Self::Viridian } /// Returns `true` if this hardfork is Emerald or later. + #[inline] pub fn is_emerald(self) -> bool { self >= Self::Emerald } + + /// Returns `true` if this hardfork is MPTFork or later. + #[inline] + pub fn is_mpt_fork(self) -> bool { + self >= Self::MPTFork + } } /// Trait for querying Morph-specific hardfork activations. @@ -113,12 +134,20 @@ pub trait MorphHardforks: EthereumHardforks { .active_at_timestamp(timestamp) } + /// Convenience method to check if MPTFork hardfork is active at a given timestamp. + fn is_mpt_fork_active_at_timestamp(&self, timestamp: u64) -> bool { + self.morph_fork_activation(MorphHardfork::MPTFork) + .active_at_timestamp(timestamp) + } + /// Retrieves the latest Morph hardfork active at a given block and timestamp. /// /// Note: This method checks both block-based (Bernoulli, Curie) and - /// timestamp-based (Morph203, Viridian, Emerald) hardforks. + /// timestamp-based (Morph203, Viridian, Emerald, MPTFork) hardforks. fn morph_hardfork_at(&self, block_number: u64, timestamp: u64) -> MorphHardfork { - if self.is_emerald_active_at_timestamp(timestamp) { + if self.is_mpt_fork_active_at_timestamp(timestamp) { + MorphHardfork::MPTFork + } else if self.is_emerald_active_at_timestamp(timestamp) { MorphHardfork::Emerald } else if self.is_viridian_active_at_timestamp(timestamp) { MorphHardfork::Viridian @@ -127,6 +156,7 @@ pub trait MorphHardforks: EthereumHardforks { } else if self.is_curie_active_at_block(block_number) { MorphHardfork::Curie } else { + // Default to Bernoulli (baseline) MorphHardfork::Bernoulli } } @@ -140,6 +170,7 @@ impl From for SpecId { MorphHardfork::Morph203 => Self::OSAKA, MorphHardfork::Viridian => Self::OSAKA, MorphHardfork::Emerald => Self::OSAKA, + MorphHardfork::MPTFork => Self::OSAKA, } } } @@ -151,7 +182,9 @@ impl From for MorphHardfork { /// `From for SpecId`, because multiple Morph /// hardforks may share the same underlying EVM spec. fn from(spec: SpecId) -> Self { - if spec.is_enabled_in(SpecId::from(Self::Emerald)) { + if spec.is_enabled_in(SpecId::from(Self::MPTFork)) { + Self::MPTFork + } else if spec.is_enabled_in(SpecId::from(Self::Emerald)) { Self::Emerald } else if spec.is_enabled_in(SpecId::from(Self::Viridian)) { Self::Viridian @@ -171,14 +204,14 @@ mod tests { use reth_chainspec::Hardfork; #[test] - fn test_bernoulli_hardfork_name() { - let fork = MorphHardfork::Bernoulli; - assert_eq!(fork.name(), "Bernoulli"); + fn test_morph203_hardfork_name() { + let fork = MorphHardfork::Morph203; + assert_eq!(fork.name(), "Morph203"); } #[test] fn test_hardfork_trait_implementation() { - let fork = MorphHardfork::Bernoulli; + let fork = MorphHardfork::Morph203; // Should implement Hardfork trait let _name: &str = Hardfork::name(&fork); } @@ -186,11 +219,11 @@ mod tests { #[test] #[cfg(feature = "serde")] fn test_morph_hardfork_serde() { - let fork = MorphHardfork::Bernoulli; + let fork = MorphHardfork::Morph203; // Serialize to JSON let json = serde_json::to_string(&fork).unwrap(); - assert_eq!(json, "\"Bernoulli\""); + assert_eq!(json, "\"Morph203\""); // Deserialize from JSON let deserialized: MorphHardfork = serde_json::from_str(&json).unwrap(); @@ -204,18 +237,17 @@ mod tests { assert!(MorphHardfork::Morph203.is_curie()); assert!(MorphHardfork::Viridian.is_curie()); assert!(MorphHardfork::Emerald.is_curie()); + assert!(MorphHardfork::MPTFork.is_curie()); } #[test] fn test_is_morph203() { assert!(!MorphHardfork::Bernoulli.is_morph203()); assert!(!MorphHardfork::Curie.is_morph203()); - assert!(MorphHardfork::Morph203.is_morph203()); assert!(MorphHardfork::Viridian.is_morph203()); assert!(MorphHardfork::Emerald.is_morph203()); - - assert!(MorphHardfork::Morph203.is_curie()); + assert!(MorphHardfork::MPTFork.is_morph203()); } #[test] @@ -224,9 +256,8 @@ mod tests { assert!(!MorphHardfork::Curie.is_viridian()); assert!(!MorphHardfork::Morph203.is_viridian()); assert!(MorphHardfork::Viridian.is_viridian()); - assert!(MorphHardfork::Viridian.is_morph203()); - assert!(MorphHardfork::Viridian.is_curie()); assert!(MorphHardfork::Emerald.is_viridian()); + assert!(MorphHardfork::MPTFork.is_viridian()); } #[test] @@ -236,8 +267,16 @@ mod tests { assert!(!MorphHardfork::Morph203.is_emerald()); assert!(!MorphHardfork::Viridian.is_emerald()); assert!(MorphHardfork::Emerald.is_emerald()); - assert!(MorphHardfork::Emerald.is_viridian()); - assert!(MorphHardfork::Emerald.is_morph203()); - assert!(MorphHardfork::Emerald.is_curie()); + assert!(MorphHardfork::MPTFork.is_emerald()); + } + + #[test] + fn test_is_mpt_fork() { + assert!(!MorphHardfork::Bernoulli.is_mpt_fork()); + assert!(!MorphHardfork::Curie.is_mpt_fork()); + assert!(!MorphHardfork::Morph203.is_mpt_fork()); + assert!(!MorphHardfork::Viridian.is_mpt_fork()); + assert!(!MorphHardfork::Emerald.is_mpt_fork()); + assert!(MorphHardfork::MPTFork.is_mpt_fork()); } } diff --git a/crates/chainspec/src/lib.rs b/crates/chainspec/src/lib.rs index 20591e5..5a8c68c 100644 --- a/crates/chainspec/src/lib.rs +++ b/crates/chainspec/src/lib.rs @@ -20,24 +20,32 @@ //! //! # Example //! -//! ```ignore +//! ``` //! use morph_chainspec::{MorphChainSpec, MORPH_MAINNET}; //! use morph_chainspec::hardfork::MorphHardforks; //! //! // Use predefined mainnet spec //! let mainnet = MORPH_MAINNET.clone(); //! assert!(mainnet.is_bernoulli_active_at_block(0)); +//! ``` +//! +//! Create from genesis JSON: //! -//! // Or create from genesis JSON -//! let genesis: Genesis = serde_json::from_str(genesis_json)?; +//! ```no_run +//! use alloy_genesis::Genesis; +//! use morph_chainspec::MorphChainSpec; +//! +//! let genesis_json = std::fs::read_to_string("genesis.json").unwrap(); +//! let genesis: Genesis = serde_json::from_str(&genesis_json).unwrap(); //! let chain_spec = MorphChainSpec::from(genesis); //! ``` #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg))] -// Used only in tests, but declared here to silence unused_crate_dependencies warning -use serde_json as _; +// Used only for feature propagation (alloy-consensus/serde), but declared here +// to silence the unused_crate_dependencies warning. +use alloy_consensus as _; pub mod constants; pub mod genesis; @@ -52,6 +60,9 @@ pub use constants::*; // Re-export genesis types pub use genesis::{MorphChainConfig, MorphGenesisInfo, MorphHardforkInfo}; +// Re-export hardfork types +pub use hardfork::{MorphHardfork, MorphHardforks}; + pub use morph::MORPH_MAINNET; pub use morph_hoodi::MORPH_HOODI; pub use spec::MorphChainSpec; diff --git a/crates/chainspec/src/spec.rs b/crates/chainspec/src/spec.rs index 6bfda8c..67a7576 100644 --- a/crates/chainspec/src/spec.rs +++ b/crates/chainspec/src/spec.rs @@ -6,11 +6,11 @@ use crate::{ hardfork::{MorphHardfork, MorphHardforks}, }; use alloy_chains::Chain; -use alloy_consensus::Header; use alloy_eips::eip7840::BlobParams; use alloy_evm::eth::spec::EthExecutorSpec; use alloy_genesis::Genesis; use alloy_primitives::{Address, B256, U256}; +use morph_primitives::MorphHeader; use reth_chainspec::{ BaseFeeParams, ChainSpec, DepositContract, DisplayHardforks, EthChainSpec, EthereumHardfork, EthereumHardforks, ForkCondition, ForkFilter, ForkId, Hardfork, Hardforks, Head, @@ -87,13 +87,14 @@ impl ChainConfig for MorphChainSpec { #[derive(Debug, Clone, PartialEq, Eq)] pub struct MorphChainSpec { /// [`ChainSpec`]. - pub inner: ChainSpec
, + pub inner: ChainSpec, + /// Morph-specific genesis info. pub info: MorphGenesisInfo, } impl MorphChainSpec { /// Create a new [`MorphChainSpec`] with the given inner spec and config. - pub fn new(inner: ChainSpec
, info: MorphGenesisInfo) -> Self { + pub fn new(inner: ChainSpec, info: MorphGenesisInfo) -> Self { Self { inner, info } } @@ -137,7 +138,7 @@ impl From for MorphChainSpec { // Add Morph hardforks // Note: Bernoulli and Curie use block-based activation, - // while Morph203, Viridian, and Emerald use timestamp-based activation. + // while Morph203, Viridian, Emerald, and MPTFork use timestamp-based activation. let block_forks = vec![ (MorphHardfork::Bernoulli, hardfork_info.bernoulli_block), (MorphHardfork::Curie, hardfork_info.curie_block), @@ -149,6 +150,7 @@ impl From for MorphChainSpec { (MorphHardfork::Morph203, hardfork_info.morph203_time), (MorphHardfork::Viridian, hardfork_info.viridian_time), (MorphHardfork::Emerald, hardfork_info.emerald_time), + (MorphHardfork::MPTFork, hardfork_info.mpt_fork_time), ] .into_iter() .filter_map(|(fork, time)| time.map(|t| (fork, ForkCondition::Timestamp(t)))); @@ -158,7 +160,7 @@ impl From for MorphChainSpec { base_spec.hardforks.extend(morph_forks); Self { - inner: base_spec, + inner: base_spec.map_header(MorphHeader::from), info: chain_info, } } @@ -191,7 +193,7 @@ impl Hardforks for MorphChainSpec { } impl EthChainSpec for MorphChainSpec { - type Header = Header; + type Header = MorphHeader; fn chain(&self) -> Chain { self.inner.chain() @@ -244,7 +246,7 @@ impl EthChainSpec for MorphChainSpec { self.inner.get_final_paris_total_difficulty() } - fn next_block_base_fee(&self, _parent: &Header, _target_timestamp: u64) -> Option { + fn next_block_base_fee(&self, _parent: &MorphHeader, _target_timestamp: u64) -> Option { Some(MORPH_BASE_FEE) } } diff --git a/crates/consensus/src/lib.rs b/crates/consensus/src/lib.rs index 4b608bd..36ce633 100644 --- a/crates/consensus/src/lib.rs +++ b/crates/consensus/src/lib.rs @@ -18,7 +18,6 @@ //! //! # Example //! -//! ```ignore //! use morph_consensus::MorphConsensus; //! use std::sync::Arc; //! diff --git a/crates/consensus/src/validation.rs b/crates/consensus/src/validation.rs index d91e9ad..727f279 100644 --- a/crates/consensus/src/validation.rs +++ b/crates/consensus/src/validation.rs @@ -38,8 +38,8 @@ use crate::MorphConsensusError; use alloy_consensus::{BlockHeader as _, EMPTY_OMMER_ROOT_HASH, TxReceipt}; use alloy_evm::block::BlockExecutionResult; use alloy_primitives::{B256, Bloom}; -use morph_chainspec::{MorphChainSpec, hardfork::MorphHardforks}; -use morph_primitives::{Block, BlockBody, MorphReceipt, MorphTxEnvelope}; +use morph_chainspec::MorphChainSpec; +use morph_primitives::{Block, BlockBody, MorphHeader, MorphReceipt, MorphTxEnvelope}; use reth_consensus::{Consensus, ConsensusError, FullConsensus, HeaderValidator}; use reth_consensus_common::validation::{ validate_against_parent_hash_number, validate_body_against_header, @@ -96,7 +96,7 @@ impl MorphConsensus { // HeaderValidator Implementation // ============================================================================ -impl HeaderValidator for MorphConsensus { +impl HeaderValidator for MorphConsensus { /// Validates a block header according to Morph L2 consensus rules. /// /// # Validation Steps @@ -110,10 +110,7 @@ impl HeaderValidator for MorphConsensus { /// 7. **Gas Limit**: Must be <= MAX_GAS_LIMIT /// 8. **Gas Used**: Must be <= gas limit /// 9. **Base Fee**: Must be set after Curie hardfork and <= 10 Gwei - fn validate_header( - &self, - header: &SealedHeader, - ) -> Result<(), ConsensusError> { + fn validate_header(&self, header: &SealedHeader) -> Result<(), ConsensusError> { // Extra data must be empty (Morph L2 specific - stricter than max length) if !header.extra_data().is_empty() { return Err(ConsensusError::ExtraDataExceedsMax { @@ -173,17 +170,14 @@ impl HeaderValidator for MorphConsensus { }); } - // Validate the EIP1559 fee is set if the header is after Curie - // Note: Curie uses block-based activation - if self.chain_spec.is_curie_active_at_block(header.number()) { - let base_fee = header - .base_fee_per_gas() - .ok_or(ConsensusError::BaseFeeMissing)?; - if base_fee > MORPH_MAXIMUM_BASE_FEE { - return Err(ConsensusError::Other( - MorphConsensusError::BaseFeeOverLimit(base_fee).to_string(), - )); - } + // Validate base fee (always required, EIP-1559 is always active) + let base_fee = header + .base_fee_per_gas() + .ok_or(ConsensusError::BaseFeeMissing)?; + if base_fee > MORPH_MAXIMUM_BASE_FEE { + return Err(ConsensusError::Other( + MorphConsensusError::BaseFeeOverLimit(base_fee).to_string(), + )); } Ok(()) } @@ -198,8 +192,8 @@ impl HeaderValidator for MorphConsensus { /// 4. **Gas Limit**: Change must be within 1/1024 of parent's limit fn validate_header_against_parent( &self, - header: &SealedHeader, - parent: &SealedHeader, + header: &SealedHeader, + parent: &SealedHeader, ) -> Result<(), ConsensusError> { // Validate parent hash and block number validate_against_parent_hash_number(header.header(), parent)?; @@ -227,7 +221,7 @@ impl Consensus for MorphConsensus { fn validate_body_against_header( &self, body: &BlockBody, - header: &SealedHeader, + header: &SealedHeader, ) -> Result<(), Self::Error> { validate_body_against_header(body, header.header()) } @@ -528,7 +522,7 @@ mod tests { use super::*; use alloy_consensus::{Header, Signed}; use alloy_genesis::Genesis; - use alloy_primitives::{Address, B64, B256, Bytes, Signature, TxKind, U256}; + use alloy_primitives::{Address, B64, B256, Bytes, Signature, U256}; use morph_primitives::transaction::TxL1Msg; fn create_test_chainspec() -> Arc { @@ -560,17 +554,17 @@ mod tests { } fn create_l1_msg_tx(queue_index: u64) -> MorphTxEnvelope { + use alloy_consensus::Sealed; let tx = TxL1Msg { queue_index, - from: Address::ZERO, - nonce: queue_index, // nonce is used as queue index for L1 messages gas_limit: 21000, - to: TxKind::Call(Address::ZERO), + to: Address::ZERO, value: U256::ZERO, input: Bytes::default(), + sender: Address::ZERO, }; - let sig = Signature::new(U256::ZERO, U256::ZERO, false); - MorphTxEnvelope::L1Msg(Signed::new_unchecked(tx, sig, B256::ZERO)) + // L1 messages have no signature - use Sealed instead of Signed + MorphTxEnvelope::L1Msg(Sealed::new(tx)) } fn create_regular_tx() -> MorphTxEnvelope { @@ -580,6 +574,11 @@ mod tests { MorphTxEnvelope::Legacy(Signed::new_unchecked(tx, sig, B256::ZERO)) } + /// Create a MorphHeader from a standard Header + fn create_morph_header(inner: Header) -> MorphHeader { + inner.into() + } + #[test] fn test_morph_consensus_creation() { let chain_spec = create_test_chainspec(); @@ -613,12 +612,12 @@ mod tests { fn test_validate_header_extra_data_not_empty() { let chain_spec = create_test_chainspec(); let consensus = MorphConsensus::new(chain_spec); - let header = Header { + let header = create_morph_header(Header { extra_data: Bytes::from([1, 2, 3].as_slice()), nonce: B64::ZERO, ommers_hash: EMPTY_OMMER_ROOT_HASH, ..Default::default() - }; + }); let sealed = SealedHeader::seal_slow(header); let result = consensus.validate_header(&sealed); assert!(matches!( @@ -631,12 +630,12 @@ mod tests { fn test_validate_header_invalid_difficulty() { let chain_spec = create_test_chainspec(); let consensus = MorphConsensus::new(chain_spec); - let header = Header { + let header = create_morph_header(Header { difficulty: U256::from(1), ommers_hash: EMPTY_OMMER_ROOT_HASH, nonce: B64::ZERO, ..Default::default() - }; + }); let sealed = SealedHeader::seal_slow(header); let result = consensus.validate_header(&sealed); assert!(matches!( @@ -649,11 +648,11 @@ mod tests { fn test_validate_header_invalid_nonce() { let chain_spec = create_test_chainspec(); let consensus = MorphConsensus::new(chain_spec); - let header = Header { + let header = create_morph_header(Header { nonce: B64::from(1u64), ommers_hash: EMPTY_OMMER_ROOT_HASH, ..Default::default() - }; + }); let sealed = SealedHeader::seal_slow(header); let result = consensus.validate_header(&sealed); assert!(matches!( @@ -666,11 +665,11 @@ mod tests { fn test_validate_header_invalid_ommers() { let chain_spec = create_test_chainspec(); let consensus = MorphConsensus::new(chain_spec); - let header = Header { + let header = create_morph_header(Header { nonce: B64::ZERO, ommers_hash: B256::ZERO, // not EMPTY_OMMER_ROOT_HASH ..Default::default() - }; + }); let sealed = SealedHeader::seal_slow(header); let result = consensus.validate_header(&sealed); assert!(matches!( @@ -683,13 +682,13 @@ mod tests { fn test_validate_header_gas_used_exceeds_limit() { let chain_spec = create_test_chainspec(); let consensus = MorphConsensus::new(chain_spec); - let header = Header { + let header = create_morph_header(Header { nonce: B64::ZERO, ommers_hash: EMPTY_OMMER_ROOT_HASH, gas_limit: 1000, gas_used: 2000, // exceeds gas_limit ..Default::default() - }; + }); let sealed = SealedHeader::seal_slow(header); let result = consensus.validate_header(&sealed); assert!(matches!( @@ -707,7 +706,7 @@ mod tests { .duration_since(std::time::SystemTime::UNIX_EPOCH) .unwrap() .as_secs(); - let header = Header { + let header = create_morph_header(Header { nonce: B64::ZERO, ommers_hash: EMPTY_OMMER_ROOT_HASH, gas_limit: 30_000_000, @@ -715,7 +714,7 @@ mod tests { timestamp: now - 10, // 10 seconds ago base_fee_per_gas: Some(1_000_000), // 0.001 Gwei (after Curie) ..Default::default() - }; + }); let sealed = SealedHeader::seal_slow(header); let result = consensus.validate_header(&sealed); assert!(result.is_ok()); @@ -826,14 +825,14 @@ mod tests { .as_secs() + 3600; // 1 hour in the future - let header = Header { + let header = create_morph_header(Header { nonce: B64::ZERO, ommers_hash: EMPTY_OMMER_ROOT_HASH, gas_limit: 30_000_000, timestamp: future_ts, base_fee_per_gas: Some(1_000_000), ..Default::default() - }; + }); let sealed = SealedHeader::seal_slow(header); let result = consensus.validate_header(&sealed); assert!(matches!( @@ -851,14 +850,14 @@ mod tests { .unwrap() .as_secs(); - let header = Header { + let header = create_morph_header(Header { nonce: B64::ZERO, ommers_hash: EMPTY_OMMER_ROOT_HASH, gas_limit: MAX_GAS_LIMIT + 1, // Exceeds max timestamp: now - 10, base_fee_per_gas: Some(1_000_000), ..Default::default() - }; + }); let sealed = SealedHeader::seal_slow(header); let result = consensus.validate_header(&sealed); assert!(matches!( @@ -876,14 +875,14 @@ mod tests { .unwrap() .as_secs(); - let header = Header { + let header = create_morph_header(Header { nonce: B64::ZERO, ommers_hash: EMPTY_OMMER_ROOT_HASH, gas_limit: 30_000_000, timestamp: now - 10, base_fee_per_gas: Some(MORPH_MAXIMUM_BASE_FEE + 1), // Over limit ..Default::default() - }; + }); let sealed = SealedHeader::seal_slow(header); let result = consensus.validate_header(&sealed); assert!(result.is_err()); @@ -892,7 +891,7 @@ mod tests { } #[test] - fn test_validate_header_base_fee_missing_after_curie() { + fn test_validate_header_base_fee_missing() { let chain_spec = create_test_chainspec(); let consensus = MorphConsensus::new(chain_spec); let now = std::time::SystemTime::now() @@ -900,14 +899,14 @@ mod tests { .unwrap() .as_secs(); - let header = Header { + let header = create_morph_header(Header { nonce: B64::ZERO, ommers_hash: EMPTY_OMMER_ROOT_HASH, gas_limit: 30_000_000, timestamp: now - 10, - base_fee_per_gas: None, // Missing after Curie + base_fee_per_gas: None, // Missing (required) ..Default::default() - }; + }); let sealed = SealedHeader::seal_slow(header); let result = consensus.validate_header(&sealed); assert!(matches!(result, Err(ConsensusError::BaseFeeMissing))); @@ -922,14 +921,14 @@ mod tests { .unwrap() .as_secs(); - let header = Header { + let header = create_morph_header(Header { nonce: B64::ZERO, ommers_hash: EMPTY_OMMER_ROOT_HASH, gas_limit: 30_000_000, timestamp: now - 10, base_fee_per_gas: Some(MORPH_MAXIMUM_BASE_FEE), // Exactly at max (valid) ..Default::default() - }; + }); let sealed = SealedHeader::seal_slow(header); let result = consensus.validate_header(&sealed); assert!(result.is_ok()); @@ -939,8 +938,8 @@ mod tests { // Header Against Parent Validation Tests // ======================================================================== - fn create_valid_header(timestamp: u64, gas_limit: u64, number: u64) -> Header { - Header { + fn create_valid_morph_header(timestamp: u64, gas_limit: u64, number: u64) -> MorphHeader { + create_morph_header(Header { nonce: B64::ZERO, ommers_hash: EMPTY_OMMER_ROOT_HASH, gas_limit, @@ -948,7 +947,7 @@ mod tests { number, base_fee_per_gas: Some(1_000_000), ..Default::default() - } + }) } #[test] @@ -956,11 +955,11 @@ mod tests { let chain_spec = create_test_chainspec(); let consensus = MorphConsensus::new(chain_spec); - let parent = create_valid_header(1000, 30_000_000, 100); + let parent = create_valid_morph_header(1000, 30_000_000, 100); let parent_sealed = SealedHeader::seal_slow(parent); - let mut child = create_valid_header(1001, 30_000_000, 101); - child.parent_hash = parent_sealed.hash(); + let mut child = create_valid_morph_header(1001, 30_000_000, 101); + child.inner.parent_hash = parent_sealed.hash(); let child_sealed = SealedHeader::seal_slow(child); let result = consensus.validate_header_against_parent(&child_sealed, &parent_sealed); @@ -972,11 +971,11 @@ mod tests { let chain_spec = create_test_chainspec(); let consensus = MorphConsensus::new(chain_spec); - let parent = create_valid_header(1000, 30_000_000, 100); + let parent = create_valid_morph_header(1000, 30_000_000, 100); let parent_sealed = SealedHeader::seal_slow(parent); - let mut child = create_valid_header(999, 30_000_000, 101); // timestamp < parent - child.parent_hash = parent_sealed.hash(); + let mut child = create_valid_morph_header(999, 30_000_000, 101); // timestamp < parent + child.inner.parent_hash = parent_sealed.hash(); let child_sealed = SealedHeader::seal_slow(child); let result = consensus.validate_header_against_parent(&child_sealed, &parent_sealed); @@ -991,11 +990,11 @@ mod tests { let chain_spec = create_test_chainspec(); let consensus = MorphConsensus::new(chain_spec); - let parent = create_valid_header(1000, 30_000_000, 100); + let parent = create_valid_morph_header(1000, 30_000_000, 100); let parent_sealed = SealedHeader::seal_slow(parent); - let mut child = create_valid_header(1000, 30_000_000, 101); // timestamp == parent (valid) - child.parent_hash = parent_sealed.hash(); + let mut child = create_valid_morph_header(1000, 30_000_000, 101); // timestamp == parent (valid) + child.inner.parent_hash = parent_sealed.hash(); let child_sealed = SealedHeader::seal_slow(child); let result = consensus.validate_header_against_parent(&child_sealed, &parent_sealed); @@ -1011,12 +1010,12 @@ mod tests { let parent_gas_limit = 30_000_000u64; let max_increase = parent_gas_limit / GAS_LIMIT_BOUND_DIVISOR; - let parent = create_valid_header(1000, parent_gas_limit, 100); + let parent = create_valid_morph_header(1000, parent_gas_limit, 100); let parent_sealed = SealedHeader::seal_slow(parent); // Increase by more than allowed - let mut child = create_valid_header(1001, parent_gas_limit + max_increase + 1, 101); - child.parent_hash = parent_sealed.hash(); + let mut child = create_valid_morph_header(1001, parent_gas_limit + max_increase + 1, 101); + child.inner.parent_hash = parent_sealed.hash(); let child_sealed = SealedHeader::seal_slow(child); let result = consensus.validate_header_against_parent(&child_sealed, &parent_sealed); @@ -1034,12 +1033,12 @@ mod tests { let parent_gas_limit = 30_000_000u64; let max_decrease = parent_gas_limit / GAS_LIMIT_BOUND_DIVISOR; - let parent = create_valid_header(1000, parent_gas_limit, 100); + let parent = create_valid_morph_header(1000, parent_gas_limit, 100); let parent_sealed = SealedHeader::seal_slow(parent); // Decrease by more than allowed - let mut child = create_valid_header(1001, parent_gas_limit - max_decrease - 1, 101); - child.parent_hash = parent_sealed.hash(); + let mut child = create_valid_morph_header(1001, parent_gas_limit - max_decrease - 1, 101); + child.inner.parent_hash = parent_sealed.hash(); let child_sealed = SealedHeader::seal_slow(child); let result = consensus.validate_header_against_parent(&child_sealed, &parent_sealed); @@ -1057,12 +1056,12 @@ mod tests { let parent_gas_limit = 30_000_000u64; let max_change = parent_gas_limit / GAS_LIMIT_BOUND_DIVISOR; - let parent = create_valid_header(1000, parent_gas_limit, 100); + let parent = create_valid_morph_header(1000, parent_gas_limit, 100); let parent_sealed = SealedHeader::seal_slow(parent); // Increase by exactly the allowed amount (valid) - let mut child = create_valid_header(1001, parent_gas_limit + max_change, 101); - child.parent_hash = parent_sealed.hash(); + let mut child = create_valid_morph_header(1001, parent_gas_limit + max_change, 101); + child.inner.parent_hash = parent_sealed.hash(); let child_sealed = SealedHeader::seal_slow(child); let result = consensus.validate_header_against_parent(&child_sealed, &parent_sealed); @@ -1077,11 +1076,11 @@ mod tests { // Use a parent gas limit that allows decreasing to below minimum within bounds // Parent = MINIMUM_GAS_LIMIT, so max decrease = MINIMUM_GAS_LIMIT / 1024 = 4 // Child = MINIMUM_GAS_LIMIT - 1 = 4999, change = 1 which is < 4 (within bounds) - let parent = create_valid_header(1000, MINIMUM_GAS_LIMIT, 100); + let parent = create_valid_morph_header(1000, MINIMUM_GAS_LIMIT, 100); let parent_sealed = SealedHeader::seal_slow(parent); - let mut child = create_valid_header(1001, MINIMUM_GAS_LIMIT - 1, 101); - child.parent_hash = parent_sealed.hash(); + let mut child = create_valid_morph_header(1001, MINIMUM_GAS_LIMIT - 1, 101); + child.inner.parent_hash = parent_sealed.hash(); let child_sealed = SealedHeader::seal_slow(child); let result = consensus.validate_header_against_parent(&child_sealed, &parent_sealed); @@ -1096,11 +1095,11 @@ mod tests { let chain_spec = create_test_chainspec(); let consensus = MorphConsensus::new(chain_spec); - let parent = create_valid_header(1000, 30_000_000, 100); + let parent = create_valid_morph_header(1000, 30_000_000, 100); let parent_sealed = SealedHeader::seal_slow(parent); - let mut child = create_valid_header(1001, 30_000_000, 101); - child.parent_hash = B256::random(); // Wrong parent hash + let mut child = create_valid_morph_header(1001, 30_000_000, 101); + child.inner.parent_hash = B256::random(); // Wrong parent hash let child_sealed = SealedHeader::seal_slow(child); let result = consensus.validate_header_against_parent(&child_sealed, &parent_sealed); @@ -1112,11 +1111,11 @@ mod tests { let chain_spec = create_test_chainspec(); let consensus = MorphConsensus::new(chain_spec); - let parent = create_valid_header(1000, 30_000_000, 100); + let parent = create_valid_morph_header(1000, 30_000_000, 100); let parent_sealed = SealedHeader::seal_slow(parent); - let mut child = create_valid_header(1001, 30_000_000, 102); // Should be 101 - child.parent_hash = parent_sealed.hash(); + let mut child = create_valid_morph_header(1001, 30_000_000, 102); // Should be 101 + child.inner.parent_hash = parent_sealed.hash(); let child_sealed = SealedHeader::seal_slow(child); let result = consensus.validate_header_against_parent(&child_sealed, &parent_sealed); @@ -1169,8 +1168,22 @@ mod tests { // ======================================================================== // Gas Limit Validation Helper Tests + // These use Header directly since the generic helper functions work + // on any type implementing BlockHeader trait. // ======================================================================== + fn create_valid_header(timestamp: u64, gas_limit: u64, number: u64) -> Header { + Header { + nonce: B64::ZERO, + ommers_hash: EMPTY_OMMER_ROOT_HASH, + gas_limit, + timestamp, + number, + base_fee_per_gas: Some(1_000_000), + ..Default::default() + } + } + #[test] fn test_validate_against_parent_gas_limit_no_change() { let parent = create_valid_header(1000, 30_000_000, 100); diff --git a/crates/evm/Cargo.toml b/crates/evm/Cargo.toml index c2a25d7..e2754da 100644 --- a/crates/evm/Cargo.toml +++ b/crates/evm/Cargo.toml @@ -20,7 +20,7 @@ reth-chainspec = { workspace = true, optional = true } reth-evm.workspace = true reth-evm-ethereum.workspace = true reth-revm.workspace = true -reth-primitives-traits = { workspace = true, optional = true } +reth-primitives-traits.workspace = true reth-rpc-eth-api = { workspace = true, optional = true } alloy-evm.workspace = true @@ -29,14 +29,17 @@ alloy-primitives.workspace = true derive_more.workspace = true thiserror.workspace = true +revm.workspace = true +tracing.workspace = true [dev-dependencies] -revm.workspace = true serde_json.workspace = true alloy-genesis.workspace = true [features] default = [] -reth-codec = ["morph-primitives/reth-codec"] +reth-codec = [ + "morph-primitives/reth-codec", + "dep:reth-chainspec", +] 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 4221edd..f2142ac 100644 --- a/crates/evm/src/assemble.rs +++ b/crates/evm/src/assemble.rs @@ -6,6 +6,7 @@ use morph_chainspec::MorphChainSpec; use morph_primitives::MorphHeader; use reth_evm::execute::{BlockAssembler, BlockAssemblerInput}; use reth_evm_ethereum::EthBlockAssembler; +use reth_primitives_traits::SealedHeader; use std::sync::Arc; /// Assembler for Morph blocks. @@ -41,18 +42,26 @@ impl BlockAssembler for MorphBlockAssembler { .. } = 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()); + // Delegate block building to the inner assembler - self.inner.assemble_block(BlockAssemblerInput::< + let block = self.inner.assemble_block(BlockAssemblerInput::< EthBlockExecutorFactory, >::new( evm_env, inner, - parent, + &inner_parent, transactions, output, bundle_state, state_provider, state_root, - )) + ))?; + + // 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)) } } diff --git a/crates/evm/src/block/curie.rs b/crates/evm/src/block/curie.rs new file mode 100644 index 0000000..36c7358 --- /dev/null +++ b/crates/evm/src/block/curie.rs @@ -0,0 +1,149 @@ +//! Curie fork transition for Morph. +//! +//! The Curie hardfork introduced blob-based DA cost calculation for Morph L2. +//! +//! ## Changes +//! +//! 1. **Fee reduction** - Uses compressed blobs on L1 for lower DA costs. +//! 2. **Updated L1 gas oracle** - New storage slots for blob-based fee calculation: +//! - `l1BlobBaseFee` slot initialized to 1 +//! - `commitScalar` slot initialized to `InitialCommitScalar` +//! - `blobScalar` slot initialized to `InitialBlobScalar` +//! - `isCurie` slot set to 1 (true) +//! +//! ## DA Cost Formula +//! +//! - Pre-Curie: `(l1GasUsed(txRlp) + overhead) * l1BaseFee * scalar` +//! - Post-Curie: `l1BaseFee * commitScalar + len(txRlp) * l1BlobBaseFee * blobScalar` +//! +//! Reference: `consensus/misc/curie.go` in morph go-ethereum + +use morph_revm::{CURIE_L1_GAS_PRICE_ORACLE_STORAGE, L1_GAS_PRICE_ORACLE_ADDRESS}; +use revm::{ + Database, + database::{State, states::StorageSlot}, +}; + +/// Applies the Morph Curie hard fork to the state. +/// +/// Updates L1GasPriceOracle storage slots: +/// - Sets `l1BlobBaseFee` slot to 1 +/// - Sets `commitScalar` slot to initial value +/// - Sets `blobScalar` slot to initial value +/// - Sets `isCurie` slot to 1 (true) +/// +/// This function should only be called once at the Curie transition block. +pub(crate) fn apply_curie_hard_fork(state: &mut State) -> Result<(), DB::Error> { + tracing::info!(target: "morph::evm", "Applying Curie hard fork"); + + let oracle = state.load_cache_account(L1_GAS_PRICE_ORACLE_ADDRESS)?; + + // Create storage updates + let new_storage = CURIE_L1_GAS_PRICE_ORACLE_STORAGE + .into_iter() + .map(|(slot, present_value)| { + ( + slot, + StorageSlot { + present_value, + previous_or_original_value: oracle.storage_slot(slot).unwrap_or_default(), + }, + ) + }) + .collect(); + + // Get existing account info or use default + let oracle_info = oracle.account_info().unwrap_or_default(); + + // Create transition for oracle storage update + let transition = oracle.change(oracle_info, new_storage); + + // Add transition to state + if let Some(s) = state.transition_state.as_mut() { + s.add_transitions(vec![(L1_GAS_PRICE_ORACLE_ADDRESS, transition)]); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use morph_revm::{ + GPO_BLOB_SCALAR_SLOT, GPO_COMMIT_SCALAR_SLOT, GPO_IS_CURIE_SLOT, GPO_L1_BASE_FEE_SLOT, + GPO_L1_BLOB_BASE_FEE_SLOT, GPO_OVERHEAD_SLOT, GPO_OWNER_SLOT, GPO_SCALAR_SLOT, + GPO_WHITELIST_SLOT, INITIAL_BLOB_SCALAR, INITIAL_COMMIT_SCALAR, INITIAL_L1_BLOB_BASE_FEE, + IS_CURIE, + }; + use revm::{ + database::{ + EmptyDB, State, + states::{StorageSlot, bundle_state::BundleRetention, plain_account::PlainStorage}, + }, + primitives::U256, + state::AccountInfo, + }; + + #[test] + fn test_apply_curie_fork() { + // init state + let db = EmptyDB::new(); + let mut state = State::builder() + .with_database(db) + .with_bundle_update() + .without_state_clear() + .build(); + + // oracle pre fork state + let oracle_pre_fork = AccountInfo::default(); + let oracle_storage_pre_fork = PlainStorage::from_iter([ + (GPO_OWNER_SLOT, U256::from(0x15f50e5eu64)), // placeholder owner + (GPO_L1_BASE_FEE_SLOT, U256::from(0x15f50e5eu64)), + (GPO_OVERHEAD_SLOT, U256::from(0x38u64)), + (GPO_SCALAR_SLOT, U256::from(0x3e95ba80u64)), + (GPO_WHITELIST_SLOT, U256::from(0x53u64)), // placeholder whitelist + ]); + state.insert_account_with_storage( + L1_GAS_PRICE_ORACLE_ADDRESS, + oracle_pre_fork, + oracle_storage_pre_fork, + ); + + // apply curie fork + apply_curie_hard_fork(&mut state).expect("should apply curie fork"); + + // merge transitions + state.merge_transitions(BundleRetention::Reverts); + let bundle = state.take_bundle(); + + // check oracle contract contains storage changeset + let oracle = bundle + .state + .get(&L1_GAS_PRICE_ORACLE_ADDRESS) + .unwrap() + .clone(); + let mut storage = oracle + .storage + .into_iter() + .collect::>(); + storage.sort_by(|(a, _), (b, _)| a.cmp(b)); + + let expected_storage = [ + (GPO_L1_BLOB_BASE_FEE_SLOT, INITIAL_L1_BLOB_BASE_FEE), + (GPO_COMMIT_SCALAR_SLOT, INITIAL_COMMIT_SCALAR), + (GPO_BLOB_SCALAR_SLOT, INITIAL_BLOB_SCALAR), + (GPO_IS_CURIE_SLOT, IS_CURIE), + ]; + + for (got, expected) in storage.into_iter().zip(expected_storage) { + assert_eq!(got.0, expected.0); + assert_eq!( + got.1, + StorageSlot { + present_value: expected.1, + ..Default::default() + } + ); + } + } +} diff --git a/crates/evm/src/block.rs b/crates/evm/src/block/mod.rs similarity index 56% rename from crates/evm/src/block.rs rename to crates/evm/src/block/mod.rs index c16a857..f6e6839 100644 --- a/crates/evm/src/block.rs +++ b/crates/evm/src/block/mod.rs @@ -1,59 +1,30 @@ +//! Block execution for Morph L2. +//! +//! This module provides block execution functionality for Morph, including: +//! - [`MorphBlockExecutor`]: The main block executor +//! - [`MorphReceiptBuilder`]: Receipt construction for transactions +//! - Hardfork application logic (Curie, etc.) + +pub(crate) mod curie; +mod receipt; + +pub(crate) use receipt::MorphReceiptBuilder; + use crate::{MorphBlockExecutionCtx, evm::MorphEvm}; -use alloy_consensus::Receipt; use alloy_evm::{ Database, Evm, block::{BlockExecutionError, BlockExecutionResult, BlockExecutor, ExecutableTx, OnStateHook}, - eth::{ - EthBlockExecutor, - receipt_builder::{ReceiptBuilder, ReceiptBuilderCtx}, - }, + eth::EthBlockExecutor, }; -use morph_chainspec::MorphChainSpec; -use morph_primitives::{MorphReceipt, MorphTransactionReceipt, MorphTxEnvelope, MorphTxType}; -use morph_revm::{MorphHaltReason, evm::MorphContext}; +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}; -/// Builder for [`MorphReceipt`]. -#[derive(Debug, Clone, Copy, Default)] -#[non_exhaustive] -pub(crate) struct MorphReceiptBuilder; - -impl ReceiptBuilder for MorphReceiptBuilder { - type Transaction = MorphTxEnvelope; - type Receipt = MorphReceipt; - - fn build_receipt( - &self, - ctx: ReceiptBuilderCtx<'_, Self::Transaction, E>, - ) -> Self::Receipt { - let ReceiptBuilderCtx { - tx, - result, - cumulative_gas_used, - .. - } = ctx; - - let inner = Receipt { - status: result.is_success().into(), - cumulative_gas_used, - logs: result.into_logs(), - }; - - // Create the appropriate receipt variant based on transaction type - // TODO: Add L1 fee calculation from execution context - 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::L1Msg => MorphReceipt::L1Msg(inner), - MorphTxType::AltFee => MorphReceipt::AltFee(MorphTransactionReceipt::new(inner)), - } - } -} - /// Block executor for Morph. Wraps an inner [`EthBlockExecutor`]. pub(crate) struct MorphBlockExecutor<'a, DB: Database, I> { + /// Inner Ethereum block executor. pub(crate) inner: EthBlockExecutor< 'a, MorphEvm<&'a mut State, I>, @@ -93,7 +64,33 @@ where type Evm = MorphEvm<&'a mut State, I>; fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> { - self.inner.apply_pre_execution_changes() + // 1. Apply base Ethereum pre-execution changes (state clear flag, EIP-2935, EIP-4788) + self.inner.apply_pre_execution_changes()?; + + // 2. Load L1 gas oracle contract into cache for Curie hardfork upgrade + let _ = self + .inner + .evm_mut() + .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()) + { + return Err(BlockExecutionError::msg(format!( + "error occurred at Curie fork: {err:?}" + ))); + } + + Ok(()) } fn execute_transaction_without_commit( diff --git a/crates/evm/src/block/receipt.rs b/crates/evm/src/block/receipt.rs new file mode 100644 index 0000000..c2bf1d3 --- /dev/null +++ b/crates/evm/src/block/receipt.rs @@ -0,0 +1,52 @@ +//! Receipt builder for Morph block execution. +//! +//! This module provides the [`MorphReceiptBuilder`] which constructs receipts +//! for executed transactions based on their type. + +use alloy_consensus::Receipt; +use alloy_evm::{ + Evm, + eth::receipt_builder::{ReceiptBuilder, ReceiptBuilderCtx}, +}; +use morph_primitives::{MorphReceipt, MorphTransactionReceipt, MorphTxEnvelope, MorphTxType}; + +/// Builder for [`MorphReceipt`]. +/// +/// Creates the appropriate receipt variant based on transaction type. +#[derive(Debug, Clone, Copy, Default)] +#[non_exhaustive] +pub(crate) struct MorphReceiptBuilder; + +impl ReceiptBuilder for MorphReceiptBuilder { + type Transaction = MorphTxEnvelope; + type Receipt = MorphReceipt; + + fn build_receipt( + &self, + ctx: ReceiptBuilderCtx<'_, Self::Transaction, E>, + ) -> Self::Receipt { + let ReceiptBuilderCtx { + tx, + result, + cumulative_gas_used, + .. + } = ctx; + + let inner = Receipt { + status: result.is_success().into(), + cumulative_gas_used, + logs: result.into_logs(), + }; + + // Create the appropriate receipt variant based on transaction type + // TODO: Add L1 fee calculation from execution context + 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::L1Msg => MorphReceipt::L1Msg(inner), + MorphTxType::AltFee => MorphReceipt::AltFee(MorphTransactionReceipt::new(inner)), + } + } +} diff --git a/crates/evm/src/config.rs b/crates/evm/src/config.rs index 1addf92..f26ed54 100644 --- a/crates/evm/src/config.rs +++ b/crates/evm/src/config.rs @@ -39,7 +39,9 @@ impl ConfigureEvm for MorphEvmConfig { .blob_params_at_timestamp(header.timestamp()), ); - let spec = self.chain_spec().morph_hardfork_at(header.timestamp()); + let spec = self + .chain_spec() + .morph_hardfork_at(header.number(), header.timestamp()); Ok(EvmEnv { cfg_env: cfg_env.with_spec(spec), @@ -69,7 +71,10 @@ impl ConfigureEvm for MorphEvmConfig { .blob_params_at_timestamp(attributes.timestamp), ); - let spec = self.chain_spec().morph_hardfork_at(attributes.timestamp); + // Next block number is parent + 1 + let spec = self + .chain_spec() + .morph_hardfork_at(parent.number() + 1, attributes.timestamp); Ok(EvmEnv { cfg_env: cfg_env.with_spec(spec), diff --git a/crates/evm/src/context.rs b/crates/evm/src/context.rs index 70584df..534eefa 100644 --- a/crates/evm/src/context.rs +++ b/crates/evm/src/context.rs @@ -1,5 +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)] @@ -21,7 +23,7 @@ pub struct MorphNextBlockEnvAttributes { impl reth_rpc_eth_api::helpers::pending_block::BuildPendingEnv for MorphNextBlockEnvAttributes { - fn build_pending_env(parent: &crate::SealedHeader) -> Self { + fn build_pending_env(parent: &SealedHeader) -> Self { Self { inner: NextBlockEnvAttributes::build_pending_env(parent), } diff --git a/crates/evm/src/engine.rs b/crates/evm/src/engine.rs deleted file mode 100644 index 0e7bde4..0000000 --- a/crates/evm/src/engine.rs +++ /dev/null @@ -1,76 +0,0 @@ -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; - -impl ConfigureEngineEvm for MorphEvmConfig { - fn evm_env_for_payload( - &self, - payload: &MorphExecutionData, - ) -> Result, Self::Error> { - self.evm_env(&payload.block) - } - - fn context_for_payload<'a>( - &self, - payload: &'a MorphExecutionData, - ) -> Result, Self::Error> { - self.context_for_block(&payload.block) - } - - fn tx_iterator_for_payload( - &self, - payload: &MorphExecutionData, - ) -> Result, Self::Error> { - let block = payload.block.clone(); - let transactions = - (0..payload.block.body().transactions.len()).map(move |i| (block.clone(), i)); - - Ok((transactions, RecoveredInBlock::new)) - } -} - -/// A [`reth_evm::execute::ExecutableTxFor`] implementation that contains a pointer to the -/// block and the transaction index, allowing to prepare a [`MorphTxEnv`] without having to -/// clone block or transaction. -#[derive(Clone)] -struct RecoveredInBlock { - block: Arc>, - index: usize, - sender: Address, -} - -impl RecoveredInBlock { - fn new((block, index): (Arc>, usize)) -> Result { - let sender = block.body().transactions[index].try_recover()?; - Ok(Self { - block, - index, - sender, - }) - } -} - -impl RecoveredTx for RecoveredInBlock { - fn tx(&self) -> &MorphTxEnvelope { - &self.block.body().transactions[self.index] - } - - fn signer(&self) -> &alloy_primitives::Address { - &self.sender - } -} - -impl ToTxEnv for RecoveredInBlock { - fn to_tx_env(&self) -> MorphTxEnv { - MorphTxEnv::from_recovered_tx(self.tx(), *self.signer()) - } -} diff --git a/crates/evm/src/evm.rs b/crates/evm/src/evm.rs index af467a5..4a873ef 100644 --- a/crates/evm/src/evm.rs +++ b/crates/evm/src/evm.rs @@ -1,5 +1,6 @@ use alloy_evm::{ Database, Evm, EvmEnv, EvmFactory, + precompiles::PrecompilesMap, revm::{ Context, ExecuteEvm, InspectEvm, Inspector, SystemCallEvm, context::result::{EVMError, ResultAndState}, @@ -29,7 +30,7 @@ impl EvmFactory for MorphEvmFactory { type HaltReason = MorphHaltReason; type Spec = MorphHardfork; type BlockEnv = MorphBlockEnv; - type Precompiles = MorphPrecompiles; + type Precompiles = PrecompilesMap; fn create_evm( &self, @@ -57,20 +58,28 @@ impl EvmFactory for MorphEvmFactory { #[expect(missing_debug_implementations)] pub struct MorphEvm { inner: morph_revm::MorphEvm, + /// Precompiles wrapped as PrecompilesMap for reth compatibility + precompiles_map: PrecompilesMap, inspect: bool, } impl MorphEvm { /// Create a new [`MorphEvm`] instance. pub fn new(db: DB, input: EvmEnv) -> Self { + let spec = input.cfg_env.spec; let ctx = Context::mainnet() .with_db(db) .with_block(input.block_env) .with_cfg(input.cfg_env) .with_tx(Default::default()); + // Create precompiles for the hardfork and wrap in PrecompilesMap + let morph_precompiles = MorphPrecompiles::new_with_spec(spec); + let precompiles_map = PrecompilesMap::from_static(morph_precompiles.precompiles()); + Self { inner: morph_revm::MorphEvm::new(ctx, NoOpInspector {}), + precompiles_map, inspect: false, } } @@ -91,6 +100,7 @@ impl MorphEvm { pub fn with_inspector(self, inspector: OINSP) -> MorphEvm { MorphEvm { inner: self.inner.with_inspector(inspector), + precompiles_map: self.precompiles_map, inspect: true, } } @@ -143,7 +153,7 @@ where type HaltReason = MorphHaltReason; type Spec = MorphHardfork; type BlockEnv = MorphBlockEnv; - type Precompiles = MorphPrecompiles; + type Precompiles = PrecompilesMap; type Inspector = I; fn block(&self) -> &Self::BlockEnv { @@ -193,7 +203,7 @@ where ( &self.inner.inner.ctx.journaled_state.database, &self.inner.inner.inspector, - &self.inner.inner.precompiles, + &self.precompiles_map, ) } @@ -201,7 +211,7 @@ where ( &mut self.inner.inner.ctx.journaled_state.database, &mut self.inner.inner.inspector, - &mut self.inner.inner.precompiles, + &mut self.precompiles_map, ) } } diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index b945949..f554657 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -37,11 +37,10 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg))] -mod assemble; #[cfg(feature = "reth-codec")] mod config; -#[cfg(feature = "engine")] -mod engine; + +mod assemble; pub use assemble::MorphBlockAssembler; mod block; mod context; @@ -52,15 +51,16 @@ pub use error::MorphEvmError; pub mod evm; use std::sync::Arc; -use crate::{block::MorphBlockExecutor, evm::MorphEvm}; use alloy_evm::{ - self, Database, + Database, block::{BlockExecutorFactory, BlockExecutorFor}, revm::{Inspector, database::State}, }; pub use evm::MorphEvmFactory; -use morph_chainspec::MorphChainSpec; use morph_primitives::{MorphReceipt, MorphTxEnvelope}; + +use crate::{block::MorphBlockExecutor, evm::MorphEvm}; +use morph_chainspec::MorphChainSpec; use morph_revm::evm::MorphContext; use reth_evm_ethereum::EthEvmConfig; @@ -125,14 +125,13 @@ impl BlockExecutorFactory for MorphEvmConfig { } } -#[cfg(feature = "reth-codec")] #[cfg(test)] mod tests { use super::*; use morph_chainspec::hardfork::{MorphHardfork, MorphHardforks}; use serde_json::json; - /// Helper function to create a test genesis with Morph hardforks at genesis + /// Helper function to create a test genesis with Morph hardforks at timestamp 0 fn create_test_genesis() -> alloy_genesis::Genesis { let genesis_json = json!({ "config": { @@ -155,7 +154,8 @@ mod tests { "bernoulliBlock": 0, "curieBlock": 0, "morph203Time": 0, - "viridianTime": 0 + "viridianTime": 0, + "morph": {} }, "alloc": {} }); @@ -164,20 +164,24 @@ mod tests { #[test] fn test_evm_config_can_query_morph_hardforks() { - // Create a test chainspec with Bernoulli at genesis + // Create a test chainspec with Morph203 at genesis let chainspec = Arc::new(morph_chainspec::MorphChainSpec::from(create_test_genesis())); let evm_config = MorphEvmConfig::new_with_default_factory(chainspec); // Should be able to query Morph hardforks through the chainspec - // Note: Bernoulli and Curie use block-based activation - assert!(evm_config.chain_spec().is_bernoulli_active_at_block(0)); - assert!(evm_config.chain_spec().is_bernoulli_active_at_block(1000)); + assert!(evm_config.chain_spec().is_morph203_active_at_timestamp(0)); + assert!( + evm_config + .chain_spec() + .is_morph203_active_at_timestamp(1000) + ); // Should be able to query activation condition let activation = evm_config .chain_spec() - .morph_fork_activation(MorphHardfork::Bernoulli); - assert_eq!(activation, reth_chainspec::ForkCondition::Block(0)); + .morph_fork_activation(MorphHardfork::Morph203); + // Morph203 is configured at timestamp 0, so it should be active at timestamp 0 + assert!(activation.active_at_timestamp(0)); } } diff --git a/crates/payload/builder/Cargo.toml b/crates/payload/builder/Cargo.toml index c6e5876..44d9e25 100644 --- a/crates/payload/builder/Cargo.toml +++ b/crates/payload/builder/Cargo.toml @@ -12,10 +12,38 @@ publish.workspace = true workspace = true [dependencies] -# Dependencies will be added when implementing the builder +# Morph +morph-chainspec.workspace = true +morph-evm = { workspace = true, features = ["reth-codec"] } +morph-payload-types.workspace = true +morph-primitives.workspace = true + +# Reth +reth-basic-payload-builder.workspace = true +reth-chainspec.workspace = true +reth-evm.workspace = true +reth-payload-builder.workspace = true +reth-payload-primitives.workspace = true +reth-payload-util.workspace = true +reth-primitives-traits.workspace = true +reth-revm.workspace = true +reth-storage-api.workspace = true +reth-transaction-pool.workspace = true + +# Alloy +alloy-consensus.workspace = true +alloy-eips.workspace = true +alloy-primitives.workspace = true +alloy-rlp.workspace = true + +# Revm +revm.workspace = true + +# Utils +tracing.workspace = true +thiserror.workspace = true [dev-dependencies] [features] default = [] - diff --git a/crates/payload/builder/src/builder.rs b/crates/payload/builder/src/builder.rs new file mode 100644 index 0000000..8312e68 --- /dev/null +++ b/crates/payload/builder/src/builder.rs @@ -0,0 +1,707 @@ +//! Morph payload builder implementation. + +use crate::{MorphBuilderConfig, MorphPayloadBuilderError, config::PayloadBuildingBreaker}; +use alloy_consensus::{BlockHeader, Transaction, Typed2718}; +use alloy_eips::eip2718::Encodable2718; +use alloy_primitives::{Address, B256, Bytes, U256, address}; +use alloy_rlp::Encodable; +use morph_chainspec::MorphChainSpec; +use morph_evm::{MorphEvmConfig, MorphNextBlockEnvAttributes}; +use morph_payload_types::{ExecutableL2Data, MorphBuiltPayload, MorphPayloadBuilderAttributes}; +use morph_primitives::{MorphHeader, MorphTxEnvelope}; +use reth_basic_payload_builder::{ + BuildArguments, BuildOutcome, BuildOutcomeKind, MissingPayloadBehaviour, PayloadBuilder, + PayloadConfig, is_better_payload, +}; +use reth_chainspec::ChainSpecProvider; +use reth_evm::{ + ConfigureEvm, Database, Evm, NextBlockEnvAttributes, + block::{BlockExecutionError, BlockValidationError}, + execute::{BlockBuilder, BlockBuilderOutcome}, +}; +use reth_payload_builder::PayloadId; +use reth_payload_primitives::{PayloadBuilderAttributes, PayloadBuilderError}; +use reth_payload_util::{BestPayloadTransactions, NoopPayloadTransactions, PayloadTransactions}; +use reth_primitives_traits::{RecoveredBlock, SealedHeader}; +use reth_revm::{database::StateProviderDatabase, db::State}; +use reth_storage_api::{StateProvider, StateProviderFactory}; +use reth_transaction_pool::{BestTransactionsAttributes, PoolTransaction, TransactionPool}; +use revm::context_interface::Block as RevmBlock; +use std::sync::Arc; + +// ============================================================================= +// L2 Message Queue Constants +// ============================================================================= + +/// L2 Message Queue contract address. +/// +/// Manages the L1-to-L2 message queue and stores the withdraw trie root. +const L2_MESSAGE_QUEUE_ADDRESS: Address = address!("5300000000000000000000000000000000000001"); + +/// Storage slot for the withdraw trie root (`messageRoot`) in L2MessageQueue contract. +/// This is slot 33, which stores the Merkle root for L2→L1 messages. +const L2_MESSAGE_QUEUE_WITHDRAW_TRIE_ROOT_SLOT: U256 = U256::from_limbs([33, 0, 0, 0]); + +/// Reads the withdraw trie root from the L2MessageQueue contract storage. +fn read_withdraw_trie_root(db: &mut DB) -> Result { + let value = db.storage( + L2_MESSAGE_QUEUE_ADDRESS, + L2_MESSAGE_QUEUE_WITHDRAW_TRIE_ROOT_SLOT, + )?; + Ok(B256::from(value)) +} + +// ============================================================================= +// Payload Transactions +// ============================================================================= + +/// A type that returns the [`PayloadTransactions`] that should be included in the pool. +pub trait MorphPayloadTransactions: Clone + Send + Sync + Unpin + 'static { + /// Returns an iterator that yields the transactions in the order they should get included in + /// the new payload. + fn best_transactions>( + &self, + pool: Pool, + attr: BestTransactionsAttributes, + ) -> impl PayloadTransactions; +} + +impl MorphPayloadTransactions for () { + fn best_transactions>( + &self, + pool: Pool, + attr: BestTransactionsAttributes, + ) -> impl PayloadTransactions { + BestPayloadTransactions::new(pool.best_transactions_with_attributes(attr)) + } +} + +/// Morph's payload builder. +/// +/// Builds L2 blocks by executing: +/// 1. Forced transactions from payload attributes (L1 messages first) +/// 2. Pool transactions (if allowed) +#[derive(Clone, Debug)] +pub struct MorphPayloadBuilder { + /// The EVM configuration. + pub evm_config: MorphEvmConfig, + /// Transaction pool. + pub pool: Pool, + /// Node client for state access. + pub client: Client, + /// The type responsible for yielding the best transactions to include. + pub best_transactions: Txs, + /// Builder configuration. + pub config: MorphBuilderConfig, +} + +impl MorphPayloadBuilder { + /// Creates a new [`MorphPayloadBuilder`] with default configuration. + pub fn new(pool: Pool, evm_config: MorphEvmConfig, client: Client) -> Self { + Self { + evm_config, + pool, + client, + best_transactions: (), + config: MorphBuilderConfig::default(), + } + } + + /// Creates a new [`MorphPayloadBuilder`] with the specified configuration. + pub const fn with_config( + pool: Pool, + evm_config: MorphEvmConfig, + client: Client, + config: MorphBuilderConfig, + ) -> Self { + Self { + evm_config, + pool, + client, + best_transactions: (), + config, + } + } +} + +impl MorphPayloadBuilder { + /// Configures the type responsible for yielding the transactions that should be included in + /// the payload. + pub fn with_transactions(self, best_transactions: T) -> MorphPayloadBuilder + where + T: MorphPayloadTransactions, + Pool: TransactionPool, + { + let Self { + evm_config, + pool, + client, + config, + .. + } = self; + MorphPayloadBuilder { + evm_config, + pool, + client, + best_transactions, + config, + } + } + + /// Sets the builder configuration. + pub fn set_config(mut self, config: MorphBuilderConfig) -> Self { + self.config = config; + self + } +} + +impl MorphPayloadBuilder +where + Pool: TransactionPool>, + Client: StateProviderFactory + ChainSpecProvider, +{ + /// Constructs a Morph payload from the transactions sent via the payload attributes. + fn build_payload<'a, BestTxs>( + &self, + args: BuildArguments, + best: impl FnOnce(BestTransactionsAttributes) -> BestTxs + Send + Sync + 'a, + ) -> Result, PayloadBuilderError> + where + BestTxs: + PayloadTransactions> + 'a, + { + let BuildArguments { + mut cached_reads, + config, + cancel, + best_payload, + } = args; + + let ctx = MorphPayloadBuilderCtx { + evm_config: self.evm_config.clone(), + config, + cancel, + best_payload, + builder_config: self.config.clone(), + }; + + let state_provider = self.client.state_by_block_hash(ctx.parent().hash())?; + let state = StateProviderDatabase::new(&state_provider); + + // Reuse cached reads from previous runs for incremental payload building + build_payload_inner(cached_reads.as_db_mut(state), &state_provider, ctx, best) + .map(|out| out.with_cached_reads(cached_reads)) + } +} + +/// Implementation of the [`PayloadBuilder`] trait for [`MorphPayloadBuilder`]. +impl PayloadBuilder for MorphPayloadBuilder +where + Pool: TransactionPool> + Clone, + Client: StateProviderFactory + ChainSpecProvider + Clone, + Txs: MorphPayloadTransactions, +{ + type Attributes = MorphPayloadBuilderAttributes; + type BuiltPayload = MorphBuiltPayload; + + fn try_build( + &self, + args: BuildArguments, + ) -> Result, PayloadBuilderError> { + let pool = self.pool.clone(); + self.build_payload(args, |attrs| { + self.best_transactions.best_transactions(pool, attrs) + }) + } + + fn on_missing_payload( + &self, + _args: BuildArguments, + ) -> MissingPayloadBehaviour { + // Wait for the job that's already in progress + MissingPayloadBehaviour::AwaitInProgress + } + + fn build_empty_payload( + &self, + config: PayloadConfig, + ) -> Result { + let args = BuildArguments { + config, + cached_reads: Default::default(), + cancel: Default::default(), + best_payload: None, + }; + self.build_payload(args, |_| { + NoopPayloadTransactions::::default() + })? + .into_payload() + .ok_or(PayloadBuilderError::MissingPayload) + } +} + +/// Container type that holds all necessities to build a new payload. +#[derive(Debug)] +struct MorphPayloadBuilderCtx { + /// The EVM configuration. + evm_config: MorphEvmConfig, + /// Payload configuration. + config: PayloadConfig, + /// Marker to check whether the job has been cancelled. + cancel: reth_revm::cancelled::CancelOnDrop, + /// The currently best payload. + best_payload: Option, + /// Builder configuration with limits. + builder_config: MorphBuilderConfig, +} + +impl MorphPayloadBuilderCtx { + /// Returns the parent block the payload will be built on. + fn parent(&self) -> &SealedHeader { + &self.config.parent_header + } + + /// Returns the builder attributes. + fn attributes(&self) -> &MorphPayloadBuilderAttributes { + &self.config.attributes + } + + /// Returns the unique ID for this payload job. + fn payload_id(&self) -> PayloadId { + self.attributes().payload_id() + } + + /// Returns true if the fees are higher than the previous payload. + fn is_better_payload(&self, total_fees: U256) -> bool { + is_better_payload(self.best_payload.as_ref(), total_fees) + } + + /// Returns the current fee settings for transactions from the mempool. + fn best_transaction_attributes(&self, base_fee: u64) -> BestTransactionsAttributes { + BestTransactionsAttributes::new(base_fee, None) + } + + /// Executes all sequencer transactions that are included in the payload attributes. + /// + /// These transactions are forced and come from the sequencer (L1 messages first). + /// Returns the executed transaction bytes for inclusion in ExecutableL2Data. + fn execute_sequencer_transactions( + &self, + builder: &mut impl BlockBuilder, + info: &mut ExecutionInfo, + ) -> Result, PayloadBuilderError> { + let block_gas_limit = builder.evm().block().gas_limit(); + let base_fee = builder.evm().block().basefee(); + let mut executed_txs: Vec = Vec::new(); + // Track gas spent by each transaction for error reporting + let mut gas_spent_by_transactions: Vec = Vec::new(); + + for tx_with_encoded in &self.attributes().transactions { + // The transaction is already recovered in `try_new` via `try_into_recovered()`. + // For L1 message transactions (which have no signature), this extracts + // the `from` address directly from the transaction. + let recovered_tx = tx_with_encoded.value(); + let tx_bytes = tx_with_encoded.encoded_bytes(); + + // Blob transactions are not supported on L2 + if recovered_tx.is_eip4844() { + return Err(PayloadBuilderError::other( + MorphPayloadBuilderError::BlobTransactionRejected, + )); + } + + let tx_gas = recovered_tx.gas_limit(); + + // Check if adding this transaction would exceed block gas limit + if info.cumulative_gas_used + tx_gas > block_gas_limit { + gas_spent_by_transactions.push(tx_gas); + return Err(PayloadBuilderError::other( + MorphPayloadBuilderError::BlockGasLimitExceededBySequencerTransactions { + gas_spent_by_tx: gas_spent_by_transactions, + gas: block_gas_limit, + }, + )); + } + + // Execute the transaction + let gas_used = match builder.execute_transaction(recovered_tx.clone()) { + Ok(gas_used) => gas_used, + Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx { + error, + .. + })) => { + // For sequencer transactions, we log and skip invalid transactions + // but don't fail the entire block build + tracing::trace!( + target: "payload_builder", + %error, + ?recovered_tx, + "Error in sequencer transaction, skipping." + ); + continue; + } + Err(BlockExecutionError::Validation(err)) => { + tracing::trace!( + target: "payload_builder", + %err, + ?recovered_tx, + "Validation error in sequencer transaction, skipping." + ); + continue; + } + Err(err) => { + // Fatal error - this is a bug or misconfiguration + return Err(PayloadBuilderError::EvmExecutionError(Box::new(err))); + } + }; + + // For L1 messages, use full gas limit (no refund) and no fees collected. + // L1 gas is prepaid on L1, so unused gas is not refunded. + // Also track the next L1 message index. + let gas_used = if recovered_tx.is_l1_msg() { + // Update next_l1_message_index to be queue_index + 1 + if let Some(queue_index) = recovered_tx.queue_index() { + info.next_l1_message_index = queue_index + 1; + } + recovered_tx.gas_limit() + } else { + // Calculate fees for L2 transactions: effective_tip * gas_used + let effective_tip = recovered_tx + .effective_tip_per_gas(base_fee) + .unwrap_or_default(); + info.total_fees += U256::from(effective_tip) * U256::from(gas_used); + gas_used + }; + + info.cumulative_gas_used += gas_used; + gas_spent_by_transactions.push(gas_used); + + // Store the original transaction bytes for ExecutableL2Data + executed_txs.push(tx_bytes.clone()); + } + + Ok(executed_txs) + } + + /// Executes the best transactions from the mempool. + /// + /// Returns `Ok(Some(()))` if the job was cancelled or breaker triggered, `Ok(None)` otherwise. + /// Executed transaction bytes are appended to the provided vector. + fn execute_pool_transactions( + &self, + builder: &mut impl BlockBuilder, + info: &mut ExecutionInfo, + executed_txs: &mut Vec, + mut best_txs: BestTxs, + breaker: &PayloadBuildingBreaker, + ) -> Result, PayloadBuilderError> + where + BestTxs: PayloadTransactions>, + { + let block_gas_limit = builder.evm().block().gas_limit(); + let base_fee = builder.evm().block().basefee(); + + while let Some(tx) = best_txs.next(()) { + // Check if the job was cancelled + if self.cancel.is_cancelled() { + return Ok(Some(())); + } + + // Check if the breaker triggers (time/gas/DA limits) + if breaker.should_break(info.cumulative_gas_used, info.cumulative_da_bytes_used) { + tracing::debug!( + target: "payload_builder", + cumulative_gas_used = info.cumulative_gas_used, + cumulative_da_bytes_used = info.cumulative_da_bytes_used, + elapsed = ?breaker.elapsed(), + "breaker triggered, stopping pool transaction execution" + ); + return Ok(Some(())); + } + + let tx = tx.into_consensus(); + + // Skip blob transactions and L1 messages from pool + if tx.is_eip4844() || tx.is_l1_msg() { + best_txs.mark_invalid(tx.signer(), tx.nonce()); + continue; + } + + // Check if the transaction exceeds block limits + if info.is_tx_over_limits( + tx.gas_limit(), + tx.length() as u64, + block_gas_limit, + self.builder_config.max_da_block_size, + ) { + best_txs.mark_invalid(tx.signer(), tx.nonce()); + continue; + } + + // Execute the transaction + let gas_used = match builder.execute_transaction(tx.clone()) { + Ok(gas_used) => gas_used, + Err(BlockExecutionError::Validation(BlockValidationError::InvalidTx { + error, + .. + })) => { + if error.is_nonce_too_low() { + // If the nonce is too low, we can skip this transaction + // but don't mark as invalid - the sender may have other valid txs + tracing::trace!( + target: "payload_builder", + %error, + ?tx, + "skipping nonce too low transaction" + ); + } else { + // If the transaction is invalid for other reasons, + // skip it and all of its descendants from this sender + tracing::trace!( + target: "payload_builder", + %error, + ?tx, + "skipping invalid transaction and its descendants" + ); + best_txs.mark_invalid(tx.signer(), tx.nonce()); + } + continue; + } + Err(BlockExecutionError::Validation(err)) => { + // Other validation errors - skip transaction and descendants + tracing::trace!( + target: "payload_builder", + %err, + ?tx, + "validation error in pool transaction, skipping" + ); + best_txs.mark_invalid(tx.signer(), tx.nonce()); + continue; + } + Err(err) => { + // Fatal error - should not continue + return Err(PayloadBuilderError::EvmExecutionError(Box::new(err))); + } + }; + + // Update execution info + info.cumulative_gas_used += gas_used; + info.cumulative_da_bytes_used += tx.length() as u64; + + // Calculate fees: effective_tip * gas_used + let effective_tip = tx.effective_tip_per_gas(base_fee).unwrap_or_default(); + info.total_fees += U256::from(effective_tip) * U256::from(gas_used); + + // Store the transaction bytes for ExecutableL2Data + let mut tx_bytes = Vec::new(); + tx.encode_2718(&mut tx_bytes); + executed_txs.push(Bytes::from(tx_bytes)); + } + + Ok(None) + } +} + +/// Execution information collected during payload building. +#[derive(Debug, Default)] +struct ExecutionInfo { + /// Cumulative gas used by all executed transactions. + cumulative_gas_used: u64, + /// Cumulative DA bytes used (for L2 data availability). + cumulative_da_bytes_used: u64, + /// Total fees collected from executed transactions. + total_fees: U256, + /// Next L1 message queue index. + next_l1_message_index: u64, +} + +impl ExecutionInfo { + /// Creates a new [`ExecutionInfo`] with the initial next L1 message index from parent. + const fn new(next_l1_message_index: u64) -> Self { + Self { + cumulative_gas_used: 0, + cumulative_da_bytes_used: 0, + total_fees: U256::ZERO, + next_l1_message_index, + } + } + + /// Returns true if the transaction would exceed the block limits. + fn is_tx_over_limits( + &self, + tx_gas_limit: u64, + tx_size: u64, + block_gas_limit: u64, + block_da_limit: Option, + ) -> bool { + // Check DA limit if configured + if block_da_limit.is_some_and(|da_limit| self.cumulative_da_bytes_used + tx_size > da_limit) + { + return true; + } + + // Check gas limit + self.cumulative_gas_used + tx_gas_limit > block_gas_limit + } +} + +/// Builds the payload on top of the state. +fn build_payload_inner<'a, DB, BestTxs>( + db: DB, + state_provider: &impl StateProvider, + ctx: MorphPayloadBuilderCtx, + best: impl FnOnce(BestTransactionsAttributes) -> BestTxs + Send + Sync + 'a, +) -> Result, PayloadBuilderError> +where + DB: Database, + BestTxs: PayloadTransactions> + 'a, +{ + let attributes = ctx.attributes(); + + tracing::debug!( + target: "payload_builder", + id = %ctx.payload_id(), + parent_hash = ?ctx.parent().hash(), + parent_number = ctx.parent().number(), + "building new payload" + ); + + let mut db = State::builder() + .with_database(db) + .with_bundle_update() + .build(); + + // Build next block env attributes + let next_block_attrs = MorphNextBlockEnvAttributes { + inner: NextBlockEnvAttributes { + timestamp: attributes.inner.timestamp, + suggested_fee_recipient: attributes.inner.suggested_fee_recipient, + prev_randao: attributes.inner.prev_randao, + gas_limit: ctx.parent().gas_limit(), + withdrawals: Some(attributes.inner.withdrawals.clone()), + parent_beacon_block_root: attributes.inner.parent_beacon_block_root, + extra_data: Default::default(), + }, + }; + + // Create block builder + let mut builder = ctx + .evm_config + .builder_for_next_block(&mut db, ctx.parent(), next_block_attrs) + .map_err(PayloadBuilderError::other)?; + + // 1. Apply pre-execution changes (system contracts, etc.) + builder.apply_pre_execution_changes().map_err(|err| { + tracing::warn!(target: "payload_builder", %err, "failed to apply pre-execution changes"); + PayloadBuilderError::Internal(err.into()) + })?; + + // Initialize next_l1_message_index from parent header + let mut info = ExecutionInfo::new(ctx.parent().next_l1_msg_index); + let base_fee = builder.evm().block().basefee(); + let block_gas_limit = builder.evm().block().gas_limit(); + + // Create breaker for early exit from pool transaction execution + let breaker = ctx.builder_config.breaker(block_gas_limit); + + // Execute sequencer transactions (L1 messages and forced transactions) + let mut executed_txs = ctx.execute_sequencer_transactions(&mut builder, &mut info)?; + + // Execute pool transactions (best transactions from mempool) + let best_txs = best(ctx.best_transaction_attributes(base_fee)); + if ctx + .execute_pool_transactions( + &mut builder, + &mut info, + &mut executed_txs, + best_txs, + &breaker, + )? + .is_some() + { + // Check if it was a cancellation or just breaker triggered + if ctx.cancel.is_cancelled() { + return Ok(BuildOutcomeKind::Cancelled); + } + // Breaker triggered - continue with current transactions + tracing::debug!( + target: "payload_builder", + elapsed = ?breaker.elapsed(), + cumulative_gas_used = info.cumulative_gas_used, + cumulative_da_bytes_used = info.cumulative_da_bytes_used, + tx_count = executed_txs.len(), + "breaker stopped pool execution, finalizing payload" + ); + } + + // Check if this payload is better than the previous one + if !ctx.is_better_payload(info.total_fees) { + return Ok(BuildOutcomeKind::Aborted { + fees: info.total_fees, + }); + } + + // Read withdraw_trie_root from L2MessageQueue contract storage + // This must be done before finish() consumes the builder + let withdraw_trie_root = read_withdraw_trie_root(builder.evm_mut().db_mut()) + .map_err(|err| PayloadBuilderError::other(MorphPayloadBuilderError::Database(err)))?; + + // 6. Finish building the block + let BlockBuilderOutcome { + execution_result, + mut block, + .. + } = builder.finish(state_provider)?; + + // Update MorphHeader with next_l1_msg_index. + // Since hash_slow() only hashes the inner header, we can update the + // MorphHeader's L2-specific fields without changing the block hash. + let (mut morph_block, senders) = block.split(); + morph_block = morph_block.map_header(|mut header: MorphHeader| { + header.next_l1_msg_index = info.next_l1_message_index; + // batch_hash remains B256::ZERO - it will be set by the batch submitter + header + }); + block = RecoveredBlock::new_unhashed(morph_block, senders); + + // Get the sealed block from the recovered block + let sealed_block = Arc::new(block.sealed_block().clone()); + let header = sealed_block.header(); + + tracing::debug!( + target: "payload_builder", + id = %ctx.payload_id(), + sealed_block_header = ?header, + "sealed built block" + ); + + // Build ExecutableL2Data from the sealed block + let mut logs_bloom_bytes = Vec::new(); + header.logs_bloom().encode(&mut logs_bloom_bytes); + + let executable_data = ExecutableL2Data { + parent_hash: header.parent_hash(), + miner: header.beneficiary(), + number: header.number(), + gas_limit: header.gas_limit(), + base_fee_per_gas: header.base_fee_per_gas().map(|f| f as u128), + timestamp: header.timestamp(), + transactions: executed_txs, + state_root: header.state_root(), + gas_used: execution_result.gas_used, + receipts_root: header.receipts_root(), + logs_bloom: Bytes::from(logs_bloom_bytes), + withdraw_trie_root, + next_l1_message_index: info.next_l1_message_index, + hash: sealed_block.hash(), + }; + + let payload = MorphBuiltPayload::new( + ctx.payload_id(), + sealed_block, + info.total_fees, + executable_data, + ); + + Ok(BuildOutcomeKind::Better { payload }) +} diff --git a/crates/payload/builder/src/config.rs b/crates/payload/builder/src/config.rs new file mode 100644 index 0000000..9260173 --- /dev/null +++ b/crates/payload/builder/src/config.rs @@ -0,0 +1,284 @@ +//! Configuration for the Morph payload builder. + +use core::time::Duration; +use reth_chainspec::MIN_TRANSACTION_GAS; +use std::{fmt::Debug, time::Instant}; + +/// Minimal data bytes size per transaction. +/// This is a conservative estimate for the minimum encoded transaction size. +pub(crate) const MIN_TRANSACTION_DATA_SIZE: u64 = 115; + +/// Settings for the Morph payload builder. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MorphBuilderConfig { + /// Optional gas limit for payload building. + /// + /// This is a soft limit that can be lower than the block gas limit. + /// When set, the builder will stop adding transactions once this limit is approached. + /// This allows operators to: + /// - Reserve gas for specific transactions + /// - Reduce payload building time + /// - Control block utilization + /// + /// If `None`, the block gas limit from the EVM environment is used. + pub gas_limit: Option, + + /// Time limit for payload building. + /// + /// The builder will stop adding pool transactions and return the current payload + /// once this duration has elapsed since the start of building. + /// This ensures timely block production even with large mempools. + pub time_limit: Duration, + + /// Maximum total data availability size for a block. + /// + /// L2 transactions need to be published to L1 for data availability. + /// This limit controls the maximum size of transaction data in a single block. + /// If `None`, no DA limit is enforced. + pub max_da_block_size: Option, +} + +impl Default for MorphBuilderConfig { + fn default() -> Self { + Self { + gas_limit: None, + // Default to 1 second - leaves time for consensus + time_limit: Duration::from_secs(1), + // No DA limit by default + max_da_block_size: None, + } + } +} + +impl MorphBuilderConfig { + /// Creates a new [`MorphBuilderConfig`] with the specified parameters. + pub const fn new( + gas_limit: Option, + time_limit: Duration, + max_da_block_size: Option, + ) -> Self { + Self { + gas_limit, + time_limit, + max_da_block_size, + } + } + + /// Sets the gas limit. + pub const fn with_gas_limit(mut self, gas_limit: u64) -> Self { + self.gas_limit = Some(gas_limit); + self + } + + /// Sets the time limit. + pub const fn with_time_limit(mut self, time_limit: Duration) -> Self { + self.time_limit = time_limit; + self + } + + /// Sets the maximum DA block size. + pub const fn with_max_da_block_size(mut self, max_da_block_size: u64) -> Self { + self.max_da_block_size = Some(max_da_block_size); + self + } + + /// Creates a [`PayloadBuildingBreaker`] for this configuration. + pub(crate) fn breaker(&self, block_gas_limit: u64) -> PayloadBuildingBreaker { + // Use configured gas limit or fall back to block gas limit + let effective_gas_limit = self.gas_limit.unwrap_or(block_gas_limit); + PayloadBuildingBreaker::new(self.time_limit, effective_gas_limit, self.max_da_block_size) + } +} + +/// Used in the payload builder to exit the transactions execution loop early. +/// +/// The breaker checks three conditions: +/// 1. Time limit - stop if building takes too long +/// 2. Gas limit - stop if remaining gas is insufficient for any transaction +/// 3. DA limit - stop if data availability size limit is reached +#[derive(Debug, Clone)] +pub struct PayloadBuildingBreaker { + /// When the payload building started. + start: Instant, + /// Maximum time allowed for building. + time_limit: Duration, + /// Gas limit for the payload. + gas_limit: u64, + /// Maximum DA block size. + max_da_block_size: Option, +} + +impl PayloadBuildingBreaker { + /// Creates a new [`PayloadBuildingBreaker`]. + fn new(time_limit: Duration, gas_limit: u64, max_da_block_size: Option) -> Self { + Self { + start: Instant::now(), + time_limit, + gas_limit, + max_da_block_size, + } + } + + /// Returns whether the payload building should stop. + /// + /// Returns `true` if any of the following conditions are met: + /// - Time limit has been exceeded + /// - Gas limit has been reached (leaving room for at least one minimal transaction) + /// - DA size limit has been reached (leaving room for at least one minimal transaction) + pub fn should_break(&self, cumulative_gas_used: u64, cumulative_da_size_used: u64) -> bool { + // Check time limit + if self.start.elapsed() >= self.time_limit { + tracing::trace!( + target: "payload_builder", + elapsed = ?self.start.elapsed(), + time_limit = ?self.time_limit, + "time limit reached" + ); + return true; + } + + // Check gas limit - stop if remaining gas can't fit even the smallest transaction + if cumulative_gas_used > self.gas_limit.saturating_sub(MIN_TRANSACTION_GAS) { + tracing::trace!( + target: "payload_builder", + cumulative_gas_used, + gas_limit = self.gas_limit, + "gas limit reached" + ); + return true; + } + + // Check DA size limit if configured + if let Some(max_size) = self.max_da_block_size + && cumulative_da_size_used > max_size.saturating_sub(MIN_TRANSACTION_DATA_SIZE) + { + tracing::trace!( + target: "payload_builder", + cumulative_da_size_used, + max_da_block_size = max_size, + "DA size limit reached" + ); + return true; + } + + false + } + + /// Returns the elapsed time since building started. + pub fn elapsed(&self) -> Duration { + self.start.elapsed() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_config_default() { + let config = MorphBuilderConfig::default(); + assert_eq!(config.gas_limit, None); + assert_eq!(config.time_limit, Duration::from_secs(1)); + assert_eq!(config.max_da_block_size, None); + } + + #[test] + fn test_config_builder_pattern() { + let config = MorphBuilderConfig::default() + .with_gas_limit(20_000_000) + .with_time_limit(Duration::from_millis(500)) + .with_max_da_block_size(128 * 1024); + + assert_eq!(config.gas_limit, Some(20_000_000)); + assert_eq!(config.time_limit, Duration::from_millis(500)); + assert_eq!(config.max_da_block_size, Some(128 * 1024)); + } + + #[test] + fn test_breaker_should_break_on_time_limit() { + let breaker = + PayloadBuildingBreaker::new(Duration::from_millis(100), 30_000_000, Some(128 * 1024)); + + // Should not break immediately + assert!(!breaker.should_break(0, 0)); + + // Wait for time limit + std::thread::sleep(Duration::from_millis(150)); + + // Should break now + assert!(breaker.should_break(0, 0)); + } + + #[test] + fn test_breaker_should_break_on_gas_limit() { + // Set gas_limit = 2 * MIN_TRANSACTION_GAS = 42000 + // Threshold = 42000 - 21000 = 21000 + // should_break returns true when cumulative_gas_used > threshold + let gas_limit = 2 * MIN_TRANSACTION_GAS; + let breaker = PayloadBuildingBreaker::new(Duration::from_secs(10), gas_limit, None); + + // At threshold (21000), should NOT break (21000 > 21000 is false) + assert!(!breaker.should_break(MIN_TRANSACTION_GAS, 0)); + + // Just over threshold, should break (21001 > 21000 is true) + assert!(breaker.should_break(MIN_TRANSACTION_GAS + 1, 0)); + } + + #[test] + fn test_breaker_should_break_on_da_limit() { + // Set max_da = 2 * MIN_TRANSACTION_DATA_SIZE = 230 + // Threshold = 230 - 115 = 115 + let max_da_size = 2 * MIN_TRANSACTION_DATA_SIZE; + let breaker = + PayloadBuildingBreaker::new(Duration::from_secs(10), 30_000_000, Some(max_da_size)); + + // At threshold (115), should NOT break (115 > 115 is false) + assert!(!breaker.should_break(0, MIN_TRANSACTION_DATA_SIZE)); + + // Just over threshold, should break (116 > 115 is true) + assert!(breaker.should_break(0, MIN_TRANSACTION_DATA_SIZE + 1)); + } + + #[test] + fn test_breaker_no_da_limit() { + let breaker = PayloadBuildingBreaker::new(Duration::from_secs(10), 30_000_000, None); + + // Should not break even with huge DA size when no limit is set + assert!(!breaker.should_break(0, u64::MAX)); + } + + #[test] + fn test_config_breaker_uses_block_gas_limit_when_not_configured() { + let config = MorphBuilderConfig::default(); + // When gas_limit is None, breaker uses block_gas_limit + let block_gas_limit = 2 * MIN_TRANSACTION_GAS; + + let breaker = config.breaker(block_gas_limit); + + // Threshold = 42000 - 21000 = 21000 + // At threshold, should NOT break + assert!(!breaker.should_break(MIN_TRANSACTION_GAS, 0)); + // Just over, should break + assert!(breaker.should_break(MIN_TRANSACTION_GAS + 1, 0)); + } + + #[test] + fn test_config_breaker_uses_configured_gas_limit() { + // Configure a gas limit lower than block gas limit + let configured_limit = 2 * MIN_TRANSACTION_GAS; // 42000 + let config = MorphBuilderConfig::default().with_gas_limit(configured_limit); + let block_gas_limit = 30_000_000; // Much higher + + let breaker = config.breaker(block_gas_limit); + + // Should use configured_limit, not block_gas_limit + // Threshold = 42000 - 21000 = 21000 + assert!(!breaker.should_break(MIN_TRANSACTION_GAS, 0)); + assert!(breaker.should_break(MIN_TRANSACTION_GAS + 1, 0)); + + // Verify it's not using block_gas_limit + // If using block_gas_limit, threshold would be ~29,979,000 + // and 21001 would NOT trigger the breaker + // Since it does trigger, we know it's using configured_limit + } +} diff --git a/crates/payload/builder/src/error.rs b/crates/payload/builder/src/error.rs new file mode 100644 index 0000000..967ef23 --- /dev/null +++ b/crates/payload/builder/src/error.rs @@ -0,0 +1,57 @@ +//! Morph payload builder error types. + +use alloy_primitives::B256; +use reth_evm::execute::ProviderError; + +/// Errors that can occur during Morph payload building. +#[derive(Debug, thiserror::Error)] +pub enum MorphPayloadBuilderError { + /// Blob transactions are not supported on Morph L2. + #[error("blob transactions are not supported")] + BlobTransactionRejected, + + /// Failed to recover transaction signer. + #[error("failed to recover transaction signer")] + TransactionEcRecoverFailed, + + /// Block gas limit exceeded by sequencer transactions. + #[error( + "block gas limit {gas} exceeded by sequencer transactions, gas spent by tx: {gas_spent_by_tx:?}" + )] + BlockGasLimitExceededBySequencerTransactions { + /// Gas spent by each transaction. + gas_spent_by_tx: Vec, + /// Block gas limit. + gas: u64, + }, + + /// Failed to decode transaction from payload attributes. + #[error("failed to decode transaction: {0}")] + TransactionDecodeError(#[from] alloy_rlp::Error), + + /// Invalid L1 message queue index. + #[error("invalid L1 message queue index: expected {expected}, got {actual}")] + InvalidL1MessageQueueIndex { + /// Expected queue index. + expected: u64, + /// Actual queue index. + actual: u64, + }, + + /// L1 message appears after regular transaction. + #[error("L1 message appears after regular transaction")] + L1MessageAfterRegularTx, + + /// Invalid transaction hash. + #[error("invalid transaction hash: expected {expected}, got {actual}")] + InvalidTransactionHash { + /// Expected hash. + expected: B256, + /// Actual hash. + actual: B256, + }, + + /// Database error when reading contract storage. + #[error("database error: {0}")] + Database(#[from] ProviderError), +} diff --git a/crates/payload/builder/src/lib.rs b/crates/payload/builder/src/lib.rs index a599cb3..1ce5228 100644 --- a/crates/payload/builder/src/lib.rs +++ b/crates/payload/builder/src/lib.rs @@ -2,14 +2,32 @@ //! //! This crate provides the payload building logic for Morph L2. //! -//! The `MorphPayloadBuilder` will implement reth's `PayloadBuilder` trait +//! The [`MorphPayloadBuilder`] implements reth's [`PayloadBuilder`] trait //! to construct L2 blocks with: -//! - L1 message transactions (prioritized) +//! - L1 message transactions (prioritized, must be at the beginning) //! - Sequencer forced transactions -//! - Pool transactions +//! - Pool transactions (optional, controlled by `no_tx_pool` flag) +//! +//! # Transaction Ordering +//! +//! Transactions are included in the following order: +//! 1. L1 messages from payload attributes (must have sequential queue indices) +//! 2. Other forced transactions from payload attributes +//! 3. Pool transactions (if `no_tx_pool` is false) +//! +//! # L1 Message Rules +//! +//! - L1 messages must appear at the beginning of the block +//! - Queue indices must be strictly sequential +//! - Gas is prepaid on L1, so no refunds for unused gas +#![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -#![allow(unused)] -// TODO: Implement MorphPayloadBuilder -// See tempo-payload-builder for reference implementation +mod builder; +mod config; +mod error; + +pub use builder::{MorphPayloadBuilder, MorphPayloadTransactions}; +pub use config::{MorphBuilderConfig, PayloadBuildingBreaker}; +pub use error::MorphPayloadBuilderError; diff --git a/crates/payload/types/Cargo.toml b/crates/payload/types/Cargo.toml index 5cefbee..f6df09e 100644 --- a/crates/payload/types/Cargo.toml +++ b/crates/payload/types/Cargo.toml @@ -13,10 +13,10 @@ workspace = true [dependencies] # Morph -morph-primitives = { workspace = true, features = ["serde", "reth-codec"] } +morph-primitives = { workspace = true, features = ["serde"] } # Reth -reth-engine-primitives.workspace = true +reth-payload-builder.workspace = true reth-payload-primitives.workspace = true reth-primitives-traits.workspace = true diff --git a/crates/payload/types/src/attributes.rs b/crates/payload/types/src/attributes.rs index 1216bdd..2871227 100644 --- a/crates/payload/types/src/attributes.rs +++ b/crates/payload/types/src/attributes.rs @@ -1,9 +1,13 @@ //! Morph payload attributes types. +use alloy_eips::eip2718::Decodable2718; use alloy_eips::eip4895::{Withdrawal, Withdrawals}; use alloy_primitives::{Address, B256, Bytes}; -use alloy_rpc_types_engine::PayloadAttributes; +use alloy_rpc_types_engine::{PayloadAttributes, PayloadId}; +use morph_primitives::MorphTxEnvelope; +use reth_payload_builder::EthPayloadBuilderAttributes; use reth_payload_primitives::PayloadBuilderAttributes; +use reth_primitives_traits::{Recovered, SignerRecoverable, WithEncoded}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -11,12 +15,6 @@ use sha2::{Digest, Sha256}; /// /// This extends the standard Ethereum [`PayloadAttributes`] with L2-specific fields /// for forced transaction inclusion (L1 messages). -/// -/// # Compatibility -/// -/// This type is designed to be compatible with both: -/// - Standard Ethereum Engine API (via the inner [`PayloadAttributes`]) -/// - Morph L2 requirements (via the `transactions` field for L1 messages) #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MorphPayloadAttributes { @@ -32,63 +30,6 @@ pub struct MorphPayloadAttributes { pub transactions: Option>, } -impl MorphPayloadAttributes { - /// Create new [`MorphPayloadAttributes`] from standard attributes. - pub fn new(inner: PayloadAttributes) -> Self { - Self { - inner, - transactions: None, - } - } - - /// Returns the timestamp for the payload. - pub fn timestamp(&self) -> u64 { - self.inner.timestamp - } - - /// Returns the suggested fee recipient (coinbase). - pub fn suggested_fee_recipient(&self) -> Address { - self.inner.suggested_fee_recipient - } - - /// Returns the prev_randao value. - pub fn prev_randao(&self) -> B256 { - self.inner.prev_randao - } - - /// Returns the parent beacon block root if set. - pub fn parent_beacon_block_root(&self) -> Option { - self.inner.parent_beacon_block_root - } - - /// Returns the withdrawals if set. - pub fn withdrawals(&self) -> Option<&Vec> { - self.inner.withdrawals.as_ref() - } - - /// Returns the forced transactions (L1 messages) if any. - pub fn forced_transactions(&self) -> &[Bytes] { - self.transactions.as_deref().unwrap_or(&[]) - } - - /// Returns true if there are forced transactions. - pub fn has_forced_transactions(&self) -> bool { - self.transactions.as_ref().is_some_and(|t| !t.is_empty()) - } - - /// Builder method to set forced transactions. - pub fn with_transactions(mut self, transactions: Vec) -> Self { - self.transactions = Some(transactions); - self - } -} - -impl From for MorphPayloadAttributes { - fn from(inner: PayloadAttributes) -> Self { - Self::new(inner) - } -} - impl reth_payload_primitives::PayloadAttributes for MorphPayloadAttributes { fn timestamp(&self) -> u64 { self.inner.timestamp @@ -109,29 +50,14 @@ impl reth_payload_primitives::PayloadAttributes for MorphPayloadAttributes { /// with decoded transactions and computed payload ID. #[derive(Debug, Clone)] pub struct MorphPayloadBuilderAttributes { - /// Payload ID. - pub id: alloy_rpc_types_engine::PayloadId, - - /// Parent block hash. - pub parent: B256, - - /// Block timestamp. - pub timestamp: u64, - - /// Suggested fee recipient (coinbase). - pub suggested_fee_recipient: Address, - - /// Previous RANDAO value. - pub prev_randao: B256, + /// Inner Ethereum payload builder attributes. + pub inner: EthPayloadBuilderAttributes, - /// Withdrawals (usually empty for L2). - pub withdrawals: Withdrawals, - - /// Parent beacon block root. - pub parent_beacon_block_root: Option, - - /// Forced transactions (L1 messages). - pub transactions: Vec, + /// Decoded sequencer transactions with original encoded bytes. + /// + /// Transactions are decoded and recovered during construction to avoid + /// repeated decoding in the payload builder. + pub transactions: Vec>>, } impl PayloadBuilderAttributes for MorphPayloadBuilderAttributes { @@ -145,7 +71,26 @@ impl PayloadBuilderAttributes for MorphPayloadBuilderAttributes { ) -> Result { let id = payload_id_morph(&parent, &attributes, version); - Ok(Self { + // Decode and recover transactions + let transactions = attributes + .transactions + .unwrap_or_default() + .into_iter() + .map(|data| { + let mut buf = data.as_ref(); + let tx = MorphTxEnvelope::decode_2718(&mut buf)?; + if !buf.is_empty() { + return Err(alloy_rlp::Error::UnexpectedLength); + } + let recovered = tx + .try_into_recovered() + .map_err(|_| alloy_rlp::Error::Custom("failed to recover signer"))?; + Ok(WithEncoded::new(data, recovered)) + }) + .collect::, alloy_rlp::Error>>()?; + + // Build inner Ethereum attributes + let inner = EthPayloadBuilderAttributes { id, parent, timestamp: attributes.inner.timestamp, @@ -153,36 +98,40 @@ impl PayloadBuilderAttributes for MorphPayloadBuilderAttributes { prev_randao: attributes.inner.prev_randao, withdrawals: attributes.inner.withdrawals.unwrap_or_default().into(), parent_beacon_block_root: attributes.inner.parent_beacon_block_root, - transactions: attributes.transactions.unwrap_or_default(), + }; + + Ok(Self { + inner, + transactions, }) } - fn payload_id(&self) -> alloy_rpc_types_engine::PayloadId { - self.id + fn payload_id(&self) -> PayloadId { + self.inner.id } fn parent(&self) -> B256 { - self.parent + self.inner.parent } fn timestamp(&self) -> u64 { - self.timestamp + self.inner.timestamp } fn parent_beacon_block_root(&self) -> Option { - self.parent_beacon_block_root + self.inner.parent_beacon_block_root } fn suggested_fee_recipient(&self) -> Address { - self.suggested_fee_recipient + self.inner.suggested_fee_recipient } fn prev_randao(&self) -> B256 { - self.prev_randao + self.inner.prev_randao } fn withdrawals(&self) -> &Withdrawals { - &self.withdrawals + &self.inner.withdrawals } } @@ -196,11 +145,7 @@ impl MorphPayloadBuilderAttributes { /// Compute payload ID from parent hash and attributes. /// /// Uses SHA-256 hashing with the version byte as the first byte of the result. -fn payload_id_morph( - parent: &B256, - attributes: &MorphPayloadAttributes, - version: u8, -) -> alloy_rpc_types_engine::PayloadId { +fn payload_id_morph(parent: &B256, attributes: &MorphPayloadAttributes, version: u8) -> PayloadId { let mut hasher = Sha256::new(); // Hash parent @@ -240,7 +185,7 @@ fn payload_id_morph( let mut result = hasher.finalize(); result[0] = version; - alloy_rpc_types_engine::PayloadId::new( + PayloadId::new( result.as_slice()[..8] .try_into() .expect("sufficient length"), @@ -265,30 +210,17 @@ mod tests { } #[test] - fn test_new_from_payload_attributes() { - let inner = PayloadAttributes { - timestamp: 1234567890, - prev_randao: B256::random(), - suggested_fee_recipient: Address::random(), - withdrawals: None, - parent_beacon_block_root: None, - }; - - let attrs = MorphPayloadAttributes::new(inner.clone()); - assert_eq!(attrs.timestamp(), inner.timestamp); - assert_eq!( - attrs.suggested_fee_recipient(), - inner.suggested_fee_recipient - ); - assert!(!attrs.has_forced_transactions()); + fn test_default_attributes() { + let attrs = MorphPayloadAttributes::default(); + assert!(attrs.transactions.is_none()); } #[test] fn test_with_transactions() { - let attrs = create_test_attributes().with_transactions(vec![Bytes::from(vec![0x01])]); + let mut attrs = create_test_attributes(); + attrs.transactions = Some(vec![Bytes::from(vec![0x01])]); - assert_eq!(attrs.forced_transactions().len(), 1); - assert!(attrs.has_forced_transactions()); + assert_eq!(attrs.transactions.as_ref().unwrap().len(), 1); } #[test] @@ -318,7 +250,8 @@ mod tests { fn test_payload_id_different_with_transactions() { let parent = B256::random(); let attrs1 = create_test_attributes(); - let attrs2 = create_test_attributes().with_transactions(vec![Bytes::from(vec![0x01])]); + let mut attrs2 = create_test_attributes(); + attrs2.transactions = Some(vec![Bytes::from(vec![0x01])]); let id1 = payload_id_morph(&parent, &attrs1, 1); let id2 = payload_id_morph(&parent, &attrs2, 1); @@ -327,21 +260,10 @@ mod tests { assert_ne!(id1, id2); } - #[test] - fn test_payload_builder_attributes_try_new() { - let parent = B256::random(); - let attrs = create_test_attributes(); - - let builder_attrs = MorphPayloadBuilderAttributes::try_new(parent, attrs.clone(), 1) - .expect("should succeed"); - - assert_eq!(builder_attrs.parent(), parent); - assert_eq!(builder_attrs.timestamp(), attrs.timestamp()); - } - #[test] fn test_serde_roundtrip() { - let attrs = create_test_attributes().with_transactions(vec![Bytes::from(vec![0x01, 0x02])]); + let mut attrs = create_test_attributes(); + attrs.transactions = Some(vec![Bytes::from(vec![0x01, 0x02])]); let json = serde_json::to_string(&attrs).expect("serialize"); let decoded: MorphPayloadAttributes = serde_json::from_str(&json).expect("deserialize"); @@ -359,8 +281,8 @@ mod tests { }"#; let attrs: MorphPayloadAttributes = serde_json::from_str(json).expect("deserialize"); - assert_eq!(attrs.timestamp(), 1234567890); - assert!(!attrs.has_forced_transactions()); + assert_eq!(attrs.inner.timestamp, 1234567890); + assert!(attrs.transactions.is_none()); } #[test] @@ -373,6 +295,6 @@ mod tests { }"#; let attrs: MorphPayloadAttributes = serde_json::from_str(json).expect("deserialize"); - assert_eq!(attrs.forced_transactions().len(), 1); + assert_eq!(attrs.transactions.as_ref().unwrap().len(), 1); } } diff --git a/crates/payload/types/src/built.rs b/crates/payload/types/src/built.rs index 5abdf2c..150d572 100644 --- a/crates/payload/types/src/built.rs +++ b/crates/payload/types/src/built.rs @@ -87,11 +87,11 @@ impl BuiltPayload for MorphBuiltPayload { mod tests { use super::*; use alloy_consensus::Header; - use morph_primitives::BlockBody; + use morph_primitives::{BlockBody, MorphHeader}; use reth_primitives_traits::Block as _; fn create_test_block() -> SealedBlock { - let header = Header::default(); + let header: MorphHeader = Header::default().into(); let body = BlockBody::default(); let block = Block::new(header, body); block.seal_slow() diff --git a/crates/payload/types/src/lib.rs b/crates/payload/types/src/lib.rs index 57c1075..bcbaa0a 100644 --- a/crates/payload/types/src/lib.rs +++ b/crates/payload/types/src/lib.rs @@ -5,10 +5,9 @@ //! - [`SafeL2Data`]: Safe block data for NewSafeL2Block (derivation) //! - [`MorphPayloadAttributes`]: Extended payload attributes for block building //! - [`MorphBuiltPayload`]: Built payload result -//! - [`MorphEngineTypes`]: Engine types for the Morph Engine API -//! - [`MorphPayloadTypes`]: Default payload types implementation +//! - [`MorphPayloadTypes`]: Payload types for reth node framework //! -//! These types are designed to be compatible with the standard Ethereum Engine API. +//! These types are designed to be compatible with the Morph L2 Engine API. #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] @@ -19,17 +18,14 @@ mod executable_l2_data; mod params; mod safe_l2_data; -use core::marker::PhantomData; - -use alloy_rpc_types_engine::{ - ExecutionData, ExecutionPayload, ExecutionPayloadEnvelopeV2, ExecutionPayloadEnvelopeV3, - ExecutionPayloadEnvelopeV4, ExecutionPayloadV1, -}; +use alloy_consensus::BlockHeader as _; +use alloy_eips::eip4895::Withdrawal; +use alloy_primitives::{B256, Bytes}; use morph_primitives::Block; -use reth_engine_primitives::EngineTypes; -use reth_payload_primitives::{BuiltPayload, PayloadTypes}; +use reth_payload_primitives::{BuiltPayload, ExecutionPayload, PayloadTypes}; use reth_primitives_traits::{NodePrimitives, SealedBlock}; use serde::{Deserialize, Serialize}; +use std::sync::Arc; // Re-export main types pub use attributes::{MorphPayloadAttributes, MorphPayloadBuilderAttributes}; @@ -38,61 +34,69 @@ pub use executable_l2_data::ExecutableL2Data; pub use params::{AssembleL2BlockParams, GenericResponse}; pub use safe_l2_data::SafeL2Data; -/// The types used in the Morph beacon consensus engine. +// ============================================================================= +// MorphPayloadTypes - Required for reth NodeBuilder framework +// ============================================================================= + +/// Payload types for Morph node. /// -/// This is a generic wrapper that allows customizing the payload types. -#[derive(Debug, Default, Clone, Serialize, Deserialize)] +/// This type is required by reth's `NodeTypes` trait to define how payloads +/// are built and represented in the node framework. +#[derive(Debug, Clone, Copy, Default)] #[non_exhaustive] -pub struct MorphEngineTypes { - _marker: PhantomData, -} +pub struct MorphPayloadTypes; -impl PayloadTypes for MorphEngineTypes -where - T: PayloadTypes, - T::BuiltPayload: BuiltPayload>, -{ - type ExecutionData = T::ExecutionData; - type BuiltPayload = T::BuiltPayload; - type PayloadAttributes = T::PayloadAttributes; - type PayloadBuilderAttributes = T::PayloadBuilderAttributes; +/// Execution data for Morph node. Simply wraps a sealed block. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MorphExecutionData { + /// The built block. + pub block: Arc>, +} - fn block_to_payload( - block: SealedBlock< - <::Primitives as NodePrimitives>::Block, - >, - ) -> ExecutionData { - let (payload, sidecar) = - ExecutionPayload::from_block_unchecked(block.hash(), &block.into_block()); - ExecutionData { payload, sidecar } +impl MorphExecutionData { + /// Creates a new `MorphExecutionData` from a sealed block. + pub fn new(block: Arc>) -> Self { + Self { block } } } -impl EngineTypes for MorphEngineTypes -where - T: PayloadTypes, - T::BuiltPayload: BuiltPayload> - + TryInto - + TryInto - + TryInto - + TryInto, -{ - type ExecutionPayloadEnvelopeV1 = ExecutionPayloadV1; - type ExecutionPayloadEnvelopeV2 = ExecutionPayloadEnvelopeV2; - type ExecutionPayloadEnvelopeV3 = ExecutionPayloadEnvelopeV3; - type ExecutionPayloadEnvelopeV4 = ExecutionPayloadEnvelopeV4; - type ExecutionPayloadEnvelopeV5 = ExecutionPayloadEnvelopeV4; -} +impl ExecutionPayload for MorphExecutionData { + fn parent_hash(&self) -> B256 { + self.block.parent_hash() + } -/// A default payload type for [`MorphEngineTypes`]. -/// -/// This type provides the concrete implementations for Morph's payload handling. -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -#[non_exhaustive] -pub struct MorphPayloadTypes; + fn block_hash(&self) -> B256 { + self.block.hash() + } + + fn block_number(&self) -> u64 { + self.block.number() + } + + fn withdrawals(&self) -> Option<&Vec> { + // Morph L2 doesn't have withdrawals + None + } + + fn parent_beacon_block_root(&self) -> Option { + self.block.parent_beacon_block_root() + } + + fn timestamp(&self) -> u64 { + self.block.timestamp() + } + + fn gas_used(&self) -> u64 { + self.block.gas_used() + } + + fn block_access_list(&self) -> Option<&Bytes> { + None + } +} impl PayloadTypes for MorphPayloadTypes { - type ExecutionData = ExecutionData; + type ExecutionData = MorphExecutionData; type BuiltPayload = MorphBuiltPayload; type PayloadAttributes = MorphPayloadAttributes; type PayloadBuilderAttributes = MorphPayloadBuilderAttributes; @@ -102,8 +106,6 @@ impl PayloadTypes for MorphPayloadTypes { <::Primitives as NodePrimitives>::Block, >, ) -> Self::ExecutionData { - let (payload, sidecar) = - ExecutionPayload::from_block_unchecked(block.hash(), &block.into_block()); - ExecutionData { payload, sidecar } + MorphExecutionData::new(Arc::new(block)) } } diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index 1f28dfa..2180863 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -32,6 +32,7 @@ modular-bitfield = { version = "0.11.2", optional = true } [dev-dependencies] +serde_json.workspace = true [features] default = ["serde", "reth"] @@ -41,6 +42,8 @@ serde = [ "alloy-primitives/serde", "alloy-eips/serde", "alloy-consensus/serde", + "reth-primitives-traits/serde", + "reth-ethereum-primitives?/serde", ] reth = [ "dep:reth-ethereum-primitives", diff --git a/crates/primitives/src/header.rs b/crates/primitives/src/header.rs new file mode 100644 index 0000000..bb6af1e --- /dev/null +++ b/crates/primitives/src/header.rs @@ -0,0 +1,349 @@ +//! Morph block header type. +//! +//! This module defines the Morph-specific header type that includes: +//! - `next_l1_msg_index`: The next L1 message queue index to process +//! - `batch_hash`: The batch hash (non-zero if this block is a batch point) +//! +//! These fields are NOT included in the block hash calculation to maintain +//! compatibility with standard Ethereum header hashing (matching go-ethereum's behavior). + +use alloy_consensus::{BlockHeader, Header, Sealable}; +use alloy_primitives::{Address, B64, B256, BlockNumber, Bloom, Bytes, U256}; +use alloy_rlp::{RlpDecodable, RlpEncodable}; + +/// Morph block header. +/// +/// This header extends the standard Ethereum header with Morph-specific fields: +/// - `next_l1_msg_index`: Next L1 message queue index to process +/// - `batch_hash`: Non-zero if this block is a batch point +/// +/// **Important**: The `hash_slow()` method only hashes the inner Ethereum header, +/// excluding `next_l1_msg_index` and `batch_hash`. This matches go-ethereum's +/// `Header.Hash()` behavior where these L2-specific fields are not part of the +/// block hash calculation. +/// +/// Note: The `inner` field must be placed last for the `Compact` derive macro, +/// as fields with unknown size must come last in the struct definition. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, RlpEncodable, RlpDecodable)] +#[cfg_attr(feature = "reth-codec", derive(reth_codecs::Compact))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct MorphHeader { + /// Next L1 message queue index to process. + /// Not part of the header hash calculation. + #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] + pub next_l1_msg_index: u64, + + /// Batch hash - non-zero if this block is a batch point. + /// Not part of the header hash calculation. + pub batch_hash: B256, + + /// Standard Ethereum header (flattened in JSON serialization). + /// Must be placed last due to Compact derive requirements. + #[cfg_attr(feature = "serde", serde(flatten))] + pub inner: Header, +} + +impl MorphHeader { + /// Returns true if this block is a batch point (batch_hash is non-zero). + pub fn is_batch_point(&self) -> bool { + self.batch_hash != B256::ZERO + } +} + +impl From
for MorphHeader { + fn from(inner: Header) -> Self { + Self { + inner, + next_l1_msg_index: 0, + batch_hash: B256::ZERO, + } + } +} + +impl AsRef for MorphHeader { + fn as_ref(&self) -> &Self { + self + } +} + +// Implement BlockHeader trait by delegating to inner header +impl BlockHeader for MorphHeader { + fn parent_hash(&self) -> B256 { + self.inner.parent_hash() + } + + fn ommers_hash(&self) -> B256 { + self.inner.ommers_hash() + } + + fn beneficiary(&self) -> Address { + self.inner.beneficiary() + } + + fn state_root(&self) -> B256 { + self.inner.state_root() + } + + fn transactions_root(&self) -> B256 { + self.inner.transactions_root() + } + + fn receipts_root(&self) -> B256 { + self.inner.receipts_root() + } + + fn withdrawals_root(&self) -> Option { + self.inner.withdrawals_root() + } + + fn logs_bloom(&self) -> Bloom { + self.inner.logs_bloom() + } + + fn difficulty(&self) -> U256 { + self.inner.difficulty() + } + + fn number(&self) -> BlockNumber { + self.inner.number() + } + + fn gas_limit(&self) -> u64 { + self.inner.gas_limit() + } + + fn gas_used(&self) -> u64 { + self.inner.gas_used() + } + + fn timestamp(&self) -> u64 { + self.inner.timestamp() + } + + fn mix_hash(&self) -> Option { + self.inner.mix_hash() + } + + fn nonce(&self) -> Option { + self.inner.nonce() + } + + fn base_fee_per_gas(&self) -> Option { + self.inner.base_fee_per_gas() + } + + fn blob_gas_used(&self) -> Option { + self.inner.blob_gas_used() + } + + fn excess_blob_gas(&self) -> Option { + self.inner.excess_blob_gas() + } + + fn parent_beacon_block_root(&self) -> Option { + self.inner.parent_beacon_block_root() + } + + fn requests_hash(&self) -> Option { + self.inner.requests_hash() + } + + fn extra_data(&self) -> &Bytes { + self.inner.extra_data() + } +} + +/// Sealable implementation for MorphHeader. +/// +/// **Critical**: The `hash_slow()` method only hashes the inner Ethereum header, +/// NOT the `next_l1_msg_index` and `batch_hash` fields. This matches go-ethereum's +/// `Header.Hash()` behavior which explicitly excludes these L2-specific fields +/// from the hash calculation. +impl Sealable for MorphHeader { + fn hash_slow(&self) -> B256 { + // Only hash the inner header to match go-ethereum behavior. + // next_l1_msg_index and batch_hash are NOT part of the hash. + self.inner.hash_slow() + } +} + +#[cfg(feature = "reth")] +impl reth_primitives_traits::InMemorySize for MorphHeader { + fn size(&self) -> usize { + reth_primitives_traits::InMemorySize::size(&self.inner) + + core::mem::size_of::() // next_l1_msg_index + + core::mem::size_of::() // batch_hash + } +} + +#[cfg(feature = "reth")] +impl reth_primitives_traits::BlockHeader for MorphHeader {} + +#[cfg(feature = "reth")] +impl reth_primitives_traits::header::HeaderMut for MorphHeader { + fn set_parent_hash(&mut self, hash: B256) { + self.inner.set_parent_hash(hash); + } + + fn set_block_number(&mut self, number: BlockNumber) { + self.inner.set_block_number(number); + } + + fn set_timestamp(&mut self, timestamp: u64) { + self.inner.set_timestamp(timestamp); + } + + fn set_state_root(&mut self, state_root: B256) { + self.inner.set_state_root(state_root); + } + + fn set_difficulty(&mut self, difficulty: U256) { + self.inner.set_difficulty(difficulty); + } +} + +#[cfg(feature = "reth-codec")] +impl reth_db_api::table::Compress for MorphHeader { + type Compressed = Vec; + + fn compress_to_buf>(&self, buf: &mut B) { + let _ = reth_codecs::Compact::to_compact(self, buf); + } +} + +#[cfg(feature = "reth-codec")] +impl reth_db_api::table::Decompress for MorphHeader { + fn decompress(value: &[u8]) -> Result { + let (obj, _) = reth_codecs::Compact::from_compact(value, value.len()); + Ok(obj) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::{Bytes, address, b256}; + + fn create_test_header() -> Header { + Header { + parent_hash: b256!("0000000000000000000000000000000000000000000000000000000000000001"), + ommers_hash: b256!("0000000000000000000000000000000000000000000000000000000000000002"), + beneficiary: address!("0000000000000000000000000000000000000011"), + state_root: b256!("0000000000000000000000000000000000000000000000000000000000000003"), + transactions_root: b256!( + "0000000000000000000000000000000000000000000000000000000000000004" + ), + receipts_root: b256!( + "0000000000000000000000000000000000000000000000000000000000000005" + ), + logs_bloom: Bloom::default(), + difficulty: U256::from(1u64), + number: 100, + gas_limit: 30_000_000, + gas_used: 21_000, + timestamp: 1234567890, + extra_data: Bytes::default(), + mix_hash: B256::ZERO, + nonce: B64::ZERO, + base_fee_per_gas: Some(1000000000), + withdrawals_root: None, + blob_gas_used: None, + excess_blob_gas: None, + parent_beacon_block_root: None, + requests_hash: None, + } + } + + #[test] + fn test_morph_header_from_header() { + let inner = create_test_header(); + let header: MorphHeader = inner.clone().into(); + + assert_eq!(header.inner, inner); + assert_eq!(header.next_l1_msg_index, 0); + assert_eq!(header.batch_hash, B256::ZERO); + assert!(!header.is_batch_point()); + } + + #[test] + fn test_morph_header_with_fields() { + let inner = create_test_header(); + let batch_hash = b256!("0000000000000000000000000000000000000000000000000000000000000abc"); + let header = MorphHeader { + inner, + next_l1_msg_index: 100, + batch_hash, + }; + + assert_eq!(header.next_l1_msg_index, 100); + assert_eq!(header.batch_hash, batch_hash); + assert!(header.is_batch_point()); + } + + #[test] + fn test_morph_header_hash_excludes_l2_fields() { + let inner = create_test_header(); + + // Create two headers with different L2 fields + let header1: MorphHeader = inner.clone().into(); + let header2 = MorphHeader { + inner: inner.clone(), + next_l1_msg_index: 999, + batch_hash: b256!("1111111111111111111111111111111111111111111111111111111111111111"), + }; + + // Both should have the same hash since L2 fields are excluded + assert_eq!(header1.hash_slow(), header2.hash_slow()); + + // And they should match the inner header's hash + assert_eq!(header1.hash_slow(), inner.hash_slow()); + } + + #[test] + fn test_morph_header_field_mutation() { + let inner = create_test_header(); + let mut header: MorphHeader = inner.into(); + + header.next_l1_msg_index = 50; + assert_eq!(header.next_l1_msg_index, 50); + + let batch_hash = b256!("2222222222222222222222222222222222222222222222222222222222222222"); + header.batch_hash = batch_hash; + assert_eq!(header.batch_hash, batch_hash); + assert!(header.is_batch_point()); + } + + #[test] + fn test_morph_header_block_header_delegation() { + let inner = create_test_header(); + let header: MorphHeader = inner.clone().into(); + + // Test that all BlockHeader methods delegate correctly + assert_eq!(header.parent_hash(), inner.parent_hash()); + assert_eq!(header.beneficiary(), inner.beneficiary()); + assert_eq!(header.state_root(), inner.state_root()); + assert_eq!(header.number(), inner.number()); + assert_eq!(header.gas_limit(), inner.gas_limit()); + assert_eq!(header.gas_used(), inner.gas_used()); + assert_eq!(header.timestamp(), inner.timestamp()); + assert_eq!(header.base_fee_per_gas(), inner.base_fee_per_gas()); + } + + #[cfg(feature = "serde")] + #[test] + fn test_morph_header_serde() { + let inner = create_test_header(); + let header = MorphHeader { + inner, + next_l1_msg_index: 42, + batch_hash: b256!("3333333333333333333333333333333333333333333333333333333333333333"), + }; + + let json = serde_json::to_string(&header).expect("serialization failed"); + let deserialized: MorphHeader = + serde_json::from_str(&json).expect("deserialization failed"); + + assert_eq!(header, deserialized); + } +} diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index a3b6a11..39c8421 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -1,4 +1,4 @@ -//! Morph primitive types. +//! Morph primitive types //! //! This crate provides core data types for Morph L2, including custom transaction //! types, receipt types, and type aliases for blocks and headers. @@ -20,7 +20,7 @@ //! //! - [`Block`]: Morph block type alias //! - [`BlockBody`]: Morph block body type alias -//! - [`MorphHeader`]: Header type alias (same as Ethereum) +//! - [`MorphHeader`]: Morph header type alias //! //! # Node Primitives //! @@ -43,15 +43,12 @@ use reth_ethereum_primitives as _; #[cfg(feature = "reth-codec")] use reth_zstd_compressors as _; +pub mod header; pub mod receipt; pub mod transaction; -// Re-export standard Ethereum types -pub use alloy_consensus::Header; -/// Header alias for backwards compatibility. -pub type MorphHeader = Header; - -use reth_primitives_traits::NodePrimitives; +// Re-export header type +pub use header::MorphHeader; /// Morph block. pub type Block = alloy_consensus::Block; @@ -60,9 +57,7 @@ pub type Block = alloy_consensus::Block; pub type BlockBody = alloy_consensus::BlockBody; // Re-export receipt types -pub use receipt::{ - MorphReceipt, MorphReceiptWithBloom, MorphTransactionReceipt, calculate_receipt_root_no_memo, -}; +pub use receipt::{MorphReceipt, MorphReceiptWithBloom, MorphTransactionReceipt}; // Re-export transaction types pub use transaction::{ @@ -75,7 +70,7 @@ pub use transaction::{ pub struct MorphPrimitives; #[cfg(feature = "reth-codec")] -impl NodePrimitives for MorphPrimitives { +impl reth_primitives_traits::NodePrimitives for MorphPrimitives { type Block = Block; type BlockHeader = MorphHeader; type BlockBody = BlockBody; diff --git a/crates/primitives/src/receipt/mod.rs b/crates/primitives/src/receipt/mod.rs index d2758fe..06e6d11 100644 --- a/crates/primitives/src/receipt/mod.rs +++ b/crates/primitives/src/receipt/mod.rs @@ -422,7 +422,11 @@ impl InMemorySize for MorphReceipt { /// /// # Example /// -/// ```ignore +/// ``` +/// use morph_primitives::receipt::{MorphReceipt, calculate_receipt_root_no_memo}; +/// use alloy_consensus::Receipt; +/// +/// let receipts: Vec = vec![]; /// let receipts_root = calculate_receipt_root_no_memo(&receipts); /// ``` pub fn calculate_receipt_root_no_memo(receipts: &[MorphReceipt]) -> B256 { diff --git a/crates/primitives/src/transaction/envelope.rs b/crates/primitives/src/transaction/envelope.rs index 8ce2a9c..23c6587 100644 --- a/crates/primitives/src/transaction/envelope.rs +++ b/crates/primitives/src/transaction/envelope.rs @@ -1,5 +1,8 @@ -use alloy_consensus::{Signed, TransactionEnvelope, TxEip1559, TxEip2930, TxEip7702, TxLegacy}; -use alloy_primitives::{B256, Bytes, U256}; +use alloy_consensus::{ + Sealed, Signed, TransactionEnvelope, TxEip1559, TxEip2930, TxEip7702, TxLegacy, +}; +use alloy_eips::eip2718::Encodable2718; +use alloy_primitives::{B256, Bytes}; use alloy_rlp::BytesMut; use crate::{TxAltFee, TxL1Msg}; @@ -25,7 +28,7 @@ pub enum MorphTxEnvelope { /// Layer1 Message Transaction #[envelope(ty = 0x7e)] - L1Msg(Signed), + L1Msg(Sealed), /// Alt Fee Transaction #[envelope(ty = 0x7f)] @@ -66,7 +69,7 @@ impl MorphTxEnvelope { | Self::Eip1559(_) | Self::Eip7702(_) | Self::AltFee(_) => None, - Self::L1Msg(tx) => Some(tx.tx().queue_index), + Self::L1Msg(tx) => Some(tx.queue_index), } } @@ -84,22 +87,6 @@ impl MorphTxEnvelope { } Bytes(bytes.freeze()) } - - /// Returns the fee token id if this is an AltFee transaction. - pub fn fee_token_id(&self) -> Option { - match self { - Self::AltFee(tx) => Some(tx.tx().fee_token_id), - _ => None, - } - } - - /// Returns the fee limit if this is an AltFee transaction. - pub fn fee_limit(&self) -> Option { - match self { - Self::AltFee(tx) => Some(tx.tx().fee_limit), - _ => None, - } - } } impl reth_primitives_traits::InMemorySize for MorphTxEnvelope { @@ -142,7 +129,7 @@ impl alloy_consensus::transaction::TxHashRef for MorphTxEnvelope { Self::Eip2930(tx) => tx.hash(), Self::Eip1559(tx) => tx.hash(), Self::Eip7702(tx) => tx.hash(), - Self::L1Msg(tx) => tx.hash(), + Self::L1Msg(tx) => tx.hash_ref(), Self::AltFee(tx) => tx.hash(), } } @@ -163,7 +150,8 @@ impl alloy_consensus::transaction::SignerRecoverable for MorphTxEnvelope { Self::Eip7702(tx) => { alloy_consensus::transaction::SignerRecoverable::recover_signer(tx) } - Self::L1Msg(tx) => alloy_consensus::transaction::SignerRecoverable::recover_signer(tx), + // L1 messages have no signature - the sender is stored in the transaction itself + Self::L1Msg(tx) => Ok(tx.sender), Self::AltFee(tx) => alloy_consensus::transaction::SignerRecoverable::recover_signer(tx), } } @@ -184,9 +172,8 @@ impl alloy_consensus::transaction::SignerRecoverable for MorphTxEnvelope { Self::Eip7702(tx) => { alloy_consensus::transaction::SignerRecoverable::recover_signer_unchecked(tx) } - Self::L1Msg(tx) => { - alloy_consensus::transaction::SignerRecoverable::recover_signer_unchecked(tx) - } + // L1 messages have no signature - the sender is stored in the transaction itself + Self::L1Msg(tx) => Ok(tx.sender), Self::AltFee(tx) => { alloy_consensus::transaction::SignerRecoverable::recover_signer_unchecked(tx) } @@ -252,7 +239,8 @@ mod codec { } MorphTxType::L1Msg => { let (tx, buf) = TxL1Msg::from_compact(buf, buf.len()); - let tx = Signed::new_unhashed(tx, signature); + // L1 messages have no signature - use Sealed instead of Signed + let tx = Sealed::new(tx); (Self::L1Msg(tx), buf) } MorphTxType::AltFee => { @@ -271,12 +259,19 @@ mod codec { 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::L1Msg(tx) => tx.to_compact(buf), Self::AltFee(tx) => tx.tx().to_compact(buf), }; } } + /// Dummy signature for L1 messages (which have no signature). + const L1_MSG_SIGNATURE: Signature = Signature::new( + alloy_primitives::U256::ZERO, + alloy_primitives::U256::ZERO, + false, + ); + impl Envelope for MorphTxEnvelope { fn signature(&self) -> &Signature { match self { @@ -284,7 +279,7 @@ mod codec { Self::Eip2930(tx) => tx.signature(), Self::Eip1559(tx) => tx.signature(), Self::Eip7702(tx) => tx.signature(), - Self::L1Msg(tx) => tx.signature(), + Self::L1Msg(_) => &L1_MSG_SIGNATURE, Self::AltFee(tx) => tx.signature(), } } diff --git a/crates/primitives/src/transaction/l1_transaction.rs b/crates/primitives/src/transaction/l1_transaction.rs index c446bf2..4df1f90 100644 --- a/crates/primitives/src/transaction/l1_transaction.rs +++ b/crates/primitives/src/transaction/l1_transaction.rs @@ -9,7 +9,10 @@ use alloy_consensus::{ SignableTransaction, Transaction, transaction::{RlpEcdsaDecodableTx, RlpEcdsaEncodableTx}, }; -use alloy_eips::{Typed2718, eip2718::Encodable2718}; +use alloy_eips::{ + Typed2718, + eip2718::{Decodable2718, Eip2718Error, Eip2718Result, Encodable2718}, +}; use alloy_primitives::{Address, B256, Bytes, ChainId, Signature, TxKind, U256, keccak256}; use alloy_rlp::{BufMut, Decodable, Encodable, Header}; use core::mem; @@ -22,6 +25,11 @@ pub const L1_TX_TYPE_ID: u8 = 0x7E; /// This transaction type represents L1 message transactions that are processed on L2, /// typically including deposit transactions or L1-originated messages. /// +/// The signature of the L1 message is already verified on the L1 and as such doesn't contain +/// a signature field. Gas for the transaction execution is already paid for on the L1. +/// +/// Note: Contract creation is NOT allowed via L1 message transactions. +/// /// Reference: #[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -32,34 +40,26 @@ pub struct TxL1Msg { #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))] pub queue_index: u64, - /// The 160-bit address of the message call's sender. - pub from: Address, + /// The gas limit for the transaction. Gas is paid for when message is sent from the L1. + #[cfg_attr( + feature = "serde", + serde(with = "alloy_serde::quantity", rename = "gas") + )] + pub gas_limit: u64, - /// 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, - - /// The 160-bit address of the message call's recipient or, for a contract - /// creation transaction, empty. - pub to: TxKind, + /// The destination address for the transaction. + /// Contract creation is NOT allowed via L1 message transactions. + pub to: Address, /// 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. + /// message call's recipient. pub value: 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. + /// The L1 sender of the transaction. + pub sender: Address, + + /// The input data of the message call. + /// Note: This field must be last for reth-codec Compact derive. #[cfg_attr(feature = "serde", serde(default, alias = "data"))] pub input: Bytes, } @@ -71,11 +71,6 @@ impl TxL1Msg { L1_TX_TYPE_ID } - /// Returns the sender address. - pub const fn sender(&self) -> Address { - self.from - } - /// Validates the transaction according to the spec rules. /// /// L1 message transactions have minimal validation requirements. @@ -85,60 +80,55 @@ impl TxL1Msg { } /// Calculate the in-memory size of this transaction. - /// - /// This accounts for all fields in the struct. pub fn size(&self) -> usize { mem::size_of::() + // queue_index - mem::size_of::
() + // from - mem::size_of::() + // nonce - mem::size_of::() + // gas_limit - mem::size_of::() + // to + mem::size_of::() + // gas_limit + mem::size_of::
() + // to mem::size_of::() + // value - self.input.len() // input (dynamic size) + self.input.len() + // input (dynamic size) + mem::size_of::
() // sender } /// Outputs the length of the transaction's fields. + /// Field order matches go-ethereum: queue_index, gas_limit, to, value, input, sender #[doc(hidden)] pub fn fields_len(&self) -> usize { - let mut len = 0; - len += self.queue_index.length(); - len += self.nonce.length(); - len += self.gas_limit.length(); - len += self.to.length(); - len += self.value.length(); - len += self.input.0.length(); - len += self.from.length(); - len + self.queue_index.length() + + self.gas_limit.length() + + self.to.length() + + self.value.length() + + self.input.0.length() + + self.sender.length() } /// Encode the transaction fields (without the RLP header). + /// Field order matches go-ethereum: queue_index, gas_limit, to, value, input, sender pub fn encode_fields(&self, out: &mut dyn BufMut) { self.queue_index.encode(out); - self.nonce.encode(out); self.gas_limit.encode(out); self.to.encode(out); self.value.encode(out); self.input.0.encode(out); - self.from.encode(out); + self.sender.encode(out); } - /// Decode the transaction fields (without the RLP header). + /// Decode the transaction fields from RLP bytes. + /// Field order matches go-ethereum: queue_index, gas_limit, to, value, input, sender pub fn decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result { Ok(Self { queue_index: 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)?, + sender: Decodable::decode(buf)?, }) } - /// Computes the hash used for the transaction. + /// Computes the transaction hash. /// - /// For L1 messages, this computes the keccak256 hash of the RLP encoding. - pub fn signature_hash(&self) -> B256 { + /// For L1 messages, this computes the keccak256 hash of the EIP-2718 encoding. + pub fn tx_hash(&self) -> B256 { let mut buf = Vec::with_capacity(self.encode_2718_len()); self.encode_2718(&mut buf); keccak256(&buf) @@ -157,15 +147,17 @@ impl Transaction for TxL1Msg { } fn nonce(&self) -> u64 { + // L1 messages always have nonce 0 0 } fn gas_limit(&self) -> u64 { - self.gas_limit as u64 + self.gas_limit } fn gas_price(&self) -> Option { - Some(0) + // L1 messages have no gas price - gas is paid on L1 + None } fn max_fee_per_gas(&self) -> u128 { @@ -193,11 +185,13 @@ impl Transaction for TxL1Msg { } fn kind(&self) -> TxKind { - self.to + // L1 messages are always calls, never contract creations + TxKind::Call(self.to) } fn is_create(&self) -> bool { - self.to.is_create() + // Contract creation is NOT allowed via L1 message transactions + false } fn value(&self) -> U256 { @@ -275,27 +269,13 @@ impl Decodable for TxL1Msg { return Err(alloy_rlp::Error::InputTooShort); } - let queue_index = Decodable::decode(buf)?; - let nonce = Decodable::decode(buf)?; - let gas_limit = Decodable::decode(buf)?; - let to = Decodable::decode(buf)?; - let value = Decodable::decode(buf)?; - let input = Decodable::decode(buf)?; - let from = Decodable::decode(buf)?; + let this = Self::decode_fields(buf)?; if buf.len() + header.payload_length != remaining { return Err(alloy_rlp::Error::UnexpectedLength); } - Ok(Self { - queue_index, - from, - nonce, - gas_limit, - to, - value, - input, - }) + Ok(this) } } @@ -325,12 +305,31 @@ impl Encodable2718 for TxL1Msg { } } +impl Decodable2718 for TxL1Msg { + fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result { + if ty != L1_TX_TYPE_ID { + return Err(Eip2718Error::UnexpectedType(ty)); + } + Self::decode(buf).map_err(Into::into) + } + + fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result { + Self::decode(buf).map_err(Into::into) + } +} + impl reth_primitives_traits::InMemorySize for TxL1Msg { fn size(&self) -> usize { Self::size(self) } } +impl alloy_consensus::Sealable for TxL1Msg { + fn hash_slow(&self) -> B256 { + self.tx_hash() + } +} + #[cfg(test)] mod tests { use super::*; @@ -339,10 +338,11 @@ mod tests { #[test] fn test_l1_transaction_default() { let tx = TxL1Msg::default(); - assert_eq!(tx.nonce, 0); + assert_eq!(tx.queue_index, 0); assert_eq!(tx.gas_limit, 0); assert_eq!(tx.value, U256::ZERO); - assert_eq!(tx.from, Address::ZERO); + assert_eq!(tx.sender, Address::ZERO); + assert_eq!(tx.to, Address::ZERO); } #[test] @@ -361,26 +361,25 @@ mod tests { fn test_l1_transaction_trait_methods() { let tx = TxL1Msg { queue_index: 0, - from: address!("0000000000000000000000000000000000000001"), - nonce: 0, gas_limit: 21_000, - to: TxKind::Call(address!("0000000000000000000000000000000000000002")), + to: address!("0000000000000000000000000000000000000002"), value: U256::from(100u64), input: Bytes::from(vec![1, 2, 3, 4]), + sender: address!("0000000000000000000000000000000000000001"), }; // Test Transaction trait methods assert_eq!(tx.chain_id(), None); - assert_eq!(Transaction::nonce(&tx), 0); // nonce is set to 0 in this test case + assert_eq!(Transaction::nonce(&tx), 0); // L1 messages always have nonce 0 assert_eq!(Transaction::gas_limit(&tx), 21_000); - assert_eq!(tx.gas_price(), Some(0)); + assert_eq!(tx.gas_price(), None); // L1 messages have no gas price assert_eq!(tx.max_fee_per_gas(), 0); assert_eq!(tx.max_priority_fee_per_gas(), None); assert_eq!(tx.max_fee_per_blob_gas(), None); assert_eq!(tx.priority_fee_or_price(), 0); assert_eq!(tx.effective_gas_price(Some(100)), 0); assert!(!tx.is_dynamic_fee()); - assert!(!tx.is_create()); + assert!(!tx.is_create()); // L1 messages can never create contracts assert_eq!( tx.kind(), TxKind::Call(address!("0000000000000000000000000000000000000002")) @@ -394,45 +393,42 @@ mod tests { } #[test] - fn test_l1_transaction_is_create() { - let create_tx = TxL1Msg { - to: TxKind::Create, - ..Default::default() - }; - assert!(create_tx.is_create()); + fn test_l1_transaction_is_never_create() { + // L1 messages should never be contract creation + let tx = TxL1Msg::default(); + assert!(!tx.is_create()); - let call_tx = TxL1Msg { - to: TxKind::Call(address!("0000000000000000000000000000000000000001")), + let tx_with_address = TxL1Msg { + to: address!("0000000000000000000000000000000000000001"), ..Default::default() }; - assert!(!call_tx.is_create()); + assert!(!tx_with_address.is_create()); } #[test] fn test_l1_transaction_sender() { let tx = TxL1Msg { - from: address!("0000000000000000000000000000000000000001"), + sender: address!("0000000000000000000000000000000000000001"), ..Default::default() }; assert_eq!( - tx.sender(), + tx.sender, address!("0000000000000000000000000000000000000001") ); } #[test] - fn test_l1_transaction_signature_hash() { + fn test_l1_transaction_tx_hash() { let tx = TxL1Msg { queue_index: 0, - from: address!("0000000000000000000000000000000000000001"), - nonce: 1, gas_limit: 21_000, - to: TxKind::Call(address!("0000000000000000000000000000000000000002")), + to: address!("0000000000000000000000000000000000000002"), value: U256::from(100u64), input: Bytes::new(), + sender: address!("0000000000000000000000000000000000000001"), }; - let hash = tx.signature_hash(); + let hash = tx.tx_hash(); assert_ne!(hash, B256::ZERO); } @@ -440,12 +436,11 @@ mod tests { fn test_l1_transaction_rlp_roundtrip() { let tx = TxL1Msg { queue_index: 5, - from: address!("0000000000000000000000000000000000000001"), - nonce: 42, gas_limit: 21_000, - to: TxKind::Call(address!("0000000000000000000000000000000000000002")), - value: U256::from(1_000_000_000_000_000_000u128), + to: address!("0000000000000000000000000000000000000002"), + value: U256::from(1_000_000_000_000_000_000u64), input: Bytes::from(vec![0x12, 0x34]), + sender: address!("0000000000000000000000000000000000000001"), }; // Encode @@ -456,46 +451,22 @@ mod tests { let decoded = TxL1Msg::decode(&mut buf.as_slice()).expect("Should decode"); assert_eq!(tx.queue_index, decoded.queue_index); - assert_eq!(tx.from, decoded.from); - assert_eq!(tx.nonce, decoded.nonce); assert_eq!(tx.gas_limit, decoded.gas_limit); assert_eq!(tx.to, decoded.to); assert_eq!(tx.value, decoded.value); assert_eq!(tx.input, decoded.input); - } - - #[test] - fn test_l1_transaction_create() { - let tx = TxL1Msg { - queue_index: 0, - from: address!("0000000000000000000000000000000000000001"), - nonce: 0, - gas_limit: 100_000, - to: TxKind::Create, - value: U256::ZERO, - input: Bytes::from(vec![0x60, 0x80, 0x60, 0x40]), - }; - - // Encode - let mut buf = Vec::new(); - tx.encode(&mut buf); - - // Decode - let decoded = TxL1Msg::decode(&mut buf.as_slice()).expect("Should decode"); - - assert_eq!(decoded.to, TxKind::Create); + assert_eq!(tx.sender, decoded.sender); } #[test] fn test_l1_transaction_encode_2718() { let tx = TxL1Msg { queue_index: 0, - from: address!("0000000000000000000000000000000000000001"), - nonce: 1, gas_limit: 21_000, - to: TxKind::Call(address!("0000000000000000000000000000000000000002")), + to: address!("0000000000000000000000000000000000000002"), value: U256::from(100u64), input: Bytes::new(), + sender: address!("0000000000000000000000000000000000000001"), }; let mut buf = Vec::new(); @@ -515,12 +486,11 @@ mod tests { fn test_l1_transaction_decode_rejects_malformed_rlp() { let tx = TxL1Msg { queue_index: 0, - from: address!("0000000000000000000000000000000000000001"), - nonce: 42, gas_limit: 21_000, - to: TxKind::Call(address!("0000000000000000000000000000000000000002")), - value: U256::from(1_000_000_000_000_000_000u128), + to: address!("0000000000000000000000000000000000000002"), + value: U256::from(1_000_000_000_000_000_000u64), input: Bytes::from(vec![0x12, 0x34]), + sender: address!("0000000000000000000000000000000000000001"), }; // Encode the transaction @@ -546,21 +516,20 @@ mod tests { fn test_l1_transaction_size() { let tx = TxL1Msg { queue_index: 0, - from: Address::ZERO, - nonce: 0, gas_limit: 0, - to: TxKind::Create, + to: Address::ZERO, value: U256::ZERO, input: Bytes::new(), + sender: Address::ZERO, }; // Calculate expected size manually let expected_size = mem::size_of::() + // queue_index - mem::size_of::
() + // from - mem::size_of::() + // nonce - mem::size_of::() + // gas_limit - mem::size_of::() + // to - mem::size_of::(); // value (empty input) + mem::size_of::() + // gas_limit + mem::size_of::
() + // to + mem::size_of::() + // value + mem::size_of::
(); // sender + // Note: input is empty so contributes 0 bytes assert_eq!(tx.size(), expected_size); } @@ -569,12 +538,11 @@ mod tests { fn test_l1_transaction_fields_len() { let tx = TxL1Msg { queue_index: 0, - from: address!("0000000000000000000000000000000000000001"), - nonce: 1, gas_limit: 21_000, - to: TxKind::Call(address!("0000000000000000000000000000000000000002")), + to: address!("0000000000000000000000000000000000000002"), value: U256::from(100u64), input: Bytes::from(vec![1, 2, 3, 4]), + sender: address!("0000000000000000000000000000000000000001"), }; let fields_len = tx.fields_len(); @@ -589,12 +557,11 @@ mod tests { fn test_l1_transaction_encode_fields() { let tx = TxL1Msg { queue_index: 0, - from: address!("0000000000000000000000000000000000000001"), - nonce: 1, gas_limit: 21_000, - to: TxKind::Call(address!("0000000000000000000000000000000000000002")), + to: address!("0000000000000000000000000000000000000002"), value: U256::from(100u64), input: Bytes::new(), + sender: address!("0000000000000000000000000000000000000001"), }; let mut buf = Vec::new(); diff --git a/crates/revm/src/l1block.rs b/crates/revm/src/l1block.rs index 64f7244..d570862 100644 --- a/crates/revm/src/l1block.rs +++ b/crates/revm/src/l1block.rs @@ -1,12 +1,34 @@ -//! L1 Block Info for Morph L2 fee calculation. +//! L1 Block Info and Gas Price Oracle constants for Morph L2. //! -//! This module provides the infrastructure for calculating L1 data fees on Morph L2. -//! The fee parameters are read from the L1 Gas Price Oracle contract deployed on L2. +//! This module provides: +//! - [`L1BlockInfo`]: L1 gas price oracle data for L1 data fee calculation +//! - Gas Price Oracle contract address and storage slot constants +//! +//! # Storage Layout +//! +//! The L1 Gas Price Oracle contract has the following storage layout: +//! +//! | Name | Type | Slot | Description | +//! |---------------|---------|------|--------------------------------| +//! | owner | address | 0 | Contract owner | +//! | l1BaseFee | uint256 | 1 | L1 base fee | +//! | overhead | uint256 | 2 | L1 overhead gas | +//! | scalar | uint256 | 3 | L1 fee scalar | +//! | whitelist | address | 4 | Whitelist contract | +//! | __deprecated0 | uint256 | 5 | Deprecated | +//! | l1BlobBaseFee | uint256 | 6 | L1 blob base fee (Curie+) | +//! | commitScalar | uint256 | 7 | Commit scalar (Curie+) | +//! | blobScalar | uint256 | 8 | Blob scalar (Curie+) | +//! | isCurie | bool | 9 | Curie hardfork flag | use alloy_primitives::{Address, U256, address}; use morph_chainspec::hardfork::MorphHardfork; use revm::Database; +// ============================================================================= +// Gas Price Oracle Constants +// ============================================================================= + /// Gas cost for zero bytes in calldata. const ZERO_BYTE_COST: u64 = 4; /// Gas cost for non-zero bytes in calldata. @@ -17,22 +39,87 @@ const TX_L1_COMMIT_EXTRA_COST: U256 = U256::from_limbs([64u64, 0, 0, 0]); /// Precision factor for L1 fee calculation (1e9). const TX_L1_FEE_PRECISION: U256 = U256::from_limbs([1_000_000_000u64, 0, 0, 0]); +// ============================================================================= +// L1 Gas Price Oracle Address +// ============================================================================= + /// L1 Gas Price Oracle contract address on Morph L2. +/// +/// This contract stores L1 gas prices used for calculating L1 data fees. +/// Reference: `rollup/rcfg/config.go` in go-ethereum pub const L1_GAS_PRICE_ORACLE_ADDRESS: Address = address!("530000000000000000000000000000000000000F"); -/// Storage slot for L1 base fee. -const L1_BASE_FEE_SLOT: U256 = U256::from_limbs([1u64, 0, 0, 0]); -/// Storage slot for L1 overhead. -const L1_OVERHEAD_SLOT: U256 = U256::from_limbs([2u64, 0, 0, 0]); -/// Storage slot for L1 scalar. -const L1_SCALAR_SLOT: U256 = U256::from_limbs([3u64, 0, 0, 0]); -/// Storage slot for L1 blob base fee (Curie+). -const L1_BLOB_BASE_FEE_SLOT: U256 = U256::from_limbs([6u64, 0, 0, 0]); -/// Storage slot for L1 commit scalar (Curie+). -const L1_COMMIT_SCALAR_SLOT: U256 = U256::from_limbs([7u64, 0, 0, 0]); -/// Storage slot for L1 blob scalar (Curie+). -const L1_BLOB_SCALAR_SLOT: U256 = U256::from_limbs([8u64, 0, 0, 0]); +// ============================================================================= +// L1 Gas Price Oracle Storage Slots +// ============================================================================= + +/// Storage slot for `owner` in the `L1GasPriceOracle` contract. +pub const GPO_OWNER_SLOT: U256 = U256::from_limbs([0, 0, 0, 0]); + +/// Storage slot for `l1BaseFee` in the `L1GasPriceOracle` contract. +pub const GPO_L1_BASE_FEE_SLOT: U256 = U256::from_limbs([1, 0, 0, 0]); + +/// Storage slot for `overhead` in the `L1GasPriceOracle` contract. +pub const GPO_OVERHEAD_SLOT: U256 = U256::from_limbs([2, 0, 0, 0]); + +/// Storage slot for `scalar` in the `L1GasPriceOracle` contract. +pub const GPO_SCALAR_SLOT: U256 = U256::from_limbs([3, 0, 0, 0]); + +/// Storage slot for `whitelist` in the `L1GasPriceOracle` contract. +pub const GPO_WHITELIST_SLOT: U256 = U256::from_limbs([4, 0, 0, 0]); + +/// Storage slot for `l1BlobBaseFee` in the `L1GasPriceOracle` contract. +/// Added in the Curie hardfork. +pub const GPO_L1_BLOB_BASE_FEE_SLOT: U256 = U256::from_limbs([6, 0, 0, 0]); + +/// Storage slot for `commitScalar` in the `L1GasPriceOracle` contract. +/// Added in the Curie hardfork. +pub const GPO_COMMIT_SCALAR_SLOT: U256 = U256::from_limbs([7, 0, 0, 0]); + +/// Storage slot for `blobScalar` in the `L1GasPriceOracle` contract. +/// Added in the Curie hardfork. +pub const GPO_BLOB_SCALAR_SLOT: U256 = U256::from_limbs([8, 0, 0, 0]); + +/// Storage slot for `isCurie` in the `L1GasPriceOracle` contract. +/// Added in the Curie hardfork. Set to 1 (true) after Curie activation. +pub const GPO_IS_CURIE_SLOT: U256 = U256::from_limbs([9, 0, 0, 0]); + +// ============================================================================= +// L1 Gas Price Oracle Initial Values (for Curie hardfork) +// ============================================================================= + +/// The initial blob base fee used by the oracle contract at Curie activation. +pub const INITIAL_L1_BLOB_BASE_FEE: U256 = U256::from_limbs([1, 0, 0, 0]); + +/// The initial commit scalar used by the oracle contract at Curie activation. +/// Reference: `rcfg.InitialCommitScalar` in go-ethereum (230759955285) +pub const INITIAL_COMMIT_SCALAR: U256 = U256::from_limbs([230759955285, 0, 0, 0]); + +/// The initial blob scalar used by the oracle contract at Curie activation. +/// Reference: `rcfg.InitialBlobScalar` in go-ethereum (417565260) +pub const INITIAL_BLOB_SCALAR: U256 = U256::from_limbs([417565260, 0, 0, 0]); + +/// Curie hardfork flag value (1 = true). +pub const IS_CURIE: U256 = U256::from_limbs([1, 0, 0, 0]); + +/// Storage updates for L1 gas price oracle Curie hardfork initialization. +/// +/// These storage slots are initialized when the Curie hardfork activates: +/// - l1BlobBaseFee = 1 +/// - commitScalar = InitialCommitScalar +/// - blobScalar = InitialBlobScalar +/// - isCurie = 1 (true) +pub const CURIE_L1_GAS_PRICE_ORACLE_STORAGE: [(U256, U256); 4] = [ + (GPO_L1_BLOB_BASE_FEE_SLOT, INITIAL_L1_BLOB_BASE_FEE), + (GPO_COMMIT_SCALAR_SLOT, INITIAL_COMMIT_SCALAR), + (GPO_BLOB_SCALAR_SLOT, INITIAL_BLOB_SCALAR), + (GPO_IS_CURIE_SLOT, IS_CURIE), +]; + +// ============================================================================= +// L1 Block Info +// ============================================================================= /// L1 block info for fee calculation. /// @@ -65,9 +152,9 @@ impl L1BlockInfo { db: &mut DB, hardfork: MorphHardfork, ) -> Result { - let l1_base_fee = db.storage(L1_GAS_PRICE_ORACLE_ADDRESS, L1_BASE_FEE_SLOT)?; - let l1_fee_overhead = db.storage(L1_GAS_PRICE_ORACLE_ADDRESS, L1_OVERHEAD_SLOT)?; - let l1_base_fee_scalar = db.storage(L1_GAS_PRICE_ORACLE_ADDRESS, L1_SCALAR_SLOT)?; + let l1_base_fee = db.storage(L1_GAS_PRICE_ORACLE_ADDRESS, GPO_L1_BASE_FEE_SLOT)?; + let l1_fee_overhead = db.storage(L1_GAS_PRICE_ORACLE_ADDRESS, GPO_OVERHEAD_SLOT)?; + let l1_base_fee_scalar = db.storage(L1_GAS_PRICE_ORACLE_ADDRESS, GPO_SCALAR_SLOT)?; if !hardfork.is_curie() { Ok(Self { @@ -78,10 +165,10 @@ impl L1BlockInfo { }) } else { let l1_blob_base_fee = - db.storage(L1_GAS_PRICE_ORACLE_ADDRESS, L1_BLOB_BASE_FEE_SLOT)?; + db.storage(L1_GAS_PRICE_ORACLE_ADDRESS, GPO_L1_BLOB_BASE_FEE_SLOT)?; let l1_commit_scalar = - db.storage(L1_GAS_PRICE_ORACLE_ADDRESS, L1_COMMIT_SCALAR_SLOT)?; - let l1_blob_scalar = db.storage(L1_GAS_PRICE_ORACLE_ADDRESS, L1_BLOB_SCALAR_SLOT)?; + db.storage(L1_GAS_PRICE_ORACLE_ADDRESS, GPO_COMMIT_SCALAR_SLOT)?; + let l1_blob_scalar = db.storage(L1_GAS_PRICE_ORACLE_ADDRESS, GPO_BLOB_SCALAR_SLOT)?; // calldata component of commit fees (calldata gas + execution) let calldata_gas = l1_commit_scalar.saturating_mul(l1_base_fee); diff --git a/crates/revm/src/lib.rs b/crates/revm/src/lib.rs index 7a37e50..94635b0 100644 --- a/crates/revm/src/lib.rs +++ b/crates/revm/src/lib.rs @@ -56,7 +56,26 @@ mod tx; pub use block::MorphBlockEnv; pub use error::{MorphHaltReason, MorphInvalidTransaction}; pub use evm::MorphEvm; -pub use l1block::{L1_GAS_PRICE_ORACLE_ADDRESS, L1BlockInfo}; +pub use l1block::{ + CURIE_L1_GAS_PRICE_ORACLE_STORAGE, + GPO_BLOB_SCALAR_SLOT, + GPO_COMMIT_SCALAR_SLOT, + GPO_IS_CURIE_SLOT, + GPO_L1_BASE_FEE_SLOT, + GPO_L1_BLOB_BASE_FEE_SLOT, + GPO_OVERHEAD_SLOT, + // Storage slots + GPO_OWNER_SLOT, + GPO_SCALAR_SLOT, + GPO_WHITELIST_SLOT, + INITIAL_BLOB_SCALAR, + INITIAL_COMMIT_SCALAR, + // Curie initial values + INITIAL_L1_BLOB_BASE_FEE, + IS_CURIE, + L1_GAS_PRICE_ORACLE_ADDRESS, + L1BlockInfo, +}; pub use precompiles::MorphPrecompiles; pub use token_fee::{L2_TOKEN_REGISTRY_ADDRESS, TokenFeeInfo, get_erc20_balance_with_evm}; pub use tx::{MorphTxEnv, MorphTxExt};