Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .config/nextest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ fail-fast = false
slow-timeout = { period = "30s", terminate-after = 4 }

# E2E integration tests spawn full nodes — each needs exclusive MDBX resources.
# threads-required = 2 means nextest counts each as needing 2 of the test-threads
# slots, so only 1 runs at a time on CI (2 slots / 2 required = 1 concurrent).
# threads-required = 2 means nextest counts each as needing 2 of the `num-cpus`
# test-thread slots, halving e2e concurrency (2 at a time on the 4-vCPU
# `ubuntu-latest` runner).
[[profile.ci.overrides]]
filter = "package(morph-node) & binary(it)"
threads-required = 2
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,9 @@ jobs:
- name: Resolve build profile
id: profile
run: |
# Tag push -> profiling (matches Dockerfile + EC2 deploy defaults;
# see PR rationale: maxperf regresses ERC20 long tail).
# Tag push -> profiling (matches the Dockerfile default; maxperf
# regresses the ERC20 long tail, see #104). EC2 deploys build
# separately via MakefileEc2.mk with the `reproducible` profile.
# workflow_dispatch -> user-selected profile (defaults to profiling,
# `maxperf` available for eth-heavy reference builds).
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
Expand Down
3 changes: 2 additions & 1 deletion crates/chainspec/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ pub const MORPH_MAINNET_CHAIN_ID: u64 = 2818;
pub const MORPH_HOODI_CHAIN_ID: u64 = 2910;

/// The default L2 sequencer fee (0.001 Gwei = 1_000_000 wei).
/// The sequencer has the right to set any base fee below `MORPH_MAX_BASE_FEE`.
/// The sequencer has the right to set any base fee up to `MORPH_MAXIMUM_BASE_FEE`
/// (enforced by `morph-consensus` header validation).
pub const MORPH_BASE_FEE: u64 = 1_000_000;

/// Maximum L2 transaction payload bytes per block (L1 messages excluded).
Expand Down
20 changes: 11 additions & 9 deletions crates/chainspec/src/hardfork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,20 @@
//! 4. Update `morph_hardfork_at()` to check for the new hardfork first (latest hardfork is checked first)
//! 5. Add `MorphHardfork::Vivace => Self::OSAKA` (or appropriate SpecId) in `From<MorphHardfork> for SpecId`
//! 6. Update `From<SpecId> for MorphHardfork` to check for the new hardfork first
//! 7. Add test `test_is_vivace` and update existing `is_*` tests to include the new variant
//! 7. Add the new variant to the `SpecId` mapping tests and to the fork list in
//! `test_morph_hardforks_do_not_enable_amsterdam_state_gas`
//!
//! ### In `genesis.rs`:
//! 8. Add `vivace_time: Option<u64>` field to `MorphHardforkInfo`, named so its camelCase key
//! matches morph-geth's genesis JSON key (e.g. `jade_fork_time` for `jadeForkTime`)
//!
//! ### In `spec.rs`:
//! 8. Add `vivace_time: Option<u64>` field to `MorphGenesisInfo`
//! 9. Extract `vivace_time` in `From<Genesis> for MorphChainSpec`
//! 10. Add `(MorphHardfork::Vivace, vivace_time)` to `morph_forks` vec
//! 11. Update tests to include `"vivaceTime": <timestamp>` in genesis JSON
//! 9. Add `(MorphHardfork::Vivace, hardfork_info.vivace_time)` to `time_forks` in
//! `build_hardforks`
//! 10. Update tests to include `"vivaceTime": <timestamp>` in genesis JSON
//!
//! ### In genesis files and generator:
//! 12. Add `"vivaceTime": 0` to `genesis/dev.json`
//! 13. Add `vivace_time: Option<u64>` arg to `xtask/src/genesis_args.rs`
//! 14. Add insertion of `"vivaceTime"` to chain_config.extra_fields
//! ### In genesis files:
//! 11. Add the activation key to `res/genesis/{mainnet,hoodi}.json` when the fork is scheduled
//!
//! ## Current State
//!
Expand Down
4 changes: 2 additions & 2 deletions crates/chainspec/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//! - [`MorphChainSpec`]: The main chain specification type that wraps reth's `ChainSpec`
//! with Morph-specific configuration.
//! - [`hardfork::MorphHardfork`]: Morph-specific hardfork definitions (Bernoulli, Curie, Morph203, etc.)
//! - [`MorphChainConfig`]: Morph L2-specific chain configuration (fee vault, max tx size, etc.)
//! - [`MorphChainConfig`]: Morph L2-specific chain configuration (fee vault address)
//!
//! # Supported Networks
//!
Expand All @@ -16,7 +16,7 @@
//!
//! Morph hardforks use two activation mechanisms:
//! - **Block-based**: Bernoulli, Curie (activated at specific block numbers)
//! - **Timestamp-based**: Morph203, Viridian, Emerald (activated at specific timestamps)
//! - **Timestamp-based**: Morph203, Viridian, Emerald, Jade (activated at specific timestamps)
//!
//! # Example
//!
Expand Down
4 changes: 2 additions & 2 deletions crates/consensus/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ use alloy_primitives::Address;
/// in the standard reth `ConsensusError`.
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
pub enum MorphConsensusError {
/// Invalid L1 message order - either L1 messages are not at the start of the block
/// or queue indices are not strictly sequential.
/// Invalid L1 message order - an L1 message appears after an L2 transaction
/// (L1 messages must be at the start of the block).
#[error("Invalid L1 message order")]
InvalidL1MessageOrder,

Expand Down
2 changes: 1 addition & 1 deletion crates/consensus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
//!
//! 1. All L1 messages must be at the beginning of the block
//! 2. L1 messages must be in ascending `queue_index` order
//! 3. No gaps in the `queue_index` sequence
//! 3. No gaps in the `queue_index` sequence within a block
//!
//! # Example
//!
Expand Down
34 changes: 20 additions & 14 deletions crates/consensus/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
//! - Transaction root must be valid
//! - L2 transaction payload (EIP-2718 encoded, L1 messages excluded) must not
//! exceed [`morph_chainspec::MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK`]
//! - MorphTx (0x7F) must be active (Emerald), use an active version (V1 needs Jade),
//! and pass field validation
//!
//! ## Post-Execution Validation
//!
Expand Down Expand Up @@ -85,7 +87,7 @@ const GAS_LIMIT_BOUND_DIVISOR: u64 = 1024;
/// L1 message ordering requires both body data (transactions) and parent header data.
/// Since reth's `Consensus` trait methods provide these separately — `validate_block_pre_execution`
/// has the block body but not the parent header, while `validate_header_against_parent` has
/// both headers but not the body — the validation is split into two independent checks:
/// both headers but not the body — the validation is split into three independent checks:
///
/// 1. **Internal consistency** (`validate_block_pre_execution`): L1 messages are at the block
/// start, have sequential queue indices, and are consistent with `header.next_l1_msg_index`.
Expand All @@ -96,10 +98,10 @@ const GAS_LIMIT_BOUND_DIVISOR: u64 = 1024;
/// from `parent.next_l1_msg_index` and the block's leading L1 messages.
///
/// The consensus trait methods have no ordering dependency and share no mutable state. The strict
/// cross-block equality check (`header.next == parent.next + l1_count`) requires simultaneous
/// access to both parent header and block body, which reth's trait API does not provide in
/// any single method, so Morph performs that final check in the engine tree payload validator
/// before a block is accepted.
/// cross-block equality check (`header.next` equals `parent.next` advanced past the block's
/// leading L1 messages) requires simultaneous access to both parent header and block body,
/// which reth's trait API does not provide in any single method, so Morph performs that final
/// check in the engine tree payload validator before a block is accepted.
#[derive(Debug, Clone)]
pub struct MorphConsensus {
/// Chain specification containing hardfork information and chain config.
Expand Down Expand Up @@ -214,8 +216,9 @@ impl HeaderValidator<MorphHeader> for MorphConsensus {
///
/// 1. **Parent Hash**: Header's parent_hash must match parent's hash
/// 2. **Block Number**: Header's number must be parent's number + 1
/// 3. **Timestamp**: Header's timestamp must be >= parent's timestamp
/// 3. **Timestamp**: Header's timestamp must be > parent's timestamp (>= from Emerald onward)
/// 4. **Gas Limit**: Change must be within 1/1024 of parent's limit
/// 5. **L1 Message Index**: `next_l1_msg_index` must not decrease relative to the parent
fn validate_header_against_parent(
&self,
header: &SealedHeader<MorphHeader>,
Expand Down Expand Up @@ -275,7 +278,8 @@ impl Consensus<Block> for MorphConsensus {
/// 4. **Withdrawals**: Must be empty (Morph L2 doesn't support withdrawals)
/// 5. **L2 Payload Size**: Encoded L2 txs (L1 messages excluded) must not
/// exceed [`MORPH_MAX_TX_PAYLOAD_BYTES_PER_BLOCK`]
/// 6. **L1 Messages**: Must be ordered correctly (sequential queue indices, L1 before L2)
/// 6. **MorphTx**: Type active (Emerald), version active (V1 needs Jade), fields valid
/// 7. **L1 Messages**: Must be ordered correctly (sequential queue indices, L1 before L2)
fn validate_block_pre_execution(
&self,
block: &SealedBlock<Block>,
Expand Down Expand Up @@ -689,12 +693,8 @@ fn validate_morph_txs(
// Receipts Validation
// ============================================================================

/// Verifies the receipts root and logs bloom against the expected values.
///
/// This function:
/// 1. Calculates the receipts root from the provided receipts
/// 2. Calculates the logs bloom by combining all receipt blooms
/// 3. Compares both against the expected values from the block header
/// Compares a receipts root and logs bloom pre-computed by the executor against the
/// expected values from the block header.
#[inline]
fn verify_receipts_precomputed(
expected_receipts_root: B256,
Expand Down Expand Up @@ -723,6 +723,12 @@ fn verify_receipts_precomputed(
Ok(())
}

/// Verifies the receipts root and logs bloom against the expected values.
///
/// This function:
/// 1. Calculates the receipts root from the provided receipts
/// 2. Calculates the logs bloom by combining all receipt blooms
/// 3. Compares both against the expected values from the block header
fn verify_receipts(
expected_receipts_root: B256,
expected_logs_bloom: Bloom,
Expand Down Expand Up @@ -1144,7 +1150,7 @@ mod tests {
create_regular_tx(),
];

// Header says 2 but should be 3 (last=2, 2+1=3). Value < min_expected triggers error.
// Header says 2 but should be 3 (last=2, 2+1=3). Value != expected triggers error.
let result = validate_l1_messages_in_block(&txs, 2, true);
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();
Expand Down
8 changes: 5 additions & 3 deletions crates/engine-api/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,20 @@ use morph_primitives::MorphHeader;
/// and provides the following methods:
///
/// - `assemble_l2_block`: Build a new L2 block with the given transactions
/// - `assemble_l2_block_v2`: Build a new L2 block on an explicitly given parent hash
/// - `validate_l2_block`: Validate an L2 block without importing it
/// - `new_l2_block`: Import and finalize a new L2 block
/// - `new_l2_block_v2`: Import a new L2 block onto the parent selected by hash (may reorg)
/// - `new_safe_l2_block`: Import a safe L2 block from derivation
/// - `set_block_tags`: Update safe/finalized block tags without importing a block
#[async_trait::async_trait]
#[auto_impl::auto_impl(Arc, &, Box)]
pub trait MorphL2EngineApi: Send + Sync {
/// Build a new L2 block with the given transactions.
///
/// This method is called by the sequencer to assemble a new block containing
/// the provided transactions. The transactions should include L1 messages
/// at the beginning, followed by L2 transactions.
/// This method is called by the sequencer to assemble a new block. The provided
/// transactions are the L1 messages to execute first; L2 transactions are then
/// packed from the local txpool.
///
/// # Arguments
///
Expand Down
5 changes: 2 additions & 3 deletions crates/engine-api/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -826,8 +826,7 @@ impl<Provider> RealMorphL2EngineApi<Provider> {
// FCU safe/finalized must be canonical ancestors. Unsafe imports pass safe zero;
// new_safe_l2_block passes the imported block itself, never a cached old safe.
// Forward only the L1-derived finalized tag; zero is a no-op when it is absent,
// and pinned reth v2.2.0 still cleans changesets/canonical memory without
// finalized.
// and reth still cleans changesets/canonical memory without finalized.
let forkchoice = alloy_rpc_types_engine::ForkchoiceState {
head_block_hash: data.hash,
safe_block_hash,
Expand Down Expand Up @@ -892,7 +891,7 @@ impl<Provider> RealMorphL2EngineApi<Provider> {

let logs_bloom = alloy_primitives::Bloom::from_slice(data.logs_bloom.as_ref());
// Override coinbase to empty address when FeeVault is enabled,
// matching go-ethereum's executableDataToBlock (l2_api.go:292-293).
// matching go-ethereum's executableDataToBlock (l2_api.go).
let beneficiary = if self.chain_spec.is_fee_vault_enabled() {
Address::ZERO
} else {
Expand Down
3 changes: 3 additions & 0 deletions crates/engine-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
//! standard Ethereum Engine API:
//!
//! - `engine_assembleL2Block`: Build a new block with given transactions
//! - `engine_assembleL2BlockV2`: Build a new block on an explicitly given parent hash
//! - `engine_validateL2Block`: Validate a block without importing
//! - `engine_newL2Block`: Import and finalize a block
//! - `engine_newL2BlockV2`: Import a block onto the parent selected by hash (may reorg)
//! - `engine_newSafeL2Block`: Import a safe block from derivation
//! - `engine_setBlockTags`: Update safe/finalized block tags without importing a block

#![cfg_attr(not(test), warn(unused_crate_dependencies))]
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
Expand Down
13 changes: 8 additions & 5 deletions crates/engine-api/src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,28 @@ pub(crate) struct MorphEngineApiMetrics {
// -------------------------------------------------------------------------
// assembleL2Block
// -------------------------------------------------------------------------
/// Latency for `engine_assembleL2Block` calls.
/// Latency for `engine_assembleL2Block` and `engine_assembleL2BlockV2` calls.
pub(crate) assemble_l2_block_duration_seconds: Histogram,
/// Number of `engine_assembleL2Block` calls that returned an error.
/// Number of `engine_assembleL2Block`/`engine_assembleL2BlockV2` calls whose payload build
/// failed.
pub(crate) assemble_l2_block_failures_total: Counter,

// -------------------------------------------------------------------------
// newL2Block
// -------------------------------------------------------------------------
/// Latency for `engine_newL2Block` calls.
/// Latency for `engine_newL2Block` and `engine_newL2BlockV2` calls.
pub(crate) new_l2_block_duration_seconds: Histogram,
/// Number of `engine_newL2Block` calls that returned an error.
/// Number of `engine_newL2Block`/`engine_newL2BlockV2` calls rejected for a discontinuous
/// block number or parent hash mismatch, or whose engine import failed.
pub(crate) new_l2_block_failures_total: Counter,

// -------------------------------------------------------------------------
// validateL2Block
// -------------------------------------------------------------------------
/// Latency for `engine_validateL2Block` calls.
pub(crate) validate_l2_block_duration_seconds: Histogram,
/// Number of `engine_validateL2Block` calls that returned `success: false`.
/// Number of `engine_validateL2Block` calls rejected for a discontinuous block number or
/// parent hash mismatch, or that returned `success: false`.
pub(crate) validate_l2_block_failures_total: Counter,

// -------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion crates/evm/src/block/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ impl TxResult for MorphTxResult {
/// ## Execution Flow
/// 1. `apply_pre_execution_changes`: Set up state and load contracts
/// 2. `execute_transaction_without_commit`: Execute transaction in EVM
/// 3. `commit_transaction`: Calculate fees, build receipt, commit state
/// 3. `commit_transaction`: Build receipt from the cached fee info, commit state
/// 4. `finish`: Return final execution result with all receipts
pub struct MorphBlockExecutor<DB: Database, I> {
/// The EVM used by executor (owned, not a reference)
Expand Down
6 changes: 3 additions & 3 deletions crates/evm/src/block/receipt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,13 @@ pub(crate) struct MorphReceiptBuilderCtx<'a, E: Evm> {
///
/// # Token Fee Calculation Formula
/// ```text
/// token_fee = eth_fee * fee_rate / token_scale
/// token_fee = eth_fee * token_scale / fee_rate (rounded up)
/// ```
///
/// # Fields
/// - `version`: The version of the Morph transaction format (0 = legacy, 1 = with reference/memo)
/// - `fee_token_id`: ID of the ERC20 token registered in L2TokenRegistry
/// - `fee_rate`: Exchange rate from L2TokenRegistry (token per ETH)
/// - `fee_rate`: Price ratio from L2TokenRegistry (token price relative to ETH)
/// - `token_scale`: Decimal scale factor for the token (e.g., 10^18)
/// - `fee_limit`: Maximum tokens the user agreed to pay
/// - `reference`: 32-byte key for transaction indexing by external systems
Expand Down Expand Up @@ -203,7 +203,7 @@ impl MorphReceiptBuilder for DefaultMorphReceiptBuilder {
// MorphTx transactions should always have MorphTx-specific fields.
// If fields are missing, it indicates one of the following:
// 1. The fee token is not registered in L2TokenRegistry
// 2. TokenFeeInfo::fetch returned None (token inactive or query failed)
// 2. The token registry lookup failed (logged by commit_transaction)
// 3. A bug in get_morph_tx_fields logic
//
// We log a warning and fallback to L1-fee-only receipt to avoid
Expand Down
2 changes: 1 addition & 1 deletion crates/evm/src/evm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ impl EvmFactory for MorphEvmFactory {
///
/// This is a wrapper type around the `revm` ethereum evm with optional [`Inspector`] (tracing)
/// support. [`Inspector`] support is configurable at runtime because it's part of the underlying
/// `RevmEvm` type.
/// `morph_revm::MorphEvm` type.
#[expect(missing_debug_implementations)]
pub struct MorphEvm<DB: Database, I = NoOpInspector> {
inner: morph_revm::MorphEvm<DB, I>,
Expand Down
4 changes: 0 additions & 4 deletions crates/evm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
//! │ │ - Calculates L1 data fee for all L2 transactions │ │
//! │ │ - Extracts token fee info for MorphTx (0x7F) │ │
//! │ │ - Builds receipts with full Morph-specific context │ │
//! │ │ - Applies hardfork state changes (Curie, etc.) │ │
//! │ └─────────────────────────────────────────────────────────┘ │
//! └─────────────────────────────────────────────────────────────────┘
//! ```
Expand Down Expand Up @@ -100,9 +99,6 @@ pub use morph_revm::{MorphBlockEnv, MorphHaltReason};
/// - Block executor creation with Morph-specific execution logic
/// - Block assembler for constructing `MorphHeader` blocks
///
/// # Usage
///
/// Create with a chain specification:
/// # Trait Implementations
///
/// - `ConfigureEvm`: Provides EVM environment setup and block context creation
Expand Down
3 changes: 2 additions & 1 deletion crates/node/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ pub const MORPH_DEFAULT_MAX_TX_PAYLOAD_BYTES: u64 = MORPH_MAX_TX_PAYLOAD_BYTES_P
/// budget, and `--morph.max-tx-payload-bytes` (the uncompressed L2 payload
/// that must fit in one 6-blob batch).
///
/// Note: Block building deadline is configured via reth's built-in `--builder.deadline` flag.
/// Note: reth's `--builder.deadline` only bounds the payload job as a whole; the per-build
/// packing time budget is `MorphBuilderConfig::time_limit` (1s), which has no CLI flag.
#[derive(Debug, Clone, Args)]
#[command(next_help_heading = "Morph")]
pub struct MorphArgs {
Expand Down
Loading
Loading