Skip to content

Conversation

@panos-xyz
Copy link
Contributor

@panos-xyz panos-xyz commented Jan 14, 2026

Summary by CodeRabbit

  • New Features

    • Built-in Morph Mainnet & Hoodi chain specs, Emerald hardfork, expanded chain config/CLI access, typed multi-format receipts (multiple tx types, L1 messages, alt-fee), Morph engine/payload types, and a new Morph consensus validation engine.
  • Bug Fixes / Reliability

    • Stronger validation across headers, bodies, parents, receipts, L1 message ordering, gas and base-fee rules.
  • Chores

    • Workspace and dependency reorganization and manifest updates to enable new crates and features.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

📝 Walkthrough
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and specifically describes the main changes: adding Morph receipt types and consensus validation logic across multiple files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings

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: 4

Caution

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

⚠️ Outside diff range comments (1)
crates/primitives/src/transaction/l1_transaction.rs (1)

108-143: Fix RLP decode/encode field order mismatch.

decode_fields() attempts to decode tx_hash as the second field, but encode_fields() and fields_len() omit it entirely. This breaks RLP decoding via RlpEcdsaDecodableTx since the encoded data contains 7 fields while the decoder expects 8, causing all subsequent fields to be read from wrong byte offsets.

The correct behavior is shown in the impl Decodable for TxL1Msg (lines 274–311), which properly decodes the 7 fields and sets tx_hash to B256::ZERO with the comment "For L1 messages, the hash is typically computed externally."

Fix: align decode_fields with encode_fields
 pub fn decode_fields(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
     Ok(Self {
         queue_index: Decodable::decode(buf)?,
-        tx_hash: Decodable::decode(buf)?,
+        tx_hash: B256::ZERO,
         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)?,
     })
 }
🤖 Fix all issues with AI agents
In `@crates/chainspec/src/morph_hoodi.rs`:
- Around line 33-40: The test hardcodes timestamp 0 but MORPH_HOODI's Bernoulli
activation isn't at 0; update the test to use the actual configured activation
time instead of 0: query the Bernoulli activation timestamp from the morph hoodi
config (the value defined on MORPH_HOODI / its hardfork schedule) and assert
MORPH_HOODI.is_bernoulli_active_at_timestamp(activation_ts) (or adjust
genesis/hardfork schedule in the MORPH_HOODI definition if the intent is to have
it active at 0). Use the existing symbol MORPH_HOODI and the
is_bernoulli_active_at_timestamp helper to locate where to plug in the correct
activation timestamp.

In `@crates/chainspec/src/morph.rs`:
- Around line 35-43: The test test_morph_mainnet_hardforks expects all hardforks
active at timestamp 0 but the MORPH_MAINNET genesis config lacks timestamp-based
activation fields; either add the missing fields (bernoulliTime, curieTime,
morph203Time, viridianTime, emeraldTime) with value 0 to the mainnet genesis
JSON so MORPH_MAINNET.is_..._active_at_timestamp(0) returns true, or change
test_morph_mainnet_hardforks to assert the actual configured activation state
(remove or adjust the five assert! calls) to match the current genesis
configuration.

In `@crates/chainspec/src/spec.rs`:
- Around line 100-104: The doc comment above the method is_fee_vault_enabled()
is stale and describes a conversion; replace it with an accurate brief comment
describing this method's behavior (e.g., "Returns whether the fee vault is
enabled.") so readers and docs match the implementation; update the triple-slash
comment immediately above the pub fn is_fee_vault_enabled(&self) -> bool { ... }
to reflect that it simply delegates to
self.info.morph_chain_info.is_fee_vault_enabled().

In `@crates/consensus/src/validation.rs`:
- Around line 151-164: The gas-limit delta validation is being applied
unconditionally in validate_header_against_parent causing post‑Curie blocks to
be rejected; change validate_header_against_parent so it first determines
whether the block is pre‑Curie (using the header/fork metadata available on
SealedHeader<alloy_consensus::Header> — e.g., a pre‑Curie check provided by the
Header or chain/fork info) and only invoke
validate_against_parent_gas_limit(header.header(), parent.header()) when that
pre‑Curie condition is true; leave the other validations
(validate_against_parent_hash_number and validate_against_parent_timestamp)
unconditional.
🧹 Nitpick comments (7)
crates/primitives/src/transaction/l1_transaction.rs (1)

30-42: Consider hex-quantity serde for queue_index.

If this struct is ever serialized for JSON/RPC, queue_index likely should use the same hex-quantity format as nonce/gas_limit. If decimal is intended, please confirm.

♻️ Suggested tweak
 pub struct TxL1Msg {
     /// The queue index of the message in the L1 contract queue.
+    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
     pub queue_index: u64,
crates/payload/types/src/lib.rs (1)

87-109: Consider de‑duplicating block_to_payload logic.
The conversion is identical to MorphEngineTypes::block_to_payload; a small helper would reduce drift risk.

crates/chainspec/Cargo.toml (1)

19-31: Consider gating alloy-consensus serde support under the serde feature.
If chainspec serializes alloy-consensus types, add alloy-consensus/serde to the serde feature for parity with other alloy crates.

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

43-43: Track TODO: L1 fee calculation is not yet implemented.

The TODO indicates that L1 fee calculation from the execution context is pending. Without this, MorphTransactionReceipt instances will have l1_fee: None, which affects receipt accuracy for rollup cost tracking.

Would you like me to open an issue to track the L1 fee calculation implementation?

crates/primitives/src/receipt/receipt.rs (1)

187-212: Consider bloom computation cost.

TxReceipt::bloom() calls self.inner.bloom_slow() which recomputes the bloom filter from logs on each call. If bloom() is called frequently (e.g., during validation or serialization), consider caching the bloom or using MorphReceiptWithBloom where possible.

This is likely acceptable for the current use case but worth noting for performance-sensitive paths.

crates/primitives/src/receipt/mod.rs (1)

507-514: Unnecessary clone for L1Msg variant.

The inner.clone() on line 508 creates a duplicate allocation that's only needed for non-L1Msg variants. For L1Msg, the original inner is used directly (line 521), making the cloned morph_receipt unused.

♻️ Suggested optimization
             let inner = Receipt {
                 status: success.into(),
                 cumulative_gas_used,
                 logs: logs.into_owned(),
             };

-            let morph_receipt = MorphTransactionReceipt {
-                inner: inner.clone(),
-                l1_fee,
-                fee_token_id: fee_token_id.map(|id| id as u16),
-                fee_rate,
-                token_scale,
-                fee_limit,
-            };
-
             match tx_type {
-                MorphTxType::Legacy => Self::Legacy(morph_receipt),
-                MorphTxType::Eip2930 => Self::Eip2930(morph_receipt),
-                MorphTxType::Eip1559 => Self::Eip1559(morph_receipt),
-                MorphTxType::Eip7702 => Self::Eip7702(morph_receipt),
                 MorphTxType::L1Msg => Self::L1Msg(inner),
-                MorphTxType::AltFee => Self::AltFee(morph_receipt),
+                _ => {
+                    let morph_receipt = MorphTransactionReceipt {
+                        inner,
+                        l1_fee,
+                        fee_token_id: fee_token_id.map(|id| id as u16),
+                        fee_rate,
+                        token_scale,
+                        fee_limit,
+                    };
+                    match tx_type {
+                        MorphTxType::Legacy => Self::Legacy(morph_receipt),
+                        MorphTxType::Eip2930 => Self::Eip2930(morph_receipt),
+                        MorphTxType::Eip1559 => Self::Eip1559(morph_receipt),
+                        MorphTxType::Eip7702 => Self::Eip7702(morph_receipt),
+                        MorphTxType::AltFee => Self::AltFee(morph_receipt),
+                        MorphTxType::L1Msg => unreachable!(),
+                    }
+                }
             }
crates/consensus/src/validation.rs (1)

319-352: Avoid panic on malformed L1 messages.
expect("is_l1_msg") can panic on inconsistent input; prefer returning a consensus error to keep validation robust.

🔧 Proposed fix
-            let tx_queue_index = tx.queue_index().expect("is_l1_msg");
+            let tx_queue_index = tx.queue_index().ok_or_else(|| {
+                ConsensusError::Other("l1 message missing queue index".to_string())
+            })?;

Comment on lines 6 to 11
pub const MORPH_FEE_VAULT_ADDRESS_HOODI: Address =
address!("29107CB79Ef8f69fE1587F77e283d47E84c5202f");

/// The transaction fee recipient on the L2.
pub const MORPH_FEE_VAULT_ADDRESS_MAINNET: Address =
address!("530000000000000000000000000000000000000a");
Copy link
Contributor

Choose a reason for hiding this comment

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

need to use these constants outside the EVM?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

because we should define those const in chainspec and evm can use by dep chainspec, like go-ethereum in params/config.go

Copy link
Contributor

Choose a reason for hiding this comment

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

If we only use these in the EVM, put them in the EVM crate?

Comment on lines 24 to 27
pub const MORPH_BASE_FEE: u64 = 1_000_000;

/// The maximum allowed L2 base fee (10 Gwei = 10_000_000_000 wei).
pub const MORPH_MAX_BASE_FEE: u64 = 10_000_000_000;
Copy link
Contributor

Choose a reason for hiding this comment

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

ditto.

};

// Create the appropriate receipt variant based on transaction type
// TODO: Add L1 fee calculation from execution context
Copy link
Contributor

Choose a reason for hiding this comment

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

you can find this code in evm

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

🤖 Fix all issues with AI agents
In `@crates/consensus/src/validation.rs`:
- Around line 483-506: In verify_receipts replace the call to
alloy_consensus::proofs::calculate_receipt_root (currently used on
receipts_with_bloom created via TxReceipt::with_bloom_ref) with Morph's own
calculate_receipt_root that uses MorphReceipt's encode_2718 (the DA-compressed
no-bloom encoding) so the computed receipts root matches Morph's expectations;
locate the call in verify_receipts and swap it to call the
calculate_receipt_root implementation from crates::primitives::receipt (the
function referenced in crates/primitives/src/receipt/mod.rs:416-420) while
leaving the logs bloom combination logic (using TxReceipt::with_bloom_ref and
r.bloom_ref()) intact.
🧹 Nitpick comments (1)
crates/consensus/src/validation.rs (1)

561-573: Clarify L1 message test helper comment.

The comment at line 565 says "nonce is used as queue index for L1 messages", but the TxL1Msg struct has a separate queue_index field that's explicitly set. The nonce being equal to queue_index here appears to be a test convenience, not a protocol requirement.

Consider updating the comment to avoid confusion:

         let tx = TxL1Msg {
             queue_index,
             from: Address::ZERO,
-            nonce: queue_index, // nonce is used as queue index for L1 messages
+            nonce: queue_index, // Set equal to queue_index for test convenience
             gas_limit: 21000,

Comment on lines +483 to +506
fn verify_receipts(
expected_receipts_root: B256,
expected_logs_bloom: Bloom,
receipts: &[MorphReceipt],
) -> Result<(), ConsensusError> {
// Calculate receipts root
let receipts_with_bloom: Vec<_> = receipts.iter().map(TxReceipt::with_bloom_ref).collect();
let receipts_root = alloy_consensus::proofs::calculate_receipt_root(&receipts_with_bloom);

// Calculate logs bloom by combining all receipt blooms
let logs_bloom = receipts_with_bloom
.iter()
.fold(Bloom::ZERO, |bloom, r| bloom | r.bloom_ref());

// Compare receipts root
if receipts_root != expected_receipts_root {
return Err(ConsensusError::BodyReceiptRootDiff(
GotExpected {
got: receipts_root,
expected: expected_receipts_root,
}
.into(),
));
}
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:

cat -n crates/primitives/src/receipt/mod.rs | head -430 | tail -30

Repository: morph-l2/morph-reth

Length of output: 1180


🏁 Script executed:

# Find the calculate_receipt_root function in primitives
rg "fn calculate_receipt_root" -A 10

Repository: morph-l2/morph-reth

Length of output: 780


🏁 Script executed:

# Check the TxReceipt implementation and conversion
rg "struct TxReceipt" -A 5

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

# Check MorphReceipt definition
rg "struct MorphReceipt" -A 15

Repository: morph-l2/morph-reth

Length of output: 867


🏁 Script executed:

# Look at the with_bloom_ref method
rg "with_bloom_ref" -B 3 -A 5

Repository: morph-l2/morph-reth

Length of output: 782


🏁 Script executed:

# Check MorphReceipt structure and its relationship to TxReceipt
cat -n crates/primitives/src/receipt/mod.rs | head -100 | tail -50

Repository: morph-l2/morph-reth

Length of output: 2149


🏁 Script executed:

# Look for TxReceipt definition and with_bloom_ref
rg "impl.*TxReceipt" -A 20

Repository: morph-l2/morph-reth

Length of output: 2670


🏁 Script executed:

# Check encode_2718 method on MorphReceipt
rg "fn encode_2718" -B 2 -A 8

Repository: morph-l2/morph-reth

Length of output: 4636


🏁 Script executed:

# Check if there are other uses of calculate_receipt_root in the codebase
rg "calculate_receipt_root" -B 2 -A 2

Repository: morph-l2/morph-reth

Length of output: 1784


🏁 Script executed:

# Check if alloy_consensus::ReceiptWithBloom has special handling for MorphReceipt
rg "ReceiptWithBloom" -B 5 -A 15

Repository: morph-l2/morph-reth

Length of output: 19555


🏁 Script executed:

# Check the test to understand what the expected behavior is
cat -n crates/consensus/src/validation.rs | grep -A 20 "test_verify_receipts_empty"

Repository: morph-l2/morph-reth

Length of output: 994


🏁 Script executed:

# Examine the TxReceipt trait and with_bloom_ref method signature
rg "fn with_bloom_ref" -B 3 -A 10

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

# Check if Morph has custom implementations for ReceiptWithBloom or Encodable2718
rg "impl.*Encodable2718.*MorphReceipt" -A 10

Repository: morph-l2/morph-reth

Length of output: 923


🏁 Script executed:

# Check what methods are on ReceiptWithBloom and how it uses the inner type
rg "ReceiptWithBloom.*encode" -B 2 -A 5

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

# Verify the actual implementation difference between the two calculate_receipt_root functions
rg "ordered_trie_root" -B 3 -A 5

Repository: morph-l2/morph-reth

Length of output: 665


Use Morph's receipt root calculation to ensure DA-compressed encoding is applied.

The code converts MorphReceipt to ReceiptWithBloom and calls alloy_consensus::proofs::calculate_receipt_root, but MorphReceipt implements a custom encode_2718 that explicitly omits bloom for DA layer compression ("it can be recalculated from logs"). Morph defines its own calculate_receipt_root function (crates/primitives/src/receipt/mod.rs:416-420) that uses ordered_trie_root_with_encoder with this no-bloom encoding. Using the standard alloy calculation instead may produce a different receipt root. Replace with the Morph-specific implementation to ensure consistency with the intended DA compression.

🤖 Prompt for AI Agents
In `@crates/consensus/src/validation.rs` around lines 483 - 506, In
verify_receipts replace the call to
alloy_consensus::proofs::calculate_receipt_root (currently used on
receipts_with_bloom created via TxReceipt::with_bloom_ref) with Morph's own
calculate_receipt_root that uses MorphReceipt's encode_2718 (the DA-compressed
no-bloom encoding) so the computed receipts root matches Morph's expectations;
locate the call in verify_receipts and swap it to call the
calculate_receipt_root implementation from crates::primitives::receipt (the
function referenced in crates/primitives/src/receipt/mod.rs:416-420) while
leaving the logs bloom combination logic (using TxReceipt::with_bloom_ref and
r.bloom_ref()) intact.

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: 2

🤖 Fix all issues with AI agents
In `@crates/primitives/src/receipt/mod.rs`:
- Around line 524-531: The code silently truncates fee_token_id when mapping to
u16 in MorphTransactionReceipt; replace the unchecked cast fee_token_id.map(|id|
id as u16) with a guarded conversion using u16::try_from or id.try_into() and
handle failures (e.g., return an error, propagate Result, or at minimum
debug_assert! that id <= u16::MAX) so corrupted data doesn't get silently
truncated; update the construction of MorphTransactionReceipt and any callers to
propagate or handle the fallible conversion accordingly.
- Around line 409-412: The current InMemorySize implementation for MorphReceipt
calls self.as_receipt(), which drops L1/AltFee fields and undercounts memory for
non-L1Msg receipts; update InMemorySize::size for MorphReceipt to match on
MorphReceipt's variants (or otherwise access the embedded full receipt struct)
and delegate to the full receipt-type size calculation (not as_receipt()) so
L1/AltFee allocations are included—modify the size() implementation in the impl
for MorphReceipt to inspect its variants and call the correct full-size helper
for each variant.

Comment on lines +409 to +412
impl InMemorySize for MorphReceipt {
fn size(&self) -> usize {
self.as_receipt().size()
}
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

InMemorySize undercounts non‑L1Msg receipts.

as_receipt() drops L1/AltFee fields, so memory accounting ignores those allocations. This can skew cache sizing and metrics. Consider matching variants and delegating to the full receipt type size.

🔧 Suggested fix
 impl InMemorySize for MorphReceipt {
     fn size(&self) -> usize {
-        self.as_receipt().size()
+        match self {
+            MorphReceipt::L1Msg(r) => r.size(),
+            MorphReceipt::Legacy(r)
+            | MorphReceipt::Eip2930(r)
+            | MorphReceipt::Eip1559(r)
+            | MorphReceipt::Eip7702(r)
+            | MorphReceipt::AltFee(r) => r.size(),
+        }
     }
 }
🤖 Prompt for AI Agents
In `@crates/primitives/src/receipt/mod.rs` around lines 409 - 412, The current
InMemorySize implementation for MorphReceipt calls self.as_receipt(), which
drops L1/AltFee fields and undercounts memory for non-L1Msg receipts; update
InMemorySize::size for MorphReceipt to match on MorphReceipt's variants (or
otherwise access the embedded full receipt struct) and delegate to the full
receipt-type size calculation (not as_receipt()) so L1/AltFee allocations are
included—modify the size() implementation in the impl for MorphReceipt to
inspect its variants and call the correct full-size helper for each variant.

Comment on lines +524 to +531
let morph_receipt = MorphTransactionReceipt {
inner,
l1_fee,
fee_token_id: fee_token_id.map(|id| id as u16),
fee_rate,
token_scale,
fee_limit,
};
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

Avoid silent truncation of fee_token_id.

fee_token_id.map(|id| id as u16) will silently truncate if corrupted data exceeds u16::MAX. A guarded conversion (debug assert or fallible conversion) would make this safer.

✅ Safer conversion
-                fee_token_id: fee_token_id.map(|id| id as u16),
+                fee_token_id: fee_token_id.and_then(|id| {
+                    debug_assert!(id <= u16::MAX as u64);
+                    u16::try_from(id).ok()
+                }),
🤖 Prompt for AI Agents
In `@crates/primitives/src/receipt/mod.rs` around lines 524 - 531, The code
silently truncates fee_token_id when mapping to u16 in MorphTransactionReceipt;
replace the unchecked cast fee_token_id.map(|id| id as u16) with a guarded
conversion using u16::try_from or id.try_into() and handle failures (e.g.,
return an error, propagate Result, or at minimum debug_assert! that id <=
u16::MAX) so corrupted data doesn't get silently truncated; update the
construction of MorphTransactionReceipt and any callers to propagate or handle
the fallible conversion accordingly.

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: 5

🤖 Fix all issues with AI agents
In `@crates/chainspec/src/genesis.rs`:
- Around line 28-31: The current try_from in genesis.rs swallows errors from
MorphHardforkInfo::try_from(others) by calling .ok(), which hides malformed
hardfork data; change that to propagate the error instead (use the ? operator on
MorphHardforkInfo::try_from(others)) so deserialization failures surface during
genesis parsing; adjust subsequent logic that assumed Option (if any) to handle
the propagated Result or Option accordingly, referencing the try_from function,
MorphHardforkInfo, MorphChainConfig, and OtherFields to locate the code to
change.
- Around line 113-118: The try_from function currently treats any non-Ok result
as a missing field; change the matching on
others.get_deserialized::<Self>("morph") so that it explicitly handles three
cases: Some(Ok(morph_config)) -> return Ok(morph_config); Some(Err(e)) -> return
Err(e) (preserving and propagating the deserialization error from
get_deserialized); None -> return
Err(serde_json::Error::missing_field("morph")). Update the match in try_from
(referencing try_from, OtherFields, get_deserialized::<Self>, and the "morph"
field) to implement these branches so malformed morph JSON yields its original
error instead of a missing_field error.

In `@crates/chainspec/src/morph_hoodi.rs`:
- Around line 35-42: The test test_morph_hoodi_hardforks should not hardcode
timestamp 0 for Morph203; instead retrieve the configured activation time from
the MORPH_HOODI config and assert against that value. Replace the call to
MORPH_HOODI.is_morph203_active_at_timestamp(0) with a call that first obtains
the configured Morph203 activation timestamp from MORPH_HOODI (e.g., via a
getter/field such as morph203_activation_time, morph203.activation, or a method
like get_morph203_activation()) and then use that retrieved timestamp in the
is_morph203_active_at_timestamp check so the test follows hoodi.json
configuration.

In `@crates/chainspec/src/spec.rs`:
- Around line 41-46: In chain_value_parser, avoid panics by validating the
parsed genesis before calling MorphChainSpec::from: after calling
reth_cli::chainspec::parse_genesis(s) convert its result into MorphGenesisInfo
via MorphGenesisInfo::try_from(...) and return an Err if that conversion fails
(indicating the required "morph" field is missing); only on successful try_from
should you call MorphChainSpec::from(...) and wrap it in Arc::new(...). This
ensures chain_value_parser returns a proper eyre::Result rather than panicking
for non-Morph genesis files.
- Around line 121-125: The current From<ChainSpec> for MorphChainSpec discards
ChainSpec fields by converting only genesis; update it to start from the full
ChainSpec (preserving fields like bootnodes, deposit_contract, boot_nodes, and
any other preconfigured fields) and then apply the same Morph hardfork extension
logic used in the From<Genesis> implementation: build the base MorphChainSpec
from the ChainSpec (not just genesis), then extend base_spec.hardforks with the
Morph hardfork entries (block-based forks: Bernoulli and Curie; timestamp-based
forks: Morph203, Viridian, Emerald) using the same conditions and ordering as in
the From<Genesis> path so both original fields and Morph hardforks are
preserved.
♻️ Duplicate comments (2)
crates/consensus/src/validation.rs (2)

199-212: Confirm gas‑limit delta rules after Curie.
If post‑Curie blocks can set arbitrary gas limits, this unconditional delta check would still reject them—please confirm fork rules and gate if needed.


483-506: Use Morph receipt root calculation to match DA‑compressed encoding.
MorphReceipt’s custom 2718 encoding omits bloom; computing the root via alloy_consensus::proofs::calculate_receipt_root on ReceiptWithBloom can diverge. Use Morph’s calculate_receipt_root and keep the bloom aggregation as‑is; update tests that call the alloy function accordingly.

🛠️ Suggested fix
-    let receipts_root = alloy_consensus::proofs::calculate_receipt_root(&receipts_with_bloom);
+    let receipts_root = morph_primitives::receipt::calculate_receipt_root(receipts);

Comment on lines +28 to +31
fn try_from(others: &OtherFields) -> Result<Self, Self::Error> {
let hard_fork_info = MorphHardforkInfo::try_from(others).ok();
let morph_chain_info = MorphChainConfig::try_from(others)?;

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:

cd crates/chainspec && wc -l src/genesis.rs

Repository: morph-l2/morph-reth

Length of output: 81


🏁 Script executed:

cd crates/chainspec && sed -n '20,40p' src/genesis.rs

Repository: morph-l2/morph-reth

Length of output: 679


🏁 Script executed:

cd crates/chainspec && grep -n "MorphHardforkInfo" src/genesis.rs

Repository: morph-l2/morph-reth

Length of output: 425


🏁 Script executed:

cd crates/chainspec && grep -n "impl.*try_from" src/genesis.rs | head -20

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

cd crates/chainspec && sed -n '1,80p' src/genesis.rs

Repository: morph-l2/morph-reth

Length of output: 2860


🏁 Script executed:

cd crates/chainspec && sed -n '64,120p' src/genesis.rs

Repository: morph-l2/morph-reth

Length of output: 1945


🏁 Script executed:

cd crates/chainspec && rg "hard_fork_info" -A 2 -B 2

Repository: morph-l2/morph-reth

Length of output: 1135


Propagate deserialization errors for hardfork configuration.

When hardfork data is provided in the genesis file but malformed, .ok() silently converts the error to None, making it indistinguishable from an omitted field. This can cause data loss and incorrect fork schedules without alerting the user. Propagate errors instead so invalid hardfork config is caught during genesis parsing.

🛠️ Suggested fix
-        let hard_fork_info = MorphHardforkInfo::try_from(others).ok();
+        let hard_fork_info = Some(MorphHardforkInfo::try_from(others)?);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn try_from(others: &OtherFields) -> Result<Self, Self::Error> {
let hard_fork_info = MorphHardforkInfo::try_from(others).ok();
let morph_chain_info = MorphChainConfig::try_from(others)?;
fn try_from(others: &OtherFields) -> Result<Self, Self::Error> {
let hard_fork_info = Some(MorphHardforkInfo::try_from(others)?);
let morph_chain_info = MorphChainConfig::try_from(others)?;
🤖 Prompt for AI Agents
In `@crates/chainspec/src/genesis.rs` around lines 28 - 31, The current try_from
in genesis.rs swallows errors from MorphHardforkInfo::try_from(others) by
calling .ok(), which hides malformed hardfork data; change that to propagate the
error instead (use the ? operator on MorphHardforkInfo::try_from(others)) so
deserialization failures surface during genesis parsing; adjust subsequent logic
that assumed Option (if any) to handle the propagated Result or Option
accordingly, referencing the try_from function, MorphHardforkInfo,
MorphChainConfig, and OtherFields to locate the code to change.

Comment on lines +113 to +118
fn try_from(others: &OtherFields) -> Result<Self, Self::Error> {
if let Some(Ok(morph_config)) = others.get_deserialized::<Self>("morph") {
Ok(morph_config)
} else {
Err(serde_json::Error::missing_field("morph"))
}
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

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the file
find . -name "genesis.rs" -path "*/chainspec/*" | head -5

Repository: morph-l2/morph-reth

Length of output: 96


🏁 Script executed:

# Read the specific lines mentioned in the review
sed -n '105,125p' crates/chainspec/src/genesis.rs

Repository: morph-l2/morph-reth

Length of output: 577


🏁 Script executed:

# Get more context around the try_from implementation
sed -n '100,150p' crates/chainspec/src/genesis.rs

Repository: morph-l2/morph-reth

Length of output: 1501


🏁 Script executed:

# Search for get_deserialized to understand its return type
rg "get_deserialized" --type rust -B 2 -A 2

Repository: morph-l2/morph-reth

Length of output: 421


🏁 Script executed:

# Find the OtherFields type and get_deserialized method definition
rg "struct OtherFields" --type rust -A 5

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

# Search for the get_deserialized method implementation
rg "fn get_deserialized" --type rust -B 2 -A 10

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

# Also check the import statements to understand the types
head -30 crates/chainspec/src/genesis.rs

Repository: morph-l2/morph-reth

Length of output: 1179


🏁 Script executed:

# OtherFields is from alloy_serde, let's check if it's in the repo
find . -name "*.rs" | xargs grep -l "impl.*OtherFields" | head -5

Repository: morph-l2/morph-reth

Length of output: 96


🏁 Script executed:

# Let's search for documentation or usage patterns of get_deserialized
rg "get_deserialized" --type rust -B 5 -A 5 | head -100

Repository: morph-l2/morph-reth

Length of output: 775


🏁 Script executed:

# Check if there are tests for the MorphChainConfig::try_from
sed -n '120,200p' crates/chainspec/src/genesis.rs

Repository: morph-l2/morph-reth

Length of output: 2074


🏁 Script executed:

# Search for alloy_serde crate definition or get_deserialized method
rg "alloy_serde" --type rust | head -20

Repository: morph-l2/morph-reth

Length of output: 2128


🏁 Script executed:

# Let's check the Cargo.toml to see alloy_serde version
grep -A 5 "alloy-serde\|alloy_serde" Cargo.toml

Repository: morph-l2/morph-reth

Length of output: 251


🏁 Script executed:

# Search for usage of get_deserialized in other files to understand the pattern
find . -name "*.rs" -exec grep -l "get_deserialized" {} \;

Repository: morph-l2/morph-reth

Length of output: 96


🌐 Web query:

alloy-serde 1.1.3 get_deserialized method documentation

💡 Result:

I couldn’t find any get_deserialized method in the alloy-serde 1.1.3 API. The alloy-serde crate docs (crate page and module index) do not list a get_deserialized item. See the crate docs: [1], [2].

If you meant a different crate (or a different version) please tell me which one and I’ll look it up.

Sources:


🏁 Script executed:

# Let's also check if there's error handling testing for malformed morph config
sed -n '150,300p' crates/chainspec/src/genesis.rs

Repository: morph-l2/morph-reth

Length of output: 1270


🏁 Script executed:

# Search for the definition of get_deserialized in the codebase
rg "fn get_deserialized\|get_deserialized.*=" --type rust -B 3 -A 10

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

# Check if OtherFields is extended with get_deserialized somewhere
rg "impl.*OtherFields\|trait.*Ext" --type rust -B 2 -A 15

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

# Let's check the alloy dependencies more carefully
grep -B 5 -A 10 "alloy-serde" Cargo.toml | head -20

Repository: morph-l2/morph-reth

Length of output: 608


🏁 Script executed:

# Search for where get_deserialized might be defined as an extension
rg "get_deserialized" --type rust -B 10 | head -80

Repository: morph-l2/morph-reth

Length of output: 771


🏁 Script executed:

# Let's check what OtherFields is from alloy_serde - maybe it has get_deserialized
rg "OtherFields" --type rust -B 5 -A 10 | head -150

Repository: morph-l2/morph-reth

Length of output: 8301


🏁 Script executed:

# Check if get_deserialized might be defined as a trait method somewhere
rg "trait\|impl" --type rust crates/chainspec/src/genesis.rs | head -20

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

# Let's look at the entire genesis.rs file to understand imports and context
wc -l crates/chainspec/src/genesis.rs

Repository: morph-l2/morph-reth

Length of output: 98


🏁 Script executed:

# Read the entire file to understand structure
cat crates/chainspec/src/genesis.rs

Repository: morph-l2/morph-reth

Length of output: 6231


🏁 Script executed:

# Let's check if there are any tests that verify error handling for malformed morph config
rg "test.*morph\|#\[test\]" crates/chainspec/src/genesis.rs -A 30 | grep -A 30 "malformed\|invalid\|error"

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

# Also check if get_deserialized behavior is tested elsewhere
find . -name "*.rs" -exec grep -l "get_deserialized" {} \; | xargs grep -A 10 "get_deserialized" 2>/dev/null | head -100

Repository: morph-l2/morph-reth

Length of output: 312


🏁 Script executed:

# Search for trait extensions or methods on OtherFields
rg "OtherFields\|get_deserialized" --type rust | grep -i "trait\|extend\|impl" | head -20

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

# Check if there's an extension trait defined elsewhere in the codebase
find . -name "*.rs" -exec grep -l "trait.*Ext\|fn get_deserialized" {} \; 2>/dev/null

Repository: morph-l2/morph-reth

Length of output: 133


🏁 Script executed:

# Let's look at the alloy-serde source to understand OtherFields
python3 << 'PYTHON_EOF'
import json
import subprocess

# Try to find what the get_deserialized method actually returns
result = subprocess.run(
    ['rg', 'get_deserialized', '--type', 'rust', '-B', '5', '-A', '15'],
    capture_output=True,
    text=True
)

# Print just the method signature and usage
lines = result.stdout.split('\n')
for i, line in enumerate(lines):
    if 'get_deserialized' in line:
        # Print context around it
        start = max(0, i - 3)
        end = min(len(lines), i + 10)
        for j in range(start, end):
            print(lines[j])
        print("---")
PYTHON_EOF

Repository: morph-l2/morph-reth

Length of output: 149


🏁 Script executed:

# Let's check the usage in alt_fee.rs
grep -B 5 -A 10 "get_deserialized" crates/primitives/src/transaction/alt_fee.rs

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

# And check the usage in tx.rs
grep -B 5 -A 10 "get_deserialized" crates/revm/src/tx.rs

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

# Let's search for any trait definition that might provide get_deserialized
rg "trait\|impl.*OtherFields" --type rust -A 10 | head -100

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

# Check if it's imported from elsewhere or if there's a custom implementation
rg "get_deserialized|OtherFieldsExt" --type rust -B 10 -A 5

Repository: morph-l2/morph-reth

Length of output: 1052


🏁 Script executed:

# Let's examine the Cargo.lock or check what version of alloy-serde is actually used
find . -name "Cargo.lock" -exec head -200 {} \; | grep -A 5 "alloy-serde"

Repository: morph-l2/morph-reth

Length of output: 294


🏁 Script executed:

# Let's verify those files actually exist and contain get_deserialized
ls -la crates/primitives/src/transaction/alt_fee.rs crates/revm/src/tx.rs 2>&1 | head -10

Repository: morph-l2/morph-reth

Length of output: 229


🏁 Script executed:

# Search the entire repository for "get_deserialized" definition
rg "fn get_deserialized|get_deserialized\s*(" --type rust -B 2 -A 8 crates/

Repository: morph-l2/morph-reth

Length of output: 193


🏁 Script executed:

# Let's check if this is a method provided by OtherFields from alloy-serde
# by looking at how OtherFields is used in the test
grep -B 20 "OtherFields.*from_str\|serde_json::from_str.*OtherFields" crates/chainspec/src/genesis.rs | head -30

Repository: morph-l2/morph-reth

Length of output: 747


🏁 Script executed:

# Let's fix the regex and search properly
rg "fn get_deserialized" --type rust -B 2 -A 8

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

# Check if get_deserialized is defined in this repo or if it's external
rg "get_deserialized" --type rust | wc -l

Repository: morph-l2/morph-reth

Length of output: 64


🏁 Script executed:

# Let's look at what methods OtherFields provides - check if there's a trait
rg "deserialize_as|OtherFields" crates/chainspec/src/genesis.rs | head -20

Repository: morph-l2/morph-reth

Length of output: 830


🏁 Script executed:

# Let's look at what deserialize_as does in OtherFields
rg "deserialize_as" --type rust -B 5 -A 10 | head -100

Repository: morph-l2/morph-reth

Length of output: 1110


🏁 Script executed:

# Check if get_deserialized is perhaps a method that returns Option<Result>
# Let's look at actual trait implementations or check if this code even compiles
# by examining if there are any CI configs or test results
find . -name "*.toml" -path "*/chainspec*" | head -5

Repository: morph-l2/morph-reth

Length of output: 92


🏁 Script executed:

# Let's check if get_deserialized might be a custom method added to OtherFields somewhere in the codebase
rg "impl.*OtherFields\|trait.*OtherFields" --type rust -B 3 -A 20

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

# Let's check the Cargo.toml to see what alloy-serde provides
cat crates/chainspec/Cargo.toml

Repository: morph-l2/morph-reth

Length of output: 970


🏁 Script executed:

# Let's see if there's a way to test the malformed config scenario
grep -A 50 "test_extract_morph_chain_config" crates/chainspec/src/genesis.rs

Repository: morph-l2/morph-reth

Length of output: 1226


🏁 Script executed:

# Let's look for any error case tests for MorphChainConfig
grep -n "Err\|error\|malformed\|invalid" crates/chainspec/src/genesis.rs

Repository: morph-l2/morph-reth

Length of output: 515


Preserve deserialization errors for morph config.

When morph is present but malformed, the if let Some(Ok(...)) pattern fails to match Some(Err(...)), causing the code to fall through to the else branch and report "missing_field" instead of the actual deserialization error.

🛠️ Suggested fix
-        if let Some(Ok(morph_config)) = others.get_deserialized::<Self>("morph") {
-            Ok(morph_config)
-        } else {
-            Err(serde_json::Error::missing_field("morph"))
-        }
+        match others.get_deserialized::<Self>("morph") {
+            Some(Ok(morph_config)) => Ok(morph_config),
+            Some(Err(err)) => Err(err),
+            None => Err(serde_json::Error::missing_field("morph")),
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn try_from(others: &OtherFields) -> Result<Self, Self::Error> {
if let Some(Ok(morph_config)) = others.get_deserialized::<Self>("morph") {
Ok(morph_config)
} else {
Err(serde_json::Error::missing_field("morph"))
}
fn try_from(others: &OtherFields) -> Result<Self, Self::Error> {
match others.get_deserialized::<Self>("morph") {
Some(Ok(morph_config)) => Ok(morph_config),
Some(Err(err)) => Err(err),
None => Err(serde_json::Error::missing_field("morph")),
}
}
🤖 Prompt for AI Agents
In `@crates/chainspec/src/genesis.rs` around lines 113 - 118, The try_from
function currently treats any non-Ok result as a missing field; change the
matching on others.get_deserialized::<Self>("morph") so that it explicitly
handles three cases: Some(Ok(morph_config)) -> return Ok(morph_config);
Some(Err(e)) -> return Err(e) (preserving and propagating the deserialization
error from get_deserialized); None -> return
Err(serde_json::Error::missing_field("morph")). Update the match in try_from
(referencing try_from, OtherFields, get_deserialized::<Self>, and the "morph"
field) to implement these branches so malformed morph JSON yields its original
error instead of a missing_field error.

Comment on lines +35 to +42
#[test]
fn test_morph_hoodi_hardforks() {
// Block-based hardforks should be active at block 0
assert!(MORPH_HOODI.is_bernoulli_active_at_block(0));
assert!(MORPH_HOODI.is_curie_active_at_block(0));
// Timestamp-based hardforks should be active at timestamp 0
assert!(MORPH_HOODI.is_morph203_active_at_timestamp(0));
// Note: Viridian and Emerald may not be active at timestamp 0 on Hoodi
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

Use configured Morph203 activation time in the test.

Hardcoding timestamp 0 makes this test brittle if hoodi.json changes.

🛠️ Suggested fix
-        // Timestamp-based hardforks should be active at timestamp 0
-        assert!(MORPH_HOODI.is_morph203_active_at_timestamp(0));
+        // Timestamp-based hardforks should be active at their configured timestamp
+        let morph203_ts = MORPH_HOODI
+            .info
+            .hard_fork_info
+            .as_ref()
+            .and_then(|h| h.morph203_time)
+            .expect("morph203Time missing in hoodi genesis");
+        assert!(MORPH_HOODI.is_morph203_active_at_timestamp(morph203_ts));
🤖 Prompt for AI Agents
In `@crates/chainspec/src/morph_hoodi.rs` around lines 35 - 42, The test
test_morph_hoodi_hardforks should not hardcode timestamp 0 for Morph203; instead
retrieve the configured activation time from the MORPH_HOODI config and assert
against that value. Replace the call to
MORPH_HOODI.is_morph203_active_at_timestamp(0) with a call that first obtains
the configured Morph203 activation timestamp from MORPH_HOODI (e.g., via a
getter/field such as morph203_activation_time, morph203.activation, or a method
like get_morph203_activation()) and then use that retrieved timestamp in the
is_morph203_active_at_timestamp check so the test follows hoodi.json
configuration.

Comment on lines 41 to +46
pub fn chain_value_parser(s: &str) -> eyre::Result<Arc<MorphChainSpec>> {
Ok(MorphChainSpec::from_genesis(reth_cli::chainspec::parse_genesis(s)?).into())
Ok(match s {
"mainnet" => MORPH_MAINNET.clone(),
"hoodi" => MORPH_HOODI.clone(),
_ => Arc::new(MorphChainSpec::from(reth_cli::chainspec::parse_genesis(s)?)),
})
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:

find crates -name "*.rs" -type f | head -20

Repository: morph-l2/morph-reth

Length of output: 662


🏁 Script executed:

rg "struct MorphChainSpec" --type rust -B 2 -A 5

Repository: morph-l2/morph-reth

Length of output: 989


🏁 Script executed:

rg "impl.*From.*MorphChainSpec" --type rust -B 2 -A 20

Repository: morph-l2/morph-reth

Length of output: 2039


🏁 Script executed:

rg "struct MorphGenesisInfo" --type rust -B 2 -A 5

Repository: morph-l2/morph-reth

Length of output: 722


🏁 Script executed:

rg "MorphGenesisInfo::try_from" --type rust -B 2 -A 10

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

rg "extract_from|try_from" crates/chainspec/src/genesis.rs -B 2 -A 10

Repository: morph-l2/morph-reth

Length of output: 3342


🏁 Script executed:

rg "impl.*MorphGenesisInfo" crates/chainspec/src/genesis.rs -B 2 -A 30

Repository: morph-l2/morph-reth

Length of output: 1461


🏁 Script executed:

cat -n crates/chainspec/src/spec.rs | head -80

Repository: morph-l2/morph-reth

Length of output: 3155


🏁 Script executed:

cat -n crates/chainspec/src/genesis.rs

Repository: morph-l2/morph-reth

Length of output: 7540


🏁 Script executed:

sed -n '60,90p' crates/chainspec/src/spec.rs

Repository: morph-l2/morph-reth

Length of output: 973


🏁 Script executed:

sed -n '90,130p' crates/chainspec/src/spec.rs

Repository: morph-l2/morph-reth

Length of output: 1390


🏁 Script executed:

sed -n '130,160p' crates/chainspec/src/spec.rs

Repository: morph-l2/morph-reth

Length of output: 1379


Prevent CLI panic on invalid or non-Morph genesis.

User-supplied inputs can panic because MorphChainSpec::from(genesis) expects Morph-specific fields in the genesis config. When parsing custom genesis files (lines 41-46), validate with MorphGenesisInfo::try_from() to ensure the required "morph" field exists before converting to MorphChainSpec.

🛠️ Suggested fix
-        _ => Arc::new(MorphChainSpec::from(reth_cli::chainspec::parse_genesis(s)?)),
+        _ => {
+            let genesis = reth_cli::chainspec::parse_genesis(s)?;
+            MorphGenesisInfo::try_from(&genesis.config.extra_fields)
+                .map_err(|e| eyre::eyre!("invalid morph genesis: {e}"))?;
+            Arc::new(MorphChainSpec::from(genesis))
+        }
🤖 Prompt for AI Agents
In `@crates/chainspec/src/spec.rs` around lines 41 - 46, In chain_value_parser,
avoid panics by validating the parsed genesis before calling
MorphChainSpec::from: after calling reth_cli::chainspec::parse_genesis(s)
convert its result into MorphGenesisInfo via MorphGenesisInfo::try_from(...) and
return an Err if that conversion fails (indicating the required "morph" field is
missing); only on successful try_from should you call MorphChainSpec::from(...)
and wrap it in Arc::new(...). This ensures chain_value_parser returns a proper
eyre::Result rather than panicking for non-Morph genesis files.

Comment on lines +121 to +125
impl From<ChainSpec> for MorphChainSpec {
fn from(value: ChainSpec) -> Self {
let genesis = value.genesis;
genesis.into()
}
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:

cd crates/chainspec && wc -l src/spec.rs

Repository: morph-l2/morph-reth

Length of output: 78


🏁 Script executed:

# First, let's examine the full context around the implementation
cd crates/chainspec && sed -n '100,150p' src/spec.rs

Repository: morph-l2/morph-reth

Length of output: 2033


🏁 Script executed:

# Search for MorphChainSpec structure definition
cd crates/chainspec && rg -A 10 "struct MorphChainSpec" src/

Repository: morph-l2/morph-reth

Length of output: 1061


🏁 Script executed:

# Search for ChainSpec structure
cd crates/chainspec && rg -A 15 "pub struct ChainSpec" src/

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

# Look for MorphGenesisInfo and its extract_from method
cd crates/chainspec && rg -B 2 -A 10 "impl MorphGenesisInfo" src/

Repository: morph-l2/morph-reth

Length of output: 613


🏁 Script executed:

cd crates/chainspec && rg "MorphChainSpec::from|\.into\(\)" -B 3 -A 3 --type rust | head -100

Repository: morph-l2/morph-reth

Length of output: 4684


🏁 Script executed:

# Get the complete From<Genesis> implementation
cd crates/chainspec && sed -n '137,200p' src/spec.rs

Repository: morph-l2/morph-reth

Length of output: 2000


🏁 Script executed:

# Search for ChainSpec usages that might be converted to MorphChainSpec
cd crates && rg "From<ChainSpec>" -l

Repository: morph-l2/morph-reth

Length of output: 84


🏁 Script executed:

cd crates && rg "pub struct ChainSpec" -A 20 | head -40

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

# Check if there are any other ChainSpec usages that convert to MorphChainSpec
cd crates && rg "parse_genesis" -B 2 -A 2

Repository: morph-l2/morph-reth

Length of output: 350


🏁 Script executed:

# Try to find ChainSpec definition in dependencies
cd crates && find . -name "*.rs" -type f | xargs grep -l "pub struct ChainSpec" | head -5

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

# Check what parse_genesis returns by looking at its definition
cd crates && rg "pub fn parse_genesis|fn parse_genesis" -A 5

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

# Look for ChainSpec definition in reth crates (if available locally)
cd crates && rg "struct ChainSpec" -B 2 -A 10 | head -60

Repository: morph-l2/morph-reth

Length of output: 45


🏁 Script executed:

# Let's check the test cases to understand the actual issue better
cd crates/chainspec && sed -n '260,330p' src/spec.rs

Repository: morph-l2/morph-reth

Length of output: 2517


🏁 Script executed:

# Look at create_test_genesis to see what structure is created
cd crates/chainspec && rg -B 5 -A 15 "fn create_test_genesis"

Repository: morph-l2/morph-reth

Length of output: 1079


🏁 Script executed:

# Check Cargo.toml to understand dependencies
cd crates/chainspec && cat Cargo.toml | grep -A 30 "\[dependencies\]"

Repository: morph-l2/morph-reth

Length of output: 755


🏁 Script executed:

# Let's see the complete flow: what is parse_genesis returning as ChainSpec
cd crates/chainspec && rg "parse_genesis" -B 10 -A 10 src/spec.rs

Repository: morph-l2/morph-reth

Length of output: 792


🏁 Script executed:

# Check if there's any documentation about what fields might be lost
cd crates/chainspec && rg "bootnodes|deposit" -i

Repository: morph-l2/morph-reth

Length of output: 532


🏁 Script executed:

# Let's look for any issue with the suggested fix - check if it preserves hardforks
cd crates/chainspec && sed -n '121,165p' src/spec.rs

Repository: morph-l2/morph-reth

Length of output: 1688


🏁 Script executed:

cd crates/chainspec && sed -n '145,165p' src/spec.rs

Repository: morph-l2/morph-reth

Length of output: 722


🏁 Script executed:

# Check if ChainSpec has hardforks field that might be separate from genesis
cd crates && rg "ChainSpec::from_genesis" -B 3 -A 10

Repository: morph-l2/morph-reth

Length of output: 1063


Preserve ChainSpec fields when converting and apply Morph hardforks.

From<ChainSpec> currently discards the original ChainSpec and rebuilds from only genesis, losing preconfigured fields like bootnodes and deposit contract. Additionally, the suggested fix in the code review is incomplete—it would preserve the original ChainSpec but skip applying Morph hardforks, which the current From<Genesis> implementation adds via base_spec.hardforks.extend(). The fix must both preserve the original spec and apply the Morph hardfork logic (block-based: Bernoulli/Curie; timestamp-based: Morph203/Viridian/Emerald).

🤖 Prompt for AI Agents
In `@crates/chainspec/src/spec.rs` around lines 121 - 125, The current
From<ChainSpec> for MorphChainSpec discards ChainSpec fields by converting only
genesis; update it to start from the full ChainSpec (preserving fields like
bootnodes, deposit_contract, boot_nodes, and any other preconfigured fields) and
then apply the same Morph hardfork extension logic used in the From<Genesis>
implementation: build the base MorphChainSpec from the ChainSpec (not just
genesis), then extend base_spec.hardforks with the Morph hardfork entries
(block-based forks: Bernoulli and Curie; timestamp-based forks: Morph203,
Viridian, Emerald) using the same conditions and ordering as in the
From<Genesis> path so both original fields and Morph hardforks are preserved.

@panos-xyz panos-xyz merged commit 4e6c477 into main Jan 20, 2026
9 checks passed
@panos-xyz panos-xyz deleted the feat/morph-receipt branch January 20, 2026 10:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants