Skip to content

Conversation

@panos-xyz
Copy link
Contributor

@panos-xyz panos-xyz commented Feb 12, 2026

Summary by CodeRabbit

  • New Features

    • New morph-reth CLI binary and comprehensive Morph node surface (node, builders, add‑ons, validators, engine API) with CLI options for max payload bytes and optional max transactions per block.
  • Improvements

    • On‑demand L1 fee estimation and refined transaction encoding for RPC calls.
    • Enhanced transaction pool and payload builder enforcing per‑block tx limits.
    • New Engine API implementations with RPC integration for Morph L2 flows.
  • Documentation

    • Updated crate docs and examples.
  • Chores

    • .gitignore restored /docs and now ignores local-test/.

@coderabbitai
Copy link

coderabbitai bot commented Feb 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new morph-reth binary and wires Morph L2 functionality across the workspace: workspace/member and dependency changes, node types and builders, RPC/Engine add-ons and RealMorphL2EngineApi, payload builder limits and breaker logic, EVM/engine wiring, DB codec impls, and L1-fee/RLP encoding updates.

Changes

Cohort / File(s) Summary
Workspace & Gitignore
./.gitignore, Cargo.toml
New workspace member bin/morph-reth, added workspace deps (dashmap, rayon, reth-rpc-api git), and ignored local-test/; preserved /docs.
Morph binary
bin/morph-reth/Cargo.toml, bin/morph-reth/src/main.rs
Added morph-reth binary crate and CLI entrypoint wiring MorphNode, handlers, and workspace-driven dependencies.
Node crate: manifests & exports
crates/node/Cargo.toml, crates/node/src/lib.rs
Added many workspace dependencies and new public modules/re-exports (add_ons, args, components, node, validator, rpc, payload types).
Node core & payload attributes
crates/node/src/node.rs, crates/payload/types/src/attributes.rs
Introduced MorphNode, MorphPayloadAttributesBuilder, Node/DebugNode impls; gated serde flatten on MorphPayloadAttributes.inner.
Node components (builders)
crates/node/src/components/..., crates/node/src/components/mod.rs
Added builders: MorphConsensusBuilder, MorphExecutorBuilder, MorphPayloadBuilderBuilder, MorphPoolBuilder; re-exports; pool integrates Morph tx handling and maintenance tasks.
Node add-ons & args
crates/node/src/add_ons.rs, crates/node/src/args.rs
Added MorphAddOns (RPC & Engine API registration with RealMorphL2EngineApi wiring) and MorphArgs (max tx payload bytes, optional max tx per block).
Engine API & validator
crates/engine-api/..., crates/node/src/validator.rs
Added RealMorphL2EngineApi, Stub/Builder, RPC conversion, and MorphEngineValidator + builder (payload→block conversion and timestamp validation).
EVM integration
crates/evm/src/engine.rs, crates/evm/src/lib.rs, crates/evm/src/config.rs
New engine module implementing ConfigureEngineEvm for MorphExecutionData; refactored EVM env construction including blob placeholders.
Payload builder: config & execution
crates/payload/builder/src/config.rs, crates/payload/builder/src/builder.rs
Added max_tx_per_block to MorphBuilderConfig and breaker; track transaction_count in ExecutionInfo and include in breaker checks/logging.
Primitives DB impl
crates/primitives/src/receipt/mod.rs
Feature-gated Compress/Decompress impls for MorphReceipt using reth_codecs::Compact under reth-codec.
RPC: L1-fee & tx encoding
crates/rpc/src/eth/call.rs, crates/rpc/src/eth/transaction.rs
Deferred/on-demand RLP generation with mock signature for eth_call/eth_estimateGas; added envelope builder build_ethereum_tx_from_env and updated encode_tx_for_l1_fee flow/signature.
Chainspec & constants
crates/chainspec/src/..., crates/chainspec/Cargo.toml
Added chain constants and helper functions for genesis header/hardforks; enabled features on workspace deps and re-exported MorphChainSpecParser under cli.
Misc docs/tests
crates/consensus/src/lib.rs, crates/payload/builder/src/lib.rs
Minor documentation/example tweaks and small test updates.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as "bin/morph-reth (CLI)"
    participant Builder as "ComponentsBuilder"
    participant Components as "Components (Consensus, Executor, Payload, Pool)"
    participant AddOns as "MorphAddOns / RealMorphL2EngineApi"
    participant Node as "MorphNode (running)"
    CLI->>Builder: build components_builder(args, config)
    Builder->>Components: construct consensus, executor, payload, pool
    Components-->>Builder: components ready
    Builder->>AddOns: register RPC & Engine APIs (RealMorphL2EngineApi)
    AddOns-->>Builder: add-ons registered
    Builder->>Node: assemble and launch node
    Node->>AddOns: start RPC & Engine servers (handle requests)
    AddOns->>Node: assemble/validate/submit L2 blocks (calls into payload_builder & provider)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

Suggested reviewers

  • anylots
  • chengwenxi

Poem

🐰 In workspace soil a binary grows,
Builders hum where payloads flow,
EVM hops and RPCs sing—
Engines stitch the blocks we bring,
Carrot cheers for morph's new show 🥕

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of adding a morph-reth binary and node wiring infrastructure, which aligns with the extensive changes throughout the codebase for node integration.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/morph-node

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/payload/types/src/attributes.rs (1)

252-264: ⚠️ Potential issue | 🟡 Minor

Test doesn't isolate what it claims to verify.

create_test_attributes() uses B256::random() and Address::random(), so attrs1 and attrs2 already have different prev_randao and suggested_fee_recipient. The IDs would differ regardless of the transactions field, making this test unable to catch a regression where transactions are accidentally excluded from the payload ID computation.

Suggested fix: share the same base attributes
     fn test_payload_id_different_with_transactions() {
         let parent = B256::random();
-        let attrs1 = create_test_attributes();
-        let mut attrs2 = create_test_attributes();
+        let attrs1 = create_test_attributes();
+        let mut attrs2 = attrs1.clone();
         attrs2.transactions = Some(vec![Bytes::from(vec![0x01])]);
crates/rpc/src/eth/transaction.rs (1)

169-172: ⚠️ Potential issue | 🟡 Minor

Fix redundant clone flagged by CI.

inner is not used after this call, so the .clone() is unnecessary — consume inner directly.

Proposed fix
-        let inner_tx_env = inner
-            .clone()
-            .try_into_tx_env(evm_env)
+        let inner_tx_env = inner
+            .try_into_tx_env(evm_env)
             .map_err(EthApiError::from)?;
🧹 Nitpick comments (7)
.gitignore (1)

4-7: LGTM! Clear and well-organized additions.

The new local-test/ entry with a descriptive comment is helpful for keeping local test artifacts out of version control.

One optional note: /docs uses an absolute path pattern (matches only at repository root), while local-test/ uses a relative pattern (matches anywhere). If local-test/ should only be ignored at the root level for consistency, consider using /local-test/ instead.

crates/node/Cargo.toml (1)

46-48: Nit: clap is miscategorized under "Async".

clap is a CLI argument parser, not an async utility. Consider moving it to a separate section (e.g., # CLI or # Other).

crates/evm/src/engine.rs (1)

34-47: Consider avoiding the intermediate Vec allocation.

The .collect::<Vec<_>>().into_iter() on lines 43–44 materializes all (Arc, index) pairs upfront. For blocks with many transactions, this allocates unnecessarily. You could return the Map iterator directly if you restructure the ownership slightly, or just note that this is bounded by block transaction count (which is practically small).

That said, this works correctly and the per-item cost is just an Arc clone (cheap).

♻️ Optional: avoid intermediate collect
     fn tx_iterator_for_payload(
         &self,
         payload: &MorphExecutionData,
     ) -> Result<impl ExecutableTxIterator<Self>, Self::Error> {
         let block = payload.block.clone();
-        // Create an iterator over (block, index) pairs
-        // Use into_iter() to get a proper Iterator implementation
-        let transactions = (0..payload.block.body().transactions.len())
-            .map(move |i| (block.clone(), i))
-            .collect::<Vec<_>>()
-            .into_iter();
+        let len = payload.block.body().transactions.len();
+        let transactions = (0..len).map(move |i| (block.clone(), i));
 
         Ok((transactions, RecoveredInBlock::new))
     }
crates/node/src/components/pool.rs (1)

88-115: Sync tests don't need #[tokio::test].

The first three tests (test_validate_oversized_transaction, test_l1_message_type_id, test_morph_tx_type_id) don't use any async operations. Using #[tokio::test] works but is misleading and adds unnecessary runtime overhead.

bin/morph-reth/src/main.rs (1)

22-24: std::env::set_var in unsafe is correct here but worth a safety comment.

Since Rust 1.66, set_var is unsafe due to thread-safety concerns. Calling it at the top of main() before any threads spawn is safe, but a brief // SAFETY: comment would document the invariant.

📝 Suggested safety comment
     if std::env::var_os("RUST_BACKTRACE").is_none() {
-        unsafe { std::env::set_var("RUST_BACKTRACE", "1") };
+        // SAFETY: Called before any threads are spawned, so this is single-threaded.
+        unsafe { std::env::set_var("RUST_BACKTRACE", "1") };
     }
crates/node/src/node.rs (1)

207-213: Consider documenting the unwrap() safety invariant.

The unwrap() on duration_since(UNIX_EPOCH) is practically safe (only fails if the system clock is before 1970), but a brief // SAFETY: or expect() with a message would make the assumption explicit.

Proposed change
 fn unix_timestamp_now() -> u64 {
     std::time::SystemTime::now()
         .duration_since(std::time::UNIX_EPOCH)
-        .unwrap()
+        .expect("system clock is before UNIX epoch")
         .as_secs()
 }
crates/rpc/src/eth/transaction.rs (1)

354-365: access_list is cloned twice — once at line 354, again at line 365.

The clone at line 365 is needed because access_list is reused at line 382 in the None branch. Consider passing by reference to try_build_morph_tx_from_env to avoid the extra allocation, or restructure so the Some branch consumes it.

@panos-xyz panos-xyz added the enhancement New feature or request label Feb 12, 2026
@panos-xyz panos-xyz self-assigned this Feb 12, 2026
@panos-xyz panos-xyz linked an issue Feb 12, 2026 that may be closed by this pull request
10 tasks
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@crates/engine-api/src/builder.rs`:
- Around line 420-429: new_l2_block currently calls validate_l2_block and only
logs a warning on validation.success == false, but still proceeds to accept the
block; change new_l2_block to stop and return an error when validation.success
is false (or make this behavior conditional on an explicit config flag), i.e.,
after calling validate_l2_block in new_l2_block, if validation.success == false
then return an Err containing a descriptive message and the block hash (or check
an explicit self.<validate_blocks_flag> to allow override), and ensure any
Ok/Err from validate_l2_block is propagated correctly instead of always
continuing.
- Around line 250-259: The current Err branch for sealed_header_by_hash marks
the block as validated and returns GenericResponse { success: true }, which
wrongly treats DB errors as successful validation; update the Err arm in the
function containing sealed_header_by_hash so it does NOT call
self.validation_cache.insert(data.hash, ()), and instead either propagate the
error (return Err(e) or map it into the function's error type) or return
GenericResponse { success: false } to force a retry; ensure the tracing::warn
stays (or is upgraded to error) and reference the same symbols
(sealed_header_by_hash, self.validation_cache.insert, GenericResponse { success:
true }) when making the change so transient DB errors are not cached as valid.
- Line 566: The current cast base_fee_per_gas: data.base_fee_per_gas.map(|f| f
as u64) silently truncates Option<u128> values above u64::MAX; replace the
unchecked cast with a fallible conversion and propagate or handle the error
(e.g., use f.try_into() and transpose() to produce Result<Option<u64>, _> and
return or map the error), so update the construction for base_fee_per_gas
(referencing data.base_fee_per_gas and the builder that constructs the header)
to use try_into() and proper error handling instead of as u64.
🧹 Nitpick comments (5)
crates/node/Cargo.toml (1)

47-49: Misleading section comment: eyre and clap are not async crates.

eyre is an error-handling library and clap is a CLI argument parser. Consider renaming this section to something like # Misc or # CLI / Utilities, or grouping them with their actual purpose.

Proposed fix
-# Async
+# Misc
 eyre.workspace = true
 clap.workspace = true
crates/evm/src/config.rs (1)

67-105: Consider extracting shared CfgEnv + blob placeholder construction.

Both evm_env and next_evm_env duplicate the CfgEnv::default().with_chain_id(...).with_spec(spec) pattern and the identical BlobExcessGasAndPrice placeholder block. A small helper (e.g., fn make_cfg_env(&self, spec) -> CfgEnv<MorphHardfork>) would reduce the duplication and make it easier to update the blob placeholder logic in one place if Morph ever changes its stance on EIP-4844.

♻️ Example helper extraction
+    fn make_cfg_env(&self, spec: MorphHardfork) -> CfgEnv<MorphHardfork> {
+        CfgEnv::<MorphHardfork>::default()
+            .with_chain_id(self.chain_spec().chain().id())
+            .with_spec(spec)
+    }
+
+    fn blob_placeholder() -> Option<BlobExcessGasAndPrice> {
+        Some(BlobExcessGasAndPrice {
+            excess_blob_gas: 0,
+            blob_gasprice: 1,
+        })
+    }
crates/engine-api/src/builder.rs (3)

262-271: Redundant parent hash check.

sealed_header_by_hash(data.parent_hash) fetches the header by data.parent_hash, so parent.hash() will always equal data.parent_hash. This branch is dead code.


126-132: unwrap() on SystemTime::now() can panic if clock is before UNIX epoch.

This is extremely unlikely in practice but violates defensive coding. Consider using unwrap_or(0) or unwrap_or_else(|_| parent.timestamp() + 1) as a safe fallback.


46-48: Unbounded validation cache can grow indefinitely.

DashMap<B256, ()> is never evicted. Over time this will consume increasing memory as every validated block hash is retained. Consider using an LRU cache or periodic eviction.

Comment on lines +250 to +259
Err(e) => {
tracing::warn!(
target: "morph::engine",
error = %e,
"failed to query parent header"
);
// Database error - but don't fail, just skip validation
self.validation_cache.insert(data.hash, ());
return Ok(GenericResponse { success: true });
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

DB errors silently pass validation as success: true.

When sealed_header_by_hash returns an Err, the block is cached as validated and success: true is returned. A transient database error (e.g., I/O glitch, corrupted page) would cause an unvalidated block to be permanently cached as valid. This is a different situation from "parent not found" — a DB error means we don't know whether the parent exists.

At minimum, don't cache the result on error and consider returning success: false or propagating the error so the caller can retry.

Proposed fix
             Err(e) => {
                 tracing::warn!(
                     target: "morph::engine",
                     error = %e,
                     "failed to query parent header"
                 );
-                // Database error - but don't fail, just skip validation
-                self.validation_cache.insert(data.hash, ());
-                return Ok(GenericResponse { success: true });
+                // Database error - can't validate, report failure so caller can retry
+                return Ok(GenericResponse { success: false });
             }
🤖 Prompt for AI Agents
In `@crates/engine-api/src/builder.rs` around lines 250 - 259, The current Err
branch for sealed_header_by_hash marks the block as validated and returns
GenericResponse { success: true }, which wrongly treats DB errors as successful
validation; update the Err arm in the function containing sealed_header_by_hash
so it does NOT call self.validation_cache.insert(data.hash, ()), and instead
either propagate the error (return Err(e) or map it into the function's error
type) or return GenericResponse { success: false } to force a retry; ensure the
tracing::warn stays (or is upgraded to error) and reference the same symbols
(sealed_header_by_hash, self.validation_cache.insert, GenericResponse { success:
true }) when making the change so transient DB errors are not cached as valid.

Comment on lines 420 to 429
// 4. Optionally validate the block content
if let Ok(validation) = self.validate_l2_block(data.clone()).await {
if !validation.success {
tracing::warn!(
target: "morph::engine",
block_hash = %data.hash,
"block content validation failed"
);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Block import proceeds after validation failure.

new_l2_block calls validate_l2_block but only logs a warning when validation fails — it still converts and "accepts" the block. While the comment says "optionally validate," silently importing a block that failed content validation seems risky. Consider at least returning an error when validation.success == false, or making the "optional" behavior an explicit configuration flag.

🤖 Prompt for AI Agents
In `@crates/engine-api/src/builder.rs` around lines 420 - 429, new_l2_block
currently calls validate_l2_block and only logs a warning on validation.success
== false, but still proceeds to accept the block; change new_l2_block to stop
and return an error when validation.success is false (or make this behavior
conditional on an explicit config flag), i.e., after calling validate_l2_block
in new_l2_block, if validation.success == false then return an Err containing a
descriptive message and the block hash (or check an explicit
self.<validate_blocks_flag> to allow override), and ensure any Ok/Err from
validate_l2_block is propagated correctly instead of always continuing.

extra_data: Default::default(),
mix_hash: alloy_primitives::B256::ZERO,
nonce: alloy_primitives::B64::ZERO,
base_fee_per_gas: data.base_fee_per_gas.map(|f| f as u64),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's read the context around line 566 in the builder.rs file
sed -n '550,580p' crates/engine-api/src/builder.rs | cat -n

Repository: morph-l2/morph-reth

Length of output: 1504


🏁 Script executed:

# Now let's find what 'data' is and its type definition around that area
sed -n '530,570p' crates/engine-api/src/builder.rs | cat -n

Repository: morph-l2/morph-reth

Length of output: 1920


🏁 Script executed:

# Search for the struct containing the base_fee_per_gas field
rg 'struct.*Data|base_fee_per_gas' crates/engine-api/src/builder.rs -A3 -B3

Repository: morph-l2/morph-reth

Length of output: 411


🏁 Script executed:

# Search for imports and type definitions related to U256 and base_fee_per_gas across the engine-api crate
rg 'base_fee_per_gas' crates/engine-api/ --type rust -B2 -A2

Repository: morph-l2/morph-reth

Length of output: 497


🏁 Script executed:

# Search for ExecutableL2Data struct definition
rg 'struct ExecutableL2Data' crates/engine-api/ -A20

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

# Also check if it's imported from elsewhere
rg 'ExecutableL2Data' crates/engine-api/src/builder.rs -B5 -A2 | head -40

Repository: morph-l2/morph-reth

Length of output: 1313


🏁 Script executed:

# Search more broadly for the struct definition
find crates/engine-api -name '*.rs' -exec grep -l 'struct ExecutableL2Data' {} \;

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

# Find the morph_payload_types crate
find . -name 'morph_payload_types' -type d | head -5

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

# Also search for ExecutableL2Data definition in the entire codebase
rg 'struct ExecutableL2Data' --type rust

Repository: morph-l2/morph-reth

Length of output: 139


🏁 Script executed:

# Search for pub struct ExecutableL2Data with various patterns
rg 'ExecutableL2Data' --type rust -B5 -A15 | grep -A15 'pub struct ExecutableL2Data'

Repository: morph-l2/morph-reth

Length of output: 1267


🏁 Script executed:

cat crates/payload/types/src/executable_l2_data.rs

Repository: morph-l2/morph-reth

Length of output: 6184


Replace silent truncation with explicit overflow handling for base_fee_per_gas conversion.

data.base_fee_per_gas is Option<u128>, and the cast as u64 will silently truncate values exceeding u64::MAX (18,446,744,073,709,551,615). Use try_into() with error handling instead to prevent incorrect header construction:

base_fee_per_gas: data.base_fee_per_gas.map(|f| f.try_into()).transpose()?,

or handle the conversion error explicitly.

🤖 Prompt for AI Agents
In `@crates/engine-api/src/builder.rs` at line 566, The current cast
base_fee_per_gas: data.base_fee_per_gas.map(|f| f as u64) silently truncates
Option<u128> values above u64::MAX; replace the unchecked cast with a fallible
conversion and propagate or handle the error (e.g., use f.try_into() and
transpose() to produce Result<Option<u64>, _> and return or map the error), so
update the construction for base_fee_per_gas (referencing data.base_fee_per_gas
and the builder that constructs the header) to use try_into() and proper error
handling instead of as u64.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@crates/engine-api/src/builder.rs`:
- Around line 126-132: The current assemble_l2_block uses SystemTime::now()
causing non-deterministic timestamps; change assemble_l2_block to accept a
timestamp via AssembleL2BlockParams (add a timestamp field) and use that value
instead of calling SystemTime::now(), and update callers accordingly;
specifically, have validate_l2_block supply the original timestamp from the
incoming ExecutableL2Data when it replays the block so the rebuild uses the
exact same timestamp and produces deterministic state roots/gas/hash
comparisons.
- Around line 541-545: The current code silently falls back to Bloom::ZERO when
data.logs_bloom.len() != 256; change this to validate and return an error
instead of masking bad data: check data.logs_bloom.len() and if it is not 256
return an Err (with a clear message including the bad length and context) rather
than using Bloom::ZERO, replacing the else branch where logs_bloom is set;
update the enclosing function (the builder creation function in this file) to
return a Result if it does not already and propagate the error to callers so
malformed bloom data is surfaced; keep the use of Bloom::from_slice on the
success path.
- Around line 547-553: Replace the incorrect zero values: set Header.ommers_hash
to EMPTY_OMMER_ROOT_HASH (imported from alloy_consensus) instead of
alloy_primitives::B256::ZERO, and compute Header.transactions_root from the
block transactions rather than leaving it as B256::ZERO—use
proofs::calculate_transaction_root(data.transactions) (as done in
crates/evm/src/assemble.rs) before calling seal_unchecked so that seal_unchecked
and subsequent block.ensure_transaction_root_valid() see the correct
transactions_root.
🧹 Nitpick comments (3)
crates/evm/src/config.rs (1)

74-96: Consider extracting shared CfgEnv + blob-placeholder construction.

Both evm_env and next_evm_env duplicate the CfgEnv construction (lines 35–37 vs 74–76) and the blob_excess_gas_and_price placeholder. A small helper (e.g., fn make_cfg_env(&self, spec) -> CfgEnv<MorphHardfork> and a constant for the blob placeholder) would reduce duplication and ensure both paths stay in sync.

crates/engine-api/src/builder.rs (2)

258-267: Redundant parent-hash check — always passes.

parent was fetched via sealed_header_by_hash(data.parent_hash) (line 229), so parent.hash() is guaranteed to equal data.parent_hash. This comparison is dead code.


635-657: Consider removing dead legacy builder or marking it #[deprecated].

The comment says this builder is "superseded" and "not used in practice," and the add_ons.rs snippet confirms NoopEngineApiBuilder is used instead. Keeping dead code without a deprecation marker or #[cfg(test)] gate adds maintenance burden.

Comment on lines +126 to +132
let timestamp = std::cmp::max(
parent.timestamp() + 1,
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Non-deterministic timestamp will cause validation rebuilds to always mismatch.

assemble_l2_block computes timestamp from SystemTime::now(). When validate_l2_block (line 275) calls assemble_l2_block to re-execute the block, a different wall-clock time will produce a different timestamp, leading to different state roots, gas usage, and block hashes. The rebuild-and-compare validation strategy (lines 290–333) will effectively never match.

The timestamp should either be passed in via AssembleL2BlockParams (so validation can replay with the original timestamp), or validate_l2_block should skip calling assemble_l2_block and instead re-execute with the exact parameters from the incoming ExecutableL2Data.

🤖 Prompt for AI Agents
In `@crates/engine-api/src/builder.rs` around lines 126 - 132, The current
assemble_l2_block uses SystemTime::now() causing non-deterministic timestamps;
change assemble_l2_block to accept a timestamp via AssembleL2BlockParams (add a
timestamp field) and use that value instead of calling SystemTime::now(), and
update callers accordingly; specifically, have validate_l2_block supply the
original timestamp from the incoming ExecutableL2Data when it replays the block
so the rebuild uses the exact same timestamp and produces deterministic state
roots/gas/hash comparisons.

Comment on lines +541 to +545
let logs_bloom = if data.logs_bloom.len() == 256 {
Bloom::from_slice(&data.logs_bloom)
} else {
Bloom::ZERO
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Silent fallback to Bloom::ZERO on unexpected bloom length.

If data.logs_bloom has an unexpected length (not 256 bytes), the code silently uses Bloom::ZERO instead of returning an error. This could mask corrupted or malformed block data. Consider returning an error for invalid bloom length.

🤖 Prompt for AI Agents
In `@crates/engine-api/src/builder.rs` around lines 541 - 545, The current code
silently falls back to Bloom::ZERO when data.logs_bloom.len() != 256; change
this to validate and return an error instead of masking bad data: check
data.logs_bloom.len() and if it is not 256 return an Err (with a clear message
including the bad length and context) rather than using Bloom::ZERO, replacing
the else branch where logs_bloom is set; update the enclosing function (the
builder creation function in this file) to return a Result if it does not
already and propagate the error to callers so malformed bloom data is surfaced;
keep the use of Bloom::from_slice on the success path.

Comment on lines +547 to +553
let inner = Header {
parent_hash: data.parent_hash,
ommers_hash: alloy_primitives::B256::ZERO, // No ommers in L2
beneficiary: data.miner,
state_root: data.state_root,
transactions_root: alloy_primitives::B256::ZERO, // Will be calculated when sealing
receipts_root: data.receipts_root,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Check if EMPTY_OMMER_ROOT_HASH exists in alloy_consensus or related crates
rg 'EMPTY_OMMER_ROOT_HASH|EMPTY_UNCLES_HASH' --type rust -l

# Check the builder.rs file around the problematic lines
fd builder.rs --type f -x cat -n {} \; | grep -A 30 -B 5 'transactions_root'

# Check what seal_unchecked does
rg 'seal_unchecked' --type rust -A 10 -B 2

Repository: morph-l2/morph-reth

Length of output: 2665


🏁 Script executed:

# Examine the full builder.rs file to understand context
wc -l crates/engine-api/src/builder.rs

Repository: morph-l2/morph-reth

Length of output: 99


🏁 Script executed:

# Search for imports and constants related to ommer/uncle hashing
rg 'EMPTY.*HASH|empty.*ommer|empty.*uncle' --type rust -i

Repository: morph-l2/morph-reth

Length of output: 2120


🏁 Script executed:

# Search for transactions_root computation patterns
rg 'transactions_root' --type rust -B 3 -A 3 -g '!target/'

# Check the ExecutableL2Data structure to see if it has transaction info
rg 'struct ExecutableL2Data|impl.*ExecutableL2Data' --type rust -A 10

Repository: morph-l2/morph-reth

Length of output: 4368


🏁 Script executed:

# Check what fields are available in the data being converted
rg 'fn executable_data_to_header' --type rust -A 50 | head -80

Repository: morph-l2/morph-reth

Length of output: 3607


🏁 Script executed:

# Look for transaction root calculation using proofs or merkle trees
rg 'calculate.*root|compute.*root|tx.*root|transaction.*root' --type rust -i

Repository: morph-l2/morph-reth

Length of output: 3091


🏁 Script executed:

# Get the full ExecutableL2Data struct definition
rg 'pub struct ExecutableL2Data' --type rust -A 50

Repository: morph-l2/morph-reth

Length of output: 3854


🏁 Script executed:

# Check if there's a pattern where transactions_root is expected to come from elsewhere (like data.hash)
rg 'data\.hash|hash.*transactions' --type rust -B 3 -A 3 -g '!target/'

Repository: morph-l2/morph-reth

Length of output: 5926


🏁 Script executed:

# Check if L2 has special validation rules that skip transactions_root
rg 'ensure_transaction_root|transaction.*root.*validation|skip.*transaction' --type rust -B 2 -A 5

Repository: morph-l2/morph-reth

Length of output: 4241


🏁 Script executed:

# Check the validation logic to see if L2 blocks are exempt from transactions_root checking
rg 'fn validate_block_content|validate.*header|transactions_root' --type rust -B 5 -A 10 | head -100

Repository: morph-l2/morph-reth

Length of output: 6783


🏁 Script executed:

# Look at the actual block sealing and assembly logic to understand the intent
sed -n '520,580p' crates/engine-api/src/builder.rs

Repository: morph-l2/morph-reth

Length of output: 2175


Use EMPTY_OMMER_ROOT_HASH instead of B256::ZERO for ommers_hash, and calculate transactions_root from the transaction list.

  1. ommers_hash (line 549): Must be EMPTY_OMMER_ROOT_HASH (the keccak256 hash of an empty RLP-encoded list), not B256::ZERO. The codebase validation enforces this check: header.ommers_hash() != EMPTY_OMMER_ROOT_HASH will fail. EMPTY_OMMER_ROOT_HASH is exported from alloy_consensus.

  2. transactions_root (line 552): The comment says "will be calculated when sealing," but seal_unchecked on line 529 only stamps the provided hash without recalculating any fields. This leaves transactions_root as permanently B256::ZERO, which will fail validation: block.ensure_transaction_root_valid() will reject the block. Calculate it from data.transactions using proofs::calculate_transaction_root() as shown in crates/evm/src/assemble.rs.

🤖 Prompt for AI Agents
In `@crates/engine-api/src/builder.rs` around lines 547 - 553, Replace the
incorrect zero values: set Header.ommers_hash to EMPTY_OMMER_ROOT_HASH (imported
from alloy_consensus) instead of alloy_primitives::B256::ZERO, and compute
Header.transactions_root from the block transactions rather than leaving it as
B256::ZERO—use proofs::calculate_transaction_root(data.transactions) (as done in
crates/evm/src/assemble.rs) before calling seal_unchecked so that seal_unchecked
and subsequent block.ensure_transaction_root_valid() see the correct
transactions_root.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@crates/chainspec/src/spec.rs`:
- Around line 36-56: The conversion of genesis.base_fee_per_gas in
make_genesis_header silently turns overflowed values into 0 (via .map(|b|
b.try_into().unwrap_or(0))); change this to surface the error instead of masking
it: update make_genesis_header (and possibly its callers) to handle conversion
failures from Genesis::base_fee_per_gas by either returning a
Result<MorphHeader, Error> or by logging a clear warning/error when b.try_into()
fails, and use a safe fallback only if explicitly desired. Locate the
base_fee_per_gas mapping in make_genesis_header and replace the unwrap_or(0)
usage with explicit error handling (e.g., match or map_err) so overflow is
reported (or propagated) rather than silently producing 0.
- Around line 62-104: The hardfork assembly logic in the From<Genesis> for
MorphChainSpec impl is duplicated; refactor that impl to call the existing
build_morph_hardforks_from_genesis(genesis) helper instead of reimplementing
block_forks/time_forks and Prague insertion: replace the duplicated construction
in the From<Genesis> implementation with a call to
build_morph_hardforks_from_genesis(genesis) and use its returned ChainHardforks
when building the MorphChainSpec, removing the redundant code while preserving
the Prague-at-Emerald insertion and any unwrap/default handling present in
build_morph_hardforks_from_genesis.

In `@crates/payload/types/Cargo.toml`:
- Around line 40-42: The serde feature in Cargo.toml is currently a no-op
causing inconsistent JSON formatting because MorphPayloadAttributes has
unconditional #[derive(Serialize, Deserialize)] while
MorphPayloadAttributes.inner is guarded with #[cfg_attr(feature = "serde",
serde(flatten))]; fix this by removing the cfg_attr guard and making
serde(flatten) unconditional on MorphPayloadAttributes.inner (or alternatively
make the derive conditional on feature "serde" throughout attributes.rs), i.e.,
update attributes.rs to either (preferred) remove cfg_attr and use
serde(flatten) directly on inner so flattening always occurs, or wrap the
Serialize/Deserialize derives with cfg(feature = "serde") to make the feature
meaningful.
🧹 Nitpick comments (3)
crates/chainspec/src/spec.rs (1)

106-115: Duplicate section comment.

Lines 106–108 and 113–115 both say "Chain Specification Parser (CLI)". Remove the first one.

Fix
-// =============================================================================
-// Chain Specification Parser (CLI)
-// =============================================================================
-
 /// Chains supported by Morph. First value should be used as the default.
 pub const SUPPORTED_CHAINS: &[&str] = &["mainnet", "hoodi"];
crates/evm/src/config.rs (2)

35-56: Consider extracting shared CfgEnv and blob-placeholder construction.

Both evm_env and next_evm_env build the CfgEnv and blob_excess_gas_and_price identically. A small helper would reduce duplication and keep the placeholder logic in one place.

♻️ Example helper extraction
+fn morph_cfg_env(chain_id: u64, spec: MorphHardfork) -> CfgEnv<MorphHardfork> {
+    CfgEnv::<MorphHardfork>::default()
+        .with_chain_id(chain_id)
+        .with_spec_and_mainnet_gas_params(spec)
+}
+
+/// Morph doesn't support EIP-4844 blob transactions, but when SpecId >= CANCUN,
+/// revm requires `blob_excess_gas_and_price` to be set.
+fn morph_blob_excess_gas_and_price() -> Option<BlobExcessGasAndPrice> {
+    Some(BlobExcessGasAndPrice { excess_blob_gas: 0, blob_gasprice: 1 })
+}

Also applies to: 74-96


82-82: Potential u64 overflow on parent.number() + 1.

If parent.number() is u64::MAX, this wraps. In practice block numbers won't reach that, but a checked_add(1) or saturating_add(1) would be more defensive.

Comment on lines +36 to +56
pub(crate) fn make_genesis_header(genesis: &Genesis, state_root: B256) -> MorphHeader {
let inner = Header {
gas_limit: genesis.gas_limit,
difficulty: genesis.difficulty,
nonce: genesis.nonce.into(),
extra_data: genesis.extra_data.clone(),
state_root,
timestamp: genesis.timestamp,
mix_hash: genesis.mix_hash,
beneficiary: genesis.coinbase,
base_fee_per_gas: genesis.base_fee_per_gas.map(|b| b.try_into().unwrap_or(0)),
withdrawals_root: None,
parent_beacon_block_root: None,
blob_gas_used: None,
excess_blob_gas: None,
requests_hash: None,
..Default::default()
};

MorphHeader::from(inner)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

base_fee_per_gas conversion silently truncates to 0 on overflow.

Line 46: genesis.base_fee_per_gas.map(|b| b.try_into().unwrap_or(0)) — if the genesis JSON specifies a baseFeePerGas value larger than u64::MAX, it silently becomes 0. While unlikely in practice, this could mask configuration errors. Consider logging a warning or returning an error.

🤖 Prompt for AI Agents
In `@crates/chainspec/src/spec.rs` around lines 36 - 56, The conversion of
genesis.base_fee_per_gas in make_genesis_header silently turns overflowed values
into 0 (via .map(|b| b.try_into().unwrap_or(0))); change this to surface the
error instead of masking it: update make_genesis_header (and possibly its
callers) to handle conversion failures from Genesis::base_fee_per_gas by either
returning a Result<MorphHeader, Error> or by logging a clear warning/error when
b.try_into() fails, and use a safe fallback only if explicitly desired. Locate
the base_fee_per_gas mapping in make_genesis_header and replace the unwrap_or(0)
usage with explicit error handling (e.g., match or map_err) so overflow is
reported (or propagated) rather than silently producing 0.

Comment on lines +62 to +104
pub(crate) fn build_morph_hardforks_from_genesis(genesis: &Genesis) -> ChainHardforks {
// Start with Ethereum hardforks from genesis
let base_spec = ChainSpec::from_genesis(genesis.clone());
let mut hardforks = base_spec.hardforks;

// Extract Morph genesis info
let chain_info = MorphGenesisInfo::extract_from(&genesis.config.extra_fields)
.expect("failed to extract morph genesis info");
let hardfork_info = chain_info
.hard_fork_info
.as_ref()
.cloned()
.unwrap_or_default();

// Add Morph hardforks
let block_forks = vec![
(MorphHardfork::Bernoulli, hardfork_info.bernoulli_block),
(MorphHardfork::Curie, hardfork_info.curie_block),
]
.into_iter()
.filter_map(|(fork, block)| block.map(|b| (fork, ForkCondition::Block(b))));

let time_forks = vec![
(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))));

hardforks.extend(block_forks.chain(time_forks));

// Add Prague at Emerald time for EIP-7702
if let Some(emerald_time) = hardfork_info.emerald_time {
hardforks.insert(
EthereumHardfork::Prague,
ForkCondition::Timestamp(emerald_time),
);
}

hardforks
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Hardfork construction logic is duplicated between build_morph_hardforks_from_genesis and From<Genesis> for MorphChainSpec.

The new helper build_morph_hardforks_from_genesis (lines 62–104) contains the exact same hardfork assembly logic as the From<Genesis> impl (lines 222–256): block forks, time forks, Prague-at-Emerald insertion. The helper is only used in morph.rs and morph_hoodi.rs for the static chain specs, while the From<Genesis> impl handles CLI-parsed genesis files.

Consider refactoring From<Genesis> to call build_morph_hardforks_from_genesis instead of reimplementing the same logic:

Proposed refactor sketch
 impl From<Genesis> for MorphChainSpec {
     fn from(genesis: Genesis) -> Self {
         let chain_info = MorphGenesisInfo::extract_from(&genesis.config.extra_fields)
             .expect("failed to extract morph genesis info");
 
-        let hardfork_info = chain_info.hard_fork_info.clone().unwrap_or_default();
-
-        // Create base chainspec from genesis (already has ordered Ethereum hardforks)
-        let mut base_spec = ChainSpec::from_genesis(genesis);
-
-        // Add Morph hardforks
-        let block_forks = vec![...];
-        ...
-        base_spec.hardforks.extend(morph_forks);
-
-        if let Some(emerald_time) = hardfork_info.emerald_time {
-            base_spec.hardforks.insert(...);
-        }
+        let hardforks = build_morph_hardforks_from_genesis(&genesis);
+        let mut base_spec = ChainSpec::from_genesis(genesis);
+        base_spec.hardforks = hardforks;
 
         Self {
             inner: base_spec.map_header(MorphHeader::from),
             info: chain_info,
         }
     }
 }
🤖 Prompt for AI Agents
In `@crates/chainspec/src/spec.rs` around lines 62 - 104, The hardfork assembly
logic in the From<Genesis> for MorphChainSpec impl is duplicated; refactor that
impl to call the existing build_morph_hardforks_from_genesis(genesis) helper
instead of reimplementing block_forks/time_forks and Prague insertion: replace
the duplicated construction in the From<Genesis> implementation with a call to
build_morph_hardforks_from_genesis(genesis) and use its returned ChainHardforks
when building the MorphChainSpec, removing the redundant code while preserving
the Prague-at-Emerald insertion and any unwrap/default handling present in
build_morph_hardforks_from_genesis.

Comment on lines +40 to +42
[features]
default = []
serde = []
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The serde feature is a no-op marker that causes a serialization inconsistency.

The serde feature gates nothing (serde = []), yet attributes.rs uses #[cfg_attr(feature = "serde", serde(flatten))] on MorphPayloadAttributes.inner. Since serde::Serialize/serde::Deserialize are derived unconditionally (line 17 of attributes.rs), when this crate is compiled without the serde feature (e.g. via default-features = false), the struct will still be (de)serializable but inner won't be flattened — breaking the Engine API JSON format silently.

Either:

  1. Make the entire serde derive conditional on the feature, or
  2. Make serde(flatten) unconditional (remove the cfg_attr guard), since serde is always a dependency anyway.

Option 2 seems simpler given that serde is already unconditional:

Proposed fix in attributes.rs
-    #[cfg_attr(feature = "serde", serde(flatten))]
+    #[serde(flatten)]
🤖 Prompt for AI Agents
In `@crates/payload/types/Cargo.toml` around lines 40 - 42, The serde feature in
Cargo.toml is currently a no-op causing inconsistent JSON formatting because
MorphPayloadAttributes has unconditional #[derive(Serialize, Deserialize)] while
MorphPayloadAttributes.inner is guarded with #[cfg_attr(feature = "serde",
serde(flatten))]; fix this by removing the cfg_attr guard and making
serde(flatten) unconditional on MorphPayloadAttributes.inner (or alternatively
make the derive conditional on feature "serde" throughout attributes.rs), i.e.,
update attributes.rs to either (preferred) remove cfg_attr and use
serde(flatten) directly on inner so flattening always occurs, or wrap the
Serialize/Deserialize derives with cfg(feature = "serde") to make the feature
meaningful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(node): implement Morph node module integration

1 participant