Skip to content

Fix RLP decoding for MorphTx#299

Merged
FletcherMan merged 2 commits intomainfrom
fix_morph_tx
Mar 17, 2026
Merged

Fix RLP decoding for MorphTx#299
FletcherMan merged 2 commits intomainfrom
fix_morph_tx

Conversation

@SegueII
Copy link
Copy Markdown
Contributor

@SegueII SegueII commented Mar 17, 2026

Summary by CodeRabbit

  • New Features

    • Transactions now support version-aware decoding to handle legacy and newer wire formats.
  • Bug Fixes

    • Improved decoding and encoding validation to prevent malformed legacy transactions.
  • Tests

    • Replaced compatibility tests with an expanded test suite covering encoding/decoding, round-trips, version detection, hashing, and edge cases.

@SegueII SegueII requested a review from a team as a code owner March 17, 2026 03:12
@SegueII SegueII requested review from secmgt and removed request for a team March 17, 2026 03:12
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 17, 2026

📝 Walkthrough

Walkthrough

Adds a version-aware RLP decoder for MorphTx and a v0 encoding guard; removes an old compatibility test file and adds a comprehensive new test suite validating V0/V1 encoding/decoding, round-trips, and hash/wire-format consistency.

Changes

Cohort / File(s) Summary
MorphTx encoding/decoding
core/types/morph_tx.go
Adds DecodeRLP(s *rlp.Stream) error implementing rlp.Decoder to detect V0 vs V1 formats and delegate to appropriate decode paths; adds a guard in the v0 encoding path requiring non-zero FeeTokenID.
Removed compatibility tests
core/types/morph_tx_compat_test.go
Removes the legacy compatibility test file that contained many V0/V1 compatibility, round-trip, version-detection, encode-consistency, and hash tests.
New comprehensive tests
core/types/morph_tx_test.go
Adds extensive tests covering DecodeRLP (V0/V1), encoding/decoding round-trips, version detection, encode vs custom wire bytes consistency, hashing vs keccak256(wire), batch decoding, and regression cases; includes new test helpers for field-level assertions.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through bytes and found the clue,
V0 or V1 — I know what to do.
Guards in place, tests bloom bright,
RLP dances, order right. ✨

🚥 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 'Fix RLP decoding for MorphTx' directly addresses the main change: adding a DecodeRLP method to implement version-aware RLP decoding for MorphTx.
Docstring Coverage ✅ Passed Docstring coverage is 94.74% 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 (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix_morph_tx
📝 Coding Plan
  • Generate coding plan for human review comments

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.11.3)

Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions
The command is terminated due to an error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions


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.

Tip

You can validate your CodeRabbit configuration file in your editor.

If your editor has YAML language server, you can enable auto-completion and validation by adding # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json at the top of your CodeRabbit configuration file.

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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/types/morph_tx_test.go`:
- Around line 1379-1449: The helper assertMorphTxEqual currently omits comparing
the MorphTx.AccessList field; add a comparison for want.AccessList vs
got.AccessList in assertMorphTxEqual that normalizes nil and empty access lists
(treat nil and empty as equivalent) and checks element-wise equality (e.g.,
convert nil to empty slice and use reflect.DeepEqual or manual element
comparison), and on mismatch call t.Errorf with both values so AccessList
serialization regressions are detected. Ensure this new block references
MorphTx.AccessList and is placed alongside the other field checks (for example
after the Memo check).

In `@core/types/morph_tx.go`:
- Around line 211-218: DecodeRLP currently rejects a leading 0x00 version byte
while decode() treats 0x00 as the V0 form; change the version handling in
DecodeRLP so that when reading versionByte via s.Uint8() a value of 0x00 is
routed to the existing V0 decoder (same behavior as decode()), allow
MorphTxVersion1 to continue to be handled as the V1 path, and only return an
unsupported-version error for other bytes. Update the same logic block
referenced around the other occurrence (lines ~292-305) so both DecodeRLP and
decode() accept identical byte streams; refer to the symbols versionByte,
MorphTxVersion1, DecodeRLP and decode() to locate and update the checks.
- Around line 209-223: Before decoding into a potentially reused MorphTx, clear
version-specific state so stale V1 data doesn't survive: in DecodeRLP (the
function that calls decodeV0MorphTxRLP/decodeV1MorphTxRLP) set tx.Version = 0
and tx.Reference = nil and tx.Memo = nil before branching; when you detect a V1
versionByte, set tx.Version = MorphTxVersion1 after validation and before
calling decodeV1MorphTxRLP. Apply the same reset pattern to the other decoding
entry-points referenced (the blocks that call
decodeV0MorphTxRLP/decodeV1MorphTxRLP at the other locations noted) so
decodeV0MorphTxRLP and decodeV1MorphTxRLP never accidentally inherit prior
Reference/Memo values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 54a63857-bc46-42e7-aef3-ee2190c7415a

📥 Commits

Reviewing files that changed from the base of the PR and between 2a37b21 and 623498a.

📒 Files selected for processing (3)
  • core/types/morph_tx.go
  • core/types/morph_tx_compat_test.go
  • core/types/morph_tx_test.go
💤 Files with no reviewable changes (1)
  • core/types/morph_tx_compat_test.go

Comment on lines +1379 to +1449
// assertMorphTxEqual compares two MorphTx structs field by field.
func assertMorphTxEqual(t *testing.T, want, got *MorphTx) {
t.Helper()

if want.Version != got.Version {
t.Errorf("Version: want %d, got %d", want.Version, got.Version)
}
if want.FeeTokenID != got.FeeTokenID {
t.Errorf("FeeTokenID: want %d, got %d", want.FeeTokenID, got.FeeTokenID)
}
if want.Nonce != got.Nonce {
t.Errorf("Nonce: want %d, got %d", want.Nonce, got.Nonce)
}
if want.Gas != got.Gas {
t.Errorf("Gas: want %d, got %d", want.Gas, got.Gas)
}
assertBigIntEqual(t, "ChainID", want.ChainID, got.ChainID)
assertBigIntEqual(t, "GasTipCap", want.GasTipCap, got.GasTipCap)
assertBigIntEqual(t, "GasFeeCap", want.GasFeeCap, got.GasFeeCap)
assertBigIntEqual(t, "Value", want.Value, got.Value)
assertBigIntEqual(t, "V", want.V, got.V)
assertBigIntEqual(t, "R", want.R, got.R)
assertBigIntEqual(t, "S", want.S, got.S)

// FeeLimit: nil and zero are treated as equivalent in RLP
wantFeeLimit := want.FeeLimit
gotFeeLimit := got.FeeLimit
if wantFeeLimit == nil {
wantFeeLimit = new(big.Int)
}
if gotFeeLimit == nil {
gotFeeLimit = new(big.Int)
}
if wantFeeLimit.Cmp(gotFeeLimit) != 0 {
t.Errorf("FeeLimit: want %v, got %v", want.FeeLimit, got.FeeLimit)
}

if !bytes.Equal(want.Data, got.Data) {
t.Errorf("Data: want %x, got %x", want.Data, got.Data)
}

// To
if want.To == nil && got.To != nil {
t.Errorf("To: want nil, got %v", got.To)
} else if want.To != nil && got.To == nil {
t.Errorf("To: want %v, got nil", want.To)
} else if want.To != nil && got.To != nil && *want.To != *got.To {
t.Errorf("To: want %v, got %v", want.To, got.To)
}

// Reference
if want.Reference == nil && got.Reference != nil {
t.Errorf("Reference: want nil, got %v", got.Reference)
} else if want.Reference != nil && got.Reference == nil {
t.Errorf("Reference: want %v, got nil", want.Reference)
} else if want.Reference != nil && got.Reference != nil && *want.Reference != *got.Reference {
t.Errorf("Reference: want %v, got %v", want.Reference, got.Reference)
}

// Memo
var wantMemo, gotMemo []byte
if want.Memo != nil {
wantMemo = *want.Memo
}
if got.Memo != nil {
gotMemo = *got.Memo
}
if !bytes.Equal(wantMemo, gotMemo) {
t.Errorf("Memo: want %x, got %x", wantMemo, gotMemo)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

assertMorphTxEqual never validates AccessList.

Cases like V0 with AccessList rely on this helper, but the helper skips the field entirely. An access-list serialization regression would still pass this suite.

Suggested fix
 func assertMorphTxEqual(t *testing.T, want, got *MorphTx) {
 	t.Helper()

 	if want.Version != got.Version {
 		t.Errorf("Version: want %d, got %d", want.Version, got.Version)
 	}
 	if want.FeeTokenID != got.FeeTokenID {
 		t.Errorf("FeeTokenID: want %d, got %d", want.FeeTokenID, got.FeeTokenID)
 	}
 	if want.Nonce != got.Nonce {
 		t.Errorf("Nonce: want %d, got %d", want.Nonce, got.Nonce)
 	}
 	if want.Gas != got.Gas {
 		t.Errorf("Gas: want %d, got %d", want.Gas, got.Gas)
 	}
 	assertBigIntEqual(t, "ChainID", want.ChainID, got.ChainID)
 	assertBigIntEqual(t, "GasTipCap", want.GasTipCap, got.GasTipCap)
 	assertBigIntEqual(t, "GasFeeCap", want.GasFeeCap, got.GasFeeCap)
 	assertBigIntEqual(t, "Value", want.Value, got.Value)
 	assertBigIntEqual(t, "V", want.V, got.V)
 	assertBigIntEqual(t, "R", want.R, got.R)
 	assertBigIntEqual(t, "S", want.S, got.S)

 	// FeeLimit: nil and zero are treated as equivalent in RLP
 	wantFeeLimit := want.FeeLimit
 	gotFeeLimit := got.FeeLimit
 	if wantFeeLimit == nil {
 		wantFeeLimit = new(big.Int)
 	}
 	if gotFeeLimit == nil {
 		gotFeeLimit = new(big.Int)
 	}
 	if wantFeeLimit.Cmp(gotFeeLimit) != 0 {
 		t.Errorf("FeeLimit: want %v, got %v", want.FeeLimit, got.FeeLimit)
 	}

 	if !bytes.Equal(want.Data, got.Data) {
 		t.Errorf("Data: want %x, got %x", want.Data, got.Data)
 	}
+
+	if len(want.AccessList) != len(got.AccessList) {
+		t.Errorf("AccessList length: want %d, got %d", len(want.AccessList), len(got.AccessList))
+	} else {
+		for i := range want.AccessList {
+			if want.AccessList[i].Address != got.AccessList[i].Address {
+				t.Errorf("AccessList[%d].Address: want %v, got %v", i, want.AccessList[i].Address, got.AccessList[i].Address)
+			}
+			if len(want.AccessList[i].StorageKeys) != len(got.AccessList[i].StorageKeys) {
+				t.Errorf("AccessList[%d].StorageKeys length: want %d, got %d", i, len(want.AccessList[i].StorageKeys), len(got.AccessList[i].StorageKeys))
+				continue
+			}
+			for j := range want.AccessList[i].StorageKeys {
+				if want.AccessList[i].StorageKeys[j] != got.AccessList[i].StorageKeys[j] {
+					t.Errorf("AccessList[%d].StorageKeys[%d]: want %v, got %v", i, j, want.AccessList[i].StorageKeys[j], got.AccessList[i].StorageKeys[j])
+				}
+			}
+		}
+	}

 	// To
 	if want.To == nil && got.To != nil {
 		t.Errorf("To: want nil, got %v", got.To)
 	} else if want.To != nil && got.To == nil {
📝 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
// assertMorphTxEqual compares two MorphTx structs field by field.
func assertMorphTxEqual(t *testing.T, want, got *MorphTx) {
t.Helper()
if want.Version != got.Version {
t.Errorf("Version: want %d, got %d", want.Version, got.Version)
}
if want.FeeTokenID != got.FeeTokenID {
t.Errorf("FeeTokenID: want %d, got %d", want.FeeTokenID, got.FeeTokenID)
}
if want.Nonce != got.Nonce {
t.Errorf("Nonce: want %d, got %d", want.Nonce, got.Nonce)
}
if want.Gas != got.Gas {
t.Errorf("Gas: want %d, got %d", want.Gas, got.Gas)
}
assertBigIntEqual(t, "ChainID", want.ChainID, got.ChainID)
assertBigIntEqual(t, "GasTipCap", want.GasTipCap, got.GasTipCap)
assertBigIntEqual(t, "GasFeeCap", want.GasFeeCap, got.GasFeeCap)
assertBigIntEqual(t, "Value", want.Value, got.Value)
assertBigIntEqual(t, "V", want.V, got.V)
assertBigIntEqual(t, "R", want.R, got.R)
assertBigIntEqual(t, "S", want.S, got.S)
// FeeLimit: nil and zero are treated as equivalent in RLP
wantFeeLimit := want.FeeLimit
gotFeeLimit := got.FeeLimit
if wantFeeLimit == nil {
wantFeeLimit = new(big.Int)
}
if gotFeeLimit == nil {
gotFeeLimit = new(big.Int)
}
if wantFeeLimit.Cmp(gotFeeLimit) != 0 {
t.Errorf("FeeLimit: want %v, got %v", want.FeeLimit, got.FeeLimit)
}
if !bytes.Equal(want.Data, got.Data) {
t.Errorf("Data: want %x, got %x", want.Data, got.Data)
}
// To
if want.To == nil && got.To != nil {
t.Errorf("To: want nil, got %v", got.To)
} else if want.To != nil && got.To == nil {
t.Errorf("To: want %v, got nil", want.To)
} else if want.To != nil && got.To != nil && *want.To != *got.To {
t.Errorf("To: want %v, got %v", want.To, got.To)
}
// Reference
if want.Reference == nil && got.Reference != nil {
t.Errorf("Reference: want nil, got %v", got.Reference)
} else if want.Reference != nil && got.Reference == nil {
t.Errorf("Reference: want %v, got nil", want.Reference)
} else if want.Reference != nil && got.Reference != nil && *want.Reference != *got.Reference {
t.Errorf("Reference: want %v, got %v", want.Reference, got.Reference)
}
// Memo
var wantMemo, gotMemo []byte
if want.Memo != nil {
wantMemo = *want.Memo
}
if got.Memo != nil {
gotMemo = *got.Memo
}
if !bytes.Equal(wantMemo, gotMemo) {
t.Errorf("Memo: want %x, got %x", wantMemo, gotMemo)
}
}
// assertMorphTxEqual compares two MorphTx structs field by field.
func assertMorphTxEqual(t *testing.T, want, got *MorphTx) {
t.Helper()
if want.Version != got.Version {
t.Errorf("Version: want %d, got %d", want.Version, got.Version)
}
if want.FeeTokenID != got.FeeTokenID {
t.Errorf("FeeTokenID: want %d, got %d", want.FeeTokenID, got.FeeTokenID)
}
if want.Nonce != got.Nonce {
t.Errorf("Nonce: want %d, got %d", want.Nonce, got.Nonce)
}
if want.Gas != got.Gas {
t.Errorf("Gas: want %d, got %d", want.Gas, got.Gas)
}
assertBigIntEqual(t, "ChainID", want.ChainID, got.ChainID)
assertBigIntEqual(t, "GasTipCap", want.GasTipCap, got.GasTipCap)
assertBigIntEqual(t, "GasFeeCap", want.GasFeeCap, got.GasFeeCap)
assertBigIntEqual(t, "Value", want.Value, got.Value)
assertBigIntEqual(t, "V", want.V, got.V)
assertBigIntEqual(t, "R", want.R, got.R)
assertBigIntEqual(t, "S", want.S, got.S)
// FeeLimit: nil and zero are treated as equivalent in RLP
wantFeeLimit := want.FeeLimit
gotFeeLimit := got.FeeLimit
if wantFeeLimit == nil {
wantFeeLimit = new(big.Int)
}
if gotFeeLimit == nil {
gotFeeLimit = new(big.Int)
}
if wantFeeLimit.Cmp(gotFeeLimit) != 0 {
t.Errorf("FeeLimit: want %v, got %v", want.FeeLimit, got.FeeLimit)
}
if !bytes.Equal(want.Data, got.Data) {
t.Errorf("Data: want %x, got %x", want.Data, got.Data)
}
if len(want.AccessList) != len(got.AccessList) {
t.Errorf("AccessList length: want %d, got %d", len(want.AccessList), len(got.AccessList))
} else {
for i := range want.AccessList {
if want.AccessList[i].Address != got.AccessList[i].Address {
t.Errorf("AccessList[%d].Address: want %v, got %v", i, want.AccessList[i].Address, got.AccessList[i].Address)
}
if len(want.AccessList[i].StorageKeys) != len(got.AccessList[i].StorageKeys) {
t.Errorf("AccessList[%d].StorageKeys length: want %d, got %d", i, len(want.AccessList[i].StorageKeys), len(got.AccessList[i].StorageKeys))
continue
}
for j := range want.AccessList[i].StorageKeys {
if want.AccessList[i].StorageKeys[j] != got.AccessList[i].StorageKeys[j] {
t.Errorf("AccessList[%d].StorageKeys[%d]: want %v, got %v", i, j, want.AccessList[i].StorageKeys[j], got.AccessList[i].StorageKeys[j])
}
}
}
}
// To
if want.To == nil && got.To != nil {
t.Errorf("To: want nil, got %v", got.To)
} else if want.To != nil && got.To == nil {
t.Errorf("To: want %v, got nil", want.To)
} else if want.To != nil && got.To != nil && *want.To != *got.To {
t.Errorf("To: want %v, got %v", want.To, got.To)
}
// Reference
if want.Reference == nil && got.Reference != nil {
t.Errorf("Reference: want nil, got %v", got.Reference)
} else if want.Reference != nil && got.Reference == nil {
t.Errorf("Reference: want %v, got nil", want.Reference)
} else if want.Reference != nil && got.Reference != nil && *want.Reference != *got.Reference {
t.Errorf("Reference: want %v, got %v", want.Reference, got.Reference)
}
// Memo
var wantMemo, gotMemo []byte
if want.Memo != nil {
wantMemo = *want.Memo
}
if got.Memo != nil {
gotMemo = *got.Memo
}
if !bytes.Equal(wantMemo, gotMemo) {
t.Errorf("Memo: want %x, got %x", wantMemo, gotMemo)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/types/morph_tx_test.go` around lines 1379 - 1449, The helper
assertMorphTxEqual currently omits comparing the MorphTx.AccessList field; add a
comparison for want.AccessList vs got.AccessList in assertMorphTxEqual that
normalizes nil and empty access lists (treat nil and empty as equivalent) and
checks element-wise equality (e.g., convert nil to empty slice and use
reflect.DeepEqual or manual element comparison), and on mismatch call t.Errorf
with both values so AccessList serialization regressions are detected. Ensure
this new block references MorphTx.AccessList and is placed alongside the other
field checks (for example after the Memo check).

Comment thread core/types/morph_tx.go
Comment on lines +209 to +223
return decodeV0MorphTxRLP(tx, raw)
}
// V1+ format: version byte followed by RLP list
versionByte, err := s.Uint8()
if err != nil {
return err
}
if versionByte != MorphTxVersion1 {
return errors.New("unsupported morph tx version: " + strconv.Itoa(int(versionByte)))
}
raw, err := s.Raw()
if err != nil {
return err
}
return decodeV1MorphTxRLP(tx, raw)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reset version-specific fields before decoding into a reused receiver.

DecodeRLP now fills the caller-supplied MorphTx, but decodeV0MorphTxRLP never restores Version/Reference/Memo, and decodeV1MorphTxRLP only assigns Reference/Memo when the new payload is non-empty. Decode V1(with memo) followed by V0 or V1 minimal into the same instance and the stale V1 state survives, which then drives the wrong downstream behavior.

Suggested fix
func decodeV1MorphTxRLP(tx *MorphTx, blob []byte) error {
	var v1 v1MorphTxRLP
	if err := rlp.DecodeBytes(blob, &v1); err != nil {
		return err
	}

+	tx.Reference = nil
+	tx.Memo = nil
 	tx.ChainID = v1.ChainID
 	tx.Nonce = v1.Nonce
 	tx.GasTipCap = v1.GasTipCap
 	tx.GasFeeCap = v1.GasFeeCap
 	tx.Gas = v1.Gas
 	tx.To = v1.To
 	tx.Value = v1.Value
 	tx.Data = v1.Data
 	tx.AccessList = v1.AccessList
 	tx.Version = MorphTxVersion1
 	tx.FeeTokenID = v1.FeeTokenID
 	tx.FeeLimit = v1.FeeLimit
 	// Convert []byte to *common.Reference
 	if len(v1.Reference) != 0 && len(v1.Reference) != common.ReferenceLength {
 		return errors.New("invalid reference length: expected 0 or " + strconv.Itoa(common.ReferenceLength) + ", got " + strconv.Itoa(len(v1.Reference)))
 	}
 	if len(v1.Reference) == common.ReferenceLength {
 		ref := common.BytesToReference(v1.Reference)
 		tx.Reference = &ref
 	}
 	// Convert []byte to *[]byte and validate memo length
 	if len(v1.Memo) > common.MaxMemoLength {
 		return errors.New("memo exceeds maximum length of " + strconv.Itoa(common.MaxMemoLength) + " bytes, got " + strconv.Itoa(len(v1.Memo)))
 	}
 	if len(v1.Memo) > 0 {
 		tx.Memo = &v1.Memo
 	}
 	tx.V = v1.V
 	tx.R = v1.R
 	tx.S = v1.S

 	return nil
}

func decodeV0MorphTxRLP(tx *MorphTx, blob []byte) error {
	var v0 v0MorphTxRLP
	if err := rlp.DecodeBytes(blob, &v0); err != nil {
		return err
	}

	if v0.FeeTokenID == 0 {
		return errors.New("invalid fee token id, expected non-zero")
	}

+	tx.Version = MorphTxVersion0
+	tx.Reference = nil
+	tx.Memo = nil
 	tx.ChainID = v0.ChainID
 	tx.Nonce = v0.Nonce
 	tx.GasTipCap = v0.GasTipCap
 	tx.GasFeeCap = v0.GasFeeCap
 	tx.Gas = v0.Gas
 	tx.To = v0.To
 	tx.Value = v0.Value
 	tx.Data = v0.Data
 	tx.AccessList = v0.AccessList
 	tx.FeeTokenID = v0.FeeTokenID
 	tx.FeeLimit = v0.FeeLimit
 	tx.V = v0.V
 	tx.R = v0.R
 	tx.S = v0.S

 	return nil
}

Also applies to: 314-340, 348-373

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

In `@core/types/morph_tx.go` around lines 209 - 223, Before decoding into a
potentially reused MorphTx, clear version-specific state so stale V1 data
doesn't survive: in DecodeRLP (the function that calls
decodeV0MorphTxRLP/decodeV1MorphTxRLP) set tx.Version = 0 and tx.Reference = nil
and tx.Memo = nil before branching; when you detect a V1 versionByte, set
tx.Version = MorphTxVersion1 after validation and before calling
decodeV1MorphTxRLP. Apply the same reset pattern to the other decoding
entry-points referenced (the blocks that call
decodeV0MorphTxRLP/decodeV1MorphTxRLP at the other locations noted) so
decodeV0MorphTxRLP and decodeV1MorphTxRLP never accidentally inherit prior
Reference/Memo values.

Comment thread core/types/morph_tx.go
Comment on lines +211 to +218
// V1+ format: version byte followed by RLP list
versionByte, err := s.Uint8()
if err != nil {
return err
}
if versionByte != MorphTxVersion1 {
return errors.New("unsupported morph tx version: " + strconv.Itoa(int(versionByte)))
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Keep DecodeRLP aligned with decode() for the 0x00 V0 form.

Line 296 still routes a leading 0x00 to the V0 decoder, but Lines 216-218 reject the same input as an unsupported version. Right now rlp.DecodeBytes and decode() do not accept the same byte streams.

Also applies to: 292-305

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

In `@core/types/morph_tx.go` around lines 211 - 218, DecodeRLP currently rejects a
leading 0x00 version byte while decode() treats 0x00 as the V0 form; change the
version handling in DecodeRLP so that when reading versionByte via s.Uint8() a
value of 0x00 is routed to the existing V0 decoder (same behavior as decode()),
allow MorphTxVersion1 to continue to be handled as the V1 path, and only return
an unsupported-version error for other bytes. Update the same logic block
referenced around the other occurrence (lines ~292-305) so both DecodeRLP and
decode() accept identical byte streams; refer to the symbols versionByte,
MorphTxVersion1, DecodeRLP and decode() to locate and update the checks.

@FletcherMan FletcherMan merged commit 1d46057 into main Mar 17, 2026
8 checks passed
@FletcherMan FletcherMan deleted the fix_morph_tx branch March 17, 2026 04:19
FletcherMan added a commit that referenced this pull request Apr 23, 2026
* ci: support multi-platform Docker image build (amd64 + arm64) (#298)

* ci: support multi-platform Docker image build (amd64 + arm64)

Use docker/build-push-action with QEMU and buildx to build multi-arch
images. Mac arm64 users can now pull and run the image natively.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: add workflow_dispatch for manual Docker image build

Allow manually triggering the Docker build from GitHub Actions UI
with a tag name input, useful for re-building existing tags.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: fix incorrect COMMIT and VERSION on manual dispatch

Use git rev-parse HEAD for COMMIT and stripped version for VERSION
build-arg, so they are correct in both tag-push and workflow_dispatch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: fletcher.fan <fletcher.fan@bitget.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* Fix RLP decoding for MorphTx (#299)

* implement version-aware RLP decoding for MorphTx

* fix morph tx

* pruner: fall back to disk snapshot root when journal is missing (#300)

* pruner: fall back to disk snapshot root when journal is missing

When geth is killed uncleanly (SIGKILL before BlockChain.Stop writes
the snapshot journal), prune-state fails with:

  WARN Loaded snapshot journal  diskroot=XXX  diffs=missing
  ERROR head doesn't match snapshot: have XXX, want YYY

NewPruner now reads the persisted disk snapshot root via
rawdb.ReadSnapshotRoot and retries snapshot initialisation with that
root when the normal head-based init fails.  Prune() then uses the
disk root as the pruning target directly, bypassing the requirement
for 128 in-memory diff layers that cannot exist when the journal was
not written.

Normal flow (clean shutdown, journal present) is unchanged.

Made-with: Cursor

* pruner: fix Cap panic on disk-layer-only tree and add generation wait log

Two follow-up fixes to the journal-missing fallback (56ae344):

1. Skip snaptree.Cap(root, 0) when root is already the disk layer.
   Cap requires a diffLayer as its target; calling it on a disk-layer-only
   tree (which is exactly what the fallback produces) returns
   "snapshot is disk layer" and aborts after all the heavy bloom-filter
   and DB-sweep work is done. Guard with DiskRoot() != root.

2. Add log lines around the fallback snapshot.New() call to make it
   visible when snapshot generation must be resumed (async=false blocks
   until generation finishes, which can take hours for large state).

* pruner: rename diskRoot to snapDiskRoot to avoid confusion with diskStateRoot

---------

Co-authored-by: corey <corey.zhang@bitget.com>

* Revert "pruner: fall back to disk snapshot root when journal is missing (#300)" (#309)

This reverts commit b3c5552.

Co-authored-by: corey <corey.zhang@bitget.com>

* tracers: fix Morph fee-token tracing paths (#308)

* tracers: fix Morph fee-token tracing paths

Keep Morph fee-token system calls bracketed consistently, forward system-call hooks through mux tracers, and make traceCall precredit alt-fee balances so tracing matches execution more closely.

Constraint: Preserve user-visible tracer output while keeping prestate and traceCall behavior correct for Morph fee-token transactions
Confidence: medium
Scope-risk: moderate
Not-tested: Full eth/tracers/internal/tracetest suite still has pre-existing fixture and VM failures on this branch

* tracers: fix prestateTracer account discovery when DisableStorage is set

* tracers: harden Morph fee-token trace edge cases

Prevent flatCallTracer from touching hidden system-call frames and keep traceCall's synthetic fee-token precredits out of prestate views so debug RPCs stay stable and prestate output matches chain state.

Constraint: Preserve Morph alt-fee trace execution without leaking synthetic prestate or hidden system-call frames
Confidence: high
Scope-risk: moderate

* core: require balanced system-call trace hooks

Only bracket fee-token helper calls when both start and end hooks are present so partial tracer wiring cannot leak system-call depth across a trace.

Constraint: Preserve existing V2-over-legacy hook selection while restoring balanced start/end semantics
Confidence: high
Scope-risk: narrow
Not-tested: Full core package outside TestStartSystemCallTrace

* fix: handle nil parameter in morph_diskRoot RPC to prevent panic (#311)

When morph_diskRoot is called without parameters, blockNrOrHash is nil,
causing a nil pointer dereference crash. Default to latest block when
no parameter is provided, consistent with other eth RPC methods.

* pruner: use teeWriter and HEAD as prune target, fix genesis root validation (#310)

* pruner: use teeWriter and HEAD as prune target, fix genesis root validation

- Add teeWriter to persist trie nodes to disk during GenerateTrie, ensuring
  pruning works correctly even after unclean shutdowns.
- Use HEAD directly as the pruning target instead of HEAD-127, eliminating
  unnecessary height rollback on L2 chains where reorgs don't occur.
- Resolve genesis root via ReadDiskStateRoot in extractGenesis so that
  zkTrie roots (overridden via GenesisStateRoot) are correctly mapped to
  the actual MPT disk root.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* handle error

* remove accidentally committed local test zip

Keep local-test.zip untracked and out of repository history from this point.

Made-with: Cursor

* pruner: enforce genesis disk-root mapping

Treat missing or invalid disk-state-root mapping for genesis as an explicit error during pruning instead of silently falling back.

Made-with: Cursor

---------

Co-authored-by: corey <corey.zhang@bitget.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: fletcher.fan <fletcher.fan@bitget.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Segue <huoda.china@163.com>
Co-authored-by: corey <coreyx1992@gmail.com>
Co-authored-by: corey <corey.zhang@bitget.com>
Co-authored-by: panos <pan107104@outlook.com>
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