diff --git a/accounts/abi/bind/backends/simulated.go b/accounts/abi/bind/backends/simulated.go
index c009c0233..a00c81b65 100644
--- a/accounts/abi/bind/backends/simulated.go
+++ b/accounts/abi/bind/backends/simulated.go
@@ -918,8 +918,11 @@ func (m callMsg) IsL1MessageTx() bool { return false }
func (m callMsg) SetCodeAuthorizations() []types.SetCodeAuthorization {
return m.CallMsg.AuthorizationList
}
-func (m callMsg) FeeTokenID() uint16 { return m.CallMsg.FeeTokenID }
-func (m callMsg) FeeLimit() *big.Int { return m.CallMsg.FeeLimit }
+func (m callMsg) FeeTokenID() uint16 { return m.CallMsg.FeeTokenID }
+func (m callMsg) FeeLimit() *big.Int { return m.CallMsg.FeeLimit }
+func (m callMsg) Version() uint8 { return m.CallMsg.Version }
+func (m callMsg) Reference() *common.Reference { return m.CallMsg.Reference }
+func (m callMsg) Memo() *[]byte { return m.CallMsg.Memo }
// filterBackend implements filters.Backend to support filtering for logs without
// taking bloom-bits acceleration structures into account.
diff --git a/accounts/abi/bind/base.go b/accounts/abi/bind/base.go
index 263a7dd8a..2f7426a60 100644
--- a/accounts/abi/bind/base.go
+++ b/accounts/abi/bind/base.go
@@ -57,8 +57,11 @@ type TransactOpts struct {
GasTipCap *big.Int // Gas priority fee cap to use for the 1559 transaction execution (nil = gas price oracle)
GasLimit uint64 // Gas limit to set for the transaction execution (0 = estimate)
- FeeTokenID uint16 // alt fee token id of transaction execution
- FeeLimit *big.Int // alt fee token limit of transaction execution
+ FeeTokenID uint16 // alt fee token id of transaction execution
+ FeeLimit *big.Int // alt fee token limit of transaction execution
+ Version *uint8 // version of morph tx (nil = auto-detect)
+ Reference *common.Reference // reference key for the transaction (optional)
+ Memo *[]byte // memo for the transaction (optional)
Context context.Context // Network context to support cancellation and timeouts (nil = no timeout)
@@ -290,12 +293,19 @@ func (c *BoundContract) createDynamicTx(opts *TransactOpts, contract *common.Add
return types.NewTx(baseTx), nil
}
-func (c *BoundContract) createAltFeeTx(opts *TransactOpts, contract *common.Address, input []byte, head *types.Header) (*types.Transaction, error) {
+func (c *BoundContract) createMorphTx(opts *TransactOpts, contract *common.Address, input []byte, head *types.Header) (*types.Transaction, error) {
// Normalize value
value := opts.Value
if value == nil {
value = new(big.Int)
}
+
+ // Determine version and validate fields
+ version, err := c.morphTxVersion(opts)
+ if err != nil {
+ return nil, err
+ }
+
// Estimate TipCap
gasTipCap := opts.GasTipCap
if gasTipCap == nil {
@@ -334,7 +344,7 @@ func (c *BoundContract) createAltFeeTx(opts *TransactOpts, contract *common.Addr
if err != nil {
return nil, err
}
- baseTx := &types.AltFeeTx{
+ baseTx := &types.MorphTx{
To: contract,
Nonce: nonce,
GasFeeCap: gasFeeCap,
@@ -342,12 +352,65 @@ func (c *BoundContract) createAltFeeTx(opts *TransactOpts, contract *common.Addr
FeeTokenID: opts.FeeTokenID,
FeeLimit: opts.FeeLimit,
Gas: gasLimit,
+ Version: version,
+ Reference: opts.Reference,
+ Memo: opts.Memo,
Value: value,
Data: input,
}
return types.NewTx(baseTx), nil
}
+// morphTxVersion determines the MorphTx version and validates field requirements.
+// If version is explicitly specified, validate that parameters match:
+// - Version 0: FeeTokenID must be > 0, Reference and Memo must not be set
+// - Version 1: FeeTokenID, Reference, Memo are all optional;
+// if FeeTokenID is 0, FeeLimit must not be set
+//
+// If version is not explicitly specified, use heuristic detection:
+// - V1 if V1-specific fields (Reference, Memo) are present
+// - V0 otherwise (backward compatible with AltFeeTx behavior)
+func (c *BoundContract) morphTxVersion(opts *TransactOpts) (uint8, error) {
+ // Validate memo length
+ if opts.Memo != nil && len(*opts.Memo) > common.MaxMemoLength {
+ return 0, types.ErrMemoTooLong
+ }
+
+ // If version is not explicitly specified, determine based on fields:
+ // - V1 if V1-specific fields (Reference, Memo) are present
+ // - V0 otherwise (backward compatible with AltFeeTx behavior)
+ if opts.Version == nil {
+ hasV1Fields := (opts.Reference != nil && *opts.Reference != (common.Reference{})) ||
+ (opts.Memo != nil && len(*opts.Memo) > 0)
+ if hasV1Fields {
+ if opts.FeeTokenID == 0 && opts.FeeLimit != nil && opts.FeeLimit.Sign() != 0 {
+ return 0, types.ErrMorphTxV1IllegalExtraParams
+ }
+ return types.MorphTxVersion1, nil
+ }
+ return types.MorphTxVersion0, nil
+ }
+
+ // Version explicitly specified - validate parameters match
+ version := *opts.Version
+ switch version {
+ case types.MorphTxVersion0:
+ if opts.FeeTokenID == 0 ||
+ opts.Reference != nil && *opts.Reference != (common.Reference{}) ||
+ opts.Memo != nil && len(*opts.Memo) > 0 {
+ return 0, types.ErrMorphTxV0IllegalExtraParams
+ }
+ case types.MorphTxVersion1:
+ if opts.FeeTokenID == 0 && opts.FeeLimit != nil && opts.FeeLimit.Sign() != 0 {
+ return 0, types.ErrMorphTxV1IllegalExtraParams
+ }
+ default:
+ return 0, types.ErrMorphTxUnsupportedVersion
+ }
+
+ return version, nil
+}
+
func (c *BoundContract) createLegacyTx(opts *TransactOpts, contract *common.Address, input []byte) (*types.Transaction, error) {
if opts.GasFeeCap != nil || opts.GasTipCap != nil {
return nil, errors.New("maxFeePerGas or maxPriorityFeePerGas specified but curie is not active yet")
@@ -438,8 +501,11 @@ func (c *BoundContract) transact(opts *TransactOpts, contract *common.Address, i
if head, errHead := c.transactor.HeaderByNumber(ensureContext(opts.Context), nil); errHead != nil {
return nil, errHead
} else if head.BaseFee != nil {
- if opts.FeeTokenID != 0 {
- rawTx, err = c.createAltFeeTx(opts, contract, input, head)
+ if opts.FeeTokenID != 0 ||
+ opts.Version != nil ||
+ (opts.Reference != nil && *opts.Reference != (common.Reference{})) ||
+ (opts.Memo != nil && len(*opts.Memo) > 0) {
+ rawTx, err = c.createMorphTx(opts, contract, input, head)
} else {
rawTx, err = c.createDynamicTx(opts, contract, input, head)
}
diff --git a/accounts/abi/bind/morph_tx_version_test.go b/accounts/abi/bind/morph_tx_version_test.go
new file mode 100644
index 000000000..73c73428a
--- /dev/null
+++ b/accounts/abi/bind/morph_tx_version_test.go
@@ -0,0 +1,183 @@
+package bind
+
+import (
+ "math/big"
+ "testing"
+
+ "github.com/morph-l2/go-ethereum/common"
+ "github.com/morph-l2/go-ethereum/core/types"
+)
+
+func versionPtr(v uint8) *uint8 {
+ return &v
+}
+
+func refTestPtr(r common.Reference) *common.Reference {
+ return &r
+}
+
+func memoTestPtr(b []byte) *[]byte {
+ return &b
+}
+
+// TestMorphTxVersion_HeuristicDefault tests the heuristic version defaulting logic
+// in morphTxVersion():
+// - Version == nil + no V1 fields → V0
+// - Version == nil + Reference or Memo → V1
+// - Explicit Version → use as-is (with validation)
+func TestMorphTxVersion_HeuristicDefault(t *testing.T) {
+ ref := common.HexToReference("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
+ emptyRef := common.Reference{}
+ memo := []byte("test memo")
+ emptyMemo := []byte{}
+
+ bc := &BoundContract{} // morphTxVersion doesn't use BoundContract fields
+
+ tests := []struct {
+ name string
+ opts *TransactOpts
+ wantVersion uint8
+ wantErr error
+ }{
+ // === Heuristic defaults (Version == nil) ===
+ {
+ name: "nil Version, FeeTokenID > 0, no V1 fields → V0",
+ opts: &TransactOpts{FeeTokenID: 1},
+ wantVersion: types.MorphTxVersion0,
+ },
+ {
+ name: "nil Version, FeeTokenID > 0, with Reference → V1",
+ opts: &TransactOpts{FeeTokenID: 1, Reference: refTestPtr(ref)},
+ wantVersion: types.MorphTxVersion1,
+ },
+ {
+ name: "nil Version, FeeTokenID > 0, with Memo → V1",
+ opts: &TransactOpts{FeeTokenID: 1, Memo: memoTestPtr(memo)},
+ wantVersion: types.MorphTxVersion1,
+ },
+ {
+ name: "nil Version, FeeTokenID > 0, with Reference + Memo → V1",
+ opts: &TransactOpts{FeeTokenID: 1, Reference: refTestPtr(ref), Memo: memoTestPtr(memo)},
+ wantVersion: types.MorphTxVersion1,
+ },
+ {
+ name: "nil Version, FeeTokenID = 0, with Reference → V1",
+ opts: &TransactOpts{FeeTokenID: 0, Reference: refTestPtr(ref)},
+ wantVersion: types.MorphTxVersion1,
+ },
+ {
+ name: "nil Version, FeeTokenID = 0, with Memo → V1",
+ opts: &TransactOpts{FeeTokenID: 0, Memo: memoTestPtr(memo)},
+ wantVersion: types.MorphTxVersion1,
+ },
+ {
+ name: "nil Version, FeeTokenID = 0, no V1 fields → V0",
+ opts: &TransactOpts{FeeTokenID: 0},
+ wantVersion: types.MorphTxVersion0,
+ },
+ {
+ name: "nil Version, empty Reference → V0",
+ opts: &TransactOpts{FeeTokenID: 1, Reference: refTestPtr(emptyRef)},
+ wantVersion: types.MorphTxVersion0,
+ },
+ {
+ name: "nil Version, empty Memo → V0",
+ opts: &TransactOpts{FeeTokenID: 1, Memo: memoTestPtr(emptyMemo)},
+ wantVersion: types.MorphTxVersion0,
+ },
+ {
+ name: "nil Version, nil Reference, nil Memo → V0",
+ opts: &TransactOpts{FeeTokenID: 1, Reference: nil, Memo: nil},
+ wantVersion: types.MorphTxVersion0,
+ },
+
+ // === V1 heuristic with illegal params ===
+ {
+ name: "nil Version, Reference + FeeTokenID=0 + FeeLimit > 0 → error",
+ opts: &TransactOpts{FeeTokenID: 0, FeeLimit: big.NewInt(100), Reference: refTestPtr(ref)},
+ wantErr: types.ErrMorphTxV1IllegalExtraParams,
+ },
+ {
+ name: "nil Version, Reference + FeeTokenID=0 + FeeLimit=0 → V1 (ok)",
+ opts: &TransactOpts{FeeTokenID: 0, FeeLimit: big.NewInt(0), Reference: refTestPtr(ref)},
+ wantVersion: types.MorphTxVersion1,
+ },
+ {
+ name: "nil Version, Reference + FeeTokenID=0 + nil FeeLimit → V1 (ok)",
+ opts: &TransactOpts{FeeTokenID: 0, FeeLimit: nil, Reference: refTestPtr(ref)},
+ wantVersion: types.MorphTxVersion1,
+ },
+
+ // === Explicit Version ===
+ {
+ name: "explicit V0, FeeTokenID > 0 → V0",
+ opts: &TransactOpts{Version: versionPtr(types.MorphTxVersion0), FeeTokenID: 1},
+ wantVersion: types.MorphTxVersion0,
+ },
+ {
+ name: "explicit V1, no special fields → V1",
+ opts: &TransactOpts{Version: versionPtr(types.MorphTxVersion1)},
+ wantVersion: types.MorphTxVersion1,
+ },
+ {
+ name: "explicit V1, with Reference + Memo → V1",
+ opts: &TransactOpts{Version: versionPtr(types.MorphTxVersion1), Reference: refTestPtr(ref), Memo: memoTestPtr(memo)},
+ wantVersion: types.MorphTxVersion1,
+ },
+ {
+ name: "explicit V0, FeeTokenID = 0 → error (V0 requires FeeTokenID > 0)",
+ opts: &TransactOpts{Version: versionPtr(types.MorphTxVersion0), FeeTokenID: 0},
+ wantErr: types.ErrMorphTxV0IllegalExtraParams,
+ },
+ {
+ name: "explicit V0, with Reference → error",
+ opts: &TransactOpts{Version: versionPtr(types.MorphTxVersion0), FeeTokenID: 1, Reference: refTestPtr(ref)},
+ wantErr: types.ErrMorphTxV0IllegalExtraParams,
+ },
+ {
+ name: "explicit V0, with Memo → error",
+ opts: &TransactOpts{Version: versionPtr(types.MorphTxVersion0), FeeTokenID: 1, Memo: memoTestPtr(memo)},
+ wantErr: types.ErrMorphTxV0IllegalExtraParams,
+ },
+ {
+ name: "explicit V1, FeeTokenID=0 + FeeLimit>0 → error",
+ opts: &TransactOpts{Version: versionPtr(types.MorphTxVersion1), FeeTokenID: 0, FeeLimit: big.NewInt(100)},
+ wantErr: types.ErrMorphTxV1IllegalExtraParams,
+ },
+ {
+ name: "unsupported version 255 → error",
+ opts: &TransactOpts{Version: versionPtr(255)},
+ wantErr: types.ErrMorphTxUnsupportedVersion,
+ },
+
+ // === Memo length validation ===
+ {
+ name: "memo too long → error",
+ opts: &TransactOpts{FeeTokenID: 1, Memo: memoTestPtr(make([]byte, common.MaxMemoLength+1))},
+ wantErr: types.ErrMemoTooLong,
+ },
+ {
+ name: "memo at max length → ok",
+ opts: &TransactOpts{FeeTokenID: 1, Memo: memoTestPtr(make([]byte, common.MaxMemoLength))},
+ wantVersion: types.MorphTxVersion1, // non-empty memo → V1
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ version, err := bc.morphTxVersion(tt.opts)
+ if tt.wantErr != nil {
+ if err != tt.wantErr {
+ t.Errorf("error: got %v, want %v", err, tt.wantErr)
+ }
+ return
+ }
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if version != tt.wantVersion {
+ t.Errorf("version: got %d, want %d", version, tt.wantVersion)
+ }
+ })
+ }
+}
diff --git a/accounts/external/backend.go b/accounts/external/backend.go
index ac620cb5f..fbcc65bbd 100644
--- a/accounts/external/backend.go
+++ b/accounts/external/backend.go
@@ -221,12 +221,19 @@ func (api *ExternalSigner) SignTx(account accounts.Account, tx *types.Transactio
case types.DynamicFeeTxType, types.SetCodeTxType:
args.MaxFeePerGas = (*hexutil.Big)(tx.GasFeeCap())
args.MaxPriorityFeePerGas = (*hexutil.Big)(tx.GasTipCap())
- case types.AltFeeTxType:
+ case types.MorphTxType:
args.MaxFeePerGas = (*hexutil.Big)(tx.GasFeeCap())
args.MaxPriorityFeePerGas = (*hexutil.Big)(tx.GasTipCap())
feeTokenID := hexutil.Uint16(tx.FeeTokenID())
args.FeeTokenID = &feeTokenID
args.FeeLimit = (*hexutil.Big)(tx.FeeLimit())
+ version := hexutil.Uint64(tx.Version())
+ args.Version = &version
+ args.Reference = tx.Reference()
+ if tx.Memo() != nil {
+ memo := hexutil.Bytes(*tx.Memo())
+ args.Memo = &memo
+ }
default:
return nil, fmt.Errorf("unsupported tx type %d", tx.Type())
}
diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go
index 5e6160287..2a26da15f 100644
--- a/cmd/evm/internal/t8ntool/execution.go
+++ b/cmd/evm/internal/t8ntool/execution.go
@@ -207,12 +207,24 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
receipt.TxHash = tx.Hash()
receipt.GasUsed = msgResult.UsedGas
receipt.L1Fee = msgResult.L1DataFee
- if msg.FeeTokenID() != 0 {
- tokenID := msg.FeeTokenID()
- receipt.FeeTokenID = &tokenID
- receipt.FeeLimit = msg.FeeLimit()
- receipt.FeeRate = msgResult.FeeRate
- receipt.TokenScale = msgResult.TokenScale
+
+ if msg.FeeTokenID() != 0 ||
+ msg.Version() != 0 ||
+ (msg.Reference() != nil && *msg.Reference() != (common.Reference{})) ||
+ (msg.Memo() != nil && len(*msg.Memo()) > 0) {
+ if msg.FeeTokenID() != 0 {
+ tokenID := msg.FeeTokenID()
+ receipt.FeeTokenID = &tokenID
+ receipt.FeeLimit = msg.FeeLimit()
+ receipt.FeeRate = msgResult.FeeRate
+ receipt.TokenScale = msgResult.TokenScale
+ }
+ // Only include V1 fields (version, reference, memo) for V1+ transactions
+ if msg.Version() >= types.MorphTxVersion1 {
+ receipt.Version = msg.Version()
+ receipt.Reference = msg.Reference()
+ receipt.Memo = msg.Memo()
+ }
}
// If the transaction created a contract, store the creation address in the receipt.
diff --git a/cmd/geth/chaincmd.go b/cmd/geth/chaincmd.go
index 8f3457c0e..6fb52aaa5 100644
--- a/cmd/geth/chaincmd.go
+++ b/cmd/geth/chaincmd.go
@@ -53,7 +53,7 @@ var (
utils.OverrideMorph203TimeFlag,
utils.OverrideViridianTimeFlag,
utils.OverrideEmeraldTimeFlag,
- utils.OverrideMPTForkTimeFlag,
+ utils.OverrideJadeForkTimeFlag,
},
Category: "BLOCKCHAIN COMMANDS",
Description: `
@@ -226,9 +226,9 @@ func initGenesis(ctx *cli.Context) error {
v := ctx.Uint64(utils.OverrideEmeraldTimeFlag.Name)
overrides.EmeraldTime = &v
}
- if ctx.IsSet(utils.OverrideMPTForkTimeFlag.Name) {
- v := ctx.Uint64(utils.OverrideMPTForkTimeFlag.Name)
- overrides.MPTForkTime = &v
+ if ctx.IsSet(utils.OverrideJadeForkTimeFlag.Name) {
+ v := ctx.Uint64(utils.OverrideJadeForkTimeFlag.Name)
+ overrides.JadeForkTime = &v
}
for _, name := range []string{"chaindata", "lightchaindata"} {
diff --git a/cmd/geth/config.go b/cmd/geth/config.go
index cfc8961f1..9851e217e 100644
--- a/cmd/geth/config.go
+++ b/cmd/geth/config.go
@@ -170,9 +170,9 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) {
v := ctx.Uint64(utils.OverrideEmeraldTimeFlag.Name)
cfg.Eth.OverrideEmeraldTime = &v
}
- if ctx.GlobalIsSet(utils.OverrideMPTForkTimeFlag.Name) {
- v := ctx.Uint64(utils.OverrideMPTForkTimeFlag.Name)
- cfg.Eth.OverrideMPTForkTime = &v
+ if ctx.GlobalIsSet(utils.OverrideJadeForkTimeFlag.Name) {
+ v := ctx.Uint64(utils.OverrideJadeForkTimeFlag.Name)
+ cfg.Eth.OverrideJadeForkTime = &v
}
backend, _ := utils.RegisterEthService(stack, &cfg.Eth)
diff --git a/cmd/geth/main.go b/cmd/geth/main.go
index 22f935c4b..8d4e6228d 100644
--- a/cmd/geth/main.go
+++ b/cmd/geth/main.go
@@ -74,7 +74,7 @@ var (
utils.OverrideMorph203TimeFlag,
utils.OverrideViridianTimeFlag,
utils.OverrideEmeraldTimeFlag,
- utils.OverrideMPTForkTimeFlag,
+ utils.OverrideJadeForkTimeFlag,
utils.EthashCacheDirFlag,
utils.EthashCachesInMemoryFlag,
utils.EthashCachesOnDiskFlag,
diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go
index 164df3602..297e672f9 100644
--- a/cmd/utils/flags.go
+++ b/cmd/utils/flags.go
@@ -287,9 +287,9 @@ var (
Name: "override.emeraldtime",
Usage: "Manually specify the Emerald fork timestamp, overriding the bundled setting",
}
- OverrideMPTForkTimeFlag = &cli.Uint64Flag{
- Name: "override.mptforktime",
- Usage: "Manually specify the MPT fork timestamp, overriding the bundled setting",
+ OverrideJadeForkTimeFlag = &cli.Uint64Flag{
+ Name: "override.jadeforktime",
+ Usage: "Manually specify the Jade fork timestamp, overriding the bundled setting",
}
// Light server and client settings
LightServeFlag = cli.IntFlag{
diff --git a/common/types.go b/common/types.go
index d438a1649..a70f2162b 100644
--- a/common/types.go
+++ b/common/types.go
@@ -39,13 +39,191 @@ const (
HashLength = 32
// AddressLength is the expected length of the address
AddressLength = 20
+ // ReferenceLength is the expected length of the reference
+ ReferenceLength = 32
+ // MaxMemoLength is the maximum length of the memo in MorphTx
+ MaxMemoLength = 64
)
var (
- hashT = reflect.TypeOf(Hash{})
- addressT = reflect.TypeOf(Address{})
+ hashT = reflect.TypeOf(Hash{})
+ addressT = reflect.TypeOf(Address{})
+ referenceT = reflect.TypeOf(Reference{})
)
+// Reference represents the 32 byte reference of a transaction.
+type Reference [ReferenceLength]byte
+
+// BytesToReference sets b to reference.
+// If b is larger than len(r), b will be cropped from the left.
+func BytesToReference(b []byte) Reference {
+ var r Reference
+ r.SetBytes(b)
+ return r
+}
+
+// BigToReference sets byte representation of b to reference.
+// If b is larger than len(r), b will be cropped from the left.
+func BigToReference(b *big.Int) Reference { return BytesToReference(b.Bytes()) }
+
+// HexToReference sets byte representation of s to reference.
+// If b is larger than len(r), b will be cropped from the left.
+func HexToReference(s string) Reference { return BytesToReference(FromHex(s)) }
+
+// Bytes gets the byte representation of the underlying reference.
+func (r Reference) Bytes() []byte { return r[:] }
+
+// Big converts a reference to a big integer.
+func (r Reference) Big() *big.Int { return new(big.Int).SetBytes(r[:]) }
+
+// Hex converts a reference to a hex string.
+func (r Reference) Hex() string { return hexutil.Encode(r[:]) }
+
+// TerminalString implements log.TerminalStringer, formatting a string for console
+// output during logging.
+func (r Reference) TerminalString() string {
+ return fmt.Sprintf("%x..%x", r[:3], r[29:])
+}
+
+// String implements the stringer interface and is used also by the logger when
+// doing full logging into a file.
+func (r Reference) String() string {
+ return r.Hex()
+}
+
+// Format implements fmt.Formatter.
+// Reference supports the %v, %s, %q, %x, %X and %d format verbs.
+func (r Reference) Format(s fmt.State, c rune) {
+ hexb := make([]byte, 2+len(r)*2)
+ copy(hexb, "0x")
+ hex.Encode(hexb[2:], r[:])
+
+ switch c {
+ case 'x', 'X':
+ if !s.Flag('#') {
+ hexb = hexb[2:]
+ }
+ if c == 'X' {
+ hexb = bytes.ToUpper(hexb)
+ }
+ fallthrough
+ case 'v', 's':
+ s.Write(hexb)
+ case 'q':
+ q := []byte{'"'}
+ s.Write(q)
+ s.Write(hexb)
+ s.Write(q)
+ case 'd':
+ fmt.Fprint(s, ([len(r)]byte)(r))
+ default:
+ fmt.Fprintf(s, "%%!%c(reference=%x)", c, r)
+ }
+}
+
+// SetBytes sets the reference to the value of b.
+// If b is larger than len(r), b will be cropped from the left.
+// The input is right-aligned: leading bytes are zeroed and b is
+// copied into the trailing bytes of r.
+func (r *Reference) SetBytes(b []byte) {
+ if len(b) > len(r) {
+ b = b[len(b)-ReferenceLength:]
+ }
+ *r = Reference{}
+ copy(r[len(r)-len(b):], b)
+}
+
+// UnmarshalText parses a reference in hex syntax.
+// Empty strings are treated as zero reference.
+func (r *Reference) UnmarshalText(input []byte) error {
+ // Handle empty string case
+ if len(input) == 0 || string(input) == "0x" {
+ *r = Reference{}
+ return nil
+ }
+ return hexutil.UnmarshalFixedText("Reference", input, r[:])
+}
+
+// UnmarshalJSON parses a reference in hex syntax.
+// Empty strings are treated as zero reference.
+func (r *Reference) UnmarshalJSON(input []byte) error {
+ // Handle empty string case: "" or "0x"
+ if len(input) == 2 && input[0] == '"' && input[1] == '"' {
+ *r = Reference{}
+ return nil
+ }
+ if len(input) == 4 && string(input) == `"0x"` {
+ *r = Reference{}
+ return nil
+ }
+ return hexutil.UnmarshalFixedJSON(referenceT, input, r[:])
+}
+
+// MarshalText returns the hex representation of r.
+func (r Reference) MarshalText() ([]byte, error) {
+ return hexutil.Bytes(r[:]).MarshalText()
+}
+
+// MarshalJSON marshals the original value
+func (r Reference) MarshalJSON() ([]byte, error) {
+ return json.Marshal(r.Hex())
+}
+
+// Generate implements testing/quick.Generator.
+func (r Reference) Generate(rand *rand.Rand, size int) reflect.Value {
+ m := rand.Intn(len(r))
+ for i := len(r) - 1; i > m; i-- {
+ r[i] = byte(rand.Uint32())
+ }
+ return reflect.ValueOf(r)
+}
+
+// Scan implements Scanner for database/sql.
+func (r *Reference) Scan(src interface{}) error {
+ srcB, ok := src.([]byte)
+ if !ok {
+ return fmt.Errorf("can't scan %T into Reference", src)
+ }
+ if len(srcB) != ReferenceLength {
+ return fmt.Errorf("can't scan []byte of len %d into Reference, want %d", len(srcB), ReferenceLength)
+ }
+ copy(r[:], srcB)
+ return nil
+}
+
+// Value implements valuer for database/sql.
+func (r Reference) Value() (driver.Value, error) {
+ return r[:], nil
+}
+
+// ImplementsGraphQLType returns true if Reference implements the specified GraphQL type.
+func (Reference) ImplementsGraphQLType(name string) bool { return name == "Bytes32" }
+
+// UnmarshalGraphQL unmarshals the provided GraphQL query data.
+func (r *Reference) UnmarshalGraphQL(input interface{}) error {
+ var err error
+ switch input := input.(type) {
+ case string:
+ err = r.UnmarshalText([]byte(input))
+ default:
+ err = fmt.Errorf("unexpected type %T for Reference", input)
+ }
+ return err
+}
+
+// UnprefixedReference allows marshaling a Reference without 0x prefix.
+type UnprefixedReference Reference
+
+// UnmarshalText decodes the reference from hex. The 0x prefix is optional.
+func (r *UnprefixedReference) UnmarshalText(input []byte) error {
+ return hexutil.UnmarshalFixedUnprefixedText("UnprefixedReference", input, r[:])
+}
+
+// MarshalText encodes the reference as hex.
+func (r UnprefixedReference) MarshalText() ([]byte, error) {
+ return []byte(hex.EncodeToString(r[:])), nil
+}
+
// Hash represents the 32 byte Keccak256 hash of arbitrary data.
type Hash [HashLength]byte
diff --git a/core/block_validator.go b/core/block_validator.go
index 811ba3106..bbda92745 100644
--- a/core/block_validator.go
+++ b/core/block_validator.go
@@ -59,6 +59,18 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error {
if !v.config.Morph.IsValidBlockSize(block.PayloadSize()) {
return ErrInvalidBlockPayloadSize
}
+ // Validate MorphTx for all transactions
+ isJadeFork := v.config.IsJadeFork(block.Time())
+ for _, tx := range block.Transactions() {
+ // Reject MorphTx V1 before jade fork is active
+ if !isJadeFork && tx.IsMorphTx() && tx.Version() == types.MorphTxVersion1 {
+ return types.ErrMorphTxV1NotYetActive
+ }
+ // Validate version, memo, and associated field requirements
+ if err := tx.ValidateMorphTxVersion(); err != nil {
+ return err
+ }
+ }
// Header validity is known at this point, check the uncles and transactions
header := block.Header()
if err := v.engine.VerifyUncles(v.bc, block); err != nil {
@@ -102,9 +114,9 @@ func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateD
// Validate the state root against the received state root and throw
// an error if they don't match.
//
- // XOR condition: Only validate state root when (UseZktrie XOR IsMPTFork) == true
+ // XOR condition: Only validate state root when (UseZktrie XOR IsJadeFork) == true
// This allows cross-format blocks to pass validation without matching local state root.
- shouldValidateStateRoot := v.config.Morph.UseZktrie != v.config.IsMPTFork(header.Time)
+ shouldValidateStateRoot := v.config.Morph.UseZktrie != v.config.IsJadeFork(header.Time)
if root := statedb.IntermediateRoot(v.config.IsEIP158(header.Number)); shouldValidateStateRoot && header.Root != root {
return fmt.Errorf("invalid merkle root (remote: %x local: %x)", header.Root, root)
diff --git a/core/block_validator_test.go b/core/block_validator_test.go
index 5969eb9a6..57f5b4d33 100644
--- a/core/block_validator_test.go
+++ b/core/block_validator_test.go
@@ -232,18 +232,18 @@ func TestCalcGasLimit(t *testing.T) {
func TestStateRootValidationXOR(t *testing.T) {
tests := []struct {
- name string
- useZktrie bool
- blockTime uint64
- mptForkTime *uint64
- expectValidation bool
- description string
+ name string
+ useZktrie bool
+ blockTime uint64
+ jadeForkTime *uint64
+ expectValidation bool
+ description string
}{
{
name: "zkTrie node, before fork",
useZktrie: true,
blockTime: 500,
- mptForkTime: uint64Ptr(1000),
+ jadeForkTime: uint64Ptr(1000),
expectValidation: true,
description: "zkTrie node processing zkTrie block (same format) - should validate",
},
@@ -251,7 +251,7 @@ func TestStateRootValidationXOR(t *testing.T) {
name: "zkTrie node, after fork",
useZktrie: true,
blockTime: 1500,
- mptForkTime: uint64Ptr(1000),
+ jadeForkTime: uint64Ptr(1000),
expectValidation: false,
description: "zkTrie node processing MPT block (cross-format) - should skip validation",
},
@@ -259,7 +259,7 @@ func TestStateRootValidationXOR(t *testing.T) {
name: "MPT node, before fork",
useZktrie: false,
blockTime: 500,
- mptForkTime: uint64Ptr(1000),
+ jadeForkTime: uint64Ptr(1000),
expectValidation: false,
description: "MPT node processing zkTrie block (cross-format) - should skip validation",
},
@@ -267,7 +267,7 @@ func TestStateRootValidationXOR(t *testing.T) {
name: "MPT node, after fork",
useZktrie: false,
blockTime: 1500,
- mptForkTime: uint64Ptr(1000),
+ jadeForkTime: uint64Ptr(1000),
expectValidation: true,
description: "MPT node processing MPT block (same format) - should validate",
},
@@ -275,7 +275,7 @@ func TestStateRootValidationXOR(t *testing.T) {
name: "No fork configured, zkTrie node",
useZktrie: true,
blockTime: 1500,
- mptForkTime: nil,
+ jadeForkTime: nil,
expectValidation: true,
description: "No MPT fork, zkTrie node always validates",
},
@@ -283,7 +283,7 @@ func TestStateRootValidationXOR(t *testing.T) {
name: "No fork configured, MPT node",
useZktrie: false,
blockTime: 1500,
- mptForkTime: nil,
+ jadeForkTime: nil,
expectValidation: false,
description: "No MPT fork, MPT node never validates (backwards compat)",
},
@@ -295,27 +295,31 @@ func TestStateRootValidationXOR(t *testing.T) {
Morph: params.MorphConfig{
UseZktrie: tt.useZktrie,
},
- MPTForkTime: tt.mptForkTime,
+ JadeForkTime: tt.jadeForkTime,
}
// Calculate XOR condition
- isMPTFork := config.IsMPTFork(tt.blockTime)
- shouldValidate := tt.useZktrie != isMPTFork
+ isJadeFork := config.IsJadeFork(tt.blockTime)
+ shouldValidate := tt.useZktrie != isJadeFork
if shouldValidate != tt.expectValidation {
- t.Errorf("%s\n Expected validation: %v, got: %v\n UseZktrie=%v, IsMPTFork=%v, XOR=%v",
+ t.Errorf(
+ "%s\n Expected validation: %v, got: %v\n UseZktrie=%v, IsJadeFork=%v, XOR=%v",
tt.description,
tt.expectValidation,
shouldValidate,
tt.useZktrie,
- isMPTFork,
- shouldValidate)
+ isJadeFork,
+ shouldValidate,
+ )
} else {
- t.Logf("✓ %s: validation=%v (UseZktrie=%v XOR IsMPTFork=%v)",
+ t.Logf(
+ "✓ %s: validation=%v (UseZktrie=%v XOR IsJadeFork=%v)",
tt.description,
shouldValidate,
tt.useZktrie,
- isMPTFork)
+ isJadeFork,
+ )
}
})
}
diff --git a/core/blockchain.go b/core/blockchain.go
index 7a1273e27..47f8c5202 100644
--- a/core/blockchain.go
+++ b/core/blockchain.go
@@ -403,12 +403,16 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *par
bc.wg.Add(1)
go bc.futureBlocksLoop()
- // Start tx indexer/unindexer.
if txLookupLimit != nil {
bc.txLookupLimit = *txLookupLimit
+ // Start tx indexer/unindexer.
bc.wg.Add(1)
go bc.maintainTxIndex(txIndexBlock)
+
+ // Start reference indexer/unindexer (using same lookup limit as tx index).
+ bc.wg.Add(1)
+ go bc.maintainReferenceIndex(txIndexBlock)
}
// If periodic cache journal is required, spin it up.
@@ -619,6 +623,13 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, root common.Hash, repair bo
}
// Rewind the header chain, deleting all block bodies until then
delFn := func(db ethdb.KeyValueWriter, hash common.Hash, num uint64) {
+ // Clean up reference index entries BEFORE deleting the block body,
+ // because DeleteReferenceIndexEntriesForBlock needs to read transactions
+ // from the block. Once the body is deleted, these entries become
+ // permanently stale.
+ if block := bc.GetBlock(hash, num); block != nil {
+ rawdb.DeleteReferenceIndexEntriesForBlock(db, block)
+ }
// Ignore the error here since light client won't hit this path
frozen, _ := bc.db.Ancients()
if num+1 <= frozen {
@@ -771,10 +782,27 @@ func (bc *BlockChain) ExportN(w io.Writer, first uint64, last uint64) error {
func (bc *BlockChain) writeHeadBlock(block *types.Block) {
// Add the block to the canonical chain number scheme and mark as the head
batch := bc.db.NewBatch()
+
+ // If the canonical block at this height is being replaced by a different block
+ // (implicit reorg / L2 sequential overwrite), clean up the old block's reference
+ // index entries to avoid stale keys. This is necessary because reference index
+ // keys embed (blockTimestamp, txIndex), so the same tx hash produces different
+ // keys in different blocks, and simply overwriting won't remove the old key.
+ oldHash := rawdb.ReadCanonicalHash(bc.db, block.NumberU64())
+ if oldHash != (common.Hash{}) && oldHash != block.Hash() {
+ if oldBlock := bc.GetBlock(oldHash, block.NumberU64()); oldBlock != nil {
+ rawdb.DeleteReferenceIndexEntriesForBlock(batch, oldBlock)
+ } else {
+ log.Warn("Cannot clean reference index entries: old block unavailable",
+ "number", block.NumberU64(), "oldHash", oldHash)
+ }
+ }
+
rawdb.WriteHeadHeaderHash(batch, block.Hash())
rawdb.WriteHeadFastBlockHash(batch, block.Hash())
rawdb.WriteCanonicalHash(batch, block.Hash(), block.NumberU64())
rawdb.WriteTxLookupEntriesByBlock(batch, block)
+ rawdb.WriteReferenceIndexEntriesForBlock(batch, block)
rawdb.WriteHeadBlockHash(batch, block.Hash())
// Flush the whole batch into the disk, exit the node if failed
@@ -1017,13 +1045,15 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [
for _, block := range blockChain {
if bc.txLookupLimit == 0 || ancientLimit <= bc.txLookupLimit || block.NumberU64() >= ancientLimit-bc.txLookupLimit {
rawdb.WriteTxLookupEntriesByBlock(batch, block)
+ rawdb.WriteReferenceIndexEntriesForBlock(batch, block)
} else if rawdb.ReadTxIndexTail(bc.db) != nil {
rawdb.WriteTxLookupEntriesByBlock(batch, block)
+ rawdb.WriteReferenceIndexEntriesForBlock(batch, block)
}
stats.processed++
}
- // Flush all tx-lookup index data.
+ // Flush all tx-lookup and reference index data.
size += int64(batch.ValueSize())
if err := batch.Write(); err != nil {
// The tx index data could not be written.
@@ -1102,11 +1132,12 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [
// Write all the data out into the database
rawdb.WriteBody(batch, block.Hash(), block.NumberU64(), block.Body())
rawdb.WriteReceipts(batch, block.Hash(), block.NumberU64(), receiptChain[i])
- rawdb.WriteTxLookupEntriesByBlock(batch, block) // Always write tx indices for live blocks, we assume they are needed
+ rawdb.WriteTxLookupEntriesByBlock(batch, block) // Always write tx indices for live blocks, we assume they are needed
+ rawdb.WriteReferenceIndexEntriesForBlock(batch, block) // Always write reference indices for live blocks
// Write everything belongs to the blocks into the database. So that
// we can ensure all components of body is completed(body, receipts,
- // tx indexes)
+ // tx indexes, reference indexes)
if batch.ValueSize() >= ethdb.IdealBatchSize {
if err := batch.Write(); err != nil {
return 0, err
@@ -1977,9 +2008,20 @@ func (bc *BlockChain) reorg(oldBlock, newBlock *types.Block) error {
// Delete useless indexes right now which includes the non-canonical
// transaction indexes, canonical chain indexes which above the head.
indexesBatch := bc.db.NewBatch()
- for _, tx := range types.TxDifference(deletedTxs, addedTxs) {
+ trulyDeletedTxs := types.TxDifference(deletedTxs, addedTxs)
+ for _, tx := range trulyDeletedTxs {
rawdb.DeleteTxLookupEntry(indexesBatch, tx.Hash())
}
+ // Reorg-safe reference index maintenance:
+ // 1) remove all reference index entries from the old canonical chain segment
+ // 2) reinsert entries for the newly canonical segment
+ // This avoids stale keys when the same tx hash moves to a different location.
+ for _, block := range oldChain {
+ rawdb.DeleteReferenceIndexEntriesForBlock(indexesBatch, block)
+ }
+ for i := len(newChain) - 1; i >= 1; i-- {
+ rawdb.WriteReferenceIndexEntriesForBlock(indexesBatch, newChain[i])
+ }
// Delete any canonical number assignments above the new head
number := bc.CurrentBlock().NumberU64()
for i := number + 1; ; i++ {
@@ -2155,6 +2197,94 @@ func (bc *BlockChain) maintainTxIndex(ancients uint64) {
}
}
+// maintainReferenceIndex is responsible for the construction and deletion of the
+// reference index for MorphTx transactions.
+//
+// User can use flag `txlookuplimit` to specify a "recentness" block, below
+// which ancient reference indices get deleted. If `txlookuplimit` is 0, it means
+// all reference indices will be reserved.
+//
+// The user can adjust the txlookuplimit value for each launch after fast
+// sync, Geth will automatically construct the missing indices and delete
+// the extra indices.
+func (bc *BlockChain) maintainReferenceIndex(ancients uint64) {
+ defer bc.wg.Done()
+
+ // Before starting the actual maintenance, we need to handle a special case,
+ // where user might init Geth with an external ancient database. If so, we
+ // need to reindex all necessary references before starting to process any
+ // pruning requests.
+ if ancients > 0 {
+ var from = uint64(0)
+ if bc.txLookupLimit != 0 && ancients > bc.txLookupLimit {
+ from = ancients - bc.txLookupLimit
+ }
+ rawdb.IndexReferences(bc.db, from, ancients, bc.quit)
+ }
+
+ // indexBlocks reindexes or unindexes references depending on user configuration
+ indexBlocks := func(tail *uint64, head uint64, done chan struct{}) {
+ defer func() { done <- struct{}{} }()
+
+ // If the user just upgraded Geth to a new version which supports reference
+ // index pruning, write the new tail and remove anything older.
+ if tail == nil {
+ if bc.txLookupLimit == 0 || head < bc.txLookupLimit {
+ // Nothing to delete, write the tail and return
+ rawdb.WriteReferenceIndexTail(bc.db, 0)
+ } else {
+ // Prune all stale reference indices and record the reference index tail
+ rawdb.UnindexReferences(bc.db, 0, head-bc.txLookupLimit+1, bc.quit)
+ }
+ return
+ }
+ // If a previous indexing existed, make sure that we fill in any missing entries
+ if bc.txLookupLimit == 0 || head < bc.txLookupLimit {
+ if *tail > 0 {
+ rawdb.IndexReferences(bc.db, 0, *tail, bc.quit)
+ }
+ return
+ }
+ // Update the reference index to the new chain state
+ if head-bc.txLookupLimit+1 < *tail {
+ // Reindex a part of missing indices and rewind index tail to HEAD-limit
+ rawdb.IndexReferences(bc.db, head-bc.txLookupLimit+1, *tail, bc.quit)
+ } else {
+ // Unindex a part of stale indices and forward index tail to HEAD-limit
+ rawdb.UnindexReferences(bc.db, *tail, head-bc.txLookupLimit+1, bc.quit)
+ }
+ }
+
+ // Any reindexing done, start listening to chain events and moving the index window
+ var (
+ done chan struct{} // Non-nil if background unindexing or reindexing routine is active.
+ headCh = make(chan ChainHeadEvent, 1) // Buffered to avoid locking up the event feed
+ )
+ sub := bc.SubscribeChainHeadEvent(headCh)
+ if sub == nil {
+ return
+ }
+ defer sub.Unsubscribe()
+
+ for {
+ select {
+ case head := <-headCh:
+ if done == nil {
+ done = make(chan struct{})
+ go indexBlocks(rawdb.ReadReferenceIndexTail(bc.db), head.Block.NumberU64(), done)
+ }
+ case <-done:
+ done = nil
+ case <-bc.quit:
+ if done != nil {
+ log.Info("Waiting background reference indexer to exit")
+ <-done
+ }
+ return
+ }
+ }
+}
+
// reportBlock logs a bad block error.
func (bc *BlockChain) reportBlock(block *types.Block, receipts types.Receipts, err error) {
rawdb.WriteBadBlock(bc.db, block)
diff --git a/core/blockchain_test.go b/core/blockchain_test.go
index 6ac7d57d9..48b4bccfa 100644
--- a/core/blockchain_test.go
+++ b/core/blockchain_test.go
@@ -3587,3 +3587,593 @@ func TestCurieTransition(t *testing.T) {
}
}
}
+
+// TestReferenceIndices tests that reference indices are correctly created
+// for MorphTx transactions with Reference field when blocks are inserted.
+func TestReferenceIndices(t *testing.T) {
+ // Configure and generate a sample block chain
+ var (
+ gendb = rawdb.NewMemoryDatabase()
+ key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
+ address = crypto.PubkeyToAddress(key.PublicKey)
+ funds = big.NewInt(100000000000000000)
+ gspec = &Genesis{
+ Config: params.TestChainConfig,
+ Alloc: GenesisAlloc{address: {Balance: funds}},
+ BaseFee: big.NewInt(params.InitialBaseFee),
+ }
+ genesis = gspec.MustCommit(gendb)
+ signer = types.LatestSigner(gspec.Config)
+ )
+
+ // Create references for testing
+ ref1 := common.BytesToReference([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x01})
+ ref2 := common.BytesToReference([]byte{0x02, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x02})
+ ref3 := common.BytesToReference([]byte{0x03, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x03}) // shared reference
+
+ var morphTxHashes []common.Hash
+ var morphTxReferences []common.Reference
+
+ height := uint64(20)
+ blocks, receipts := GenerateChain(gspec.Config, genesis, ethash.NewFaker(), gendb, int(height), func(i int, block *BlockGen) {
+ nonce := block.TxNonce(address)
+
+ // Add a regular transaction (no Reference)
+ regularTx, err := types.SignTx(types.NewTransaction(nonce, common.Address{0x00}, big.NewInt(1000), params.TxGas, block.header.BaseFee, nil), signer, key)
+ if err != nil {
+ panic(err)
+ }
+ block.AddTx(regularTx)
+
+ // Add MorphTx with different References
+ var ref common.Reference
+ if i%3 == 0 {
+ ref = ref3 // shared reference
+ } else if i%2 == 0 {
+ ref = ref2
+ } else {
+ ref = ref1
+ }
+
+ morphTx := types.NewTx(&types.MorphTx{
+ ChainID: gspec.Config.ChainID,
+ Nonce: nonce + 1,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: block.header.BaseFee,
+ Gas: params.TxGas,
+ To: &common.Address{0x01},
+ Value: big.NewInt(100),
+ Data: nil,
+ AccessList: nil,
+ Version: types.MorphTxVersion1,
+ FeeTokenID: 0,
+ FeeLimit: nil,
+ Reference: &ref,
+ Memo: nil,
+ })
+ signedMorphTx, err := types.SignTx(morphTx, signer, key)
+ if err != nil {
+ panic(err)
+ }
+ block.AddTx(signedMorphTx)
+
+ morphTxHashes = append(morphTxHashes, signedMorphTx.Hash())
+ morphTxReferences = append(morphTxReferences, ref)
+ })
+
+ frdir, err := ioutil.TempDir("", "")
+ if err != nil {
+ t.Fatalf("failed to create temp freezer dir: %v", err)
+ }
+ defer os.Remove(frdir)
+ ancientDb, err := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), frdir, "", false)
+ if err != nil {
+ t.Fatalf("failed to create temp freezer db: %v", err)
+ }
+ gspec.MustCommit(ancientDb)
+
+ // Create blockchain with txLookupLimit = 0 (no limit, index all)
+ l := uint64(0)
+ chain, err := NewBlockChain(ancientDb, nil, params.TestChainConfig, ethash.NewFaker(), vm.Config{}, nil, &l)
+ if err != nil {
+ t.Fatalf("failed to create tester chain: %v", err)
+ }
+
+ // Insert headers and receipts
+ headers := make([]*types.Header, len(blocks))
+ for i, block := range blocks {
+ headers[i] = block.Header()
+ }
+ if n, err := chain.InsertHeaderChain(headers, 0); err != nil {
+ t.Fatalf("failed to insert header %d: %v", n, err)
+ }
+ if n, err := chain.InsertReceiptChain(blocks, receipts, 0); err != nil {
+ t.Fatalf("block %d: failed to insert into chain: %v", n, err)
+ }
+
+ // Test 1: All MorphTx reference indices should be created
+ for i, txHash := range morphTxHashes {
+ ref := morphTxReferences[i]
+ entries := rawdb.ReadReferenceIndexEntries(chain.db, ref)
+ found := false
+ for _, entry := range entries {
+ if entry.TxHash == txHash {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Fatalf("Missing reference index for txHash %s with reference %s", txHash.Hex(), ref.Hex())
+ }
+ }
+
+ // Test 2: Query by reference returns correct results
+ entries1 := rawdb.ReadReferenceIndexEntries(chain.db, ref1)
+ entries2 := rawdb.ReadReferenceIndexEntries(chain.db, ref2)
+ entries3 := rawdb.ReadReferenceIndexEntries(chain.db, ref3)
+ t.Logf("ref1 entries: %d, ref2 entries: %d, ref3 entries: %d", len(entries1), len(entries2), len(entries3))
+
+ // Test 3: Entries are sorted by timestamp then txIndex
+ if len(entries3) >= 2 {
+ for i := 1; i < len(entries3); i++ {
+ prev := entries3[i-1]
+ curr := entries3[i]
+ if prev.BlockTimestamp > curr.BlockTimestamp {
+ t.Fatalf("Entries not sorted by timestamp: prev=%d, curr=%d", prev.BlockTimestamp, curr.BlockTimestamp)
+ }
+ if prev.BlockTimestamp == curr.BlockTimestamp && prev.TxIndex > curr.TxIndex {
+ t.Fatalf("Entries not sorted by txIndex: prev=%d, curr=%d", prev.TxIndex, curr.TxIndex)
+ }
+ }
+ }
+
+ chain.Stop()
+ ancientDb.Close()
+}
+
+// TestReferenceIndexBasicOperations tests the basic CRUD operations for reference indices.
+// This test directly tests the index functions without relying on background goroutines.
+func TestReferenceIndexBasicOperations(t *testing.T) {
+ db := rawdb.NewMemoryDatabase()
+
+ ref1 := common.BytesToReference([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x01})
+ ref2 := common.BytesToReference([]byte{0x02, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x02})
+
+ txHash1 := common.HexToHash("0x1111111111111111111111111111111111111111111111111111111111111111")
+ txHash2 := common.HexToHash("0x2222222222222222222222222222222222222222222222222222222222222222")
+ txHash3 := common.HexToHash("0x3333333333333333333333333333333333333333333333333333333333333333")
+
+ // Test 1: Write and read a single entry
+ rawdb.WriteReferenceIndexEntry(db, ref1, 100, 0, txHash1)
+ entries := rawdb.ReadReferenceIndexEntries(db, ref1)
+ if len(entries) != 1 {
+ t.Fatalf("Expected 1 entry, got %d", len(entries))
+ }
+ if entries[0].TxHash != txHash1 || entries[0].BlockTimestamp != 100 || entries[0].TxIndex != 0 {
+ t.Fatalf("Entry mismatch: got %+v", entries[0])
+ }
+
+ // Test 2: Multiple entries for the same reference (sorted by timestamp)
+ rawdb.WriteReferenceIndexEntry(db, ref1, 200, 0, txHash2)
+ rawdb.WriteReferenceIndexEntry(db, ref1, 150, 1, txHash3)
+ entries = rawdb.ReadReferenceIndexEntries(db, ref1)
+ if len(entries) != 3 {
+ t.Fatalf("Expected 3 entries, got %d", len(entries))
+ }
+ // Verify sorting by timestamp
+ if entries[0].BlockTimestamp != 100 || entries[1].BlockTimestamp != 150 || entries[2].BlockTimestamp != 200 {
+ t.Fatalf("Entries not sorted by timestamp: %v", entries)
+ }
+
+ // Test 3: Different references don't interfere
+ rawdb.WriteReferenceIndexEntry(db, ref2, 300, 0, txHash1)
+ entries1 := rawdb.ReadReferenceIndexEntries(db, ref1)
+ entries2 := rawdb.ReadReferenceIndexEntries(db, ref2)
+ if len(entries1) != 3 || len(entries2) != 1 {
+ t.Fatalf("Reference entries interference: ref1=%d, ref2=%d", len(entries1), len(entries2))
+ }
+
+ // Test 3.1: Pagination reads only the requested window
+ page := rawdb.ReadReferenceIndexEntriesPaginated(db, ref1, 1, 1)
+ if len(page) != 1 {
+ t.Fatalf("Expected 1 paginated entry, got %d", len(page))
+ }
+ if page[0].BlockTimestamp != 150 || page[0].TxHash != txHash3 {
+ t.Fatalf("Unexpected paginated entry: %+v", page[0])
+ }
+ page = rawdb.ReadReferenceIndexEntriesPaginated(db, ref1, 10, 10)
+ if len(page) != 0 {
+ t.Fatalf("Expected empty page for out-of-range offset, got %d", len(page))
+ }
+ interrupt := make(chan struct{})
+ close(interrupt)
+ pageWithInterrupt, interrupted := rawdb.ReadReferenceIndexEntriesPaginatedWithInterrupt(db, ref1, 0, 10, interrupt)
+ if !interrupted {
+ t.Fatal("Expected pagination read to be interrupted")
+ }
+ if len(pageWithInterrupt) != 0 {
+ t.Fatalf("Expected no entries on interrupted pagination read, got %d", len(pageWithInterrupt))
+ }
+
+ // Test 4: Delete an entry
+ rawdb.DeleteReferenceIndexEntry(db, ref1, 100, 0, txHash1)
+ entries = rawdb.ReadReferenceIndexEntries(db, ref1)
+ if len(entries) != 2 {
+ t.Fatalf("Expected 2 entries after delete, got %d", len(entries))
+ }
+ for _, e := range entries {
+ if e.TxHash == txHash1 {
+ t.Fatal("Deleted entry still present")
+ }
+ }
+
+ // Test 5: Tail operations
+ if tail := rawdb.ReadReferenceIndexTail(db); tail != nil {
+ t.Fatalf("Expected nil tail initially, got %d", *tail)
+ }
+ rawdb.WriteReferenceIndexTail(db, 100)
+ if tail := rawdb.ReadReferenceIndexTail(db); tail == nil || *tail != 100 {
+ t.Fatalf("Expected tail 100, got %v", tail)
+ }
+}
+
+// TestReferenceIndicesWithMixedTransactions tests that only MorphTx transactions
+// with Reference field are indexed, while regular transactions are not.
+func TestReferenceIndicesWithMixedTransactions(t *testing.T) {
+ var (
+ gendb = rawdb.NewMemoryDatabase()
+ key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
+ address = crypto.PubkeyToAddress(key.PublicKey)
+ funds = big.NewInt(100000000000000000)
+ gspec = &Genesis{
+ Config: params.TestChainConfig,
+ Alloc: GenesisAlloc{address: {Balance: funds}},
+ BaseFee: big.NewInt(params.InitialBaseFee),
+ }
+ genesis = gspec.MustCommit(gendb)
+ signer = types.LatestSigner(gspec.Config)
+ )
+
+ ref1 := common.BytesToReference([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x01})
+
+ var morphTxWithRef *types.Transaction
+ var morphTxWithoutRef *types.Transaction
+ var regularTx *types.Transaction
+
+ height := uint64(10)
+ blocks, receipts := GenerateChain(gspec.Config, genesis, ethash.NewFaker(), gendb, int(height), func(i int, block *BlockGen) {
+ nonce := block.TxNonce(address)
+
+ // Regular legacy transaction
+ tx1, err := types.SignTx(types.NewTransaction(nonce, common.Address{0x00}, big.NewInt(1000), params.TxGas, block.header.BaseFee, nil), signer, key)
+ if err != nil {
+ panic(err)
+ }
+ block.AddTx(tx1)
+ if i == 5 {
+ regularTx = tx1
+ }
+
+ // MorphTx WITH Reference
+ tx2 := types.NewTx(&types.MorphTx{
+ ChainID: gspec.Config.ChainID,
+ Nonce: nonce + 1,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: block.header.BaseFee,
+ Gas: params.TxGas,
+ To: &common.Address{0x01},
+ Value: big.NewInt(100),
+ Data: nil,
+ AccessList: nil,
+ Version: types.MorphTxVersion1,
+ FeeTokenID: 0,
+ FeeLimit: nil,
+ Reference: &ref1,
+ Memo: nil,
+ })
+ signedTx2, err := types.SignTx(tx2, signer, key)
+ if err != nil {
+ panic(err)
+ }
+ block.AddTx(signedTx2)
+ if i == 5 {
+ morphTxWithRef = signedTx2
+ }
+
+ // MorphTx WITHOUT Reference
+ tx3 := types.NewTx(&types.MorphTx{
+ ChainID: gspec.Config.ChainID,
+ Nonce: nonce + 2,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: block.header.BaseFee,
+ Gas: params.TxGas,
+ To: &common.Address{0x02},
+ Value: big.NewInt(200),
+ Data: nil,
+ AccessList: nil,
+ Version: types.MorphTxVersion1,
+ FeeTokenID: 0,
+ FeeLimit: nil,
+ Reference: nil, // No reference
+ Memo: nil,
+ })
+ signedTx3, err := types.SignTx(tx3, signer, key)
+ if err != nil {
+ panic(err)
+ }
+ block.AddTx(signedTx3)
+ if i == 5 {
+ morphTxWithoutRef = signedTx3
+ }
+ })
+
+ frdir, err := ioutil.TempDir("", "")
+ if err != nil {
+ t.Fatalf("failed to create temp freezer dir: %v", err)
+ }
+ defer os.Remove(frdir)
+
+ ancientDb, err := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), frdir, "", false)
+ if err != nil {
+ t.Fatalf("failed to create temp freezer db: %v", err)
+ }
+ gspec.MustCommit(ancientDb)
+
+ l := uint64(0)
+ chain, err := NewBlockChain(ancientDb, nil, params.TestChainConfig, ethash.NewFaker(), vm.Config{}, nil, &l)
+ if err != nil {
+ t.Fatalf("failed to create tester chain: %v", err)
+ }
+
+ headers := make([]*types.Header, len(blocks))
+ for i, block := range blocks {
+ headers[i] = block.Header()
+ }
+ if n, err := chain.InsertHeaderChain(headers, 0); err != nil {
+ t.Fatalf("failed to insert header %d: %v", n, err)
+ }
+ if n, err := chain.InsertReceiptChain(blocks, receipts, 0); err != nil {
+ t.Fatalf("block %d: failed to insert into chain: %v", n, err)
+ }
+ time.Sleep(50 * time.Millisecond) // Wait for indices initialisation
+
+ // Verify: MorphTx with Reference should be indexed
+ entries := rawdb.ReadReferenceIndexEntries(chain.db, ref1)
+ foundMorphTxWithRef := false
+ for _, entry := range entries {
+ if entry.TxHash == morphTxWithRef.Hash() {
+ foundMorphTxWithRef = true
+ }
+ // Regular tx and MorphTx without ref should not appear
+ if entry.TxHash == regularTx.Hash() {
+ t.Fatal("Regular transaction should not be in reference index")
+ }
+ if entry.TxHash == morphTxWithoutRef.Hash() {
+ t.Fatal("MorphTx without reference should not be in reference index")
+ }
+ }
+ if !foundMorphTxWithRef {
+ t.Fatal("MorphTx with reference should be in reference index")
+ }
+
+ // Verify: All MorphTx with ref1 should be indexed (10 blocks * 1 tx per block = 10 entries)
+ if len(entries) != int(height) {
+ t.Fatalf("Expected %d reference index entries, got %d", height, len(entries))
+ }
+
+ chain.Stop()
+ ancientDb.Close()
+}
+
+// TestReferenceIndicesMultipleTxsSameReference tests that multiple transactions
+// with the same Reference are correctly indexed and queried.
+func TestReferenceIndicesMultipleTxsSameReference(t *testing.T) {
+ var (
+ gendb = rawdb.NewMemoryDatabase()
+ key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
+ address = crypto.PubkeyToAddress(key.PublicKey)
+ funds = big.NewInt(100000000000000000)
+ gspec = &Genesis{
+ Config: params.TestChainConfig,
+ Alloc: GenesisAlloc{address: {Balance: funds}},
+ BaseFee: big.NewInt(params.InitialBaseFee),
+ }
+ genesis = gspec.MustCommit(gendb)
+ signer = types.LatestSigner(gspec.Config)
+ )
+
+ // All transactions share the same reference
+ sharedRef := common.BytesToReference([]byte{0xaa, 0xbb, 0xcc, 0xdd, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0xee})
+
+ var txHashes []common.Hash
+
+ height := uint64(20)
+ blocks, receipts := GenerateChain(gspec.Config, genesis, ethash.NewFaker(), gendb, int(height), func(i int, block *BlockGen) {
+ nonce := block.TxNonce(address)
+
+ // Add 2 MorphTx with same reference per block
+ for j := 0; j < 2; j++ {
+ morphTx := types.NewTx(&types.MorphTx{
+ ChainID: gspec.Config.ChainID,
+ Nonce: nonce + uint64(j),
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: block.header.BaseFee,
+ Gas: params.TxGas,
+ To: &common.Address{0x01},
+ Value: big.NewInt(100),
+ Data: nil,
+ AccessList: nil,
+ Version: types.MorphTxVersion1,
+ FeeTokenID: 0,
+ FeeLimit: nil,
+ Reference: &sharedRef,
+ Memo: nil,
+ })
+ signedMorphTx, err := types.SignTx(morphTx, signer, key)
+ if err != nil {
+ panic(err)
+ }
+ block.AddTx(signedMorphTx)
+ txHashes = append(txHashes, signedMorphTx.Hash())
+ }
+ })
+
+ frdir, err := ioutil.TempDir("", "")
+ if err != nil {
+ t.Fatalf("failed to create temp freezer dir: %v", err)
+ }
+ defer os.Remove(frdir)
+
+ ancientDb, err := rawdb.NewDatabaseWithFreezer(rawdb.NewMemoryDatabase(), frdir, "", false)
+ if err != nil {
+ t.Fatalf("failed to create temp freezer db: %v", err)
+ }
+ gspec.MustCommit(ancientDb)
+
+ l := uint64(0)
+ chain, err := NewBlockChain(ancientDb, nil, params.TestChainConfig, ethash.NewFaker(), vm.Config{}, nil, &l)
+ if err != nil {
+ t.Fatalf("failed to create tester chain: %v", err)
+ }
+
+ headers := make([]*types.Header, len(blocks))
+ for i, block := range blocks {
+ headers[i] = block.Header()
+ }
+ if n, err := chain.InsertHeaderChain(headers, 0); err != nil {
+ t.Fatalf("failed to insert header %d: %v", n, err)
+ }
+ if n, err := chain.InsertReceiptChain(blocks, receipts, 0); err != nil {
+ t.Fatalf("block %d: failed to insert into chain: %v", n, err)
+ }
+ time.Sleep(50 * time.Millisecond) // Wait for indices initialisation
+
+ // Query all entries for the shared reference
+ entries := rawdb.ReadReferenceIndexEntries(chain.db, sharedRef)
+
+ // Should have 20 blocks * 2 txs = 40 entries
+ expectedCount := int(height) * 2
+ if len(entries) != expectedCount {
+ t.Fatalf("Expected %d entries for shared reference, got %d", expectedCount, len(entries))
+ }
+
+ // Verify all tx hashes are found
+ entryHashes := make(map[common.Hash]bool)
+ for _, entry := range entries {
+ entryHashes[entry.TxHash] = true
+ }
+ for _, txHash := range txHashes {
+ if !entryHashes[txHash] {
+ t.Fatalf("Missing tx hash in reference index: %s", txHash.Hex())
+ }
+ }
+
+ // Verify sorting by blockTimestamp and txIndex
+ for i := 1; i < len(entries); i++ {
+ prev := entries[i-1]
+ curr := entries[i]
+ if prev.BlockTimestamp > curr.BlockTimestamp {
+ t.Fatalf("Entries not sorted by timestamp: prev=%d, curr=%d", prev.BlockTimestamp, curr.BlockTimestamp)
+ }
+ if prev.BlockTimestamp == curr.BlockTimestamp && prev.TxIndex > curr.TxIndex {
+ t.Fatalf("Entries not sorted by txIndex within same block: prev=%d, curr=%d", prev.TxIndex, curr.TxIndex)
+ }
+ }
+
+ chain.Stop()
+ ancientDb.Close()
+}
+
+// TestReferenceIndicesReorgCleanup ensures reference indices from the old canonical
+// branch are removed during reorg, while entries on the new canonical branch remain.
+func TestReferenceIndicesReorgCleanup(t *testing.T) {
+ // Need a config with JadeForkTime enabled so MorphTx V1 is accepted.
+ jadeCfg := *params.TestChainConfig
+ jadeCfg.EmeraldTime = new(uint64) // Emerald at time 0
+ jadeCfg.JadeForkTime = new(uint64) // Jade at time 0
+
+ var (
+ db = rawdb.NewMemoryDatabase()
+ key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
+ address = crypto.PubkeyToAddress(key.PublicKey)
+ funds = big.NewInt(100000000000000000)
+ gspec = &Genesis{
+ Config: &jadeCfg,
+ Alloc: GenesisAlloc{address: {Balance: funds}},
+ BaseFee: big.NewInt(params.InitialBaseFee),
+ }
+ genesis = gspec.MustCommit(db)
+ signer = types.LatestSigner(gspec.Config)
+ )
+
+ ref := common.BytesToReference([]byte{0xaa, 0xbb, 0xcc, 0xdd, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0xee})
+ sharedTx := types.NewTx(&types.MorphTx{
+ ChainID: gspec.Config.ChainID,
+ Nonce: 0,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: new(big.Int).Mul(big.NewInt(params.InitialBaseFee), big.NewInt(100)),
+ Gas: params.TxGas,
+ To: &common.Address{0x01},
+ Value: big.NewInt(1),
+ Data: nil,
+ AccessList: nil,
+ Version: types.MorphTxVersion1,
+ FeeTokenID: 0,
+ FeeLimit: nil,
+ Reference: &ref,
+ Memo: nil,
+ })
+ signedTx, err := types.SignTx(sharedTx, signer, key)
+ if err != nil {
+ t.Fatalf("failed to sign shared tx: %v", err)
+ }
+
+ chainA, _ := GenerateChain(gspec.Config, genesis, ethash.NewFaker(), db, 3, func(i int, block *BlockGen) {
+ if i == 0 {
+ block.AddTx(signedTx)
+ }
+ })
+ chainB, _ := GenerateChain(gspec.Config, genesis, ethash.NewFaker(), db, 4, func(i int, block *BlockGen) {
+ if i == 1 {
+ block.AddTx(signedTx)
+ }
+ })
+
+ l := uint64(0)
+ chain, err := NewBlockChain(db, nil, &jadeCfg, ethash.NewFaker(), vm.Config{}, nil, &l)
+ if err != nil {
+ t.Fatalf("failed to create tester chain: %v", err)
+ }
+ defer chain.Stop()
+
+ if _, err := chain.InsertChain(chainA); err != nil {
+ t.Fatalf("failed to insert chainA: %v", err)
+ }
+ entries := rawdb.ReadReferenceIndexEntries(chain.db, ref)
+ if len(entries) != 1 {
+ t.Fatalf("expected 1 entry after chainA insert, got %d", len(entries))
+ }
+ oldTimestamp := chainA[0].Time()
+ if entries[0].BlockTimestamp != oldTimestamp {
+ t.Fatalf("expected old timestamp %d, got %d", oldTimestamp, entries[0].BlockTimestamp)
+ }
+
+ if _, err := chain.InsertChain(chainB); err != nil {
+ t.Fatalf("failed to insert chainB: %v", err)
+ }
+
+ // After reorg, there should be exactly 1 entry with the new chain's position.
+ // The original code (trulyDeletedTxs approach) would leave 2 entries (stale + new)
+ // because the same tx hash exists in both chains but at different positions.
+ entries = rawdb.ReadReferenceIndexEntries(chain.db, ref)
+ if len(entries) != 1 {
+ t.Fatalf("expected exactly 1 entry after reorg cleanup, got %d (stale key leak!)", len(entries))
+ }
+ newTimestamp := chainB[1].Time()
+ if entries[0].BlockTimestamp != newTimestamp {
+ t.Fatalf("expected reorged entry timestamp %d, got %d", newTimestamp, entries[0].BlockTimestamp)
+ }
+ if entries[0].TxHash != signedTx.Hash() {
+ t.Fatalf("unexpected tx hash after reorg cleanup: %s", entries[0].TxHash.Hex())
+ }
+}
diff --git a/core/genesis.go b/core/genesis.go
index fc5099f25..202480889 100644
--- a/core/genesis.go
+++ b/core/genesis.go
@@ -149,7 +149,7 @@ type ChainOverrides struct {
Morph203Time *uint64
ViridianTime *uint64
EmeraldTime *uint64
- MPTForkTime *uint64
+ JadeForkTime *uint64
}
// apply applies the chain overrides on the supplied chain config.
@@ -166,8 +166,8 @@ func (o *ChainOverrides) apply(cfg *params.ChainConfig) error {
if o.EmeraldTime != nil {
cfg.EmeraldTime = o.EmeraldTime
}
- if o.MPTForkTime != nil {
- cfg.MPTForkTime = o.MPTForkTime
+ if o.JadeForkTime != nil {
+ cfg.JadeForkTime = o.JadeForkTime
}
return cfg.CheckConfigForkOrder()
}
diff --git a/core/mpt_fork_test.go b/core/mpt_fork_test.go
index 8ddef79e1..5461140ee 100644
--- a/core/mpt_fork_test.go
+++ b/core/mpt_fork_test.go
@@ -29,58 +29,58 @@ import (
"github.com/morph-l2/go-ethereum/params"
)
-// TestMPTForkTransition tests the behavior around MPT fork time
-func TestMPTForkTransition(t *testing.T) {
+// TestJadeForkTransition tests the behavior around Jade fork time
+func TestJadeForkTransition(t *testing.T) {
forkTime := uint64(1000)
tests := []struct {
- name string
- blockTime uint64
- mptForkTime *uint64
- expectFork bool
- description string
+ name string
+ blockTime uint64
+ jadeForkTime *uint64
+ expectFork bool
+ description string
}{
{
- name: "Before fork time",
- blockTime: 500,
- mptForkTime: &forkTime,
- expectFork: false,
- description: "Block before MPT fork should not be in fork period",
+ name: "Before fork time",
+ blockTime: 500,
+ jadeForkTime: &forkTime,
+ expectFork: false,
+ description: "Block before Jade fork should not be in fork period",
},
{
- name: "At fork time",
- blockTime: 1000,
- mptForkTime: &forkTime,
- expectFork: true,
- description: "Block at exact fork time should be in fork period",
+ name: "At fork time",
+ blockTime: 1000,
+ jadeForkTime: &forkTime,
+ expectFork: true,
+ description: "Block at exact fork time should be in fork period",
},
{
- name: "After fork time",
- blockTime: 1500,
- mptForkTime: &forkTime,
- expectFork: true,
- description: "Block after fork time should be in fork period",
+ name: "After fork time",
+ blockTime: 1500,
+ jadeForkTime: &forkTime,
+ expectFork: true,
+ description: "Block after fork time should be in fork period",
},
{
- name: "No fork configured",
- blockTime: 1500,
- mptForkTime: nil,
- expectFork: false,
- description: "Without fork config, should never be in fork period",
+ name: "No fork configured",
+ blockTime: 1500,
+ jadeForkTime: nil,
+ expectFork: false,
+ description: "Without fork config, should never be in fork period",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := ¶ms.ChainConfig{
- MPTForkTime: tt.mptForkTime,
+ JadeForkTime: tt.jadeForkTime,
}
- isFork := config.IsMPTFork(tt.blockTime)
+ isFork := config.IsJadeFork(tt.blockTime)
if isFork != tt.expectFork {
- t.Errorf("%s: Expected IsMPTFork=%v, got %v", tt.description, tt.expectFork, isFork)
+ t.Errorf("%s: Expected IsJadeFork=%v, got %v", tt.description, tt.expectFork, isFork)
} else {
- t.Logf("✓ %s: IsMPTFork=%v (correct)", tt.description, isFork)
+ t.Logf("✓ %s: IsJadeFork=%v (correct)", tt.description, isFork)
}
})
}
@@ -356,7 +356,7 @@ func TestZkTrieNodeSyncsMPTBlocks(t *testing.T) {
forkTime := uint64(10)
zkTrieConfig := params.TestChainConfig.Clone()
zkTrieConfig.Morph.UseZktrie = true
- zkTrieConfig.MPTForkTime = &forkTime // Enable cross-format validation skipping
+ zkTrieConfig.JadeForkTime = &forkTime // Enable cross-format validation skipping
// zkTrie node uses same zkTrie genesis (no GenesisStateRoot override)
zkGspec2 := &Genesis{
diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go
index b402eb25f..f69d0bd80 100644
--- a/core/rawdb/accessors_chain.go
+++ b/core/rawdb/accessors_chain.go
@@ -639,6 +639,9 @@ type storedReceiptRLP struct {
FeeRate *big.Int
TokenScale *big.Int
FeeLimit *big.Int
+ Version byte `rlp:"optional"`
+ Reference []byte `rlp:"optional"` // Use []byte for RLP compatibility (common.Reference is [32]byte, can't decode empty)
+ Memo []byte `rlp:"optional"`
}
// ReceiptLogs is a barebone version of ReceiptForStorage which only keeps
diff --git a/core/rawdb/accessors_reference_index.go b/core/rawdb/accessors_reference_index.go
new file mode 100644
index 000000000..65df2f36b
--- /dev/null
+++ b/core/rawdb/accessors_reference_index.go
@@ -0,0 +1,172 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package rawdb
+
+import (
+ "encoding/binary"
+ "math"
+ "math/big"
+
+ "github.com/morph-l2/go-ethereum/common"
+ "github.com/morph-l2/go-ethereum/core/types"
+ "github.com/morph-l2/go-ethereum/ethdb"
+ "github.com/morph-l2/go-ethereum/log"
+)
+
+// ReferenceIndexEntry stores the transaction location info for reference index lookups.
+type ReferenceIndexEntry struct {
+ BlockTimestamp uint64
+ TxIndex uint64
+ TxHash common.Hash
+}
+
+// WriteReferenceIndexEntry stores a reference index entry.
+// Key format: prefix + reference + blockTimestamp + txIndex + txHash
+// Value: nil (empty, we only need the key for lookups)
+func WriteReferenceIndexEntry(db ethdb.KeyValueWriter, reference common.Reference, blockTimestamp uint64, txIndex uint64, txHash common.Hash) {
+ key := referenceIndexKey(reference, blockTimestamp, txIndex, txHash)
+ if err := db.Put(key, nil); err != nil {
+ log.Crit("Failed to store reference index entry", "err", err)
+ }
+}
+
+// WriteReferenceIndexEntriesForBlock writes reference index entries for all MorphTx transactions in a block.
+func WriteReferenceIndexEntriesForBlock(db ethdb.KeyValueWriter, block *types.Block) {
+ for txIndex, tx := range block.Transactions() {
+ if tx.IsMorphTx() {
+ if ref := tx.Reference(); ref != nil {
+ WriteReferenceIndexEntry(db, *ref, block.Time(), uint64(txIndex), tx.Hash())
+ }
+ }
+ }
+}
+
+// DeleteReferenceIndexEntry removes a reference index entry.
+func DeleteReferenceIndexEntry(db ethdb.KeyValueWriter, reference common.Reference, blockTimestamp uint64, txIndex uint64, txHash common.Hash) {
+ key := referenceIndexKey(reference, blockTimestamp, txIndex, txHash)
+ if err := db.Delete(key); err != nil {
+ log.Crit("Failed to delete reference index entry", "err", err)
+ }
+}
+
+// DeleteReferenceIndexEntriesForBlock removes reference index entries for all MorphTx transactions in a block.
+func DeleteReferenceIndexEntriesForBlock(db ethdb.KeyValueWriter, block *types.Block) {
+ for txIndex, tx := range block.Transactions() {
+ if tx.IsMorphTx() {
+ if ref := tx.Reference(); ref != nil {
+ DeleteReferenceIndexEntry(db, *ref, block.Time(), uint64(txIndex), tx.Hash())
+ }
+ }
+ }
+}
+
+func parseReferenceIndexEntry(key []byte) (ReferenceIndexEntry, bool) {
+ // Validate key length: prefix(3) + reference(32) + blockTimestamp(8) + txIndex(8) + txHash(32) = 83 bytes
+ if len(key) != len(referenceIndexPrefix)+common.ReferenceLength+8+8+common.HashLength {
+ return ReferenceIndexEntry{}, false
+ }
+ offset := len(referenceIndexPrefix) + common.ReferenceLength
+ blockTimestamp := binary.BigEndian.Uint64(key[offset:])
+ offset += 8
+ txIndex := binary.BigEndian.Uint64(key[offset:])
+ offset += 8
+ txHash := common.BytesToHash(key[offset:])
+
+ return ReferenceIndexEntry{
+ BlockTimestamp: blockTimestamp,
+ TxIndex: txIndex,
+ TxHash: txHash,
+ }, true
+}
+
+// ReadReferenceIndexEntriesPaginatedWithInterrupt returns a page of transaction entries for a given reference.
+// Results are sorted by blockTimestamp and txIndex (ascending order due to key structure).
+// If interrupt is signaled, the call aborts and returns interrupted=true.
+func ReadReferenceIndexEntriesPaginatedWithInterrupt(db ethdb.Database, reference common.Reference, offset uint64, limit uint64, interrupt <-chan struct{}) ([]ReferenceIndexEntry, bool) {
+ if limit == 0 {
+ return nil, false
+ }
+ prefix := referenceIndexKeyPrefix(reference)
+ it := db.NewIterator(prefix, nil)
+ defer it.Release()
+
+ end := uint64(math.MaxUint64)
+ if offset <= math.MaxUint64-limit {
+ end = offset + limit
+ }
+ // Cap initial allocation to avoid huge pre-allocation when limit is very large
+ initCap := limit
+ if initCap > 1024 {
+ initCap = 1024
+ }
+ entries := make([]ReferenceIndexEntry, 0, initCap)
+ index := uint64(0)
+ for it.Next() {
+ select {
+ case <-interrupt:
+ return nil, true
+ default:
+ }
+ entry, ok := parseReferenceIndexEntry(it.Key())
+ if !ok {
+ continue
+ }
+ if index < offset {
+ index++
+ continue
+ }
+ if index >= end {
+ break
+ }
+ entries = append(entries, entry)
+ index++
+ }
+ if it.Error() != nil {
+ log.Error("Failed to iterate paginated reference index entries", "reference", reference.Hex(), "err", it.Error())
+ }
+ return entries, false
+}
+
+// ReadReferenceIndexEntriesPaginated returns a page of transaction entries for a given reference.
+// Results are sorted by blockTimestamp and txIndex (ascending order due to key structure).
+func ReadReferenceIndexEntriesPaginated(db ethdb.Database, reference common.Reference, offset uint64, limit uint64) []ReferenceIndexEntry {
+ entries, _ := ReadReferenceIndexEntriesPaginatedWithInterrupt(db, reference, offset, limit, nil)
+ return entries
+}
+
+// ReadReferenceIndexEntries returns all transaction entries for a given reference.
+// Results are sorted by blockTimestamp and txIndex (ascending order due to key structure).
+func ReadReferenceIndexEntries(db ethdb.Database, reference common.Reference) []ReferenceIndexEntry {
+ return ReadReferenceIndexEntriesPaginated(db, reference, 0, math.MaxUint64)
+}
+
+// ReadReferenceIndexTail retrieves the block number whose reference index is the oldest stored.
+func ReadReferenceIndexTail(db ethdb.KeyValueReader) *uint64 {
+ data, err := db.Get(referenceIndexTailKey)
+ if err != nil || len(data) == 0 {
+ return nil
+ }
+ number := new(big.Int).SetBytes(data).Uint64()
+ return &number
+}
+
+// WriteReferenceIndexTail stores the block number whose reference index is the oldest stored.
+func WriteReferenceIndexTail(db ethdb.KeyValueWriter, blockNumber uint64) {
+ if err := db.Put(referenceIndexTailKey, new(big.Int).SetUint64(blockNumber).Bytes()); err != nil {
+ log.Crit("Failed to store reference index tail", "err", err)
+ }
+}
diff --git a/core/rawdb/accessors_reference_index_test.go b/core/rawdb/accessors_reference_index_test.go
new file mode 100644
index 000000000..85ffd6a1c
--- /dev/null
+++ b/core/rawdb/accessors_reference_index_test.go
@@ -0,0 +1,261 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package rawdb
+
+import (
+ "testing"
+
+ "github.com/morph-l2/go-ethereum/common"
+ "github.com/morph-l2/go-ethereum/ethdb"
+)
+
+// ---------- helpers ----------
+
+func makeRef(b byte) common.Reference {
+ var ref common.Reference
+ ref[0] = b
+ return ref
+}
+
+func makeTxHash(b byte) common.Hash {
+ var h common.Hash
+ h[0] = b
+ return h
+}
+
+// writeN writes n reference index entries for the given reference, with
+// blockTimestamp = base+i, txIndex = i, txHash = makeTxHash(byte(i)).
+func writeN(db ethdb.Database, ref common.Reference, n int, base uint64) {
+ for i := 0; i < n; i++ {
+ WriteReferenceIndexEntry(db, ref, base+uint64(i), uint64(i), makeTxHash(byte(i)))
+ }
+}
+
+// ---------- Issue 1: Full-scan DoS verification ----------
+
+// TestPagination_FullReadMatchesOld verifies that ReadReferenceIndexEntries
+// (backward-compat wrapper) still returns every entry — confirming it was a
+// full scan prior to the pagination fix.
+func TestPagination_FullReadMatchesOld(t *testing.T) {
+ db := NewMemoryDatabase()
+ ref := makeRef(0x01)
+ writeN(db, ref, 50, 1000)
+
+ all := ReadReferenceIndexEntries(db, ref)
+ if len(all) != 50 {
+ t.Fatalf("expected 50 entries from full read, got %d", len(all))
+ }
+}
+
+// TestPagination_LimitStopsEarly proves that the paginated reader returns
+// exactly `limit` entries and does NOT load everything.
+func TestPagination_LimitStopsEarly(t *testing.T) {
+ db := NewMemoryDatabase()
+ ref := makeRef(0x02)
+ writeN(db, ref, 200, 0) // 200 entries stored
+
+ // Request only the first 10
+ page := ReadReferenceIndexEntriesPaginated(db, ref, 0, 10)
+ if len(page) != 10 {
+ t.Fatalf("expected 10 entries, got %d", len(page))
+ }
+
+ // Verify the entries are the first 10 (sorted by timestamp)
+ for i, e := range page {
+ if e.BlockTimestamp != uint64(i) {
+ t.Fatalf("entry %d: expected timestamp %d, got %d", i, i, e.BlockTimestamp)
+ }
+ }
+}
+
+// TestPagination_OffsetSkips verifies that offset correctly skips entries
+// and returns the right slice window.
+func TestPagination_OffsetSkips(t *testing.T) {
+ db := NewMemoryDatabase()
+ ref := makeRef(0x03)
+ writeN(db, ref, 50, 100) // timestamps 100..149
+
+ // Read entries 10..14 (offset=10, limit=5)
+ page := ReadReferenceIndexEntriesPaginated(db, ref, 10, 5)
+ if len(page) != 5 {
+ t.Fatalf("expected 5 entries, got %d", len(page))
+ }
+ if page[0].BlockTimestamp != 110 {
+ t.Fatalf("expected first entry timestamp 110, got %d", page[0].BlockTimestamp)
+ }
+ if page[4].BlockTimestamp != 114 {
+ t.Fatalf("expected last entry timestamp 114, got %d", page[4].BlockTimestamp)
+ }
+}
+
+// TestPagination_OffsetBeyondEnd returns empty result instead of error.
+func TestPagination_OffsetBeyondEnd(t *testing.T) {
+ db := NewMemoryDatabase()
+ ref := makeRef(0x04)
+ writeN(db, ref, 10, 0)
+
+ page := ReadReferenceIndexEntriesPaginated(db, ref, 999, 10)
+ if len(page) != 0 {
+ t.Fatalf("expected 0 entries for out-of-range offset, got %d", len(page))
+ }
+}
+
+// TestPagination_LimitZero returns empty.
+func TestPagination_LimitZero(t *testing.T) {
+ db := NewMemoryDatabase()
+ ref := makeRef(0x05)
+ writeN(db, ref, 10, 0)
+
+ page := ReadReferenceIndexEntriesPaginated(db, ref, 0, 0)
+ if len(page) != 0 {
+ t.Fatalf("expected 0 entries for limit=0, got %d", len(page))
+ }
+}
+
+// TestPagination_LimitExceedsTotal returns only what exists.
+func TestPagination_LimitExceedsTotal(t *testing.T) {
+ db := NewMemoryDatabase()
+ ref := makeRef(0x06)
+ writeN(db, ref, 5, 0)
+
+ page := ReadReferenceIndexEntriesPaginated(db, ref, 0, 100)
+ if len(page) != 5 {
+ t.Fatalf("expected 5 entries, got %d", len(page))
+ }
+}
+
+// ---------- Issue 3: Interrupt verification ----------
+
+// TestInterrupt_ImmediateStop verifies that a pre-closed interrupt channel
+// causes the reader to return immediately with interrupted=true and no results.
+func TestInterrupt_ImmediateStop(t *testing.T) {
+ db := NewMemoryDatabase()
+ ref := makeRef(0x07)
+ writeN(db, ref, 100, 0)
+
+ ch := make(chan struct{})
+ close(ch) // pre-closed
+
+ entries, interrupted := ReadReferenceIndexEntriesPaginatedWithInterrupt(db, ref, 0, 100, ch)
+ if !interrupted {
+ t.Fatal("expected interrupted=true for pre-closed channel")
+ }
+ if len(entries) != 0 {
+ t.Fatalf("expected 0 entries on interrupt, got %d", len(entries))
+ }
+}
+
+// TestInterrupt_NilChannel means no interrupt — should read normally.
+func TestInterrupt_NilChannel(t *testing.T) {
+ db := NewMemoryDatabase()
+ ref := makeRef(0x08)
+ writeN(db, ref, 20, 0)
+
+ entries, interrupted := ReadReferenceIndexEntriesPaginatedWithInterrupt(db, ref, 0, 20, nil)
+ if interrupted {
+ t.Fatal("expected interrupted=false for nil channel")
+ }
+ if len(entries) != 20 {
+ t.Fatalf("expected 20 entries, got %d", len(entries))
+ }
+}
+
+// ---------- Delete / Write consistency ----------
+
+// TestDeleteReferenceIndexEntry verifies single-entry deletion.
+func TestDeleteReferenceIndexEntry(t *testing.T) {
+ db := NewMemoryDatabase()
+ ref := makeRef(0x09)
+ txHash := makeTxHash(0xAA)
+
+ WriteReferenceIndexEntry(db, ref, 500, 3, txHash)
+ entries := ReadReferenceIndexEntries(db, ref)
+ if len(entries) != 1 {
+ t.Fatalf("expected 1 entry, got %d", len(entries))
+ }
+
+ DeleteReferenceIndexEntry(db, ref, 500, 3, txHash)
+ entries = ReadReferenceIndexEntries(db, ref)
+ if len(entries) != 0 {
+ t.Fatalf("expected 0 entries after delete, got %d", len(entries))
+ }
+}
+
+// ---------- Issue 2: Reorg stale entry scenario (rawdb level) ----------
+
+// TestReorg_StaleKeyScenario simulates the exact bug where the same tx hash
+// exists in both old and new chains at DIFFERENT positions. In the old code,
+// the "trulyDeletedTxs" approach would miss the old entry because the tx is
+// not "truly deleted" (it's in both chains). Our fix deletes ALL old chain
+// entries unconditionally.
+func TestReorg_StaleKeyScenario(t *testing.T) {
+ db := NewMemoryDatabase()
+ ref := makeRef(0x0A)
+ txHash := makeTxHash(0xBB)
+
+ // Simulate old chain: tx at block timestamp 1000, txIndex 0
+ WriteReferenceIndexEntry(db, ref, 1000, 0, txHash)
+
+ // Verify one entry exists
+ entries := ReadReferenceIndexEntries(db, ref)
+ if len(entries) != 1 || entries[0].BlockTimestamp != 1000 {
+ t.Fatalf("setup: expected 1 entry at ts=1000, got %d entries", len(entries))
+ }
+
+ // === Simulate the BUG (old trulyDeletedTxs approach) ===
+ // If tx is in both old and new chains, old code wouldn't delete the old entry.
+ // It would just write a new one, resulting in 2 entries for the same tx.
+ WriteReferenceIndexEntry(db, ref, 2000, 5, txHash) // new chain position
+
+ entries = ReadReferenceIndexEntries(db, ref)
+ if len(entries) != 2 {
+ t.Fatalf("bug scenario: expected 2 entries (stale + new), got %d", len(entries))
+ }
+ t.Logf("BUG CONFIRMED: %d stale+new entries for same tx hash (old-code behavior)", len(entries))
+
+ // === Simulate the FIX (delete old chain entries, then write new) ===
+ // Delete the old chain entry
+ DeleteReferenceIndexEntry(db, ref, 1000, 0, txHash)
+
+ entries = ReadReferenceIndexEntries(db, ref)
+ if len(entries) != 1 {
+ t.Fatalf("fix: expected 1 entry after cleanup, got %d", len(entries))
+ }
+ if entries[0].BlockTimestamp != 2000 {
+ t.Fatalf("fix: expected surviving entry at ts=2000, got ts=%d", entries[0].BlockTimestamp)
+ }
+ if entries[0].TxIndex != 5 {
+ t.Fatalf("fix: expected surviving entry at txIdx=5, got txIdx=%d", entries[0].TxIndex)
+ }
+ t.Logf("FIX VERIFIED: only 1 correct entry remains after cleanup")
+}
+
+// TestParseReferenceIndexEntry_InvalidKey ensures malformed keys are rejected.
+func TestParseReferenceIndexEntry_InvalidKey(t *testing.T) {
+ // Too short key
+ _, ok := parseReferenceIndexEntry([]byte{0x01, 0x02, 0x03})
+ if ok {
+ t.Fatal("expected invalid parse for short key")
+ }
+ // Correct length but arbitrary bytes (should still parse)
+ key := make([]byte, len(referenceIndexPrefix)+common.ReferenceLength+8+8+common.HashLength)
+ copy(key, referenceIndexPrefix)
+ _, ok = parseReferenceIndexEntry(key)
+ if !ok {
+ t.Fatal("expected valid parse for correct-length key")
+ }
+}
diff --git a/core/rawdb/reference_index_iterator.go b/core/rawdb/reference_index_iterator.go
new file mode 100644
index 000000000..fc69a86ee
--- /dev/null
+++ b/core/rawdb/reference_index_iterator.go
@@ -0,0 +1,337 @@
+// Copyright 2024 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+package rawdb
+
+import (
+ "runtime"
+ "sync/atomic"
+ "time"
+
+ "github.com/morph-l2/go-ethereum/common"
+ "github.com/morph-l2/go-ethereum/common/prque"
+ "github.com/morph-l2/go-ethereum/core/types"
+ "github.com/morph-l2/go-ethereum/ethdb"
+ "github.com/morph-l2/go-ethereum/log"
+ "github.com/morph-l2/go-ethereum/rlp"
+)
+
+// blockReferenceInfo contains reference information for a block
+type blockReferenceInfo struct {
+ number uint64
+ blockTimestamp uint64
+ references []referenceEntry
+}
+
+// referenceEntry contains a single reference entry
+type referenceEntry struct {
+ reference common.Reference
+ txIndex uint64
+ txHash common.Hash
+}
+
+// iterateReferences iterates over all MorphTx transactions with references in the (canon) block
+// number(s) given, and yields the reference entries on a channel. If there is a signal
+// received from interrupt channel, the iteration will be aborted and result
+// channel will be closed.
+func iterateReferences(db ethdb.Database, from uint64, to uint64, reverse bool, interrupt chan struct{}) chan *blockReferenceInfo {
+ if to == from {
+ return nil
+ }
+ threads := to - from
+ if cpus := runtime.NumCPU(); threads > uint64(cpus) {
+ threads = uint64(cpus)
+ }
+
+ type numberBodyRlp struct {
+ number uint64
+ rlp rlp.RawValue
+ header *types.Header
+ }
+
+ var (
+ rlpCh = make(chan *numberBodyRlp, threads*2)
+ resultCh = make(chan *blockReferenceInfo, threads*2)
+ )
+
+ // lookup runs in one instance
+ lookup := func() {
+ n, end := from, to
+ if reverse {
+ n, end = to-1, from-1
+ }
+ defer close(rlpCh)
+ for n != end {
+ data := ReadCanonicalBodyRLP(db, n)
+ hash := ReadCanonicalHash(db, n)
+ header := ReadHeader(db, hash, n)
+ // Feed the block to the aggregator, or abort on interrupt
+ select {
+ case rlpCh <- &numberBodyRlp{n, data, header}:
+ case <-interrupt:
+ return
+ }
+ if reverse {
+ n--
+ } else {
+ n++
+ }
+ }
+ }
+
+ // process runs in parallel
+ nThreadsAlive := int32(threads)
+ process := func() {
+ defer func() {
+ // Last processor closes the result channel
+ if atomic.AddInt32(&nThreadsAlive, -1) == 0 {
+ close(resultCh)
+ }
+ }()
+ for data := range rlpCh {
+ if data.header == nil {
+ log.Warn("Failed to read header for reference indexing", "block", data.number)
+ // Emit placeholder result to maintain contiguous block numbers
+ select {
+ case resultCh <- &blockReferenceInfo{number: data.number}:
+ case <-interrupt:
+ return
+ }
+ continue
+ }
+ var body types.Body
+ if err := rlp.DecodeBytes(data.rlp, &body); err != nil {
+ log.Warn("Failed to decode block body", "block", data.number, "error", err)
+ // Emit placeholder result to maintain contiguous block numbers
+ select {
+ case resultCh <- &blockReferenceInfo{number: data.number, blockTimestamp: data.header.Time}:
+ case <-interrupt:
+ return
+ }
+ continue
+ }
+
+ var refs []referenceEntry
+ for txIndex, tx := range body.Transactions {
+ if tx.IsMorphTx() {
+ if ref := tx.Reference(); ref != nil {
+ refs = append(refs, referenceEntry{
+ reference: *ref,
+ txIndex: uint64(txIndex),
+ txHash: tx.Hash(),
+ })
+ }
+ }
+ }
+
+ // Always send result for every block (even if no references) to maintain
+ // contiguous block numbers for gap-filling logic in indexReferences
+ result := &blockReferenceInfo{
+ number: data.number,
+ blockTimestamp: data.header.Time,
+ references: refs,
+ }
+ // Feed the block to the aggregator, or abort on interrupt
+ select {
+ case resultCh <- result:
+ case <-interrupt:
+ return
+ }
+ }
+ }
+
+ go lookup() // start the sequential db accessor
+ for i := 0; i < int(threads); i++ {
+ go process()
+ }
+ return resultCh
+}
+
+// indexReferences creates reference indices of the specified block range.
+//
+// This function iterates canonical chain in reverse order, it has one main advantage:
+// We can write reference index tail flag periodically even without the whole indexing
+// procedure is finished. So that we can resume indexing procedure next time quickly.
+//
+// There is a passed channel, the whole procedure will be interrupted if any
+// signal received.
+func indexReferences(db ethdb.Database, from uint64, to uint64, interrupt chan struct{}, hook func(uint64) bool) {
+ // short circuit for invalid range
+ if from >= to {
+ return
+ }
+ var (
+ resultCh = iterateReferences(db, from, to, true, interrupt)
+ batch = db.NewBatch()
+ start = time.Now()
+ logged = start.Add(-7 * time.Second)
+ // Since we iterate in reverse, we expect the first number to come
+ // in to be [to-1]. Therefore, setting lastNum to means that the
+ // prqueue gap-evaluation will work correctly
+ lastNum = to
+ queue = prque.New(nil)
+ // for stats reporting
+ blocks, refs = 0, 0
+ )
+ for chanDelivery := range resultCh {
+ // Push the delivery into the queue and process contiguous ranges.
+ // Since we iterate in reverse, so lower numbers have lower prio, and
+ // we can use the number directly as prio marker
+ queue.Push(chanDelivery, int64(chanDelivery.number))
+ for !queue.Empty() {
+ // If the next available item is gapped, return
+ if _, priority := queue.Peek(); priority != int64(lastNum-1) {
+ break
+ }
+ // For testing
+ if hook != nil && !hook(lastNum-1) {
+ break
+ }
+ // Next block available, pop it off and index it
+ delivery := queue.PopItem().(*blockReferenceInfo)
+ lastNum = delivery.number
+ for _, ref := range delivery.references {
+ WriteReferenceIndexEntry(batch, ref.reference, delivery.blockTimestamp, ref.txIndex, ref.txHash)
+ }
+ blocks++
+ refs += len(delivery.references)
+ // If enough data was accumulated in memory or we're at the last block, dump to disk
+ if batch.ValueSize() > ethdb.IdealBatchSize {
+ WriteReferenceIndexTail(batch, lastNum) // Also write the tail here
+ if err := batch.Write(); err != nil {
+ log.Crit("Failed writing batch to db", "error", err)
+ return
+ }
+ batch.Reset()
+ }
+ // If we've spent too much time already, notify the user of what we're doing
+ if time.Since(logged) > 8*time.Second {
+ log.Info("Indexing references", "blocks", blocks, "refs", refs, "tail", lastNum, "total", to-from, "elapsed", common.PrettyDuration(time.Since(start)))
+ logged = time.Now()
+ }
+ }
+ }
+ // Flush the new indexing tail and the last committed data. It can also happen
+ // that the last batch is empty because nothing to index, but the tail has to
+ // be flushed anyway.
+ WriteReferenceIndexTail(batch, lastNum)
+ if err := batch.Write(); err != nil {
+ log.Crit("Failed writing batch to db", "error", err)
+ return
+ }
+ select {
+ case <-interrupt:
+ log.Debug("Reference indexing interrupted", "blocks", blocks, "refs", refs, "tail", lastNum, "elapsed", common.PrettyDuration(time.Since(start)))
+ default:
+ log.Info("Indexed references", "blocks", blocks, "refs", refs, "tail", lastNum, "elapsed", common.PrettyDuration(time.Since(start)))
+ }
+}
+
+// IndexReferences creates reference indices of the specified block range.
+//
+// This function iterates canonical chain in reverse order, it has one main advantage:
+// We can write reference index tail flag periodically even without the whole indexing
+// procedure is finished. So that we can resume indexing procedure next time quickly.
+//
+// There is a passed channel, the whole procedure will be interrupted if any
+// signal received.
+func IndexReferences(db ethdb.Database, from uint64, to uint64, interrupt chan struct{}) {
+ indexReferences(db, from, to, interrupt, nil)
+}
+
+// unindexReferences removes reference indices of the specified block range.
+//
+// There is a passed channel, the whole procedure will be interrupted if any
+// signal received.
+func unindexReferences(db ethdb.Database, from uint64, to uint64, interrupt chan struct{}, hook func(uint64) bool) {
+ // short circuit for invalid range
+ if from >= to {
+ return
+ }
+ var (
+ resultCh = iterateReferences(db, from, to, false, interrupt)
+ batch = db.NewBatch()
+ start = time.Now()
+ logged = start.Add(-7 * time.Second)
+ // we expect the first number to come in to be [from]. Therefore, setting
+ // nextNum to from means that the prqueue gap-evaluation will work correctly
+ nextNum = from
+ queue = prque.New(nil)
+ // for stats reporting
+ blocks, refs = 0, 0
+ )
+ // Otherwise spin up the concurrent iterator and unindexer
+ for delivery := range resultCh {
+ // Push the delivery into the queue and process contiguous ranges.
+ queue.Push(delivery, -int64(delivery.number))
+ for !queue.Empty() {
+ // If the next available item is gapped, return
+ if _, priority := queue.Peek(); -priority != int64(nextNum) {
+ break
+ }
+ // For testing
+ if hook != nil && !hook(nextNum) {
+ break
+ }
+ delivery := queue.PopItem().(*blockReferenceInfo)
+ nextNum = delivery.number + 1
+ for _, ref := range delivery.references {
+ DeleteReferenceIndexEntry(batch, ref.reference, delivery.blockTimestamp, ref.txIndex, ref.txHash)
+ }
+ refs += len(delivery.references)
+ blocks++
+
+ // If enough data was accumulated in memory or we're at the last block, dump to disk
+ // A batch counts the size of deletion as '1', so we need to flush more
+ // often than that.
+ if blocks%1000 == 0 {
+ WriteReferenceIndexTail(batch, nextNum)
+ if err := batch.Write(); err != nil {
+ log.Crit("Failed writing batch to db", "error", err)
+ return
+ }
+ batch.Reset()
+ }
+ // If we've spent too much time already, notify the user of what we're doing
+ if time.Since(logged) > 8*time.Second {
+ log.Info("Unindexing references", "blocks", blocks, "refs", refs, "total", to-from, "elapsed", common.PrettyDuration(time.Since(start)))
+ logged = time.Now()
+ }
+ }
+ }
+ // Flush the new indexing tail and the last committed data. It can also happen
+ // that the last batch is empty because nothing to unindex, but the tail has to
+ // be flushed anyway.
+ WriteReferenceIndexTail(batch, nextNum)
+ if err := batch.Write(); err != nil {
+ log.Crit("Failed writing batch to db", "error", err)
+ return
+ }
+ select {
+ case <-interrupt:
+ log.Debug("Reference unindexing interrupted", "blocks", blocks, "refs", refs, "tail", to, "elapsed", common.PrettyDuration(time.Since(start)))
+ default:
+ log.Info("Unindexed references", "blocks", blocks, "refs", refs, "tail", to, "elapsed", common.PrettyDuration(time.Since(start)))
+ }
+}
+
+// UnindexReferences removes reference indices of the specified block range.
+//
+// There is a passed channel, the whole procedure will be interrupted if any
+// signal received.
+func UnindexReferences(db ethdb.Database, from uint64, to uint64, interrupt chan struct{}) {
+ unindexReferences(db, from, to, interrupt, nil)
+}
diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go
index c8d0de0d9..ea77b71ee 100644
--- a/core/rawdb/schema.go
+++ b/core/rawdb/schema.go
@@ -126,6 +126,11 @@ var (
rollupBatchL1DataFeePrefix = []byte("R-df")
rollupBatchHeadBatchHasFeeKey = []byte("R-hbf")
+ // Reference index: Key(prefix + reference + blockTimestamp + txIndex + txHash) : Value (nil)
+ referenceIndexPrefix = []byte("ref")
+ // Track the oldest block whose reference indices have been indexed
+ referenceIndexTailKey = []byte("ReferenceIndexTail")
+
// diskStateRoot mapping
diskStateRootPrefix = []byte("dsr") // diskStateRootPrefix + headerRoot -> diskRoot
)
@@ -317,6 +322,28 @@ func RollupBatchL1DataFeeKey(batchIndex uint64) []byte {
return append(rollupBatchL1DataFeePrefix, encodeBigEndian(batchIndex)...)
}
+// referenceIndexKey = referenceIndexPrefix + reference + blockTimestamp + txIndex + txHash
+// Key format: prefix(3) + reference(32) + blockTimestamp(8) + txIndex(8) + txHash(32) = 83 bytes
+func referenceIndexKey(reference common.Reference, blockTimestamp uint64, txIndex uint64, txHash common.Hash) []byte {
+ key := make([]byte, len(referenceIndexPrefix)+common.ReferenceLength+8+8+common.HashLength)
+ copy(key, referenceIndexPrefix)
+ offset := len(referenceIndexPrefix)
+ copy(key[offset:], reference[:])
+ offset += common.ReferenceLength
+ binary.BigEndian.PutUint64(key[offset:], blockTimestamp)
+ offset += 8
+ binary.BigEndian.PutUint64(key[offset:], txIndex)
+ offset += 8
+ copy(key[offset:], txHash[:])
+ return key
+}
+
+// referenceIndexKeyPrefix = referenceIndexPrefix + reference
+// Used for range queries by reference
+func referenceIndexKeyPrefix(reference common.Reference) []byte {
+ return append(referenceIndexPrefix, reference[:]...)
+}
+
// diskStateRootKey = diskStateRootPrefix + headerRoot
func diskStateRootKey(headerRoot common.Hash) []byte {
return append(diskStateRootPrefix, headerRoot.Bytes()...)
diff --git a/core/state_processor.go b/core/state_processor.go
index 8daab4d6f..06f98dd01 100644
--- a/core/state_processor.go
+++ b/core/state_processor.go
@@ -192,12 +192,18 @@ func ApplyTransactionWithEVM(msg Message, config *params.ChainConfig, gp *GasPoo
receipt.BlockNumber = blockNumber
receipt.TransactionIndex = uint(statedb.TxIndex())
receipt.L1Fee = result.L1DataFee
- if tx.IsAltFeeTx() {
+ if tx.IsMorphTx() {
tokenID := tx.FeeTokenID()
receipt.FeeTokenID = &tokenID
receipt.FeeLimit = tx.FeeLimit()
receipt.FeeRate = result.FeeRate
receipt.TokenScale = result.TokenScale
+ // Only include V1 fields (version, reference, memo) for V1+ transactions
+ if tx.Version() >= types.MorphTxVersion1 {
+ receipt.Version = tx.Version()
+ receipt.Reference = tx.Reference()
+ receipt.Memo = tx.Memo()
+ }
}
return receipt, err
diff --git a/core/state_transition.go b/core/state_transition.go
index a062a18df..72c0efca8 100644
--- a/core/state_transition.go
+++ b/core/state_transition.go
@@ -100,6 +100,9 @@ type Message interface {
SetCodeAuthorizations() []types.SetCodeAuthorization
FeeTokenID() uint16
FeeLimit() *big.Int
+ Version() uint8
+ Reference() *common.Reference
+ Memo() *[]byte
}
// ExecutionResult includes all output after executing given evm
diff --git a/core/tx_list.go b/core/tx_list.go
index e7edda2e8..d36ccc594 100644
--- a/core/tx_list.go
+++ b/core/tx_list.go
@@ -343,7 +343,7 @@ func (l *txList) Add(tx *types.Transaction, state *state.StateDB, priceBump uint
}
l.txs.Put(tx)
ethCost := big.NewInt(0)
- if tx.IsAltFeeTx() {
+ if tx.IsMorphTxWithAltFee() {
ethCost = new(big.Int).Set(tx.Value())
altCost, err := fees.EthToAlt(state, tx.FeeTokenID(), new(big.Int).Add(tx.GasFee(), l1DataFee))
if err != nil {
@@ -394,7 +394,7 @@ func (l *txList) Filter(costLimit *big.Int, gasLimit uint64, altCostLimit map[ui
// Filter out all the transactions above the account's funds
removed := l.txs.Filter(func(tx *types.Transaction) bool {
allLower := true
- if tx.IsAltFeeTx() {
+ if tx.IsMorphTxWithAltFee() {
for id, limit := range altCostLimit {
lower := l.costcap.Alt(id).Cmp(limit) <= 0
if !lower {
diff --git a/core/tx_pool.go b/core/tx_pool.go
index a07170239..76f96024e 100644
--- a/core/tx_pool.go
+++ b/core/tx_pool.go
@@ -257,6 +257,7 @@ type TxPool struct {
eip1559 bool // Fork indicator whether we are using EIP-1559 type transactions.
shanghai bool // Fork indicator whether we are in the Shanghai stage.
eip7702 bool // Fork indicator whether we are in the Morph 3.0.0 stage.
+ jade bool // Fork indicator whether we are in the Jade stage.
currentState *state.StateDB // Current state in the blockchain head
currentHead *big.Int // Current blockchain head
@@ -668,15 +669,24 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
if !pool.eip1559 && tx.Type() == types.DynamicFeeTxType {
return ErrTxTypeNotSupported
}
-
// Reject erc20 fee transactions until EIP-1559 activates.
- if !pool.eip1559 && tx.Type() == types.AltFeeTxType {
+ if !pool.eip1559 && tx.Type() == types.MorphTxType {
return ErrTxTypeNotSupported
}
if !pool.eip7702 && tx.Type() == types.SetCodeTxType {
return ErrTxTypeNotSupported
}
+ // Reject MorphTx V1 before jade fork is active
+ if !pool.jade && tx.IsMorphTx() && tx.Version() == types.MorphTxVersion1 {
+ return types.ErrMorphTxV1NotYetActive
+ }
+
+ // Validate MorphTx version, memo, and associated field requirements
+ if err := tx.ValidateMorphTxVersion(); err != nil {
+ return err
+ }
+
// Reject transactions over defined size to prevent DOS attacks
if uint64(tx.Size()) > uint64(pool.txMaxSize) {
return ErrOversizedData
@@ -724,7 +734,7 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
// 1. Check balance >= transaction cost (V + GP * GL) to maintain compatibility with the logic without considering L1 data fee.
// Transactor should have enough funds to cover the costs
// cost == V + GP * GL
- if tx.IsAltFeeTx() {
+ if tx.IsMorphTxWithAltFee() {
active, err := fees.IsTokenActive(pool.currentState, tx.FeeTokenID())
if err != nil {
return fmt.Errorf("get token status failed %v", err)
@@ -763,7 +773,7 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
return fmt.Errorf("failed to calculate L1 data fee, err: %w", err)
}
// Transactor should have enough funds to cover the costs
- if tx.IsAltFeeTx() {
+ if tx.IsMorphTxWithAltFee() {
if b := pool.currentState.GetBalance(from); b.Cmp(tx.Value()) < 0 {
return ErrInsufficientValue
}
@@ -1261,6 +1271,23 @@ func (pool *TxPool) removeTx(hash common.Hash, outofbound bool) {
}
}
+// removeMorphTxV1 removes all MorphTx V1 transactions from the pool.
+// This is called when the jade fork is not active (e.g. after a reorg that
+// rolls back past the fork time) to ensure no V1 transactions remain in the pool.
+func (pool *TxPool) removeMorphTxV1() {
+ var toRemove []common.Hash
+ pool.all.Range(func(hash common.Hash, tx *types.Transaction, local bool) bool {
+ if tx.IsMorphTx() && tx.Version() == types.MorphTxVersion1 {
+ toRemove = append(toRemove, hash)
+ }
+ return true
+ }, true, true)
+ for _, hash := range toRemove {
+ log.Trace("Removing MorphTx V1 (jade fork not active)", "hash", hash)
+ pool.removeTx(hash, true)
+ }
+}
+
// requestReset requests a pool reset to the new head block.
// The returned channel is closed when the reset has occurred.
func (pool *TxPool) requestReset(oldHead *types.Header, newHead *types.Header) chan struct{} {
@@ -1533,6 +1560,12 @@ func (pool *TxPool) reset(oldHead, newHead *types.Header) {
pool.eip1559 = pool.chainconfig.IsCurie(next)
pool.shanghai = pool.chainconfig.IsShanghai(next)
pool.eip7702 = pool.chainconfig.IsViridian(next, newHead.Time)
+ pool.jade = pool.chainconfig.IsJadeFork(newHead.Time)
+
+ // Remove MorphTx V1 transactions if jade fork is not active (e.g. after reorg)
+ if !pool.jade {
+ pool.removeMorphTxV1()
+ }
// Update current head
pool.currentHead = next
@@ -1617,7 +1650,16 @@ func (pool *TxPool) promoteExecutables(accounts []common.Address) []*types.Trans
func (pool *TxPool) executableTxFilter(addr common.Address, costLimit *big.Int, altCostLimit map[uint16]*big.Int) func(tx *types.Transaction) bool {
return func(tx *types.Transaction) bool {
- if !tx.IsAltFeeTx() && (tx.Gas() > pool.currentMaxGas || tx.Cost().Cmp(costLimit) > 0) {
+ // Calculate cost based on transaction type
+ var txCost *big.Int
+ if tx.IsMorphTxWithAltFee() {
+ // MorphTx with alt fee is handled separately
+ txCost = nil
+ } else {
+ txCost = tx.Cost()
+ }
+
+ if txCost != nil && (tx.Gas() > pool.currentMaxGas || txCost.Cmp(costLimit) > 0) {
return true
}
if pool.chainconfig.Morph.FeeVaultEnabled() {
@@ -1625,14 +1667,14 @@ func (pool *TxPool) executableTxFilter(addr common.Address, costLimit *big.Int,
l1DataFee, err := fees.CalculateL1DataFee(tx, pool.currentState, pool.chainconfig, pool.currentHead)
if err != nil {
log.Error("Failed to calculate L1 data fee", "err", err, "tx", tx)
- return false
+ return true
}
- if tx.IsAltFeeTx() {
+ if tx.IsMorphTxWithAltFee() {
if altCostLimit[tx.FeeTokenID()] == nil {
balance, err := pool.getBalanceFunc(pool.chain.CurrentBlock().Header(), pool.currentState, tx.FeeTokenID(), addr)
if err != nil || balance == nil {
log.Error("Failed to query balance", "err", err, "tx", tx)
- return false
+ return true
}
// account cost limit
altCostLimit[tx.FeeTokenID()] = balance
@@ -1640,7 +1682,7 @@ func (pool *TxPool) executableTxFilter(addr common.Address, costLimit *big.Int,
altAmount, err := fees.EthToAlt(pool.currentState, tx.FeeTokenID(), new(big.Int).Add(tx.GasFee(), l1DataFee))
if err != nil {
log.Error("Failed to swap to erc20", "err", err, "tx", tx)
- return false
+ return true
}
limit := altCostLimit[tx.FeeTokenID()]
if tx.FeeLimit() != nil && tx.FeeLimit().Sign() > 0 {
diff --git a/core/tx_pool_test.go b/core/tx_pool_test.go
index 26bde89dc..432fc06b4 100644
--- a/core/tx_pool_test.go
+++ b/core/tx_pool_test.go
@@ -54,6 +54,12 @@ var (
// eip1559NoL1feeConfig is a chain config with EIP-1559 enabled at block 0 but not enabling L1fee.
eip1559NoL1feeConfig *params.ChainConfig
+
+ // morphTxConfig is a chain config with Emerald fork enabled (supports MorphTx).
+ morphTxConfig *params.ChainConfig
+
+ // noEmeraldConfig is a chain config without Emerald fork (MorphTx not supported).
+ noEmeraldConfig *params.ChainConfig
)
func init() {
@@ -72,6 +78,21 @@ func init() {
eip1559NoL1feeConfig = &cpy2
eip1559NoL1feeConfig.BerlinBlock = common.Big0
eip1559NoL1feeConfig.LondonBlock = common.Big0
+
+ // MorphTx config with Emerald and Jade fork enabled (supports MorphTx V0 and V1)
+ cpy3 := *params.TestChainConfig
+ morphTxConfig = &cpy3
+ morphTxConfig.BerlinBlock = common.Big0
+ morphTxConfig.LondonBlock = common.Big0
+ morphTxConfig.EmeraldTime = new(uint64) // Enable Emerald fork at time 0
+ morphTxConfig.JadeForkTime = new(uint64) // Enable Jade fork at time 0
+
+ // Config without Emerald fork (for testing MorphTx rejection)
+ cpy4 := *params.TestChainConfig
+ noEmeraldConfig = &cpy4
+ noEmeraldConfig.BerlinBlock = common.Big0
+ noEmeraldConfig.LondonBlock = common.Big0
+ noEmeraldConfig.EmeraldTime = nil // Disable Emerald fork
}
type testBlockChain struct {
@@ -130,6 +151,71 @@ func dynamicFeeTx(nonce uint64, gaslimit uint64, gasFee *big.Int, tip *big.Int,
return tx
}
+// morphTxV0 creates a MorphTx Version 0 (legacy format with alt fee).
+// V0 requires FeeTokenID > 0.
+func morphTxV0(nonce uint64, gaslimit uint64, gasFee *big.Int, tip *big.Int, feeTokenID uint16, key *ecdsa.PrivateKey) *types.Transaction {
+ tx, _ := types.SignNewTx(key, types.LatestSignerForChainID(params.TestChainConfig.ChainID), &types.MorphTx{
+ ChainID: params.TestChainConfig.ChainID,
+ Nonce: nonce,
+ GasTipCap: tip,
+ GasFeeCap: gasFee,
+ Gas: gaslimit,
+ To: &common.Address{},
+ Value: big.NewInt(100),
+ Data: nil,
+ AccessList: nil,
+ FeeTokenID: feeTokenID,
+ FeeLimit: big.NewInt(1000000),
+ Version: types.MorphTxVersion0,
+ })
+ return tx
+}
+
+// morphTxV1 creates a MorphTx Version 1 (with Reference and Memo).
+// V1 allows FeeTokenID = 0.
+func morphTxV1(nonce uint64, gaslimit uint64, gasFee *big.Int, tip *big.Int, key *ecdsa.PrivateKey) *types.Transaction {
+ ref := common.HexToReference("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")
+ memo := []byte("test memo")
+ tx, _ := types.SignNewTx(key, types.LatestSignerForChainID(params.TestChainConfig.ChainID), &types.MorphTx{
+ ChainID: params.TestChainConfig.ChainID,
+ Nonce: nonce,
+ GasTipCap: tip,
+ GasFeeCap: gasFee,
+ Gas: gaslimit,
+ To: &common.Address{},
+ Value: big.NewInt(100),
+ Data: nil,
+ AccessList: nil,
+ FeeTokenID: 0,
+ FeeLimit: big.NewInt(0),
+ Version: types.MorphTxVersion1,
+ Reference: &ref,
+ Memo: &memo,
+ })
+ return tx
+}
+
+// morphTxV1WithMemo creates a MorphTx Version 1 with custom memo.
+func morphTxV1WithMemo(nonce uint64, gaslimit uint64, gasFee *big.Int, tip *big.Int, memo []byte, key *ecdsa.PrivateKey) *types.Transaction {
+ ref := common.HexToReference("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")
+ tx, _ := types.SignNewTx(key, types.LatestSignerForChainID(params.TestChainConfig.ChainID), &types.MorphTx{
+ ChainID: params.TestChainConfig.ChainID,
+ Nonce: nonce,
+ GasTipCap: tip,
+ GasFeeCap: gasFee,
+ Gas: gaslimit,
+ To: &common.Address{},
+ Value: big.NewInt(100),
+ Data: nil,
+ AccessList: nil,
+ FeeTokenID: 0,
+ Version: types.MorphTxVersion1,
+ Reference: &ref,
+ Memo: &memo,
+ })
+ return tx
+}
+
func setupTxPool() (*TxPool, *ecdsa.PrivateKey) {
return setupTxPoolWithConfig(params.TestChainConfig)
}
@@ -2602,3 +2688,488 @@ func TestPoolPending(t *testing.T) {
maxAccounts := 10
assert.Len(t, pool.PendingWithMax(nil, nil, maxAccounts), maxAccounts)
}
+
+// ==================== MorphTx Tests ====================
+
+// TestMorphTxValidation tests MorphTx validation in the transaction pool.
+func TestMorphTxValidation(t *testing.T) {
+ t.Parallel()
+
+ t.Run("MemoTooLong", func(t *testing.T) {
+ t.Parallel()
+
+ pool, key := setupTxPoolWithConfig(morphTxConfig)
+ defer pool.Stop()
+
+ account := crypto.PubkeyToAddress(key.PublicKey)
+ testAddBalance(pool, account, big.NewInt(1000000000))
+
+ // Memo exceeds max length (64 bytes)
+ longMemo := make([]byte, common.MaxMemoLength+1)
+ tx := morphTxV1WithMemo(0, 100000, big.NewInt(10), big.NewInt(1), longMemo, key)
+ if err := pool.AddRemote(tx); !errors.Is(err, types.ErrMemoTooLong) {
+ t.Errorf("expected ErrMemoTooLong, got %v", err)
+ }
+ })
+
+ t.Run("MemoMaxLength", func(t *testing.T) {
+ t.Parallel()
+
+ pool, key := setupTxPoolWithConfig(morphTxConfig)
+ defer pool.Stop()
+
+ account := crypto.PubkeyToAddress(key.PublicKey)
+ testAddBalance(pool, account, big.NewInt(1000000000))
+
+ // Memo at max length (64 bytes) should be accepted
+ maxMemo := make([]byte, common.MaxMemoLength)
+ tx := morphTxV1WithMemo(0, 100000, big.NewInt(10), big.NewInt(1), maxMemo, key)
+ if err := pool.AddRemote(tx); err != nil {
+ t.Errorf("expected no error for max memo length, got %v", err)
+ }
+ })
+
+ t.Run("V0RequiresFeeTokenID", func(t *testing.T) {
+ t.Parallel()
+
+ pool, key := setupTxPoolWithConfig(morphTxConfig)
+ defer pool.Stop()
+
+ account := crypto.PubkeyToAddress(key.PublicKey)
+ testAddBalance(pool, account, big.NewInt(1000000000))
+
+ // V0 with FeeTokenID = 0 should be rejected
+ tx := morphTxV0(0, 100000, big.NewInt(10), big.NewInt(1), 0, key)
+ if err := pool.AddRemote(tx); !errors.Is(err, types.ErrMorphTxV0IllegalExtraParams) {
+ t.Errorf("expected ErrMorphTxV0IllegalExtraParams, got %v", err)
+ }
+ })
+
+ t.Run("V0WithFeeTokenID", func(t *testing.T) {
+ t.Parallel()
+
+ pool, key := setupTxPoolWithConfig(morphTxConfig)
+ defer pool.Stop()
+
+ account := crypto.PubkeyToAddress(key.PublicKey)
+ testAddBalance(pool, account, big.NewInt(1000000000))
+
+ // V0 with FeeTokenID > 0 - this will fail due to token not being active,
+ // but it should pass version validation
+ tx := morphTxV0(0, 100000, big.NewInt(10), big.NewInt(1), 1, key)
+ err := pool.AddRemote(tx)
+ // Should not be version error (but may be token error)
+ if errors.Is(err, types.ErrMorphTxV0IllegalExtraParams) {
+ t.Errorf("V0 with FeeTokenID > 0 should pass version validation, got %v", err)
+ }
+ })
+
+ t.Run("UnsupportedVersion", func(t *testing.T) {
+ t.Parallel()
+
+ pool, key := setupTxPoolWithConfig(morphTxConfig)
+ defer pool.Stop()
+
+ account := crypto.PubkeyToAddress(key.PublicKey)
+ testAddBalance(pool, account, big.NewInt(1000000000))
+
+ // Create a MorphTx with unsupported version
+ ref := common.HexToReference("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")
+ memo := []byte("test")
+ tx, _ := types.SignNewTx(key, types.LatestSignerForChainID(params.TestChainConfig.ChainID), &types.MorphTx{
+ ChainID: params.TestChainConfig.ChainID,
+ Nonce: 0,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 100000,
+ To: &common.Address{},
+ Value: big.NewInt(100),
+ FeeTokenID: 0,
+ Version: 2, // Unsupported version
+ Reference: &ref,
+ Memo: &memo,
+ })
+ if err := pool.AddRemote(tx); !errors.Is(err, types.ErrMorphTxUnsupportedVersion) {
+ t.Errorf("expected ErrMorphTxUnsupportedVersion, got %v", err)
+ }
+ })
+
+ t.Run("V0WithReference", func(t *testing.T) {
+ t.Parallel()
+
+ pool, key := setupTxPoolWithConfig(morphTxConfig)
+ defer pool.Stop()
+
+ account := crypto.PubkeyToAddress(key.PublicKey)
+ testAddBalance(pool, account, big.NewInt(1000000000))
+
+ // V0 with Reference should be rejected
+ ref := common.HexToReference("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")
+ tx, _ := types.SignNewTx(key, types.LatestSignerForChainID(params.TestChainConfig.ChainID), &types.MorphTx{
+ ChainID: params.TestChainConfig.ChainID,
+ Nonce: 0,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 100000,
+ To: &common.Address{},
+ Value: big.NewInt(100),
+ FeeTokenID: 1,
+ Version: types.MorphTxVersion0,
+ Reference: &ref,
+ })
+ if err := pool.AddRemote(tx); !errors.Is(err, types.ErrMorphTxV0IllegalExtraParams) {
+ t.Errorf("expected ErrMorphTxV0IllegalExtraParams, got %v", err)
+ }
+ })
+
+ t.Run("V0WithMemo", func(t *testing.T) {
+ t.Parallel()
+
+ pool, key := setupTxPoolWithConfig(morphTxConfig)
+ defer pool.Stop()
+
+ account := crypto.PubkeyToAddress(key.PublicKey)
+ testAddBalance(pool, account, big.NewInt(1000000000))
+
+ // V0 with Memo should be rejected
+ memo := []byte("test memo")
+ tx, _ := types.SignNewTx(key, types.LatestSignerForChainID(params.TestChainConfig.ChainID), &types.MorphTx{
+ ChainID: params.TestChainConfig.ChainID,
+ Nonce: 0,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 100000,
+ To: &common.Address{},
+ Value: big.NewInt(100),
+ FeeTokenID: 1,
+ Version: types.MorphTxVersion0,
+ Memo: &memo,
+ })
+ if err := pool.AddRemote(tx); !errors.Is(err, types.ErrMorphTxV0IllegalExtraParams) {
+ t.Errorf("expected ErrMorphTxV0IllegalExtraParams, got %v", err)
+ }
+ })
+
+ t.Run("V1WithFeeLimitButNoFeeToken", func(t *testing.T) {
+ t.Parallel()
+
+ pool, key := setupTxPoolWithConfig(morphTxConfig)
+ defer pool.Stop()
+
+ account := crypto.PubkeyToAddress(key.PublicKey)
+ testAddBalance(pool, account, big.NewInt(1000000000))
+
+ // V1 with FeeLimit but FeeTokenID=0 should be rejected
+ ref := common.HexToReference("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")
+ memo := []byte("test memo")
+ tx, _ := types.SignNewTx(key, types.LatestSignerForChainID(params.TestChainConfig.ChainID), &types.MorphTx{
+ ChainID: params.TestChainConfig.ChainID,
+ Nonce: 0,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 100000,
+ To: &common.Address{},
+ Value: big.NewInt(100),
+ FeeTokenID: 0,
+ FeeLimit: big.NewInt(1000000), // FeeLimit set but no FeeToken
+ Version: types.MorphTxVersion1,
+ Reference: &ref,
+ Memo: &memo,
+ })
+ if err := pool.AddRemote(tx); !errors.Is(err, types.ErrMorphTxV1IllegalExtraParams) {
+ t.Errorf("expected ErrMorphTxV1IllegalExtraParams, got %v", err)
+ }
+ })
+}
+
+// TestMorphTxEIP1559Activation tests that MorphTx is rejected before Emerald fork.
+func TestMorphTxEIP1559Activation(t *testing.T) {
+ t.Parallel()
+
+ t.Run("RejectBeforeEmerald", func(t *testing.T) {
+ t.Parallel()
+
+ pool, key := setupTxPoolWithConfig(noEmeraldConfig)
+ defer pool.Stop()
+
+ account := crypto.PubkeyToAddress(key.PublicKey)
+ testAddBalance(pool, account, big.NewInt(1000000000))
+
+ tx := morphTxV1(0, 100000, big.NewInt(10), big.NewInt(1), key)
+ err := pool.AddRemote(tx)
+ // Before Emerald, the signer doesn't support MorphTx, so we get either
+ // ErrTxTypeNotSupported or invalid sender error (signer check happens first)
+ if err == nil {
+ t.Error("expected MorphTx to be rejected before Emerald")
+ }
+ })
+
+ t.Run("AcceptAfterEmerald", func(t *testing.T) {
+ t.Parallel()
+
+ pool, key := setupTxPoolWithConfig(morphTxConfig)
+ defer pool.Stop()
+
+ account := crypto.PubkeyToAddress(key.PublicKey)
+ testAddBalance(pool, account, big.NewInt(1000000000))
+
+ tx := morphTxV1(0, 100000, big.NewInt(10), big.NewInt(1), key)
+ if err := pool.AddRemote(tx); err != nil {
+ t.Errorf("expected MorphTx to be accepted after Emerald, got %v", err)
+ }
+ })
+}
+
+// TestMorphTxPoolManagement tests MorphTx behavior in transaction pool.
+func TestMorphTxPoolManagement(t *testing.T) {
+ t.Parallel()
+
+ t.Run("PendingAndQueued", func(t *testing.T) {
+ t.Parallel()
+
+ pool, key := setupTxPoolWithConfig(morphTxConfig)
+ defer pool.Stop()
+
+ account := crypto.PubkeyToAddress(key.PublicKey)
+ testAddBalance(pool, account, big.NewInt(1000000000))
+
+ // Add executable MorphTx (nonce 0)
+ tx0 := morphTxV1(0, 100000, big.NewInt(10), big.NewInt(1), key)
+ if err := pool.addRemoteSync(tx0); err != nil {
+ t.Fatalf("failed to add tx0: %v", err)
+ }
+
+ // Add queued MorphTx (nonce 2, gap at nonce 1)
+ tx2 := morphTxV1(2, 100000, big.NewInt(10), big.NewInt(1), key)
+ if err := pool.addRemoteSync(tx2); err != nil {
+ t.Fatalf("failed to add tx2: %v", err)
+ }
+
+ pending, queued := pool.Stats()
+ if pending != 1 {
+ t.Errorf("pending mismatch: have %d, want 1", pending)
+ }
+ if queued != 1 {
+ t.Errorf("queued mismatch: have %d, want 1", queued)
+ }
+
+ // Fill the gap
+ tx1 := morphTxV1(1, 100000, big.NewInt(10), big.NewInt(1), key)
+ if err := pool.addRemoteSync(tx1); err != nil {
+ t.Fatalf("failed to add tx1: %v", err)
+ }
+
+ pending, queued = pool.Stats()
+ if pending != 3 {
+ t.Errorf("after gap fill pending mismatch: have %d, want 3", pending)
+ }
+ if queued != 0 {
+ t.Errorf("after gap fill queued mismatch: have %d, want 0", queued)
+ }
+ })
+
+ t.Run("Replacement", func(t *testing.T) {
+ t.Parallel()
+
+ pool, key := setupTxPoolWithConfig(morphTxConfig)
+ defer pool.Stop()
+
+ account := crypto.PubkeyToAddress(key.PublicKey)
+ testAddBalance(pool, account, big.NewInt(1000000000))
+
+ // Add initial MorphTx
+ tx1 := morphTxV1(0, 100000, big.NewInt(10), big.NewInt(1), key)
+ if err := pool.addRemoteSync(tx1); err != nil {
+ t.Fatalf("failed to add tx1: %v", err)
+ }
+
+ // Try to replace with same gas price but different gas limit (should fail)
+ tx2 := morphTxV1(0, 200000, big.NewInt(10), big.NewInt(1), key)
+ if err := pool.addRemoteSync(tx2); err == nil {
+ t.Error("expected rejection for same-price replacement")
+ }
+
+ // Replace with higher gas price (10% bump required)
+ tx3 := morphTxV1(0, 100000, big.NewInt(12), big.NewInt(2), key)
+ if err := pool.addRemoteSync(tx3); err != nil {
+ t.Errorf("expected acceptance for higher-price replacement, got %v", err)
+ }
+
+ pending, _ := pool.Stats()
+ if pending != 1 {
+ t.Errorf("pending mismatch after replacement: have %d, want 1", pending)
+ }
+ })
+}
+
+// TestMorphTxMixedTransactions tests pool with mixed transaction types.
+func TestMorphTxMixedTransactions(t *testing.T) {
+ t.Parallel()
+
+ pool, key := setupTxPoolWithConfig(morphTxConfig)
+ defer pool.Stop()
+
+ account := crypto.PubkeyToAddress(key.PublicKey)
+ testAddBalance(pool, account, big.NewInt(10000000000))
+
+ // Add legacy transaction
+ tx0 := transaction(0, 100000, key)
+ if err := pool.addRemoteSync(tx0); err != nil {
+ t.Fatalf("failed to add legacy tx: %v", err)
+ }
+
+ // Add dynamic fee transaction
+ tx1 := dynamicFeeTx(1, 100000, big.NewInt(10), big.NewInt(1), key)
+ if err := pool.addRemoteSync(tx1); err != nil {
+ t.Fatalf("failed to add dynamic fee tx: %v", err)
+ }
+
+ // Add MorphTx V1
+ tx2 := morphTxV1(2, 100000, big.NewInt(10), big.NewInt(1), key)
+ if err := pool.addRemoteSync(tx2); err != nil {
+ t.Fatalf("failed to add MorphTx V1: %v", err)
+ }
+
+ pending, queued := pool.Stats()
+ if pending != 3 {
+ t.Errorf("pending mismatch: have %d, want 3", pending)
+ }
+ if queued != 0 {
+ t.Errorf("queued mismatch: have %d, want 0", queued)
+ }
+
+ if err := validateTxPoolInternals(pool); err != nil {
+ t.Fatalf("pool internal state corrupted: %v", err)
+ }
+}
+
+// TestMorphTxAccessors tests MorphTx accessor methods.
+func TestMorphTxAccessors(t *testing.T) {
+ t.Parallel()
+
+ key, _ := crypto.GenerateKey()
+
+ t.Run("V0Accessors", func(t *testing.T) {
+ tx := morphTxV0(0, 100000, big.NewInt(10), big.NewInt(1), 1, key)
+ if !tx.IsMorphTx() {
+ t.Error("IsMorphTx should return true")
+ }
+ if !tx.IsMorphTxWithAltFee() {
+ t.Error("IsMorphTxWithAltFee should return true for FeeTokenID > 0")
+ }
+ if tx.Version() != types.MorphTxVersion0 {
+ t.Errorf("Version mismatch: have %d, want %d", tx.Version(), types.MorphTxVersion0)
+ }
+ if tx.FeeTokenID() != 1 {
+ t.Errorf("FeeTokenID mismatch: have %d, want 1", tx.FeeTokenID())
+ }
+ })
+
+ t.Run("V1Accessors", func(t *testing.T) {
+ tx := morphTxV1(0, 100000, big.NewInt(10), big.NewInt(1), key)
+ if !tx.IsMorphTx() {
+ t.Error("IsMorphTx should return true")
+ }
+ if tx.IsMorphTxWithAltFee() {
+ t.Error("IsMorphTxWithAltFee should return false for FeeTokenID = 0")
+ }
+ if tx.Version() != types.MorphTxVersion1 {
+ t.Errorf("Version mismatch: have %d, want %d", tx.Version(), types.MorphTxVersion1)
+ }
+ if tx.Reference() == nil {
+ t.Error("Reference should not be nil")
+ }
+ if tx.Memo() == nil {
+ t.Error("Memo should not be nil")
+ }
+ })
+}
+
+// TestMorphTxV1JadeForkGating tests that MorphTx V1 is rejected before the jade
+// fork and accepted after the jade fork, while V0 is always accepted when Emerald
+// is active.
+func TestMorphTxV1JadeForkGating(t *testing.T) {
+ t.Parallel()
+
+ // Config with Emerald enabled but Jade NOT enabled (no JadeForkTime).
+ preJadeConfig := func() *params.ChainConfig {
+ cpy := *params.TestChainConfig
+ cfg := &cpy
+ cfg.BerlinBlock = common.Big0
+ cfg.LondonBlock = common.Big0
+ cfg.EmeraldTime = new(uint64) // Emerald at time 0
+ cfg.JadeForkTime = nil // Jade not active
+ return cfg
+ }()
+
+ t.Run("V1RejectedBeforeJadeFork", func(t *testing.T) {
+ t.Parallel()
+ pool, key := setupTxPoolWithConfig(preJadeConfig)
+ defer pool.Stop()
+
+ account := crypto.PubkeyToAddress(key.PublicKey)
+ testAddBalance(pool, account, big.NewInt(1000000000))
+
+ tx := morphTxV1(0, 100000, big.NewInt(10), big.NewInt(1), key)
+ if err := pool.AddRemote(tx); !errors.Is(err, types.ErrMorphTxV1NotYetActive) {
+ t.Errorf("expected ErrMorphTxV1NotYetActive, got %v", err)
+ }
+ })
+
+ t.Run("V0AcceptedBeforeJadeFork", func(t *testing.T) {
+ t.Parallel()
+ pool, key := setupTxPoolWithConfig(preJadeConfig)
+ defer pool.Stop()
+
+ account := crypto.PubkeyToAddress(key.PublicKey)
+ testAddBalance(pool, account, big.NewInt(1000000000))
+
+ // V0 with FeeTokenID > 0 should still pass version validation
+ tx := morphTxV0(0, 100000, big.NewInt(10), big.NewInt(1), 1, key)
+ err := pool.AddRemote(tx)
+ // Should NOT be the jade fork error or version error
+ if errors.Is(err, types.ErrMorphTxV1NotYetActive) {
+ t.Errorf("V0 should not be rejected by jade fork check, got %v", err)
+ }
+ if errors.Is(err, types.ErrMorphTxV0IllegalExtraParams) {
+ t.Errorf("V0 with FeeTokenID > 0 should pass version validation, got %v", err)
+ }
+ })
+
+ t.Run("V1AcceptedAfterJadeFork", func(t *testing.T) {
+ t.Parallel()
+ // Use morphTxConfig which has JadeForkTime enabled at time 0
+ pool, key := setupTxPoolWithConfig(morphTxConfig)
+ defer pool.Stop()
+
+ account := crypto.PubkeyToAddress(key.PublicKey)
+ testAddBalance(pool, account, big.NewInt(1000000000))
+
+ tx := morphTxV1(0, 100000, big.NewInt(10), big.NewInt(1), key)
+ if err := pool.AddRemote(tx); err != nil {
+ t.Errorf("V1 should be accepted after jade fork, got %v", err)
+ }
+ })
+
+ t.Run("V0AndV1BothAcceptedAfterJadeFork", func(t *testing.T) {
+ t.Parallel()
+ pool, key := setupTxPoolWithConfig(morphTxConfig)
+ defer pool.Stop()
+
+ account := crypto.PubkeyToAddress(key.PublicKey)
+ testAddBalance(pool, account, big.NewInt(1000000000))
+
+ // V1 should be accepted
+ txV1 := morphTxV1(0, 100000, big.NewInt(10), big.NewInt(1), key)
+ if err := pool.AddRemote(txV1); err != nil {
+ t.Errorf("V1 should be accepted after jade fork, got %v", err)
+ }
+
+ // V0 with FeeTokenID > 0 should also pass version validation
+ txV0 := morphTxV0(1, 100000, big.NewInt(10), big.NewInt(1), 1, key)
+ err := pool.AddRemote(txV0)
+ if errors.Is(err, types.ErrMorphTxV1NotYetActive) || errors.Is(err, types.ErrMorphTxV0IllegalExtraParams) {
+ t.Errorf("V0 should be accepted after jade fork, got %v", err)
+ }
+ })
+}
diff --git a/core/types/alt_fee_tx.go b/core/types/alt_fee_tx.go
deleted file mode 100644
index 28c06dfd3..000000000
--- a/core/types/alt_fee_tx.go
+++ /dev/null
@@ -1,149 +0,0 @@
-package types
-
-import (
- "bytes"
- "math/big"
-
- "github.com/morph-l2/go-ethereum/common"
- "github.com/morph-l2/go-ethereum/rlp"
-)
-
-// Copyright 2021 The go-ethereum Authors
-// This file is part of the go-ethereum library.
-//
-// The go-ethereum library is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Lesser General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// The go-ethereum library is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Lesser General Public License for more details.
-//
-// You should have received a copy of the GNU Lesser General Public License
-// along with the go-ethereum library. If not, see .
-
-type AltFeeTx struct {
- ChainID *big.Int
- Nonce uint64
- GasTipCap *big.Int
- GasFeeCap *big.Int
- Gas uint64
- To *common.Address `rlp:"nil"` // nil means contract creation
- Value *big.Int
- Data []byte
- AccessList AccessList
-
- FeeTokenID uint16
- FeeLimit *big.Int
-
- // Signature values
- V *big.Int `json:"v" gencodec:"required"`
- R *big.Int `json:"r" gencodec:"required"`
- S *big.Int `json:"s" gencodec:"required"`
-}
-
-// copy creates a deep copy of the transaction data and initializes all fields.
-func (tx *AltFeeTx) copy() TxData {
- cpy := &AltFeeTx{
- Nonce: tx.Nonce,
- To: copyAddressPtr(tx.To),
- Data: common.CopyBytes(tx.Data),
- Gas: tx.Gas,
- FeeTokenID: tx.FeeTokenID,
- // These are copied below.
- AccessList: make(AccessList, len(tx.AccessList)),
- Value: new(big.Int),
- ChainID: new(big.Int),
- GasTipCap: new(big.Int),
- GasFeeCap: new(big.Int),
- V: new(big.Int),
- R: new(big.Int),
- S: new(big.Int),
- }
- copy(cpy.AccessList, tx.AccessList)
- if tx.Value != nil {
- cpy.Value.Set(tx.Value)
- }
- if tx.ChainID != nil {
- cpy.ChainID.Set(tx.ChainID)
- }
- if tx.GasTipCap != nil {
- cpy.GasTipCap.Set(tx.GasTipCap)
- }
- if tx.GasFeeCap != nil {
- cpy.GasFeeCap.Set(tx.GasFeeCap)
- }
- if tx.FeeLimit != nil {
- cpy.FeeLimit = new(big.Int).Set(tx.FeeLimit)
- }
- if tx.V != nil {
- cpy.V.Set(tx.V)
- }
- if tx.R != nil {
- cpy.R.Set(tx.R)
- }
- if tx.S != nil {
- cpy.S.Set(tx.S)
- }
- return cpy
-}
-
-// accessors for innerTx.
-func (tx *AltFeeTx) txType() byte { return AltFeeTxType }
-func (tx *AltFeeTx) chainID() *big.Int { return tx.ChainID }
-func (tx *AltFeeTx) accessList() AccessList { return tx.AccessList }
-func (tx *AltFeeTx) data() []byte { return tx.Data }
-func (tx *AltFeeTx) gas() uint64 { return tx.Gas }
-func (tx *AltFeeTx) gasFeeCap() *big.Int { return tx.GasFeeCap }
-func (tx *AltFeeTx) gasTipCap() *big.Int { return tx.GasTipCap }
-func (tx *AltFeeTx) gasPrice() *big.Int { return tx.GasFeeCap }
-func (tx *AltFeeTx) value() *big.Int { return tx.Value }
-func (tx *AltFeeTx) nonce() uint64 { return tx.Nonce }
-func (tx *AltFeeTx) to() *common.Address { return tx.To }
-
-func (tx *AltFeeTx) effectiveGasPrice(dst *big.Int, baseFee *big.Int) *big.Int {
- if baseFee == nil {
- return dst.Set(tx.GasFeeCap)
- }
- tip := dst.Sub(tx.GasFeeCap, baseFee)
- if tip.Cmp(tx.GasTipCap) > 0 {
- tip.Set(tx.GasTipCap)
- }
- return tip.Add(tip, baseFee)
-}
-
-func (tx *AltFeeTx) rawSignatureValues() (v, r, s *big.Int) {
- return tx.V, tx.R, tx.S
-}
-
-func (tx *AltFeeTx) setSignatureValues(chainID, v, r, s *big.Int) {
- tx.ChainID, tx.V, tx.R, tx.S = chainID, v, r, s
-}
-
-func (tx *AltFeeTx) encode(b *bytes.Buffer) error {
- return rlp.Encode(b, tx)
-}
-
-func (tx *AltFeeTx) decode(input []byte) error {
- return rlp.DecodeBytes(input, tx)
-}
-
-func (tx *AltFeeTx) sigHash(chainID *big.Int) common.Hash {
- return prefixedRlpHash(
- AltFeeTxType,
- []any{
- chainID,
- tx.Nonce,
- tx.GasTipCap,
- tx.GasFeeCap,
- tx.Gas,
- tx.To,
- tx.Value,
- tx.Data,
- tx.AccessList,
- tx.FeeTokenID,
- tx.FeeLimit,
- })
-}
diff --git a/core/types/gen_receipt_json.go b/core/types/gen_receipt_json.go
index 34b44dd3c..932c7fa1d 100644
--- a/core/types/gen_receipt_json.go
+++ b/core/types/gen_receipt_json.go
@@ -16,27 +16,30 @@ var _ = (*receiptMarshaling)(nil)
// MarshalJSON marshals as JSON.
func (r Receipt) MarshalJSON() ([]byte, error) {
type Receipt struct {
- Type hexutil.Uint64 `json:"type,omitempty"`
- PostState hexutil.Bytes `json:"root"`
- Status hexutil.Uint64 `json:"status"`
- CumulativeGasUsed hexutil.Uint64 `json:"cumulativeGasUsed" gencodec:"required"`
- Bloom Bloom `json:"logsBloom" gencodec:"required"`
- Logs []*Log `json:"logs" gencodec:"required"`
- TxHash common.Hash `json:"transactionHash" gencodec:"required"`
- ContractAddress common.Address `json:"contractAddress"`
- GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"`
- EffectiveGasPrice *hexutil.Big `json:"effectiveGasPrice"`
- BlobGasUsed hexutil.Uint64 `json:"blobGasUsed,omitempty"`
- BlobGasPrice *hexutil.Big `json:"blobGasPrice,omitempty"`
- BlockHash common.Hash `json:"blockHash,omitempty"`
- BlockNumber *hexutil.Big `json:"blockNumber,omitempty"`
- TransactionIndex hexutil.Uint `json:"transactionIndex"`
- ReturnValue []byte `json:"returnValue,omitempty"`
- L1Fee *hexutil.Big `json:"l1Fee,omitempty"`
- FeeTokenID *uint16 `json:"feeTokenID,omitempty"`
- FeeRate *hexutil.Big `json:"feeRate,omitempty"`
- TokenScale *hexutil.Big `json:"tokenScale,omitempty"`
- FeeLimit *hexutil.Big `json:"feeLimit,omitempty"`
+ Type hexutil.Uint64 `json:"type,omitempty"`
+ PostState hexutil.Bytes `json:"root"`
+ Status hexutil.Uint64 `json:"status"`
+ CumulativeGasUsed hexutil.Uint64 `json:"cumulativeGasUsed" gencodec:"required"`
+ Bloom Bloom `json:"logsBloom" gencodec:"required"`
+ Logs []*Log `json:"logs" gencodec:"required"`
+ TxHash common.Hash `json:"transactionHash" gencodec:"required"`
+ ContractAddress common.Address `json:"contractAddress"`
+ GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"`
+ EffectiveGasPrice *hexutil.Big `json:"effectiveGasPrice"`
+ BlobGasUsed hexutil.Uint64 `json:"blobGasUsed,omitempty"`
+ BlobGasPrice *hexutil.Big `json:"blobGasPrice,omitempty"`
+ BlockHash common.Hash `json:"blockHash,omitempty"`
+ BlockNumber *hexutil.Big `json:"blockNumber,omitempty"`
+ TransactionIndex hexutil.Uint `json:"transactionIndex"`
+ ReturnValue []byte `json:"returnValue,omitempty"`
+ L1Fee *hexutil.Big `json:"l1Fee,omitempty"`
+ FeeTokenID *hexutil.Uint16 `json:"feeTokenID,omitempty"`
+ FeeRate *hexutil.Big `json:"feeRate,omitempty"`
+ TokenScale *hexutil.Big `json:"tokenScale,omitempty"`
+ FeeLimit *hexutil.Big `json:"feeLimit,omitempty"`
+ Version hexutil.Uint64 `json:"version,omitempty"`
+ Reference *common.Reference `json:"reference,omitempty"`
+ Memo *hexutil.Bytes `json:"memo,omitempty"`
}
var enc Receipt
enc.Type = hexutil.Uint64(r.Type)
@@ -56,37 +59,43 @@ func (r Receipt) MarshalJSON() ([]byte, error) {
enc.TransactionIndex = hexutil.Uint(r.TransactionIndex)
enc.ReturnValue = r.ReturnValue
enc.L1Fee = (*hexutil.Big)(r.L1Fee)
- enc.FeeTokenID = r.FeeTokenID
+ enc.FeeTokenID = (*hexutil.Uint16)(r.FeeTokenID)
enc.FeeRate = (*hexutil.Big)(r.FeeRate)
enc.TokenScale = (*hexutil.Big)(r.TokenScale)
enc.FeeLimit = (*hexutil.Big)(r.FeeLimit)
+ enc.Version = hexutil.Uint64(r.Version)
+ enc.Reference = r.Reference
+ enc.Memo = (*hexutil.Bytes)(r.Memo)
return json.Marshal(&enc)
}
// UnmarshalJSON unmarshals from JSON.
func (r *Receipt) UnmarshalJSON(input []byte) error {
type Receipt struct {
- Type *hexutil.Uint64 `json:"type,omitempty"`
- PostState *hexutil.Bytes `json:"root"`
- Status *hexutil.Uint64 `json:"status"`
- CumulativeGasUsed *hexutil.Uint64 `json:"cumulativeGasUsed" gencodec:"required"`
- Bloom *Bloom `json:"logsBloom" gencodec:"required"`
- Logs []*Log `json:"logs" gencodec:"required"`
- TxHash *common.Hash `json:"transactionHash" gencodec:"required"`
- ContractAddress *common.Address `json:"contractAddress"`
- GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"`
- EffectiveGasPrice *hexutil.Big `json:"effectiveGasPrice"`
- BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed,omitempty"`
- BlobGasPrice *hexutil.Big `json:"blobGasPrice,omitempty"`
- BlockHash *common.Hash `json:"blockHash,omitempty"`
- BlockNumber *hexutil.Big `json:"blockNumber,omitempty"`
- TransactionIndex *hexutil.Uint `json:"transactionIndex"`
- ReturnValue []byte `json:"returnValue,omitempty"`
- L1Fee *hexutil.Big `json:"l1Fee,omitempty"`
- FeeTokenID *hexutil.Uint16 `json:"feeTokenID,omitempty"`
- FeeRate *hexutil.Big `json:"feeRate,omitempty"`
- TokenScale *hexutil.Big `json:"tokenScale,omitempty"`
- FeeLimit *hexutil.Big `json:"feeLimit,omitempty"`
+ Type *hexutil.Uint64 `json:"type,omitempty"`
+ PostState *hexutil.Bytes `json:"root"`
+ Status *hexutil.Uint64 `json:"status"`
+ CumulativeGasUsed *hexutil.Uint64 `json:"cumulativeGasUsed" gencodec:"required"`
+ Bloom *Bloom `json:"logsBloom" gencodec:"required"`
+ Logs []*Log `json:"logs" gencodec:"required"`
+ TxHash *common.Hash `json:"transactionHash" gencodec:"required"`
+ ContractAddress *common.Address `json:"contractAddress"`
+ GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"`
+ EffectiveGasPrice *hexutil.Big `json:"effectiveGasPrice"`
+ BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed,omitempty"`
+ BlobGasPrice *hexutil.Big `json:"blobGasPrice,omitempty"`
+ BlockHash *common.Hash `json:"blockHash,omitempty"`
+ BlockNumber *hexutil.Big `json:"blockNumber,omitempty"`
+ TransactionIndex *hexutil.Uint `json:"transactionIndex"`
+ ReturnValue []byte `json:"returnValue,omitempty"`
+ L1Fee *hexutil.Big `json:"l1Fee,omitempty"`
+ FeeTokenID *hexutil.Uint16 `json:"feeTokenID,omitempty"`
+ FeeRate *hexutil.Big `json:"feeRate,omitempty"`
+ TokenScale *hexutil.Big `json:"tokenScale,omitempty"`
+ FeeLimit *hexutil.Big `json:"feeLimit,omitempty"`
+ Version *hexutil.Uint64 `json:"version,omitempty"`
+ Reference *common.Reference `json:"reference,omitempty"`
+ Memo *hexutil.Bytes `json:"memo,omitempty"`
}
var dec Receipt
if err := json.Unmarshal(input, &dec); err != nil {
@@ -149,8 +158,7 @@ func (r *Receipt) UnmarshalJSON(input []byte) error {
r.L1Fee = (*big.Int)(dec.L1Fee)
}
if dec.FeeTokenID != nil {
- tokenID := uint16(*dec.FeeTokenID)
- r.FeeTokenID = &tokenID
+ r.FeeTokenID = (*uint16)(dec.FeeTokenID)
}
if dec.FeeRate != nil {
r.FeeRate = (*big.Int)(dec.FeeRate)
@@ -161,6 +169,14 @@ func (r *Receipt) UnmarshalJSON(input []byte) error {
if dec.FeeLimit != nil {
r.FeeLimit = (*big.Int)(dec.FeeLimit)
}
-
+ if dec.Version != nil {
+ r.Version = uint8(*dec.Version)
+ }
+ if dec.Reference != nil {
+ r.Reference = dec.Reference
+ }
+ if dec.Memo != nil {
+ r.Memo = (*[]byte)(dec.Memo)
+ }
return nil
}
diff --git a/core/types/l2trace.go b/core/types/l2trace.go
index decb681f7..80f620b85 100644
--- a/core/types/l2trace.go
+++ b/core/types/l2trace.go
@@ -49,14 +49,17 @@ type StorageTrace struct {
// while replaying a transaction in debug mode as well as transaction
// execution status, the amount of gas used and the return value
type ExecutionResult struct {
- L1DataFee *hexutil.Big `json:"l1DataFee,omitempty"`
- FeeTokenID *uint16 `json:"feeTokenID,omitempty"`
- FeeRate *hexutil.Big `json:"feeRate,omitempty"`
- TokenScale *hexutil.Big `json:"tokenScale,omitempty"`
- FeeLimit *hexutil.Big `json:"feeLimit,omitempty"`
- Gas uint64 `json:"gas"`
- Failed bool `json:"failed"`
- ReturnValue string `json:"returnValue"`
+ L1DataFee *hexutil.Big `json:"l1DataFee,omitempty"`
+ FeeTokenID *uint16 `json:"feeTokenID,omitempty"`
+ FeeRate *hexutil.Big `json:"feeRate,omitempty"`
+ TokenScale *hexutil.Big `json:"tokenScale,omitempty"`
+ FeeLimit *hexutil.Big `json:"feeLimit,omitempty"`
+ Version *uint8 `json:"version,omitempty"`
+ Reference *common.Reference `json:"reference,omitempty"`
+ Memo *hexutil.Bytes `json:"memo,omitempty"`
+ Gas uint64 `json:"gas"`
+ Failed bool `json:"failed"`
+ ReturnValue string `json:"returnValue"`
// Sender's account state (before Tx)
From *AccountWrapper `json:"from,omitempty"`
// Receiver's account state (before Tx)
@@ -133,6 +136,9 @@ type TransactionData struct {
GasFeeCap *hexutil.Big `json:"gasFeeCap"`
FeeTokenID *uint16 `json:"feeTokenID,omitempty"`
FeeLimit *hexutil.Big `json:"feeLimit,omitempty"`
+ Version uint8 `json:"version,omitempty"`
+ Reference *common.Reference `json:"reference,omitempty"`
+ Memo *hexutil.Bytes `json:"memo,omitempty"`
From common.Address `json:"from"`
To *common.Address `json:"to"`
ChainId *hexutil.Big `json:"chainId"`
@@ -188,8 +194,8 @@ func NewTransactionData(tx *Transaction, blockNumber uint64, blockTime uint64, c
S: (*hexutil.Big)(s),
}
- // Set FeeTokenID and FeeLimit for AltFeeTx
- if tx.Type() == AltFeeTxType {
+ // Set FeeTokenID and FeeLimit for MorphTx
+ if tx.Type() == MorphTxType {
feeTokenID := tx.FeeTokenID()
if feeTokenID != 0 {
result.FeeTokenID = &feeTokenID
@@ -197,6 +203,15 @@ func NewTransactionData(tx *Transaction, blockNumber uint64, blockTime uint64, c
if feeLimit := tx.FeeLimit(); feeLimit != nil && feeLimit.Sign() > 0 {
result.FeeLimit = (*hexutil.Big)(feeLimit)
}
+ // Only include V1 fields (version, reference, memo) for V1+ transactions
+ if tx.Version() >= MorphTxVersion1 {
+ result.Version = tx.Version()
+ result.Reference = tx.Reference()
+ if tx.Memo() != nil {
+ memo := hexutil.Bytes(*tx.Memo())
+ result.Memo = &memo
+ }
+ }
}
return result
diff --git a/core/types/morph_tx.go b/core/types/morph_tx.go
new file mode 100644
index 000000000..d0e7a906b
--- /dev/null
+++ b/core/types/morph_tx.go
@@ -0,0 +1,387 @@
+package types
+
+import (
+ "bytes"
+ "errors"
+ "io"
+ "math/big"
+ "strconv"
+
+ "github.com/morph-l2/go-ethereum/common"
+ "github.com/morph-l2/go-ethereum/rlp"
+)
+
+// Copyright 2021 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see .
+
+// MorphTx version constants
+const (
+ // MorphTxVersion0 is the original format without Version, Reference, Memo fields
+ MorphTxVersion0 = uint8(0)
+ // MorphTxVersion1 includes Version, Reference, Memo fields
+ MorphTxVersion1 = uint8(1)
+)
+
+type MorphTx struct {
+ ChainID *big.Int
+ Nonce uint64
+ GasTipCap *big.Int
+ GasFeeCap *big.Int
+ Gas uint64
+ To *common.Address `rlp:"nil"` // nil means contract creation
+ Value *big.Int
+ Data []byte
+ AccessList AccessList
+
+ Version uint8 // version of morph tx (0 = legacy, 1 = with reference/memo)
+ FeeTokenID uint16 // ERC20 token ID for fee payment (0 = ETH)
+ FeeLimit *big.Int // maximum fee in token units (optional)
+ Reference *common.Reference // reference key for the transaction (optional, v1 only)
+ Memo *[]byte // memo for the transaction (optional, v1 only)
+
+ // Signature values
+ V *big.Int `json:"v" gencodec:"required"`
+ R *big.Int `json:"r" gencodec:"required"`
+ S *big.Int `json:"s" gencodec:"required"`
+}
+
+// morphTxV0RLP is the RLP encoding structure for MorphTx version 0 (legacy format)
+type v0MorphTxRLP struct {
+ ChainID *big.Int
+ Nonce uint64
+ GasTipCap *big.Int
+ GasFeeCap *big.Int
+ Gas uint64
+ To *common.Address `rlp:"nil"`
+ Value *big.Int
+ Data []byte
+ AccessList AccessList
+ FeeTokenID uint16
+ FeeLimit *big.Int
+ V *big.Int
+ R *big.Int
+ S *big.Int
+}
+
+// morphTxV1RLP is the RLP encoding structure for MorphTx version 1 (with Reference/Memo)
+// Note: Version is NOT included here - it's encoded as a prefix byte before the RLP data
+type v1MorphTxRLP struct {
+ ChainID *big.Int
+ Nonce uint64
+ GasTipCap *big.Int
+ GasFeeCap *big.Int
+ Gas uint64
+ To *common.Address `rlp:"nil"`
+ Value *big.Int
+ Data []byte
+ AccessList AccessList
+ FeeTokenID uint16
+ FeeLimit *big.Int
+ Reference []byte // Use []byte for RLP compatibility (common.Reference is [32]byte, can't decode empty)
+ Memo []byte // Use []byte for RLP compatibility
+ V *big.Int
+ R *big.Int
+ S *big.Int
+}
+
+// copy creates a deep copy of the transaction data and initializes all fields.
+func (tx *MorphTx) copy() TxData {
+ cpy := &MorphTx{
+ Nonce: tx.Nonce,
+ Gas: tx.Gas,
+ To: copyAddressPtr(tx.To),
+ Data: common.CopyBytes(tx.Data),
+ FeeTokenID: tx.FeeTokenID,
+ Version: tx.Version,
+ Reference: copyReferencePtr(tx.Reference),
+ Memo: copyBytesPtr(tx.Memo),
+ // These are copied below.
+ AccessList: make(AccessList, len(tx.AccessList)),
+ Value: new(big.Int),
+ ChainID: new(big.Int),
+ GasTipCap: new(big.Int),
+ GasFeeCap: new(big.Int),
+ V: new(big.Int),
+ R: new(big.Int),
+ S: new(big.Int),
+ }
+ copy(cpy.AccessList, tx.AccessList)
+ if tx.Value != nil {
+ cpy.Value.Set(tx.Value)
+ }
+ if tx.ChainID != nil {
+ cpy.ChainID.Set(tx.ChainID)
+ }
+ if tx.GasTipCap != nil {
+ cpy.GasTipCap.Set(tx.GasTipCap)
+ }
+ if tx.GasFeeCap != nil {
+ cpy.GasFeeCap.Set(tx.GasFeeCap)
+ }
+ if tx.FeeLimit != nil {
+ cpy.FeeLimit = new(big.Int).Set(tx.FeeLimit)
+ }
+ if tx.V != nil {
+ cpy.V.Set(tx.V)
+ }
+ if tx.R != nil {
+ cpy.R.Set(tx.R)
+ }
+ if tx.S != nil {
+ cpy.S.Set(tx.S)
+ }
+ return cpy
+}
+
+// accessors for innerTx.
+func (tx *MorphTx) txType() byte { return MorphTxType }
+func (tx *MorphTx) chainID() *big.Int { return tx.ChainID }
+func (tx *MorphTx) accessList() AccessList { return tx.AccessList }
+func (tx *MorphTx) data() []byte { return tx.Data }
+func (tx *MorphTx) gas() uint64 { return tx.Gas }
+func (tx *MorphTx) gasFeeCap() *big.Int { return tx.GasFeeCap }
+func (tx *MorphTx) gasTipCap() *big.Int { return tx.GasTipCap }
+func (tx *MorphTx) gasPrice() *big.Int { return tx.GasFeeCap }
+func (tx *MorphTx) value() *big.Int { return tx.Value }
+func (tx *MorphTx) nonce() uint64 { return tx.Nonce }
+func (tx *MorphTx) to() *common.Address { return tx.To }
+
+func (tx *MorphTx) effectiveGasPrice(dst *big.Int, baseFee *big.Int) *big.Int {
+ if baseFee == nil {
+ return dst.Set(tx.GasFeeCap)
+ }
+ tip := dst.Sub(tx.GasFeeCap, baseFee)
+ if tip.Cmp(tx.GasTipCap) > 0 {
+ tip.Set(tx.GasTipCap)
+ }
+ return tip.Add(tip, baseFee)
+}
+
+func (tx *MorphTx) rawSignatureValues() (v, r, s *big.Int) {
+ return tx.V, tx.R, tx.S
+}
+
+func (tx *MorphTx) setSignatureValues(chainID, v, r, s *big.Int) {
+ tx.ChainID, tx.V, tx.R, tx.S = chainID, v, r, s
+}
+
+// EncodeRLP implements rlp.Encoder so that rlp.Encode produces the same output
+// as the custom encode() method. Without this, Hash() falls back to reflection-based
+// struct encoding which has different field order/count than the wire format.
+func (tx *MorphTx) EncodeRLP(w io.Writer) error {
+ buf := new(bytes.Buffer)
+ if err := tx.encode(buf); err != nil {
+ return err
+ }
+ _, err := w.Write(buf.Bytes())
+ return err
+}
+
+func (tx *MorphTx) encode(b *bytes.Buffer) error {
+ switch tx.Version {
+ case MorphTxVersion0:
+ // Validate FeeTokenID for v0 (must match decodeV0MorphTxRLP behavior)
+ if tx.FeeTokenID == 0 {
+ return errors.New("invalid FeeTokenID for v0 morph tx")
+ }
+ // Encode as v0 format (legacy AltFeeTx compatible)
+ // Format: RLP([chainID, nonce, ..., feeLimit, v, r, s])
+ return rlp.Encode(b, &v0MorphTxRLP{
+ ChainID: tx.ChainID,
+ Nonce: tx.Nonce,
+ GasTipCap: tx.GasTipCap,
+ GasFeeCap: tx.GasFeeCap,
+ Gas: tx.Gas,
+ To: tx.To,
+ Value: tx.Value,
+ Data: tx.Data,
+ AccessList: tx.AccessList,
+ FeeTokenID: tx.FeeTokenID,
+ FeeLimit: tx.FeeLimit,
+ V: tx.V,
+ R: tx.R,
+ S: tx.S,
+ })
+ case MorphTxVersion1:
+ // Encode as v1 format: [version byte] + RLP([...fields..., reference, memo])
+ // Version is encoded as a prefix byte (similar to txType), not inside RLP
+ b.WriteByte(tx.Version)
+ // Convert pointer types to []byte for RLP encoding
+ var reference []byte
+ if tx.Reference != nil {
+ reference = tx.Reference[:]
+ }
+ var memo []byte
+ if tx.Memo != nil {
+ memo = *tx.Memo
+ }
+ return rlp.Encode(b, &v1MorphTxRLP{
+ ChainID: tx.ChainID,
+ Nonce: tx.Nonce,
+ GasTipCap: tx.GasTipCap,
+ GasFeeCap: tx.GasFeeCap,
+ Gas: tx.Gas,
+ To: tx.To,
+ Value: tx.Value,
+ Data: tx.Data,
+ AccessList: tx.AccessList,
+ FeeTokenID: tx.FeeTokenID,
+ FeeLimit: tx.FeeLimit,
+ Reference: reference,
+ Memo: memo,
+ V: tx.V,
+ R: tx.R,
+ S: tx.S,
+ })
+ default:
+ return errors.New("unsupported morph tx version: " + strconv.Itoa(int(tx.Version)))
+ }
+}
+
+func (tx *MorphTx) decode(input []byte) error {
+ if len(input) == 0 {
+ return errors.New("empty morph tx input")
+ }
+
+ // Check first byte to determine version:
+ // - V0 format (legacy AltFeeTx): first byte is 0 or RLP list prefix (0xC0-0xFF)
+ // - V1+ format: first byte is version (0x01, 0x02, ...) followed by RLP
+ firstByte := input[0]
+ if firstByte == 0 || firstByte >= 0xC0 {
+ // V0 format: direct RLP decode (legacy compatible)
+ return decodeV0MorphTxRLP(tx, input)
+ }
+
+ // V1+ format: first byte is version, rest is RLP
+ if firstByte != MorphTxVersion1 {
+ return errors.New("unsupported morph tx version: " + strconv.Itoa(int(firstByte)))
+ }
+ return decodeV1MorphTxRLP(tx, input[1:])
+}
+
+func decodeV1MorphTxRLP(tx *MorphTx, blob []byte) error {
+ var v1 v1MorphTxRLP
+ if err := rlp.DecodeBytes(blob, &v1); err != nil {
+ return err
+ }
+
+ 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.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
+}
+
+func (tx *MorphTx) sigHash(chainID *big.Int) common.Hash {
+ if tx.Version == MorphTxVersion0 {
+ return tx.v0SigHash(chainID)
+ }
+ return tx.v1SigHash(chainID)
+}
+
+func (tx *MorphTx) v1SigHash(chainID *big.Int) common.Hash {
+ return prefixedRlpHash(
+ MorphTxType,
+ []any{
+ chainID,
+ tx.Nonce,
+ tx.GasTipCap,
+ tx.GasFeeCap,
+ tx.Gas,
+ tx.To,
+ tx.Value,
+ tx.Data,
+ tx.AccessList,
+ tx.FeeTokenID,
+ tx.FeeLimit,
+ tx.Version,
+ tx.Reference,
+ tx.Memo,
+ })
+}
+
+func (tx *MorphTx) v0SigHash(chainID *big.Int) common.Hash {
+ return prefixedRlpHash(
+ MorphTxType,
+ []any{
+ chainID,
+ tx.Nonce,
+ tx.GasTipCap,
+ tx.GasFeeCap,
+ tx.Gas,
+ tx.To,
+ tx.Value,
+ tx.Data,
+ tx.AccessList,
+ tx.FeeTokenID,
+ tx.FeeLimit,
+ })
+}
diff --git a/core/types/morph_tx_compat_test.go b/core/types/morph_tx_compat_test.go
new file mode 100644
index 000000000..464fcc96b
--- /dev/null
+++ b/core/types/morph_tx_compat_test.go
@@ -0,0 +1,593 @@
+package types
+
+import (
+ "bytes"
+ "encoding/hex"
+ "math/big"
+ "testing"
+
+ "github.com/morph-l2/go-ethereum/common"
+ "github.com/morph-l2/go-ethereum/crypto"
+ "github.com/morph-l2/go-ethereum/rlp"
+)
+
+// TestMorphTxV0BackwardCompatibility tests that old AltFeeTx encoded data
+// can be correctly decoded by the new MorphTx decoder.
+// These hex values were generated from the original AltFeeTx implementation.
+func TestMorphTxV0BackwardCompatibility(t *testing.T) {
+ // Expected values from the original encoding
+ expectedTo := common.HexToAddress("0x1234567890123456789012345678901234567890")
+ expectedChainID := big.NewInt(2818)
+ expectedNonce := uint64(1)
+ expectedGasTipCap := big.NewInt(1000000000)
+ expectedGasFeeCap := big.NewInt(2000000000)
+ expectedGas := uint64(21000)
+ expectedValue := big.NewInt(1000000000000000000) // 1 ETH
+ expectedR, _ := new(big.Int).SetString("abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", 16)
+ expectedS, _ := new(big.Int).SetString("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", 16)
+
+ testCases := []struct {
+ name string
+ fullHex string // Full hex including 0x7F prefix
+ feeTokenID uint16
+ feeLimit *big.Int
+ }{
+ {
+ // Case 1: FeeLimit has value (0.5 ETH = 500000000000000000)
+ name: "V0 with FeeLimit value",
+ fullHex: "7ff87e820b0201843b9aca008477359400825208941234567890123456789012345678901234567890880de0b6b3a764000080c0018806f05b59d3b2000001a0abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890a01234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
+ feeTokenID: 1,
+ feeLimit: big.NewInt(500000000000000000),
+ },
+ {
+ // Case 2: FeeLimit is nil (encoded as 0x80)
+ name: "V0 with nil FeeLimit",
+ fullHex: "7ff876820b0201843b9aca008477359400825208941234567890123456789012345678901234567890880de0b6b3a764000080c0018001a0abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890a01234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
+ feeTokenID: 1,
+ feeLimit: nil,
+ },
+ {
+ // Case 3: FeeLimit is 0 (also encoded as 0x80)
+ name: "V0 with zero FeeLimit",
+ fullHex: "7ff876820b0201843b9aca008477359400825208941234567890123456789012345678901234567890880de0b6b3a764000080c0018001a0abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890a01234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
+ feeTokenID: 1,
+ feeLimit: nil, // 0 is encoded as empty, decoded as nil
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ data, err := hex.DecodeString(tc.fullHex)
+ if err != nil {
+ t.Fatalf("failed to decode hex: %v", err)
+ }
+
+ // Verify first byte is MorphTxType (0x7F)
+ if data[0] != MorphTxType {
+ t.Fatalf("expected first byte 0x7F, got 0x%x", data[0])
+ }
+
+ // Skip txType byte, decode the rest
+ innerData := data[1:]
+ t.Logf("First inner byte: 0x%x (should be RLP list prefix >= 0xC0)", innerData[0])
+
+ // Verify it's RLP list prefix (V0 format)
+ if innerData[0] < 0xC0 {
+ t.Errorf("V0 data should start with RLP list prefix, got 0x%x", innerData[0])
+ }
+
+ // Decode using MorphTx.decode
+ var decoded MorphTx
+ if err := decoded.decode(innerData); err != nil {
+ t.Fatalf("failed to decode MorphTx: %v", err)
+ }
+
+ // Verify version is 0 (V0 format)
+ if decoded.Version != MorphTxVersion0 {
+ t.Errorf("expected Version 0, got %d", decoded.Version)
+ }
+
+ // Verify FeeTokenID
+ if decoded.FeeTokenID != tc.feeTokenID {
+ t.Errorf("expected FeeTokenID %d, got %d", tc.feeTokenID, decoded.FeeTokenID)
+ }
+
+ // Verify FeeLimit
+ if tc.feeLimit == nil {
+ if decoded.FeeLimit != nil && decoded.FeeLimit.Sign() != 0 {
+ t.Errorf("expected nil/zero FeeLimit, got %v", decoded.FeeLimit)
+ }
+ } else {
+ if decoded.FeeLimit == nil || decoded.FeeLimit.Cmp(tc.feeLimit) != 0 {
+ t.Errorf("expected FeeLimit %v, got %v", tc.feeLimit, decoded.FeeLimit)
+ }
+ }
+
+ // Verify other common fields
+ if decoded.ChainID.Cmp(expectedChainID) != 0 {
+ t.Errorf("ChainID mismatch: expected %v, got %v", expectedChainID, decoded.ChainID)
+ }
+ if decoded.Nonce != expectedNonce {
+ t.Errorf("Nonce mismatch: expected %d, got %d", expectedNonce, decoded.Nonce)
+ }
+ if decoded.GasTipCap.Cmp(expectedGasTipCap) != 0 {
+ t.Errorf("GasTipCap mismatch: expected %v, got %v", expectedGasTipCap, decoded.GasTipCap)
+ }
+ if decoded.GasFeeCap.Cmp(expectedGasFeeCap) != 0 {
+ t.Errorf("GasFeeCap mismatch: expected %v, got %v", expectedGasFeeCap, decoded.GasFeeCap)
+ }
+ if decoded.Gas != expectedGas {
+ t.Errorf("Gas mismatch: expected %d, got %d", expectedGas, decoded.Gas)
+ }
+ if decoded.To == nil || *decoded.To != expectedTo {
+ t.Errorf("To mismatch: expected %v, got %v", expectedTo, decoded.To)
+ }
+ if decoded.Value.Cmp(expectedValue) != 0 {
+ t.Errorf("Value mismatch: expected %v, got %v", expectedValue, decoded.Value)
+ }
+ if decoded.R.Cmp(expectedR) != 0 {
+ t.Errorf("R mismatch: expected %v, got %v", expectedR, decoded.R)
+ }
+ if decoded.S.Cmp(expectedS) != 0 {
+ t.Errorf("S mismatch: expected %v, got %v", expectedS, decoded.S)
+ }
+
+ t.Logf("Successfully decoded V0 MorphTx: ChainID=%v, Nonce=%d, FeeTokenID=%d, FeeLimit=%v, Version=%d",
+ decoded.ChainID, decoded.Nonce, decoded.FeeTokenID, decoded.FeeLimit, decoded.Version)
+ })
+ }
+}
+
+// encodeMorphTx encodes a MorphTx using its encode method
+func encodeMorphTx(tx *MorphTx) ([]byte, error) {
+ buf := new(bytes.Buffer)
+ buf.WriteByte(MorphTxType) // Write txType prefix
+ if err := tx.encode(buf); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+// TestMorphTxV1Encoding tests the new V1 encoding format
+// where version is a prefix byte before RLP data.
+func TestMorphTxV1Encoding(t *testing.T) {
+ reference := common.HexToReference("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")
+ memo := []byte("test memo")
+ to := common.HexToAddress("0x1234567890123456789012345678901234567890")
+
+ tx := &MorphTx{
+ ChainID: big.NewInt(2818),
+ Nonce: 1,
+ GasTipCap: big.NewInt(1000000000),
+ GasFeeCap: big.NewInt(2000000000),
+ Gas: 21000,
+ To: &to,
+ Value: big.NewInt(0),
+ Data: []byte{},
+ AccessList: AccessList{},
+ FeeTokenID: 0, // ETH
+ FeeLimit: nil,
+ Version: MorphTxVersion1,
+ Reference: &reference,
+ Memo: &memo,
+ V: big.NewInt(0),
+ R: big.NewInt(0),
+ S: big.NewInt(0),
+ }
+
+ // Encode
+ encoded, err := encodeMorphTx(tx)
+ if err != nil {
+ t.Fatalf("failed to encode: %v", err)
+ }
+
+ t.Logf("V1 encoded hex: %s", hex.EncodeToString(encoded))
+ t.Logf("First byte (type): 0x%x", encoded[0])
+ t.Logf("Second byte (version): 0x%x", encoded[1])
+
+ // Verify first byte is MorphTxType
+ if encoded[0] != MorphTxType {
+ t.Errorf("expected first byte 0x%x, got 0x%x", MorphTxType, encoded[0])
+ }
+
+ // Verify second byte is version
+ if encoded[1] != MorphTxVersion1 {
+ t.Errorf("expected second byte 0x%x (version 1), got 0x%x", MorphTxVersion1, encoded[1])
+ }
+
+ // Decode back
+ var decoded MorphTx
+ if err := decoded.decode(encoded[1:]); err != nil { // Skip txType byte
+ t.Fatalf("failed to decode: %v", err)
+ }
+
+ // Verify fields
+ if decoded.Version != MorphTxVersion1 {
+ t.Errorf("expected Version 1, got %d", decoded.Version)
+ }
+ if decoded.Reference == nil || *decoded.Reference != reference {
+ t.Errorf("Reference mismatch")
+ }
+ if decoded.Memo == nil || string(*decoded.Memo) != string(memo) {
+ t.Errorf("Memo mismatch")
+ }
+
+ t.Logf("Successfully encoded and decoded V1 MorphTx")
+}
+
+// TestMorphTxV0V1RoundTrip tests encoding/decoding round trip for both versions
+func TestMorphTxV0V1RoundTrip(t *testing.T) {
+ to := common.HexToAddress("0x1234567890123456789012345678901234567890")
+ reference := common.HexToReference("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890")
+ memo := []byte("hello")
+
+ testCases := []struct {
+ name string
+ tx *MorphTx
+ }{
+ {
+ name: "V0 with FeeTokenID",
+ tx: &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 1,
+ GasTipCap: big.NewInt(1000000000),
+ GasFeeCap: big.NewInt(2000000000),
+ Gas: 21000,
+ To: &to,
+ Value: big.NewInt(1000000000000000000),
+ Data: []byte{},
+ AccessList: AccessList{},
+ FeeTokenID: 1, // Non-zero required for V0
+ FeeLimit: big.NewInt(100000000000000000),
+ Version: MorphTxVersion0,
+ V: big.NewInt(1),
+ R: big.NewInt(123456),
+ S: big.NewInt(654321),
+ },
+ },
+ {
+ name: "V1 with Reference and Memo",
+ tx: &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 2,
+ GasTipCap: big.NewInt(1000000000),
+ GasFeeCap: big.NewInt(2000000000),
+ Gas: 21000,
+ To: &to,
+ Value: big.NewInt(0),
+ Data: []byte{0x01, 0x02, 0x03},
+ AccessList: AccessList{},
+ FeeTokenID: 0,
+ FeeLimit: nil,
+ Version: MorphTxVersion1,
+ Reference: &reference,
+ Memo: &memo,
+ V: big.NewInt(0),
+ R: big.NewInt(111),
+ S: big.NewInt(222),
+ },
+ },
+ {
+ name: "V1 with FeeTokenID and Reference",
+ tx: &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 3,
+ GasTipCap: big.NewInt(1000000000),
+ GasFeeCap: big.NewInt(2000000000),
+ Gas: 50000,
+ To: &to,
+ Value: big.NewInt(500000000000000000),
+ Data: []byte{},
+ AccessList: AccessList{},
+ FeeTokenID: 2,
+ FeeLimit: big.NewInt(200000000000000000),
+ Version: MorphTxVersion1,
+ Reference: &reference,
+ Memo: nil,
+ V: big.NewInt(1),
+ R: big.NewInt(333),
+ S: big.NewInt(444),
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ // Encode
+ encoded, err := encodeMorphTx(tc.tx)
+ if err != nil {
+ t.Fatalf("failed to encode: %v", err)
+ }
+
+ t.Logf("Encoded hex: %s", hex.EncodeToString(encoded))
+ t.Logf("Length: %d bytes", len(encoded))
+
+ // Decode
+ var decoded MorphTx
+ if err := decoded.decode(encoded[1:]); err != nil { // Skip txType byte
+ t.Fatalf("failed to decode: %v", err)
+ }
+
+ // Verify key fields
+ if decoded.Version != tc.tx.Version {
+ t.Errorf("Version mismatch: expected %d, got %d", tc.tx.Version, decoded.Version)
+ }
+ if decoded.FeeTokenID != tc.tx.FeeTokenID {
+ t.Errorf("FeeTokenID mismatch: expected %d, got %d", tc.tx.FeeTokenID, decoded.FeeTokenID)
+ }
+ if decoded.Nonce != tc.tx.Nonce {
+ t.Errorf("Nonce mismatch: expected %d, got %d", tc.tx.Nonce, decoded.Nonce)
+ }
+ if decoded.Gas != tc.tx.Gas {
+ t.Errorf("Gas mismatch: expected %d, got %d", tc.tx.Gas, decoded.Gas)
+ }
+
+ t.Logf("Round-trip successful for %s", tc.name)
+ })
+ }
+}
+
+// TestMorphTxVersionDetection tests the version detection logic in decode
+func TestMorphTxVersionDetection(t *testing.T) {
+ // Create a V0 transaction (legacy format)
+ to := common.HexToAddress("0x1234567890123456789012345678901234567890")
+ v0Tx := &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 1,
+ GasTipCap: big.NewInt(1000000000),
+ GasFeeCap: big.NewInt(2000000000),
+ Gas: 21000,
+ To: &to,
+ Value: big.NewInt(0),
+ FeeTokenID: 1,
+ FeeLimit: big.NewInt(100),
+ Version: MorphTxVersion0,
+ V: big.NewInt(0),
+ R: big.NewInt(0),
+ S: big.NewInt(0),
+ }
+
+ encoded, err := encodeMorphTx(v0Tx)
+ if err != nil {
+ t.Fatalf("failed to encode V0: %v", err)
+ }
+ innerData := encoded[1:] // Skip txType
+
+ // V0 should start with RLP list prefix (0xC0-0xFF)
+ if innerData[0] < 0xC0 {
+ t.Errorf("V0 encoded data should start with RLP list prefix, got 0x%x", innerData[0])
+ }
+ t.Logf("V0 first inner byte: 0x%x (RLP list prefix)", innerData[0])
+
+ // Create a V1 transaction
+ reference := common.HexToReference("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")
+ v1Tx := &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 1,
+ GasTipCap: big.NewInt(1000000000),
+ GasFeeCap: big.NewInt(2000000000),
+ Gas: 21000,
+ To: &to,
+ Value: big.NewInt(0),
+ FeeTokenID: 0,
+ Version: MorphTxVersion1,
+ Reference: &reference,
+ V: big.NewInt(0),
+ R: big.NewInt(0),
+ S: big.NewInt(0),
+ }
+
+ encoded, err = encodeMorphTx(v1Tx)
+ if err != nil {
+ t.Fatalf("failed to encode V1: %v", err)
+ }
+ innerData = encoded[1:] // Skip txType
+
+ // V1 should start with version byte (0x01)
+ if innerData[0] != MorphTxVersion1 {
+ t.Errorf("V1 encoded data should start with version byte 0x01, got 0x%x", innerData[0])
+ }
+ t.Logf("V1 first inner byte: 0x%x (version prefix)", innerData[0])
+
+ // Second byte should be RLP list prefix
+ if innerData[1] < 0xC0 {
+ t.Errorf("V1 second byte should be RLP list prefix, got 0x%x", innerData[1])
+ }
+ t.Logf("V1 second inner byte: 0x%x (RLP list prefix)", innerData[1])
+}
+
+// TestMorphTxEncodeRLPConsistency verifies that rlp.Encode(morphTx) produces
+// the same output as the custom encode() method. This ensures Hash() (which
+// uses rlp.Encode internally) is consistent with the wire format.
+func TestMorphTxEncodeRLPConsistency(t *testing.T) {
+ to := common.HexToAddress("0x1234567890123456789012345678901234567890")
+ reference := common.HexToReference("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890")
+ memo := []byte("hello")
+
+ testCases := []struct {
+ name string
+ tx *MorphTx
+ }{
+ {
+ name: "V0",
+ tx: &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 1,
+ GasTipCap: big.NewInt(1000000000),
+ GasFeeCap: big.NewInt(2000000000),
+ Gas: 21000,
+ To: &to,
+ Value: big.NewInt(1000000000000000000),
+ Data: []byte{},
+ AccessList: AccessList{},
+ FeeTokenID: 1,
+ FeeLimit: big.NewInt(100000000000000000),
+ Version: MorphTxVersion0,
+ V: big.NewInt(1),
+ R: big.NewInt(123456),
+ S: big.NewInt(654321),
+ },
+ },
+ {
+ name: "V1 with Reference and Memo",
+ tx: &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 2,
+ GasTipCap: big.NewInt(1000000000),
+ GasFeeCap: big.NewInt(2000000000),
+ Gas: 21000,
+ To: &to,
+ Value: big.NewInt(0),
+ Data: []byte{0x01, 0x02, 0x03},
+ AccessList: AccessList{},
+ FeeTokenID: 0,
+ FeeLimit: nil,
+ Version: MorphTxVersion1,
+ Reference: &reference,
+ Memo: &memo,
+ V: big.NewInt(0),
+ R: big.NewInt(111),
+ S: big.NewInt(222),
+ },
+ },
+ {
+ name: "V1 minimal",
+ tx: &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 3,
+ GasTipCap: big.NewInt(1000000000),
+ GasFeeCap: big.NewInt(2000000000),
+ Gas: 50000,
+ To: &to,
+ Value: big.NewInt(0),
+ Data: []byte{},
+ AccessList: AccessList{},
+ FeeTokenID: 0,
+ FeeLimit: nil,
+ Version: MorphTxVersion1,
+ Reference: nil,
+ Memo: nil,
+ V: big.NewInt(0),
+ R: big.NewInt(0),
+ S: big.NewInt(0),
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ // Path 1: rlp.Encode (used by Hash via prefixedRlpHash)
+ var rlpBuf bytes.Buffer
+ if err := rlp.Encode(&rlpBuf, tc.tx); err != nil {
+ t.Fatalf("rlp.Encode failed: %v", err)
+ }
+
+ // Path 2: custom encode() (used by wire format via encodeTyped)
+ var encodeBuf bytes.Buffer
+ if err := tc.tx.encode(&encodeBuf); err != nil {
+ t.Fatalf("encode() failed: %v", err)
+ }
+
+ if !bytes.Equal(rlpBuf.Bytes(), encodeBuf.Bytes()) {
+ t.Errorf("rlp.Encode and encode() produce different output:\n rlp.Encode = %s\n encode() = %s",
+ hex.EncodeToString(rlpBuf.Bytes()), hex.EncodeToString(encodeBuf.Bytes()))
+ }
+ })
+ }
+}
+
+// TestMorphTxHashMatchesWireFormat verifies that tx.Hash() equals
+// keccak256(wire_bytes) for both V0 and V1 MorphTx transactions.
+func TestMorphTxHashMatchesWireFormat(t *testing.T) {
+ to := common.HexToAddress("0x1234567890123456789012345678901234567890")
+ reference := common.HexToReference("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890")
+ memo := []byte("hello")
+
+ testCases := []struct {
+ name string
+ tx *MorphTx
+ }{
+ {
+ name: "V0",
+ tx: &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 1,
+ GasTipCap: big.NewInt(1000000000),
+ GasFeeCap: big.NewInt(2000000000),
+ Gas: 21000,
+ To: &to,
+ Value: big.NewInt(1000000000000000000),
+ Data: []byte{},
+ AccessList: AccessList{},
+ FeeTokenID: 1,
+ FeeLimit: big.NewInt(100000000000000000),
+ Version: MorphTxVersion0,
+ V: big.NewInt(1),
+ R: big.NewInt(123456),
+ S: big.NewInt(654321),
+ },
+ },
+ {
+ name: "V1 with Reference and Memo",
+ tx: &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 2,
+ GasTipCap: big.NewInt(1000000000),
+ GasFeeCap: big.NewInt(2000000000),
+ Gas: 21000,
+ To: &to,
+ Value: big.NewInt(0),
+ Data: []byte{0x01, 0x02, 0x03},
+ AccessList: AccessList{},
+ FeeTokenID: 0,
+ FeeLimit: nil,
+ Version: MorphTxVersion1,
+ Reference: &reference,
+ Memo: &memo,
+ V: big.NewInt(0),
+ R: big.NewInt(111),
+ S: big.NewInt(222),
+ },
+ },
+ {
+ name: "V1 minimal",
+ tx: &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 3,
+ GasTipCap: big.NewInt(1000000000),
+ GasFeeCap: big.NewInt(2000000000),
+ Gas: 50000,
+ To: &to,
+ Value: big.NewInt(0),
+ Data: []byte{},
+ AccessList: AccessList{},
+ FeeTokenID: 0,
+ FeeLimit: nil,
+ Version: MorphTxVersion1,
+ Reference: nil,
+ Memo: nil,
+ V: big.NewInt(0),
+ R: big.NewInt(0),
+ S: big.NewInt(0),
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ tx := NewTx(tc.tx)
+
+ wireBytes, err := tx.MarshalBinary()
+ if err != nil {
+ t.Fatalf("MarshalBinary failed: %v", err)
+ }
+
+ expectedHash := crypto.Keccak256Hash(wireBytes)
+
+ if tx.Hash() != expectedHash {
+ t.Errorf("Hash mismatch:\n tx.Hash() = %s\n keccak256(wire) = %s\n wireBytes = %s",
+ tx.Hash().Hex(), expectedHash.Hex(), hex.EncodeToString(wireBytes))
+ }
+ })
+ }
+}
diff --git a/core/types/receipt.go b/core/types/receipt.go
index 96bb17f98..303459420 100644
--- a/core/types/receipt.go
+++ b/core/types/receipt.go
@@ -78,11 +78,15 @@ type Receipt struct {
// Morph rollup
L1Fee *big.Int `json:"l1Fee,omitempty"`
- // Alt Fee
- FeeTokenID *uint16 `json:"feeTokenID,omitempty"`
- FeeRate *big.Int `json:"feeRate,omitempty"`
- TokenScale *big.Int `json:"tokenScale,omitempty"`
- FeeLimit *big.Int `json:"feeLimit,omitempty"`
+
+ // MorphTx fields
+ FeeTokenID *uint16 `json:"feeTokenID,omitempty"`
+ FeeRate *big.Int `json:"feeRate,omitempty"`
+ TokenScale *big.Int `json:"tokenScale,omitempty"`
+ FeeLimit *big.Int `json:"feeLimit,omitempty"`
+ Version uint8 `json:"version,omitempty"`
+ Reference *common.Reference `json:"reference,omitempty"`
+ Memo *[]byte `json:"memo,omitempty"`
}
type receiptMarshaling struct {
@@ -101,6 +105,9 @@ type receiptMarshaling struct {
FeeRate *hexutil.Big
TokenScale *hexutil.Big
FeeLimit *hexutil.Big
+ Version hexutil.Uint64
+ Reference *common.Reference
+ Memo *hexutil.Bytes
}
// receiptRLP is the consensus encoding of a receipt.
@@ -112,6 +119,10 @@ type receiptRLP struct {
}
// storedReceiptRLP is the storage encoding of a receipt.
+// Note: Version, Reference, and Memo are marked as rlp:"optional" for backward compatibility.
+// When these fields are zero-valued (V0 receipts), they are omitted from the RLP encoding,
+// producing an 8-element list identical to v7StoredReceiptRLP format. This allows older nodes
+// (without V1 logic) to decode receipts stored by newer nodes before the Jade fork activates.
type storedReceiptRLP struct {
PostStateOrStatus []byte
CumulativeGasUsed uint64
@@ -121,6 +132,26 @@ type storedReceiptRLP struct {
FeeRate *big.Int
TokenScale *big.Int
FeeLimit *big.Int
+ Version uint8 `rlp:"optional"`
+ Reference []byte `rlp:"optional"` // Note: use []byte for RLP compatibility (common.Reference is [32]byte, can't decode empty)
+ Memo []byte `rlp:"optional"` // Note: use []byte for RLP compatibility, convert to *[]byte when needed
+}
+
+// v8StoredReceiptRLP is the storage encoding of a receipt used in database version 8.
+// This version was introduced when MorphTx feature was added.
+// It includes L1Fee and all MorphTx fields (FeeTokenID, FeeRate, TokenScale, FeeLimit, Version, Reference, Memo).
+type v8StoredReceiptRLP struct {
+ PostStateOrStatus []byte
+ CumulativeGasUsed uint64
+ Logs []*LogForStorage
+ L1Fee *big.Int
+ FeeTokenID *uint16
+ FeeRate *big.Int
+ TokenScale *big.Int
+ FeeLimit *big.Int
+ Version uint8
+ Reference []byte // Note: use []byte for RLP compatibility (common.Reference is [32]byte, can't decode empty)
+ Memo []byte // Note: use []byte for RLP compatibility
}
// v7StoredReceiptRLP is the storage encoding of a receipt used in database version 7.
@@ -277,7 +308,7 @@ func (r *Receipt) decodeTyped(b []byte) error {
return errShortTypedReceipt
}
switch b[0] {
- case DynamicFeeTxType, AccessListTxType, BlobTxType, L1MessageTxType, SetCodeTxType, AltFeeTxType:
+ case DynamicFeeTxType, AccessListTxType, BlobTxType, L1MessageTxType, SetCodeTxType, MorphTxType:
var data receiptRLP
err := rlp.DecodeBytes(b[1:], &data)
if err != nil {
@@ -337,6 +368,15 @@ type ReceiptForStorage Receipt
// EncodeRLP implements rlp.Encoder, and flattens all content fields of a receipt
// into an RLP stream.
func (r *ReceiptForStorage) EncodeRLP(w io.Writer) error {
+ // Convert pointer types to []byte for RLP encoding
+ var memo []byte
+ if r.Memo != nil {
+ memo = *r.Memo
+ }
+ var reference []byte
+ if r.Reference != nil {
+ reference = r.Reference[:]
+ }
enc := &storedReceiptRLP{
PostStateOrStatus: (*Receipt)(r).statusEncoding(),
CumulativeGasUsed: r.CumulativeGasUsed,
@@ -346,6 +386,9 @@ func (r *ReceiptForStorage) EncodeRLP(w io.Writer) error {
FeeRate: r.FeeRate,
TokenScale: r.TokenScale,
FeeLimit: r.FeeLimit,
+ Version: r.Version,
+ Reference: reference,
+ Memo: memo,
}
for i, log := range r.Logs {
enc.Logs[i] = (*LogForStorage)(log)
@@ -367,6 +410,9 @@ func (r *ReceiptForStorage) DecodeRLP(s *rlp.Stream) error {
if err := decodeStoredReceiptRLP(r, blob); err == nil {
return nil
}
+ if err := decodeV8StoredReceiptRLP(r, blob); err == nil {
+ return nil
+ }
if err := decodeV7StoredReceiptRLP(r, blob); err == nil {
return nil
}
@@ -401,6 +447,49 @@ func decodeStoredReceiptRLP(r *ReceiptForStorage, blob []byte) error {
r.FeeRate = stored.FeeRate
r.TokenScale = stored.TokenScale
r.FeeLimit = stored.FeeLimit
+ r.Version = stored.Version
+ // Convert []byte to *common.Reference
+ if len(stored.Reference) == common.ReferenceLength {
+ ref := common.BytesToReference(stored.Reference)
+ r.Reference = &ref
+ }
+ // Convert []byte to *[]byte
+ if len(stored.Memo) > 0 {
+ r.Memo = &stored.Memo
+ }
+
+ return nil
+}
+
+func decodeV8StoredReceiptRLP(r *ReceiptForStorage, blob []byte) error {
+ var stored v8StoredReceiptRLP
+ if err := rlp.DecodeBytes(blob, &stored); err != nil {
+ return err
+ }
+ if err := (*Receipt)(r).setStatus(stored.PostStateOrStatus); err != nil {
+ return err
+ }
+ r.CumulativeGasUsed = stored.CumulativeGasUsed
+ r.Logs = make([]*Log, len(stored.Logs))
+ for i, log := range stored.Logs {
+ r.Logs[i] = (*Log)(log)
+ }
+ r.Bloom = CreateBloom(Receipts{(*Receipt)(r)})
+ r.L1Fee = stored.L1Fee
+ r.FeeTokenID = stored.FeeTokenID
+ r.FeeRate = stored.FeeRate
+ r.TokenScale = stored.TokenScale
+ r.FeeLimit = stored.FeeLimit
+ r.Version = stored.Version
+ // Convert []byte to *common.Reference
+ if len(stored.Reference) == common.ReferenceLength {
+ ref := common.BytesToReference(stored.Reference)
+ r.Reference = &ref
+ }
+ // Convert []byte to *[]byte
+ if len(stored.Memo) > 0 {
+ r.Memo = &stored.Memo
+ }
return nil
}
@@ -531,8 +620,8 @@ func (rs Receipts) EncodeIndex(i int, w *bytes.Buffer) {
case L1MessageTxType:
w.WriteByte(L1MessageTxType)
rlp.Encode(w, data)
- case AltFeeTxType:
- w.WriteByte(AltFeeTxType)
+ case MorphTxType:
+ w.WriteByte(MorphTxType)
rlp.Encode(w, data)
case SetCodeTxType:
w.WriteByte(SetCodeTxType)
diff --git a/core/types/receipt_test.go b/core/types/receipt_test.go
index 0939f429d..1ddaeb1b5 100644
--- a/core/types/receipt_test.go
+++ b/core/types/receipt_test.go
@@ -80,7 +80,7 @@ var (
},
Type: DynamicFeeTxType,
}
- altFeeReceipt = &Receipt{
+ morphTxReceipt = &Receipt{
Status: ReceiptStatusFailed,
CumulativeGasUsed: 1,
Logs: []*Log{
@@ -95,7 +95,108 @@ var (
Data: []byte{0x01, 0x00, 0xff},
},
},
- Type: AltFeeTxType,
+ Type: MorphTxType,
+ }
+
+ // MorphTx receipt with Version 1 and all new fields
+ testReference = common.HexToReference("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")
+ testMemo = []byte("test memo data")
+ testFeeTokenID = uint16(1)
+ morphTxV1Receipt = &Receipt{
+ Status: ReceiptStatusSuccessful,
+ CumulativeGasUsed: 100,
+ Logs: []*Log{
+ {
+ Address: common.BytesToAddress([]byte{0x11}),
+ Topics: []common.Hash{common.HexToHash("dead"), common.HexToHash("beef")},
+ Data: []byte{0x01, 0x00, 0xff},
+ },
+ },
+ Type: MorphTxType,
+ Version: MorphTxVersion1,
+ Reference: &testReference,
+ Memo: &testMemo,
+ FeeTokenID: &testFeeTokenID,
+ FeeRate: big.NewInt(1000),
+ TokenScale: big.NewInt(18),
+ FeeLimit: big.NewInt(1000000),
+ L1Fee: big.NewInt(500),
+ }
+
+ // MorphTx receipt with Version 0 (legacy format)
+ // Note: V0 format doesn't use Reference/Memo, so we set them to nil equivalent zero values for RLP compatibility
+ zeroReference = common.Reference{}
+ nilMemo = []byte{}
+ morphTxV0Receipt = &Receipt{
+ Status: ReceiptStatusSuccessful,
+ CumulativeGasUsed: 200,
+ Logs: []*Log{
+ {
+ Address: common.BytesToAddress([]byte{0x22}),
+ Topics: []common.Hash{common.HexToHash("cafe"), common.HexToHash("babe")},
+ Data: []byte{0x02, 0x00, 0xfe},
+ },
+ },
+ Type: MorphTxType,
+ Version: MorphTxVersion0,
+ Reference: &zeroReference, // V0 has no Reference (use zero value for RLP)
+ Memo: &nilMemo, // V0 has no Memo (use empty for RLP)
+ FeeTokenID: &testFeeTokenID,
+ FeeRate: big.NewInt(2000),
+ TokenScale: big.NewInt(18),
+ FeeLimit: big.NewInt(2000000),
+ L1Fee: big.NewInt(600),
+ }
+
+ // MorphTx receipt with only Reference (no Memo)
+ zeroFeeTokenID = uint16(0)
+ morphTxRefOnlyReceipt = &Receipt{
+ Status: ReceiptStatusSuccessful,
+ CumulativeGasUsed: 300,
+ Logs: []*Log{},
+ Type: MorphTxType,
+ Version: MorphTxVersion1,
+ Reference: &testReference,
+ Memo: &nilMemo, // Use empty memo for RLP compatibility
+ FeeTokenID: &zeroFeeTokenID,
+ FeeRate: big.NewInt(0),
+ TokenScale: big.NewInt(0),
+ FeeLimit: big.NewInt(0),
+ L1Fee: big.NewInt(0),
+ }
+
+ // MorphTx receipt with only Memo (no Reference)
+ testMemoOnly = []byte("memo only test")
+ morphTxMemoOnlyReceipt = &Receipt{
+ Status: ReceiptStatusSuccessful,
+ CumulativeGasUsed: 400,
+ Logs: []*Log{},
+ Type: MorphTxType,
+ Version: MorphTxVersion1,
+ Reference: &zeroReference, // Use zero reference for RLP compatibility
+ Memo: &testMemoOnly,
+ FeeTokenID: &zeroFeeTokenID,
+ FeeRate: big.NewInt(0),
+ TokenScale: big.NewInt(0),
+ FeeLimit: big.NewInt(0),
+ L1Fee: big.NewInt(0),
+ }
+
+ // MorphTx receipt with empty Memo
+ emptyMemo = []byte{}
+ morphTxEmptyMemoReceipt = &Receipt{
+ Status: ReceiptStatusSuccessful,
+ CumulativeGasUsed: 500,
+ Logs: []*Log{},
+ Type: MorphTxType,
+ Version: MorphTxVersion1,
+ Reference: &testReference,
+ Memo: &emptyMemo,
+ FeeTokenID: &zeroFeeTokenID,
+ FeeRate: big.NewInt(0),
+ TokenScale: big.NewInt(0),
+ FeeLimit: big.NewInt(0),
+ L1Fee: big.NewInt(0),
}
)
@@ -117,6 +218,10 @@ func TestLegacyReceiptDecoding(t *testing.T) {
"StoredReceiptRLP",
encodeAsStoredReceiptRLP,
},
+ {
+ "V8StoredReceiptRLP",
+ encodeAsV8StoredReceiptRLP,
+ },
{
"V7StoredReceiptRLP",
encodeAsV7StoredReceiptRLP,
@@ -140,6 +245,9 @@ func TestLegacyReceiptDecoding(t *testing.T) {
}
tx := NewTransaction(1, common.HexToAddress("0x1"), big.NewInt(1), 1, big.NewInt(1), nil)
+ legacyZeroRef := common.Reference{}
+ legacyNilMemo := []byte{}
+ legacyFeeTokenID := uint16(0)
receipt := &Receipt{
Status: ReceiptStatusFailed,
CumulativeGasUsed: 1,
@@ -158,6 +266,15 @@ func TestLegacyReceiptDecoding(t *testing.T) {
TxHash: tx.Hash(),
ContractAddress: common.BytesToAddress([]byte{0x01, 0x11, 0x11}),
GasUsed: 111111,
+ // Set new fields to zero/empty values for RLP compatibility
+ Version: 0,
+ Reference: &legacyZeroRef,
+ Memo: &legacyNilMemo,
+ FeeTokenID: &legacyFeeTokenID,
+ FeeRate: big.NewInt(0),
+ TokenScale: big.NewInt(0),
+ FeeLimit: big.NewInt(0),
+ L1Fee: big.NewInt(0),
}
receipt.Bloom = CreateBloom(Receipts{receipt})
@@ -205,6 +322,49 @@ func encodeAsStoredReceiptRLP(want *Receipt) ([]byte, error) {
CumulativeGasUsed: want.CumulativeGasUsed,
Logs: make([]*LogForStorage, len(want.Logs)),
L1Fee: want.L1Fee,
+ FeeTokenID: want.FeeTokenID,
+ FeeRate: want.FeeRate,
+ TokenScale: want.TokenScale,
+ FeeLimit: want.FeeLimit,
+ Version: want.Version,
+ Reference: derefReference(want.Reference),
+ Memo: derefMemo(want.Memo),
+ }
+ for i, log := range want.Logs {
+ stored.Logs[i] = (*LogForStorage)(log)
+ }
+ return rlp.EncodeToBytes(stored)
+}
+
+// derefMemo converts *[]byte to []byte for test encoding
+func derefMemo(m *[]byte) []byte {
+ if m == nil {
+ return nil
+ }
+ return *m
+}
+
+// derefReference converts *common.Reference to []byte for test encoding
+func derefReference(r *common.Reference) []byte {
+ if r == nil {
+ return nil
+ }
+ return r[:]
+}
+
+func encodeAsV8StoredReceiptRLP(want *Receipt) ([]byte, error) {
+ stored := &v8StoredReceiptRLP{
+ PostStateOrStatus: want.statusEncoding(),
+ CumulativeGasUsed: want.CumulativeGasUsed,
+ Logs: make([]*LogForStorage, len(want.Logs)),
+ L1Fee: want.L1Fee,
+ FeeTokenID: want.FeeTokenID,
+ FeeRate: want.FeeRate,
+ TokenScale: want.TokenScale,
+ FeeLimit: want.FeeLimit,
+ Version: want.Version,
+ Reference: derefReference(want.Reference),
+ Memo: derefMemo(want.Memo),
}
for i, log := range want.Logs {
stored.Logs[i] = (*LogForStorage)(log)
@@ -501,22 +661,22 @@ func TestReceiptMarshalBinary(t *testing.T) {
t.Errorf("encoded RLP mismatch, got %x want %x", have, eip1559Want)
}
- // alt fee Receipt
+ // MorphTx Receipt
buf.Reset()
- altFeeReceipt.Bloom = CreateBloom(Receipts{altFeeReceipt})
- have, err = altFeeReceipt.MarshalBinary()
+ morphTxReceipt.Bloom = CreateBloom(Receipts{morphTxReceipt})
+ have, err = morphTxReceipt.MarshalBinary()
if err != nil {
t.Fatalf("marshal binary error: %v", err)
}
- altFeeReceipts := Receipts{altFeeReceipt}
- altFeeReceipts.EncodeIndex(0, buf)
+ morphTxReceipts := Receipts{morphTxReceipt}
+ morphTxReceipts.EncodeIndex(0, buf)
haveEncodeIndex = buf.Bytes()
if !bytes.Equal(have, haveEncodeIndex) {
t.Errorf("BinaryMarshal and EncodeIndex mismatch, got %x want %x", have, haveEncodeIndex)
}
- altFeeWant := common.FromHex("02f901c58001b9010000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000010000080000000000000000000004000000000000000000000000000040000000000000000000000000000800000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000f8bef85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100fff85d940000000000000000000000000000000000000111f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff")
- if !bytes.Equal(have, altFeeWant) {
- t.Errorf("encoded RLP mismatch, got %x want %x", have, altFeeWant)
+ morphTxWant := common.FromHex("7ff901c58001b9010000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000010000080000000000000000000004000000000000000000000000000040000000000000000000000000000800000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000f8bef85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100fff85d940000000000000000000000000000000000000111f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff")
+ if !bytes.Equal(have, morphTxWant) {
+ t.Errorf("encoded RLP mismatch, got %x want %x", have, morphTxWant)
}
}
@@ -593,3 +753,572 @@ func clearComputedFieldsOnLog(t *testing.T, log *Log) {
log.TxIndex = math.MaxUint32
log.Index = math.MaxUint32
}
+
+// TestMorphTxReceiptStorageEncoding tests the storage encoding/decoding of MorphTx receipts
+// with Version, Reference, and Memo fields.
+func TestMorphTxReceiptStorageEncoding(t *testing.T) {
+ tests := []struct {
+ name string
+ receipt *Receipt
+ }{
+ {
+ name: "MorphTx V1 with all fields",
+ receipt: morphTxV1Receipt,
+ },
+ {
+ name: "MorphTx V0 (legacy format)",
+ receipt: morphTxV0Receipt,
+ },
+ {
+ name: "MorphTx V1 with Reference only",
+ receipt: morphTxRefOnlyReceipt,
+ },
+ {
+ name: "MorphTx V1 with Memo only",
+ receipt: morphTxMemoOnlyReceipt,
+ },
+ {
+ name: "MorphTx V1 with empty Memo",
+ receipt: morphTxEmptyMemoReceipt,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ // Encode the receipt
+ enc, err := encodeAsStoredReceiptRLP(tc.receipt)
+ if err != nil {
+ t.Fatalf("Error encoding receipt: %v", err)
+ }
+
+ // Decode the receipt
+ var dec ReceiptForStorage
+ if err := rlp.DecodeBytes(enc, &dec); err != nil {
+ t.Fatalf("Error decoding RLP receipt: %v", err)
+ }
+
+ // Check consensus fields
+ if dec.Status != tc.receipt.Status {
+ t.Errorf("Status mismatch, want %v, have %v", tc.receipt.Status, dec.Status)
+ }
+ if dec.CumulativeGasUsed != tc.receipt.CumulativeGasUsed {
+ t.Errorf("CumulativeGasUsed mismatch, want %v, have %v", tc.receipt.CumulativeGasUsed, dec.CumulativeGasUsed)
+ }
+ if len(dec.Logs) != len(tc.receipt.Logs) {
+ t.Errorf("Logs count mismatch, want %v, have %v", len(tc.receipt.Logs), len(dec.Logs))
+ }
+
+ // Check MorphTx fields
+ if dec.Version != tc.receipt.Version {
+ t.Errorf("Version mismatch, want %v, have %v", tc.receipt.Version, dec.Version)
+ }
+ if !compareReference(dec.Reference, tc.receipt.Reference) {
+ t.Errorf("Reference mismatch, want %v, have %v", tc.receipt.Reference, dec.Reference)
+ }
+ if !compareMemo(dec.Memo, tc.receipt.Memo) {
+ t.Errorf("Memo mismatch, want %v, have %v", tc.receipt.Memo, dec.Memo)
+ }
+
+ // Check other MorphTx fields
+ if !compareFeeTokenID(dec.FeeTokenID, tc.receipt.FeeTokenID) {
+ t.Errorf("FeeTokenID mismatch, want %v, have %v", tc.receipt.FeeTokenID, dec.FeeTokenID)
+ }
+ if !compareBigInt(dec.L1Fee, tc.receipt.L1Fee) {
+ t.Errorf("L1Fee mismatch, want %v, have %v", tc.receipt.L1Fee, dec.L1Fee)
+ }
+ })
+ }
+}
+
+// Helper functions for comparing receipt fields that may have different nil/zero representations after RLP encoding
+func compareReference(a, b *common.Reference) bool {
+ if a == nil && b == nil {
+ return true
+ }
+ if a == nil || b == nil {
+ // After RLP decode, nil becomes zero Reference
+ if a == nil {
+ return *b == common.Reference{}
+ }
+ return *a == common.Reference{}
+ }
+ return *a == *b
+}
+
+func compareMemo(a, b *[]byte) bool {
+ if a == nil && b == nil {
+ return true
+ }
+ if a == nil || b == nil {
+ // After RLP decode, nil becomes empty slice
+ if a == nil {
+ return len(*b) == 0
+ }
+ return len(*a) == 0
+ }
+ return bytes.Equal(*a, *b)
+}
+
+func compareFeeTokenID(a, b *uint16) bool {
+ if a == nil && b == nil {
+ return true
+ }
+ if a == nil || b == nil {
+ // After RLP decode, nil becomes 0
+ if a == nil {
+ return *b == 0
+ }
+ return *a == 0
+ }
+ return *a == *b
+}
+
+func compareBigInt(a, b *big.Int) bool {
+ if a == nil && b == nil {
+ return true
+ }
+ if a == nil || b == nil {
+ // After RLP decode, nil becomes 0
+ if a == nil {
+ return b.Sign() == 0
+ }
+ return a.Sign() == 0
+ }
+ return a.Cmp(b) == 0
+}
+
+// TestMorphTxReceiptV8StorageEncoding tests V8 storage format specifically
+func TestMorphTxReceiptV8StorageEncoding(t *testing.T) {
+ tests := []struct {
+ name string
+ receipt *Receipt
+ }{
+ {
+ name: "MorphTx V1 with all fields",
+ receipt: morphTxV1Receipt,
+ },
+ {
+ name: "MorphTx V0 (legacy format)",
+ receipt: morphTxV0Receipt,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ // Encode using V8 format
+ enc, err := encodeAsV8StoredReceiptRLP(tc.receipt)
+ if err != nil {
+ t.Fatalf("Error encoding receipt: %v", err)
+ }
+
+ // Decode the receipt
+ var dec ReceiptForStorage
+ if err := rlp.DecodeBytes(enc, &dec); err != nil {
+ t.Fatalf("Error decoding RLP receipt: %v", err)
+ }
+
+ // Check new fields
+ if dec.Version != tc.receipt.Version {
+ t.Errorf("Version mismatch, want %v, have %v", tc.receipt.Version, dec.Version)
+ }
+ if !compareReference(dec.Reference, tc.receipt.Reference) {
+ t.Errorf("Reference mismatch, want %v, have %v", tc.receipt.Reference, dec.Reference)
+ }
+ if !compareMemo(dec.Memo, tc.receipt.Memo) {
+ t.Errorf("Memo mismatch, want %v, have %v", tc.receipt.Memo, dec.Memo)
+ }
+ })
+ }
+}
+
+// TestMorphTxReceiptBackwardCompatibility tests that old format receipts (V3-V7)
+// decode correctly with new fields having default values
+func TestMorphTxReceiptBackwardCompatibility(t *testing.T) {
+ // Create a basic receipt without new fields
+ receipt := &Receipt{
+ Status: ReceiptStatusSuccessful,
+ CumulativeGasUsed: 1000,
+ Logs: []*Log{
+ {
+ Address: common.BytesToAddress([]byte{0x11}),
+ Topics: []common.Hash{common.HexToHash("dead")},
+ Data: []byte{0x01},
+ },
+ },
+ L1Fee: big.NewInt(100),
+ }
+
+ tests := []struct {
+ name string
+ encode func(*Receipt) ([]byte, error)
+ }{
+ {"V7StoredReceiptRLP", encodeAsV7StoredReceiptRLP},
+ {"V6StoredReceiptRLP", encodeAsV6StoredReceiptRLP},
+ {"V5StoredReceiptRLP", encodeAsV5StoredReceiptRLP},
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ enc, err := tc.encode(receipt)
+ if err != nil {
+ t.Fatalf("Error encoding receipt: %v", err)
+ }
+
+ var dec ReceiptForStorage
+ if err := rlp.DecodeBytes(enc, &dec); err != nil {
+ t.Fatalf("Error decoding RLP receipt: %v", err)
+ }
+
+ // New fields should have default/zero values
+ if dec.Version != 0 {
+ t.Errorf("Version should be 0 for old format, got %v", dec.Version)
+ }
+ if dec.Reference != nil {
+ t.Errorf("Reference should be nil for old format, got %v", dec.Reference)
+ }
+ if dec.Memo != nil {
+ t.Errorf("Memo should be nil for old format, got %v", dec.Memo)
+ }
+
+ // Original fields should be preserved
+ if dec.Status != receipt.Status {
+ t.Errorf("Status mismatch, want %v, have %v", receipt.Status, dec.Status)
+ }
+ if dec.CumulativeGasUsed != receipt.CumulativeGasUsed {
+ t.Errorf("CumulativeGasUsed mismatch, want %v, have %v", receipt.CumulativeGasUsed, dec.CumulativeGasUsed)
+ }
+ })
+ }
+}
+
+// TestMorphTxReceiptJSONMarshal tests JSON marshaling/unmarshaling of MorphTx receipts
+func TestMorphTxReceiptJSONMarshal(t *testing.T) {
+ tests := []struct {
+ name string
+ receipt *Receipt
+ }{
+ {
+ name: "MorphTx V1 with all fields",
+ receipt: morphTxV1Receipt,
+ },
+ {
+ name: "MorphTx V0 (legacy format)",
+ receipt: morphTxV0Receipt,
+ },
+ {
+ name: "MorphTx V1 with Reference only",
+ receipt: morphTxRefOnlyReceipt,
+ },
+ {
+ name: "MorphTx V1 with Memo only",
+ receipt: morphTxMemoOnlyReceipt,
+ },
+ {
+ name: "MorphTx V1 with empty Memo",
+ receipt: morphTxEmptyMemoReceipt,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ // Marshal to JSON
+ data, err := tc.receipt.MarshalJSON()
+ if err != nil {
+ t.Fatalf("Error marshaling receipt to JSON: %v", err)
+ }
+
+ // Unmarshal from JSON
+ var dec Receipt
+ if err := dec.UnmarshalJSON(data); err != nil {
+ t.Fatalf("Error unmarshaling receipt from JSON: %v", err)
+ }
+
+ // Check new fields
+ if dec.Version != tc.receipt.Version {
+ t.Errorf("Version mismatch, want %v, have %v", tc.receipt.Version, dec.Version)
+ }
+ if !reflect.DeepEqual(dec.Reference, tc.receipt.Reference) {
+ t.Errorf("Reference mismatch, want %v, have %v", tc.receipt.Reference, dec.Reference)
+ }
+ if !reflect.DeepEqual(dec.Memo, tc.receipt.Memo) {
+ t.Errorf("Memo mismatch, want %v, have %v", tc.receipt.Memo, dec.Memo)
+ }
+
+ // Check core fields
+ if dec.Status != tc.receipt.Status {
+ t.Errorf("Status mismatch, want %v, have %v", tc.receipt.Status, dec.Status)
+ }
+ if dec.CumulativeGasUsed != tc.receipt.CumulativeGasUsed {
+ t.Errorf("CumulativeGasUsed mismatch, want %v, have %v", tc.receipt.CumulativeGasUsed, dec.CumulativeGasUsed)
+ }
+ if dec.Type != tc.receipt.Type {
+ t.Errorf("Type mismatch, want %v, have %v", tc.receipt.Type, dec.Type)
+ }
+ })
+ }
+}
+
+// TestDeriveFieldsWithMorphTx tests DeriveFields with MorphTx transactions
+func TestDeriveFieldsWithMorphTx(t *testing.T) {
+ to := common.HexToAddress("0x1")
+ ref := common.HexToReference("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890")
+ memo := []byte("derive fields test memo")
+
+ // Create transactions including MorphTx
+ txs := Transactions{
+ NewTx(&LegacyTx{
+ Nonce: 1,
+ Value: big.NewInt(1),
+ Gas: 1,
+ GasPrice: big.NewInt(1),
+ }),
+ NewTx(&MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 2,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(1),
+ Gas: 2,
+ To: &to,
+ Value: big.NewInt(2),
+ Version: MorphTxVersion1,
+ Reference: &ref,
+ Memo: &memo,
+ }),
+ NewTx(&MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 3,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(1),
+ Gas: 3,
+ To: &to,
+ Value: big.NewInt(3),
+ Version: MorphTxVersion0,
+ FeeTokenID: 1,
+ }),
+ }
+
+ // Create corresponding receipts
+ receipts := Receipts{
+ &Receipt{
+ Status: ReceiptStatusSuccessful,
+ CumulativeGasUsed: 1,
+ Logs: []*Log{{Address: common.BytesToAddress([]byte{0x11})}},
+ TxHash: txs[0].Hash(),
+ GasUsed: 1,
+ },
+ &Receipt{
+ Status: ReceiptStatusSuccessful,
+ CumulativeGasUsed: 3,
+ Logs: []*Log{{Address: common.BytesToAddress([]byte{0x22})}},
+ TxHash: txs[1].Hash(),
+ GasUsed: 2,
+ Version: MorphTxVersion1,
+ Reference: &ref,
+ Memo: &memo,
+ },
+ &Receipt{
+ Status: ReceiptStatusSuccessful,
+ CumulativeGasUsed: 6,
+ Logs: []*Log{{Address: common.BytesToAddress([]byte{0x33})}},
+ TxHash: txs[2].Hash(),
+ GasUsed: 3,
+ Version: MorphTxVersion0,
+ },
+ }
+
+ // Test DeriveFields
+ number := big.NewInt(1)
+ blockTime := uint64(2)
+ hash := common.BytesToHash([]byte{0x03, 0x14})
+
+ clearComputedFieldsOnReceipts(t, receipts)
+ if err := receipts.DeriveFields(params.TestChainConfig, hash, number.Uint64(), blockTime, nil, txs); err != nil {
+ t.Fatalf("DeriveFields(...) = %v, want ", err)
+ }
+
+ // Verify MorphTx receipts
+ for i := range receipts {
+ if receipts[i].Type != txs[i].Type() {
+ t.Errorf("receipts[%d].Type = %d, want %d", i, receipts[i].Type, txs[i].Type())
+ }
+ if receipts[i].TxHash != txs[i].Hash() {
+ t.Errorf("receipts[%d].TxHash = %s, want %s", i, receipts[i].TxHash.String(), txs[i].Hash().String())
+ }
+ if receipts[i].BlockHash != hash {
+ t.Errorf("receipts[%d].BlockHash = %s, want %s", i, receipts[i].BlockHash.String(), hash.String())
+ }
+ if receipts[i].TransactionIndex != uint(i) {
+ t.Errorf("receipts[%d].TransactionIndex = %d, want %d", i, receipts[i].TransactionIndex, i)
+ }
+ }
+
+ // Verify MorphTx V1 receipt preserves Version/Reference/Memo
+ if receipts[1].Version != MorphTxVersion1 {
+ t.Errorf("receipts[1].Version = %d, want %d", receipts[1].Version, MorphTxVersion1)
+ }
+ if !reflect.DeepEqual(receipts[1].Reference, &ref) {
+ t.Errorf("receipts[1].Reference = %v, want %v", receipts[1].Reference, &ref)
+ }
+ if !reflect.DeepEqual(receipts[1].Memo, &memo) {
+ t.Errorf("receipts[1].Memo = %v, want %v", receipts[1].Memo, &memo)
+ }
+
+ // Verify MorphTx V0 receipt
+ if receipts[2].Version != MorphTxVersion0 {
+ t.Errorf("receipts[2].Version = %d, want %d", receipts[2].Version, MorphTxVersion0)
+ }
+}
+
+// TestMorphTxReceiptVersionValues tests specific version values
+func TestMorphTxReceiptVersionValues(t *testing.T) {
+ feeTokenID := uint16(1)
+ zeroRef := common.Reference{}
+ emptyMemo := []byte{}
+ tests := []struct {
+ name string
+ version uint8
+ expectVersion uint8
+ }{
+ {"Version 0", MorphTxVersion0, 0},
+ {"Version 1", MorphTxVersion1, 1},
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ receipt := &Receipt{
+ Status: ReceiptStatusSuccessful,
+ CumulativeGasUsed: 100,
+ Logs: []*Log{},
+ Type: MorphTxType,
+ Version: tc.version,
+ FeeTokenID: &feeTokenID,
+ FeeRate: big.NewInt(100),
+ TokenScale: big.NewInt(18),
+ FeeLimit: big.NewInt(1000),
+ L1Fee: big.NewInt(50),
+ Reference: &zeroRef, // Use zero value for RLP compatibility
+ Memo: &emptyMemo, // Use empty for RLP compatibility
+ }
+
+ // Encode
+ enc, err := encodeAsStoredReceiptRLP(receipt)
+ if err != nil {
+ t.Fatalf("Error encoding receipt: %v", err)
+ }
+
+ // Decode
+ var dec ReceiptForStorage
+ if err := rlp.DecodeBytes(enc, &dec); err != nil {
+ t.Fatalf("Error decoding RLP receipt: %v", err)
+ }
+
+ if dec.Version != tc.expectVersion {
+ t.Errorf("Version mismatch, want %v, have %v", tc.expectVersion, dec.Version)
+ }
+ })
+ }
+}
+
+// TestMorphTxReceiptMemoEdgeCases tests edge cases for Memo field
+func TestMorphTxReceiptMemoEdgeCases(t *testing.T) {
+ feeTokenID := uint16(1)
+ zeroRef := common.Reference{}
+ tests := []struct {
+ name string
+ memo *[]byte
+ }{
+ // Note: nil memo cannot be tested with storedReceiptRLP as RLP encoding changes the element count
+ {"empty memo", func() *[]byte { m := []byte{}; return &m }()},
+ {"single byte memo", func() *[]byte { m := []byte{0x01}; return &m }()},
+ {"max length memo", func() *[]byte { m := make([]byte, common.MaxMemoLength); return &m }()},
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ receipt := &Receipt{
+ Status: ReceiptStatusSuccessful,
+ CumulativeGasUsed: 100,
+ Logs: []*Log{},
+ Type: MorphTxType,
+ Version: MorphTxVersion1,
+ Memo: tc.memo,
+ FeeTokenID: &feeTokenID,
+ FeeRate: big.NewInt(100),
+ TokenScale: big.NewInt(18),
+ FeeLimit: big.NewInt(1000),
+ L1Fee: big.NewInt(50),
+ Reference: &zeroRef, // Required for RLP compatibility
+ }
+
+ // Encode
+ enc, err := encodeAsStoredReceiptRLP(receipt)
+ if err != nil {
+ t.Fatalf("Error encoding receipt: %v", err)
+ }
+
+ // Decode
+ var dec ReceiptForStorage
+ if err := rlp.DecodeBytes(enc, &dec); err != nil {
+ t.Fatalf("Error decoding RLP receipt: %v", err)
+ }
+
+ if !compareMemo(dec.Memo, tc.memo) {
+ t.Errorf("Memo mismatch, want %v, have %v", tc.memo, dec.Memo)
+ }
+ })
+ }
+}
+
+// TestMorphTxReceiptReferenceEdgeCases tests edge cases for Reference field
+func TestMorphTxReceiptReferenceEdgeCases(t *testing.T) {
+ feeTokenID := uint16(1)
+ emptyMemo := []byte{}
+ zeroRef := common.Reference{}
+ fullRef := common.HexToReference("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
+ partialRef := common.HexToReference("0x1234567890abcdef1234567890abcdef00000000000000000000000000000000")
+
+ tests := []struct {
+ name string
+ reference *common.Reference
+ }{
+ // Note: nil reference cannot be tested with storedReceiptRLP as RLP encoding changes the element count
+ {"zero reference", &zeroRef},
+ {"full reference", &fullRef},
+ {"partial reference", &partialRef},
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ receipt := &Receipt{
+ Status: ReceiptStatusSuccessful,
+ CumulativeGasUsed: 100,
+ Logs: []*Log{},
+ Type: MorphTxType,
+ Version: MorphTxVersion1,
+ Reference: tc.reference,
+ FeeTokenID: &feeTokenID,
+ FeeRate: big.NewInt(100),
+ TokenScale: big.NewInt(18),
+ FeeLimit: big.NewInt(1000),
+ L1Fee: big.NewInt(50),
+ Memo: &emptyMemo, // Required for RLP compatibility
+ }
+
+ // Encode
+ enc, err := encodeAsStoredReceiptRLP(receipt)
+ if err != nil {
+ t.Fatalf("Error encoding receipt: %v", err)
+ }
+
+ // Decode
+ var dec ReceiptForStorage
+ if err := rlp.DecodeBytes(enc, &dec); err != nil {
+ t.Fatalf("Error decoding RLP receipt: %v", err)
+ }
+
+ if !compareReference(dec.Reference, tc.reference) {
+ t.Errorf("Reference mismatch, want %v, have %v", tc.reference, dec.Reference)
+ }
+ })
+ }
+}
diff --git a/core/types/transaction.go b/core/types/transaction.go
index c164886e5..74eda4c44 100644
--- a/core/types/transaction.go
+++ b/core/types/transaction.go
@@ -33,17 +33,22 @@ import (
)
var (
- ErrInvalidSig = errors.New("invalid transaction v, r, s values")
- ErrUnexpectedProtection = errors.New("transaction type does not supported EIP-155 protected signatures")
- ErrInvalidTxType = errors.New("transaction type not valid in this context")
- ErrCostNotSupported = errors.New("cost function alt fee transaction not support or use gasFee()")
- ErrTxTypeNotSupported = errors.New("transaction type not supported")
- ErrGasFeeCapTooLow = errors.New("fee cap less than base fee")
- errEmptyTypedTx = errors.New("empty typed transaction bytes")
- errShortTypedTx = errors.New("typed transaction too short")
- errInvalidYParity = errors.New("'yParity' field must be 0 or 1")
- errVYParityMismatch = errors.New("'v' and 'yParity' fields do not match")
- errVYParityMissing = errors.New("missing 'yParity' or 'v' field in transaction")
+ ErrInvalidSig = errors.New("invalid transaction v, r, s values")
+ ErrUnexpectedProtection = errors.New("transaction type does not supported EIP-155 protected signatures")
+ ErrInvalidTxType = errors.New("transaction type not valid in this context")
+ ErrCostNotSupported = errors.New("cost function morph transaction not support or use gasFee()")
+ ErrTxTypeNotSupported = errors.New("transaction type not supported")
+ ErrGasFeeCapTooLow = errors.New("fee cap less than base fee")
+ ErrMemoTooLong = errors.New("memo exceeds maximum length of 64 bytes")
+ ErrMorphTxV0IllegalExtraParams = errors.New("illegal extra parameters of version 0 MorphTx")
+ ErrMorphTxV1IllegalExtraParams = errors.New("illegal extra parameters of version 1 MorphTx")
+ ErrMorphTxUnsupportedVersion = errors.New("unsupported MorphTx version")
+ ErrMorphTxV1NotYetActive = errors.New("MorphTx version 1 is not yet active (jade fork not reached)")
+ errEmptyTypedTx = errors.New("empty typed transaction bytes")
+ errShortTypedTx = errors.New("typed transaction too short")
+ errInvalidYParity = errors.New("'yParity' field must be 0 or 1")
+ errVYParityMismatch = errors.New("'v' and 'yParity' fields do not match")
+ errVYParityMissing = errors.New("missing 'yParity' or 'v' field in transaction")
)
// Transaction types.
@@ -55,7 +60,7 @@ const (
SetCodeTxType = 0x04
L1MessageTxType = 0x7E
- AltFeeTxType = 0x7F
+ MorphTxType = 0x7F
)
// Transaction is an Ethereum transaction.
@@ -220,8 +225,8 @@ func (tx *Transaction) decodeTyped(b []byte) (TxData, error) {
inner = new(L1MessageTx)
case SetCodeTxType:
inner = new(SetCodeTx)
- case AltFeeTxType:
- inner = new(AltFeeTx)
+ case MorphTxType:
+ inner = new(MorphTx)
default:
return nil, ErrTxTypeNotSupported
}
@@ -331,9 +336,14 @@ func (tx *Transaction) IsL1MessageTx() bool {
return tx.Type() == L1MessageTxType
}
-// IsAltFeeTx returns true if the transaction is erc20 fee tx.
-func (tx *Transaction) IsAltFeeTx() bool {
- return tx.Type() == AltFeeTxType
+// IsMorphTx returns true if the transaction is morph tx.
+func (tx *Transaction) IsMorphTx() bool {
+ return tx.Type() == MorphTxType
+}
+
+// IsMorphTxWithAltFee returns true if the transaction is morph tx with alt fee.
+func (tx *Transaction) IsMorphTxWithAltFee() bool {
+ return tx.IsMorphTx() && tx.FeeTokenID() != 0
}
// AsL1MessageTx casts the tx into an L1 cross-domain tx.
@@ -353,33 +363,93 @@ func (tx *Transaction) L1MessageQueueIndex() uint64 {
return tx.AsL1MessageTx().QueueIndex
}
-// AsAltFeeTx casts the tx into an erc20 fee tx.
-func (tx *Transaction) AsAltFeeTx() *AltFeeTx {
- if !tx.IsAltFeeTx() {
+// AsMorphTx casts the tx into an morph tx.
+func (tx *Transaction) AsMorphTx() *MorphTx {
+ if !tx.IsMorphTx() {
return nil
}
- return tx.inner.(*AltFeeTx)
+ return tx.inner.(*MorphTx)
}
func (tx *Transaction) FeeTokenID() uint16 {
- if !tx.IsAltFeeTx() {
+ if !tx.IsMorphTx() {
return 0
}
- return tx.AsAltFeeTx().FeeTokenID
+ return tx.AsMorphTx().FeeTokenID
}
func (tx *Transaction) FeeLimit() *big.Int {
- if !tx.IsAltFeeTx() {
+ if !tx.IsMorphTx() {
return big.NewInt(0)
}
- return tx.AsAltFeeTx().FeeLimit
+ return tx.AsMorphTx().FeeLimit
+}
+
+// Version returns the version of the MorphTx, or 0 if not a MorphTx.
+func (tx *Transaction) Version() uint8 {
+ if !tx.IsMorphTx() {
+ return 0
+ }
+ return tx.AsMorphTx().Version
+}
+
+// Reference returns the reference of the MorphTx, or nil if not a MorphTx.
+func (tx *Transaction) Reference() *common.Reference {
+ if !tx.IsMorphTx() {
+ return nil
+ }
+ return tx.AsMorphTx().Reference
+}
+
+// Memo returns the memo of the MorphTx, or nil if not a MorphTx.
+func (tx *Transaction) Memo() *[]byte {
+ if !tx.IsMorphTx() {
+ return nil
+ }
+ return tx.AsMorphTx().Memo
+}
+
+// ValidateMorphTxVersion validates the MorphTx version, memo length, and associated field requirements.
+// Rules:
+// - Memo must not exceed MaxMemoLength bytes
+// - Version 0 (legacy format): FeeTokenID must be > 0, Reference and Memo must not be set
+// - Version 1 (with Reference/Memo): FeeTokenID, Reference, Memo are all optional;
+// if FeeTokenID is 0, FeeLimit must not be set
+// - Other versions: not supported
+//
+// Returns nil if the transaction is not a MorphTx or if all checks pass.
+func (tx *Transaction) ValidateMorphTxVersion() error {
+ if !tx.IsMorphTx() {
+ return nil
+ }
+ morphTx := tx.AsMorphTx()
+
+ switch morphTx.Version {
+ case MorphTxVersion0:
+ // Version 0 requires FeeTokenID > 0 (legacy format used for alt-fee transactions)
+ if morphTx.FeeTokenID == 0 ||
+ morphTx.Reference != nil && *morphTx.Reference != (common.Reference{}) ||
+ morphTx.Memo != nil && len(*morphTx.Memo) > 0 {
+ return ErrMorphTxV0IllegalExtraParams
+ }
+ case MorphTxVersion1:
+ // Version 1: FeeTokenID, Reference, Memo are all optional
+ // If FeeTokenID is 0, FeeLimit must not be set
+ if morphTx.FeeTokenID == 0 && morphTx.FeeLimit != nil && morphTx.FeeLimit.Sign() != 0 {
+ return ErrMorphTxV1IllegalExtraParams
+ }
+ // Validate memo length
+ if morphTx.Memo != nil && len(*morphTx.Memo) > common.MaxMemoLength {
+ return ErrMemoTooLong
+ }
+ default:
+ return ErrMorphTxUnsupportedVersion
+ }
+ return nil
}
// Cost returns gas * gasPrice + value.
func (tx *Transaction) Cost() *big.Int {
- if tx.IsAltFeeTx() {
- panic(ErrCostNotSupported)
- }
total := tx.GasFee()
total.Add(total, tx.Value())
return total
@@ -791,9 +861,30 @@ type Message struct {
setCodeAuthorizations []SetCodeAuthorization
feeTokenID uint16
feeLimit *big.Int
-}
-
-func NewMessage(from common.Address, to *common.Address, nonce uint64, amount *big.Int, gasLimit uint64, gasPrice, gasFeeCap, gasTipCap *big.Int, feeTokenID uint16, feeLimit *big.Int, data []byte, accessList AccessList, authList []SetCodeAuthorization, isFake bool) Message {
+ version uint8
+ reference *common.Reference
+ memo *[]byte
+}
+
+func NewMessage(
+ from common.Address,
+ to *common.Address,
+ nonce uint64,
+ amount *big.Int,
+ gasLimit uint64,
+ gasPrice *big.Int,
+ gasFeeCap *big.Int,
+ gasTipCap *big.Int,
+ feeTokenID uint16,
+ feeLimit *big.Int,
+ version uint8,
+ reference *common.Reference,
+ memo *[]byte,
+ data []byte,
+ accessList AccessList,
+ authList []SetCodeAuthorization,
+ isFake bool,
+) Message {
return Message{
from: from,
to: to,
@@ -810,6 +901,9 @@ func NewMessage(from common.Address, to *common.Address, nonce uint64, amount *b
isL1MessageTx: false,
feeTokenID: feeTokenID,
feeLimit: feeLimit,
+ version: version,
+ reference: reference,
+ memo: memo,
}
}
@@ -829,17 +923,23 @@ func (tx *Transaction) AsMessage(s Signer, baseFee *big.Int) (Message, error) {
isL1MessageTx: tx.IsL1MessageTx(),
setCodeAuthorizations: tx.SetCodeAuthorizations(),
feeTokenID: tx.FeeTokenID(),
+ version: tx.Version(),
+ reference: tx.Reference(),
+ memo: tx.Memo(),
}
// If baseFee provided, set gasPrice to effectiveGasPrice.
if baseFee != nil {
msg.gasPrice = math.BigMin(msg.gasPrice.Add(msg.gasTipCap, baseFee), msg.gasFeeCap)
}
- if tx.IsAltFeeTx() && tx.FeeTokenID() == 0 {
- return msg, errors.New("token id 0 not support")
+
+ if err := tx.ValidateMorphTxVersion(); err != nil {
+ return Message{}, err
}
+
if tx.FeeLimit() != nil {
msg.feeLimit = tx.FeeLimit()
}
+
var err error
msg.from, err = Sender(s, tx)
return msg, err
@@ -860,6 +960,9 @@ func (m Message) IsL1MessageTx() bool { return m.isL1M
func (m Message) SetCodeAuthorizations() []SetCodeAuthorization { return m.setCodeAuthorizations }
func (m Message) FeeTokenID() uint16 { return m.feeTokenID }
func (m Message) FeeLimit() *big.Int { return m.feeLimit }
+func (m Message) Version() uint8 { return m.version }
+func (m Message) Reference() *common.Reference { return m.reference }
+func (m Message) Memo() *[]byte { return m.memo }
// copyAddressPtr copies an address.
func copyAddressPtr(a *common.Address) *common.Address {
@@ -870,6 +973,25 @@ func copyAddressPtr(a *common.Address) *common.Address {
return &cpy
}
+// copyReferencePtr copies a common.Reference and returns a pointer to the copy.
+func copyReferencePtr(h *common.Reference) *common.Reference {
+ if h == nil {
+ return nil
+ }
+ cpy := *h
+ return &cpy
+}
+
+// copyBytesPtr creates a deep copy of a byte slice pointer.
+func copyBytesPtr(b *[]byte) *[]byte {
+ if b == nil {
+ return nil
+ }
+ cpy := make([]byte, len(*b))
+ copy(cpy, *b)
+ return &cpy
+}
+
type SkippedTransaction struct {
Tx *Transaction
Reason string
diff --git a/core/types/transaction_marshalling.go b/core/types/transaction_marshalling.go
index d7f43b193..bf5e72e08 100644
--- a/core/types/transaction_marshalling.go
+++ b/core/types/transaction_marshalling.go
@@ -62,9 +62,12 @@ type txJSON struct {
Sender *common.Address `json:"sender,omitempty"`
QueueIndex *hexutil.Uint64 `json:"queueIndex,omitempty"`
- // Alt fee transaction fields:
- FeeTokenID hexutil.Uint16 `json:"feeTokenID"`
- FeeLimit *hexutil.Big `json:"feeLimit"`
+ // MorphTx transaction fields:
+ FeeTokenID hexutil.Uint16 `json:"feeTokenID"`
+ FeeLimit *hexutil.Big `json:"feeLimit"`
+ Version *hexutil.Uint64 `json:"version"`
+ Reference *common.Reference `json:"reference"`
+ Memo *hexutil.Bytes `json:"memo"`
}
// yParityValue returns the YParity value from JSON. For backwards-compatibility reasons,
@@ -189,7 +192,7 @@ func (tx *Transaction) MarshalJSON() ([]byte, error) {
yparity := itx.V.Uint64()
enc.YParity = (*hexutil.Uint64)(&yparity)
- case *AltFeeTx:
+ case *MorphTx:
enc.ChainID = (*hexutil.Big)(itx.ChainID)
enc.Nonce = (*hexutil.Uint64)(&itx.Nonce)
enc.To = tx.To()
@@ -201,6 +204,13 @@ func (tx *Transaction) MarshalJSON() ([]byte, error) {
enc.AccessList = &itx.AccessList
enc.FeeTokenID = hexutil.Uint16(itx.FeeTokenID)
enc.FeeLimit = (*hexutil.Big)(itx.FeeLimit)
+ // Only include V1 fields (version, reference, memo) for V1+ transactions
+ if itx.Version >= MorphTxVersion1 {
+ v := hexutil.Uint64(itx.Version)
+ enc.Version = &v
+ enc.Reference = (*common.Reference)(itx.Reference)
+ enc.Memo = (*hexutil.Bytes)(itx.Memo)
+ }
enc.V = (*hexutil.Big)(itx.V)
enc.R = (*hexutil.Big)(itx.R)
enc.S = (*hexutil.Big)(itx.S)
@@ -567,8 +577,8 @@ func (tx *Transaction) UnmarshalJSON(input []byte) error {
}
}
- case AltFeeTxType:
- var itx AltFeeTx
+ case MorphTxType:
+ var itx MorphTx
inner = &itx
if dec.ChainID == nil {
return errors.New("missing required field 'chainId' in transaction")
@@ -599,6 +609,11 @@ func (tx *Transaction) UnmarshalJSON(input []byte) error {
itx.FeeTokenID = uint16(dec.FeeTokenID)
itx.FeeLimit = (*big.Int)(dec.FeeLimit)
itx.Value = (*big.Int)(dec.Value)
+ if dec.Version != nil {
+ itx.Version = uint8(*dec.Version)
+ }
+ itx.Reference = (*common.Reference)(dec.Reference)
+ itx.Memo = (*[]byte)(dec.Memo)
if dec.Input == nil {
return errors.New("missing required field 'input' in transaction")
}
diff --git a/core/types/transaction_signing.go b/core/types/transaction_signing.go
index 767a4d62f..8204497ef 100644
--- a/core/types/transaction_signing.go
+++ b/core/types/transaction_signing.go
@@ -229,7 +229,7 @@ func newModernSigner(chainID *big.Int, fork forks.Fork) Signer {
s.txtypes[SetCodeTxType] = struct{}{}
}
if fork >= forks.Emerald {
- s.txtypes[AltFeeTxType] = struct{}{}
+ s.txtypes[MorphTxType] = struct{}{}
}
return s
}
diff --git a/core/types/transaction_test.go b/core/types/transaction_test.go
index 28da3d85b..157d59928 100644
--- a/core/types/transaction_test.go
+++ b/core/types/transaction_test.go
@@ -20,6 +20,7 @@ import (
"bytes"
"crypto/ecdsa"
"encoding/json"
+ "errors"
"fmt"
"math/big"
"math/rand"
@@ -70,13 +71,78 @@ var (
NewEIP2930Signer(big.NewInt(1)),
common.Hex2Bytes("c9519f4f2b30335884581971573fadf60c6204f59a911df35ee8a540456b266032f1e8e2c5dd761f9e4f88f41c8310aeaba26a8bfcdacfedfa12ec3862d3752101"),
)
+
+ // MorphTx test fixtures
+ testMorphTxReference = common.HexToReference("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")
+ testMorphTxMemo = []byte("test memo data for morph tx")
+
+ // MorphTx Version 0 (legacy format with alt fee)
+ emptyMorphTxV0 = NewTx(&MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 3,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 25000,
+ To: &testAddr,
+ Value: big.NewInt(10),
+ Data: common.FromHex("5544"),
+ AccessList: nil,
+ FeeTokenID: 1, // V0 requires FeeTokenID > 0
+ FeeLimit: big.NewInt(1000000),
+ Version: MorphTxVersion0,
+ })
+
+ // MorphTx Version 1 (with Reference and Memo)
+ emptyMorphTxV1 = NewTx(&MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 3,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 25000,
+ To: &testAddr,
+ Value: big.NewInt(10),
+ Data: common.FromHex("5544"),
+ AccessList: nil,
+ FeeTokenID: 0, // V1 allows FeeTokenID = 0
+ FeeLimit: big.NewInt(0),
+ Version: MorphTxVersion1,
+ Reference: &testMorphTxReference,
+ Memo: &testMorphTxMemo,
+ })
+
+ // MorphTx V1 with only Reference (no Memo)
+ morphTxV1RefOnly = NewTx(&MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 4,
+ GasTipCap: big.NewInt(2),
+ GasFeeCap: big.NewInt(20),
+ Gas: 30000,
+ To: &testAddr,
+ Value: big.NewInt(20),
+ Version: MorphTxVersion1,
+ Reference: &testMorphTxReference,
+ })
+
+ // MorphTx V1 with only Memo (no Reference)
+ morphTxV1MemoOnly = NewTx(&MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 5,
+ GasTipCap: big.NewInt(3),
+ GasFeeCap: big.NewInt(30),
+ Gas: 35000,
+ To: &testAddr,
+ Value: big.NewInt(30),
+ Version: MorphTxVersion1,
+ Memo: &testMorphTxMemo,
+ })
)
func TestDecodeEmptyTypedTx(t *testing.T) {
input := []byte{0x80}
var tx Transaction
err := rlp.DecodeBytes(input, &tx)
- if err != errEmptyTypedTx {
+ // The error should be either errEmptyTypedTx or errShortTypedTx
+ if err != errEmptyTypedTx && err != errShortTypedTx {
t.Fatal("wrong error:", err)
}
}
@@ -171,15 +237,17 @@ func TestEIP2930Signer(t *testing.T) {
t.Errorf("test %d: wrong sig hash: got %x, want %x", i, sigHash, test.wantSignerHash)
}
sender, err := Sender(test.signer, test.tx)
- if err != test.wantSenderErr {
- t.Errorf("test %d: wrong Sender error %q", i, err)
+ // Use errors.Is for wrapped errors
+ if !errors.Is(err, test.wantSenderErr) {
+ t.Errorf("test %d: wrong Sender error %q, want %q", i, err, test.wantSenderErr)
}
if err == nil && sender != keyAddr {
t.Errorf("test %d: wrong sender address %x", i, sender)
}
signedTx, err := SignTx(test.tx, test.signer, key)
- if err != test.wantSignErr {
- t.Fatalf("test %d: wrong SignTx error %q", i, err)
+ // Use errors.Is for wrapped errors
+ if !errors.Is(err, test.wantSignErr) {
+ t.Fatalf("test %d: wrong SignTx error %q, want %q", i, err, test.wantSignErr)
}
if signedTx != nil {
if signedTx.Hash() != test.wantHash {
@@ -412,15 +480,18 @@ func TestTransactionCoding(t *testing.T) {
t.Fatalf("could not generate key: %v", err)
}
var (
- signer = NewEIP2930Signer(common.Big1)
- addr = common.HexToAddress("0x0000000000000000000000000000000000000001")
- recipient = common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87")
- accesses = AccessList{{Address: addr, StorageKeys: []common.Hash{{0}}}}
+ signer = NewEIP2930Signer(common.Big1)
+ morphSigner = NewEmeraldSigner(common.Big1) // Signer for MorphTx
+ addr = common.HexToAddress("0x0000000000000000000000000000000000000001")
+ recipient = common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87")
+ accesses = AccessList{{Address: addr, StorageKeys: []common.Hash{{0}}}}
)
- for i := uint64(0); i < 500; i++ {
+ morphTxRef := common.HexToReference("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890")
+ morphTxMemo := []byte("test memo")
+ for i := uint64(0); i < 800; i++ {
var txdata TxData
var isL1MessageTx bool
- switch i % 6 {
+ switch i % 8 {
case 0:
// Legacy tx.
txdata = &LegacyTx{
@@ -479,13 +550,52 @@ func TestTransactionCoding(t *testing.T) {
Data: []byte("abcdef"),
Sender: addr,
}
+ case 6:
+ // MorphTx V0 (legacy format with alt fee)
+ txdata = &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: i,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 123457,
+ To: &recipient,
+ Value: big.NewInt(10),
+ Data: []byte("abcdef"),
+ AccessList: accesses,
+ FeeTokenID: 1, // V0 requires FeeTokenID > 0
+ FeeLimit: big.NewInt(1000000),
+ Version: MorphTxVersion0,
+ }
+ case 7:
+ // MorphTx V1 (with Reference and Memo)
+ txdata = &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: i,
+ GasTipCap: big.NewInt(2),
+ GasFeeCap: big.NewInt(20),
+ Gas: 123457,
+ To: &recipient,
+ Value: big.NewInt(20),
+ Data: []byte("abcdef"),
+ AccessList: accesses,
+ FeeTokenID: 0,
+ FeeLimit: big.NewInt(0),
+ Version: MorphTxVersion1,
+ Reference: &morphTxRef,
+ Memo: &morphTxMemo,
+ }
}
var tx *Transaction
// dont sign L1MessageTx
if isL1MessageTx {
tx = NewTx(txdata)
} else {
- tx, err = SignNewTx(key, signer, txdata)
+ // Use morphSigner for MorphTx types
+ txSigner := signer
+ if _, ok := txdata.(*MorphTx); ok {
+ txSigner = morphSigner
+ }
+ tx, err = SignNewTx(key, txSigner, txdata)
if err != nil {
t.Fatalf("could not sign transaction: %v", err)
}
@@ -566,3 +676,840 @@ func assertEqual(orig *Transaction, cpy *Transaction) error {
}
return nil
}
+
+// ==================== MorphTx Tests ====================
+
+// TestMorphTxSigHash tests the signature hash calculation for MorphTx V0 and V1.
+// These hashes are stable and should not change - any change indicates a breaking change.
+func TestMorphTxSigHash(t *testing.T) {
+ signer := NewEmeraldSigner(big.NewInt(1))
+
+ // Test V0 sigHash (legacy format)
+ v0SigHash := signer.Hash(emptyMorphTxV0)
+ t.Logf("MorphTx V0 sigHash: %s", v0SigHash.Hex())
+ expectedV0Hash := common.HexToHash("0x88cdb3aa657406af62d0c9d752d3f496829487516d70ef1c6172bd69c2ab9c4a")
+ if v0SigHash != expectedV0Hash {
+ t.Errorf("MorphTx V0 sigHash mismatch, got %s, want %s", v0SigHash.Hex(), expectedV0Hash.Hex())
+ }
+
+ // Test V1 sigHash (with Reference/Memo)
+ v1SigHash := signer.Hash(emptyMorphTxV1)
+ t.Logf("MorphTx V1 sigHash: %s", v1SigHash.Hex())
+ expectedV1Hash := common.HexToHash("0xe80adb944285a68b17dfcf50e95e90f19a7ce23352563786f3732bf07be8f8cf")
+ if v1SigHash != expectedV1Hash {
+ t.Errorf("MorphTx V1 sigHash mismatch, got %s, want %s", v1SigHash.Hex(), expectedV1Hash.Hex())
+ }
+
+ // Ensure V0 and V1 sigHashes are different
+ if v0SigHash == v1SigHash {
+ t.Error("MorphTx V0 and V1 sigHashes should be different")
+ }
+}
+
+// TestMorphTxSigner tests signature operations on MorphTx.
+func TestMorphTxSigner(t *testing.T) {
+ key, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
+ keyAddr := crypto.PubkeyToAddress(key.PublicKey)
+ signer := NewEmeraldSigner(big.NewInt(1))
+
+ tests := []struct {
+ name string
+ tx *Transaction
+ wantSenderErr error
+ }{
+ {
+ name: "MorphTx V0 unsigned",
+ tx: emptyMorphTxV0,
+ wantSenderErr: ErrInvalidSig,
+ },
+ {
+ name: "MorphTx V1 unsigned",
+ tx: emptyMorphTxV1,
+ wantSenderErr: ErrInvalidSig,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ // Test unsigned tx returns expected error
+ _, err := Sender(signer, tc.tx)
+ if !errors.Is(err, tc.wantSenderErr) {
+ t.Errorf("Sender error mismatch, got %v, want %v (using errors.Is)", err, tc.wantSenderErr)
+ }
+
+ // Sign the tx
+ signedTx, err := SignTx(tc.tx, signer, key)
+ if err != nil {
+ t.Fatalf("SignTx failed: %v", err)
+ }
+
+ // Verify sender
+ sender, err := Sender(signer, signedTx)
+ if err != nil {
+ t.Fatalf("Sender failed after signing: %v", err)
+ }
+ if sender != keyAddr {
+ t.Errorf("Sender mismatch, got %s, want %s", sender.Hex(), keyAddr.Hex())
+ }
+
+ // Verify hash is stable after signing
+ hash1 := signedTx.Hash()
+ hash2 := signedTx.Hash()
+ if hash1 != hash2 {
+ t.Error("Hash should be stable")
+ }
+ })
+ }
+}
+
+// TestMorphTxEncoding tests RLP and JSON encoding/decoding for MorphTx.
+func TestMorphTxEncoding(t *testing.T) {
+ key, _ := crypto.GenerateKey()
+ signer := NewEmeraldSigner(big.NewInt(1))
+ ref := common.HexToReference("0x1111111111111111111111111111111111111111111111111111111111111111")
+ memo := []byte("encoding test memo")
+
+ zeroRef := common.Reference{}
+ emptyMemo := []byte{}
+
+ tests := []struct {
+ name string
+ tx *MorphTx
+ }{
+ {
+ name: "V0 basic",
+ tx: &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 1,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 21000,
+ To: &testAddr,
+ Value: big.NewInt(100),
+ FeeTokenID: 1,
+ FeeLimit: big.NewInt(1000),
+ Version: MorphTxVersion0,
+ },
+ },
+ {
+ name: "V1 with all fields",
+ tx: &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 2,
+ GasTipCap: big.NewInt(2),
+ GasFeeCap: big.NewInt(20),
+ Gas: 21000,
+ To: &testAddr,
+ Value: big.NewInt(200),
+ FeeTokenID: 0,
+ Version: MorphTxVersion1,
+ Reference: &ref,
+ Memo: &memo,
+ },
+ },
+ {
+ name: "V1 with Reference only",
+ tx: &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 3,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 21000,
+ To: &testAddr,
+ Value: big.NewInt(0),
+ Version: MorphTxVersion1,
+ Reference: &ref,
+ Memo: &emptyMemo, // V1 requires Memo field for RLP encoding
+ },
+ },
+ {
+ name: "V1 with Memo only",
+ tx: &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 4,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 21000,
+ To: &testAddr,
+ Value: big.NewInt(0),
+ Version: MorphTxVersion1,
+ Reference: &zeroRef, // V1 requires Reference field for RLP encoding
+ Memo: &memo,
+ },
+ },
+ {
+ name: "V1 with empty Memo",
+ tx: &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 5,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 21000,
+ To: &testAddr,
+ Value: big.NewInt(0),
+ Version: MorphTxVersion1,
+ Reference: &zeroRef, // V1 requires Reference field for RLP encoding
+ Memo: &emptyMemo,
+ },
+ },
+ {
+ name: "V1 with max length Memo",
+ tx: &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 6,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 21000,
+ To: &testAddr,
+ Value: big.NewInt(0),
+ Version: MorphTxVersion1,
+ Reference: &zeroRef, // V1 requires Reference field for RLP encoding
+ Memo: func() *[]byte { m := make([]byte, common.MaxMemoLength); return &m }(),
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ tx := NewTx(tc.tx)
+ signedTx, err := SignTx(tx, signer, key)
+ if err != nil {
+ t.Fatalf("SignTx failed: %v", err)
+ }
+
+ // Test RLP encoding/decoding
+ parsedTx, err := encodeDecodeBinary(signedTx)
+ if err != nil {
+ t.Fatalf("RLP encode/decode failed: %v", err)
+ }
+ if parsedTx.Hash() != signedTx.Hash() {
+ t.Error("Hash mismatch after RLP encode/decode")
+ }
+ if parsedTx.Version() != signedTx.Version() {
+ t.Errorf("Version mismatch, got %d, want %d", parsedTx.Version(), signedTx.Version())
+ }
+ if !reflect.DeepEqual(parsedTx.Reference(), signedTx.Reference()) {
+ t.Error("Reference mismatch after RLP encode/decode")
+ }
+ if !compareMemoPtr(parsedTx.Memo(), signedTx.Memo()) {
+ t.Error("Memo mismatch after RLP encode/decode")
+ }
+
+ // Test JSON encoding/decoding
+ parsedTx, err = encodeDecodeJSON(signedTx)
+ if err != nil {
+ t.Fatalf("JSON encode/decode failed: %v", err)
+ }
+ if parsedTx.Hash() != signedTx.Hash() {
+ t.Error("Hash mismatch after JSON encode/decode")
+ }
+ if parsedTx.Version() != signedTx.Version() {
+ t.Errorf("Version mismatch after JSON, got %d, want %d", parsedTx.Version(), signedTx.Version())
+ }
+ })
+ }
+}
+
+// TestMorphTxValidation tests ValidateMorphTxVersion (which includes memo validation).
+func TestMorphTxValidation(t *testing.T) {
+ t.Run("ValidateMorphTxVersion", func(t *testing.T) {
+ ref := common.HexToReference("0x1111111111111111111111111111111111111111111111111111111111111111")
+ emptyRef := common.Reference{}
+ memo := []byte("test memo")
+ emptyMemo := []byte{}
+ feeLimit := big.NewInt(1000)
+
+ tests := []struct {
+ name string
+ version uint8
+ feeTokenID uint16
+ feeLimit *big.Int
+ reference *common.Reference
+ memo *[]byte
+ wantErr error
+ }{
+ // Memo validation tests
+ {"nil memo", MorphTxVersion1, 0, nil, nil, nil, nil},
+ {"empty memo", MorphTxVersion1, 0, nil, nil, func() *[]byte { m := []byte{}; return &m }(), nil},
+ {"valid memo", MorphTxVersion1, 0, nil, nil, func() *[]byte { m := []byte("hello"); return &m }(), nil},
+ {"max length memo", MorphTxVersion1, 0, nil, nil, func() *[]byte { m := make([]byte, common.MaxMemoLength); return &m }(), nil},
+ {"over max length memo", MorphTxVersion1, 0, nil, nil, func() *[]byte { m := make([]byte, common.MaxMemoLength+1); return &m }(), ErrMemoTooLong},
+ // Version 0 tests
+ {"V0 with FeeTokenID > 0", MorphTxVersion0, 1, nil, nil, nil, nil},
+ {"V0 with FeeTokenID = 0", MorphTxVersion0, 0, nil, nil, nil, ErrMorphTxV0IllegalExtraParams},
+ {"V0 with Reference", MorphTxVersion0, 1, nil, &ref, nil, ErrMorphTxV0IllegalExtraParams},
+ {"V0 with empty Reference", MorphTxVersion0, 1, nil, &emptyRef, nil, nil},
+ {"V0 with Memo", MorphTxVersion0, 1, nil, nil, &memo, ErrMorphTxV0IllegalExtraParams},
+ {"V0 with empty Memo", MorphTxVersion0, 1, nil, nil, &emptyMemo, nil},
+ {"V0 with Reference and Memo", MorphTxVersion0, 1, nil, &ref, &memo, ErrMorphTxV0IllegalExtraParams},
+ // Version 1 tests
+ {"V1 with FeeTokenID = 0", MorphTxVersion1, 0, nil, nil, nil, nil},
+ {"V1 with FeeTokenID > 0", MorphTxVersion1, 1, nil, nil, nil, nil},
+ {"V1 with Reference", MorphTxVersion1, 0, nil, &ref, nil, nil},
+ {"V1 with Memo", MorphTxVersion1, 0, nil, nil, &memo, nil},
+ {"V1 with Reference and Memo", MorphTxVersion1, 1, nil, &ref, &memo, nil},
+ {"V1 FeeTokenID=0 with FeeLimit", MorphTxVersion1, 0, feeLimit, nil, nil, ErrMorphTxV1IllegalExtraParams},
+ {"V1 FeeTokenID>0 with FeeLimit", MorphTxVersion1, 1, feeLimit, nil, nil, nil},
+ {"V1 FeeTokenID=0 with zero FeeLimit", MorphTxVersion1, 0, big.NewInt(0), nil, nil, nil},
+ // Unsupported versions
+ {"Unsupported version 2", 2, 0, nil, nil, nil, ErrMorphTxUnsupportedVersion},
+ {"Unsupported version 255", 255, 1, nil, nil, nil, ErrMorphTxUnsupportedVersion},
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ tx := NewTx(&MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 1,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 21000,
+ To: &testAddr,
+ Version: tc.version,
+ FeeTokenID: tc.feeTokenID,
+ FeeLimit: tc.feeLimit,
+ Reference: tc.reference,
+ Memo: tc.memo,
+ })
+ err := tx.ValidateMorphTxVersion()
+ if err != tc.wantErr {
+ t.Errorf("ValidateMorphTxVersion error mismatch, got %v, want %v", err, tc.wantErr)
+ }
+ })
+ }
+ })
+
+ t.Run("Non-MorphTx validation", func(t *testing.T) {
+ // ValidateMorphTxVersion on non-MorphTx should return nil
+ legacyTx := NewTx(&LegacyTx{
+ Nonce: 1,
+ GasPrice: big.NewInt(1),
+ Gas: 21000,
+ To: &testAddr,
+ })
+ if err := legacyTx.ValidateMorphTxVersion(); err != nil {
+ t.Errorf("ValidateMorphTxVersion on LegacyTx should return nil, got %v", err)
+ }
+ })
+}
+
+// TestMorphTxAsMessage tests that AsMessage calls ValidateMorphTxVersion
+// and correctly rejects invalid MorphTx while accepting valid ones.
+func TestMorphTxAsMessage(t *testing.T) {
+ key, _ := crypto.GenerateKey()
+ signer := NewEmeraldSigner(big.NewInt(1))
+ ref := common.HexToReference("0x1111111111111111111111111111111111111111111111111111111111111111")
+ memo := []byte("test memo")
+ longMemo := make([]byte, common.MaxMemoLength+1)
+ baseFee := big.NewInt(7)
+
+ tests := []struct {
+ name string
+ txdata *MorphTx
+ wantErr error
+ }{
+ // --- Valid cases ---
+ {
+ name: "V0 with FeeTokenID > 0 → success",
+ txdata: &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 1,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 21000,
+ To: &testAddr,
+ Value: big.NewInt(0),
+ Version: MorphTxVersion0,
+ FeeTokenID: 1,
+ FeeLimit: big.NewInt(1000),
+ },
+ wantErr: nil,
+ },
+ {
+ name: "V1 with FeeTokenID == 0, Reference + Memo → success",
+ txdata: &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 3,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 21000,
+ To: &testAddr,
+ Value: big.NewInt(0),
+ Version: MorphTxVersion1,
+ FeeTokenID: 0,
+ Reference: &ref,
+ Memo: &memo,
+ },
+ wantErr: nil,
+ },
+ {
+ name: "V1 with FeeTokenID > 0, Reference → success",
+ txdata: &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 4,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 21000,
+ To: &testAddr,
+ Value: big.NewInt(0),
+ Version: MorphTxVersion1,
+ FeeTokenID: 1,
+ FeeLimit: big.NewInt(500),
+ Reference: &ref,
+ },
+ wantErr: nil,
+ },
+ // --- V0 rejection cases ---
+ {
+ name: "V0 with FeeTokenID == 0 → ErrMorphTxV0IllegalExtraParams",
+ txdata: &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 10,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 21000,
+ To: &testAddr,
+ Value: big.NewInt(0),
+ Version: MorphTxVersion0,
+ FeeTokenID: 0,
+ },
+ wantErr: ErrMorphTxV0IllegalExtraParams,
+ },
+ {
+ name: "V0 with Reference → ErrMorphTxV0IllegalExtraParams",
+ txdata: &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 11,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 21000,
+ To: &testAddr,
+ Value: big.NewInt(0),
+ Version: MorphTxVersion0,
+ FeeTokenID: 1,
+ FeeLimit: big.NewInt(100),
+ Reference: &ref,
+ },
+ wantErr: ErrMorphTxV0IllegalExtraParams,
+ },
+ {
+ name: "V0 with Memo → ErrMorphTxV0IllegalExtraParams",
+ txdata: &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 12,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 21000,
+ To: &testAddr,
+ Value: big.NewInt(0),
+ Version: MorphTxVersion0,
+ FeeTokenID: 1,
+ FeeLimit: big.NewInt(100),
+ Memo: &memo,
+ },
+ wantErr: ErrMorphTxV0IllegalExtraParams,
+ },
+ // --- V1 rejection cases ---
+ {
+ name: "V1 FeeTokenID=0 + FeeLimit>0 → ErrMorphTxV1IllegalExtraParams",
+ txdata: &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 20,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 21000,
+ To: &testAddr,
+ Value: big.NewInt(0),
+ Version: MorphTxVersion1,
+ FeeTokenID: 0,
+ FeeLimit: big.NewInt(999),
+ },
+ wantErr: ErrMorphTxV1IllegalExtraParams,
+ },
+ {
+ name: "V1 memo too long → ErrMemoTooLong",
+ txdata: &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 21,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 21000,
+ To: &testAddr,
+ Value: big.NewInt(0),
+ Version: MorphTxVersion1,
+ FeeTokenID: 0,
+ Memo: &longMemo,
+ },
+ wantErr: ErrMemoTooLong,
+ },
+ // --- Unsupported version ---
+ {
+ name: "unsupported version 255 → ErrMorphTxUnsupportedVersion",
+ txdata: &MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 30,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 21000,
+ To: &testAddr,
+ Value: big.NewInt(0),
+ Version: 255,
+ FeeTokenID: 1,
+ },
+ wantErr: ErrMorphTxUnsupportedVersion,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ signedTx, err := SignNewTx(key, signer, tc.txdata)
+ if err != nil {
+ t.Fatalf("SignNewTx failed: %v", err)
+ }
+
+ msg, err := signedTx.AsMessage(signer, baseFee)
+ if !errors.Is(err, tc.wantErr) {
+ t.Fatalf("AsMessage error mismatch: got %v, want %v", err, tc.wantErr)
+ }
+ if err == nil {
+ // Verify message fields are correctly populated
+ if msg.FeeTokenID() != tc.txdata.FeeTokenID {
+ t.Errorf("FeeTokenID mismatch: got %d, want %d", msg.FeeTokenID(), tc.txdata.FeeTokenID)
+ }
+ if msg.Version() != tc.txdata.Version {
+ t.Errorf("Version mismatch: got %d, want %d", msg.Version(), tc.txdata.Version)
+ }
+ }
+ })
+ }
+
+ // Non-MorphTx should always pass (not affected by the check)
+ t.Run("DynamicFeeTx → success (not affected)", func(t *testing.T) {
+ dynTx, err := SignNewTx(key, NewLondonSigner(big.NewInt(1)), &DynamicFeeTx{
+ ChainID: big.NewInt(1),
+ Nonce: 5,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 21000,
+ To: &testAddr,
+ Value: big.NewInt(0),
+ })
+ if err != nil {
+ t.Fatalf("SignNewTx failed: %v", err)
+ }
+ if _, err := dynTx.AsMessage(NewLondonSigner(big.NewInt(1)), baseFee); err != nil {
+ t.Fatalf("AsMessage should succeed for DynamicFeeTx, got: %v", err)
+ }
+ })
+}
+
+// TestMorphTxAccessors tests accessor methods for MorphTx fields.
+func TestMorphTxAccessors(t *testing.T) {
+ ref := common.HexToReference("0x2222222222222222222222222222222222222222222222222222222222222222")
+ memo := []byte("accessor test memo")
+ feeLimit := big.NewInt(999999)
+
+ t.Run("MorphTx V0 accessors", func(t *testing.T) {
+ tx := NewTx(&MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 10,
+ GasTipCap: big.NewInt(5),
+ GasFeeCap: big.NewInt(50),
+ Gas: 30000,
+ To: &testAddr,
+ Value: big.NewInt(1000),
+ FeeTokenID: 42,
+ FeeLimit: feeLimit,
+ Version: MorphTxVersion0,
+ })
+
+ if !tx.IsMorphTx() {
+ t.Error("IsMorphTx should return true")
+ }
+ if !tx.IsMorphTxWithAltFee() {
+ t.Error("IsMorphTxWithAltFee should return true for V0 with FeeTokenID > 0")
+ }
+ if tx.Version() != MorphTxVersion0 {
+ t.Errorf("Version mismatch, got %d, want %d", tx.Version(), MorphTxVersion0)
+ }
+ if tx.FeeTokenID() != 42 {
+ t.Errorf("FeeTokenID mismatch, got %d, want 42", tx.FeeTokenID())
+ }
+ if tx.FeeLimit().Cmp(feeLimit) != 0 {
+ t.Errorf("FeeLimit mismatch, got %v, want %v", tx.FeeLimit(), feeLimit)
+ }
+ if tx.Reference() != nil {
+ t.Error("Reference should be nil for V0")
+ }
+ if tx.Memo() != nil {
+ t.Error("Memo should be nil for V0")
+ }
+ })
+
+ t.Run("MorphTx V1 accessors", func(t *testing.T) {
+ tx := NewTx(&MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 20,
+ GasTipCap: big.NewInt(10),
+ GasFeeCap: big.NewInt(100),
+ Gas: 40000,
+ To: &testAddr,
+ Value: big.NewInt(2000),
+ FeeTokenID: 0,
+ Version: MorphTxVersion1,
+ Reference: &ref,
+ Memo: &memo,
+ })
+
+ if !tx.IsMorphTx() {
+ t.Error("IsMorphTx should return true")
+ }
+ if tx.IsMorphTxWithAltFee() {
+ t.Error("IsMorphTxWithAltFee should return false for FeeTokenID = 0")
+ }
+ if tx.Version() != MorphTxVersion1 {
+ t.Errorf("Version mismatch, got %d, want %d", tx.Version(), MorphTxVersion1)
+ }
+ if tx.FeeTokenID() != 0 {
+ t.Errorf("FeeTokenID mismatch, got %d, want 0", tx.FeeTokenID())
+ }
+ if tx.Reference() == nil || *tx.Reference() != ref {
+ t.Error("Reference mismatch")
+ }
+ if tx.Memo() == nil || !bytes.Equal(*tx.Memo(), memo) {
+ t.Error("Memo mismatch")
+ }
+ })
+
+ t.Run("Non-MorphTx accessors", func(t *testing.T) {
+ legacyTx := NewTx(&LegacyTx{
+ Nonce: 1,
+ GasPrice: big.NewInt(1),
+ Gas: 21000,
+ To: &testAddr,
+ })
+
+ if legacyTx.IsMorphTx() {
+ t.Error("IsMorphTx should return false for LegacyTx")
+ }
+ if legacyTx.IsMorphTxWithAltFee() {
+ t.Error("IsMorphTxWithAltFee should return false for LegacyTx")
+ }
+ if legacyTx.Version() != 0 {
+ t.Errorf("Version should return 0 for non-MorphTx, got %d", legacyTx.Version())
+ }
+ if legacyTx.FeeTokenID() != 0 {
+ t.Errorf("FeeTokenID should return 0 for non-MorphTx, got %d", legacyTx.FeeTokenID())
+ }
+ if legacyTx.Reference() != nil {
+ t.Error("Reference should return nil for non-MorphTx")
+ }
+ if legacyTx.Memo() != nil {
+ t.Error("Memo should return nil for non-MorphTx")
+ }
+ })
+}
+
+// TestMorphTxVersionBackwardCompatibility tests that V0 and V1 transactions
+// can be correctly encoded, decoded, and signed.
+func TestMorphTxVersionBackwardCompatibility(t *testing.T) {
+ key, _ := crypto.GenerateKey()
+ signer := NewEmeraldSigner(big.NewInt(1))
+
+ // Create V0 tx
+ v0Tx := NewTx(&MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 1,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 21000,
+ To: &testAddr,
+ Value: big.NewInt(100),
+ FeeTokenID: 1,
+ FeeLimit: big.NewInt(1000),
+ Version: MorphTxVersion0,
+ })
+
+ // Create V1 tx with same basic params but different version/fields
+ ref := common.HexToReference("0x3333333333333333333333333333333333333333333333333333333333333333")
+ memo := []byte("backward compat test")
+ v1Tx := NewTx(&MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 1,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 21000,
+ To: &testAddr,
+ Value: big.NewInt(100),
+ FeeTokenID: 1,
+ FeeLimit: big.NewInt(1000),
+ Version: MorphTxVersion1,
+ Reference: &ref,
+ Memo: &memo,
+ })
+
+ // Sign both transactions
+ signedV0, err := SignTx(v0Tx, signer, key)
+ if err != nil {
+ t.Fatalf("Failed to sign V0 tx: %v", err)
+ }
+ signedV1, err := SignTx(v1Tx, signer, key)
+ if err != nil {
+ t.Fatalf("Failed to sign V1 tx: %v", err)
+ }
+
+ // Hashes should be different (different sigHash due to version)
+ if signedV0.Hash() == signedV1.Hash() {
+ t.Error("V0 and V1 transaction hashes should be different")
+ }
+
+ // Both should be recoverable
+ sender0, err := Sender(signer, signedV0)
+ if err != nil {
+ t.Fatalf("Failed to recover V0 sender: %v", err)
+ }
+ sender1, err := Sender(signer, signedV1)
+ if err != nil {
+ t.Fatalf("Failed to recover V1 sender: %v", err)
+ }
+ if sender0 != sender1 {
+ t.Error("Senders should be the same")
+ }
+
+ // Both should round-trip through RLP
+ decoded0, err := encodeDecodeBinary(signedV0)
+ if err != nil {
+ t.Fatalf("V0 RLP round-trip failed: %v", err)
+ }
+ if decoded0.Version() != MorphTxVersion0 {
+ t.Errorf("V0 version not preserved, got %d", decoded0.Version())
+ }
+
+ decoded1, err := encodeDecodeBinary(signedV1)
+ if err != nil {
+ t.Fatalf("V1 RLP round-trip failed: %v", err)
+ }
+ if decoded1.Version() != MorphTxVersion1 {
+ t.Errorf("V1 version not preserved, got %d", decoded1.Version())
+ }
+ if decoded1.Reference() == nil || *decoded1.Reference() != ref {
+ t.Error("V1 Reference not preserved")
+ }
+ if decoded1.Memo() == nil || !bytes.Equal(*decoded1.Memo(), memo) {
+ t.Error("V1 Memo not preserved")
+ }
+}
+
+// TestMorphTxReferenceEdgeCases tests edge cases for Reference field.
+// Note: V1 MorphTx requires both Reference and Memo fields for RLP encoding.
+func TestMorphTxReferenceEdgeCases(t *testing.T) {
+ key, _ := crypto.GenerateKey()
+ signer := NewEmeraldSigner(big.NewInt(1))
+
+ zeroRef := common.Reference{}
+ fullRef := common.HexToReference("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
+ partialRef := common.HexToReference("0x1234567800000000000000000000000000000000000000000000000000000000")
+ emptyMemo := []byte{}
+
+ tests := []struct {
+ name string
+ ref *common.Reference
+ }{
+ {"zero reference", &zeroRef},
+ {"full reference", &fullRef},
+ {"partial reference", &partialRef},
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ tx := NewTx(&MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 1,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 21000,
+ To: &testAddr,
+ Version: MorphTxVersion1,
+ Reference: tc.ref,
+ Memo: &emptyMemo, // V1 requires Memo field for RLP encoding
+ })
+
+ signedTx, err := SignTx(tx, signer, key)
+ if err != nil {
+ t.Fatalf("SignTx failed: %v", err)
+ }
+
+ decoded, err := encodeDecodeBinary(signedTx)
+ if err != nil {
+ t.Fatalf("RLP round-trip failed: %v", err)
+ }
+
+ if !reflect.DeepEqual(decoded.Reference(), tc.ref) {
+ t.Errorf("Reference mismatch after round-trip, got %v, want %v", decoded.Reference(), tc.ref)
+ }
+ })
+ }
+}
+
+// TestMorphTxMemoEdgeCases tests edge cases for Memo field.
+// Note: V1 MorphTx requires both Reference and Memo fields for RLP encoding.
+func TestMorphTxMemoEdgeCases(t *testing.T) {
+ key, _ := crypto.GenerateKey()
+ signer := NewEmeraldSigner(big.NewInt(1))
+
+ zeroRef := common.Reference{}
+
+ tests := []struct {
+ name string
+ memo *[]byte
+ }{
+ {"empty memo", func() *[]byte { m := []byte{}; return &m }()},
+ {"single byte", func() *[]byte { m := []byte{0x42}; return &m }()},
+ {"max length", func() *[]byte { m := make([]byte, common.MaxMemoLength); return &m }()},
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ tx := NewTx(&MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 1,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 21000,
+ To: &testAddr,
+ Version: MorphTxVersion1,
+ Reference: &zeroRef, // V1 requires Reference field for RLP encoding
+ Memo: tc.memo,
+ })
+
+ signedTx, err := SignTx(tx, signer, key)
+ if err != nil {
+ t.Fatalf("SignTx failed: %v", err)
+ }
+
+ decoded, err := encodeDecodeBinary(signedTx)
+ if err != nil {
+ t.Fatalf("RLP round-trip failed: %v", err)
+ }
+
+ if !compareMemoPtr(decoded.Memo(), tc.memo) {
+ t.Errorf("Memo mismatch after round-trip, got %v, want %v", decoded.Memo(), tc.memo)
+ }
+ })
+ }
+}
+
+// Helper function to compare memo pointers
+// Treats nil and empty as equivalent since RLP encodes both as 0x80
+func compareMemoPtr(a, b *[]byte) bool {
+ if a == nil && b == nil {
+ return true
+ }
+ if a == nil {
+ return len(*b) == 0
+ }
+ if b == nil {
+ return len(*a) == 0
+ }
+ return bytes.Equal(*a, *b)
+}
diff --git a/eth/backend.go b/eth/backend.go
index 2a72465a8..409c04ad5 100644
--- a/eth/backend.go
+++ b/eth/backend.go
@@ -140,8 +140,8 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
if config.OverrideEmeraldTime != nil {
overrides.EmeraldTime = config.OverrideEmeraldTime
}
- if config.OverrideMPTForkTime != nil {
- overrides.MPTForkTime = config.OverrideMPTForkTime
+ if config.OverrideJadeForkTime != nil {
+ overrides.JadeForkTime = config.OverrideJadeForkTime
}
chainConfig, genesisHash, genesisErr := core.SetupGenesisBlockWithOverride(chainDb, config.Genesis, &overrides)
if _, ok := genesisErr.(*params.ConfigCompatError); genesisErr != nil && !ok {
diff --git a/eth/catalyst/l2_api.go b/eth/catalyst/l2_api.go
index ec86be41f..464a72930 100644
--- a/eth/catalyst/l2_api.go
+++ b/eth/catalyst/l2_api.go
@@ -86,7 +86,7 @@ func (api *l2ConsensusAPI) AssembleL2Block(params AssembleL2BlockParams) (*Execu
if params.Timestamp != nil {
timestamp = time.Unix(int64(*params.Timestamp), 0)
}
- if api.eth.BlockChain().Config().IsMPTFork(uint64(timestamp.Unix())) == api.eth.BlockChain().Config().Morph.UseZktrie {
+ if api.eth.BlockChain().Config().IsJadeFork(uint64(timestamp.Unix())) == api.eth.BlockChain().Config().Morph.UseZktrie {
return nil, fmt.Errorf("cannot assemble block for fork, useZKtrie: %v, please switch geth", api.eth.BlockChain().Config().Morph.UseZktrie)
}
newBlockResult, err := api.eth.Miner().BuildBlock(parent.Hash(), timestamp, transactions)
diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go
index f7cdc9615..7e1f14549 100644
--- a/eth/ethconfig/config.go
+++ b/eth/ethconfig/config.go
@@ -225,8 +225,8 @@ type Config struct {
// EmeraldTime override
OverrideEmeraldTime *uint64 `toml:",omitempty"`
- // MPTForkTime override
- OverrideMPTForkTime *uint64 `toml:",omitempty"`
+ // JadeForkTime override
+ OverrideJadeForkTime *uint64 `toml:",omitempty"`
}
// CreateConsensusEngine creates a consensus engine for the given chain config.
diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go
index 7fc7d9232..22f997b2e 100644
--- a/ethclient/ethclient.go
+++ b/ethclient/ethclient.go
@@ -371,6 +371,25 @@ func (ec *Client) GetSkippedTransaction(ctx context.Context, txHash common.Hash)
return tx, ec.c.CallContext(ctx, &tx, "morph_getSkippedTransaction", txHash)
}
+// GetTransactionHashesByReference returns transactions for the given reference with pagination.
+// Results are sorted by blockTimestamp and txIndex (ascending order).
+// Parameters:
+// - reference: the reference key to query
+// - offset: pagination offset (default: 0)
+// - limit: pagination limit (default: 100, max: 100)
+func (ec *Client) GetTransactionHashesByReference(ctx context.Context, reference common.Reference, offset *hexutil.Uint64, limit *hexutil.Uint64) ([]rpc.ReferenceTransactionResult, error) {
+ var result []rpc.ReferenceTransactionResult
+ args := rpc.ReferenceQueryArgs{
+ Reference: reference,
+ Offset: offset,
+ Limit: limit,
+ }
+ if err := ec.c.CallContext(ctx, &result, "morph_getTransactionHashesByReference", args); err != nil {
+ return nil, err
+ }
+ return result, nil
+}
+
// GetBlockByNumberOrHash returns the requested block
func (ec *Client) GetBlockByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.BlockMore, error) {
var raw json.RawMessage
diff --git a/graphql/graphql.go b/graphql/graphql.go
index 64890619d..ebd035430 100644
--- a/graphql/graphql.go
+++ b/graphql/graphql.go
@@ -243,7 +243,7 @@ func (t *Transaction) GasPrice(ctx context.Context) (hexutil.Big, error) {
}
}
return hexutil.Big(*tx.GasPrice()), nil
- case types.AltFeeTxType:
+ case types.MorphTxType:
if t.block != nil {
if baseFee, _ := t.block.BaseFeePerGas(ctx); baseFee != nil {
return (hexutil.Big)(*math.BigMin(new(big.Int).Add(tx.GasTipCap(), baseFee.ToInt()), tx.GasFeeCap())), nil
@@ -280,7 +280,7 @@ func (t *Transaction) MaxFeePerGas(ctx context.Context) (*hexutil.Big, error) {
return nil, nil
case types.DynamicFeeTxType, types.SetCodeTxType:
return (*hexutil.Big)(tx.GasFeeCap()), nil
- case types.AltFeeTxType:
+ case types.MorphTxType:
return (*hexutil.Big)(tx.GasFeeCap()), nil
default:
return nil, nil
@@ -297,7 +297,7 @@ func (t *Transaction) MaxPriorityFeePerGas(ctx context.Context) (*hexutil.Big, e
return nil, nil
case types.DynamicFeeTxType, types.SetCodeTxType:
return (*hexutil.Big)(tx.GasTipCap()), nil
- case types.AltFeeTxType:
+ case types.MorphTxType:
return (*hexutil.Big)(tx.GasTipCap()), nil
default:
return nil, nil
diff --git a/interfaces.go b/interfaces.go
index 52c255258..78ead865a 100644
--- a/interfaces.go
+++ b/interfaces.go
@@ -142,9 +142,13 @@ type CallMsg struct {
BlobGasFeeCap *big.Int
BlobHashes []common.Hash
- // For ERC20FeeType
+ // For MorphTxType
FeeTokenID uint16
FeeLimit *big.Int
+ Version uint8
+ Reference *common.Reference
+ Memo *[]byte
+
// For SetCodeTxType
AuthorizationList []types.SetCodeAuthorization
}
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
index 6f85bd5d2..1fcd600ec 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -40,6 +40,7 @@ import (
"github.com/morph-l2/go-ethereum/consensus/misc"
"github.com/morph-l2/go-ethereum/core"
"github.com/morph-l2/go-ethereum/core/forkid"
+ "github.com/morph-l2/go-ethereum/core/rawdb"
"github.com/morph-l2/go-ethereum/core/state"
"github.com/morph-l2/go-ethereum/core/tracing"
"github.com/morph-l2/go-ethereum/core/types"
@@ -652,8 +653,8 @@ func (api *PublicBlockChainAPI) ChainId() (*hexutil.Big, error) {
// morphExtension contains Morph-specific configuration fields (EIP-7910 extension)
type morphExtension struct {
- UseZktrie bool `json:"useZktrie"`
- MPTForkTime *uint64 `json:"mptForkTime,omitempty"`
+ UseZktrie bool `json:"useZktrie"`
+ JadeForkTime *uint64 `json:"jadeForkTime,omitempty"`
}
// config represents a single fork configuration (EIP-7910)
@@ -715,8 +716,8 @@ func (api *PublicBlockChainAPI) Config(ctx context.Context) (*configResponse, er
// Morph extension
morph := &morphExtension{
- UseZktrie: c.Morph.UseZktrie,
- MPTForkTime: c.MPTForkTime,
+ UseZktrie: c.Morph.UseZktrie,
+ JadeForkTime: c.JadeForkTime,
}
return &config{
@@ -1547,9 +1548,12 @@ type RPCTransaction struct {
Sender *common.Address `json:"sender,omitempty"`
QueueIndex *hexutil.Uint64 `json:"queueIndex,omitempty"`
- // Alt fee transaction fields:
- FeeTokenID hexutil.Uint64 `json:"feeTokenID,omitempty"`
- FeeLimit *hexutil.Big `json:"feeLimit,omitempty"`
+ // MorphTx fields:
+ FeeTokenID *hexutil.Uint16 `json:"feeTokenID,omitempty"`
+ FeeLimit *hexutil.Big `json:"feeLimit,omitempty"`
+ Version *hexutil.Uint64 `json:"version,omitempty"`
+ Reference *common.Reference `json:"reference,omitempty"`
+ Memo *hexutil.Bytes `json:"memo,omitempty"`
}
// NewRPCTransaction returns a transaction that will serialize to the RPC
@@ -1600,7 +1604,7 @@ func NewRPCTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber
msg := tx.AsL1MessageTx()
result.Sender = &msg.Sender
result.QueueIndex = (*hexutil.Uint64)(&msg.QueueIndex)
- case types.AltFeeTxType:
+ case types.MorphTxType:
al := tx.AccessList()
result.Accesses = &al
result.ChainID = (*hexutil.Big)(tx.ChainId())
@@ -1614,8 +1618,16 @@ func NewRPCTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber
} else {
result.GasPrice = (*hexutil.Big)(tx.GasFeeCap())
}
- result.FeeTokenID = (hexutil.Uint64)(tx.FeeTokenID())
+ feeTokenID := hexutil.Uint16(tx.FeeTokenID())
+ result.FeeTokenID = &feeTokenID
result.FeeLimit = (*hexutil.Big)(tx.FeeLimit())
+ // Only include V1 fields (version, reference, memo) for V1+ transactions
+ if tx.Version() >= types.MorphTxVersion1 {
+ version := hexutil.Uint64(tx.Version())
+ result.Version = &version
+ result.Reference = (*common.Reference)(tx.Reference())
+ result.Memo = (*hexutil.Bytes)(tx.Memo())
+ }
case types.SetCodeTxType:
al := tx.AccessList()
yparity := hexutil.Uint64(v.Sign())
@@ -1985,11 +1997,13 @@ func marshalReceipt(ctx context.Context, b Backend, receipt *types.Receipt, bigb
"l1Fee": (*hexutil.Big)(receipt.L1Fee),
"feeRate": (*hexutil.Big)(receipt.FeeRate),
"tokenScale": (*hexutil.Big)(receipt.TokenScale),
+ "feeTokenID": (*hexutil.Uint16)(receipt.FeeTokenID),
"feeLimit": (*hexutil.Big)(receipt.FeeLimit),
+ "version": hexutil.Uint(receipt.Version),
+ "reference": (*common.Reference)(receipt.Reference),
+ "memo": (*hexutil.Bytes)(receipt.Memo),
}
- if receipt.FeeTokenID != nil {
- fields["feeTokenID"] = hexutil.Uint16(*receipt.FeeTokenID)
- }
+
// Assign the effective gas price paid
if !b.ChainConfig().IsCurie(bigblock) {
fields["effectiveGasPrice"] = hexutil.Uint64(tx.GasPrice().Uint64())
@@ -2449,3 +2463,70 @@ func toHexSlice(b [][]byte) []string {
}
return r
}
+
+// PublicMorphAPI provides morph-specific RPC methods.
+type PublicMorphAPI struct {
+ b Backend
+}
+
+// NewPublicMorphAPI creates a new RPC service for morph-specific methods.
+func NewPublicMorphAPI(b Backend) *PublicMorphAPI {
+ return &PublicMorphAPI{b}
+}
+
+// GetTransactionHashesByReference returns transactions for the given reference with pagination.
+// Results are sorted by blockTimestamp and txIndex (ascending order).
+// Parameters:
+// - args.reference: the reference key to query
+// - args.offset: pagination offset (default: 0)
+// - args.limit: pagination limit (default: 100, max: 100)
+func (s *PublicMorphAPI) GetTransactionHashesByReference(
+ ctx context.Context,
+ args rpc.ReferenceQueryArgs,
+) (
+ []rpc.ReferenceTransactionResult,
+ error,
+) {
+ // Set default values
+ offsetVal := uint64(0)
+ if args.Offset != nil {
+ offsetVal = uint64(*args.Offset)
+ }
+ limitVal := uint64(100)
+ if args.Limit != nil {
+ limitVal = uint64(*args.Limit)
+ }
+
+ // Validate limit (max 100)
+ if limitVal > 100 {
+ return nil, errors.New("limit exceeds maximum value of 100")
+ }
+ // Cap offset to prevent linear scan DoS
+ if offsetVal > 10000 {
+ return nil, errors.New("offset exceeds maximum value of 10000")
+ }
+
+ paginatedEntries, interrupted := rawdb.ReadReferenceIndexEntriesPaginatedWithInterrupt(s.b.ChainDb(), args.Reference, offsetVal, limitVal, ctx.Done())
+ if interrupted {
+ return nil, ctx.Err()
+ }
+ if len(paginatedEntries) == 0 {
+ return nil, nil
+ }
+
+ // Build result
+ result := make([]rpc.ReferenceTransactionResult, 0, len(paginatedEntries))
+ for _, entry := range paginatedEntries {
+ blockNumber := rawdb.ReadTxLookupEntry(s.b.ChainDb(), entry.TxHash)
+ if blockNumber == nil {
+ continue
+ }
+ result = append(result, rpc.ReferenceTransactionResult{
+ TransactionHash: entry.TxHash,
+ BlockNumber: hexutil.Uint64(*blockNumber),
+ BlockTimestamp: hexutil.Uint64(entry.BlockTimestamp),
+ TransactionIndex: hexutil.Uint64(entry.TxIndex),
+ })
+ }
+ return result, nil
+}
diff --git a/internal/ethapi/api_morph_test.go b/internal/ethapi/api_morph_test.go
new file mode 100644
index 000000000..e2c5c0e98
--- /dev/null
+++ b/internal/ethapi/api_morph_test.go
@@ -0,0 +1,629 @@
+package ethapi
+
+import (
+ "context"
+ "crypto/ecdsa"
+ "math/big"
+ "testing"
+
+ "github.com/morph-l2/go-ethereum/common"
+ "github.com/morph-l2/go-ethereum/common/hexutil"
+ "github.com/morph-l2/go-ethereum/core/rawdb"
+ "github.com/morph-l2/go-ethereum/core/types"
+ "github.com/morph-l2/go-ethereum/crypto"
+ "github.com/morph-l2/go-ethereum/ethdb"
+ "github.com/morph-l2/go-ethereum/params"
+ "github.com/morph-l2/go-ethereum/rpc"
+)
+
+// mockMorphBackend is a minimal Backend mock that only implements ChainDb().
+// All other Backend methods are left unimplemented via embedding.
+// This is sufficient because GetTransactionHashesByReference only uses ChainDb().
+type mockMorphBackend struct {
+ Backend // embed interface — calling unimplemented methods will panic
+ db ethdb.Database
+}
+
+func (m *mockMorphBackend) ChainDb() ethdb.Database { return m.db }
+
+func makeTestRef(b byte) common.Reference {
+ var ref common.Reference
+ ref[0] = b
+ return ref
+}
+
+func uint64Ptr(v uint64) *hexutil.Uint64 {
+ h := hexutil.Uint64(v)
+ return &h
+}
+
+// TestGetTransactionHashesByReference_OffsetExceedsMax verifies that offset > 10000 is rejected.
+func TestGetTransactionHashesByReference_OffsetExceedsMax(t *testing.T) {
+ // Validation errors are returned before ChainDb() is called, so nil backend is safe.
+ api := &PublicMorphAPI{b: nil}
+ ref := makeTestRef(0x01)
+
+ tests := []struct {
+ name string
+ offset uint64
+ want string
+ }{
+ {"offset 10001", 10001, "offset exceeds maximum value of 10000"},
+ {"offset max uint64", ^uint64(0), "offset exceeds maximum value of 10000"},
+ {"offset 50000", 50000, "offset exceeds maximum value of 10000"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ args := rpc.ReferenceQueryArgs{
+ Reference: ref,
+ Offset: uint64Ptr(tt.offset),
+ }
+ _, err := api.GetTransactionHashesByReference(context.Background(), args)
+ if err == nil {
+ t.Fatalf("expected error for offset=%d, got nil", tt.offset)
+ }
+ if err.Error() != tt.want {
+ t.Fatalf("expected error %q, got %q", tt.want, err.Error())
+ }
+ })
+ }
+}
+
+// TestGetTransactionHashesByReference_LimitExceedsMax verifies that limit > 100 is rejected.
+func TestGetTransactionHashesByReference_LimitExceedsMax(t *testing.T) {
+ api := &PublicMorphAPI{b: nil}
+ ref := makeTestRef(0x02)
+
+ args := rpc.ReferenceQueryArgs{
+ Reference: ref,
+ Limit: uint64Ptr(101),
+ }
+ _, err := api.GetTransactionHashesByReference(context.Background(), args)
+ if err == nil {
+ t.Fatal("expected error for limit=101, got nil")
+ }
+ if err.Error() != "limit exceeds maximum value of 100" {
+ t.Fatalf("unexpected error: %s", err.Error())
+ }
+}
+
+// TestGetTransactionHashesByReference_OffsetBoundary verifies boundary values.
+func TestGetTransactionHashesByReference_OffsetBoundary(t *testing.T) {
+ db := rawdb.NewMemoryDatabase()
+ api := NewPublicMorphAPI(&mockMorphBackend{db: db})
+ ref := makeTestRef(0x03)
+
+ tests := []struct {
+ name string
+ offset *hexutil.Uint64
+ limit *hexutil.Uint64
+ expectErr bool
+ }{
+ {"offset 0 (default)", nil, nil, false},
+ {"offset 10000 (max allowed)", uint64Ptr(10000), uint64Ptr(1), false},
+ {"offset 10001 (over max)", uint64Ptr(10001), uint64Ptr(1), true},
+ {"offset 9999 limit 100", uint64Ptr(9999), uint64Ptr(100), false},
+ {"limit 0", nil, uint64Ptr(0), false},
+ {"limit 100 (max allowed)", nil, uint64Ptr(100), false},
+ {"limit 101 (over max)", nil, uint64Ptr(101), true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ args := rpc.ReferenceQueryArgs{
+ Reference: ref,
+ Offset: tt.offset,
+ Limit: tt.limit,
+ }
+ _, err := api.GetTransactionHashesByReference(context.Background(), args)
+ if tt.expectErr && err == nil {
+ t.Fatal("expected error, got nil")
+ }
+ if !tt.expectErr && err != nil {
+ t.Fatalf("expected no error, got: %s", err.Error())
+ }
+ })
+ }
+}
+
+// TestGetTransactionHashesByReference_EmptyDB verifies that valid params on an empty DB return nil.
+func TestGetTransactionHashesByReference_EmptyDB(t *testing.T) {
+ db := rawdb.NewMemoryDatabase()
+ api := NewPublicMorphAPI(&mockMorphBackend{db: db})
+ ref := makeTestRef(0x04)
+
+ args := rpc.ReferenceQueryArgs{
+ Reference: ref,
+ Offset: uint64Ptr(0),
+ Limit: uint64Ptr(100),
+ }
+ result, err := api.GetTransactionHashesByReference(context.Background(), args)
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err.Error())
+ }
+ if result != nil {
+ t.Fatalf("expected nil result for empty db, got %d entries", len(result))
+ }
+}
+
+// --- setDefaults version-heuristic tests ---
+
+// mockSetDefaultsBackend implements the Backend methods required by setDefaults
+// when Gas, Nonce, MaxFeePerGas, and MaxPriorityFeePerGas are pre-filled.
+type mockSetDefaultsBackend struct {
+ Backend // embed — unimplemented methods panic if called
+ chainConfig *params.ChainConfig
+ header *types.Header
+}
+
+func (m *mockSetDefaultsBackend) CurrentHeader() *types.Header { return m.header }
+func (m *mockSetDefaultsBackend) ChainConfig() *params.ChainConfig { return m.chainConfig }
+
+func uint16VersionPtr(v uint8) *hexutil.Uint16 {
+ h := hexutil.Uint16(v)
+ return &h
+}
+
+// TestSetDefaults_MorphTxVersionHeuristic tests the heuristic version defaulting logic
+// in TransactionArgs.setDefaults():
+// - Version == nil + no V1 fields → V0
+// - Version == nil + Reference or Memo present → V1
+// - Explicit Version → use as-is
+// - Before jade fork: V1-specific fields rejected; default is V0
+// - After jade fork: V1 fields allowed; heuristic picks V1 when present
+func TestSetDefaults_MorphTxVersionHeuristic(t *testing.T) {
+ to := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678")
+ ref := common.HexToReference("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
+ emptyRef := common.Reference{}
+ memo := hexutil.Bytes([]byte("test memo"))
+ emptyMemo := hexutil.Bytes([]byte{})
+
+ jadeForkTime := uint64(1000)
+
+ // Helper to build a backend with jade fork at time 1000
+ makeBackend := func(headTime uint64) *mockSetDefaultsBackend {
+ return &mockSetDefaultsBackend{
+ chainConfig: ¶ms.ChainConfig{
+ ChainID: big.NewInt(1),
+ CurieBlock: big.NewInt(0), // IsCurie = true so EIP-1559 path is used
+ JadeForkTime: &jadeForkTime,
+ },
+ header: &types.Header{
+ Number: big.NewInt(1),
+ Time: headTime,
+ BaseFee: big.NewInt(100),
+ },
+ }
+ }
+
+ // Common base args that avoid deep mocking (Gas, Nonce, gas fees pre-filled)
+ gas := hexutil.Uint64(21000)
+ nonce := hexutil.Uint64(0)
+ maxFee := (*hexutil.Big)(big.NewInt(100))
+ tip := (*hexutil.Big)(big.NewInt(1))
+
+ baseArgs := func() TransactionArgs {
+ return TransactionArgs{
+ To: &to,
+ Gas: &gas,
+ Nonce: &nonce,
+ MaxFeePerGas: maxFee,
+ MaxPriorityFeePerGas: tip,
+ }
+ }
+
+ tests := []struct {
+ name string
+ headTime uint64 // head.Time (jade fork at 1000)
+ modify func(args *TransactionArgs)
+ wantVersion *uint16 // nil means version field should not be set (non-MorphTx)
+ wantErr bool
+ }{
+ // === After jade fork (headTime >= 1000) ===
+ {
+ name: "jade fork: FeeTokenID only → V0",
+ headTime: 1000,
+ modify: func(args *TransactionArgs) {
+ fid := hexutil.Uint16(1)
+ args.FeeTokenID = &fid
+ },
+ wantVersion: uint16Ref(types.MorphTxVersion0),
+ },
+ {
+ name: "jade fork: FeeTokenID + Reference → V1",
+ headTime: 1000,
+ modify: func(args *TransactionArgs) {
+ fid := hexutil.Uint16(1)
+ args.FeeTokenID = &fid
+ args.Reference = &ref
+ },
+ wantVersion: uint16Ref(types.MorphTxVersion1),
+ },
+ {
+ name: "jade fork: FeeTokenID + Memo → V1",
+ headTime: 1000,
+ modify: func(args *TransactionArgs) {
+ fid := hexutil.Uint16(1)
+ args.FeeTokenID = &fid
+ args.Memo = &memo
+ },
+ wantVersion: uint16Ref(types.MorphTxVersion1),
+ },
+ {
+ name: "jade fork: Reference only → V1",
+ headTime: 1000,
+ modify: func(args *TransactionArgs) {
+ args.Reference = &ref
+ },
+ wantVersion: uint16Ref(types.MorphTxVersion1),
+ },
+ {
+ name: "jade fork: Memo only → V1",
+ headTime: 1000,
+ modify: func(args *TransactionArgs) {
+ args.Memo = &memo
+ },
+ wantVersion: uint16Ref(types.MorphTxVersion1),
+ },
+ {
+ name: "jade fork: empty Reference + FeeTokenID → V0",
+ headTime: 1000,
+ modify: func(args *TransactionArgs) {
+ fid := hexutil.Uint16(1)
+ args.FeeTokenID = &fid
+ args.Reference = &emptyRef
+ },
+ wantVersion: uint16Ref(types.MorphTxVersion0),
+ },
+ {
+ name: "jade fork: empty Memo + FeeTokenID → V0",
+ headTime: 1000,
+ modify: func(args *TransactionArgs) {
+ fid := hexutil.Uint16(1)
+ args.FeeTokenID = &fid
+ args.Memo = &emptyMemo
+ },
+ wantVersion: uint16Ref(types.MorphTxVersion0),
+ },
+ {
+ name: "jade fork: explicit V0 → V0",
+ headTime: 1000,
+ modify: func(args *TransactionArgs) {
+ fid := hexutil.Uint16(1)
+ args.FeeTokenID = &fid
+ args.Version = uint16VersionPtr(types.MorphTxVersion0)
+ },
+ wantVersion: uint16Ref(types.MorphTxVersion0),
+ },
+ {
+ name: "jade fork: explicit V1 → V1",
+ headTime: 1000,
+ modify: func(args *TransactionArgs) {
+ args.Version = uint16VersionPtr(types.MorphTxVersion1)
+ },
+ wantVersion: uint16Ref(types.MorphTxVersion1),
+ },
+ {
+ name: "jade fork: no MorphTx fields → not MorphTx (version nil)",
+ headTime: 1000,
+ modify: func(args *TransactionArgs) {},
+ wantVersion: nil,
+ },
+
+ // === Before jade fork (headTime < 1000) ===
+ {
+ name: "pre-jade: FeeTokenID only → V0",
+ headTime: 500,
+ modify: func(args *TransactionArgs) {
+ fid := hexutil.Uint16(1)
+ args.FeeTokenID = &fid
+ },
+ wantVersion: uint16Ref(types.MorphTxVersion0),
+ },
+ {
+ name: "pre-jade: explicit V1 → rejected",
+ headTime: 500,
+ modify: func(args *TransactionArgs) {
+ args.Version = uint16VersionPtr(types.MorphTxVersion1)
+ },
+ wantErr: true,
+ },
+ {
+ name: "pre-jade: Reference → rejected (V1-only field before fork)",
+ headTime: 500,
+ modify: func(args *TransactionArgs) {
+ fid := hexutil.Uint16(1)
+ args.FeeTokenID = &fid
+ args.Reference = &ref
+ },
+ wantErr: true,
+ },
+ {
+ name: "pre-jade: Memo → rejected (V1-only field before fork)",
+ headTime: 500,
+ modify: func(args *TransactionArgs) {
+ fid := hexutil.Uint16(1)
+ args.FeeTokenID = &fid
+ args.Memo = &memo
+ },
+ wantErr: true,
+ },
+ {
+ name: "pre-jade: explicit V0 + FeeTokenID → V0 (ok)",
+ headTime: 500,
+ modify: func(args *TransactionArgs) {
+ fid := hexutil.Uint16(1)
+ args.FeeTokenID = &fid
+ args.Version = uint16VersionPtr(types.MorphTxVersion0)
+ },
+ wantVersion: uint16Ref(types.MorphTxVersion0),
+ },
+
+ // === validateMorphTxVersion rejection cases ===
+ {
+ name: "jade fork: explicit V0 + FeeTokenID=0 → rejected",
+ headTime: 1000,
+ modify: func(args *TransactionArgs) {
+ fid := hexutil.Uint16(0)
+ args.FeeTokenID = &fid
+ args.Version = uint16VersionPtr(types.MorphTxVersion0)
+ },
+ wantErr: true,
+ },
+ {
+ name: "jade fork: explicit V1 + FeeTokenID=0 + FeeLimit>0 → rejected",
+ headTime: 1000,
+ modify: func(args *TransactionArgs) {
+ fid := hexutil.Uint16(0)
+ args.FeeTokenID = &fid
+ args.FeeLimit = (*hexutil.Big)(big.NewInt(1000))
+ args.Version = uint16VersionPtr(types.MorphTxVersion1)
+ },
+ wantErr: true,
+ },
+ {
+ name: "jade fork: auto V1 (Reference) + FeeTokenID=0 + FeeLimit>0 → rejected",
+ headTime: 1000,
+ modify: func(args *TransactionArgs) {
+ fid := hexutil.Uint16(0)
+ args.FeeTokenID = &fid
+ args.FeeLimit = (*hexutil.Big)(big.NewInt(1000))
+ args.Reference = &ref
+ },
+ wantErr: true,
+ },
+ {
+ name: "jade fork: explicit V1 + FeeTokenID=0 + FeeLimit=nil → ok",
+ headTime: 1000,
+ modify: func(args *TransactionArgs) {
+ args.Version = uint16VersionPtr(types.MorphTxVersion1)
+ },
+ wantVersion: uint16Ref(types.MorphTxVersion1),
+ },
+ {
+ name: "jade fork: explicit V1 + FeeTokenID=0 + FeeLimit=0 → ok",
+ headTime: 1000,
+ modify: func(args *TransactionArgs) {
+ fid := hexutil.Uint16(0)
+ args.FeeTokenID = &fid
+ args.FeeLimit = (*hexutil.Big)(big.NewInt(0))
+ args.Version = uint16VersionPtr(types.MorphTxVersion1)
+ },
+ wantVersion: uint16Ref(types.MorphTxVersion1),
+ },
+ {
+ name: "jade fork: auto V0 (FeeTokenID=1) + FeeLimit>0 → ok (V0 ignores FeeLimit)",
+ headTime: 1000,
+ modify: func(args *TransactionArgs) {
+ fid := hexutil.Uint16(1)
+ args.FeeTokenID = &fid
+ args.FeeLimit = (*hexutil.Big)(big.NewInt(1000))
+ },
+ wantVersion: uint16Ref(types.MorphTxVersion0),
+ },
+ {
+ name: "jade fork: explicit V1 + FeeTokenID>0 + FeeLimit>0 → ok",
+ headTime: 1000,
+ modify: func(args *TransactionArgs) {
+ fid := hexutil.Uint16(1)
+ args.FeeTokenID = &fid
+ args.FeeLimit = (*hexutil.Big)(big.NewInt(1000))
+ args.Version = uint16VersionPtr(types.MorphTxVersion1)
+ },
+ wantVersion: uint16Ref(types.MorphTxVersion1),
+ },
+ {
+ name: "jade fork: unsupported version 99 → rejected",
+ headTime: 1000,
+ modify: func(args *TransactionArgs) {
+ args.Version = uint16VersionPtr(99)
+ },
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ backend := makeBackend(tt.headTime)
+ args := baseArgs()
+ tt.modify(&args)
+
+ err := args.setDefaults(context.Background(), backend)
+ if tt.wantErr {
+ if err == nil {
+ t.Fatal("expected error, got nil")
+ }
+ return
+ }
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ if tt.wantVersion == nil {
+ // Not a MorphTx — Version should remain nil
+ if args.Version != nil {
+ t.Errorf("expected Version to be nil (non-MorphTx), got %d", *args.Version)
+ }
+ } else {
+ if args.Version == nil {
+ t.Fatalf("expected Version = %d, got nil", *tt.wantVersion)
+ }
+ if uint16(*args.Version) != *tt.wantVersion {
+ t.Errorf("Version: got %d, want %d", *args.Version, *tt.wantVersion)
+ }
+ }
+ })
+ }
+}
+
+func uint16Ref(v uint8) *uint16 {
+ u := uint16(v)
+ return &u
+}
+
+// --- marshalReceipt field-presence tests ---
+
+// mockReceiptBackend implements just ChainConfig() for marshalReceipt tests.
+// IsCurie returns false so marshalReceipt takes the simpler gasPrice path
+// and does not call HeaderByHash.
+type mockReceiptBackend struct {
+ Backend // embed — unimplemented methods panic if called
+}
+
+func (m *mockReceiptBackend) ChainConfig() *params.ChainConfig {
+ return ¶ms.ChainConfig{
+ ChainID: big.NewInt(1),
+ // CurieBlock is nil → IsCurie returns false
+ }
+}
+
+// signTx is a helper that signs a transaction with the given key and signer.
+func signTx(t *testing.T, key *ecdsa.PrivateKey, signer types.Signer, inner types.TxData) *types.Transaction {
+ t.Helper()
+ tx, err := types.SignNewTx(key, signer, inner)
+ if err != nil {
+ t.Fatalf("failed to sign tx: %v", err)
+ }
+ return tx
+}
+
+// TestMarshalReceipt_FieldPresence verifies that all MorphTx-specific fields
+// are always present in marshalled receipts regardless of tx type:
+// - l1Fee, feeRate, tokenScale, feeTokenID, feeLimit, version, reference, memo
+// are unconditionally included (values may be nil/zero for non-MorphTx).
+func TestMarshalReceipt_FieldPresence(t *testing.T) {
+ key, _ := crypto.GenerateKey()
+ testAddr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678")
+ morphSigner := types.NewEmeraldSigner(big.NewInt(1))
+ londonSigner := types.NewLondonSigner(big.NewInt(1))
+
+ ref := common.HexToReference("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
+ memo := []byte("test memo")
+ feeTokenID := uint16(1)
+ feeRate := big.NewInt(500)
+ tokenScale := big.NewInt(1000)
+
+ tests := []struct {
+ name string
+ tx *types.Transaction
+ // receipt fields to populate
+ receiptFeeTokenID *uint16
+ receiptFeeRate *big.Int
+ receiptTokenScale *big.Int
+ receiptFeeLimit *big.Int
+ receiptVersion uint8
+ receiptReference *common.Reference
+ receiptMemo *[]byte
+ }{
+ {
+ name: "DynamicFeeTx (non-MorphTx)",
+ tx: signTx(t, key, londonSigner, &types.DynamicFeeTx{
+ ChainID: big.NewInt(1),
+ Nonce: 0,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 21000,
+ To: &testAddr,
+ Value: big.NewInt(1),
+ }),
+ },
+ {
+ name: "MorphTx V0",
+ tx: signTx(t, key, morphSigner, &types.MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 1,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 21000,
+ To: &testAddr,
+ Value: big.NewInt(1),
+ FeeTokenID: 1,
+ FeeLimit: big.NewInt(1000),
+ Version: types.MorphTxVersion0,
+ }),
+ receiptFeeTokenID: &feeTokenID,
+ receiptFeeRate: feeRate,
+ receiptTokenScale: tokenScale,
+ receiptFeeLimit: big.NewInt(1000),
+ receiptVersion: types.MorphTxVersion0,
+ },
+ {
+ name: "MorphTx V1",
+ tx: signTx(t, key, morphSigner, &types.MorphTx{
+ ChainID: big.NewInt(1),
+ Nonce: 2,
+ GasTipCap: big.NewInt(1),
+ GasFeeCap: big.NewInt(10),
+ Gas: 21000,
+ To: &testAddr,
+ Value: big.NewInt(1),
+ FeeTokenID: 0,
+ FeeLimit: big.NewInt(0),
+ Version: types.MorphTxVersion1,
+ Reference: &ref,
+ Memo: &memo,
+ }),
+ receiptFeeRate: feeRate,
+ receiptTokenScale: tokenScale,
+ receiptFeeLimit: big.NewInt(0),
+ receiptVersion: types.MorphTxVersion1,
+ receiptReference: &ref,
+ receiptMemo: &memo,
+ },
+ }
+
+ backend := &mockReceiptBackend{}
+ blockHash := common.HexToHash("0xdeadbeef")
+ bigblock := big.NewInt(1)
+
+ // All MorphTx-related fields should always be present in the result map
+ allMorphFields := []string{"l1Fee", "feeRate", "tokenScale", "feeTokenID", "feeLimit", "version", "reference", "memo"}
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ receipt := &types.Receipt{
+ Status: types.ReceiptStatusSuccessful,
+ GasUsed: 21000,
+ FeeTokenID: tt.receiptFeeTokenID,
+ FeeRate: tt.receiptFeeRate,
+ TokenScale: tt.receiptTokenScale,
+ FeeLimit: tt.receiptFeeLimit,
+ Version: tt.receiptVersion,
+ Reference: tt.receiptReference,
+ Memo: tt.receiptMemo,
+ }
+
+ signer := types.NewEmeraldSigner(big.NewInt(1))
+ fields, err := marshalReceipt(context.Background(), backend, receipt, bigblock, blockHash, 1, signer, tt.tx, 0)
+ if err != nil {
+ t.Fatalf("marshalReceipt error: %v", err)
+ }
+
+ // All MorphTx fields should always be present regardless of tx type
+ for _, field := range allMorphFields {
+ if _, exists := fields[field]; !exists {
+ t.Errorf("expected field %q to always be present for %s, but it was absent", field, tt.name)
+ }
+ }
+ })
+ }
+}
diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go
index 6b0f43a83..2a38fb8a1 100644
--- a/internal/ethapi/backend.go
+++ b/internal/ethapi/backend.go
@@ -141,6 +141,11 @@ func GetAPIs(apiBackend Backend) []rpc.API {
Version: "1.0",
Service: NewPrivateAccountAPI(apiBackend, nonceLock),
Public: false,
+ }, {
+ Namespace: "morph",
+ Version: "1.0",
+ Service: NewPublicMorphAPI(apiBackend),
+ Public: true,
},
}
}
diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go
index 383829875..30c5cb2c4 100644
--- a/internal/ethapi/transaction_args.go
+++ b/internal/ethapi/transaction_args.go
@@ -50,9 +50,12 @@ type TransactionArgs struct {
Data *hexutil.Bytes `json:"data"`
Input *hexutil.Bytes `json:"input"`
- // AltFeeTxType
- FeeTokenID *hexutil.Uint16 `json:"feeTokenID,omitempty"`
- FeeLimit *hexutil.Big `json:"feeLimit,omitempty"`
+ // MorphTxType
+ FeeTokenID *hexutil.Uint16 `json:"feeTokenID,omitempty"`
+ FeeLimit *hexutil.Big `json:"feeLimit,omitempty"`
+ Version *hexutil.Uint16 `json:"version,omitempty"`
+ Reference *common.Reference `json:"reference,omitempty"`
+ Memo *hexutil.Bytes `json:"memo,omitempty"`
// Introduced by AccessListTxType transaction.
AccessList *types.AccessList `json:"accessList,omitempty"`
@@ -81,6 +84,59 @@ func (arg *TransactionArgs) data() []byte {
return nil
}
+// isMorphTxArgs returns true if the transaction args indicate a MorphTx type.
+func (args *TransactionArgs) isMorphTxArgs() bool {
+ // Check if any MorphTx-specific field is set
+ // Note: Version=0 with FeeTokenID=0 is invalid, but we still want to validate it
+ return (args.FeeTokenID != nil && *args.FeeTokenID > 0) ||
+ (args.Version != nil) || // Any explicit version setting indicates MorphTx intent
+ (args.Reference != nil && *args.Reference != (common.Reference{})) ||
+ (args.Memo != nil && len(*args.Memo) > 0)
+}
+
+// validateMorphTxVersion validates the MorphTx version and its associated field requirements.
+// If version is explicitly specified, validate that parameters match:
+// - Version 0: FeeTokenID must be > 0, Reference and Memo must not be set
+// - Version 1: FeeTokenID, Reference, Memo are all optional;
+// if FeeTokenID is not set or is 0, FeeLimit must not be set
+//
+// If version is not explicitly specified, no version-specific validation is needed
+// because determineMorphTxVersion will assign the highest version.
+func (args *TransactionArgs) validateMorphTxVersion() error {
+ if !args.isMorphTxArgs() {
+ return nil
+ }
+
+ // Only validate when version is explicitly specified
+ if args.Version == nil {
+ return nil
+ }
+
+ version := uint8(*args.Version)
+ switch version {
+ case types.MorphTxVersion0:
+ // Version 0 requires FeeTokenID > 0
+ if args.FeeTokenID == nil || *args.FeeTokenID == 0 ||
+ args.Reference != nil && *args.Reference != (common.Reference{}) ||
+ args.Memo != nil && len(*args.Memo) > 0 {
+ return types.ErrMorphTxV0IllegalExtraParams
+ }
+ case types.MorphTxVersion1:
+ // Version 1: FeeTokenID, Reference, Memo are all optional
+ // If FeeTokenID is not set or is 0, FeeLimit must not be set
+ feeTokenID := uint16(0)
+ if args.FeeTokenID != nil {
+ feeTokenID = uint16(*args.FeeTokenID)
+ }
+ if feeTokenID == 0 && args.FeeLimit != nil && args.FeeLimit.ToInt().Sign() != 0 {
+ return types.ErrMorphTxV1IllegalExtraParams
+ }
+ default:
+ return types.ErrMorphTxUnsupportedVersion
+ }
+ return nil
+}
+
// setDefaults fills in default values for unspecified tx fields.
func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend) error {
if args.GasPrice != nil && (args.MaxFeePerGas != nil || args.MaxPriorityFeePerGas != nil) {
@@ -158,6 +214,42 @@ func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend) error {
if args.To == nil && len(args.data()) == 0 {
return errors.New(`contract creation without any data provided`)
}
+ // Handle MorphTx version defaults and jade fork validation
+ if args.isMorphTxArgs() {
+ isJadeFork := b.ChainConfig().IsJadeFork(head.Time)
+ if !isJadeFork {
+ // Reject explicit V1 before jade fork
+ if args.Version != nil && uint8(*args.Version) == types.MorphTxVersion1 {
+ return types.ErrMorphTxV1NotYetActive
+ }
+ // Reject V1-only fields (reference/memo) before jade fork
+ if (args.Reference != nil && *args.Reference != (common.Reference{})) ||
+ (args.Memo != nil && len(*args.Memo) > 0) {
+ return types.ErrMorphTxV1NotYetActive
+ }
+ }
+ // Determine version: explicit > V1 if V1-specific fields present > V0 (backward compatible)
+ if args.Version == nil {
+ hasV1Fields := (args.Reference != nil && *args.Reference != (common.Reference{})) ||
+ (args.Memo != nil && len(*args.Memo) > 0)
+ if hasV1Fields {
+ v := hexutil.Uint16(types.MorphTxVersion1)
+ args.Version = &v
+ } else {
+ v := hexutil.Uint16(types.MorphTxVersion0)
+ args.Version = &v
+ }
+ }
+ }
+ // Validate memo length for MorphTx
+ if args.Memo != nil && len(*args.Memo) > common.MaxMemoLength {
+ return errors.New("memo exceeds maximum length of 64 bytes")
+ }
+ // Validate MorphTx version and associated field requirements
+ if err := args.validateMorphTxVersion(); err != nil {
+ return err
+ }
+
// Estimate the gas usage if necessary.
if args.Gas == nil {
// These fields are immutable during the estimation, safe to
@@ -171,6 +263,9 @@ func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend) error {
MaxPriorityFeePerGas: args.MaxPriorityFeePerGas,
FeeTokenID: args.FeeTokenID,
FeeLimit: args.FeeLimit,
+ Version: args.Version,
+ Reference: args.Reference,
+ Memo: args.Memo,
Value: args.Value,
Data: (*hexutil.Bytes)(&data),
AccessList: args.AccessList,
@@ -307,16 +402,35 @@ func (args *TransactionArgs) ToMessage(globalGasCap uint64, baseFee *big.Int) (t
if args.AccessList != nil {
accessList = *args.AccessList
}
- var feeTokenID uint16
- if args.FeeTokenID != nil {
- feeTokenID = uint16(*args.FeeTokenID)
- }
- feeLimit := new(big.Int)
- if args.FeeLimit != nil {
- feeLimit = args.FeeLimit.ToInt()
+
+ // Only set MorphTx-specific fields when this is a MorphTx call
+ var (
+ feeTokenID uint16
+ feeLimit *big.Int
+ version uint8
+ reference *common.Reference
+ memo *[]byte
+ )
+ if args.isMorphTxArgs() {
+ if args.FeeTokenID != nil {
+ feeTokenID = uint16(*args.FeeTokenID)
+ }
+ if args.FeeLimit != nil {
+ feeLimit = args.FeeLimit.ToInt()
+ }
+ // Use version from args (set by setDefaults or explicitly by caller)
+ if args.Version != nil {
+ version = uint8(*args.Version)
+ }
+ if args.Reference != nil {
+ reference = args.Reference
+ }
+ if args.Memo != nil {
+ memo = (*[]byte)(args.Memo)
+ }
}
- msg := types.NewMessage(addr, args.To, 0, value, gas, gasPrice, gasFeeCap, gasTipCap, feeTokenID, feeLimit, data, accessList, args.AuthorizationList, true)
+ msg := types.NewMessage(addr, args.To, 0, value, gas, gasPrice, gasFeeCap, gasTipCap, feeTokenID, feeLimit, version, reference, memo, data, accessList, args.AuthorizationList, true)
return msg, nil
}
@@ -326,8 +440,11 @@ func (args *TransactionArgs) toTransaction() *types.Transaction {
usedType := types.LegacyTxType
switch {
// must take precedence over MaxFeePerGas.
- case args.FeeTokenID != nil && *args.FeeTokenID > 0:
- usedType = types.AltFeeTxType
+ case (args.FeeTokenID != nil && *args.FeeTokenID > 0) ||
+ (args.Version != nil) || // Any explicit version setting indicates MorphTx intent
+ (args.Reference != nil && *args.Reference != (common.Reference{})) ||
+ (args.Memo != nil && len(*args.Memo) > 0):
+ usedType = types.MorphTxType
case args.AuthorizationList != nil:
usedType = types.SetCodeTxType
case args.MaxFeePerGas != nil:
@@ -385,20 +502,32 @@ func (args *TransactionArgs) toTransaction() *types.Transaction {
AccessList: al,
}
- case types.AltFeeTxType:
+ case types.MorphTxType:
al := types.AccessList{}
if args.AccessList != nil {
al = *args.AccessList
}
- data = &types.AltFeeTx{
+ // Use version from args (set by setDefaults or explicitly by caller)
+ var version uint8
+ if args.Version != nil {
+ version = uint8(*args.Version)
+ }
+ var feeTokenID uint16
+ if args.FeeTokenID != nil {
+ feeTokenID = uint16(*args.FeeTokenID)
+ }
+ data = &types.MorphTx{
To: args.To,
ChainID: (*big.Int)(args.ChainID),
Nonce: uint64(*args.Nonce),
Gas: uint64(*args.Gas),
GasFeeCap: (*big.Int)(args.MaxFeePerGas),
GasTipCap: (*big.Int)(args.MaxPriorityFeePerGas),
- FeeTokenID: uint16(*args.FeeTokenID),
+ Version: version,
+ FeeTokenID: feeTokenID,
FeeLimit: (*big.Int)(args.FeeLimit),
+ Reference: args.Reference,
+ Memo: (*[]byte)(args.Memo),
Value: (*big.Int)(args.Value),
Data: args.data(),
AccessList: al,
diff --git a/les/client.go b/les/client.go
index fa0cba19d..f69d54587 100644
--- a/les/client.go
+++ b/les/client.go
@@ -99,8 +99,8 @@ func New(stack *node.Node, config *ethconfig.Config) (*LightEthereum, error) {
if config.OverrideEmeraldTime != nil {
overrides.EmeraldTime = config.OverrideEmeraldTime
}
- if config.OverrideMPTForkTime != nil {
- overrides.MPTForkTime = config.OverrideMPTForkTime
+ if config.OverrideJadeForkTime != nil {
+ overrides.JadeForkTime = config.OverrideJadeForkTime
}
chainConfig, genesisHash, genesisErr := core.SetupGenesisBlockWithOverride(chainDb, config.Genesis, &overrides)
if _, isCompat := genesisErr.(*params.ConfigCompatError); genesisErr != nil && !isCompat {
diff --git a/les/odr_test.go b/les/odr_test.go
index f70031418..088cf820d 100644
--- a/les/odr_test.go
+++ b/les/odr_test.go
@@ -141,7 +141,27 @@ func odrContractCall(ctx context.Context, db ethdb.Database, config *params.Chai
from := statedb.GetOrNewStateObject(bankAddr)
from.SetBalance(math.MaxBig256)
- msg := callmsg{types.NewMessage(from.Address(), &testContractAddr, 0, new(big.Int), 100000, big.NewInt(params.InitialBaseFee), big.NewInt(params.InitialBaseFee), new(big.Int), data, nil, nil, true)}
+ msg := callmsg{
+ types.NewMessage(
+ from.Address(),
+ &testContractAddr,
+ 0,
+ new(big.Int),
+ 100000,
+ big.NewInt(params.InitialBaseFee),
+ big.NewInt(params.InitialBaseFee),
+ new(big.Int),
+ 0,
+ new(big.Int),
+ 0,
+ nil,
+ nil,
+ data,
+ nil,
+ nil,
+ true,
+ ),
+ }
context := core.NewEVMBlockContext(header, bc, config, nil)
txContext := core.NewEVMTxContext(msg)
@@ -158,7 +178,26 @@ func odrContractCall(ctx context.Context, db ethdb.Database, config *params.Chai
header := lc.GetHeaderByHash(bhash)
state := light.NewState(ctx, header, lc.Odr())
state.SetBalance(bankAddr, math.MaxBig256, tracing.BalanceChangeUnspecified)
- msg := callmsg{types.NewMessage(bankAddr, &testContractAddr, 0, new(big.Int), 100000, big.NewInt(params.InitialBaseFee), big.NewInt(params.InitialBaseFee), new(big.Int), data, nil, nil, true)}
+ msg := callmsg{
+ types.NewMessage(
+ bankAddr,
+ &testContractAddr,
+ 0, new(big.Int),
+ 100000,
+ big.NewInt(params.InitialBaseFee),
+ big.NewInt(params.InitialBaseFee),
+ new(big.Int),
+ 0,
+ new(big.Int),
+ 0,
+ nil,
+ nil,
+ data,
+ nil,
+ nil,
+ true,
+ ),
+ }
context := core.NewEVMBlockContext(header, lc, config, nil)
txContext := core.NewEVMTxContext(msg)
vmenv := vm.NewEVM(context, txContext, state, config, vm.Config{NoBaseFee: true})
diff --git a/light/odr_test.go b/light/odr_test.go
index 3d5dc091c..4e55381f9 100644
--- a/light/odr_test.go
+++ b/light/odr_test.go
@@ -199,7 +199,27 @@ func odrContractCall(ctx context.Context, db ethdb.Database, bc *core.BlockChain
// Perform read-only call.
st.SetBalance(testBankAddress, math.MaxBig256, tracing.BalanceChangeUnspecified)
- msg := callmsg{types.NewMessage(testBankAddress, &testContractAddr, 0, new(big.Int), 1000000, big.NewInt(params.InitialBaseFee), big.NewInt(params.InitialBaseFee), new(big.Int), 0, nil, data, nil, nil, true)}
+ msg := callmsg{
+ types.NewMessage(
+ testBankAddress,
+ &testContractAddr,
+ 0,
+ new(big.Int),
+ 1000000,
+ big.NewInt(params.InitialBaseFee),
+ big.NewInt(params.InitialBaseFee),
+ new(big.Int),
+ 0,
+ nil,
+ 0,
+ nil,
+ nil,
+ data,
+ nil,
+ nil,
+ true,
+ ),
+ }
txContext := core.NewEVMTxContext(msg)
context := core.NewEVMBlockContext(header, chain, config, nil)
vmenv := vm.NewEVM(context, txContext, st, config, vm.Config{NoBaseFee: true})
diff --git a/light/txpool.go b/light/txpool.go
index 09a1e0816..0756c2338 100644
--- a/light/txpool.go
+++ b/light/txpool.go
@@ -412,10 +412,15 @@ func (pool *TxPool) validateTx(ctx context.Context, tx *types.Transaction) error
if tx.Value().Sign() < 0 {
return core.ErrNegativeValue
}
+
+ if err := tx.ValidateMorphTxVersion(); err != nil {
+ return err
+ }
+
// 1. Check balance >= transaction cost (V + GP * GL) to maintain compatibility with the logic without considering L1 data fee.
// Transactor should have enough funds to cover the costs
// cost == V + GP * GL
- if tx.IsAltFeeTx() {
+ if tx.IsMorphTxWithAltFee() {
active, err := fees.IsTokenActive(currentState, tx.FeeTokenID())
if err != nil {
return fmt.Errorf("get token status failed %v", err)
@@ -455,7 +460,7 @@ func (pool *TxPool) validateTx(ctx context.Context, tx *types.Transaction) error
}
// Transactor should have enough funds to cover the costs
// cost == L1 data fee + V + GP * GL
- if tx.IsAltFeeTx() {
+ if tx.IsMorphTxWithAltFee() {
if b := currentState.GetBalance(from); b.Cmp(tx.Value()) < 0 {
return errors.New("invalid transaction: insufficient funds for value")
}
diff --git a/params/config.go b/params/config.go
index 96092c705..dd6bee1df 100644
--- a/params/config.go
+++ b/params/config.go
@@ -589,7 +589,7 @@ type ChainConfig struct {
Morph203Time *uint64 `json:"morph203Time,omitempty"` // Morph203Time switch time (nil = no fork, 0 = already on morph203)
ViridianTime *uint64 `json:"viridianTime,omitempty"` // ViridianTime switch time (nil = no fork, 0 = already on viridian)
EmeraldTime *uint64 `json:"emeraldTime,omitempty"` // EmeraldTime switch time (nil = no fork, 0 = already on emerald)
- MPTForkTime *uint64 `json:"mptForkTime,omitempty"` // MPTForkTime switch time (nil = no fork, blocks use zkTrie format)
+ JadeForkTime *uint64 `json:"jadeForkTime,omitempty"` // JadeForkTime switch time (nil = no fork, blocks use zkTrie format)
// TerminalTotalDifficulty is the amount of total difficulty reached by
// the network that triggers the consensus upgrade.
@@ -701,7 +701,8 @@ func (c *ChainConfig) String() string {
default:
engine = "unknown"
}
- return fmt.Sprintf("{ChainID: %v Homestead: %v DAO: %v DAOSupport: %v EIP150: %v EIP155: %v EIP158: %v Byzantium: %v Constantinople: %v Petersburg: %v Istanbul: %v, Muir Glacier: %v, Berlin: %v, London: %v, Arrow Glacier: %v, Archimedes: %v, Shanghai: %v, Bernoulli: %v, Curie: %v, Morph203: %v, Viridian: %v, Emerald: %v, MPTFork: %v, Engine: %v, Morph config: %v}",
+ return fmt.Sprintf(
+ "{ChainID: %v Homestead: %v DAO: %v DAOSupport: %v EIP150: %v EIP155: %v EIP158: %v Byzantium: %v Constantinople: %v Petersburg: %v Istanbul: %v, Muir Glacier: %v, Berlin: %v, London: %v, Arrow Glacier: %v, Archimedes: %v, Shanghai: %v, Bernoulli: %v, Curie: %v, Morph203: %v, Viridian: %v, Emerald: %v, JadeFork: %v, Engine: %v, Morph config: %v}",
c.ChainID,
c.HomesteadBlock,
c.DAOForkBlock,
@@ -724,7 +725,7 @@ func (c *ChainConfig) String() string {
c.Morph203Time,
c.ViridianTime,
c.EmeraldTime,
- c.MPTForkTime,
+ c.JadeForkTime,
engine,
c.Morph,
)
@@ -830,10 +831,10 @@ func (c *ChainConfig) IsEmerald(num *big.Int, time uint64) bool {
return isTimestampForked(c.EmeraldTime, time)
}
-// IsMPTFork returns whether the given time is at or after the MPT fork time.
+// IsJadeFork returns whether the given time is at or after the Jade fork time.
// After this fork, blocks use MPT (Merkle Patricia Trie) format instead of zkTrie.
-func (c *ChainConfig) IsMPTFork(time uint64) bool {
- return isTimestampForked(c.MPTForkTime, time)
+func (c *ChainConfig) IsJadeFork(time uint64) bool {
+ return isTimestampForked(c.JadeForkTime, time)
}
// IsTerminalPoWBlock returns whether the given block is the last block of PoW stage.
@@ -900,7 +901,7 @@ func (c *ChainConfig) CheckConfigForkOrder() error {
{name: "morph203Time", timestamp: c.Morph203Time, optional: true},
{name: "viridianTime", timestamp: c.ViridianTime, optional: true},
{name: "emeraldTime", timestamp: c.EmeraldTime, optional: true},
- {name: "mptForkTime", timestamp: c.MPTForkTime, optional: true},
+ {name: "jadeForkTime", timestamp: c.JadeForkTime, optional: true},
} {
if lastFork.name != "" {
switch {
@@ -1010,8 +1011,8 @@ func (c *ChainConfig) checkCompatible(newcfg *ChainConfig, head *big.Int, headTi
if isForkTimestampIncompatible(c.EmeraldTime, newcfg.EmeraldTime, headTimestamp) {
return newTimestampCompatError("EmeraldTime fork timestamp", c.EmeraldTime, newcfg.EmeraldTime)
}
- if isForkTimestampIncompatible(c.MPTForkTime, newcfg.MPTForkTime, headTimestamp) {
- return newTimestampCompatError("MPTForkTime fork timestamp", c.MPTForkTime, newcfg.MPTForkTime)
+ if isForkTimestampIncompatible(c.JadeForkTime, newcfg.JadeForkTime, headTimestamp) {
+ return newTimestampCompatError("JadeForkTime fork timestamp", c.JadeForkTime, newcfg.JadeForkTime)
}
return nil
}
@@ -1155,11 +1156,11 @@ func (err *ConfigCompatError) Error() string {
// Rules is a one time interface meaning that it shouldn't be used in between transition
// phases.
type Rules struct {
- ChainID *big.Int
- IsHomestead, IsEIP150, IsEIP155, IsEIP158 bool
- IsByzantium, IsConstantinople, IsPetersburg, IsIstanbul bool
- IsBerlin, IsLondon, IsArchimedes, IsShanghai bool
- IsBernoulli, IsCurie, IsMorph203, IsViridian, IsEmerald, IsMPTFork bool
+ ChainID *big.Int
+ IsHomestead, IsEIP150, IsEIP155, IsEIP158 bool
+ IsByzantium, IsConstantinople, IsPetersburg, IsIstanbul bool
+ IsBerlin, IsLondon, IsArchimedes, IsShanghai, IsBernoulli bool
+ IsCurie, IsMorph203, IsViridian, IsEmerald, IsJadeFork bool
}
// Rules ensures c's ChainID is not nil.
@@ -1187,7 +1188,7 @@ func (c *ChainConfig) Rules(num *big.Int, time uint64) Rules {
IsMorph203: c.IsMorph203(time),
IsViridian: c.IsViridian(num, time),
IsEmerald: c.IsEmerald(num, time),
- IsMPTFork: c.IsMPTFork(time),
+ IsJadeFork: c.IsJadeFork(time),
}
}
diff --git a/rollup/fees/rollup_fee.go b/rollup/fees/rollup_fee.go
index 456776fe5..7d8dadd37 100644
--- a/rollup/fees/rollup_fee.go
+++ b/rollup/fees/rollup_fee.go
@@ -37,6 +37,9 @@ type Message interface {
IsL1MessageTx() bool
FeeTokenID() uint16
FeeLimit() *big.Int
+ Version() uint8
+ Reference() *common.Reference
+ Memo() *[]byte
}
// StateDB represents the StateDB interface
@@ -94,8 +97,11 @@ func asUnsignedTx(msg Message, baseFee, chainID *big.Int) *types.Transaction {
return asUnsignedAccessListTx(msg, chainID)
}
- if msg.FeeTokenID() != 0 {
- return asUnsignedAltFeeTx(msg, chainID)
+ if msg.FeeTokenID() != 0 ||
+ msg.Version() != types.MorphTxVersion0 ||
+ (msg.Reference() != nil && *msg.Reference() != (common.Reference{})) ||
+ (msg.Memo() != nil && len(*msg.Memo()) > 0) {
+ return asUnsignedMorphTx(msg, chainID)
}
return asUnsignedDynamicTx(msg, chainID)
@@ -139,8 +145,8 @@ func asUnsignedDynamicTx(msg Message, chainID *big.Int) *types.Transaction {
})
}
-func asUnsignedAltFeeTx(msg Message, chainID *big.Int) *types.Transaction {
- return types.NewTx(&types.AltFeeTx{
+func asUnsignedMorphTx(msg Message, chainID *big.Int) *types.Transaction {
+ return types.NewTx(&types.MorphTx{
Nonce: msg.Nonce(),
To: msg.To(),
Value: msg.Value(),
@@ -149,6 +155,9 @@ func asUnsignedAltFeeTx(msg Message, chainID *big.Int) *types.Transaction {
GasTipCap: msg.GasTipCap(),
FeeTokenID: msg.FeeTokenID(),
FeeLimit: msg.FeeLimit(),
+ Version: msg.Version(),
+ Reference: msg.Reference(),
+ Memo: msg.Memo(),
Data: msg.Data(),
AccessList: msg.AccessList(),
ChainID: chainID,
diff --git a/rollup/tracing/tracing.go b/rollup/tracing/tracing.go
index 8610d1376..41bd52374 100644
--- a/rollup/tracing/tracing.go
+++ b/rollup/tracing/tracing.go
@@ -355,9 +355,9 @@ func (env *TraceEnv) getTxResult(statedb *state.StateDB, index int, block *types
}
}
- // For AltFeeTx, manually collect token contract bytecode
+ // For MorphTx, manually collect token contract bytecode
// since direct storage slot operations don't trigger EVM execution
- if tx.Type() == types.AltFeeTxType && tx.FeeTokenID() != 0 {
+ if tx.Type() == types.MorphTxType && tx.FeeTokenID() != 0 {
tokenInfo, err := fees.GetTokenInfo(statedb, tx.FeeTokenID())
if err == nil && tokenInfo.TokenAddress != (common.Address{}) {
collectBytecode := func(addr common.Address) {
@@ -524,6 +524,9 @@ func (env *TraceEnv) getTxResult(statedb *state.StateDB, index int, block *types
L1DataFee: (*hexutil.Big)(receipt.L1Fee),
FeeTokenID: receipt.FeeTokenID,
FeeLimit: (*hexutil.Big)(receipt.FeeLimit),
+ Version: &receipt.Version,
+ Reference: receipt.Reference,
+ Memo: (*hexutil.Bytes)(receipt.Memo),
FeeRate: (*hexutil.Big)(receipt.FeeRate),
TokenScale: (*hexutil.Big)(receipt.TokenScale),
Gas: receipt.GasUsed,
diff --git a/rpc/types.go b/rpc/types.go
index 73570c4b0..61174d9f3 100644
--- a/rpc/types.go
+++ b/rpc/types.go
@@ -264,3 +264,18 @@ func (dh *DecimalOrHex) UnmarshalJSON(data []byte) error {
*dh = DecimalOrHex(value)
return nil
}
+
+// ReferenceTransactionResult represents a simplified transaction result for reference queries.
+type ReferenceTransactionResult struct {
+ TransactionHash common.Hash `json:"transactionHash"`
+ BlockNumber hexutil.Uint64 `json:"blockNumber"`
+ BlockTimestamp hexutil.Uint64 `json:"blockTimestamp"`
+ TransactionIndex hexutil.Uint64 `json:"transactionIndex"`
+}
+
+// ReferenceQueryArgs represents the arguments for querying transactions by reference.
+type ReferenceQueryArgs struct {
+ Reference common.Reference `json:"reference"`
+ Offset *hexutil.Uint64 `json:"offset,omitempty"`
+ Limit *hexutil.Uint64 `json:"limit,omitempty"`
+}
diff --git a/signer/core/apitypes/types.go b/signer/core/apitypes/types.go
index cdc9121f5..a6ba9b0c3 100644
--- a/signer/core/apitypes/types.go
+++ b/signer/core/apitypes/types.go
@@ -89,6 +89,9 @@ type SendTxArgs struct {
ChainID *hexutil.Big `json:"chainId,omitempty"`
FeeTokenID *hexutil.Uint16 `json:"feeTokenID,omitempty"`
FeeLimit *hexutil.Big `json:"feeLimit,omitempty"`
+ Version *hexutil.Uint64 `json:"version,omitempty"`
+ Reference *common.Reference `json:"reference,omitempty"`
+ Memo *hexutil.Bytes `json:"memo,omitempty"`
}
func (args SendTxArgs) String() string {
@@ -118,20 +121,38 @@ func (args *SendTxArgs) ToTransaction() *types.Transaction {
var data types.TxData
switch {
// must take precedence over MaxFeePerGas.
- case args.FeeTokenID != nil && *args.FeeTokenID > 0:
+ case (args.FeeTokenID != nil && *args.FeeTokenID > 0) ||
+ (args.Version != nil) || // Any explicit version setting indicates MorphTx intent
+ (args.Reference != nil && *args.Reference != (common.Reference{})) ||
+ (args.Memo != nil && len(*args.Memo) > 0):
al := types.AccessList{}
if args.AccessList != nil {
al = *args.AccessList
}
- data = &types.AltFeeTx{
+ // Determine version: explicit > V1 if V1-specific fields present > V0 (backward compatible)
+ version := uint8(types.MorphTxVersion0)
+ if args.Version != nil {
+ version = uint8(*args.Version)
+ } else if (args.Reference != nil && *args.Reference != (common.Reference{})) ||
+ (args.Memo != nil && len(*args.Memo) > 0) {
+ version = uint8(types.MorphTxVersion1)
+ }
+ var feeTokenID uint16
+ if args.FeeTokenID != nil {
+ feeTokenID = uint16(*args.FeeTokenID)
+ }
+ data = &types.MorphTx{
To: to,
ChainID: (*big.Int)(args.ChainID),
Nonce: uint64(args.Nonce),
Gas: uint64(args.Gas),
GasFeeCap: (*big.Int)(args.MaxFeePerGas),
GasTipCap: (*big.Int)(args.MaxPriorityFeePerGas),
- FeeTokenID: uint16(*args.FeeTokenID),
+ FeeTokenID: feeTokenID,
FeeLimit: (*big.Int)(args.FeeLimit),
+ Version: version,
+ Reference: args.Reference,
+ Memo: (*[]byte)(args.Memo),
Value: (*big.Int)(&args.Value),
Data: input,
AccessList: al,
diff --git a/signer/core/apitypes/types_test.go b/signer/core/apitypes/types_test.go
new file mode 100644
index 000000000..f1e1781e8
--- /dev/null
+++ b/signer/core/apitypes/types_test.go
@@ -0,0 +1,219 @@
+package apitypes
+
+import (
+ "testing"
+
+ "github.com/morph-l2/go-ethereum/common"
+ "github.com/morph-l2/go-ethereum/common/hexutil"
+ "github.com/morph-l2/go-ethereum/core/types"
+)
+
+func uint16Ptr(v uint16) *hexutil.Uint16 {
+ h := hexutil.Uint16(v)
+ return &h
+}
+
+func uint64VersionPtr(v uint64) *hexutil.Uint64 {
+ h := hexutil.Uint64(v)
+ return &h
+}
+
+func refPtr(r common.Reference) *common.Reference {
+ return &r
+}
+
+func memoPtr(b []byte) *hexutil.Bytes {
+ h := hexutil.Bytes(b)
+ return &h
+}
+
+// TestSendTxArgs_ToTransaction_VersionHeuristic tests the heuristic version defaulting logic
+// in SendTxArgs.ToTransaction():
+// - Explicit Version → use as-is
+// - No Version + Reference or Memo present → V1
+// - No Version + no V1 fields → V0 (backward compatible)
+func TestSendTxArgs_ToTransaction_VersionHeuristic(t *testing.T) {
+ from := common.NewMixedcaseAddress(common.HexToAddress("0x1234"))
+ to := common.NewMixedcaseAddress(common.HexToAddress("0x5678"))
+ ref := common.HexToReference("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
+ emptyRef := common.Reference{}
+ maxFee := (*hexutil.Big)(common.Big1)
+ tip := (*hexutil.Big)(common.Big1)
+
+ tests := []struct {
+ name string
+ args SendTxArgs
+ expectType uint8
+ expectVersion uint8
+ expectIsMorphTx bool
+ }{
+ {
+ name: "FeeTokenID only, no Version → V0",
+ args: SendTxArgs{
+ From: from,
+ To: &to,
+ MaxFeePerGas: maxFee,
+ MaxPriorityFeePerGas: tip,
+ FeeTokenID: uint16Ptr(1),
+ },
+ expectType: types.MorphTxType,
+ expectVersion: types.MorphTxVersion0,
+ expectIsMorphTx: true,
+ },
+ {
+ name: "FeeTokenID + Reference → V1",
+ args: SendTxArgs{
+ From: from,
+ To: &to,
+ MaxFeePerGas: maxFee,
+ MaxPriorityFeePerGas: tip,
+ FeeTokenID: uint16Ptr(1),
+ Reference: refPtr(ref),
+ },
+ expectType: types.MorphTxType,
+ expectVersion: types.MorphTxVersion1,
+ expectIsMorphTx: true,
+ },
+ {
+ name: "FeeTokenID + Memo → V1",
+ args: SendTxArgs{
+ From: from,
+ To: &to,
+ MaxFeePerGas: maxFee,
+ MaxPriorityFeePerGas: tip,
+ FeeTokenID: uint16Ptr(1),
+ Memo: memoPtr([]byte("hello")),
+ },
+ expectType: types.MorphTxType,
+ expectVersion: types.MorphTxVersion1,
+ expectIsMorphTx: true,
+ },
+ {
+ name: "Reference only (no FeeTokenID) → V1",
+ args: SendTxArgs{
+ From: from,
+ To: &to,
+ MaxFeePerGas: maxFee,
+ MaxPriorityFeePerGas: tip,
+ Reference: refPtr(ref),
+ },
+ expectType: types.MorphTxType,
+ expectVersion: types.MorphTxVersion1,
+ expectIsMorphTx: true,
+ },
+ {
+ name: "Memo only (no FeeTokenID) → V1",
+ args: SendTxArgs{
+ From: from,
+ To: &to,
+ MaxFeePerGas: maxFee,
+ MaxPriorityFeePerGas: tip,
+ Memo: memoPtr([]byte("test")),
+ },
+ expectType: types.MorphTxType,
+ expectVersion: types.MorphTxVersion1,
+ expectIsMorphTx: true,
+ },
+ {
+ name: "Empty Reference (all zeros) + FeeTokenID → V0",
+ args: SendTxArgs{
+ From: from,
+ To: &to,
+ MaxFeePerGas: maxFee,
+ MaxPriorityFeePerGas: tip,
+ FeeTokenID: uint16Ptr(1),
+ Reference: refPtr(emptyRef),
+ },
+ expectType: types.MorphTxType,
+ expectVersion: types.MorphTxVersion0,
+ expectIsMorphTx: true,
+ },
+ {
+ name: "Empty Memo (len=0) + FeeTokenID → V0",
+ args: SendTxArgs{
+ From: from,
+ To: &to,
+ MaxFeePerGas: maxFee,
+ MaxPriorityFeePerGas: tip,
+ FeeTokenID: uint16Ptr(1),
+ Memo: memoPtr([]byte{}),
+ },
+ expectType: types.MorphTxType,
+ expectVersion: types.MorphTxVersion0,
+ expectIsMorphTx: true,
+ },
+ {
+ name: "Explicit Version=0 with Reference → V0 (explicit wins)",
+ args: SendTxArgs{
+ From: from,
+ To: &to,
+ MaxFeePerGas: maxFee,
+ MaxPriorityFeePerGas: tip,
+ FeeTokenID: uint16Ptr(1),
+ Version: uint64VersionPtr(uint64(types.MorphTxVersion0)),
+ Reference: refPtr(ref),
+ },
+ expectType: types.MorphTxType,
+ expectVersion: types.MorphTxVersion0,
+ expectIsMorphTx: true,
+ },
+ {
+ name: "Explicit Version=1 without V1 fields → V1 (explicit wins)",
+ args: SendTxArgs{
+ From: from,
+ To: &to,
+ MaxFeePerGas: maxFee,
+ MaxPriorityFeePerGas: tip,
+ Version: uint64VersionPtr(uint64(types.MorphTxVersion1)),
+ },
+ expectType: types.MorphTxType,
+ expectVersion: types.MorphTxVersion1,
+ expectIsMorphTx: true,
+ },
+ {
+ name: "Reference + Memo → V1",
+ args: SendTxArgs{
+ From: from,
+ To: &to,
+ MaxFeePerGas: maxFee,
+ MaxPriorityFeePerGas: tip,
+ Reference: refPtr(ref),
+ Memo: memoPtr([]byte("memo")),
+ },
+ expectType: types.MorphTxType,
+ expectVersion: types.MorphTxVersion1,
+ expectIsMorphTx: true,
+ },
+ {
+ name: "MaxFeePerGas only → DynamicFeeTx (not MorphTx)",
+ args: SendTxArgs{
+ From: from,
+ To: &to,
+ MaxFeePerGas: maxFee,
+ MaxPriorityFeePerGas: tip,
+ },
+ expectType: types.DynamicFeeTxType,
+ expectVersion: 0,
+ expectIsMorphTx: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tx := tt.args.ToTransaction()
+
+ if tx.Type() != tt.expectType {
+ t.Errorf("tx type: got %d, want %d", tx.Type(), tt.expectType)
+ }
+
+ if tt.expectIsMorphTx {
+ if !tx.IsMorphTx() {
+ t.Fatal("expected IsMorphTx() = true")
+ }
+ if tx.Version() != tt.expectVersion {
+ t.Errorf("version: got %d, want %d", tx.Version(), tt.expectVersion)
+ }
+ }
+ })
+ }
+}
diff --git a/tests/state_test_util.go b/tests/state_test_util.go
index 75cd62b24..c480adb47 100644
--- a/tests/state_test_util.go
+++ b/tests/state_test_util.go
@@ -113,8 +113,11 @@ type stTransaction struct {
Value []string `json:"value"`
PrivateKey []byte `json:"secretKey"`
- FeeTokenID uint16 `json:"feeTokenID,omitempty"`
- FeeLimit *big.Int `json:"feeLimit,omitempty"`
+ FeeTokenID uint16 `json:"feeTokenID,omitempty"`
+ FeeLimit *big.Int `json:"feeLimit,omitempty"`
+ Version byte `json:"version,omitempty"`
+ Reference *common.Reference `json:"reference,omitempty"`
+ Memo *[]byte `json:"memo,omitempty"`
}
type stTransactionMarshaling struct {
@@ -374,9 +377,12 @@ func (tx *stTransaction) toMessage(ps stPostState, baseFee *big.Int) (core.Messa
return nil, fmt.Errorf("no gas price provided")
}
- msg := types.NewMessage(from, to, tx.Nonce, value, gasLimit, gasPrice,
- tx.MaxFeePerGas, tx.MaxPriorityFeePerGas, tx.FeeTokenID, tx.FeeLimit,
- data, accessList, nil, false)
+ msg := types.NewMessage(
+ from, to, tx.Nonce, value, gasLimit, gasPrice,
+ tx.MaxFeePerGas, tx.MaxPriorityFeePerGas,
+ tx.FeeTokenID, tx.FeeLimit, tx.Version, tx.Reference, tx.Memo,
+ data, accessList, nil, false,
+ )
return msg, nil
}