Fix RLP decoding for MorphTx#299
Conversation
📝 WalkthroughWalkthroughAdds 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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
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 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. Comment 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 |
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
core/types/morph_tx.gocore/types/morph_tx_compat_test.gocore/types/morph_tx_test.go
💤 Files with no reviewable changes (1)
- core/types/morph_tx_compat_test.go
| // 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| // 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).
| 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) |
There was a problem hiding this comment.
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.
| // 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))) | ||
| } |
There was a problem hiding this comment.
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.
* 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>
Summary by CodeRabbit
New Features
Bug Fixes
Tests