Skip to content

test: add comprehensive test coverage and E2E integration tests#55

Open
panos-xyz wants to merge 3 commits intomainfrom
panos/test
Open

test: add comprehensive test coverage and E2E integration tests#55
panos-xyz wants to merge 3 commits intomainfrom
panos/test

Conversation

@panos-xyz
Copy link
Contributor

@panos-xyz panos-xyz commented Mar 18, 2026

Summary

  • Add unit tests across all morph-reth crates (chainspec, primitives, revm, evm, consensus, txpool, payload-builder, payload-types, engine-api, node, rpc) — 500+ tests total
  • Add E2E integration test (can_sync) that starts an ephemeral Morph node, produces 10 blocks via Engine API, and verifies chain advancement
  • Add test_utils module with setup(), advance_chain() helpers and test-utils feature gate for E2E test dependencies
  • Add From<EthPayloadBuilderAttributes> for MorphPayloadBuilderAttributes to enable reth E2E test framework compatibility

Test Plan

  • cargo test --all — all unit tests pass (500+ tests, 0 failures)
  • cargo test --package morph-node --test it --features test-utils -- sync::can_sync — E2E integration test passes
  • cargo check --all --tests — compilation clean (excluding pre-existing upstream reth-ethereum-primitives serde issue)

Summary by CodeRabbit

  • Tests
    • Vast expansion of unit and integration tests across consensus, EVM, payloads, RPCs, tx pool, receipts, and precompiles to improve reliability and edge-case coverage.
  • New Features
    • Added end-to-end test utilities and an integration test suite for node syncing and chain advancement.
  • Chores
    • Introduced a dev-only "test-utils" feature to enable the new E2E/testing helpers and workspace test dependencies.

- Add unit tests across all morph-reth crates (primitives, chainspec,
  revm, evm, txpool, consensus, payload-builder, payload-types,
  engine-api, node, rpc)
- Add E2E integration test (can_sync) that starts an ephemeral Morph
  node, produces 10 blocks via Engine API, and verifies chain advancement
- Add test_utils module with setup(), advance_chain(), and
  morph_payload_attributes() helpers following scroll-reth's pattern
- Add From<EthPayloadBuilderAttributes> for MorphPayloadBuilderAttributes
  to enable reth E2E test framework compatibility
- Add test-utils feature gate for E2E test dependencies
@coderabbitai
Copy link

coderabbitai bot commented Mar 18, 2026

📝 Walkthrough

Walkthrough

Adds extensive unit and integration tests across many crates (chainspec, consensus, engine-api, evm, payload, rpc, node, revm, txpool, primitives, etc.), plus a test-utils module and a new integration test; no public API or runtime behavior changes.

Changes

Cohort / File(s) Summary
Chainspec Constants & Hardforks
crates/chainspec/src/constants.rs, crates/chainspec/src/hardfork.rs
Added unit tests for chain IDs, genesis hashes/state roots, hardfork mapping, ordering, and related constants.
Consensus & Block Validation
crates/consensus/src/validation.rs
Expanded tests covering L1 message ordering/indexing, coinbase/FeeVault checks, MorphTx version and memo constraints, and post-execution gas/receipt validations.
Engine API
crates/engine-api/src/builder.rs, crates/engine-api/src/error.rs, crates/engine-api/src/validator.rs
Tests for InMemoryHead equality, EngineStateTracker behaviors, apply_executable_data_overrides edge cases, error conversions, and state-root validation at Jade boundaries.
EVM Execution & Block Assembly
crates/evm/src/assemble.rs, crates/evm/src/block/receipt.rs, crates/evm/src/config.rs
Tests for MorphBlockAssembler (Arc sharing, clone), extensive MorphReceiptBuilder paths (Legacy/EIP-1559/L1Msg/Morph), and EVM env/config behaviors (chain_id, basefee, blob placeholders).
Revm Core Logic
crates/revm/src/error.rs, crates/revm/src/evm.rs, crates/revm/src/l1block.rs, crates/revm/src/precompiles.rs, crates/revm/src/token_fee.rs
Added tests for invalid-transaction errors, BLOCKHASH semantics/determinism, L1 gas/cost calculations across forks, precompile availability/limits per hardfork, and token fee conversions/rounding.
Node Setup & Integration
crates/node/Cargo.toml, crates/node/src/lib.rs, crates/node/src/args.rs, crates/node/src/node.rs, crates/node/src/validator.rs, crates/node/tests/it/main.rs, crates/node/tests/it/sync.rs
Introduced test-utils feature, integration test harness, unit tests for MorphArgs/MorphNode/payload attributes/timestamps, and an E2E sync integration test.
Test Utilities Module
crates/node/src/test_utils.rs
New public test helpers: MorphTestNode alias, setup, advance_chain, morph_payload_attributes, and a signed-transfer helper for E2E tests.
Payload Builder & Types
crates/payload/builder/src/builder.rs, crates/payload/types/src/attributes.rs, crates/payload/types/src/executable_l2_data.rs, crates/payload/types/src/lib.rs, crates/payload/types/src/params.rs, crates/payload/types/src/safe_l2_data.rs
Extensive tests for ExecutionInfo, payload builder/context, withdraw trie root handling (MockDb), serde round-trips (including large base fees), From for MorphPayloadBuilderAttributes, and AssembleL2BlockParams behavior.
Primitives
crates/primitives/src/header.rs, crates/primitives/src/receipt/envelope.rs
Added tests for MorphHeader RLP round-trips, size/in-memory calculations, HeaderMut trait, and comprehensive MorphReceiptEnvelope encoding/decoding across variants.
RPC Types & Error Handling
crates/rpc/Cargo.toml, crates/rpc/src/error.rs, crates/rpc/src/eth/receipt.rs, crates/rpc/src/eth/transaction.rs, crates/rpc/src/types/receipt.rs, crates/rpc/src/types/request.rs
Tests for MorphEthApiError display/conversion, morph receipt extraction, morph_envelope and MorphTransactionRequest conversions/serde, and Morph RPC receipt serde/field behavior.
Transaction Pool
crates/txpool/src/error.rs, crates/txpool/src/morph_tx_validation.rs, crates/txpool/src/transaction.rs
Error-conversion and display tests, expanded MorphTx validation scenarios (fee-token vs ETH-fee, insufficient balances), and MorphPooledTransaction creation/encoding tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • anylots
  • chengwenxi

Poem

🐰 I hopped through tests both far and near,
I checked each constant, hash, and ledger cheer,
From builders to nodes, receipts and fees,
I nibbled bugs and left good guarantees,
A tiny rabbit, proud—your tests are clear! 🎉

🚥 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 title 'test: add comprehensive test coverage and E2E integration tests' is directly and clearly related to the main changes in the PR, which adds 500+ unit tests across multiple crates and new E2E integration tests.
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
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch panos/test
📝 Coding Plan
  • Generate coding plan for human review comments

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

🧹 Nitpick comments (9)
crates/revm/src/error.rs (2)

158-162: test_into_evm_error can validate the wrapped payload too.

Matching only EVMError::Transaction(_) is good, but checking the inner error value would make this assertion more robust.

Optional tightening
     fn test_into_evm_error() {
         let morph_err = MorphInvalidTransaction::TokenNotRegistered(1);
         let evm_err: EVMError<std::convert::Infallible, MorphInvalidTransaction> = morph_err.into();
-        assert!(matches!(evm_err, EVMError::Transaction(_)));
+        assert!(matches!(
+            evm_err,
+            EVMError::Transaction(MorphInvalidTransaction::TokenNotRegistered(1))
+        ));
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/revm/src/error.rs` around lines 158 - 162, The test
test_into_evm_error currently only asserts the variant EVMError::Transaction(_)
— update it to also extract and assert the wrapped payload equals the original
MorphInvalidTransaction::TokenNotRegistered(1); specifically, match or if-let on
evm_err (EVMError::Transaction(inner)) and assert_eq!(inner,
MorphInvalidTransaction::TokenNotRegistered(1)) so the inner value is validated
as well.

111-117: Consider asserting the full InsufficientTokenBalance message.

Using exact string comparison here would make this test stricter and reduce false positives from partial matches.

Optional tightening
-        assert!(err.to_string().contains("100"));
-        assert!(err.to_string().contains("50"));
+        assert_eq!(
+            err.to_string(),
+            "Insufficient token balance for gas payment: required 100, available 50"
+        );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/revm/src/error.rs` around lines 111 - 117, Replace the two partial
contains assertions with a single exact-string assertion for the full error
message: build the expected string that
MorphInvalidTransaction::InsufficientTokenBalance produces (including the
required and available U256 values) and assert_eq!(err.to_string(), expected) so
the test verifies the entire error formatting rather than only substrings;
target the test that constructs err from
MorphInvalidTransaction::InsufficientTokenBalance and replaces the two
assert!(err.to_string().contains(...)) calls with the single exact equality
check against the constructed expected string.
crates/evm/src/assemble.rs (1)

136-171: Consider extracting shared test helper to avoid duplication.

The create_test_chainspec() helper is duplicated nearly identically in config.rs. Consider extracting it to a shared test_utils module within the crate or a #[cfg(test)] public helper to reduce maintenance burden and ensure consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/evm/src/assemble.rs` around lines 136 - 171, Tests duplicate the
create_test_chainspec() helper between assemble.rs and config.rs; extract it
into a single shared test helper (e.g., a new #[cfg(test)] mod test_utils with a
pub fn create_test_chainspec() -> Arc<MorphChainSpec>) and import it from both
test modules to avoid duplication. Move the genesis_json creation and
Genesis->MorphChainSpec conversion into that helper, keep it behind #[cfg(test)]
so it’s not compiled in production, and update references in assemble.rs and
config.rs to call test_utils::create_test_chainspec() (or use a use statement)
so both test modules reuse the single implementation.
crates/txpool/src/transaction.rs (1)

326-332: test_encoded_2718_is_cached checks content equality, not cache reuse.

On Line 330, identical Bytes content can still pass even if encoding is recomputed. Consider asserting pointer identity between two encoded_2718() calls to verify actual cache reuse.

Suggested tightening for cache verification
     #[test]
     fn test_encoded_2718_is_cached() {
         let tx = create_legacy_pooled_tx();
-        let bytes1 = tx.encoded_2718().clone();
-        let bytes2 = tx.encoded_2718().clone();
-        assert_eq!(bytes1, bytes2, "cached encoding should be identical");
-        assert!(!bytes1.is_empty());
+        let bytes1 = tx.encoded_2718();
+        let bytes2 = tx.encoded_2718();
+        assert_eq!(bytes1, bytes2, "cached encoding should be identical");
+        assert!(
+            core::ptr::eq(bytes1.as_ptr(), bytes2.as_ptr()),
+            "expected the same cached buffer instance"
+        );
+        assert!(!bytes1.is_empty());
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/txpool/src/transaction.rs` around lines 326 - 332, The test
test_encoded_2718_is_cached currently compares content equality which can
succeed even if encoding is recomputed; update it to assert that the cached
Bytes are the same allocation by comparing pointer identity from two calls to
encoded_2718() (e.g., compare bytes1.as_ptr() == bytes2.as_ptr() and lengths) so
the test verifies reuse of the cached buffer rather than just equal content;
locate the test and replace or augment the assert_eq!(bytes1, bytes2) with a
pointer identity check on encoded_2718().
crates/engine-api/src/builder.rs (2)

1151-1183: Consider renaming test to match actual behavior.

The test name ignores_non_canonical_events implies it sends non-canonical events and verifies they're ignored, but it actually only tests that CanonicalChainCommitted updates the head (as the comment at lines 1157-1161 acknowledges). A clearer name like test_engine_state_tracker_updates_only_on_canonical_commit would better describe what's being verified.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/engine-api/src/builder.rs` around lines 1151 - 1183, Rename the test
function to accurately reflect its behavior: change fn
test_engine_state_tracker_ignores_non_canonical_events() to a clearer name such
as fn test_engine_state_tracker_updates_only_on_canonical_commit() (or similar)
and update any inline comment if needed; this makes the intent explicit for
EngineStateTracker and its methods current_head() and
on_consensus_engine_event() which are being validated by sending a
ConsensusEngineEvent::CanonicalChainCommitted.

1185-1195: Test doesn't verify actual concurrency.

The test claims to verify concurrent reads but performs sequential reads on a single thread. To truly test concurrent read behavior, you'd need multiple threads accessing current_head() simultaneously. As-is, this only verifies read consistency across sequential calls.

This is fine for basic sanity checking, but consider renaming to test_engine_state_tracker_multiple_reads_consistent or adding actual thread-based concurrency if the original intent matters.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/engine-api/src/builder.rs` around lines 1185 - 1195, The test
test_engine_state_tracker_concurrent_reads does not actually exercise
concurrency because it calls EngineStateTracker::current_head sequentially on
one thread; either rename the test to something like
test_engine_state_tracker_multiple_reads_consistent to reflect its current
behavior, or change the test to spawn multiple threads that concurrently call
EngineStateTracker::current_head after initializing state with record_local_head
to verify no panics and consistent results across threads; update assertions to
collect and compare results from the spawned threads and join them to ensure
true concurrent access was exercised.
crates/primitives/src/receipt/envelope.rs (1)

417-451: Add missing 2718 roundtrip coverage for Eip2930 and Eip7702 (and consider consolidating).

Line 427 and Line 445 currently roundtrip only a subset of variants. Adding explicit 2718 roundtrip checks for MorphTxType::Eip2930 and MorphTxType::Eip7702 would complete the matrix; this is also a good place to reduce test duplication.

♻️ Suggested consolidation with full variant coverage
-    #[test]
-    fn test_eip2718_roundtrip_legacy() {
-        let receipt = create_test_receipt(MorphTxType::Legacy);
-        let mut buf = Vec::new();
-        receipt.encode_2718(&mut buf);
-        let decoded = MorphReceiptEnvelope::decode_2718(&mut buf.as_slice()).unwrap();
-        assert_eq!(receipt, decoded);
-    }
-
-    #[test]
-    fn test_eip2718_roundtrip_eip1559() {
-        let receipt = create_test_receipt(MorphTxType::Eip1559);
-        let mut buf = Vec::new();
-        receipt.encode_2718(&mut buf);
-        let decoded = MorphReceiptEnvelope::decode_2718(&mut buf.as_slice()).unwrap();
-        assert_eq!(receipt, decoded);
-    }
-
-    #[test]
-    fn test_eip2718_roundtrip_l1msg() {
-        let receipt = create_test_receipt(MorphTxType::L1Msg);
-        let mut buf = Vec::new();
-        receipt.encode_2718(&mut buf);
-        let decoded = MorphReceiptEnvelope::decode_2718(&mut buf.as_slice()).unwrap();
-        assert_eq!(receipt, decoded);
-    }
-
-    #[test]
-    fn test_eip2718_roundtrip_morph() {
-        let receipt = create_test_receipt(MorphTxType::Morph);
-        let mut buf = Vec::new();
-        receipt.encode_2718(&mut buf);
-        let decoded = MorphReceiptEnvelope::decode_2718(&mut buf.as_slice()).unwrap();
-        assert_eq!(receipt, decoded);
-    }
+    #[test]
+    fn test_eip2718_roundtrip_all_variants() {
+        for tx_type in [
+            MorphTxType::Legacy,
+            MorphTxType::Eip2930,
+            MorphTxType::Eip1559,
+            MorphTxType::Eip7702,
+            MorphTxType::L1Msg,
+            MorphTxType::Morph,
+        ] {
+            let receipt = create_test_receipt(tx_type);
+            let mut buf = Vec::new();
+            receipt.encode_2718(&mut buf);
+            let decoded = MorphReceiptEnvelope::decode_2718(&mut buf.as_slice()).unwrap();
+            assert_eq!(receipt, decoded);
+        }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/primitives/src/receipt/envelope.rs` around lines 417 - 451, The test
suite is missing Eip2930 and Eip7702 coverage for the 2718 roundtrip; update the
tests so that MorphTxType::Eip2930 and MorphTxType::Eip7702 are also encoded
with encode_2718 and decoded with MorphReceiptEnvelope::decode_2718 and asserted
equal to the original receipt; to avoid duplication replace the four separate
functions (test_eip2718_roundtrip_legacy, _eip1559, _l1msg, _morph) with a
single parameterized loop (or a helper function) that iterates over all
MorphTxType variants from create_test_receipt and performs the encode_2718 ->
decode_2718 -> assert_eq roundtrip for each variant.
crates/node/Cargo.toml (1)

59-74: Move serde_json to the test-utils feature as an optional dependency.

Currently serde_json is unconditionally included in regular dependencies (line 59) but only used in test_utils.rs behind #[cfg(feature = "test-utils")]. Add "dep:serde_json" to the test-utils feature block and move the line 59 declaration to:

serde_json = { workspace = true, optional = true }

Remove it from line 74 (dev-dependencies) since the feature will handle it. This prevents unnecessary dependency bloat in default builds.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/node/Cargo.toml` around lines 59 - 74, Update Cargo.toml so serde_json
is an optional dependency tied to the test-utils feature: change the top-level
declaration `serde_json.workspace = true` to `serde_json = { workspace = true,
optional = true }`, add `"dep:serde_json"` to the `test-utils` feature entry,
and remove the `serde_json.workspace = true` line from the [dev-dependencies]
block; reference the `test-utils` feature and the `serde_json` dependency to
locate edits.
crates/rpc/src/eth/transaction.rs (1)

651-715: Add a boundary test for fee_token_id overflow.

The new try_build_morph_tx_from_request tests are solid, but the explicit conversion failure path (u16::try_from(...)) is still untested. A case with fee_token_id > u16::MAX would lock in this validation behavior.

➕ Suggested test case
+    #[test]
+    fn try_build_morph_tx_rejects_fee_token_id_overflow() {
+        let req = create_basic_transaction_request();
+        let overflow_id = U64::from((u16::MAX as u64) + 1);
+        let result = try_build_morph_tx_from_request(&req, overflow_id, U256::ZERO, None, None);
+        assert!(result.is_err());
+        assert!(result.unwrap_err().contains("invalid token"));
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/rpc/src/eth/transaction.rs` around lines 651 - 715, Add a boundary
test calling try_build_morph_tx_from_request with a request whose fee_token_id
value exceeds u16::MAX (e.g., U64::from(u16::MAX as u64 + 1)) and assert the
call returns Err and the error message contains the conversion failure; place
this new test alongside the other tests in the same module (use
create_basic_transaction_request() to build the request and call
try_build_morph_tx_from_request with U64::from(u16::MAX + 1) to exercise the
u16::try_from(...) failure path).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/consensus/src/validation.rs`:
- Around line 1487-1516: The test currently only checks for a rejection when the
default create_test_chainspec() happens to have FeeVault enabled, making it a
no-op otherwise; modify the
test_validate_header_coinbase_non_zero_with_fee_vault to construct an explicit
Chainspec with FeeVault enabled (either by calling a helper like
create_test_chainspec_with_fee_vault(true) or by mutating the returned chainspec
to set fee_vault enabled) and instantiate MorphConsensus from that explicit
chainspec, then call consensus.validate_header(&sealed) and assert the result is
an Err unconditionally (and check the error string contains "coinbase");
reference functions/values: create_test_chainspec, MorphConsensus::new,
consensus.chain_spec().is_fee_vault_enabled(), and
test_validate_header_coinbase_non_zero_with_fee_vault when making the change.

In `@crates/evm/src/block/receipt.rs`:
- Around line 406-457: The tests test_build_morph_tx_receipt_with_fields and
test_build_morph_tx_receipt_without_fields_fallback currently only check the
variant and l1_fee; update them to pattern-match/destructure the
MorphReceipt::Morph payload returned by
DefaultMorphReceiptBuilder::build_receipt and assert the MorphTx-specific fields
(fee_token_id, fee_rate, token_scale, fee_limit, reference, memo) are present
and equal to the values in MorphReceiptTxFields when morph_tx_fields is Some,
and that those fields are absent or defaulted when morph_tx_fields is None
(verifying the fallback path). Locate the receipt variable in each test, match
it to MorphReceipt::Morph(inner) (or equivalent accessor), and add assertions
against inner.* fields (or the builder helper with_morph_tx_v1() output) instead
of only checking l1_fee and variant.
- Around line 385-403: The test test_build_l1_msg_receipt_no_l1_fee currently
sets MorphReceiptBuilderCtx.l1_fee to U256::ZERO which doesn't catch accidental
routing through with_l1_fee(); change the seeded l1_fee to a non-zero value
(e.g. U256::from(1)) when constructing MorphReceiptBuilderCtx in that test (the
builder used is DefaultMorphReceiptBuilder with tx from create_l1_msg_tx) and
keep the final assertion assert_eq!(receipt.l1_fee(), U256::ZERO) to ensure
L1Msg receipts always report zero even if the context contains a non-zero
l1_fee.

In `@crates/node/src/test_utils.rs`:
- Around line 34-37: The setup helper (pub async fn setup) should validate its
input and immediately return an error when num_nodes == 0; add a guard at the
top of setup that checks if num_nodes == 0 and returns an eyre::Result::Err
(e.g., return Err(eyre::eyre!("num_nodes must be > 0"))) so callers get a clear,
early failure instead of flowing into downstream setup logic (affecting
MorphTestNode/TaskManager/Wallet creation).

In `@crates/txpool/src/morph_tx_validation.rs`:
- Around line 418-460: The test
test_validate_morph_tx_token_fee_path_token_not_found should assert the
deterministic EmptyDB path returns MorphTxError::TokenNotFound only; change the
assertion so validate_morph_tx(&mut db, &input).unwrap_err() is matched strictly
against MorphTxError::TokenNotFound (since TokenFeeInfo::load_for_caller() on
EmptyDB yields Ok(None)), and if you still want to cover the fetch-failure
branch create a separate test using a failing-DB fixture that causes
TokenFeeInfo::load_for_caller() to return an Err which you then assert maps to
MorphTxError::TokenInfoFetchFailed.

---

Nitpick comments:
In `@crates/engine-api/src/builder.rs`:
- Around line 1151-1183: Rename the test function to accurately reflect its
behavior: change fn test_engine_state_tracker_ignores_non_canonical_events() to
a clearer name such as fn
test_engine_state_tracker_updates_only_on_canonical_commit() (or similar) and
update any inline comment if needed; this makes the intent explicit for
EngineStateTracker and its methods current_head() and
on_consensus_engine_event() which are being validated by sending a
ConsensusEngineEvent::CanonicalChainCommitted.
- Around line 1185-1195: The test test_engine_state_tracker_concurrent_reads
does not actually exercise concurrency because it calls
EngineStateTracker::current_head sequentially on one thread; either rename the
test to something like test_engine_state_tracker_multiple_reads_consistent to
reflect its current behavior, or change the test to spawn multiple threads that
concurrently call EngineStateTracker::current_head after initializing state with
record_local_head to verify no panics and consistent results across threads;
update assertions to collect and compare results from the spawned threads and
join them to ensure true concurrent access was exercised.

In `@crates/evm/src/assemble.rs`:
- Around line 136-171: Tests duplicate the create_test_chainspec() helper
between assemble.rs and config.rs; extract it into a single shared test helper
(e.g., a new #[cfg(test)] mod test_utils with a pub fn create_test_chainspec()
-> Arc<MorphChainSpec>) and import it from both test modules to avoid
duplication. Move the genesis_json creation and Genesis->MorphChainSpec
conversion into that helper, keep it behind #[cfg(test)] so it’s not compiled in
production, and update references in assemble.rs and config.rs to call
test_utils::create_test_chainspec() (or use a use statement) so both test
modules reuse the single implementation.

In `@crates/node/Cargo.toml`:
- Around line 59-74: Update Cargo.toml so serde_json is an optional dependency
tied to the test-utils feature: change the top-level declaration
`serde_json.workspace = true` to `serde_json = { workspace = true, optional =
true }`, add `"dep:serde_json"` to the `test-utils` feature entry, and remove
the `serde_json.workspace = true` line from the [dev-dependencies] block;
reference the `test-utils` feature and the `serde_json` dependency to locate
edits.

In `@crates/primitives/src/receipt/envelope.rs`:
- Around line 417-451: The test suite is missing Eip2930 and Eip7702 coverage
for the 2718 roundtrip; update the tests so that MorphTxType::Eip2930 and
MorphTxType::Eip7702 are also encoded with encode_2718 and decoded with
MorphReceiptEnvelope::decode_2718 and asserted equal to the original receipt; to
avoid duplication replace the four separate functions
(test_eip2718_roundtrip_legacy, _eip1559, _l1msg, _morph) with a single
parameterized loop (or a helper function) that iterates over all MorphTxType
variants from create_test_receipt and performs the encode_2718 -> decode_2718 ->
assert_eq roundtrip for each variant.

In `@crates/revm/src/error.rs`:
- Around line 158-162: The test test_into_evm_error currently only asserts the
variant EVMError::Transaction(_) — update it to also extract and assert the
wrapped payload equals the original
MorphInvalidTransaction::TokenNotRegistered(1); specifically, match or if-let on
evm_err (EVMError::Transaction(inner)) and assert_eq!(inner,
MorphInvalidTransaction::TokenNotRegistered(1)) so the inner value is validated
as well.
- Around line 111-117: Replace the two partial contains assertions with a single
exact-string assertion for the full error message: build the expected string
that MorphInvalidTransaction::InsufficientTokenBalance produces (including the
required and available U256 values) and assert_eq!(err.to_string(), expected) so
the test verifies the entire error formatting rather than only substrings;
target the test that constructs err from
MorphInvalidTransaction::InsufficientTokenBalance and replaces the two
assert!(err.to_string().contains(...)) calls with the single exact equality
check against the constructed expected string.

In `@crates/rpc/src/eth/transaction.rs`:
- Around line 651-715: Add a boundary test calling
try_build_morph_tx_from_request with a request whose fee_token_id value exceeds
u16::MAX (e.g., U64::from(u16::MAX as u64 + 1)) and assert the call returns Err
and the error message contains the conversion failure; place this new test
alongside the other tests in the same module (use
create_basic_transaction_request() to build the request and call
try_build_morph_tx_from_request with U64::from(u16::MAX + 1) to exercise the
u16::try_from(...) failure path).

In `@crates/txpool/src/transaction.rs`:
- Around line 326-332: The test test_encoded_2718_is_cached currently compares
content equality which can succeed even if encoding is recomputed; update it to
assert that the cached Bytes are the same allocation by comparing pointer
identity from two calls to encoded_2718() (e.g., compare bytes1.as_ptr() ==
bytes2.as_ptr() and lengths) so the test verifies reuse of the cached buffer
rather than just equal content; locate the test and replace or augment the
assert_eq!(bytes1, bytes2) with a pointer identity check on encoded_2718().

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8c1abc49-ddd4-4477-abe1-17387e8b139d

📥 Commits

Reviewing files that changed from the base of the PR and between 0f3f0a7 and 91bfda5.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (39)
  • crates/chainspec/src/constants.rs
  • crates/chainspec/src/hardfork.rs
  • crates/consensus/src/validation.rs
  • crates/engine-api/src/builder.rs
  • crates/engine-api/src/error.rs
  • crates/engine-api/src/validator.rs
  • crates/evm/src/assemble.rs
  • crates/evm/src/block/receipt.rs
  • crates/evm/src/config.rs
  • crates/node/Cargo.toml
  • crates/node/src/args.rs
  • crates/node/src/lib.rs
  • crates/node/src/node.rs
  • crates/node/src/test_utils.rs
  • crates/node/src/validator.rs
  • crates/node/tests/it/main.rs
  • crates/node/tests/it/sync.rs
  • crates/payload/builder/src/builder.rs
  • crates/payload/types/src/attributes.rs
  • crates/payload/types/src/executable_l2_data.rs
  • crates/payload/types/src/lib.rs
  • crates/payload/types/src/params.rs
  • crates/payload/types/src/safe_l2_data.rs
  • crates/primitives/src/header.rs
  • crates/primitives/src/receipt/envelope.rs
  • crates/revm/src/error.rs
  • crates/revm/src/evm.rs
  • crates/revm/src/l1block.rs
  • crates/revm/src/precompiles.rs
  • crates/revm/src/token_fee.rs
  • crates/rpc/Cargo.toml
  • crates/rpc/src/error.rs
  • crates/rpc/src/eth/receipt.rs
  • crates/rpc/src/eth/transaction.rs
  • crates/rpc/src/types/receipt.rs
  • crates/rpc/src/types/request.rs
  • crates/txpool/src/error.rs
  • crates/txpool/src/morph_tx_validation.rs
  • crates/txpool/src/transaction.rs

@panos-xyz panos-xyz requested review from anylots and chengwenxi March 18, 2026 09:48
- Fix no-op FeeVault test by creating chainspec with feeVaultAddress
  and making assertion unconditional
- Use non-zero l1_fee in L1Msg receipt test to verify builder ignores it
- Destructure MorphReceipt::Morph to assert fee_token_id, fee_rate,
  token_scale, fee_limit, reference, memo fields
- Narrow token-not-found assertion to deterministic TokenNotFound path
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.

♻️ Duplicate comments (1)
crates/txpool/src/morph_tx_validation.rs (1)

454-456: ⚠️ Potential issue | 🟡 Minor

Tighten TokenNotFound assertion to exact token id.

Line 454 still matches any token_id via { .. }. This can hide regressions in error payload mapping; assert token_id: 42 explicitly for deterministic coverage.

💡 Suggested change
-        assert!(
-            matches!(err, MorphTxError::TokenNotFound { .. }),
-            "expected TokenNotFound, got {err:?}"
-        );
+        assert_eq!(err, MorphTxError::TokenNotFound { token_id: 42 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/txpool/src/morph_tx_validation.rs` around lines 454 - 456, The current
assertion on the error allows any token_id because it uses the wildcard pattern
in matches!(err, MorphTxError::TokenNotFound { .. }); update the assertion to
match the exact expected token id (token_id: 42) by changing the pattern to
matches!(err, MorphTxError::TokenNotFound { token_id: 42 }) and update the
failure message if needed so the test fails when the token_id payload is
different; target the MorphTxError::TokenNotFound pattern to ensure
deterministic coverage.
🧹 Nitpick comments (4)
crates/evm/src/block/receipt.rs (2)

362-382: Add tests for Eip2930 and Eip7702 routing paths.

build_receipt has explicit match arms for MorphTxType::Eip2930 and MorphTxType::Eip7702, but this module currently only exercises Legacy/EIP-1559/L1Msg/Morph. Adding two focused tests here would complete branch coverage for tx-type-to-receipt mapping.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/evm/src/block/receipt.rs` around lines 362 - 382, Add two unit tests
similar to test_build_eip1559_receipt that exercise the Eip2930 and Eip7702
match arms in build_receipt: create a tx for each type (e.g. create_eip2930_tx
and create_eip7702_tx or reuse existing helpers), build a
MorphReceiptBuilderCtx::<TestEvm> with that tx, a success result
(make_success_result), appropriate l1_fee and cumulative_gas_used, call
DefaultMorphReceiptBuilder.build_receipt(ctx), and assert the returned
MorphReceipt is the expected variant (matches!(receipt,
MorphReceipt::Eip2930(_)) and MorphReceipt::Eip7702(_)) and that
receipt.l1_fee() and receipt.cumulative_gas_used() match the ctx values.

510-533: Current log test doesn’t verify pre/main/post ordering contract.

test_build_receipt_with_logs only checks a single main log count. It misses the core behavior documented in the builder: [pre_fee_logs] + [main tx logs] + [post_fee_logs], including revert behavior where main logs are absent but fee logs remain.

♻️ Proposed test additions
@@
     #[test]
     fn test_build_receipt_with_logs() {
@@
         let receipt = builder.build_receipt(ctx);
         assert_eq!(TxReceipt::logs(&receipt).len(), 1);
     }
+
+    #[test]
+    fn test_build_receipt_log_order_pre_main_post() {
+        let builder = DefaultMorphReceiptBuilder;
+        let tx = create_legacy_tx();
+
+        let pre = Log::new(
+            Address::repeat_byte(0x11),
+            vec![B256::repeat_byte(0xA1)],
+            alloy_primitives::Bytes::from_static(b"pre"),
+        )
+        .unwrap();
+        let main = Log::new(
+            Address::repeat_byte(0x22),
+            vec![B256::repeat_byte(0xB2)],
+            alloy_primitives::Bytes::from_static(b"main"),
+        )
+        .unwrap();
+        let post = Log::new(
+            Address::repeat_byte(0x33),
+            vec![B256::repeat_byte(0xC3)],
+            alloy_primitives::Bytes::from_static(b"post"),
+        )
+        .unwrap();
+
+        let ctx = MorphReceiptBuilderCtx::<TestEvm> {
+            tx: &tx,
+            result: make_success_with_logs(21_000, vec![main.clone()]),
+            cumulative_gas_used: 21_000,
+            l1_fee: U256::ZERO,
+            morph_tx_fields: None,
+            pre_fee_logs: vec![pre.clone()],
+            post_fee_logs: vec![post.clone()],
+        };
+
+        let receipt = builder.build_receipt(ctx);
+        assert_eq!(TxReceipt::logs(&receipt), &[pre, main, post]);
+    }
+
+    #[test]
+    fn test_build_reverted_receipt_keeps_fee_logs() {
+        let builder = DefaultMorphReceiptBuilder;
+        let tx = create_legacy_tx();
+
+        let pre = Log::new(
+            Address::repeat_byte(0x44),
+            vec![B256::repeat_byte(0xD4)],
+            alloy_primitives::Bytes::from_static(b"pre"),
+        )
+        .unwrap();
+        let post = Log::new(
+            Address::repeat_byte(0x55),
+            vec![B256::repeat_byte(0xE5)],
+            alloy_primitives::Bytes::from_static(b"post"),
+        )
+        .unwrap();
+
+        let ctx = MorphReceiptBuilderCtx::<TestEvm> {
+            tx: &tx,
+            result: make_revert_result(15_000),
+            cumulative_gas_used: 15_000,
+            l1_fee: U256::ZERO,
+            morph_tx_fields: None,
+            pre_fee_logs: vec![pre.clone()],
+            post_fee_logs: vec![post.clone()],
+        };
+
+        let receipt = builder.build_receipt(ctx);
+        assert_eq!(TxReceipt::logs(&receipt), &[pre, post]);
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/evm/src/block/receipt.rs` around lines 510 - 533, Update the test
suite to assert the full pre/main/post log ordering and revert behavior: in
test_build_receipt_with_logs (and add a new test for revert) construct distinct
pre_fee_logs, main logs (via make_success_with_logs), and post_fee_logs in the
MorphReceiptBuilderCtx used with DefaultMorphReceiptBuilder::build_receipt, then
assert TxReceipt::logs(&receipt) yields the concatenation [pre_fee_logs, main
logs, post_fee_logs] in order (check counts and that entries match the
corresponding Log instances); also add a separate test that uses a revert result
(e.g., make_revert_with_logs or equivalent) to verify main tx logs are omitted
while pre_fee_logs and post_fee_logs remain present and ordered.
crates/consensus/src/validation.rs (2)

1865-1889: Make this fixture valid apart from gas_used.

Right now the header also has default receipts_root / logs_bloom, and result.gas_used happens to equal the receipt total. That makes the test depend on validation order and doesn't prove the comparison is receipt-derived. Populate receipts_root / logs_bloom from result.receipts, and set result.gas_used to match the header so the failure can only come from the cumulative receipt gas mismatch.

♻️ Tighten the fixture
         let result = alloy_evm::block::BlockExecutionResult {
             receipts: vec![receipt],
             requests: Default::default(),
-            gas_used: 21000,
+            gas_used: 50000,
             blob_gas_used: 0,
         };
+
+        let receipts_with_bloom: Vec<_> =
+            result.receipts.iter().map(TxReceipt::with_bloom_ref).collect();
+        let receipts_root =
+            alloy_consensus::proofs::calculate_receipt_root(&receipts_with_bloom);
+        let logs_bloom = receipts_with_bloom
+            .iter()
+            .fold(Bloom::ZERO, |bloom, r| bloom | r.bloom_ref());

         // Create a block header with gas_used = 50000 (mismatch!)
         let header = create_morph_header(Header {
             nonce: B64::ZERO,
             ommers_hash: EMPTY_OMMER_ROOT_HASH,
             gas_limit: 30_000_000,
             gas_used: 50000, // Does not match receipt
+            receipts_root,
+            logs_bloom,
             timestamp: 1000,
             base_fee_per_gas: Some(1_000_000),
             ..Default::default()
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/consensus/src/validation.rs` around lines 1865 - 1889, The test
fixture must be valid except for the cumulative gas mismatch: populate the
header's receipts_root and logs_bloom from result.receipts so the header isn't
left with defaults, and set result.gas_used to match the header's gas_used (the
50000 value in the Header created by create_morph_header) so the only failing
check is the cumulative receipt gas vs header.gas_used; update the
BlockExecutionResult construction (result.receipts / gas_used) and the header
fields (receipts_root, logs_bloom) accordingly so the receipt-derived
roots/bloom are consistent while the cumulative gas remains the intended
mismatch.

1549-1601: Return a base TxMorph from these helpers.

create_morph_tx_v0() / create_morph_tx_v1() already encode the common defaults, but because they return MorphTxEnvelope, each negative case still has to restate the full TxMorph literal. A base_morph_tx_v0() / base_morph_tx_v1() helper that returns TxMorph would keep each test focused on the one field under test and reduce drift when the schema changes.

Also applies to: 1626-1798

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/consensus/src/validation.rs` around lines 1549 - 1601, The helpers
create_morph_tx_v0 and create_morph_tx_v1 duplicate a full TxMorph literal
because they return MorphTxEnvelope; add base_morph_tx_v0() and
base_morph_tx_v1() that return a TxMorph with the shared defaults (chain_id,
nonce, gas_limit, fees, to, access_list, version, etc.), then change
create_morph_tx_v0/create_morph_tx_v1 to call the new base_* helpers and wrap
the returned TxMorph in Signed::new_unchecked(...) to produce the
MorphTxEnvelope; update tests that currently construct full TxMorphs (the cases
mentioned around the create helpers) to call base_* and override only the field
under test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@crates/txpool/src/morph_tx_validation.rs`:
- Around line 454-456: The current assertion on the error allows any token_id
because it uses the wildcard pattern in matches!(err,
MorphTxError::TokenNotFound { .. }); update the assertion to match the exact
expected token id (token_id: 42) by changing the pattern to matches!(err,
MorphTxError::TokenNotFound { token_id: 42 }) and update the failure message if
needed so the test fails when the token_id payload is different; target the
MorphTxError::TokenNotFound pattern to ensure deterministic coverage.

---

Nitpick comments:
In `@crates/consensus/src/validation.rs`:
- Around line 1865-1889: The test fixture must be valid except for the
cumulative gas mismatch: populate the header's receipts_root and logs_bloom from
result.receipts so the header isn't left with defaults, and set result.gas_used
to match the header's gas_used (the 50000 value in the Header created by
create_morph_header) so the only failing check is the cumulative receipt gas vs
header.gas_used; update the BlockExecutionResult construction (result.receipts /
gas_used) and the header fields (receipts_root, logs_bloom) accordingly so the
receipt-derived roots/bloom are consistent while the cumulative gas remains the
intended mismatch.
- Around line 1549-1601: The helpers create_morph_tx_v0 and create_morph_tx_v1
duplicate a full TxMorph literal because they return MorphTxEnvelope; add
base_morph_tx_v0() and base_morph_tx_v1() that return a TxMorph with the shared
defaults (chain_id, nonce, gas_limit, fees, to, access_list, version, etc.),
then change create_morph_tx_v0/create_morph_tx_v1 to call the new base_* helpers
and wrap the returned TxMorph in Signed::new_unchecked(...) to produce the
MorphTxEnvelope; update tests that currently construct full TxMorphs (the cases
mentioned around the create helpers) to call base_* and override only the field
under test.

In `@crates/evm/src/block/receipt.rs`:
- Around line 362-382: Add two unit tests similar to test_build_eip1559_receipt
that exercise the Eip2930 and Eip7702 match arms in build_receipt: create a tx
for each type (e.g. create_eip2930_tx and create_eip7702_tx or reuse existing
helpers), build a MorphReceiptBuilderCtx::<TestEvm> with that tx, a success
result (make_success_result), appropriate l1_fee and cumulative_gas_used, call
DefaultMorphReceiptBuilder.build_receipt(ctx), and assert the returned
MorphReceipt is the expected variant (matches!(receipt,
MorphReceipt::Eip2930(_)) and MorphReceipt::Eip7702(_)) and that
receipt.l1_fee() and receipt.cumulative_gas_used() match the ctx values.
- Around line 510-533: Update the test suite to assert the full pre/main/post
log ordering and revert behavior: in test_build_receipt_with_logs (and add a new
test for revert) construct distinct pre_fee_logs, main logs (via
make_success_with_logs), and post_fee_logs in the MorphReceiptBuilderCtx used
with DefaultMorphReceiptBuilder::build_receipt, then assert
TxReceipt::logs(&receipt) yields the concatenation [pre_fee_logs, main logs,
post_fee_logs] in order (check counts and that entries match the corresponding
Log instances); also add a separate test that uses a revert result (e.g.,
make_revert_with_logs or equivalent) to verify main tx logs are omitted while
pre_fee_logs and post_fee_logs remain present and ordered.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a7aecbdc-1b01-42a5-967d-f8ecf676e2e7

📥 Commits

Reviewing files that changed from the base of the PR and between 91bfda5 and 4b375ba.

📒 Files selected for processing (3)
  • crates/consensus/src/validation.rs
  • crates/evm/src/block/receipt.rs
  • crates/txpool/src/morph_tx_validation.rs

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.

1 participant