Skip to content

fix: handle MorphTx V0/V1 decoding in DecodeTxsFromBytes#920

Merged
FletcherMan merged 1 commit intomainfrom
fix/decode-txs-morph-tx-support
Mar 27, 2026
Merged

fix: handle MorphTx V0/V1 decoding in DecodeTxsFromBytes#920
FletcherMan merged 1 commit intomainfrom
fix/decode-txs-morph-tx-support

Conversation

@FletcherMan
Copy link
Copy Markdown
Collaborator

@FletcherMan FletcherMan commented Mar 26, 2026

DecodeTxsFromBytes panicked with "makeslice: len out of range" when decoding MorphTx V1 transactions. The V1 wire format includes a version byte between the type byte and the RLP payload, which the old code mistakenly treated as an RLP prefix, causing a uint8 underflow.

Additionally, the old code used rlp.DecodeBytes with *MorphTx which bypasses MorphTx.decode() and cannot handle the V0/V1 field differences.

Changes:

  • Add decodeMorphTx() to handle both V0 and V1 wire formats, using Transaction.UnmarshalBinary() which correctly routes to MorphTx.decode()
  • Add decodeTypedTx() for standard EIP-2718 typed txs, also using UnmarshalBinary() for consistency
  • Add bounds check in extractInnerTxFullBytes to prevent panic on invalid RLP prefix bytes
  • Add test cases for MorphTx V0, V1, and mixed multi-tx decoding

Made-with: Cursor

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced transaction decoding with improved error handling and validation across multiple transaction type formats, including support for new transaction variants (V0 and V1).
  • Tests

    • Added comprehensive test coverage for transaction decoding scenarios.

DecodeTxsFromBytes panicked with "makeslice: len out of range" when
decoding MorphTx V1 transactions. The V1 wire format includes a version
byte between the type byte and the RLP payload, which the old code
mistakenly treated as an RLP prefix, causing a uint8 underflow.

Additionally, the old code used rlp.DecodeBytes with *MorphTx which
bypasses MorphTx.decode() and cannot handle the V0/V1 field differences.

Changes:
- Add decodeMorphTx() to handle both V0 and V1 wire formats, using
  Transaction.UnmarshalBinary() which correctly routes to MorphTx.decode()
- Add decodeTypedTx() for standard EIP-2718 typed txs, also using
  UnmarshalBinary() for consistency
- Add bounds check in extractInnerTxFullBytes to prevent panic on
  invalid RLP prefix bytes
- Add test cases for MorphTx V0, V1, and mixed multi-tx decoding

Made-with: Cursor
@FletcherMan FletcherMan requested a review from a team as a code owner March 26, 2026 12:20
@FletcherMan FletcherMan requested review from SecurityLife and removed request for a team March 26, 2026 12:20
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 2026

📝 Walkthrough

Walkthrough

The pull request refactors the DecodeTxsFromBytes function to use per-iteration type-byte dispatch for transaction decoding. It introduces decodeTypedTx and decodeMorphTx helper functions to replace inline decoding logic, handles multiple transaction types (AccessList, DynamicFee, SetCode, Morph, and legacy), and includes test coverage for MorphTx variants and mixed transaction batches.

Changes

Cohort / File(s) Summary
Transaction Decoding Refactoring
node/types/blob.go
Refactored DecodeTxsFromBytes to dispatch decoding by type byte: standard typed txs use decodeTypedTx, Morph txs use decodeMorphTx, legacy/unrecognized types extract full inner bytes and construct via RLP. Added validation in extractInnerTxFullBytes rejecting sizeByteLen > 4. Error messages now reference typeByte instead of firstByte.
Decoding Test Coverage
node/types/blob_test.go
Added three test cases: MorphTxV0 and MorphTxV1 single-transaction decoding, and a mixed-batch test decoding DynamicFee, two Morph variants, and Legacy transactions with type and hash verification.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • panos-xyz
  • r3aker86

Poem

🐰 Hops through the bytes with glee so bright,
Dispatching types by byte alight!
Morph and Legacy now sorted neat,
Helper functions make refactoring sweet!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically identifies the primary change: adding MorphTx V0/V1 decoding support to DecodeTxsFromBytes, which matches the core objective and file changes.

✏️ 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/decode-txs-morph-tx-support

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
Copy Markdown
Contributor

@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.

Caution

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

⚠️ Outside diff range comments (1)
node/types/blob.go (1)

228-241: ⚠️ Potential issue | 🔴 Critical

Reject oversized RLP lengths before allocating.

Line 240 still trusts the declared RLP payload length and allocates before checking how many bytes remain. DecodeTxsFromBytes is fed directly from blob sidecar data in node/derivation/batch_info.go:95-171, so a malformed prefix like 0xfb ffffffff can request ~4 GiB and take the node down before EOF is returned.

🛡️ Proposed hardening
+type remainingReader interface {
+	io.Reader
+	Len() int
+}
+
-func decodeTypedTx(typeByte byte, reader io.Reader) (*eth.Transaction, error) {
+func decodeTypedTx(typeByte byte, reader remainingReader) (*eth.Transaction, error) {
 	...
 }
 
-func decodeMorphTx(reader io.Reader) (*eth.Transaction, error) {
+func decodeMorphTx(reader remainingReader) (*eth.Transaction, error) {
 	...
 }
 
-func extractInnerTxFullBytes(firstByte byte, reader io.Reader) ([]byte, error) {
-	sizeByteLen := firstByte - 0xf7
+func extractInnerTxFullBytes(firstByte byte, reader remainingReader) ([]byte, error) {
+	if firstByte <= 0xf7 {
+		return nil, fmt.Errorf("expected long-form RLP list prefix, got 0x%x", firstByte)
+	}
+	sizeByteLen := int(firstByte - 0xf7)
 	if sizeByteLen > 4 {
 		return nil, fmt.Errorf("invalid RLP size byte length: %d (firstByte=0x%x)", sizeByteLen, firstByte)
 	}
 
 	sizeByte := make([]byte, sizeByteLen)
-	if err := binary.Read(reader, binary.BigEndian, sizeByte); err != nil {
+	if _, err := io.ReadFull(reader, sizeByte); err != nil {
 		return nil, err
 	}
 	size := binary.BigEndian.Uint32(append(make([]byte, 4-len(sizeByte)), sizeByte...))
+	if size > uint32(reader.Len()) {
+		return nil, io.ErrUnexpectedEOF
+	}
 
 	txRaw := make([]byte, size)
-	if err := binary.Read(reader, binary.BigEndian, txRaw); err != nil {
+	if _, err := io.ReadFull(reader, txRaw); err != nil {
 		return nil, err
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@node/types/blob.go` around lines 228 - 241, In extractInnerTxFullBytes ensure
you validate the decoded RLP payload length before allocating txRaw: after
computing size (from firstByte and sizeByte), check that size is within safe
bounds (e.g. not greater than the remaining bytes available from the provided
reader or a project-wide MAX_TX_SIZE) and return an error if it exceeds that
limit; do not call make([]byte, size) until that check passes. Reference
functions: extractInnerTxFullBytes (for the check/early reject) and
DecodeTxsFromBytes/node/derivation/batch_info.go (callers that feed untrusted
blob data). Ensure the error is descriptive (e.g. "declared RLP size too large")
so malformed prefixes like 0xfb ffffffff cannot trigger huge allocations.
🧹 Nitpick comments (2)
node/types/blob_test.go (2)

156-203: Add a malformed-prefix regression for the panic fix.

The new happy-path tests don't exercise the defensive branch added in extractInnerTxFullBytes. Please add a malformed Morph payload (for example 0x7f0180) and assert DecodeTxsFromBytes returns an error, so the original panic path stays covered by tests.

🧪 Minimal regression test
+func TestDecodeTxsFromBytes_InvalidMorphPrefix(t *testing.T) {
+	_, err := DecodeTxsFromBytes(common.FromHex("0x7f0180"))
+	require.Error(t, err)
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@node/types/blob_test.go` around lines 156 - 203, Add a regression test that
exercises the defensive branch in extractInnerTxFullBytes by passing a malformed
Morph prefix (e.g. bytes for "0x7f0180") into DecodeTxsFromBytes and asserting
it returns an error; specifically create a new test (similar style to
TestDecodeTxsFromBytes_MorphTxV0/V1) that calls DecodeTxsFromBytes with
common.FromHex("0x7f0180") (or equivalent raw bytes), requires a non-nil error
via require.Error(t, err) and ensures no panic occurs, so the malformed-prefix
case is covered.

156-170: Strengthen the Morph assertions beyond Type().

These tests only prove that decoding returns a Morph tx, not that the V0/V1-specific contents survived decoding. A decoder that still drops the V1-only fields would pass here, so please assert at least one stable value from each Morph fixture that depends on the decoded contents—e.g. an expected hash or another version-sensitive field set. Based on learnings: V0 transactions predate the Morph tx version field, so None → 0 is the correct semantic mapping.

Also applies to: 172-203

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

In `@node/types/blob_test.go` around lines 156 - 170, The tests only check
txs[0].Type()—add assertions that verify a version-sensitive field from each
fixture after DecodeTxsFromBytes returns (e.g., type-assert txs[0] to the
concrete Morph tx struct and assert its Version or a stable content-derived
value like the expected hash or the known payload string present in the V1
fixture ("morph hoodi test tx"); for V0 assert the decoder maps missing version
→ 0). Update TestDecodeTxsFromBytes_MorphTxV0 and
TestDecodeTxsFromBytes_MorphTxV1 to include these extra checks so the decoder
must preserve V0/V1-specific fields, while still keeping the existing Type()
assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@node/types/blob.go`:
- Around line 228-241: In extractInnerTxFullBytes ensure you validate the
decoded RLP payload length before allocating txRaw: after computing size (from
firstByte and sizeByte), check that size is within safe bounds (e.g. not greater
than the remaining bytes available from the provided reader or a project-wide
MAX_TX_SIZE) and return an error if it exceeds that limit; do not call
make([]byte, size) until that check passes. Reference functions:
extractInnerTxFullBytes (for the check/early reject) and
DecodeTxsFromBytes/node/derivation/batch_info.go (callers that feed untrusted
blob data). Ensure the error is descriptive (e.g. "declared RLP size too large")
so malformed prefixes like 0xfb ffffffff cannot trigger huge allocations.

---

Nitpick comments:
In `@node/types/blob_test.go`:
- Around line 156-203: Add a regression test that exercises the defensive branch
in extractInnerTxFullBytes by passing a malformed Morph prefix (e.g. bytes for
"0x7f0180") into DecodeTxsFromBytes and asserting it returns an error;
specifically create a new test (similar style to
TestDecodeTxsFromBytes_MorphTxV0/V1) that calls DecodeTxsFromBytes with
common.FromHex("0x7f0180") (or equivalent raw bytes), requires a non-nil error
via require.Error(t, err) and ensures no panic occurs, so the malformed-prefix
case is covered.
- Around line 156-170: The tests only check txs[0].Type()—add assertions that
verify a version-sensitive field from each fixture after DecodeTxsFromBytes
returns (e.g., type-assert txs[0] to the concrete Morph tx struct and assert its
Version or a stable content-derived value like the expected hash or the known
payload string present in the V1 fixture ("morph hoodi test tx"); for V0 assert
the decoder maps missing version → 0). Update TestDecodeTxsFromBytes_MorphTxV0
and TestDecodeTxsFromBytes_MorphTxV1 to include these extra checks so the
decoder must preserve V0/V1-specific fields, while still keeping the existing
Type() assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b2832882-6b0d-458e-89c0-30c513445186

📥 Commits

Reviewing files that changed from the base of the PR and between ac87fbd and 2919567.

📒 Files selected for processing (2)
  • node/types/blob.go
  • node/types/blob_test.go

@FletcherMan FletcherMan merged commit df02f26 into main Mar 27, 2026
13 checks passed
@FletcherMan FletcherMan deleted the fix/decode-txs-morph-tx-support branch March 27, 2026 02:26
FletcherMan added a commit that referenced this pull request Mar 27, 2026
fix: handle MorphTx V0/V1 decoding in DecodeTxsFromBytes (#920)
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