Skip to content

fix(primitives): emit nonce/v/r/s placeholders for L1 message RPC#115

Merged
panos-xyz merged 1 commit into
mainfrom
fix/l1msg-rpc-geth-parity
Jun 2, 2026
Merged

fix(primitives): emit nonce/v/r/s placeholders for L1 message RPC#115
panos-xyz merged 1 commit into
mainfrom
fix/l1msg-rpc-geth-parity

Conversation

@panos-xyz
Copy link
Copy Markdown
Contributor

@panos-xyz panos-xyz commented Jun 2, 2026

Summary

L1 message transactions (type 0x7E) have no signature or nonce at the consensus layer, but every standard EVM JSON-RPC client — ethers v5/v6, viem, web3.js, and the @morph-network/* SDK adapters that wrap them — expects every transaction object returned by eth_getBlockByNumber(num, true) / eth_getTransactionByHash(...) to expose nonce, v, r, s, yParity. Today morph-reth omits all five for L1 messages, so anything that reads a block or fetches an L1-message tx hash via these libraries fails to deserialize. The prover already shipped an ad-hoc tolerance patch on its own TransactionTrace for the same reason (morph#fix/reth-l1msg-tx-deserialize).

This PR fixes it on the producing side so the RPC output matches morph-geth's existing JSON contract.

Approach

Single-file change in l1_transaction.rs on the serde JSON path only — RLP, EIP-2718, signing, and reth-codec Compact (DB) encodings are unchanged.

TxL1Msg now serializes through a private L1MsgSerdeHelper (serde(from = ..., into = ...)), which carries five extra placeholder fields — nonce, v, r, s, yParity — all rendered as "0x0" and silently dropped on deserialize so geth-shaped JSON also round-trips.

alloy_consensus::Sealed already flattens its inner T into the surrounding object, so these top-level fields naturally appear at the envelope's RPC root with no envelope-level wiring required.

Resulting envelope JSON:

{
  "type":"0x7e","queueIndex":"0x7","gas":"0x1e8480",
  "to":"0x...","value":"0x0","sender":"0x...","input":"0x...",
  "nonce":"0x0","v":"0x0","r":"0x0","s":"0x0","yParity":"0x0",
  "hash":"0x..."
}

This matches morph-geth's RPCTransaction shape exactly for L1 messages.

Test plan

  • New unit tests in l1_transaction.rs:
    • test_l1_transaction_json_includes_geth_parity_fields — verifies all five placeholders are emitted by the helper struct.
    • test_l1_transaction_json_round_tripserde_json round-trip preserves real fields, drops placeholders.
    • test_l1_envelope_rpc_json_includes_geth_parity_fields — verifies the full envelope JSON has type / queueIndex / gas / to / value / sender / input / nonce / v / r / s / yParity / hash at the top level (the actual shape eth_* returns).
  • cargo nextest run -p morph-primitives -p morph-rpc -p morph-consensus -p morph-txpool — 253/253 pass.
  • cargo check --workspace --all-targets — clean.
  • cargo clippy --all --all-targets -- -D warnings — clean.
  • cargo fmt --all -- --check — clean.
  • cargo test --doc -p morph-primitives — clean.

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

⚠️ Code review skipped — your organization's overage spend limit has been reached.

Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.

Once credits are available, reopen this pull request to trigger a review.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR refactors L1 message transaction serialization to enforce JSON-RPC compatibility. TxL1Msg now serializes through a custom helper type that injects a fixed nonce placeholder and zero signature fields. The envelope integrates this via a serialize_with attribute, and tests verify the complete flow including sealed envelope output.

Changes

L1 Message RPC Serialization

Layer / File(s) Summary
L1 message serde helper and zero signature
crates/primitives/src/transaction/l1_transaction.rs
TxL1Msg switches from field-level serde attributes to serialize/deserialize via L1MsgSerdeHelper, a new helper struct that always emits nonce: "0x0" and tolerates it on decode. A TxL1Msg::signature() const fn produces a zero Signature for JSON-RPC parity fields. The public serde_l1_message_tx_rpc serializer flattens the zero signature fields (v, r, s, yParity) into RPC output.
Envelope integration and validation
crates/primitives/src/transaction/envelope.rs, crates/primitives/src/transaction/l1_transaction.rs
MorphTxEnvelope::L1Msg adds a feature-gated serialize_with attribute that routes serialization through serde_l1_message_tx_rpc. Tests verify TxL1Msg includes the nonce placeholder in JSON output, round-tripping preserves fields, the serializer injects flattened signature fields, and sealed envelope serialization includes top-level parity fields plus a string hash.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • chengwenxi
  • anylots

Poem

🐰 A rabbit hops through serialized lands,
Where nonces dance in JSON bands,
Zero signatures, parity true,
RPC calls now know what to do! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main change: adding nonce/v/r/s placeholder fields to L1 message RPC serialization.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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 fix/l1msg-rpc-geth-parity

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.

@panos-xyz panos-xyz force-pushed the fix/l1msg-rpc-geth-parity branch from 1a2e061 to 2b0fe8b Compare June 2, 2026 07:22
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/primitives/src/transaction/l1_transaction.rs`:
- Around line 35-42: The current serde from/into on TxL1Msg using
msg_serde::L1MsgSerdeHelper injects an RPC-only nonce field and RPC-style
renames for all formats; restrict that behavior to human-readable (JSON/RPC)
only by removing the global #[cfg_attr(... serde(from/into =
"msg_serde::L1MsgSerdeHelper"))] from TxL1Msg and instead implement a manual
impl Serialize and impl Deserialize for TxL1Msg that check
serializer.is_human_readable()/deserializer.is_human_readable() and delegate to
L1MsgSerdeHelper only when human-readable, or alternatively create a separate
RPC wrapper type (e.g., L1MsgRpc or similar) that uses
msg_serde::L1MsgSerdeHelper for RPC transport while leaving TxL1Msg’s default
serde untouched.
- Around line 403-424: The helper always flattens TxL1Msg::signature() into
every serde format; change serde_l1_message_tx_rpc to check
serializer.is_human_readable() and only emit the flattened signature for
human-readable serializers (JSON-RPC) — for non-human-readable serializers
serialize just the inner value without the signature. Concretely, inside
serde_l1_message_tx_rpc inspect S::is_human_readable
(serializer.is_human_readable()), and branch: when true use the existing
SerdeHelper struct that #[serde(flatten)]s signature, otherwise serialize value
alone (e.g., call value.serialize(serializer) or use a helper struct with only
value) so v/r/s/yParity are not emitted for binary formats.
- Around line 669-790: The serde-dependent tests
(test_l1_transaction_json_includes_nonce_placeholder,
test_l1_transaction_json_round_trip,
test_serde_l1_message_tx_rpc_flattens_zero_signature,
test_l1_envelope_rpc_json_includes_geth_parity_fields) must be compiled only
when the serde feature is enabled; add #[cfg(feature = "serde")] to each of
these test functions or wrap them in a single #[cfg(feature = "serde")] mod
serde_tests { ... } inside mod tests so they are excluded when building with
--no-default-features.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d05c2d95-27a8-4f94-9f35-75ca72601558

📥 Commits

Reviewing files that changed from the base of the PR and between 08557ca and 1a2e061.

📒 Files selected for processing (2)
  • crates/primitives/src/transaction/envelope.rs
  • crates/primitives/src/transaction/l1_transaction.rs

Comment on lines 35 to +42
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
#[cfg_attr(
feature = "serde",
serde(
from = "msg_serde::L1MsgSerdeHelper",
into = "msg_serde::L1MsgSerdeHelper"
)
)]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate TxL1Msg and surrounding serde attrs
fd -a "l1_transaction.rs" crates/primitives/src/transaction -g'*' > /tmp/l1_transaction_paths.txt || true
echo "== l1_transaction.rs match =="
cat /tmp/l1_transaction_paths.txt || true

# Grep for TxL1Msg declaration
rg -n "struct\s+TxL1Msg|enum\s+TxL1Msg|TxL1Msg" crates/primitives/src/transaction/l1_transaction.rs

# Dump the exact serde attribute block region for context
sed -n '1,120p' crates/primitives/src/transaction/l1_transaction.rs | nl -ba | sed -n '20,80p'

# Find L1MsgSerdeHelper definition/impls
echo "== Searching for L1MsgSerdeHelper =="
rg -n "L1MsgSerdeHelper" crates/primitives/src

# Print the helper implementation file sections
helper_file="$(rg -n "struct\s+L1MsgSerdeHelper|enum\s+L1MsgSerdeHelper" crates/primitives/src -l | head -n 1 || true)"
echo "== helper_file =="
echo "$helper_file"

if [[ -n "${helper_file:-}" ]]; then
  wc -l "$helper_file"
  sed -n '1,220p' "$helper_file" | nl -ba | head -n 220
fi

# Also look for where helper decides human_readable
if [[ -n "${helper_file:-}" ]]; then
  echo "== human_readable gating checks =="
  rg -n "is_human_readable|human_readable" "$helper_file"
  rg -n "nonce" "$helper_file" | head -n 50
  rg -n "queueIndex|gas" "$helper_file" | head -n 50
fi

# If helper impl is in a different file/module, print those too
echo "== Checking for serde helper module msg_serde =="
rg -n "mod\s+msg_serde|msg_serde::L1MsgSerdeHelper" crates/primitives/src/transaction/l1_transaction.rs crates/primitives/src -S

Repository: morph-l2/morph-reth

Length of output: 2494


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="crates/primitives/src/transaction/l1_transaction.rs"
echo "== File: $file =="
test -f "$file" && echo "exists" || { echo "missing"; exit 1; }

echo "== TxL1Msg serde attributes near lines 30-60 =="
sed -n '25,60p' "$file" | cat -n

echo "== TxL1Msg serde attribute block requested (around 35-42) =="
sed -n '32,48p' "$file" | cat -n

echo "== L1MsgSerdeHelper module/impl region (around 320-460) =="
sed -n '300,470p' "$file" | cat -n

echo "== Look for is_human_readable gating and field mapping inside helper =="
rg -n "is_human_readable|human_readable" "$file" || true
rg -n "nonce|queueIndex|gas" "$file" | head -n 80

echo "== show where serde helper is used in From impls =="
rg -n "From<.*L1MsgSerdeHelper>|impl From<.*TxL1Msg.*L1MsgSerdeHelper>" "$file"

Repository: morph-l2/morph-reth

Length of output: 13309


Scope the nonce/RPC-shaped L1MsgSerdeHelper to human-readable serde only

TxL1Msg is configured with serde(from/into = "msg_serde::L1MsgSerdeHelper"), and that helper unconditionally injects an extra nonce field and applies RPC-oriented renames (queueIndex, gas, camelCase, plus the data/input alias). There’s no serializer.is_human_readable()/deserializer.is_human_readable() gating, so this changes the serde representation for all formats—not just the JSON-RPC layer—broadening the public contract beyond the stated intent. Move the behavior into a human-readable-only manual Serialize/Deserialize, or into an RPC-specific wrapper rather than the base TxL1Msg serde attributes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/primitives/src/transaction/l1_transaction.rs` around lines 35 - 42,
The current serde from/into on TxL1Msg using msg_serde::L1MsgSerdeHelper injects
an RPC-only nonce field and RPC-style renames for all formats; restrict that
behavior to human-readable (JSON/RPC) only by removing the global #[cfg_attr(...
serde(from/into = "msg_serde::L1MsgSerdeHelper"))] from TxL1Msg and instead
implement a manual impl Serialize and impl Deserialize for TxL1Msg that check
serializer.is_human_readable()/deserializer.is_human_readable() and delegate to
L1MsgSerdeHelper only when human-readable, or alternatively create a separate
RPC wrapper type (e.g., L1MsgRpc or similar) that uses
msg_serde::L1MsgSerdeHelper for RPC transport while leaving TxL1Msg’s default
serde untouched.

Comment on lines +403 to +424
#[cfg(feature = "serde")]
pub fn serde_l1_message_tx_rpc<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
where
T: serde::Serialize,
S: serde::Serializer,
{
use serde::Serialize;

#[derive(Serialize)]
struct SerdeHelper<'a, T> {
#[serde(flatten)]
value: &'a T,
#[serde(flatten)]
signature: Signature,
}

SerdeHelper {
value,
signature: TxL1Msg::signature(),
}
.serialize(serializer)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the helper and related envelope wiring
rg -n "serde_l1_message_tx_rpc" crates/primitives/src/transaction -S
rg -n "MorphTxEnvelope::L1Msg|serialize_with\\s*=\\s*\"crate::transaction::l1_transaction::serde_l1_message_tx_rpc|serialize_with\\s*=\\s*serde_l1_message_tx_rpc" crates/primitives/src/transaction -S

# Inspect the helper implementation around the reported line range
FILE="crates/primitives/src/transaction/l1_transaction.rs"
echo "---- context from $FILE around 380-470 ----"
sed -n '380,470p' "$FILE"

# Inspect TxL1Msg::signature implementation + any docs/comments
rg -n "fn signature\\(|TxL1Msg::signature\\(" crates/primitives/src/transaction/l1_transaction.rs -S
rg -n "signature\\(\\).*U256::ZERO|yParity|v\\b|r\\b|s\\b|yParity" crates/primitives/src/transaction/l1_transaction.rs -S
sed -n '1,260p' "$FILE" | rg -n "signature\\(" -n || true
# If signature impl is elsewhere, locate it broadly in the transaction module
rg -n "struct Signature|type Signature|Signature\\b" crates/primitives/src/transaction/l1_transaction.rs crates/primitives/src/transaction -S

# Search for serializer.is_human_readable usage in the crate
rg -n "is_human_readable\\(" crates -S

# Check for tests asserting injected fields for JSON/RPC vs other formats
rg -n "serde_l1_message_tx_rpc|yParity|\"yParity\"|\"v\"|\"r\"|\"s\"|MorphTxEnvelope::L1Msg" crates/primitives/src/transaction/l1_transaction.rs crates/primitives/src/transaction -S
# Show nearby test sections if they exist
rg -n "#\\[test\\]" crates/primitives/src/transaction/l1_transaction.rs -S

Repository: morph-l2/morph-reth

Length of output: 22867


Gate the flattened zero signature to human-readable (JSON-RPC) serializers only

serde_l1_message_tx_rpc unconditionally #[serde(flatten)]s TxL1Msg::signature() (emitting v/r/s/yParity), and MorphTxEnvelope::L1Msg wires it via serde(serialize_with = ...), so these RPC-parity placeholder fields will be included for any serde format, not just JSON-RPC.

Suggested fix
 pub fn serde_l1_message_tx_rpc<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
 where
     T: serde::Serialize,
     S: serde::Serializer,
 {
     use serde::Serialize;
+
+    if !serializer.is_human_readable() {
+        return value.serialize(serializer);
+    }

     #[derive(Serialize)]
     struct SerdeHelper<'a, T> {
         #[serde(flatten)]
         value: &'a T,
         #[serde(flatten)]
         signature: Signature,
     }
📝 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
#[cfg(feature = "serde")]
pub fn serde_l1_message_tx_rpc<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
where
T: serde::Serialize,
S: serde::Serializer,
{
use serde::Serialize;
#[derive(Serialize)]
struct SerdeHelper<'a, T> {
#[serde(flatten)]
value: &'a T,
#[serde(flatten)]
signature: Signature,
}
SerdeHelper {
value,
signature: TxL1Msg::signature(),
}
.serialize(serializer)
}
#[cfg(feature = "serde")]
pub fn serde_l1_message_tx_rpc<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
where
T: serde::Serialize,
S: serde::Serializer,
{
use serde::Serialize;
if !serializer.is_human_readable() {
return value.serialize(serializer);
}
#[derive(Serialize)]
struct SerdeHelper<'a, T> {
#[serde(flatten)]
value: &'a T,
#[serde(flatten)]
signature: Signature,
}
SerdeHelper {
value,
signature: TxL1Msg::signature(),
}
.serialize(serializer)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/primitives/src/transaction/l1_transaction.rs` around lines 403 - 424,
The helper always flattens TxL1Msg::signature() into every serde format; change
serde_l1_message_tx_rpc to check serializer.is_human_readable() and only emit
the flattened signature for human-readable serializers (JSON-RPC) — for
non-human-readable serializers serialize just the inner value without the
signature. Concretely, inside serde_l1_message_tx_rpc inspect
S::is_human_readable (serializer.is_human_readable()), and branch: when true use
the existing SerdeHelper struct that #[serde(flatten)]s signature, otherwise
serialize value alone (e.g., call value.serialize(serializer) or use a helper
struct with only value) so v/r/s/yParity are not emitted for binary formats.

Comment on lines +669 to +790
/// JSON serialization must include the geth/scroll RPC parity placeholder
/// `nonce: "0x0"` so downstream clients (ethers v5/v6, viem) can parse L1
/// message tx objects returned by `eth_getBlockByNumber` etc.
#[test]
fn test_l1_transaction_json_includes_nonce_placeholder() {
let tx = TxL1Msg {
queue_index: 7,
gas_limit: 0x1e8480,
to: address!("5300000000000000000000000000000000000007"),
value: U256::ZERO,
input: Bytes::from(vec![0x8e, 0xf1, 0x33, 0x2e]),
sender: address!("ed82366effa760804dcfc3edf87fa2a6f1624415"),
};

let json = serde_json::to_value(&tx).expect("serialization must succeed");

assert_eq!(json["queueIndex"], "0x7");
assert_eq!(json["gas"], "0x1e8480");
assert_eq!(json["input"], "0x8ef1332e");
assert_eq!(json["nonce"], "0x0");
// v / r / s / yParity are emitted by `serde_l1_message_tx_rpc` via the
// envelope, not by `TxL1Msg` itself, so they are absent here.
assert!(json.get("v").is_none());
assert!(json.get("r").is_none());
assert!(json.get("s").is_none());
}

/// Round-trip through `serde_json` must be lossless: real fields preserved,
/// the `nonce` placeholder must be tolerated on the way back in.
#[test]
fn test_l1_transaction_json_round_trip() {
let original = TxL1Msg {
queue_index: 42,
gas_limit: 100_000,
to: address!("0000000000000000000000000000000000000002"),
value: U256::from(1_000u64),
input: Bytes::from(vec![0x01, 0x02]),
sender: address!("0000000000000000000000000000000000000001"),
};

let json = serde_json::to_value(&original).unwrap();
let decoded: TxL1Msg = serde_json::from_value(json).expect("round-trip must succeed");

assert_eq!(decoded, original);
}

/// `serde_l1_message_tx_rpc` adds a flattened zero signature on top of any
/// serializable inner value. This is the function the envelope uses to emit
/// `v`/`r`/`s`/`yParity` for L1 message transactions in RPC responses.
#[test]
fn test_serde_l1_message_tx_rpc_flattens_zero_signature() {
use serde::Serialize;

// Serialize a TxL1Msg through `serde_l1_message_tx_rpc` and inspect the
// resulting JSON object. We attach it to a wrapper field then read that
// field out — the function flattens the signature into the same object
// as the inner value.
#[derive(Serialize)]
struct WrapperOut<'a> {
#[serde(serialize_with = "serde_l1_message_tx_rpc")]
inner: &'a TxL1Msg,
}

let tx = TxL1Msg {
queue_index: 1,
gas_limit: 21_000,
to: address!("0000000000000000000000000000000000000002"),
value: U256::ZERO,
input: Bytes::new(),
sender: address!("0000000000000000000000000000000000000001"),
};

let outer = serde_json::to_value(WrapperOut { inner: &tx })
.expect("wrapper serialization must succeed");
let inner = &outer["inner"];

// Real TxL1Msg fields preserved (nonce comes from the helper struct).
assert_eq!(inner["queueIndex"], "0x1");
assert_eq!(inner["nonce"], "0x0");
// Signature fields injected by `serde_l1_message_tx_rpc`.
assert_eq!(inner["v"], "0x0");
assert_eq!(inner["r"], "0x0");
assert_eq!(inner["s"], "0x0");
assert_eq!(inner["yParity"], "0x0");
}

/// End-to-end check: serializing a [`MorphTxEnvelope::L1Msg`] (the type
/// actually emitted by RPC) must include all the morph-geth parity fields
/// at the top level: type, queueIndex, sender, nonce, v, r, s, yParity, hash.
#[test]
fn test_l1_envelope_rpc_json_includes_geth_parity_fields() {
use crate::MorphTxEnvelope;
use alloy_consensus::Sealable;

let tx = TxL1Msg {
queue_index: 7,
gas_limit: 0x1e8480,
to: address!("5300000000000000000000000000000000000007"),
value: U256::ZERO,
input: Bytes::from(vec![0x8e, 0xf1, 0x33, 0x2e]),
sender: address!("ed82366effa760804dcfc3edf87fa2a6f1624415"),
};
let envelope = MorphTxEnvelope::L1Msg(tx.seal_slow());

let json = serde_json::to_value(&envelope).expect("envelope serialization must succeed");

assert_eq!(json["type"], "0x7e");
assert_eq!(json["queueIndex"], "0x7");
assert_eq!(json["gas"], "0x1e8480");
assert_eq!(
json["sender"].as_str().unwrap().to_lowercase(),
"0xed82366effa760804dcfc3edf87fa2a6f1624415"
);
// morph-geth RPC parity placeholders, must all be present at the top level.
assert_eq!(json["nonce"], "0x0");
assert_eq!(json["v"], "0x0");
assert_eq!(json["r"], "0x0");
assert_eq!(json["s"], "0x0");
assert_eq!(json["yParity"], "0x0");
// Sealed<T> contributes the precomputed hash.
assert!(json["hash"].is_string());
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="crates/primitives/src/transaction/l1_transaction.rs"

# Show the relevant area around the reported line range
echo "=== Context around lines 620-840 ==="
nl -ba "$FILE" | sed -n '620,840p'

# Find any cfg/feature gating around the tests in this region
echo
echo "=== Search for cfg(feature = \"serde\") near tests ==="
rg -n --no-heading 'cfg\(feature *= *"serde"\)|cfg_attr\(feature *= *"serde"' "$FILE"

# Check the serde helper function gating
echo
echo "=== Search for serde_l1_message_tx_rpc definition and its cfg ==="
rg -n --no-heading 'serde_l1_message_tx_rpc' "$FILE"

# Check whether serde_json is gated/imported behind serde
echo
echo "=== Search for serde_json usage and related cfg ==="
rg -n --no-heading 'serde_json::|use .*serde_json' "$FILE"

Repository: morph-l2/morph-reth

Length of output: 161


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="crates/primitives/src/transaction/l1_transaction.rs"

echo "=== Context around lines 620-840 (with line numbers via python) ==="
python3 - <<'PY'
import itertools
path="crates/primitives/src/transaction/l1_transaction.rs"
start, end = 620, 840
with open(path, 'r', encoding='utf-8') as f:
    for i, line in enumerate(f, start=1):
        if start <= i <= end:
            print(f"{i:5d}\t{line.rstrip()}")
        elif i > end:
            break
PY

echo
echo "=== Search for cfg(feature = \"serde\") / cfg_attr in file ==="
rg -n --no-heading 'cfg(feature *= *"serde")|cfg_attr\(feature *= *"serde"' "$FILE" || true

echo
echo "=== Search for serde_l1_message_tx_rpc definition and its cfg ==="
rg -n --no-heading 'serde_l1_message_tx_rpc' "$FILE" || true

echo
echo "=== Search for serde_json usage and related imports in file ==="
rg -n --no-heading 'serde_json::|use .*serde_json|extern crate serde_json' "$FILE" || true

Repository: morph-l2/morph-reth

Length of output: 9291


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the Cargo.toml for the primitives crate
# (Using fd to avoid guessing path.)
fd -a -t f "Cargo.toml" crates/primitives | head -n 20

# Dump features/deps area for the primitives crate
# Prefer rg to narrow quickly.
FILE="$(fd -a -t f "Cargo.toml" crates/primitives | head -n 1)"
echo "=== Using Cargo.toml: $FILE ==="
python3 - <<'PY'
import re, itertools, pathlib, os, sys
path=os.environ.get("FILE")
text=pathlib.Path(path).read_text(encoding="utf-8")
# Print the [features] section and nearby [dependencies] entries for serde/serde_json.
m=re.search(r'(?ms)^\[features\].*$', text)
print("HAS [features]:", bool(m))
PY

echo
echo "=== Search for serde feature and serde_json dependency in $FILE ==="
rg -n --no-heading '^\[features\]|\bserde\b|serde_json' "$FILE" || true

echo
echo "=== Show the serde/serde_json related block (features+deps context) ==="
python3 - <<'PY'
import os, pathlib, re
path=os.environ.get("FILE")
text=pathlib.Path(path).read_text(encoding="utf-8").splitlines()
# Print lines around feature "serde" and any serde_json deps
targets=[]
for i,line in enumerate(text, start=1):
    if re.search(r'\bserde_json\b', line) or re.search(r'^\s*serde\s*=\s*\[', line) or re.search(r'"serde"', line) and 'features' not in line:
        targets.append(i)
# Also include [features] section start
for i,line in enumerate(text, start=1):
    if line.strip()=="[features]":
        targets.append(i)
if not targets:
    targets=[1]
lo=max(min(targets)-30,1)
hi=min(max(targets)+30, len(text))
for i in range(lo, min(hi, lo+260)):
    print(f"{i:5d}\t{text[i-1]}")
PY

Repository: morph-l2/morph-reth

Length of output: 737


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="crates/primitives/Cargo.toml"
echo "=== Using Cargo.toml: $FILE ==="

echo
echo "=== Search for serde/serde_json in features + dependencies ==="
rg -n --no-heading '^\[features\]|serde_json|(?<!\w)serde(?!_json)|"serde"' "$FILE" || true

echo
echo "=== Print the [features] section (serde-related window) ==="
python3 - <<'PY'
import pathlib, re
path=pathlib.Path("crates/primitives/Cargo.toml")
lines=path.read_text(encoding="utf-8").splitlines()
# find [features]
idx=None
for i,l in enumerate(lines):
    if l.strip()=="[features]":
        idx=i
        break
if idx is None:
    print("No [features] section found")
    raise SystemExit(0)

# print until next top-level section
start=idx
end=len(lines)
for j in range(idx+1, len(lines)):
    if re.match(r'^\s*\[.+\]\s*$', lines[j]) and not lines[j].strip().startswith("[features"):
        end=j
        break

for k in range(start, min(end, start+250)):
    print(f"{k+1:5d}\t{lines[k]}")
PY

echo
echo "=== Print serde_json and serde dependency declarations ==="
rg -n --no-heading 'serde_json\s*=\s*|serde\s*=\s*\{|\[dependencies\]|\[dependencies\.serde_json\]|\[dependencies\.serde\]' "$FILE" || true

echo
echo "=== Extract exact blocks for any serde/serde_json dependency entries ==="
python3 - <<'PY'
import pathlib, re
path=pathlib.Path("crates/primitives/Cargo.toml")
text=path.read_text(encoding="utf-8")

def show_block(name):
    # match lines like `serde_json = { ... }` or `serde_json = "..."` within [dependencies] or elsewhere.
    pat = rf'(?m)^([ \t]*{re.escape(name)}[ \t]*=[^\n]*$)'
    m=re.finditer(pat, text)
    found=False
    for mm in m:
        found=True
        line=mm.group(1)
        print(line)
    return found

show_block("serde_json")
show_block("serde")
PY

Repository: morph-l2/morph-reth

Length of output: 1823


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="crates/primitives/Cargo.toml"
echo "=== Using Cargo.toml: $FILE ==="

echo
echo "=== Search for serde_json and feature serde in the whole Cargo.toml ==="
rg -n --no-heading 'serde_json|^serde\s*=|^\s*\[dev-dependencies\]|\[dependencies\]|\[dev-dependencies\]|\[build-dependencies\]|\bserde\b' "$FILE" || true

echo
echo "=== Show [dependencies], [dev-dependencies], and [features] sections (serde_json/serde context) ==="
python3 - <<'PY'
import pathlib, re
path=pathlib.Path("crates/primitives/Cargo.toml")
lines=path.read_text(encoding="utf-8").splitlines()

sections=[]
for i,l in enumerate(lines):
    if re.match(r'^\[.+\]\s*$', l.strip()):
        sections.append((i, l.strip()))

# Extract a window around each relevant section
targets = {"[dependencies]", "[dev-dependencies]", "[features]"}
for si, header in sections:
    if header in targets:
        start=si
        # end at next top-level section or EOF
        end=len(lines)
        for sj, _h in sections:
            if sj>si:
                end=sj
                break
        for k in range(start, min(end, start+260)):
            print(f"{k+1:5d}\t{lines[k]}")
        print()
PY

Repository: morph-l2/morph-reth

Length of output: 2663


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="crates/primitives/src/transaction/l1_transaction.rs"

echo "=== Context near the cfg_attr derive (around line 1-120) ==="
python3 - <<'PY'
path="crates/primitives/src/transaction/l1_transaction.rs"
for i in range(1,121):
    with open(path,'r',encoding='utf-8') as f:
        lines=f.read().splitlines()
    break
PY

Repository: morph-l2/morph-reth

Length of output: 124


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="crates/primitives/src/transaction/l1_transaction.rs"
python3 - <<'PY'
path="crates/primitives/src/transaction/l1_transaction.rs"
start,end=1,120
with open(path,'r',encoding='utf-8') as f:
    for i,line in enumerate(f, start=1):
        if start<=i<=end:
            print(f"{i:5d}\t{line.rstrip()}")
        elif i>end:
            break
PY

echo
echo "=== Search for cfg_attr(feature = \"serde\", derive(...)) and serde gating ==="
rg -n --no-heading 'cfg_attr\(feature *= *"serde".*derive\(|cfg\(feature *= *"serde"\)|cfg!\(feature *= *"serde"\)|mod .*serde' "$FILE" || true

Repository: morph-l2/morph-reth

Length of output: 5502


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="crates/primitives/src/transaction/l1_transaction.rs"
echo "=== Search for test module boundaries near the serde JSON tests ==="
rg -n --no-heading 'cfg\(test\)|mod tests|mod serde|serde_tests' "$FILE"

echo
echo "=== Show a small context around the first cfg(test) occurrence ==="
line=$(rg -n --no-heading 'cfg\(test\)' "$FILE" | head -n 1 | cut -d: -f1 || true)
if [ -n "${line:-}" ]; then
  start=$((line-10))
  end=$((line+30))
  python3 - <<PY
path="$FILE"
start=$start
end=$end
with open(path,'r',encoding='utf-8') as f:
  for i,line in enumerate(f, start=1):
    if start<=i<=end:
      print(f"{i:5d}\t{line.rstrip()}")
    elif i>end:
      break
PY
else
  echo "No cfg(test) found"
fi

Repository: morph-l2/morph-reth

Length of output: 1412


Gate the serde JSON/RPC tests behind feature = "serde"

These tests call serde_json::{to_value, from_value} on TxL1Msg and use serde_l1_message_tx_rpc, but TxL1Msg only gets serde::Serialize/Deserialize via #[cfg_attr(feature = "serde", ...)] and serde_l1_message_tx_rpc is #[cfg(feature = "serde")]. With --no-default-features (disabling serde), this block won’t compile.

Suggested fix
+#[cfg(feature = "serde")]
 #[test]
 fn test_l1_transaction_json_includes_nonce_placeholder() {

Apply the same #[cfg(feature = "serde")] guard to the other serde-dependent tests in this block (lines ~669-790) or wrap them in a dedicated #[cfg(feature = "serde")] mod ... inside mod tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/primitives/src/transaction/l1_transaction.rs` around lines 669 - 790,
The serde-dependent tests (test_l1_transaction_json_includes_nonce_placeholder,
test_l1_transaction_json_round_trip,
test_serde_l1_message_tx_rpc_flattens_zero_signature,
test_l1_envelope_rpc_json_includes_geth_parity_fields) must be compiled only
when the serde feature is enabled; add #[cfg(feature = "serde")] to each of
these test functions or wrap them in a single #[cfg(feature = "serde")] mod
serde_tests { ... } inside mod tests so they are excluded when building with
--no-default-features.

L1 message transactions (type 0x7E) have no signature or nonce at the
consensus layer, but every standard EVM JSON-RPC client (ethers v5/v6,
viem, web3.js — including the @morph-network/* SDK adapters that wrap
them) expects every transaction object to expose `nonce`, `v`, `r`, `s`
and `yParity`. Without these fields, calling `eth_getBlockByNumber(num,
true)` or `eth_getTransactionByHash(l1MsgHash)` against a morph-reth RPC
endpoint causes downstream deserialization to fail; the prover landed an
ad-hoc tolerance patch on its own `TransactionTrace` for the same reason.

Fix it on the producing side, mirroring morph-geth's RPC contract:
TxL1Msg now serializes through L1MsgSerdeHelper (`serde(from = ...,
into = ...)`), which carries five extra placeholder fields — `nonce`,
`v`, `r`, `s`, `yParity` — all rendered as `"0x0"` and silently dropped
on deserialize so geth-shaped JSON also round-trips.

Because `alloy_consensus::Sealed` already flattens its inner T into the
surrounding object, these top-level fields naturally appear at the
envelope's RPC root with no envelope-level wiring required. RLP, EIP-2718,
signing, and reth-codec Compact (DB) encodings are unchanged.
@panos-xyz panos-xyz force-pushed the fix/l1msg-rpc-geth-parity branch from 2b0fe8b to b734aaf Compare June 2, 2026 07:35
@panos-xyz panos-xyz merged commit 27c9102 into main Jun 2, 2026
13 checks passed
@panos-xyz panos-xyz deleted the fix/l1msg-rpc-geth-parity branch June 2, 2026 08:29
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.

2 participants