From a8a8cae597fe927dbedb380dea6e49dd73ff6f59 Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Sun, 4 Jan 2026 15:05:54 +0800
Subject: [PATCH 01/33] update morph tx
---
accounts/abi/bind/base.go | 6 +-
accounts/external/backend.go | 2 +-
common/types.go | 163 +++++++++++++++++++++-
core/state_processor.go | 2 +-
core/tx_list.go | 4 +-
core/tx_pool.go | 10 +-
core/types/l2trace.go | 4 +-
core/types/{alt_fee_tx.go => morph_tx.go} | 57 ++++----
core/types/receipt.go | 14 +-
core/types/receipt_test.go | 2 +-
core/types/transaction.go | 43 +++---
core/types/transaction_marshalling.go | 6 +-
core/types/transaction_signing.go | 2 +-
graphql/graphql.go | 6 +-
internal/ethapi/api.go | 2 +-
internal/ethapi/transaction_args.go | 12 +-
light/txpool.go | 4 +-
rollup/fees/rollup_fee.go | 6 +-
rollup/tracing/tracing.go | 4 +-
signer/core/apitypes/types.go | 2 +-
20 files changed, 269 insertions(+), 82 deletions(-)
rename core/types/{alt_fee_tx.go => morph_tx.go} (62%)
diff --git a/accounts/abi/bind/base.go b/accounts/abi/bind/base.go
index 263a7dd8a..84fe5645f 100644
--- a/accounts/abi/bind/base.go
+++ b/accounts/abi/bind/base.go
@@ -290,7 +290,7 @@ 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 {
@@ -334,7 +334,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,
@@ -439,7 +439,7 @@ func (c *BoundContract) transact(opts *TransactOpts, contract *common.Address, i
return nil, errHead
} else if head.BaseFee != nil {
if opts.FeeTokenID != 0 {
- rawTx, err = c.createAltFeeTx(opts, contract, input, head)
+ rawTx, err = c.createMorphTx(opts, contract, input, head)
} else {
rawTx, err = c.createDynamicTx(opts, contract, input, head)
}
diff --git a/accounts/external/backend.go b/accounts/external/backend.go
index ac620cb5f..392808eb4 100644
--- a/accounts/external/backend.go
+++ b/accounts/external/backend.go
@@ -221,7 +221,7 @@ 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())
diff --git a/common/types.go b/common/types.go
index d438a1649..4e320203a 100644
--- a/common/types.go
+++ b/common/types.go
@@ -39,13 +39,172 @@ const (
HashLength = 32
// AddressLength is the expected length of the address
AddressLength = 20
+ // ReferenceLength is the expected length of the reference
+ ReferenceLength = 32
)
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.
+func (r *Reference) SetBytes(b []byte) {
+ if len(b) > len(r) {
+ b = b[len(b)-ReferenceLength:]
+ }
+ copy(r[:], b)
+}
+
+// UnmarshalText parses a reference in hex syntax.
+func (r *Reference) UnmarshalText(input []byte) error {
+ return hexutil.UnmarshalFixedText("Reference", input, r[:])
+}
+
+// UnmarshalJSON parses a reference in hex syntax.
+func (r *Reference) UnmarshalJSON(input []byte) error {
+ 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
+
+// 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/state_processor.go b/core/state_processor.go
index 8daab4d6f..58b07f630 100644
--- a/core/state_processor.go
+++ b/core/state_processor.go
@@ -192,7 +192,7 @@ 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()
diff --git a/core/tx_list.go b/core/tx_list.go
index e7edda2e8..9a1103b12 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.IsMorphTx() {
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.IsMorphTx() {
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..2afafe26a 100644
--- a/core/tx_pool.go
+++ b/core/tx_pool.go
@@ -670,7 +670,7 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
}
// 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 {
@@ -724,7 +724,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.IsMorphTx() {
active, err := fees.IsTokenActive(pool.currentState, tx.FeeTokenID())
if err != nil {
return fmt.Errorf("get token status failed %v", err)
@@ -763,7 +763,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.IsMorphTx() {
if b := pool.currentState.GetBalance(from); b.Cmp(tx.Value()) < 0 {
return ErrInsufficientValue
}
@@ -1617,7 +1617,7 @@ 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) {
+ if !tx.IsMorphTx() && (tx.Gas() > pool.currentMaxGas || tx.Cost().Cmp(costLimit) > 0) {
return true
}
if pool.chainconfig.Morph.FeeVaultEnabled() {
@@ -1627,7 +1627,7 @@ func (pool *TxPool) executableTxFilter(addr common.Address, costLimit *big.Int,
log.Error("Failed to calculate L1 data fee", "err", err, "tx", tx)
return false
}
- if tx.IsAltFeeTx() {
+ if tx.IsMorphTx() {
if altCostLimit[tx.FeeTokenID()] == nil {
balance, err := pool.getBalanceFunc(pool.chain.CurrentBlock().Header(), pool.currentState, tx.FeeTokenID(), addr)
if err != nil || balance == nil {
diff --git a/core/types/l2trace.go b/core/types/l2trace.go
index b1844ab62..6dc751a06 100644
--- a/core/types/l2trace.go
+++ b/core/types/l2trace.go
@@ -195,8 +195,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
diff --git a/core/types/alt_fee_tx.go b/core/types/morph_tx.go
similarity index 62%
rename from core/types/alt_fee_tx.go
rename to core/types/morph_tx.go
index 28c06dfd3..f7f718601 100644
--- a/core/types/alt_fee_tx.go
+++ b/core/types/morph_tx.go
@@ -24,7 +24,7 @@ import (
// 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 {
+type MorphTx struct {
ChainID *big.Int
Nonce uint64
GasTipCap *big.Int
@@ -35,8 +35,11 @@ type AltFeeTx struct {
Data []byte
AccessList AccessList
- FeeTokenID uint16
- FeeLimit *big.Int
+ Version byte // version of morph tx
+ 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)
+ Memo []byte // memo for the transaction (optional)
// Signature values
V *big.Int `json:"v" gencodec:"required"`
@@ -45,15 +48,19 @@ type AltFeeTx struct {
}
// copy creates a deep copy of the transaction data and initializes all fields.
-func (tx *AltFeeTx) copy() TxData {
- cpy := &AltFeeTx{
+func (tx *MorphTx) copy() TxData {
+ cpy := &MorphTx{
Nonce: tx.Nonce,
+ Gas: tx.Gas,
To: copyAddressPtr(tx.To),
Data: common.CopyBytes(tx.Data),
- Gas: tx.Gas,
+ Reference: copyReferencePtr(tx.Reference),
+ Memo: common.CopyBytes(tx.Memo),
+ Version: tx.Version,
FeeTokenID: tx.FeeTokenID,
// These are copied below.
AccessList: make(AccessList, len(tx.AccessList)),
+ FeeLimit: new(big.Int),
Value: new(big.Int),
ChainID: new(big.Int),
GasTipCap: new(big.Int),
@@ -76,7 +83,7 @@ func (tx *AltFeeTx) copy() TxData {
cpy.GasFeeCap.Set(tx.GasFeeCap)
}
if tx.FeeLimit != nil {
- cpy.FeeLimit = new(big.Int).Set(tx.FeeLimit)
+ cpy.FeeLimit.Set(tx.FeeLimit)
}
if tx.V != nil {
cpy.V.Set(tx.V)
@@ -91,19 +98,19 @@ func (tx *AltFeeTx) copy() TxData {
}
// 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 *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 *AltFeeTx) effectiveGasPrice(dst *big.Int, baseFee *big.Int) *big.Int {
+func (tx *MorphTx) effectiveGasPrice(dst *big.Int, baseFee *big.Int) *big.Int {
if baseFee == nil {
return dst.Set(tx.GasFeeCap)
}
@@ -114,25 +121,25 @@ func (tx *AltFeeTx) effectiveGasPrice(dst *big.Int, baseFee *big.Int) *big.Int {
return tip.Add(tip, baseFee)
}
-func (tx *AltFeeTx) rawSignatureValues() (v, r, s *big.Int) {
+func (tx *MorphTx) rawSignatureValues() (v, r, s *big.Int) {
return tx.V, tx.R, tx.S
}
-func (tx *AltFeeTx) setSignatureValues(chainID, v, r, s *big.Int) {
+func (tx *MorphTx) 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 {
+func (tx *MorphTx) encode(b *bytes.Buffer) error {
return rlp.Encode(b, tx)
}
-func (tx *AltFeeTx) decode(input []byte) error {
+func (tx *MorphTx) decode(input []byte) error {
return rlp.DecodeBytes(input, tx)
}
-func (tx *AltFeeTx) sigHash(chainID *big.Int) common.Hash {
+func (tx *MorphTx) sigHash(chainID *big.Int) common.Hash {
return prefixedRlpHash(
- AltFeeTxType,
+ MorphTxType,
[]any{
chainID,
tx.Nonce,
diff --git a/core/types/receipt.go b/core/types/receipt.go
index 96bb17f98..d82c3fdd5 100644
--- a/core/types/receipt.go
+++ b/core/types/receipt.go
@@ -78,11 +78,19 @@ type Receipt struct {
// Morph rollup
L1Fee *big.Int `json:"l1Fee,omitempty"`
+
+ // MorphTx version
+ Version byte `json:"version,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"`
+
+ // Reference
+ ReferenceKey *common.Hash `json:"referenceKey,omitempty"`
+ Memo []byte `json:"memo,omitempty"`
}
type receiptMarshaling struct {
@@ -277,7 +285,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 {
@@ -531,8 +539,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..99fa5ae95 100644
--- a/core/types/receipt_test.go
+++ b/core/types/receipt_test.go
@@ -95,7 +95,7 @@ var (
Data: []byte{0x01, 0x00, 0xff},
},
},
- Type: AltFeeTxType,
+ Type: MorphTxType,
}
)
diff --git a/core/types/transaction.go b/core/types/transaction.go
index c164886e5..70a6de4f6 100644
--- a/core/types/transaction.go
+++ b/core/types/transaction.go
@@ -36,7 +36,7 @@ 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()")
+ 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")
errEmptyTypedTx = errors.New("empty typed transaction bytes")
@@ -55,7 +55,7 @@ const (
SetCodeTxType = 0x04
L1MessageTxType = 0x7E
- AltFeeTxType = 0x7F
+ MorphTxType = 0x7F
)
// Transaction is an Ethereum transaction.
@@ -220,8 +220,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 +331,9 @@ 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
}
// AsL1MessageTx casts the tx into an L1 cross-domain tx.
@@ -353,31 +353,31 @@ 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
}
// Cost returns gas * gasPrice + value.
func (tx *Transaction) Cost() *big.Int {
- if tx.IsAltFeeTx() {
+ if tx.IsMorphTx() {
panic(ErrCostNotSupported)
}
total := tx.GasFee()
@@ -834,7 +834,7 @@ func (tx *Transaction) AsMessage(s Signer, baseFee *big.Int) (Message, error) {
if baseFee != nil {
msg.gasPrice = math.BigMin(msg.gasPrice.Add(msg.gasTipCap, baseFee), msg.gasFeeCap)
}
- if tx.IsAltFeeTx() && tx.FeeTokenID() == 0 {
+ if tx.IsMorphTx() && tx.FeeTokenID() == 0 {
return msg, errors.New("token id 0 not support")
}
if tx.FeeLimit() != nil {
@@ -870,6 +870,15 @@ func copyAddressPtr(a *common.Address) *common.Address {
return &cpy
}
+// copyAddressPtr copies an address.
+func copyReferencePtr(h *common.Reference) *common.Reference {
+ if h == nil {
+ return nil
+ }
+ cpy := *h
+ 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..aa9d6344e 100644
--- a/core/types/transaction_marshalling.go
+++ b/core/types/transaction_marshalling.go
@@ -189,7 +189,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()
@@ -567,8 +567,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")
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/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/internal/ethapi/api.go b/internal/ethapi/api.go
index e032228bc..038b2b481 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -1505,7 +1505,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())
diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go
index 383829875..7861dcc9f 100644
--- a/internal/ethapi/transaction_args.go
+++ b/internal/ethapi/transaction_args.go
@@ -50,10 +50,14 @@ type TransactionArgs struct {
Data *hexutil.Bytes `json:"data"`
Input *hexutil.Bytes `json:"input"`
- // AltFeeTxType
+ // MorphTxType
FeeTokenID *hexutil.Uint16 `json:"feeTokenID,omitempty"`
FeeLimit *hexutil.Big `json:"feeLimit,omitempty"`
+ // Reference
+ Reference *common.Hash `json:"reference,omitempty"`
+ // TODO
+
// Introduced by AccessListTxType transaction.
AccessList *types.AccessList `json:"accessList,omitempty"`
ChainID *hexutil.Big `json:"chainId,omitempty"`
@@ -327,7 +331,7 @@ func (args *TransactionArgs) toTransaction() *types.Transaction {
switch {
// must take precedence over MaxFeePerGas.
case args.FeeTokenID != nil && *args.FeeTokenID > 0:
- usedType = types.AltFeeTxType
+ usedType = types.MorphTxType
case args.AuthorizationList != nil:
usedType = types.SetCodeTxType
case args.MaxFeePerGas != nil:
@@ -385,12 +389,12 @@ 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{
+ data = &types.MorphTx{
To: args.To,
ChainID: (*big.Int)(args.ChainID),
Nonce: uint64(*args.Nonce),
diff --git a/light/txpool.go b/light/txpool.go
index 09a1e0816..48846de2f 100644
--- a/light/txpool.go
+++ b/light/txpool.go
@@ -415,7 +415,7 @@ func (pool *TxPool) validateTx(ctx context.Context, tx *types.Transaction) 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.IsMorphTx() {
active, err := fees.IsTokenActive(currentState, tx.FeeTokenID())
if err != nil {
return fmt.Errorf("get token status failed %v", err)
@@ -455,7 +455,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.IsMorphTx() {
if b := currentState.GetBalance(from); b.Cmp(tx.Value()) < 0 {
return errors.New("invalid transaction: insufficient funds for value")
}
diff --git a/rollup/fees/rollup_fee.go b/rollup/fees/rollup_fee.go
index 456776fe5..c8d7c040b 100644
--- a/rollup/fees/rollup_fee.go
+++ b/rollup/fees/rollup_fee.go
@@ -95,7 +95,7 @@ func asUnsignedTx(msg Message, baseFee, chainID *big.Int) *types.Transaction {
return asUnsignedAccessListTx(msg, chainID)
}
if msg.FeeTokenID() != 0 {
- return asUnsignedAltFeeTx(msg, chainID)
+ return asUnsignedMorphTx(msg, chainID)
}
return asUnsignedDynamicTx(msg, chainID)
@@ -139,8 +139,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(),
diff --git a/rollup/tracing/tracing.go b/rollup/tracing/tracing.go
index 285a82968..65a1eede1 100644
--- a/rollup/tracing/tracing.go
+++ b/rollup/tracing/tracing.go
@@ -377,9 +377,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) {
diff --git a/signer/core/apitypes/types.go b/signer/core/apitypes/types.go
index cdc9121f5..dbcc84b65 100644
--- a/signer/core/apitypes/types.go
+++ b/signer/core/apitypes/types.go
@@ -123,7 +123,7 @@ func (args *SendTxArgs) ToTransaction() *types.Transaction {
if args.AccessList != nil {
al = *args.AccessList
}
- data = &types.AltFeeTx{
+ data = &types.MorphTx{
To: to,
ChainID: (*big.Int)(args.ChainID),
Nonce: uint64(args.Nonce),
From 739f07bb81ab1e0cb4e13a4ee7b72f13cefa843f Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Mon, 26 Jan 2026 09:48:17 +0800
Subject: [PATCH 02/33] add reference
---
accounts/abi/bind/backends/simulated.go | 7 +-
accounts/abi/bind/base.go | 12 +-
accounts/external/backend.go | 4 +
cmd/evm/internal/t8ntool/execution.go | 18 +-
core/blockchain.go | 94 +++++++
core/rawdb/accessors_chain.go | 3 +
core/rawdb/accessors_reference_index.go | 128 ++++++++++
core/rawdb/reference_index_iterator.go | 326 ++++++++++++++++++++++++
core/rawdb/schema.go | 27 ++
core/state_processor.go | 3 +
core/state_transition.go | 3 +
core/tx_list.go | 4 +-
core/tx_pool.go | 8 +-
core/types/l2trace.go | 25 +-
core/types/morph_tx.go | 5 +-
core/types/receipt.go | 76 +++++-
core/types/receipt_test.go | 43 +++-
core/types/transaction.go | 71 +++++-
core/types/transaction_marshalling.go | 15 +-
interfaces.go | 6 +-
internal/ethapi/api.go | 44 +++-
internal/ethapi/transaction_args.go | 36 ++-
light/odr_test.go | 22 +-
light/txpool.go | 4 +-
rollup/fees/rollup_fee.go | 8 +-
rollup/tracing/tracing.go | 3 +
signer/core/apitypes/types.go | 6 +
tests/state_test_util.go | 16 +-
28 files changed, 940 insertions(+), 77 deletions(-)
create mode 100644 core/rawdb/accessors_reference_index.go
create mode 100644 core/rawdb/reference_index_iterator.go
diff --git a/accounts/abi/bind/backends/simulated.go b/accounts/abi/bind/backends/simulated.go
index c009c0233..0aa37e8f6 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() byte { 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 84fe5645f..2027490a5 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 byte // version of morph tx
+ 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)
@@ -342,6 +345,9 @@ func (c *BoundContract) createMorphTx(opts *TransactOpts, contract *common.Addre
FeeTokenID: opts.FeeTokenID,
FeeLimit: opts.FeeLimit,
Gas: gasLimit,
+ Version: opts.Version,
+ Reference: opts.Reference,
+ Memo: opts.Memo,
Value: value,
Data: input,
}
@@ -438,7 +444,7 @@ 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 {
+ if opts.FeeTokenID != 0 || opts.Version != 0 || opts.Reference != 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/external/backend.go b/accounts/external/backend.go
index 392808eb4..a70276137 100644
--- a/accounts/external/backend.go
+++ b/accounts/external/backend.go
@@ -227,6 +227,10 @@ func (api *ExternalSigner) SignTx(account accounts.Account, tx *types.Transactio
feeTokenID := hexutil.Uint16(tx.FeeTokenID())
args.FeeTokenID = &feeTokenID
args.FeeLimit = (*hexutil.Big)(tx.FeeLimit())
+ args.Version = tx.Version()
+ args.Reference = tx.Reference()
+ 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..c132bf53e 100644
--- a/cmd/evm/internal/t8ntool/execution.go
+++ b/cmd/evm/internal/t8ntool/execution.go
@@ -207,12 +207,18 @@ 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 || 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
+ }
+ 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/core/blockchain.go b/core/blockchain.go
index 3b26115a7..764ccb267 100644
--- a/core/blockchain.go
+++ b/core/blockchain.go
@@ -408,6 +408,12 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *par
go bc.maintainTxIndex(txIndexBlock)
}
+ // Start reference indexer/unindexer (using same lookup limit as tx index).
+ if txLookupLimit != nil {
+ bc.wg.Add(1)
+ go bc.maintainReferenceIndex(txIndexBlock)
+ }
+
// If periodic cache journal is required, spin it up.
if bc.cacheConfig.TrieCleanRejournal > 0 {
if bc.cacheConfig.TrieCleanRejournal < time.Minute {
@@ -2134,6 +2140,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/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go
index d150d09d8..7f804bec6 100644
--- a/core/rawdb/accessors_chain.go
+++ b/core/rawdb/accessors_chain.go
@@ -623,6 +623,9 @@ type storedReceiptRLP struct {
FeeRate *big.Int
TokenScale *big.Int
FeeLimit *big.Int
+ Version byte
+ Reference *common.Reference
+ Memo []byte
}
// 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..c7531df99
--- /dev/null
+++ b/core/rawdb/accessors_reference_index.go
@@ -0,0 +1,128 @@
+// 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/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())
+ }
+ }
+ }
+}
+
+// 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 {
+ prefix := referenceIndexKeyPrefix(reference)
+ it := db.NewIterator(prefix, nil)
+ defer it.Release()
+
+ var entries []ReferenceIndexEntry
+ for it.Next() {
+ key := it.Key()
+ // 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 {
+ continue
+ }
+
+ 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:])
+
+ entries = append(entries, ReferenceIndexEntry{
+ BlockTimestamp: blockTimestamp,
+ TxIndex: txIndex,
+ TxHash: txHash,
+ })
+ }
+
+ if it.Error() != nil {
+ log.Error("Failed to iterate reference index entries", "reference", reference.Hex(), "err", it.Error())
+ }
+
+ return entries
+}
+
+// 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/reference_index_iterator.go b/core/rawdb/reference_index_iterator.go
new file mode 100644
index 000000000..42a970ae6
--- /dev/null
+++ b/core/rawdb/reference_index_iterator.go
@@ -0,0 +1,326 @@
+// 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)
+ 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)
+ 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(),
+ })
+ }
+ }
+ }
+
+ if len(refs) > 0 {
+ 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 8b7ffd07a..1b5e4b55a 100644
--- a/core/rawdb/schema.go
+++ b/core/rawdb/schema.go
@@ -122,6 +122,11 @@ var (
rollupBatchSignaturePrefix = []byte("R-bs")
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")
)
const (
@@ -310,3 +315,25 @@ func RollupBatchSignatureSignerKey(batchHash common.Hash, signer common.Address)
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[:]...)
+}
diff --git a/core/state_processor.go b/core/state_processor.go
index 58b07f630..a5a239e19 100644
--- a/core/state_processor.go
+++ b/core/state_processor.go
@@ -198,6 +198,9 @@ func ApplyTransactionWithEVM(msg Message, config *params.ChainConfig, gp *GasPoo
receipt.FeeLimit = tx.FeeLimit()
receipt.FeeRate = result.FeeRate
receipt.TokenScale = result.TokenScale
+ 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..7790d0bc8 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 9a1103b12..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.IsMorphTx() {
+ 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.IsMorphTx() {
+ 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 2afafe26a..661cc1c3a 100644
--- a/core/tx_pool.go
+++ b/core/tx_pool.go
@@ -724,7 +724,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.IsMorphTx() {
+ 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 +763,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.IsMorphTx() {
+ if tx.IsMorphTxWithAltFee() {
if b := pool.currentState.GetBalance(from); b.Cmp(tx.Value()) < 0 {
return ErrInsufficientValue
}
@@ -1617,7 +1617,7 @@ 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.IsMorphTx() && (tx.Gas() > pool.currentMaxGas || tx.Cost().Cmp(costLimit) > 0) {
+ if !tx.IsMorphTxWithAltFee() && (tx.Gas() > pool.currentMaxGas || tx.Cost().Cmp(costLimit) > 0) {
return true
}
if pool.chainconfig.Morph.FeeVaultEnabled() {
@@ -1627,7 +1627,7 @@ func (pool *TxPool) executableTxFilter(addr common.Address, costLimit *big.Int,
log.Error("Failed to calculate L1 data fee", "err", err, "tx", tx)
return false
}
- if tx.IsMorphTx() {
+ 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 {
diff --git a/core/types/l2trace.go b/core/types/l2trace.go
index 6dc751a06..1921f132a 100644
--- a/core/types/l2trace.go
+++ b/core/types/l2trace.go
@@ -56,14 +56,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 []byte `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)
@@ -140,6 +143,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 []byte `json:"memo,omitempty"`
From common.Address `json:"from"`
To *common.Address `json:"to"`
ChainId *hexutil.Big `json:"chainId"`
@@ -204,6 +210,9 @@ func NewTransactionData(tx *Transaction, blockNumber uint64, blockTime uint64, c
if feeLimit := tx.FeeLimit(); feeLimit != nil && feeLimit.Sign() > 0 {
result.FeeLimit = (*hexutil.Big)(feeLimit)
}
+ result.Version = tx.Version()
+ result.Reference = (*common.Reference)(tx.Reference())
+ result.Memo = []byte(hexutil.Encode(tx.Memo()))
}
return result
diff --git a/core/types/morph_tx.go b/core/types/morph_tx.go
index f7f718601..3eba2d868 100644
--- a/core/types/morph_tx.go
+++ b/core/types/morph_tx.go
@@ -35,7 +35,7 @@ type MorphTx struct {
Data []byte
AccessList AccessList
- Version byte // version of morph tx
+ Version uint8 // version of morph tx
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)
@@ -150,7 +150,10 @@ func (tx *MorphTx) sigHash(chainID *big.Int) common.Hash {
tx.Value,
tx.Data,
tx.AccessList,
+ tx.Version,
tx.FeeTokenID,
tx.FeeLimit,
+ tx.Reference,
+ tx.Memo,
})
}
diff --git a/core/types/receipt.go b/core/types/receipt.go
index d82c3fdd5..a6868c469 100644
--- a/core/types/receipt.go
+++ b/core/types/receipt.go
@@ -79,18 +79,14 @@ type Receipt struct {
// Morph rollup
L1Fee *big.Int `json:"l1Fee,omitempty"`
- // MorphTx version
- Version byte `json:"version,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"`
-
- // Reference
- ReferenceKey *common.Hash `json:"referenceKey,omitempty"`
- Memo []byte `json:"memo,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 {
@@ -109,6 +105,10 @@ type receiptMarshaling struct {
FeeRate *hexutil.Big
TokenScale *hexutil.Big
FeeLimit *hexutil.Big
+ // TODO
+ Version hexutil.Uint64
+ Reference *common.Reference
+ Memo hexutil.Bytes
}
// receiptRLP is the consensus encoding of a receipt.
@@ -129,6 +129,26 @@ type storedReceiptRLP struct {
FeeRate *big.Int
TokenScale *big.Int
FeeLimit *big.Int
+ Version uint8
+ Reference *common.Reference
+ Memo []byte
+}
+
+// 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 *common.Reference
+ Memo []byte
}
// v7StoredReceiptRLP is the storage encoding of a receipt used in database version 7.
@@ -354,6 +374,9 @@ func (r *ReceiptForStorage) EncodeRLP(w io.Writer) error {
FeeRate: r.FeeRate,
TokenScale: r.TokenScale,
FeeLimit: r.FeeLimit,
+ Version: r.Version,
+ Reference: r.Reference,
+ Memo: r.Memo,
}
for i, log := range r.Logs {
enc.Logs[i] = (*LogForStorage)(log)
@@ -375,6 +398,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
}
@@ -413,6 +439,32 @@ func decodeStoredReceiptRLP(r *ReceiptForStorage, blob []byte) error {
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
+ r.Reference = stored.Reference
+ r.Memo = stored.Memo
+
+ return nil
+}
+
func decodeV7StoredReceiptRLP(r *ReceiptForStorage, blob []byte) error {
var stored v7StoredReceiptRLP
if err := rlp.DecodeBytes(blob, &stored); err != nil {
diff --git a/core/types/receipt_test.go b/core/types/receipt_test.go
index 99fa5ae95..4b859bbb7 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{
@@ -117,6 +117,10 @@ func TestLegacyReceiptDecoding(t *testing.T) {
"StoredReceiptRLP",
encodeAsStoredReceiptRLP,
},
+ {
+ "V8StoredReceiptRLP",
+ encodeAsV8StoredReceiptRLP,
+ },
{
"V7StoredReceiptRLP",
encodeAsV7StoredReceiptRLP,
@@ -212,6 +216,26 @@ func encodeAsStoredReceiptRLP(want *Receipt) ([]byte, error) {
return rlp.EncodeToBytes(stored)
}
+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: want.Reference,
+ Memo: want.Memo,
+ }
+ for i, log := range want.Logs {
+ stored.Logs[i] = (*LogForStorage)(log)
+ }
+ return rlp.EncodeToBytes(stored)
+}
+
func encodeAsV7StoredReceiptRLP(want *Receipt) ([]byte, error) {
stored := &v7StoredReceiptRLP{
PostStateOrStatus: want.statusEncoding(),
@@ -501,22 +525,23 @@ func TestReceiptMarshalBinary(t *testing.T) {
t.Errorf("encoded RLP mismatch, got %x want %x", have, eip1559Want)
}
- // alt fee Receipt
+ // TODO
+ // 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("02f901c58001b9010000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000010000080000000000000000000004000000000000000000000000000040000000000000000000000000000800000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000f8bef85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100fff85d940000000000000000000000000000000000000111f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff")
+ if !bytes.Equal(have, morphTxWant) {
+ t.Errorf("encoded RLP mismatch, got %x want %x", have, morphTxWant)
}
}
diff --git a/core/types/transaction.go b/core/types/transaction.go
index 70a6de4f6..e22f60d54 100644
--- a/core/types/transaction.go
+++ b/core/types/transaction.go
@@ -333,9 +333,15 @@ func (tx *Transaction) IsL1MessageTx() bool {
// IsMorphTx returns true if the transaction is morph tx.
func (tx *Transaction) IsMorphTx() bool {
+ // TODO: check if altfee used
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.
func (tx *Transaction) AsL1MessageTx() *L1MessageTx {
if !tx.IsL1MessageTx() {
@@ -375,8 +381,33 @@ func (tx *Transaction) FeeLimit() *big.Int {
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 []byte{}
+ }
+ return tx.AsMorphTx().Memo
+}
+
// Cost returns gas * gasPrice + value.
func (tx *Transaction) Cost() *big.Int {
+ // TODO: morph tx without fee token
if tx.IsMorphTx() {
panic(ErrCostNotSupported)
}
@@ -791,9 +822,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 +862,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 +884,18 @@ 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.IsMorphTx() && tx.FeeTokenID() == 0 {
- return msg, errors.New("token id 0 not support")
- }
if tx.FeeLimit() != nil {
msg.feeLimit = tx.FeeLimit()
}
+
var err error
msg.from, err = Sender(s, tx)
return msg, err
@@ -860,6 +916,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 {
diff --git a/core/types/transaction_marshalling.go b/core/types/transaction_marshalling.go
index aa9d6344e..bfb91be21 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 *uint8 `json:"version"`
+ Reference *common.Reference `json:"reference"`
+ Memo *hexutil.Bytes `json:"memo"`
}
// yParityValue returns the YParity value from JSON. For backwards-compatibility reasons,
@@ -201,6 +204,9 @@ func (tx *Transaction) MarshalJSON() ([]byte, error) {
enc.AccessList = &itx.AccessList
enc.FeeTokenID = hexutil.Uint16(itx.FeeTokenID)
enc.FeeLimit = (*hexutil.Big)(itx.FeeLimit)
+ enc.Version = (*uint8)(&itx.Version)
+ 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)
@@ -599,6 +605,9 @@ func (tx *Transaction) UnmarshalJSON(input []byte) error {
itx.FeeTokenID = uint16(dec.FeeTokenID)
itx.FeeLimit = (*big.Int)(dec.FeeLimit)
itx.Value = (*big.Int)(dec.Value)
+ itx.Version = *dec.Version
+ itx.Reference = (*common.Reference)(dec.Reference)
+ itx.Memo = *dec.Memo
if dec.Input == nil {
return errors.New("missing required field 'input' in transaction")
}
diff --git a/interfaces.go b/interfaces.go
index 52c255258..879514d98 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 byte
+ Reference *common.Reference
+ Memo []byte
+
// For SetCodeTxType
AuthorizationList []types.SetCodeAuthorization
}
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
index 038b2b481..85e603afe 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -39,6 +39,7 @@ import (
"github.com/morph-l2/go-ethereum/consensus/ethash"
"github.com/morph-l2/go-ethereum/consensus/misc"
"github.com/morph-l2/go-ethereum/core"
+ "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"
@@ -1452,9 +1453,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.Uint64 `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"`
}
// NewRPCTransaction returns a transaction that will serialize to the RPC
@@ -1521,6 +1525,10 @@ func NewRPCTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber
}
result.FeeTokenID = (hexutil.Uint64)(tx.FeeTokenID())
result.FeeLimit = (*hexutil.Big)(tx.FeeLimit())
+ result.Version = tx.Version()
+ result.Reference = tx.Reference()
+ memo := hexutil.Bytes(tx.Memo())
+ result.Memo = &memo
case types.SetCodeTxType:
al := tx.AccessList()
yparity := hexutil.Uint64(v.Sign())
@@ -1796,6 +1804,31 @@ func (s *PublicTransactionPoolAPI) GetTransactionCount(ctx context.Context, addr
return (*hexutil.Uint64)(&nonce), state.Error()
}
+// GetTransactionsByReference returns all transactions for the given reference.
+// Results are sorted by blockTimestamp and txIndex (ascending order).
+func (s *PublicTransactionPoolAPI) GetTransactionsByReference(ctx context.Context, reference common.Reference) ([]*RPCTransaction, error) {
+ entries := rawdb.ReadReferenceIndexEntries(s.b.ChainDb(), reference)
+ if len(entries) == 0 {
+ return nil, nil
+ }
+
+ var result []*RPCTransaction
+ for _, entry := range entries {
+ tx, blockHash, blockNumber, index, err := s.b.GetTransaction(ctx, entry.TxHash)
+ if err != nil {
+ continue
+ }
+ if tx != nil {
+ header, err := s.b.HeaderByHash(ctx, blockHash)
+ if err != nil {
+ continue
+ }
+ result = append(result, NewRPCTransaction(tx, blockHash, blockNumber, header.Time, index, header.BaseFee, s.b.ChainConfig()))
+ }
+ }
+ return result, nil
+}
+
// GetTransactionByHash returns the transaction for the given hash
func (s *PublicTransactionPoolAPI) GetTransactionByHash(ctx context.Context, hash common.Hash) (*RPCTransaction, error) {
// Try to return an already finalized transaction
@@ -1891,7 +1924,12 @@ func marshalReceipt(ctx context.Context, b Backend, receipt *types.Receipt, bigb
"feeRate": (*hexutil.Big)(receipt.FeeRate),
"tokenScale": (*hexutil.Big)(receipt.TokenScale),
"feeLimit": (*hexutil.Big)(receipt.FeeLimit),
+ // TODO
+ "version": hexutil.Uint64(receipt.Version),
+ "reference": (*common.Reference)(receipt.Reference),
+ "memo": hexutil.Bytes(receipt.Memo),
}
+ // TODO
if receipt.FeeTokenID != nil {
fields["feeTokenID"] = hexutil.Uint16(*receipt.FeeTokenID)
}
diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go
index 7861dcc9f..0cb200d3a 100644
--- a/internal/ethapi/transaction_args.go
+++ b/internal/ethapi/transaction_args.go
@@ -51,12 +51,11 @@ type TransactionArgs struct {
Input *hexutil.Bytes `json:"input"`
// MorphTxType
- FeeTokenID *hexutil.Uint16 `json:"feeTokenID,omitempty"`
- FeeLimit *hexutil.Big `json:"feeLimit,omitempty"`
-
- // Reference
- Reference *common.Hash `json:"reference,omitempty"`
- // TODO
+ FeeTokenID *hexutil.Uint16 `json:"feeTokenID,omitempty"`
+ FeeLimit *hexutil.Big `json:"feeLimit,omitempty"`
+ Version *byte `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"`
@@ -175,6 +174,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,
@@ -319,8 +321,20 @@ func (args *TransactionArgs) ToMessage(globalGasCap uint64, baseFee *big.Int) (t
if args.FeeLimit != nil {
feeLimit = args.FeeLimit.ToInt()
}
+ if args.Version == nil {
+ return types.Message{}, errors.New("version is not set")
+ }
+ version := *args.Version
+ reference := new(common.Reference)
+ if args.Reference != nil {
+ reference = args.Reference
+ }
+ memo := []byte{}
+ if args.Memo != nil {
+ memo = *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
}
@@ -330,7 +344,10 @@ func (args *TransactionArgs) toTransaction() *types.Transaction {
usedType := types.LegacyTxType
switch {
// must take precedence over MaxFeePerGas.
- case args.FeeTokenID != nil && *args.FeeTokenID > 0:
+ case (args.FeeTokenID != nil && *args.FeeTokenID > 0) ||
+ (args.Version != nil && *args.Version > 0) ||
+ (args.Reference != nil && *args.Reference != (common.Reference{})) ||
+ (args.Memo != nil && len(*args.Memo) > 0):
usedType = types.MorphTxType
case args.AuthorizationList != nil:
usedType = types.SetCodeTxType
@@ -401,8 +418,11 @@ func (args *TransactionArgs) toTransaction() *types.Transaction {
Gas: uint64(*args.Gas),
GasFeeCap: (*big.Int)(args.MaxFeePerGas),
GasTipCap: (*big.Int)(args.MaxPriorityFeePerGas),
+ Version: *args.Version,
FeeTokenID: uint16(*args.FeeTokenID),
FeeLimit: (*big.Int)(args.FeeLimit),
+ Reference: args.Reference,
+ Memo: *args.Memo,
Value: (*big.Int)(args.Value),
Data: args.data(),
AccessList: al,
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 48846de2f..2b737dd34 100644
--- a/light/txpool.go
+++ b/light/txpool.go
@@ -415,7 +415,7 @@ func (pool *TxPool) validateTx(ctx context.Context, tx *types.Transaction) 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.IsMorphTx() {
+ if tx.IsMorphTxWithAltFee() {
active, err := fees.IsTokenActive(currentState, tx.FeeTokenID())
if err != nil {
return fmt.Errorf("get token status failed %v", err)
@@ -455,7 +455,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.IsMorphTx() {
+ 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/rollup/fees/rollup_fee.go b/rollup/fees/rollup_fee.go
index c8d7c040b..5f9cfc157 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() byte
+ Reference() *common.Reference
+ Memo() []byte
}
// StateDB represents the StateDB interface
@@ -94,7 +97,7 @@ func asUnsignedTx(msg Message, baseFee, chainID *big.Int) *types.Transaction {
return asUnsignedAccessListTx(msg, chainID)
}
- if msg.FeeTokenID() != 0 {
+ if msg.FeeTokenID() != 0 || msg.Version() != 0 || msg.Reference() != nil || len(msg.Memo()) > 0 {
return asUnsignedMorphTx(msg, chainID)
}
@@ -149,6 +152,9 @@ func asUnsignedMorphTx(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 65a1eede1..561965d56 100644
--- a/rollup/tracing/tracing.go
+++ b/rollup/tracing/tracing.go
@@ -551,6 +551,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: receipt.Memo,
FeeRate: (*hexutil.Big)(receipt.FeeRate),
TokenScale: (*hexutil.Big)(receipt.TokenScale),
Gas: receipt.GasUsed,
diff --git a/signer/core/apitypes/types.go b/signer/core/apitypes/types.go
index dbcc84b65..f012b15e8 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 byte `json:"version,omitempty"`
+ Reference *common.Reference `json:"reference,omitempty"`
+ Memo *hexutil.Bytes `json:"memo,omitempty"`
}
func (args SendTxArgs) String() string {
@@ -132,6 +135,9 @@ func (args *SendTxArgs) ToTransaction() *types.Transaction {
GasTipCap: (*big.Int)(args.MaxPriorityFeePerGas),
FeeTokenID: uint16(*args.FeeTokenID),
FeeLimit: (*big.Int)(args.FeeLimit),
+ Version: args.Version,
+ Reference: args.Reference,
+ Memo: *args.Memo,
Value: (*big.Int)(&args.Value),
Data: input,
AccessList: al,
diff --git a/tests/state_test_util.go b/tests/state_test_util.go
index 75cd62b24..9ac93c2b1 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
}
From 2a10ba930e4e58c91c17f51278d031de25efe292 Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Wed, 28 Jan 2026 09:15:36 +0800
Subject: [PATCH 03/33] update
---
accounts/abi/bind/backends/simulated.go | 2 +-
accounts/abi/bind/base.go | 9 +-
accounts/external/backend.go | 5 +-
cmd/evm/internal/t8ntool/execution.go | 8 +-
common/types.go | 2 +
core/block_validator.go | 6 ++
core/state_processor.go | 3 +-
core/state_transition.go | 2 +-
core/tx_pool.go | 4 +
core/types/gen_receipt_json.go | 108 ++++++++++++++----------
core/types/l2trace.go | 11 +--
core/types/morph_tx.go | 4 +-
core/types/receipt.go | 13 ++-
core/types/receipt_test.go | 3 +-
core/types/transaction.go | 23 +++--
core/types/transaction_marshalling.go | 4 +-
ethclient/ethclient.go | 12 +++
interfaces.go | 2 +-
internal/ethapi/api.go | 101 ++++++++++++++++------
internal/ethapi/transaction_args.go | 10 ++-
les/odr_test.go | 43 +++++++++-
rollup/fees/rollup_fee.go | 9 +-
rollup/tracing/tracing.go | 4 +-
signer/core/apitypes/types.go | 6 +-
tests/state_test_util.go | 2 +-
25 files changed, 274 insertions(+), 122 deletions(-)
diff --git a/accounts/abi/bind/backends/simulated.go b/accounts/abi/bind/backends/simulated.go
index 0aa37e8f6..c91201549 100644
--- a/accounts/abi/bind/backends/simulated.go
+++ b/accounts/abi/bind/backends/simulated.go
@@ -922,7 +922,7 @@ func (m callMsg) FeeTokenID() uint16 { return m.CallMsg.FeeTokenID }
func (m callMsg) FeeLimit() *big.Int { return m.CallMsg.FeeLimit }
func (m callMsg) Version() byte { return m.CallMsg.Version }
func (m callMsg) Reference() *common.Reference { return m.CallMsg.Reference }
-func (m callMsg) Memo() []byte { return m.CallMsg.Memo }
+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 2027490a5..c18ee9929 100644
--- a/accounts/abi/bind/base.go
+++ b/accounts/abi/bind/base.go
@@ -59,9 +59,9 @@ type TransactOpts struct {
FeeTokenID uint16 // alt fee token id of transaction execution
FeeLimit *big.Int // alt fee token limit of transaction execution
- Version byte // version of morph tx
+ Version uint8 // version of morph tx
Reference *common.Reference // reference key for the transaction (optional)
- Memo []byte // memo for the transaction (optional)
+ Memo *[]byte // memo for the transaction (optional)
Context context.Context // Network context to support cancellation and timeouts (nil = no timeout)
@@ -444,7 +444,10 @@ 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 || opts.Version != 0 || opts.Reference != nil || len(opts.Memo) > 0 {
+ if opts.FeeTokenID != 0 ||
+ opts.Version != 0 ||
+ (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/external/backend.go b/accounts/external/backend.go
index a70276137..bdce46497 100644
--- a/accounts/external/backend.go
+++ b/accounts/external/backend.go
@@ -227,9 +227,10 @@ func (api *ExternalSigner) SignTx(account accounts.Account, tx *types.Transactio
feeTokenID := hexutil.Uint16(tx.FeeTokenID())
args.FeeTokenID = &feeTokenID
args.FeeLimit = (*hexutil.Big)(tx.FeeLimit())
- args.Version = tx.Version()
+ version := hexutil.Uint64(tx.Version())
+ args.Version = &version
args.Reference = tx.Reference()
- memo := hexutil.Bytes(tx.Memo())
+ 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 c132bf53e..225aa2fd8 100644
--- a/cmd/evm/internal/t8ntool/execution.go
+++ b/cmd/evm/internal/t8ntool/execution.go
@@ -208,7 +208,10 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
receipt.GasUsed = msgResult.UsedGas
receipt.L1Fee = msgResult.L1DataFee
- if msg.FeeTokenID() != 0 || msg.Version() != 0 || msg.Reference() != nil || len(msg.Memo()) > 0 {
+ 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
@@ -216,7 +219,8 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
receipt.FeeRate = msgResult.FeeRate
receipt.TokenScale = msgResult.TokenScale
}
- receipt.Version = msg.Version()
+ version := msg.Version()
+ receipt.Version = version
receipt.Reference = msg.Reference()
receipt.Memo = msg.Memo()
}
diff --git a/common/types.go b/common/types.go
index 4e320203a..c118f61d7 100644
--- a/common/types.go
+++ b/common/types.go
@@ -41,6 +41,8 @@ const (
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 (
diff --git a/core/block_validator.go b/core/block_validator.go
index 01382fd64..60448294d 100644
--- a/core/block_validator.go
+++ b/core/block_validator.go
@@ -59,6 +59,12 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error {
if !v.config.Morph.IsValidBlockSize(block.PayloadSize()) {
return ErrInvalidBlockPayloadSize
}
+ // Validate MorphTx memo length for all transactions
+ for _, tx := range block.Transactions() {
+ if err := tx.ValidateMemo(); 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 {
diff --git a/core/state_processor.go b/core/state_processor.go
index a5a239e19..5679a8df7 100644
--- a/core/state_processor.go
+++ b/core/state_processor.go
@@ -200,7 +200,8 @@ func ApplyTransactionWithEVM(msg Message, config *params.ChainConfig, gp *GasPoo
receipt.TokenScale = result.TokenScale
receipt.Version = tx.Version()
receipt.Reference = tx.Reference()
- receipt.Memo = tx.Memo()
+ memo := tx.Memo()
+ receipt.Memo = memo
}
return receipt, err
diff --git a/core/state_transition.go b/core/state_transition.go
index 7790d0bc8..72c0efca8 100644
--- a/core/state_transition.go
+++ b/core/state_transition.go
@@ -102,7 +102,7 @@ type Message interface {
FeeLimit() *big.Int
Version() uint8
Reference() *common.Reference
- Memo() []byte
+ Memo() *[]byte
}
// ExecutionResult includes all output after executing given evm
diff --git a/core/tx_pool.go b/core/tx_pool.go
index 661cc1c3a..bf3acc9a9 100644
--- a/core/tx_pool.go
+++ b/core/tx_pool.go
@@ -673,6 +673,10 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
if !pool.eip1559 && tx.Type() == types.MorphTxType {
return ErrTxTypeNotSupported
}
+ // Validate MorphTx memo length
+ if err := tx.ValidateMemo(); err != nil {
+ return err
+ }
if !pool.eip7702 && tx.Type() == types.SetCodeTxType {
return ErrTxTypeNotSupported
}
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 1921f132a..cb7be01e7 100644
--- a/core/types/l2trace.go
+++ b/core/types/l2trace.go
@@ -61,9 +61,9 @@ type ExecutionResult struct {
FeeRate *hexutil.Big `json:"feeRate,omitempty"`
TokenScale *hexutil.Big `json:"tokenScale,omitempty"`
FeeLimit *hexutil.Big `json:"feeLimit,omitempty"`
- Version uint8 `json:"version,omitempty"`
+ Version *uint8 `json:"version,omitempty"`
Reference *common.Reference `json:"reference,omitempty"`
- Memo []byte `json:"memo,omitempty"`
+ Memo *hexutil.Bytes `json:"memo,omitempty"`
Gas uint64 `json:"gas"`
Failed bool `json:"failed"`
ReturnValue string `json:"returnValue"`
@@ -145,7 +145,7 @@ type TransactionData struct {
FeeLimit *hexutil.Big `json:"feeLimit,omitempty"`
Version uint8 `json:"version,omitempty"`
Reference *common.Reference `json:"reference,omitempty"`
- Memo []byte `json:"memo,omitempty"`
+ Memo *hexutil.Bytes `json:"memo,omitempty"`
From common.Address `json:"from"`
To *common.Address `json:"to"`
ChainId *hexutil.Big `json:"chainId"`
@@ -211,8 +211,9 @@ func NewTransactionData(tx *Transaction, blockNumber uint64, blockTime uint64, c
result.FeeLimit = (*hexutil.Big)(feeLimit)
}
result.Version = tx.Version()
- result.Reference = (*common.Reference)(tx.Reference())
- result.Memo = []byte(hexutil.Encode(tx.Memo()))
+ result.Reference = tx.Reference()
+ memo := hexutil.Bytes(*tx.Memo())
+ result.Memo = &memo
}
return result
diff --git a/core/types/morph_tx.go b/core/types/morph_tx.go
index 3eba2d868..f63929af7 100644
--- a/core/types/morph_tx.go
+++ b/core/types/morph_tx.go
@@ -39,7 +39,7 @@ type MorphTx struct {
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)
- Memo []byte // memo for the transaction (optional)
+ Memo *[]byte // memo for the transaction (optional)
// Signature values
V *big.Int `json:"v" gencodec:"required"`
@@ -55,7 +55,7 @@ func (tx *MorphTx) copy() TxData {
To: copyAddressPtr(tx.To),
Data: common.CopyBytes(tx.Data),
Reference: copyReferencePtr(tx.Reference),
- Memo: common.CopyBytes(tx.Memo),
+ Memo: tx.Memo,
Version: tx.Version,
FeeTokenID: tx.FeeTokenID,
// These are copied below.
diff --git a/core/types/receipt.go b/core/types/receipt.go
index a6868c469..4eeb300bc 100644
--- a/core/types/receipt.go
+++ b/core/types/receipt.go
@@ -86,7 +86,7 @@ type Receipt struct {
FeeLimit *big.Int `json:"feeLimit,omitempty"`
Version uint8 `json:"version,omitempty"`
Reference *common.Reference `json:"reference,omitempty"`
- Memo []byte `json:"memo,omitempty"`
+ Memo *[]byte `json:"memo,omitempty"`
}
type receiptMarshaling struct {
@@ -105,10 +105,9 @@ type receiptMarshaling struct {
FeeRate *hexutil.Big
TokenScale *hexutil.Big
FeeLimit *hexutil.Big
- // TODO
- Version hexutil.Uint64
- Reference *common.Reference
- Memo hexutil.Bytes
+ Version hexutil.Uint64
+ Reference *common.Reference
+ Memo *hexutil.Bytes
}
// receiptRLP is the consensus encoding of a receipt.
@@ -131,7 +130,7 @@ type storedReceiptRLP struct {
FeeLimit *big.Int
Version uint8
Reference *common.Reference
- Memo []byte
+ Memo *[]byte
}
// v8StoredReceiptRLP is the storage encoding of a receipt used in database version 8.
@@ -148,7 +147,7 @@ type v8StoredReceiptRLP struct {
FeeLimit *big.Int
Version uint8
Reference *common.Reference
- Memo []byte
+ Memo *[]byte
}
// v7StoredReceiptRLP is the storage encoding of a receipt used in database version 7.
diff --git a/core/types/receipt_test.go b/core/types/receipt_test.go
index 4b859bbb7..fed78ad70 100644
--- a/core/types/receipt_test.go
+++ b/core/types/receipt_test.go
@@ -525,7 +525,6 @@ func TestReceiptMarshalBinary(t *testing.T) {
t.Errorf("encoded RLP mismatch, got %x want %x", have, eip1559Want)
}
- // TODO
// MorphTx Receipt
buf.Reset()
morphTxReceipt.Bloom = CreateBloom(Receipts{morphTxReceipt})
@@ -539,7 +538,7 @@ func TestReceiptMarshalBinary(t *testing.T) {
if !bytes.Equal(have, haveEncodeIndex) {
t.Errorf("BinaryMarshal and EncodeIndex mismatch, got %x want %x", have, haveEncodeIndex)
}
- morphTxWant := common.FromHex("02f901c58001b9010000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000010000080000000000000000000004000000000000000000000000000040000000000000000000000000000800000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000f8bef85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100fff85d940000000000000000000000000000000000000111f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff")
+ morphTxWant := common.FromHex("7ff901c58001b9010000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000010000080000000000000000000004000000000000000000000000000040000000000000000000000000000800000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000f8bef85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100fff85d940000000000000000000000000000000000000111f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff")
if !bytes.Equal(have, morphTxWant) {
t.Errorf("encoded RLP mismatch, got %x want %x", have, morphTxWant)
}
diff --git a/core/types/transaction.go b/core/types/transaction.go
index e22f60d54..a13a20f6f 100644
--- a/core/types/transaction.go
+++ b/core/types/transaction.go
@@ -39,6 +39,7 @@ var (
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")
errEmptyTypedTx = errors.New("empty typed transaction bytes")
errShortTypedTx = errors.New("typed transaction too short")
errInvalidYParity = errors.New("'yParity' field must be 0 or 1")
@@ -398,13 +399,25 @@ func (tx *Transaction) Reference() *common.Reference {
}
// Memo returns the memo of the MorphTx, or nil if not a MorphTx.
-func (tx *Transaction) Memo() []byte {
+func (tx *Transaction) Memo() *[]byte {
if !tx.IsMorphTx() {
- return []byte{}
+ return nil
}
return tx.AsMorphTx().Memo
}
+// ValidateMemo validates that the memo length does not exceed the maximum limit.
+// Returns nil if the transaction is not a MorphTx or if the memo length is valid.
+func (tx *Transaction) ValidateMemo() error {
+ if !tx.IsMorphTx() {
+ return nil
+ }
+ if tx.AsMorphTx().Memo != nil && len(*tx.AsMorphTx().Memo) > common.MaxMemoLength {
+ return ErrMemoTooLong
+ }
+ return nil
+}
+
// Cost returns gas * gasPrice + value.
func (tx *Transaction) Cost() *big.Int {
// TODO: morph tx without fee token
@@ -824,7 +837,7 @@ type Message struct {
feeLimit *big.Int
version uint8
reference *common.Reference
- memo []byte
+ memo *[]byte
}
func NewMessage(
@@ -840,7 +853,7 @@ func NewMessage(
feeLimit *big.Int,
version uint8,
reference *common.Reference,
- memo []byte,
+ memo *[]byte,
data []byte,
accessList AccessList,
authList []SetCodeAuthorization,
@@ -918,7 +931,7 @@ func (m Message) FeeTokenID() uint16 { return m.feeTo
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 }
+func (m Message) Memo() *[]byte { return m.memo }
// copyAddressPtr copies an address.
func copyAddressPtr(a *common.Address) *common.Address {
diff --git a/core/types/transaction_marshalling.go b/core/types/transaction_marshalling.go
index bfb91be21..476ab8926 100644
--- a/core/types/transaction_marshalling.go
+++ b/core/types/transaction_marshalling.go
@@ -206,7 +206,7 @@ func (tx *Transaction) MarshalJSON() ([]byte, error) {
enc.FeeLimit = (*hexutil.Big)(itx.FeeLimit)
enc.Version = (*uint8)(&itx.Version)
enc.Reference = (*common.Reference)(itx.Reference)
- enc.Memo = (*hexutil.Bytes)(&itx.Memo)
+ enc.Memo = (*hexutil.Bytes)(itx.Memo)
enc.V = (*hexutil.Big)(itx.V)
enc.R = (*hexutil.Big)(itx.R)
enc.S = (*hexutil.Big)(itx.S)
@@ -607,7 +607,7 @@ func (tx *Transaction) UnmarshalJSON(input []byte) error {
itx.Value = (*big.Int)(dec.Value)
itx.Version = *dec.Version
itx.Reference = (*common.Reference)(dec.Reference)
- itx.Memo = *dec.Memo
+ itx.Memo = (*[]byte)(dec.Memo)
if dec.Input == nil {
return errors.New("missing required field 'input' in transaction")
}
diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go
index 7fc7d9232..321a6b08f 100644
--- a/ethclient/ethclient.go
+++ b/ethclient/ethclient.go
@@ -30,6 +30,7 @@ import (
"github.com/morph-l2/go-ethereum/core/types"
"github.com/morph-l2/go-ethereum/eth"
"github.com/morph-l2/go-ethereum/eth/tracers"
+ "github.com/morph-l2/go-ethereum/internal/ethapi"
"github.com/morph-l2/go-ethereum/rpc"
)
@@ -371,6 +372,17 @@ 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) ([]ethapi.ReferenceTransactionResult, error) {
+ var result []ethapi.ReferenceTransactionResult
+ return result, ec.c.CallContext(ctx, &result, "morph_getTransactionHashesByReference", reference, offset, limit)
+}
+
// 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/interfaces.go b/interfaces.go
index 879514d98..68800ff5e 100644
--- a/interfaces.go
+++ b/interfaces.go
@@ -147,7 +147,7 @@ type CallMsg struct {
FeeLimit *big.Int
Version byte
Reference *common.Reference
- Memo []byte
+ Memo *[]byte
// For SetCodeTxType
AuthorizationList []types.SetCodeAuthorization
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
index 85e603afe..dcdbe1ca1 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -1454,9 +1454,9 @@ type RPCTransaction struct {
QueueIndex *hexutil.Uint64 `json:"queueIndex,omitempty"`
// MorphTx fields:
- FeeTokenID hexutil.Uint64 `json:"feeTokenID,omitempty"`
+ FeeTokenID *hexutil.Uint16 `json:"feeTokenID,omitempty"`
FeeLimit *hexutil.Big `json:"feeLimit,omitempty"`
- Version uint8 `json:"version,omitempty"`
+ Version *hexutil.Uint64 `json:"version,omitempty"`
Reference *common.Reference `json:"reference,omitempty"`
Memo *hexutil.Bytes `json:"memo,omitempty"`
}
@@ -1523,12 +1523,13 @@ 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())
- result.Version = tx.Version()
- result.Reference = tx.Reference()
- memo := hexutil.Bytes(tx.Memo())
- result.Memo = &memo
+ 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())
@@ -1804,27 +1805,74 @@ func (s *PublicTransactionPoolAPI) GetTransactionCount(ctx context.Context, addr
return (*hexutil.Uint64)(&nonce), state.Error()
}
-// GetTransactionsByReference returns all transactions for the given reference.
+// 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"`
+}
+
+// GetTransactionHashesByReference returns transactions for the given reference with pagination.
// Results are sorted by blockTimestamp and txIndex (ascending order).
-func (s *PublicTransactionPoolAPI) GetTransactionsByReference(ctx context.Context, reference common.Reference) ([]*RPCTransaction, error) {
+// Parameters:
+// - reference: the reference key to query
+// - offset: pagination offset (default: 0)
+// - limit: pagination limit (default: 100, max: 100)
+func (s *PublicTransactionPoolAPI) GetTransactionHashesByReference(
+ ctx context.Context,
+ reference common.Reference,
+ offset *hexutil.Uint64,
+ limit *hexutil.Uint64,
+) (
+ []ReferenceTransactionResult,
+ error,
+) {
+ // Set default values
+ offsetVal := uint64(0)
+ if offset != nil {
+ offsetVal = uint64(*offset)
+ }
+ limitVal := uint64(100)
+ if limit != nil {
+ limitVal = uint64(*limit)
+ }
+
+ // Validate limit (max 100)
+ if limitVal > 100 {
+ return nil, errors.New("limit exceeds maximum value of 100")
+ }
+
entries := rawdb.ReadReferenceIndexEntries(s.b.ChainDb(), reference)
if len(entries) == 0 {
return nil, nil
}
- var result []*RPCTransaction
- for _, entry := range entries {
- tx, blockHash, blockNumber, index, err := s.b.GetTransaction(ctx, entry.TxHash)
- if err != nil {
+ // Validate offset
+ if offsetVal >= uint64(len(entries)) {
+ return nil, fmt.Errorf("offset %d exceeds total results %d", offsetVal, len(entries))
+ }
+
+ // Apply pagination
+ end := offsetVal + limitVal
+ if end > uint64(len(entries)) {
+ end = uint64(len(entries))
+ }
+ paginatedEntries := entries[offsetVal:end]
+
+ // Build result
+ result := make([]ReferenceTransactionResult, 0, len(paginatedEntries))
+ for _, entry := range paginatedEntries {
+ blockNumber := rawdb.ReadTxLookupEntry(s.b.ChainDb(), entry.TxHash)
+ if blockNumber == nil {
continue
}
- if tx != nil {
- header, err := s.b.HeaderByHash(ctx, blockHash)
- if err != nil {
- continue
- }
- result = append(result, NewRPCTransaction(tx, blockHash, blockNumber, header.Time, index, header.BaseFee, s.b.ChainConfig()))
- }
+ result = append(result, ReferenceTransactionResult{
+ TransactionHash: entry.TxHash,
+ BlockNumber: hexutil.Uint64(*blockNumber),
+ BlockTimestamp: hexutil.Uint64(entry.BlockTimestamp),
+ TransactionIndex: hexutil.Uint64(entry.TxIndex),
+ })
}
return result, nil
}
@@ -1923,16 +1971,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),
- // TODO
- "version": hexutil.Uint64(receipt.Version),
- "reference": (*common.Reference)(receipt.Reference),
- "memo": hexutil.Bytes(receipt.Memo),
- }
- // TODO
- if receipt.FeeTokenID != nil {
- fields["feeTokenID"] = hexutil.Uint16(*receipt.FeeTokenID)
+ "version": hexutil.Uint(receipt.Version),
+ "reference": (*common.Reference)(receipt.Reference),
+ "memo": (*hexutil.Bytes)(receipt.Memo),
}
+
// Assign the effective gas price paid
if !b.ChainConfig().IsCurie(bigblock) {
fields["effectiveGasPrice"] = hexutil.Uint64(tx.GasPrice().Uint64())
diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go
index 0cb200d3a..4a17d7da4 100644
--- a/internal/ethapi/transaction_args.go
+++ b/internal/ethapi/transaction_args.go
@@ -161,6 +161,10 @@ 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`)
}
+ // Validate memo length for MorphTx
+ if args.Memo != nil && len(*args.Memo) > common.MaxMemoLength {
+ return errors.New("memo exceeds maximum length of 64 bytes")
+ }
// Estimate the gas usage if necessary.
if args.Gas == nil {
// These fields are immutable during the estimation, safe to
@@ -329,9 +333,9 @@ func (args *TransactionArgs) ToMessage(globalGasCap uint64, baseFee *big.Int) (t
if args.Reference != nil {
reference = args.Reference
}
- memo := []byte{}
+ memo := new([]byte)
if args.Memo != nil {
- memo = *args.Memo
+ memo = (*[]byte)(args.Memo)
}
msg := types.NewMessage(addr, args.To, 0, value, gas, gasPrice, gasFeeCap, gasTipCap, feeTokenID, feeLimit, version, reference, memo, data, accessList, args.AuthorizationList, true)
@@ -422,7 +426,7 @@ func (args *TransactionArgs) toTransaction() *types.Transaction {
FeeTokenID: uint16(*args.FeeTokenID),
FeeLimit: (*big.Int)(args.FeeLimit),
Reference: args.Reference,
- Memo: *args.Memo,
+ Memo: (*[]byte)(args.Memo),
Value: (*big.Int)(args.Value),
Data: args.data(),
AccessList: al,
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/rollup/fees/rollup_fee.go b/rollup/fees/rollup_fee.go
index 5f9cfc157..79a5f4d9f 100644
--- a/rollup/fees/rollup_fee.go
+++ b/rollup/fees/rollup_fee.go
@@ -37,9 +37,9 @@ type Message interface {
IsL1MessageTx() bool
FeeTokenID() uint16
FeeLimit() *big.Int
- Version() byte
+ Version() uint8
Reference() *common.Reference
- Memo() []byte
+ Memo() *[]byte
}
// StateDB represents the StateDB interface
@@ -97,7 +97,10 @@ func asUnsignedTx(msg Message, baseFee, chainID *big.Int) *types.Transaction {
return asUnsignedAccessListTx(msg, chainID)
}
- if msg.FeeTokenID() != 0 || msg.Version() != 0 || msg.Reference() != nil || len(msg.Memo()) > 0 {
+ if msg.FeeTokenID() != 0 ||
+ msg.Version() != 0 ||
+ (msg.Reference() != nil && *msg.Reference() != (common.Reference{})) ||
+ (msg.Memo() != nil && len(*msg.Memo()) > 0) {
return asUnsignedMorphTx(msg, chainID)
}
diff --git a/rollup/tracing/tracing.go b/rollup/tracing/tracing.go
index 561965d56..52699cd62 100644
--- a/rollup/tracing/tracing.go
+++ b/rollup/tracing/tracing.go
@@ -551,9 +551,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,
+ Version: &receipt.Version,
Reference: receipt.Reference,
- Memo: receipt.Memo,
+ Memo: (*hexutil.Bytes)(receipt.Memo),
FeeRate: (*hexutil.Big)(receipt.FeeRate),
TokenScale: (*hexutil.Big)(receipt.TokenScale),
Gas: receipt.GasUsed,
diff --git a/signer/core/apitypes/types.go b/signer/core/apitypes/types.go
index f012b15e8..650480310 100644
--- a/signer/core/apitypes/types.go
+++ b/signer/core/apitypes/types.go
@@ -89,7 +89,7 @@ type SendTxArgs struct {
ChainID *hexutil.Big `json:"chainId,omitempty"`
FeeTokenID *hexutil.Uint16 `json:"feeTokenID,omitempty"`
FeeLimit *hexutil.Big `json:"feeLimit,omitempty"`
- Version byte `json:"version,omitempty"`
+ Version *hexutil.Uint64 `json:"version,omitempty"`
Reference *common.Reference `json:"reference,omitempty"`
Memo *hexutil.Bytes `json:"memo,omitempty"`
}
@@ -135,9 +135,9 @@ func (args *SendTxArgs) ToTransaction() *types.Transaction {
GasTipCap: (*big.Int)(args.MaxPriorityFeePerGas),
FeeTokenID: uint16(*args.FeeTokenID),
FeeLimit: (*big.Int)(args.FeeLimit),
- Version: args.Version,
+ Version: uint8(*args.Version),
Reference: args.Reference,
- Memo: *args.Memo,
+ Memo: (*[]byte)(args.Memo),
Value: (*big.Int)(&args.Value),
Data: input,
AccessList: al,
diff --git a/tests/state_test_util.go b/tests/state_test_util.go
index 9ac93c2b1..c480adb47 100644
--- a/tests/state_test_util.go
+++ b/tests/state_test_util.go
@@ -117,7 +117,7 @@ type stTransaction struct {
FeeLimit *big.Int `json:"feeLimit,omitempty"`
Version byte `json:"version,omitempty"`
Reference *common.Reference `json:"reference,omitempty"`
- Memo []byte `json:"memo,omitempty"`
+ Memo *[]byte `json:"memo,omitempty"`
}
type stTransactionMarshaling struct {
From 9ab3adf07611c7acc1adefe053f325210a1ce783 Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Wed, 28 Jan 2026 09:44:53 +0800
Subject: [PATCH 04/33] update
---
accounts/abi/bind/base.go | 7 +-
core/types/morph_tx.go | 219 +++++++++++++++++++++++++---
core/types/receipt.go | 3 +
core/types/transaction.go | 10 ++
internal/ethapi/transaction_args.go | 20 ++-
signer/core/apitypes/types.go | 18 ++-
6 files changed, 244 insertions(+), 33 deletions(-)
diff --git a/accounts/abi/bind/base.go b/accounts/abi/bind/base.go
index c18ee9929..8ed7df4f5 100644
--- a/accounts/abi/bind/base.go
+++ b/accounts/abi/bind/base.go
@@ -337,6 +337,11 @@ func (c *BoundContract) createMorphTx(opts *TransactOpts, contract *common.Addre
if err != nil {
return nil, err
}
+ // Default to Version 1 for new MorphTx transactions
+ version := opts.Version
+ if version == 0 {
+ version = types.MorphTxVersion1
+ }
baseTx := &types.MorphTx{
To: contract,
Nonce: nonce,
@@ -345,7 +350,7 @@ func (c *BoundContract) createMorphTx(opts *TransactOpts, contract *common.Addre
FeeTokenID: opts.FeeTokenID,
FeeLimit: opts.FeeLimit,
Gas: gasLimit,
- Version: opts.Version,
+ Version: version,
Reference: opts.Reference,
Memo: opts.Memo,
Value: value,
diff --git a/core/types/morph_tx.go b/core/types/morph_tx.go
index f63929af7..5b248f85e 100644
--- a/core/types/morph_tx.go
+++ b/core/types/morph_tx.go
@@ -24,6 +24,14 @@ import (
// 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 = byte(0)
+ // MorphTxVersion1 includes Version, Reference, Memo fields
+ MorphTxVersion1 = byte(1)
+)
+
type MorphTx struct {
ChainID *big.Int
Nonce uint64
@@ -35,11 +43,11 @@ type MorphTx struct {
Data []byte
AccessList AccessList
- Version uint8 // version of morph tx
+ 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)
- Memo *[]byte // memo for the transaction (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"`
@@ -47,6 +55,45 @@ type MorphTx struct {
S *big.Int `json:"s" gencodec:"required"`
}
+// morphTxV0RLP is the RLP encoding structure for MorphTx version 0 (legacy format)
+type morphTxV0RLP 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)
+type morphTxV1RLP 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
+ Version uint8
+ FeeTokenID uint16
+ FeeLimit *big.Int
+ Reference *common.Reference
+ Memo *[]byte
+ 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{
@@ -55,7 +102,7 @@ func (tx *MorphTx) copy() TxData {
To: copyAddressPtr(tx.To),
Data: common.CopyBytes(tx.Data),
Reference: copyReferencePtr(tx.Reference),
- Memo: tx.Memo,
+ Memo: copyBytesPtr(tx.Memo),
Version: tx.Version,
FeeTokenID: tx.FeeTokenID,
// These are copied below.
@@ -130,30 +177,154 @@ func (tx *MorphTx) setSignatureValues(chainID, v, r, s *big.Int) {
}
func (tx *MorphTx) encode(b *bytes.Buffer) error {
- return rlp.Encode(b, tx)
+ if tx.Version == MorphTxVersion0 {
+ // Encode as v0 format (legacy)
+ return rlp.Encode(b, &morphTxV0RLP{
+ 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,
+ })
+ }
+ // Encode as v1 format (with Version, Reference, Memo)
+ return rlp.Encode(b, &morphTxV1RLP{
+ 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,
+ Version: tx.Version,
+ FeeTokenID: tx.FeeTokenID,
+ FeeLimit: tx.FeeLimit,
+ Reference: tx.Reference,
+ Memo: tx.Memo,
+ V: tx.V,
+ R: tx.R,
+ S: tx.S,
+ })
}
func (tx *MorphTx) decode(input []byte) error {
- return rlp.DecodeBytes(input, tx)
+ // Try to decode as v1 format first
+ var v1 morphTxV1RLP
+ if err := rlp.DecodeBytes(input, &v1); err == nil && v1.Version > 0 {
+ 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 = v1.Version
+ tx.FeeTokenID = v1.FeeTokenID
+ tx.FeeLimit = v1.FeeLimit
+ tx.Reference = v1.Reference
+ tx.Memo = v1.Memo
+ tx.V = v1.V
+ tx.R = v1.R
+ tx.S = v1.S
+ return nil
+ }
+
+ // Fall back to v0 format (legacy)
+ var v0 morphTxV0RLP
+ if err := rlp.DecodeBytes(input, &v0); err != nil {
+ return err
+ }
+ 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.Version = MorphTxVersion0
+ tx.FeeTokenID = v0.FeeTokenID
+ tx.FeeLimit = v0.FeeLimit
+ tx.Reference = nil
+ tx.Memo = nil
+ tx.V = v0.V
+ tx.R = v0.R
+ tx.S = v0.S
+ return nil
}
func (tx *MorphTx) sigHash(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.Version,
- tx.FeeTokenID,
- tx.FeeLimit,
- tx.Reference,
- tx.Memo,
- })
+ switch tx.Version {
+ case MorphTxVersion0:
+ // v0 sigHash (legacy format without Version, Reference, Memo)
+ 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,
+ })
+ case MorphTxVersion1:
+ // v1 sigHash (with Version, Reference, Memo)
+ return prefixedRlpHash(
+ MorphTxType,
+ []any{
+ chainID,
+ tx.Nonce,
+ tx.GasTipCap,
+ tx.GasFeeCap,
+ tx.Gas,
+ tx.To,
+ tx.Value,
+ tx.Data,
+ tx.AccessList,
+ tx.Version,
+ tx.FeeTokenID,
+ tx.FeeLimit,
+ tx.Reference,
+ tx.Memo,
+ })
+ default:
+ // v1 sigHash (with Version, Reference, Memo)
+ return prefixedRlpHash(
+ MorphTxType,
+ []any{
+ chainID,
+ tx.Nonce,
+ tx.GasTipCap,
+ tx.GasFeeCap,
+ tx.Gas,
+ tx.To,
+ tx.Value,
+ tx.Data,
+ tx.AccessList,
+ tx.Version,
+ tx.FeeTokenID,
+ tx.FeeLimit,
+ tx.Reference,
+ tx.Memo,
+ })
+ }
}
diff --git a/core/types/receipt.go b/core/types/receipt.go
index 4eeb300bc..db291dcac 100644
--- a/core/types/receipt.go
+++ b/core/types/receipt.go
@@ -434,6 +434,9 @@ func decodeStoredReceiptRLP(r *ReceiptForStorage, blob []byte) error {
r.FeeRate = stored.FeeRate
r.TokenScale = stored.TokenScale
r.FeeLimit = stored.FeeLimit
+ r.Version = stored.Version
+ r.Reference = stored.Reference
+ r.Memo = stored.Memo
return nil
}
diff --git a/core/types/transaction.go b/core/types/transaction.go
index a13a20f6f..e94698bd8 100644
--- a/core/types/transaction.go
+++ b/core/types/transaction.go
@@ -951,6 +951,16 @@ func copyReferencePtr(h *common.Reference) *common.Reference {
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/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go
index 4a17d7da4..72ff0deab 100644
--- a/internal/ethapi/transaction_args.go
+++ b/internal/ethapi/transaction_args.go
@@ -325,10 +325,11 @@ func (args *TransactionArgs) ToMessage(globalGasCap uint64, baseFee *big.Int) (t
if args.FeeLimit != nil {
feeLimit = args.FeeLimit.ToInt()
}
- if args.Version == nil {
- return types.Message{}, errors.New("version is not set")
+ // Default to Version 1 for new MorphTx transactions
+ version := byte(types.MorphTxVersion1)
+ if args.Version != nil {
+ version = *args.Version
}
- version := *args.Version
reference := new(common.Reference)
if args.Reference != nil {
reference = args.Reference
@@ -415,6 +416,15 @@ func (args *TransactionArgs) toTransaction() *types.Transaction {
if args.AccessList != nil {
al = *args.AccessList
}
+ // Default to Version 1 for new MorphTx transactions
+ version := uint8(types.MorphTxVersion1)
+ if args.Version != nil {
+ version = *args.Version
+ }
+ var feeTokenID uint16
+ if args.FeeTokenID != nil {
+ feeTokenID = uint16(*args.FeeTokenID)
+ }
data = &types.MorphTx{
To: args.To,
ChainID: (*big.Int)(args.ChainID),
@@ -422,8 +432,8 @@ func (args *TransactionArgs) toTransaction() *types.Transaction {
Gas: uint64(*args.Gas),
GasFeeCap: (*big.Int)(args.MaxFeePerGas),
GasTipCap: (*big.Int)(args.MaxPriorityFeePerGas),
- Version: *args.Version,
- FeeTokenID: uint16(*args.FeeTokenID),
+ Version: version,
+ FeeTokenID: feeTokenID,
FeeLimit: (*big.Int)(args.FeeLimit),
Reference: args.Reference,
Memo: (*[]byte)(args.Memo),
diff --git a/signer/core/apitypes/types.go b/signer/core/apitypes/types.go
index 650480310..dd6cd3205 100644
--- a/signer/core/apitypes/types.go
+++ b/signer/core/apitypes/types.go
@@ -121,11 +121,23 @@ 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 && *args.Version > 0) ||
+ (args.Reference != nil && *args.Reference != (common.Reference{})) ||
+ (args.Memo != nil && len(*args.Memo) > 0):
al := types.AccessList{}
if args.AccessList != nil {
al = *args.AccessList
}
+ // Default to Version 1 for new MorphTx transactions
+ version := uint8(types.MorphTxVersion1)
+ if args.Version != nil {
+ version = uint8(*args.Version)
+ }
+ var feeTokenID uint16
+ if args.FeeTokenID != nil {
+ feeTokenID = uint16(*args.FeeTokenID)
+ }
data = &types.MorphTx{
To: to,
ChainID: (*big.Int)(args.ChainID),
@@ -133,9 +145,9 @@ func (args *SendTxArgs) ToTransaction() *types.Transaction {
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: uint8(*args.Version),
+ Version: version,
Reference: args.Reference,
Memo: (*[]byte)(args.Memo),
Value: (*big.Int)(&args.Value),
From f622530716c6821fe2f116d0bdfb7206dd34f688 Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Fri, 30 Jan 2026 03:50:59 +0800
Subject: [PATCH 05/33] add test
---
core/blockchain.go | 16 +-
core/blockchain_test.go | 474 +++++++++++++++++++++++++
core/rawdb/reference_index_iterator.go | 26 +-
3 files changed, 496 insertions(+), 20 deletions(-)
diff --git a/core/blockchain.go b/core/blockchain.go
index 764ccb267..a463e26bd 100644
--- a/core/blockchain.go
+++ b/core/blockchain.go
@@ -400,16 +400,14 @@ 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).
- if txLookupLimit != nil {
+ // Start reference indexer/unindexer (using same lookup limit as tx index).
bc.wg.Add(1)
go bc.maintainReferenceIndex(txIndexBlock)
}
@@ -767,6 +765,7 @@ func (bc *BlockChain) writeHeadBlock(block *types.Block) {
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
@@ -1009,13 +1008,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.
@@ -1094,11 +1095,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
diff --git a/core/blockchain_test.go b/core/blockchain_test.go
index 6ac7d57d9..ad04a98a8 100644
--- a/core/blockchain_test.go
+++ b/core/blockchain_test.go
@@ -3587,3 +3587,477 @@ 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 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()
+}
diff --git a/core/rawdb/reference_index_iterator.go b/core/rawdb/reference_index_iterator.go
index 42a970ae6..8ee62a795 100644
--- a/core/rawdb/reference_index_iterator.go
+++ b/core/rawdb/reference_index_iterator.go
@@ -125,19 +125,19 @@ func iterateReferences(db ethdb.Database, from uint64, to uint64, reverse bool,
}
}
- if len(refs) > 0 {
- 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
- }
- }
+ // 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
+ }
}
}
From f0520fa4052b7a518813862b165bc749bf33305c Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Fri, 30 Jan 2026 04:49:36 +0800
Subject: [PATCH 06/33] fix sth
---
core/types/morph_tx.go | 209 +++++++++++++++++++++--------------------
1 file changed, 108 insertions(+), 101 deletions(-)
diff --git a/core/types/morph_tx.go b/core/types/morph_tx.go
index 5b248f85e..3a31aa57a 100644
--- a/core/types/morph_tx.go
+++ b/core/types/morph_tx.go
@@ -2,7 +2,9 @@ package types
import (
"bytes"
+ "errors"
"math/big"
+ "strconv"
"github.com/morph-l2/go-ethereum/common"
"github.com/morph-l2/go-ethereum/rlp"
@@ -43,20 +45,21 @@ type MorphTx struct {
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)
+ FeeTokenID uint16 // ERC20 token ID for fee payment (0 = ETH)
+ FeeLimit *big.Int // maximum fee in token units (optional)
// Signature values
V *big.Int `json:"v" gencodec:"required"`
R *big.Int `json:"r" gencodec:"required"`
S *big.Int `json:"s" gencodec:"required"`
+
+ Version uint8 // version of morph tx (0 = legacy, 1 = with reference/memo)
+ Reference *common.Reference // reference key for the transaction (optional, v1 only)
+ Memo *[]byte // memo for the transaction (optional, v1 only)
}
// morphTxV0RLP is the RLP encoding structure for MorphTx version 0 (legacy format)
-type morphTxV0RLP struct {
+type v0MorphTxRLP struct {
ChainID *big.Int
Nonce uint64
GasTipCap *big.Int
@@ -74,7 +77,7 @@ type morphTxV0RLP struct {
}
// morphTxV1RLP is the RLP encoding structure for MorphTx version 1 (with Reference/Memo)
-type morphTxV1RLP struct {
+type v1MorphTxRLP struct {
ChainID *big.Int
Nonce uint64
GasTipCap *big.Int
@@ -84,14 +87,14 @@ type morphTxV1RLP struct {
Value *big.Int
Data []byte
AccessList AccessList
- Version uint8
FeeTokenID uint16
FeeLimit *big.Int
- Reference *common.Reference
- Memo *[]byte
V *big.Int
R *big.Int
S *big.Int
+ Version uint8
+ Reference *common.Reference
+ Memo *[]byte
}
// copy creates a deep copy of the transaction data and initializes all fields.
@@ -101,10 +104,10 @@ func (tx *MorphTx) copy() TxData {
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),
- Version: tx.Version,
- FeeTokenID: tx.FeeTokenID,
// These are copied below.
AccessList: make(AccessList, len(tx.AccessList)),
FeeLimit: new(big.Int),
@@ -179,7 +182,7 @@ func (tx *MorphTx) setSignatureValues(chainID, v, r, s *big.Int) {
func (tx *MorphTx) encode(b *bytes.Buffer) error {
if tx.Version == MorphTxVersion0 {
// Encode as v0 format (legacy)
- return rlp.Encode(b, &morphTxV0RLP{
+ return rlp.Encode(b, &v0MorphTxRLP{
ChainID: tx.ChainID,
Nonce: tx.Nonce,
GasTipCap: tx.GasTipCap,
@@ -197,7 +200,7 @@ func (tx *MorphTx) encode(b *bytes.Buffer) error {
})
}
// Encode as v1 format (with Version, Reference, Memo)
- return rlp.Encode(b, &morphTxV1RLP{
+ return rlp.Encode(b, &v1MorphTxRLP{
ChainID: tx.ChainID,
Nonce: tx.Nonce,
GasTipCap: tx.GasTipCap,
@@ -207,46 +210,68 @@ func (tx *MorphTx) encode(b *bytes.Buffer) error {
Value: tx.Value,
Data: tx.Data,
AccessList: tx.AccessList,
- Version: tx.Version,
FeeTokenID: tx.FeeTokenID,
FeeLimit: tx.FeeLimit,
- Reference: tx.Reference,
- Memo: tx.Memo,
V: tx.V,
R: tx.R,
S: tx.S,
+ Version: tx.Version,
+ Reference: tx.Reference,
+ Memo: tx.Memo,
})
}
func (tx *MorphTx) decode(input []byte) error {
- // Try to decode as v1 format first
- var v1 morphTxV1RLP
- if err := rlp.DecodeBytes(input, &v1); err == nil && v1.Version > 0 {
- 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 = v1.Version
- tx.FeeTokenID = v1.FeeTokenID
- tx.FeeLimit = v1.FeeLimit
- tx.Reference = v1.Reference
- tx.Memo = v1.Memo
- tx.V = v1.V
- tx.R = v1.R
- tx.S = v1.S
+ if err := decodeV1MorphTxRLP(tx, input); err == nil {
return nil
}
+ if err := decodeV0MorphTxRLP(tx, input); err == nil {
+ return nil
+ }
+ return errors.New("failed to decode morph tx")
+}
+
+func decodeV1MorphTxRLP(tx *MorphTx, blob []byte) error {
+ var v1 v1MorphTxRLP
+ if err := rlp.DecodeBytes(blob, &v1); err != nil {
+ return err
+ }
+
+ if v1.Version != MorphTxVersion1 {
+ return errors.New("invalid morph tx version, expected 1, got " + strconv.Itoa(int(v1.Version)))
+ }
- // Fall back to v0 format (legacy)
- var v0 morphTxV0RLP
- if err := rlp.DecodeBytes(input, &v0); err != nil {
+ tx.ChainID = v1.ChainID
+ tx.Nonce = v1.Nonce
+ tx.GasTipCap = v1.GasTipCap
+ tx.GasFeeCap = v1.GasFeeCap
+ tx.Gas = v1.Gas
+ tx.To = v1.To
+ tx.Value = v1.Value
+ tx.Data = v1.Data
+ tx.AccessList = v1.AccessList
+ tx.Version = v1.Version
+ tx.FeeTokenID = v1.FeeTokenID
+ tx.FeeLimit = v1.FeeLimit
+ tx.Reference = v1.Reference
+ 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
@@ -256,75 +281,57 @@ func (tx *MorphTx) decode(input []byte) error {
tx.Value = v0.Value
tx.Data = v0.Data
tx.AccessList = v0.AccessList
- tx.Version = MorphTxVersion0
tx.FeeTokenID = v0.FeeTokenID
tx.FeeLimit = v0.FeeLimit
- tx.Reference = nil
- tx.Memo = nil
tx.V = v0.V
tx.R = v0.R
tx.S = v0.S
+
return nil
}
func (tx *MorphTx) sigHash(chainID *big.Int) common.Hash {
- switch tx.Version {
- case MorphTxVersion0:
- // v0 sigHash (legacy format without Version, Reference, Memo)
- 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,
- })
- case MorphTxVersion1:
- // v1 sigHash (with Version, Reference, Memo)
- return prefixedRlpHash(
- MorphTxType,
- []any{
- chainID,
- tx.Nonce,
- tx.GasTipCap,
- tx.GasFeeCap,
- tx.Gas,
- tx.To,
- tx.Value,
- tx.Data,
- tx.AccessList,
- tx.Version,
- tx.FeeTokenID,
- tx.FeeLimit,
- tx.Reference,
- tx.Memo,
- })
- default:
- // v1 sigHash (with Version, Reference, Memo)
- return prefixedRlpHash(
- MorphTxType,
- []any{
- chainID,
- tx.Nonce,
- tx.GasTipCap,
- tx.GasFeeCap,
- tx.Gas,
- tx.To,
- tx.Value,
- tx.Data,
- tx.AccessList,
- tx.Version,
- tx.FeeTokenID,
- tx.FeeLimit,
- tx.Reference,
- tx.Memo,
- })
+ 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,
+ })
}
From 8b336fba5c0a95b3f4f2eac57373cbef5b8331e5 Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Fri, 30 Jan 2026 05:55:36 +0800
Subject: [PATCH 07/33] add check and test
---
accounts/abi/bind/base.go | 7 +-
core/block_validator.go | 7 +-
core/tx_pool.go | 4 +
core/types/receipt_test.go | 689 +++++++++++++++++++++++++
core/types/transaction.go | 53 +-
core/types/transaction_test.go | 761 +++++++++++++++++++++++++++-
internal/ethapi/transaction_args.go | 47 +-
signer/core/apitypes/types.go | 2 +-
8 files changed, 1541 insertions(+), 29 deletions(-)
diff --git a/accounts/abi/bind/base.go b/accounts/abi/bind/base.go
index 8ed7df4f5..97f0e50be 100644
--- a/accounts/abi/bind/base.go
+++ b/accounts/abi/bind/base.go
@@ -337,9 +337,12 @@ func (c *BoundContract) createMorphTx(opts *TransactOpts, contract *common.Addre
if err != nil {
return nil, err
}
- // Default to Version 1 for new MorphTx transactions
+ // Determine version:
+ // - If Version is explicitly set (> 0), use it
+ // - If Version is 0 and FeeTokenID > 0, keep Version 0 (backward compatible legacy format)
+ // - If Version is 0 and FeeTokenID == 0, default to Version 1 (new format)
version := opts.Version
- if version == 0 {
+ if version == 0 && opts.FeeTokenID == 0 {
version = types.MorphTxVersion1
}
baseTx := &types.MorphTx{
diff --git a/core/block_validator.go b/core/block_validator.go
index 60448294d..e8fa37218 100644
--- a/core/block_validator.go
+++ b/core/block_validator.go
@@ -59,11 +59,16 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error {
if !v.config.Morph.IsValidBlockSize(block.PayloadSize()) {
return ErrInvalidBlockPayloadSize
}
- // Validate MorphTx memo length for all transactions
+ // Validate MorphTx for all transactions
for _, tx := range block.Transactions() {
+ // Validate memo length
if err := tx.ValidateMemo(); err != nil {
return err
}
+ // Validate version 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()
diff --git a/core/tx_pool.go b/core/tx_pool.go
index bf3acc9a9..a4ca9d258 100644
--- a/core/tx_pool.go
+++ b/core/tx_pool.go
@@ -677,6 +677,10 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
if err := tx.ValidateMemo(); err != nil {
return err
}
+ // Validate MorphTx version and associated field requirements
+ if err := tx.ValidateMorphTxVersion(); err != nil {
+ return err
+ }
if !pool.eip7702 && tx.Type() == types.SetCodeTxType {
return ErrTxTypeNotSupported
}
diff --git a/core/types/receipt_test.go b/core/types/receipt_test.go
index fed78ad70..4e5438604 100644
--- a/core/types/receipt_test.go
+++ b/core/types/receipt_test.go
@@ -97,6 +97,107 @@ var (
},
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),
+ }
)
func TestDecodeEmptyTypedReceipt(t *testing.T) {
@@ -144,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,
@@ -162,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})
@@ -209,6 +322,13 @@ 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: want.Reference,
+ Memo: want.Memo,
}
for i, log := range want.Logs {
stored.Logs[i] = (*LogForStorage)(log)
@@ -617,3 +737,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 !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)
+ }
+ })
+ }
+}
+
+// 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 e94698bd8..49784f0ad 100644
--- a/core/types/transaction.go
+++ b/core/types/transaction.go
@@ -33,18 +33,20 @@ 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 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")
- 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")
+ ErrMorphTxV0RequiresFeeToken = errors.New("version 0 MorphTx requires FeeTokenID > 0")
+ ErrMorphTxUnsupportedVersion = errors.New("unsupported MorphTx version")
+ 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.
@@ -418,6 +420,33 @@ func (tx *Transaction) ValidateMemo() error {
return nil
}
+// ValidateMorphTxVersion validates the MorphTx version and its associated field requirements.
+// Rules:
+// - Version 0 (legacy format): FeeTokenID must be > 0
+// - Version 1 (with Reference/Memo): FeeTokenID, FeeLimit, Reference, Memo are all optional
+// - Other versions: not supported
+//
+// Returns nil if the transaction is not a MorphTx or if the version is valid.
+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 {
+ return ErrMorphTxV0RequiresFeeToken
+ }
+ case MorphTxVersion1:
+ // Version 1: FeeTokenID, FeeLimit, Reference, Memo are all optional
+ // No additional validation needed
+ default:
+ return ErrMorphTxUnsupportedVersion
+ }
+ return nil
+}
+
// Cost returns gas * gasPrice + value.
func (tx *Transaction) Cost() *big.Int {
// TODO: morph tx without fee token
diff --git a/core/types/transaction_test.go b/core/types/transaction_test.go
index 28da3d85b..adb8bd77f 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,630 @@ 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 err != tc.wantSenderErr {
+ t.Errorf("Sender error mismatch, got %v, want %v", 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 ValidateMemo and ValidateMorphTxVersion.
+func TestMorphTxValidation(t *testing.T) {
+ t.Run("ValidateMemo", func(t *testing.T) {
+ tests := []struct {
+ name string
+ memo *[]byte
+ wantErr error
+ }{
+ {"nil memo", nil, nil},
+ {"empty memo", func() *[]byte { m := []byte{}; return &m }(), nil},
+ {"valid memo", func() *[]byte { m := []byte("hello"); return &m }(), nil},
+ {"max length memo", func() *[]byte { m := make([]byte, common.MaxMemoLength); return &m }(), nil},
+ {"over max length memo", func() *[]byte { m := make([]byte, common.MaxMemoLength+1); return &m }(), ErrMemoTooLong},
+ }
+
+ 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,
+ Memo: tc.memo,
+ })
+ err := tx.ValidateMemo()
+ if err != tc.wantErr {
+ t.Errorf("ValidateMemo error mismatch, got %v, want %v", err, tc.wantErr)
+ }
+ })
+ }
+ })
+
+ t.Run("ValidateMorphTxVersion", func(t *testing.T) {
+ tests := []struct {
+ name string
+ version uint8
+ feeTokenID uint16
+ wantErr error
+ }{
+ {"V0 with FeeTokenID > 0", MorphTxVersion0, 1, nil},
+ {"V0 with FeeTokenID = 0", MorphTxVersion0, 0, ErrMorphTxV0RequiresFeeToken},
+ {"V1 with FeeTokenID = 0", MorphTxVersion1, 0, nil},
+ {"V1 with FeeTokenID > 0", MorphTxVersion1, 1, nil},
+ {"Unsupported version 2", 2, 0, ErrMorphTxUnsupportedVersion},
+ {"Unsupported version 255", 255, 1, 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,
+ })
+ 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) {
+ // ValidateMemo on non-MorphTx should return nil
+ legacyTx := NewTx(&LegacyTx{
+ Nonce: 1,
+ GasPrice: big.NewInt(1),
+ Gas: 21000,
+ To: &testAddr,
+ })
+ if err := legacyTx.ValidateMemo(); err != nil {
+ t.Errorf("ValidateMemo on LegacyTx should return nil, got %v", err)
+ }
+ if err := legacyTx.ValidateMorphTxVersion(); err != nil {
+ t.Errorf("ValidateMorphTxVersion on LegacyTx should return nil, 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
+func compareMemoPtr(a, b *[]byte) bool {
+ if a == nil && b == nil {
+ return true
+ }
+ if a == nil || b == nil {
+ return false
+ }
+ return bytes.Equal(*a, *b)
+}
diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go
index 72ff0deab..98a90b973 100644
--- a/internal/ethapi/transaction_args.go
+++ b/internal/ethapi/transaction_args.go
@@ -84,6 +84,47 @@ 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.
+// Rules:
+// - Version 0 (legacy format): FeeTokenID must be > 0
+// - Version 1 (with Reference/Memo): FeeTokenID, FeeLimit, Reference, Memo are all optional
+// - Other versions: not supported
+func (args *TransactionArgs) validateMorphTxVersion() error {
+ if !args.isMorphTxArgs() {
+ return nil
+ }
+
+ // Default to Version 1 if not specified
+ version := byte(types.MorphTxVersion1)
+ if args.Version != nil {
+ version = *args.Version
+ }
+
+ switch version {
+ case types.MorphTxVersion0:
+ // Version 0 requires FeeTokenID > 0 (legacy format used for alt-fee transactions)
+ if args.FeeTokenID == nil || *args.FeeTokenID == 0 {
+ return types.ErrMorphTxV0RequiresFeeToken
+ }
+ case types.MorphTxVersion1:
+ // Version 1: FeeTokenID, FeeLimit, Reference, Memo are all optional
+ // No additional validation needed
+ 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) {
@@ -165,6 +206,10 @@ func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend) error {
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
@@ -350,7 +395,7 @@ func (args *TransactionArgs) toTransaction() *types.Transaction {
switch {
// must take precedence over MaxFeePerGas.
case (args.FeeTokenID != nil && *args.FeeTokenID > 0) ||
- (args.Version != nil && *args.Version > 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
diff --git a/signer/core/apitypes/types.go b/signer/core/apitypes/types.go
index dd6cd3205..af632f75e 100644
--- a/signer/core/apitypes/types.go
+++ b/signer/core/apitypes/types.go
@@ -122,7 +122,7 @@ func (args *SendTxArgs) ToTransaction() *types.Transaction {
switch {
// must take precedence over MaxFeePerGas.
case (args.FeeTokenID != nil && *args.FeeTokenID > 0) ||
- (args.Version != nil && *args.Version > 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{}
From 029260fa86a2503d1a15ec89adb078e6b08c52b5 Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Fri, 30 Jan 2026 06:10:14 +0800
Subject: [PATCH 08/33] add test
---
core/tx_list.go | 14 +-
core/tx_pool.go | 30 +++-
core/tx_pool_test.go | 417 +++++++++++++++++++++++++++++++++++++++++++
3 files changed, 459 insertions(+), 2 deletions(-)
diff --git a/core/tx_list.go b/core/tx_list.go
index d36ccc594..2d521cabd 100644
--- a/core/tx_list.go
+++ b/core/tx_list.go
@@ -353,6 +353,10 @@ func (l *txList) Add(tx *types.Transaction, state *state.StateDB, priceBump uint
if l.costcap.Alt(tx.FeeTokenID()).Cmp(altCost) < 0 {
l.costcap.SetAltAmount(tx.FeeTokenID(), altCost)
}
+ } else if tx.IsMorphTx() {
+ // MorphTx V1 with FeeTokenID=0 uses GasFee() instead of Cost()
+ cost := new(big.Int).Add(tx.GasFee(), tx.Value())
+ ethCost = new(big.Int).Add(cost, l1DataFee)
} else {
ethCost = new(big.Int).Add(tx.Cost(), l1DataFee)
}
@@ -404,7 +408,15 @@ func (l *txList) Filter(costLimit *big.Int, gasLimit uint64, altCostLimit map[ui
}
return tx.Gas() > gasLimit || tx.Value().Cmp(costLimit) > 0
}
- return !allLower || tx.Gas() > gasLimit || tx.Cost().Cmp(costLimit) > 0
+ // Calculate cost based on transaction type
+ var txCost *big.Int
+ if tx.IsMorphTx() {
+ // MorphTx V1 with FeeTokenID=0 uses GasFee() instead of Cost()
+ txCost = new(big.Int).Add(tx.GasFee(), tx.Value())
+ } else {
+ txCost = tx.Cost()
+ }
+ return !allLower || tx.Gas() > gasLimit || txCost.Cmp(costLimit) > 0
})
if len(removed) == 0 {
diff --git a/core/tx_pool.go b/core/tx_pool.go
index a4ca9d258..04e24aa76 100644
--- a/core/tx_pool.go
+++ b/core/tx_pool.go
@@ -758,6 +758,12 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
if pool.currentState.GetBalance(from).Cmp(tx.Value()) < 0 {
return ErrInsufficientFunds
}
+ } else if tx.IsMorphTx() {
+ // MorphTx V1 with FeeTokenID=0 uses GasFee() instead of Cost()
+ cost := new(big.Int).Add(tx.GasFee(), tx.Value())
+ if pool.currentState.GetBalance(from).Cmp(cost) < 0 {
+ return ErrInsufficientFunds
+ }
} else {
if pool.currentState.GetBalance(from).Cmp(tx.Cost()) < 0 {
return ErrInsufficientFunds
@@ -790,6 +796,12 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
if limit.Cmp(erc20Amount) < 0 {
return errors.New("invalid transaction: insufficient funds for l1fee + gas * price or fee limit too small")
}
+ } else if tx.IsMorphTx() {
+ // MorphTx V1 with FeeTokenID=0 uses GasFee() instead of Cost()
+ cost := new(big.Int).Add(tx.GasFee(), tx.Value())
+ if b := pool.currentState.GetBalance(from); b.Cmp(new(big.Int).Add(cost, l1DataFee)) < 0 {
+ return errors.New("invalid transaction: insufficient funds for l1fee + gas * price + value")
+ }
} else {
// cost == L1 data fee + V + GP * GL
if b := pool.currentState.GetBalance(from); b.Cmp(new(big.Int).Add(tx.Cost(), l1DataFee)) < 0 {
@@ -1625,7 +1637,19 @@ 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.IsMorphTxWithAltFee() && (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 if tx.IsMorphTx() {
+ // MorphTx V1 with FeeTokenID=0 uses GasFee() instead of Cost()
+ txCost = new(big.Int).Add(tx.GasFee(), tx.Value())
+ } else {
+ txCost = tx.Cost()
+ }
+
+ if txCost != nil && (tx.Gas() > pool.currentMaxGas || txCost.Cmp(costLimit) > 0) {
return true
}
if pool.chainconfig.Morph.FeeVaultEnabled() {
@@ -1655,6 +1679,10 @@ func (pool *TxPool) executableTxFilter(addr common.Address, costLimit *big.Int,
limit = cmath.BigMin(altCostLimit[tx.FeeTokenID()], tx.FeeLimit())
}
return costLimit.Cmp(tx.Value()) < 0 || limit.Cmp(altAmount) < 0
+ } else if tx.IsMorphTx() {
+ // MorphTx V1 with FeeTokenID=0 uses GasFee() instead of Cost()
+ cost := new(big.Int).Add(tx.GasFee(), tx.Value())
+ return costLimit.Cmp(new(big.Int).Add(cost, l1DataFee)) < 0
} else {
return costLimit.Cmp(new(big.Int).Add(tx.Cost(), l1DataFee)) < 0
}
diff --git a/core/tx_pool_test.go b/core/tx_pool_test.go
index 26bde89dc..331a66ab3 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,20 @@ func init() {
eip1559NoL1feeConfig = &cpy2
eip1559NoL1feeConfig.BerlinBlock = common.Big0
eip1559NoL1feeConfig.LondonBlock = common.Big0
+
+ // MorphTx config with Emerald fork enabled
+ cpy3 := *params.TestChainConfig
+ morphTxConfig = &cpy3
+ morphTxConfig.BerlinBlock = common.Big0
+ morphTxConfig.LondonBlock = common.Big0
+ morphTxConfig.EmeraldTime = new(uint64) // Enable Emerald 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 +150,94 @@ 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
+}
+
+// morphTxV1WithAltFee creates a MorphTx V1 with alt fee (FeeTokenID > 0).
+func morphTxV1WithAltFee(nonce uint64, gaslimit uint64, gasFee *big.Int, tip *big.Int, feeTokenID uint16, 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: feeTokenID,
+ FeeLimit: big.NewInt(1000000),
+ Version: types.MorphTxVersion1,
+ Reference: &ref,
+ Memo: &memo,
+ })
+ return tx
+}
+
func setupTxPool() (*TxPool, *ecdsa.PrivateKey) {
return setupTxPoolWithConfig(params.TestChainConfig)
}
@@ -2602,3 +2710,312 @@ 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.ErrMorphTxV0RequiresFeeToken) {
+ t.Errorf("expected ErrMorphTxV0RequiresFeeToken, 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.ErrMorphTxV0RequiresFeeToken) {
+ 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)
+ }
+ })
+}
+
+// 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 (should fail)
+ tx2 := morphTxV1(0, 100000, 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")
+ }
+ })
+}
From 846ff39232de58bfc9188b6efd8ed4dd04b055fd Mon Sep 17 00:00:00 2001
From: FletcherMan
Date: Fri, 30 Jan 2026 17:40:45 +0800
Subject: [PATCH 09/33] refactor: encode MorphTx version as prefix byte instead
of RLP field (#284)
Co-authored-by: fletcher.fan
---
core/types/morph_tx.go | 94 ++++---
core/types/morph_tx_compat_test.go | 395 +++++++++++++++++++++++++++++
2 files changed, 448 insertions(+), 41 deletions(-)
create mode 100644 core/types/morph_tx_compat_test.go
diff --git a/core/types/morph_tx.go b/core/types/morph_tx.go
index 3a31aa57a..62c865729 100644
--- a/core/types/morph_tx.go
+++ b/core/types/morph_tx.go
@@ -45,17 +45,16 @@ type MorphTx struct {
Data []byte
AccessList AccessList
- FeeTokenID uint16 // ERC20 token ID for fee payment (0 = ETH)
- FeeLimit *big.Int // maximum fee in token units (optional)
+ 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"`
-
- Version uint8 // version of morph tx (0 = legacy, 1 = with reference/memo)
- Reference *common.Reference // reference key for the transaction (optional, v1 only)
- Memo *[]byte // memo for the transaction (optional, v1 only)
}
// morphTxV0RLP is the RLP encoding structure for MorphTx version 0 (legacy format)
@@ -77,6 +76,7 @@ type v0MorphTxRLP struct {
}
// 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
@@ -89,12 +89,11 @@ type v1MorphTxRLP struct {
AccessList AccessList
FeeTokenID uint16
FeeLimit *big.Int
+ Reference *common.Reference
+ Memo *[]byte
V *big.Int
R *big.Int
S *big.Int
- Version uint8
- Reference *common.Reference
- Memo *[]byte
}
// copy creates a deep copy of the transaction data and initializes all fields.
@@ -180,8 +179,10 @@ func (tx *MorphTx) setSignatureValues(chainID, v, r, s *big.Int) {
}
func (tx *MorphTx) encode(b *bytes.Buffer) error {
- if tx.Version == MorphTxVersion0 {
- // Encode as v0 format (legacy)
+ switch tx.Version {
+ case MorphTxVersion0:
+ // 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,
@@ -198,37 +199,52 @@ func (tx *MorphTx) encode(b *bytes.Buffer) error {
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)
+ 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: tx.Reference,
+ Memo: tx.Memo,
+ V: tx.V,
+ R: tx.R,
+ S: tx.S,
+ })
+ default:
+ return errors.New("unsupported morph tx version: " + strconv.Itoa(int(tx.Version)))
}
- // Encode as v1 format (with Version, Reference, 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,
- V: tx.V,
- R: tx.R,
- S: tx.S,
- Version: tx.Version,
- Reference: tx.Reference,
- Memo: tx.Memo,
- })
}
func (tx *MorphTx) decode(input []byte) error {
- if err := decodeV1MorphTxRLP(tx, input); err == nil {
- return nil
+ if len(input) == 0 {
+ return errors.New("empty morph tx input")
}
- if err := decodeV0MorphTxRLP(tx, input); err == nil {
- return nil
+
+ // 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)
}
- return errors.New("failed to decode morph tx")
+
+ // 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 {
@@ -237,10 +253,6 @@ func decodeV1MorphTxRLP(tx *MorphTx, blob []byte) error {
return err
}
- if v1.Version != MorphTxVersion1 {
- return errors.New("invalid morph tx version, expected 1, got " + strconv.Itoa(int(v1.Version)))
- }
-
tx.ChainID = v1.ChainID
tx.Nonce = v1.Nonce
tx.GasTipCap = v1.GasTipCap
@@ -250,7 +262,7 @@ func decodeV1MorphTxRLP(tx *MorphTx, blob []byte) error {
tx.Value = v1.Value
tx.Data = v1.Data
tx.AccessList = v1.AccessList
- tx.Version = v1.Version
+ tx.Version = MorphTxVersion1
tx.FeeTokenID = v1.FeeTokenID
tx.FeeLimit = v1.FeeLimit
tx.Reference = v1.Reference
diff --git a/core/types/morph_tx_compat_test.go b/core/types/morph_tx_compat_test.go
new file mode 100644
index 000000000..2f3918e4f
--- /dev/null
+++ b/core/types/morph_tx_compat_test.go
@@ -0,0 +1,395 @@
+package types
+
+import (
+ "bytes"
+ "encoding/hex"
+ "math/big"
+ "testing"
+
+ "github.com/morph-l2/go-ethereum/common"
+)
+
+// 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])
+}
From a3bcaeb8018f01047bb3c78fb11a03d662b4e216 Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Tue, 3 Feb 2026 16:05:19 +0800
Subject: [PATCH 10/33] fix decode morphtx & receipt
---
core/types/morph_tx.go | 28 +++++++++++++++++-----
core/types/receipt.go | 43 ++++++++++++++++++++++++++--------
core/types/receipt_test.go | 28 +++++++++++++++++-----
core/types/transaction_test.go | 8 +++++--
4 files changed, 83 insertions(+), 24 deletions(-)
diff --git a/core/types/morph_tx.go b/core/types/morph_tx.go
index 62c865729..e10cdd11f 100644
--- a/core/types/morph_tx.go
+++ b/core/types/morph_tx.go
@@ -89,8 +89,8 @@ type v1MorphTxRLP struct {
AccessList AccessList
FeeTokenID uint16
FeeLimit *big.Int
- Reference *common.Reference
- Memo *[]byte
+ 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
@@ -203,6 +203,15 @@ func (tx *MorphTx) encode(b *bytes.Buffer) error {
// 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,
@@ -215,8 +224,8 @@ func (tx *MorphTx) encode(b *bytes.Buffer) error {
AccessList: tx.AccessList,
FeeTokenID: tx.FeeTokenID,
FeeLimit: tx.FeeLimit,
- Reference: tx.Reference,
- Memo: tx.Memo,
+ Reference: reference,
+ Memo: memo,
V: tx.V,
R: tx.R,
S: tx.S,
@@ -265,8 +274,15 @@ func decodeV1MorphTxRLP(tx *MorphTx, blob []byte) error {
tx.Version = MorphTxVersion1
tx.FeeTokenID = v1.FeeTokenID
tx.FeeLimit = v1.FeeLimit
- tx.Reference = v1.Reference
- tx.Memo = v1.Memo
+ // Convert []byte to *common.Reference
+ if len(v1.Reference) == common.ReferenceLength {
+ ref := common.BytesToReference(v1.Reference)
+ tx.Reference = &ref
+ }
+ // Convert []byte to *[]byte
+ if len(v1.Memo) > 0 {
+ tx.Memo = &v1.Memo
+ }
tx.V = v1.V
tx.R = v1.R
tx.S = v1.S
diff --git a/core/types/receipt.go b/core/types/receipt.go
index db291dcac..6747ee83a 100644
--- a/core/types/receipt.go
+++ b/core/types/receipt.go
@@ -129,8 +129,8 @@ type storedReceiptRLP struct {
TokenScale *big.Int
FeeLimit *big.Int
Version uint8
- Reference *common.Reference
- Memo *[]byte
+ 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, convert to *[]byte when needed
}
// v8StoredReceiptRLP is the storage encoding of a receipt used in database version 8.
@@ -146,8 +146,8 @@ type v8StoredReceiptRLP struct {
TokenScale *big.Int
FeeLimit *big.Int
Version uint8
- Reference *common.Reference
- Memo *[]byte
+ 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.
@@ -364,6 +364,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,
@@ -374,8 +383,8 @@ func (r *ReceiptForStorage) EncodeRLP(w io.Writer) error {
TokenScale: r.TokenScale,
FeeLimit: r.FeeLimit,
Version: r.Version,
- Reference: r.Reference,
- Memo: r.Memo,
+ Reference: reference,
+ Memo: memo,
}
for i, log := range r.Logs {
enc.Logs[i] = (*LogForStorage)(log)
@@ -435,8 +444,15 @@ func decodeStoredReceiptRLP(r *ReceiptForStorage, blob []byte) error {
r.TokenScale = stored.TokenScale
r.FeeLimit = stored.FeeLimit
r.Version = stored.Version
- r.Reference = stored.Reference
- r.Memo = stored.Memo
+ // 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
}
@@ -461,8 +477,15 @@ func decodeV8StoredReceiptRLP(r *ReceiptForStorage, blob []byte) error {
r.TokenScale = stored.TokenScale
r.FeeLimit = stored.FeeLimit
r.Version = stored.Version
- r.Reference = stored.Reference
- r.Memo = stored.Memo
+ // 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
}
diff --git a/core/types/receipt_test.go b/core/types/receipt_test.go
index 4e5438604..1ddaeb1b5 100644
--- a/core/types/receipt_test.go
+++ b/core/types/receipt_test.go
@@ -327,8 +327,8 @@ func encodeAsStoredReceiptRLP(want *Receipt) ([]byte, error) {
TokenScale: want.TokenScale,
FeeLimit: want.FeeLimit,
Version: want.Version,
- Reference: want.Reference,
- Memo: want.Memo,
+ Reference: derefReference(want.Reference),
+ Memo: derefMemo(want.Memo),
}
for i, log := range want.Logs {
stored.Logs[i] = (*LogForStorage)(log)
@@ -336,6 +336,22 @@ func encodeAsStoredReceiptRLP(want *Receipt) ([]byte, error) {
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(),
@@ -347,8 +363,8 @@ func encodeAsV8StoredReceiptRLP(want *Receipt) ([]byte, error) {
TokenScale: want.TokenScale,
FeeLimit: want.FeeLimit,
Version: want.Version,
- Reference: want.Reference,
- Memo: want.Memo,
+ Reference: derefReference(want.Reference),
+ Memo: derefMemo(want.Memo),
}
for i, log := range want.Logs {
stored.Logs[i] = (*LogForStorage)(log)
@@ -905,10 +921,10 @@ func TestMorphTxReceiptV8StorageEncoding(t *testing.T) {
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) {
+ if !compareReference(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) {
+ if !compareMemo(dec.Memo, tc.receipt.Memo) {
t.Errorf("Memo mismatch, want %v, have %v", tc.receipt.Memo, dec.Memo)
}
})
diff --git a/core/types/transaction_test.go b/core/types/transaction_test.go
index adb8bd77f..0e3fe0123 100644
--- a/core/types/transaction_test.go
+++ b/core/types/transaction_test.go
@@ -1294,12 +1294,16 @@ func TestMorphTxMemoEdgeCases(t *testing.T) {
}
// 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 || b == nil {
- return false
+ if a == nil {
+ return len(*b) == 0
+ }
+ if b == nil {
+ return len(*a) == 0
}
return bytes.Equal(*a, *b)
}
From 909e2dc12bec72045ade541a89eeb760b8293f51 Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Tue, 3 Feb 2026 22:34:19 +0800
Subject: [PATCH 11/33] fix get transaction hashes by reference
---
accounts/abi/bind/backends/simulated.go | 2 +-
core/types/morph_tx.go | 4 +-
interfaces.go | 2 +-
internal/ethapi/api.go | 74 +++++++++++++++++++++++++
internal/ethapi/backend.go | 5 ++
internal/ethapi/transaction_args.go | 14 ++---
rollup/fees/rollup_fee.go | 2 +-
7 files changed, 91 insertions(+), 12 deletions(-)
diff --git a/accounts/abi/bind/backends/simulated.go b/accounts/abi/bind/backends/simulated.go
index c91201549..a00c81b65 100644
--- a/accounts/abi/bind/backends/simulated.go
+++ b/accounts/abi/bind/backends/simulated.go
@@ -920,7 +920,7 @@ func (m callMsg) SetCodeAuthorizations() []types.SetCodeAuthorization {
}
func (m callMsg) FeeTokenID() uint16 { return m.CallMsg.FeeTokenID }
func (m callMsg) FeeLimit() *big.Int { return m.CallMsg.FeeLimit }
-func (m callMsg) Version() byte { return m.CallMsg.Version }
+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 }
diff --git a/core/types/morph_tx.go b/core/types/morph_tx.go
index e10cdd11f..b2ab509e1 100644
--- a/core/types/morph_tx.go
+++ b/core/types/morph_tx.go
@@ -29,9 +29,9 @@ import (
// MorphTx version constants
const (
// MorphTxVersion0 is the original format without Version, Reference, Memo fields
- MorphTxVersion0 = byte(0)
+ MorphTxVersion0 = uint8(0)
// MorphTxVersion1 includes Version, Reference, Memo fields
- MorphTxVersion1 = byte(1)
+ MorphTxVersion1 = uint8(1)
)
type MorphTx struct {
diff --git a/interfaces.go b/interfaces.go
index 68800ff5e..78ead865a 100644
--- a/interfaces.go
+++ b/interfaces.go
@@ -145,7 +145,7 @@ type CallMsg struct {
// For MorphTxType
FeeTokenID uint16
FeeLimit *big.Int
- Version byte
+ Version uint8
Reference *common.Reference
Memo *[]byte
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
index dcdbe1ca1..70f5a49ef 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -2437,3 +2437,77 @@ 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:
+// - reference: the reference key to query
+// - offset: pagination offset (default: 0)
+// - limit: pagination limit (default: 100, max: 100)
+func (s *PublicMorphAPI) GetTransactionHashesByReference(
+ ctx context.Context,
+ reference common.Reference,
+ offset *hexutil.Uint64,
+ limit *hexutil.Uint64,
+) (
+ []ReferenceTransactionResult,
+ error,
+) {
+ // Set default values
+ offsetVal := uint64(0)
+ if offset != nil {
+ offsetVal = uint64(*offset)
+ }
+ limitVal := uint64(100)
+ if limit != nil {
+ limitVal = uint64(*limit)
+ }
+
+ // Validate limit (max 100)
+ if limitVal > 100 {
+ return nil, errors.New("limit exceeds maximum value of 100")
+ }
+
+ entries := rawdb.ReadReferenceIndexEntries(s.b.ChainDb(), reference)
+ if len(entries) == 0 {
+ return nil, nil
+ }
+
+ // Validate offset
+ if offsetVal >= uint64(len(entries)) {
+ return nil, fmt.Errorf("offset %d exceeds total results %d", offsetVal, len(entries))
+ }
+
+ // Apply pagination
+ end := offsetVal + limitVal
+ if end > uint64(len(entries)) {
+ end = uint64(len(entries))
+ }
+ paginatedEntries := entries[offsetVal:end]
+
+ // Build result
+ result := make([]ReferenceTransactionResult, 0, len(paginatedEntries))
+ for _, entry := range paginatedEntries {
+ blockNumber := rawdb.ReadTxLookupEntry(s.b.ChainDb(), entry.TxHash)
+ if blockNumber == nil {
+ continue
+ }
+ result = append(result, 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/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 98a90b973..37d74b9ef 100644
--- a/internal/ethapi/transaction_args.go
+++ b/internal/ethapi/transaction_args.go
@@ -53,7 +53,7 @@ type TransactionArgs struct {
// MorphTxType
FeeTokenID *hexutil.Uint16 `json:"feeTokenID,omitempty"`
FeeLimit *hexutil.Big `json:"feeLimit,omitempty"`
- Version *byte `json:"version,omitempty"`
+ Version *hexutil.Uint16 `json:"version,omitempty"`
Reference *common.Reference `json:"reference,omitempty"`
Memo *hexutil.Bytes `json:"memo,omitempty"`
@@ -105,9 +105,9 @@ func (args *TransactionArgs) validateMorphTxVersion() error {
}
// Default to Version 1 if not specified
- version := byte(types.MorphTxVersion1)
+ version := types.MorphTxVersion1
if args.Version != nil {
- version = *args.Version
+ version = uint8(*args.Version)
}
switch version {
@@ -371,9 +371,9 @@ func (args *TransactionArgs) ToMessage(globalGasCap uint64, baseFee *big.Int) (t
feeLimit = args.FeeLimit.ToInt()
}
// Default to Version 1 for new MorphTx transactions
- version := byte(types.MorphTxVersion1)
+ version := types.MorphTxVersion1
if args.Version != nil {
- version = *args.Version
+ version = uint8(*args.Version)
}
reference := new(common.Reference)
if args.Reference != nil {
@@ -462,9 +462,9 @@ func (args *TransactionArgs) toTransaction() *types.Transaction {
al = *args.AccessList
}
// Default to Version 1 for new MorphTx transactions
- version := uint8(types.MorphTxVersion1)
+ version := types.MorphTxVersion1
if args.Version != nil {
- version = *args.Version
+ version = uint8(*args.Version)
}
var feeTokenID uint16
if args.FeeTokenID != nil {
diff --git a/rollup/fees/rollup_fee.go b/rollup/fees/rollup_fee.go
index 79a5f4d9f..7d8dadd37 100644
--- a/rollup/fees/rollup_fee.go
+++ b/rollup/fees/rollup_fee.go
@@ -98,7 +98,7 @@ func asUnsignedTx(msg Message, baseFee, chainID *big.Int) *types.Transaction {
return asUnsignedAccessListTx(msg, chainID)
}
if msg.FeeTokenID() != 0 ||
- msg.Version() != 0 ||
+ msg.Version() != types.MorphTxVersion0 ||
(msg.Reference() != nil && *msg.Reference() != (common.Reference{})) ||
(msg.Memo() != nil && len(*msg.Memo()) > 0) {
return asUnsignedMorphTx(msg, chainID)
From 0097c2a6b77d1536fbe7d141f2e79a4a01f7bbed Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Tue, 3 Feb 2026 22:44:59 +0800
Subject: [PATCH 12/33] update args
---
ethclient/ethclient.go | 7 ++++++-
internal/ethapi/api.go | 27 ++++++++++++++++-----------
2 files changed, 22 insertions(+), 12 deletions(-)
diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go
index 321a6b08f..62dc06e8e 100644
--- a/ethclient/ethclient.go
+++ b/ethclient/ethclient.go
@@ -380,7 +380,12 @@ func (ec *Client) GetSkippedTransaction(ctx context.Context, txHash common.Hash)
// - limit: pagination limit (default: 100, max: 100)
func (ec *Client) GetTransactionHashesByReference(ctx context.Context, reference common.Reference, offset *hexutil.Uint64, limit *hexutil.Uint64) ([]ethapi.ReferenceTransactionResult, error) {
var result []ethapi.ReferenceTransactionResult
- return result, ec.c.CallContext(ctx, &result, "morph_getTransactionHashesByReference", reference, offset, limit)
+ args := ethapi.ReferenceQueryArgs{
+ Reference: reference,
+ Offset: offset,
+ Limit: limit,
+ }
+ return result, ec.c.CallContext(ctx, &result, "morph_getTransactionHashesByReference", args)
}
// GetBlockByNumberOrHash returns the requested block
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
index 70f5a49ef..33c0984a2 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -2448,29 +2448,34 @@ func NewPublicMorphAPI(b Backend) *PublicMorphAPI {
return &PublicMorphAPI{b}
}
+// 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"`
+}
+
// 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)
+// - 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,
- reference common.Reference,
- offset *hexutil.Uint64,
- limit *hexutil.Uint64,
+ args ReferenceQueryArgs,
) (
[]ReferenceTransactionResult,
error,
) {
// Set default values
offsetVal := uint64(0)
- if offset != nil {
- offsetVal = uint64(*offset)
+ if args.Offset != nil {
+ offsetVal = uint64(*args.Offset)
}
limitVal := uint64(100)
- if limit != nil {
- limitVal = uint64(*limit)
+ if args.Limit != nil {
+ limitVal = uint64(*args.Limit)
}
// Validate limit (max 100)
@@ -2478,7 +2483,7 @@ func (s *PublicMorphAPI) GetTransactionHashesByReference(
return nil, errors.New("limit exceeds maximum value of 100")
}
- entries := rawdb.ReadReferenceIndexEntries(s.b.ChainDb(), reference)
+ entries := rawdb.ReadReferenceIndexEntries(s.b.ChainDb(), args.Reference)
if len(entries) == 0 {
return nil, nil
}
From e912d45d18adc9b5c143abeddae687fbc785e29d Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Tue, 3 Feb 2026 23:13:48 +0800
Subject: [PATCH 13/33] fix args checks
---
common/types.go | 16 ++++++++++++++++
internal/ethapi/transaction_args.go | 9 ++++++++-
2 files changed, 24 insertions(+), 1 deletion(-)
diff --git a/common/types.go b/common/types.go
index c118f61d7..fe4d0d269 100644
--- a/common/types.go
+++ b/common/types.go
@@ -131,12 +131,28 @@ func (r *Reference) SetBytes(b []byte) {
}
// 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[:])
}
diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go
index 37d74b9ef..00bcf71e0 100644
--- a/internal/ethapi/transaction_args.go
+++ b/internal/ethapi/transaction_args.go
@@ -96,7 +96,7 @@ func (args *TransactionArgs) isMorphTxArgs() bool {
// validateMorphTxVersion validates the MorphTx version and its associated field requirements.
// Rules:
-// - Version 0 (legacy format): FeeTokenID must be > 0
+// - Version 0 (legacy format): FeeTokenID must be > 0, Reference and Memo must not be set
// - Version 1 (with Reference/Memo): FeeTokenID, FeeLimit, Reference, Memo are all optional
// - Other versions: not supported
func (args *TransactionArgs) validateMorphTxVersion() error {
@@ -116,6 +116,13 @@ func (args *TransactionArgs) validateMorphTxVersion() error {
if args.FeeTokenID == nil || *args.FeeTokenID == 0 {
return types.ErrMorphTxV0RequiresFeeToken
}
+ // Version 0 does not support Reference and Memo fields
+ if args.Reference != nil && *args.Reference != (common.Reference{}) {
+ return errors.New("version 0 does not support reference field")
+ }
+ if args.Memo != nil && len(*args.Memo) > 0 {
+ return errors.New("version 0 does not support memo field")
+ }
case types.MorphTxVersion1:
// Version 1: FeeTokenID, FeeLimit, Reference, Memo are all optional
// No additional validation needed
From cdcfef65a4c7b37190abf480f29a9c840383c270 Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Wed, 4 Feb 2026 11:29:02 +0800
Subject: [PATCH 14/33] fix version check
---
accounts/abi/bind/base.go | 89 ++++++++++++++++++---
core/tx_pool_test.go | 87 ++++++++++++++++++++
core/types/transaction.go | 30 +++++--
core/types/transaction_test.go | 38 +++++++--
internal/ethapi/transaction_args.go | 120 ++++++++++++++++++++--------
signer/core/apitypes/types.go | 36 +++++++--
6 files changed, 337 insertions(+), 63 deletions(-)
diff --git a/accounts/abi/bind/base.go b/accounts/abi/bind/base.go
index 97f0e50be..1e320587c 100644
--- a/accounts/abi/bind/base.go
+++ b/accounts/abi/bind/base.go
@@ -59,7 +59,7 @@ type TransactOpts struct {
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
+ 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)
@@ -299,6 +299,13 @@ func (c *BoundContract) createMorphTx(opts *TransactOpts, contract *common.Addre
if value == nil {
value = new(big.Int)
}
+
+ // Determine version and validate fields
+ version, err := c.determineMorphTxVersion(opts)
+ if err != nil {
+ return nil, err
+ }
+
// Estimate TipCap
gasTipCap := opts.GasTipCap
if gasTipCap == nil {
@@ -337,14 +344,6 @@ func (c *BoundContract) createMorphTx(opts *TransactOpts, contract *common.Addre
if err != nil {
return nil, err
}
- // Determine version:
- // - If Version is explicitly set (> 0), use it
- // - If Version is 0 and FeeTokenID > 0, keep Version 0 (backward compatible legacy format)
- // - If Version is 0 and FeeTokenID == 0, default to Version 1 (new format)
- version := opts.Version
- if version == 0 && opts.FeeTokenID == 0 {
- version = types.MorphTxVersion1
- }
baseTx := &types.MorphTx{
To: contract,
Nonce: nonce,
@@ -362,6 +361,76 @@ func (c *BoundContract) createMorphTx(opts *TransactOpts, contract *common.Addre
return types.NewTx(baseTx), nil
}
+// determineMorphTxVersion determines the MorphTx version and validates field requirements.
+// Rules when version is explicitly specified:
+// - 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
+//
+// Rules when version is not explicitly specified (auto-detection):
+// - (FeeTokenID > 0) && (no Reference) && (no Memo) -> Version 0
+// - (valid Reference) || (valid Memo with 0 < len < 64) -> Version 1
+// - (FeeTokenID == 0) && (empty Reference) && (empty Memo) -> Error
+func (c *BoundContract) determineMorphTxVersion(opts *TransactOpts) (uint8, error) {
+ hasReference := opts.Reference != nil && *opts.Reference != (common.Reference{})
+ hasMemo := opts.Memo != nil && len(*opts.Memo) > 0
+ hasFeeToken := opts.FeeTokenID > 0
+ hasFeeLimit := opts.FeeLimit != nil && opts.FeeLimit.Sign() > 0
+
+ // Validate memo length
+ if opts.Memo != nil && len(*opts.Memo) > common.MaxMemoLength {
+ return 0, types.ErrMemoTooLong
+ }
+
+ // Check if version is explicitly specified
+ if opts.Version != nil {
+ version := *opts.Version
+ switch version {
+ case types.MorphTxVersion0:
+ // Version 0 requires FeeTokenID > 0
+ if !hasFeeToken {
+ return 0, types.ErrMorphTxV0RequiresFeeToken
+ }
+ // Version 0 does not support Reference field
+ if hasReference {
+ return 0, types.ErrMorphTxV0HasReference
+ }
+ // Version 0 does not support Memo field
+ if hasMemo {
+ return 0, types.ErrMorphTxV0HasMemo
+ }
+ return types.MorphTxVersion0, nil
+ case types.MorphTxVersion1:
+ // Version 1: FeeTokenID, Reference, Memo are all optional
+ // If FeeTokenID is 0, FeeLimit must not be set
+ if !hasFeeToken && hasFeeLimit {
+ return 0, types.ErrMorphTxV1FeeLimitWithoutFeeToken
+ }
+ return types.MorphTxVersion1, nil
+ default:
+ return 0, types.ErrMorphTxUnsupportedVersion
+ }
+ }
+
+ // Version not explicitly specified - auto-detect version
+ // (FeeTokenID > 0) && (no Reference) && (no Memo) -> Version 0
+ if hasFeeToken && !hasReference && !hasMemo {
+ return types.MorphTxVersion0, nil
+ }
+
+ // (valid Reference) || (valid Memo) -> Version 1
+ if hasReference || hasMemo {
+ // For Version 1, if FeeTokenID is 0, FeeLimit must not be set
+ if !hasFeeToken && hasFeeLimit {
+ return 0, types.ErrMorphTxV1FeeLimitWithoutFeeToken
+ }
+ return types.MorphTxVersion1, nil
+ }
+
+ // (FeeTokenID == 0) && (empty Reference) && (empty Memo) -> Error
+ return 0, types.ErrMorphTxCannotDetermineVersion
+}
+
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")
@@ -453,7 +522,7 @@ func (c *BoundContract) transact(opts *TransactOpts, contract *common.Address, i
return nil, errHead
} else if head.BaseFee != nil {
if opts.FeeTokenID != 0 ||
- opts.Version != 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)
diff --git a/core/tx_pool_test.go b/core/tx_pool_test.go
index 331a66ab3..c33ef3f0c 100644
--- a/core/tx_pool_test.go
+++ b/core/tx_pool_test.go
@@ -2815,6 +2815,93 @@ func TestMorphTxValidation(t *testing.T) {
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.ErrMorphTxV0HasReference) {
+ t.Errorf("expected ErrMorphTxV0HasReference, 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.ErrMorphTxV0HasMemo) {
+ t.Errorf("expected ErrMorphTxV0HasMemo, 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.ErrMorphTxV1FeeLimitWithoutFeeToken) {
+ t.Errorf("expected ErrMorphTxV1FeeLimitWithoutFeeToken, got %v", err)
+ }
+ })
}
// TestMorphTxEIP1559Activation tests that MorphTx is rejected before Emerald fork.
diff --git a/core/types/transaction.go b/core/types/transaction.go
index 49784f0ad..ccee3b7a7 100644
--- a/core/types/transaction.go
+++ b/core/types/transaction.go
@@ -39,9 +39,13 @@ var (
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")
- ErrMorphTxV0RequiresFeeToken = errors.New("version 0 MorphTx requires FeeTokenID > 0")
- ErrMorphTxUnsupportedVersion = errors.New("unsupported MorphTx version")
+ ErrMemoTooLong = errors.New("memo exceeds maximum length of 64 bytes")
+ ErrMorphTxV0RequiresFeeToken = errors.New("version 0 MorphTx requires FeeTokenID > 0")
+ ErrMorphTxV0HasReference = errors.New("version 0 MorphTx does not support Reference field")
+ ErrMorphTxV0HasMemo = errors.New("version 0 MorphTx does not support Memo field")
+ ErrMorphTxV1FeeLimitWithoutFeeToken = errors.New("version 1 MorphTx cannot have FeeLimit when FeeTokenID is 0")
+ ErrMorphTxCannotDetermineVersion = errors.New("cannot determine MorphTx version: FeeTokenID=0 without Reference or Memo")
+ ErrMorphTxUnsupportedVersion = errors.New("unsupported MorphTx version")
errEmptyTypedTx = errors.New("empty typed transaction bytes")
errShortTypedTx = errors.New("typed transaction too short")
errInvalidYParity = errors.New("'yParity' field must be 0 or 1")
@@ -422,8 +426,9 @@ func (tx *Transaction) ValidateMemo() error {
// ValidateMorphTxVersion validates the MorphTx version and its associated field requirements.
// Rules:
-// - Version 0 (legacy format): FeeTokenID must be > 0
-// - Version 1 (with Reference/Memo): FeeTokenID, FeeLimit, Reference, Memo are all optional
+// - 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 the version is valid.
@@ -438,9 +443,20 @@ func (tx *Transaction) ValidateMorphTxVersion() error {
if morphTx.FeeTokenID == 0 {
return ErrMorphTxV0RequiresFeeToken
}
+ // Version 0 does not support Reference field
+ if morphTx.Reference != nil && *morphTx.Reference != (common.Reference{}) {
+ return ErrMorphTxV0HasReference
+ }
+ // Version 0 does not support Memo field
+ if morphTx.Memo != nil && len(*morphTx.Memo) > 0 {
+ return ErrMorphTxV0HasMemo
+ }
case MorphTxVersion1:
- // Version 1: FeeTokenID, FeeLimit, Reference, Memo are all optional
- // No additional validation needed
+ // 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 ErrMorphTxV1FeeLimitWithoutFeeToken
+ }
default:
return ErrMorphTxUnsupportedVersion
}
diff --git a/core/types/transaction_test.go b/core/types/transaction_test.go
index 0e3fe0123..7326f6560 100644
--- a/core/types/transaction_test.go
+++ b/core/types/transaction_test.go
@@ -946,18 +946,41 @@ 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
}{
- {"V0 with FeeTokenID > 0", MorphTxVersion0, 1, nil},
- {"V0 with FeeTokenID = 0", MorphTxVersion0, 0, ErrMorphTxV0RequiresFeeToken},
- {"V1 with FeeTokenID = 0", MorphTxVersion1, 0, nil},
- {"V1 with FeeTokenID > 0", MorphTxVersion1, 1, nil},
- {"Unsupported version 2", 2, 0, ErrMorphTxUnsupportedVersion},
- {"Unsupported version 255", 255, 1, ErrMorphTxUnsupportedVersion},
+ // Version 0 tests
+ {"V0 with FeeTokenID > 0", MorphTxVersion0, 1, nil, nil, nil, nil},
+ {"V0 with FeeTokenID = 0", MorphTxVersion0, 0, nil, nil, nil, ErrMorphTxV0RequiresFeeToken},
+ {"V0 with Reference", MorphTxVersion0, 1, nil, &ref, nil, ErrMorphTxV0HasReference},
+ {"V0 with empty Reference", MorphTxVersion0, 1, nil, &emptyRef, nil, nil},
+ {"V0 with Memo", MorphTxVersion0, 1, nil, nil, &memo, ErrMorphTxV0HasMemo},
+ {"V0 with empty Memo", MorphTxVersion0, 1, nil, nil, &emptyMemo, nil},
+ {"V0 with Reference and Memo", MorphTxVersion0, 1, nil, &ref, &memo, ErrMorphTxV0HasReference},
+ // 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, ErrMorphTxV1FeeLimitWithoutFeeToken},
+ {"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 {
@@ -971,6 +994,9 @@ func TestMorphTxValidation(t *testing.T) {
To: &testAddr,
Version: tc.version,
FeeTokenID: tc.feeTokenID,
+ FeeLimit: tc.feeLimit,
+ Reference: tc.reference,
+ Memo: tc.memo,
})
err := tx.ValidateMorphTxVersion()
if err != tc.wantErr {
diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go
index 00bcf71e0..e7e350385 100644
--- a/internal/ethapi/transaction_args.go
+++ b/internal/ethapi/transaction_args.go
@@ -95,41 +95,97 @@ func (args *TransactionArgs) isMorphTxArgs() bool {
}
// validateMorphTxVersion validates the MorphTx version and its associated field requirements.
-// Rules:
-// - Version 0 (legacy format): FeeTokenID must be > 0, Reference and Memo must not be set
-// - Version 1 (with Reference/Memo): FeeTokenID, FeeLimit, Reference, Memo are all optional
-// - Other versions: not supported
+// Rules when version is explicitly specified:
+// - 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
+//
+// Rules when version is not explicitly specified (auto-detection):
+// - (FeeTokenID > 0) && (no Reference) && (no Memo) -> Version 0
+// - (valid Reference) || (valid Memo with 0 < len < 64) -> Version 1
+// - (FeeTokenID == 0) && (empty Reference) && (empty Memo) -> Error
func (args *TransactionArgs) validateMorphTxVersion() error {
if !args.isMorphTxArgs() {
return nil
}
- // Default to Version 1 if not specified
- version := types.MorphTxVersion1
+ // Check if version is explicitly specified
if args.Version != nil {
- version = uint8(*args.Version)
+ // Version explicitly specified - validate according to version
+ version := uint8(*args.Version)
+ switch version {
+ case types.MorphTxVersion0:
+ // Version 0 requires FeeTokenID > 0
+ if args.FeeTokenID == nil || *args.FeeTokenID == 0 {
+ return types.ErrMorphTxV0RequiresFeeToken
+ }
+ // Version 0 does not support Reference field
+ if args.Reference != nil && *args.Reference != (common.Reference{}) {
+ return types.ErrMorphTxV0HasReference
+ }
+ // Version 0 does not support Memo field
+ if args.Memo != nil && len(*args.Memo) > 0 {
+ return types.ErrMorphTxV0HasMemo
+ }
+ 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.ErrMorphTxV1FeeLimitWithoutFeeToken
+ }
+ default:
+ return types.ErrMorphTxUnsupportedVersion
+ }
+ return nil
}
- switch version {
- case types.MorphTxVersion0:
- // Version 0 requires FeeTokenID > 0 (legacy format used for alt-fee transactions)
- if args.FeeTokenID == nil || *args.FeeTokenID == 0 {
- return types.ErrMorphTxV0RequiresFeeToken
- }
- // Version 0 does not support Reference and Memo fields
- if args.Reference != nil && *args.Reference != (common.Reference{}) {
- return errors.New("version 0 does not support reference field")
- }
- if args.Memo != nil && len(*args.Memo) > 0 {
- return errors.New("version 0 does not support memo field")
+ // Version not explicitly specified - auto-detect version
+ hasFeeToken := args.FeeTokenID != nil && *args.FeeTokenID > 0
+ hasReference := args.Reference != nil && *args.Reference != (common.Reference{})
+ hasMemo := args.Memo != nil && len(*args.Memo) > 0
+
+ // Auto-detect version based on fields
+ if hasFeeToken && !hasReference && !hasMemo {
+ // (FeeTokenID > 0) && (no Reference) && (no Memo) -> Version 0
+ return nil
+ }
+ if hasReference || hasMemo {
+ // (valid Reference) || (valid Memo) -> Version 1
+ // For Version 1, if FeeTokenID is 0, FeeLimit must not be set
+ if !hasFeeToken && args.FeeLimit != nil && args.FeeLimit.ToInt().Sign() > 0 {
+ return types.ErrMorphTxV1FeeLimitWithoutFeeToken
}
- case types.MorphTxVersion1:
- // Version 1: FeeTokenID, FeeLimit, Reference, Memo are all optional
- // No additional validation needed
- default:
- return types.ErrMorphTxUnsupportedVersion
+ return nil
}
- return nil
+ // (FeeTokenID == 0) && (empty Reference) && (empty Memo) -> Error
+ return types.ErrMorphTxCannotDetermineVersion
+}
+
+// determineMorphTxVersion determines the MorphTx version based on explicit setting or auto-detection.
+// This function should be called after validateMorphTxVersion() has passed.
+func (args *TransactionArgs) determineMorphTxVersion() uint8 {
+ // If version is explicitly specified, use it
+ if args.Version != nil {
+ return uint8(*args.Version)
+ }
+
+ // Auto-detect version based on fields
+ hasFeeToken := args.FeeTokenID != nil && *args.FeeTokenID > 0
+ hasReference := args.Reference != nil && *args.Reference != (common.Reference{})
+ hasMemo := args.Memo != nil && len(*args.Memo) > 0
+
+ // (FeeTokenID > 0) && (no Reference) && (no Memo) -> Version 0
+ if hasFeeToken && !hasReference && !hasMemo {
+ return types.MorphTxVersion0
+ }
+
+ // (valid Reference) || (valid Memo) -> Version 1
+ // Also fallback to Version 1 for other cases
+ return types.MorphTxVersion1
}
// setDefaults fills in default values for unspecified tx fields.
@@ -377,11 +433,8 @@ func (args *TransactionArgs) ToMessage(globalGasCap uint64, baseFee *big.Int) (t
if args.FeeLimit != nil {
feeLimit = args.FeeLimit.ToInt()
}
- // Default to Version 1 for new MorphTx transactions
- version := types.MorphTxVersion1
- if args.Version != nil {
- version = uint8(*args.Version)
- }
+ // Determine version based on explicit setting or auto-detection
+ version := args.determineMorphTxVersion()
reference := new(common.Reference)
if args.Reference != nil {
reference = args.Reference
@@ -468,11 +521,8 @@ func (args *TransactionArgs) toTransaction() *types.Transaction {
if args.AccessList != nil {
al = *args.AccessList
}
- // Default to Version 1 for new MorphTx transactions
- version := types.MorphTxVersion1
- if args.Version != nil {
- version = uint8(*args.Version)
- }
+ // Determine version based on explicit setting or auto-detection
+ version := args.determineMorphTxVersion()
var feeTokenID uint16
if args.FeeTokenID != nil {
feeTokenID = uint16(*args.FeeTokenID)
diff --git a/signer/core/apitypes/types.go b/signer/core/apitypes/types.go
index af632f75e..38de24bec 100644
--- a/signer/core/apitypes/types.go
+++ b/signer/core/apitypes/types.go
@@ -102,6 +102,35 @@ func (args SendTxArgs) String() string {
return err.Error()
}
+// determineMorphTxVersion determines the MorphTx version based on explicit setting or auto-detection.
+// Rules when version is explicitly specified:
+// - Use the specified version directly
+//
+// Rules when version is not explicitly specified (auto-detection):
+// - (FeeTokenID > 0) && (no Reference) && (no Memo) -> Version 0
+// - (valid Reference) || (valid Memo) -> Version 1
+// - Otherwise -> Version 1
+func (args *SendTxArgs) determineMorphTxVersion() uint8 {
+ // If version is explicitly specified, use it
+ if args.Version != nil {
+ return uint8(*args.Version)
+ }
+
+ // Auto-detect version based on fields
+ hasFeeToken := args.FeeTokenID != nil && *args.FeeTokenID > 0
+ hasReference := args.Reference != nil && *args.Reference != (common.Reference{})
+ hasMemo := args.Memo != nil && len(*args.Memo) > 0
+
+ // (FeeTokenID > 0) && (no Reference) && (no Memo) -> Version 0
+ if hasFeeToken && !hasReference && !hasMemo {
+ return types.MorphTxVersion0
+ }
+
+ // (valid Reference) || (valid Memo) -> Version 1
+ // Also fallback to Version 1 for other cases
+ return types.MorphTxVersion1
+}
+
// ToTransaction converts the arguments to a transaction.
func (args *SendTxArgs) ToTransaction() *types.Transaction {
// Add the To-field, if specified
@@ -129,11 +158,8 @@ func (args *SendTxArgs) ToTransaction() *types.Transaction {
if args.AccessList != nil {
al = *args.AccessList
}
- // Default to Version 1 for new MorphTx transactions
- version := uint8(types.MorphTxVersion1)
- if args.Version != nil {
- version = uint8(*args.Version)
- }
+ // Determine version based on explicit setting or auto-detection
+ version := args.determineMorphTxVersion()
var feeTokenID uint16
if args.FeeTokenID != nil {
feeTokenID = uint16(*args.FeeTokenID)
From 2467803c6ee6c69c9d80ea8321c66200f81a185f Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Wed, 4 Feb 2026 13:34:43 +0800
Subject: [PATCH 15/33] some improve
---
accounts/external/backend.go | 6 ++--
common/types.go | 5 +++-
core/rawdb/reference_index_iterator.go | 39 +++++++++++++++++---------
core/types/l2trace.go | 6 ++--
core/types/morph_tx.go | 4 +++
core/types/transaction.go | 2 +-
core/types/transaction_test.go | 4 +--
ethclient/ethclient.go | 27 ++++++++++++++----
8 files changed, 66 insertions(+), 27 deletions(-)
diff --git a/accounts/external/backend.go b/accounts/external/backend.go
index bdce46497..fbcc65bbd 100644
--- a/accounts/external/backend.go
+++ b/accounts/external/backend.go
@@ -230,8 +230,10 @@ func (api *ExternalSigner) SignTx(account accounts.Account, tx *types.Transactio
version := hexutil.Uint64(tx.Version())
args.Version = &version
args.Reference = tx.Reference()
- memo := hexutil.Bytes(*tx.Memo())
- args.Memo = &memo
+ 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/common/types.go b/common/types.go
index fe4d0d269..3b2b3c7c4 100644
--- a/common/types.go
+++ b/common/types.go
@@ -123,11 +123,14 @@ func (r Reference) Format(s fmt.State, c rune) {
// 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:]
}
- copy(r[:], b)
+ *r = Reference{}
+ copy(r[len(r)-len(b):], b)
}
// UnmarshalText parses a reference in hex syntax.
diff --git a/core/rawdb/reference_index_iterator.go b/core/rawdb/reference_index_iterator.go
index 8ee62a795..fc69a86ee 100644
--- a/core/rawdb/reference_index_iterator.go
+++ b/core/rawdb/reference_index_iterator.go
@@ -104,11 +104,23 @@ func iterateReferences(db ethdb.Database, from uint64, to uint64, reverse bool,
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
}
@@ -125,19 +137,19 @@ func iterateReferences(db ethdb.Database, from uint64, to uint64, reverse bool,
}
}
- // 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
- }
+ // 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
+ }
}
}
@@ -323,4 +335,3 @@ func unindexReferences(db ethdb.Database, from uint64, to uint64, interrupt chan
func UnindexReferences(db ethdb.Database, from uint64, to uint64, interrupt chan struct{}) {
unindexReferences(db, from, to, interrupt, nil)
}
-
diff --git a/core/types/l2trace.go b/core/types/l2trace.go
index cb7be01e7..71664f787 100644
--- a/core/types/l2trace.go
+++ b/core/types/l2trace.go
@@ -212,8 +212,10 @@ func NewTransactionData(tx *Transaction, blockNumber uint64, blockTime uint64, c
}
result.Version = tx.Version()
result.Reference = tx.Reference()
- memo := hexutil.Bytes(*tx.Memo())
- result.Memo = &memo
+ 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
index b2ab509e1..17b577594 100644
--- a/core/types/morph_tx.go
+++ b/core/types/morph_tx.go
@@ -181,6 +181,10 @@ func (tx *MorphTx) setSignatureValues(chainID, v, r, s *big.Int) {
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{
diff --git a/core/types/transaction.go b/core/types/transaction.go
index ccee3b7a7..e229eccb8 100644
--- a/core/types/transaction.go
+++ b/core/types/transaction.go
@@ -987,7 +987,7 @@ func copyAddressPtr(a *common.Address) *common.Address {
return &cpy
}
-// copyAddressPtr copies an address.
+// copyReferencePtr copies a common.Reference and returns a pointer to the copy.
func copyReferencePtr(h *common.Reference) *common.Reference {
if h == nil {
return nil
diff --git a/core/types/transaction_test.go b/core/types/transaction_test.go
index 7326f6560..923b048b4 100644
--- a/core/types/transaction_test.go
+++ b/core/types/transaction_test.go
@@ -733,8 +733,8 @@ func TestMorphTxSigner(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
// Test unsigned tx returns expected error
_, err := Sender(signer, tc.tx)
- if err != tc.wantSenderErr {
- t.Errorf("Sender error mismatch, got %v, want %v", err, tc.wantSenderErr)
+ if !errors.Is(err, tc.wantSenderErr) {
+ t.Errorf("Sender error mismatch, got %v, want %v (using errors.Is)", err, tc.wantSenderErr)
}
// Sign the tx
diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go
index 62dc06e8e..41baaa5f2 100644
--- a/ethclient/ethclient.go
+++ b/ethclient/ethclient.go
@@ -30,7 +30,6 @@ import (
"github.com/morph-l2/go-ethereum/core/types"
"github.com/morph-l2/go-ethereum/eth"
"github.com/morph-l2/go-ethereum/eth/tracers"
- "github.com/morph-l2/go-ethereum/internal/ethapi"
"github.com/morph-l2/go-ethereum/rpc"
)
@@ -39,6 +38,21 @@ type Client struct {
c *rpc.Client
}
+// ReferenceTransactionResult contains transaction information for a reference query result.
+type ReferenceTransactionResult struct {
+ TransactionHash common.Hash `json:"transactionHash"`
+ BlockNumber hexutil.Uint64 `json:"blockNumber"`
+ BlockTimestamp hexutil.Uint64 `json:"blockTimestamp"`
+ TransactionIndex hexutil.Uint64 `json:"transactionIndex"`
+}
+
+// ReferenceQueryArgs contains 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"`
+}
+
// Dial connects a client to the given URL.
func Dial(rawurl string) (*Client, error) {
return DialContext(context.Background(), rawurl)
@@ -378,14 +392,17 @@ func (ec *Client) GetSkippedTransaction(ctx context.Context, txHash common.Hash)
// - 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) ([]ethapi.ReferenceTransactionResult, error) {
- var result []ethapi.ReferenceTransactionResult
- args := ethapi.ReferenceQueryArgs{
+func (ec *Client) GetTransactionHashesByReference(ctx context.Context, reference common.Reference, offset *hexutil.Uint64, limit *hexutil.Uint64) ([]ReferenceTransactionResult, error) {
+ var result []ReferenceTransactionResult
+ args := ReferenceQueryArgs{
Reference: reference,
Offset: offset,
Limit: limit,
}
- return result, ec.c.CallContext(ctx, &result, "morph_getTransactionHashesByReference", args)
+ if err := ec.c.CallContext(ctx, &result, "morph_getTransactionHashesByReference", args); err != nil {
+ return nil, err
+ }
+ return result, nil
}
// GetBlockByNumberOrHash returns the requested block
From 4ebfb51ec582a40fce41e2d0243755935927710d Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Wed, 4 Feb 2026 13:50:22 +0800
Subject: [PATCH 16/33] add args check
---
internal/ethapi/transaction_args.go | 41 +++++++++++++++++------------
1 file changed, 24 insertions(+), 17 deletions(-)
diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go
index e7e350385..ce84c5a13 100644
--- a/internal/ethapi/transaction_args.go
+++ b/internal/ethapi/transaction_args.go
@@ -425,23 +425,30 @@ 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()
- }
- // Determine version based on explicit setting or auto-detection
- version := args.determineMorphTxVersion()
- reference := new(common.Reference)
- if args.Reference != nil {
- reference = args.Reference
- }
- memo := new([]byte)
- if args.Memo != nil {
- memo = (*[]byte)(args.Memo)
+
+ // 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()
+ }
+ // Determine version based on explicit setting or auto-detection
+ version = args.determineMorphTxVersion()
+ 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, version, reference, memo, data, accessList, args.AuthorizationList, true)
From f748d29855e0f24d3338e175c858948cc65923b9 Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Fri, 13 Feb 2026 13:11:55 +0800
Subject: [PATCH 17/33] update
---
accounts/abi/bind/base.go | 78 ++++++-------------
core/tx_list.go | 7 +-
core/tx_pool.go | 19 -----
core/tx_pool_test.go | 18 ++---
core/types/transaction.go | 55 +++++--------
core/types/transaction_test.go | 38 ++++-----
internal/ethapi/transaction_args.go | 117 +++++++++-------------------
signer/core/apitypes/types.go | 36 ++-------
8 files changed, 114 insertions(+), 254 deletions(-)
diff --git a/accounts/abi/bind/base.go b/accounts/abi/bind/base.go
index 1e320587c..671dddbe8 100644
--- a/accounts/abi/bind/base.go
+++ b/accounts/abi/bind/base.go
@@ -301,7 +301,7 @@ func (c *BoundContract) createMorphTx(opts *TransactOpts, contract *common.Addre
}
// Determine version and validate fields
- version, err := c.determineMorphTxVersion(opts)
+ version, err := c.morphTxVersion(opts)
if err != nil {
return nil, err
}
@@ -361,74 +361,42 @@ func (c *BoundContract) createMorphTx(opts *TransactOpts, contract *common.Addre
return types.NewTx(baseTx), nil
}
-// determineMorphTxVersion determines the MorphTx version and validates field requirements.
-// Rules when version is explicitly specified:
+// 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
//
-// Rules when version is not explicitly specified (auto-detection):
-// - (FeeTokenID > 0) && (no Reference) && (no Memo) -> Version 0
-// - (valid Reference) || (valid Memo with 0 < len < 64) -> Version 1
-// - (FeeTokenID == 0) && (empty Reference) && (empty Memo) -> Error
-func (c *BoundContract) determineMorphTxVersion(opts *TransactOpts) (uint8, error) {
- hasReference := opts.Reference != nil && *opts.Reference != (common.Reference{})
- hasMemo := opts.Memo != nil && len(*opts.Memo) > 0
- hasFeeToken := opts.FeeTokenID > 0
- hasFeeLimit := opts.FeeLimit != nil && opts.FeeLimit.Sign() > 0
-
+// If version is not explicitly specified, default to the highest version.
+func (c *BoundContract) morphTxVersion(opts *TransactOpts) (uint8, error) {
// Validate memo length
if opts.Memo != nil && len(*opts.Memo) > common.MaxMemoLength {
return 0, types.ErrMemoTooLong
}
- // Check if version is explicitly specified
- if opts.Version != nil {
- version := *opts.Version
- switch version {
- case types.MorphTxVersion0:
- // Version 0 requires FeeTokenID > 0
- if !hasFeeToken {
- return 0, types.ErrMorphTxV0RequiresFeeToken
- }
- // Version 0 does not support Reference field
- if hasReference {
- return 0, types.ErrMorphTxV0HasReference
- }
- // Version 0 does not support Memo field
- if hasMemo {
- return 0, types.ErrMorphTxV0HasMemo
- }
- return types.MorphTxVersion0, nil
- case types.MorphTxVersion1:
- // Version 1: FeeTokenID, Reference, Memo are all optional
- // If FeeTokenID is 0, FeeLimit must not be set
- if !hasFeeToken && hasFeeLimit {
- return 0, types.ErrMorphTxV1FeeLimitWithoutFeeToken
- }
- return types.MorphTxVersion1, nil
- default:
- return 0, types.ErrMorphTxUnsupportedVersion
- }
- }
-
- // Version not explicitly specified - auto-detect version
- // (FeeTokenID > 0) && (no Reference) && (no Memo) -> Version 0
- if hasFeeToken && !hasReference && !hasMemo {
- return types.MorphTxVersion0, nil
+ // If version is not explicitly specified, use the highest version
+ if opts.Version == nil {
+ return types.MorphTxVersion1, nil
}
- // (valid Reference) || (valid Memo) -> Version 1
- if hasReference || hasMemo {
- // For Version 1, if FeeTokenID is 0, FeeLimit must not be set
- if !hasFeeToken && hasFeeLimit {
- return 0, types.ErrMorphTxV1FeeLimitWithoutFeeToken
+ // 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
}
- return types.MorphTxVersion1, nil
+ case types.MorphTxVersion1:
+ if opts.FeeTokenID == 0 && opts.FeeLimit != nil && opts.FeeLimit.Sign() != 0 {
+ return 0, types.ErrMorphTxV1IllegalExtraParams
+ }
+ default:
+ return 0, types.ErrMorphTxUnsupportedVersion
}
- // (FeeTokenID == 0) && (empty Reference) && (empty Memo) -> Error
- return 0, types.ErrMorphTxCannotDetermineVersion
+ return version, nil
}
func (c *BoundContract) createLegacyTx(opts *TransactOpts, contract *common.Address, input []byte) (*types.Transaction, error) {
diff --git a/core/tx_list.go b/core/tx_list.go
index 2d521cabd..20975592a 100644
--- a/core/tx_list.go
+++ b/core/tx_list.go
@@ -353,10 +353,6 @@ func (l *txList) Add(tx *types.Transaction, state *state.StateDB, priceBump uint
if l.costcap.Alt(tx.FeeTokenID()).Cmp(altCost) < 0 {
l.costcap.SetAltAmount(tx.FeeTokenID(), altCost)
}
- } else if tx.IsMorphTx() {
- // MorphTx V1 with FeeTokenID=0 uses GasFee() instead of Cost()
- cost := new(big.Int).Add(tx.GasFee(), tx.Value())
- ethCost = new(big.Int).Add(cost, l1DataFee)
} else {
ethCost = new(big.Int).Add(tx.Cost(), l1DataFee)
}
@@ -411,8 +407,7 @@ func (l *txList) Filter(costLimit *big.Int, gasLimit uint64, altCostLimit map[ui
// Calculate cost based on transaction type
var txCost *big.Int
if tx.IsMorphTx() {
- // MorphTx V1 with FeeTokenID=0 uses GasFee() instead of Cost()
- txCost = new(big.Int).Add(tx.GasFee(), tx.Value())
+ // TODO
} else {
txCost = tx.Cost()
}
diff --git a/core/tx_pool.go b/core/tx_pool.go
index 04e24aa76..4cd3d1e9d 100644
--- a/core/tx_pool.go
+++ b/core/tx_pool.go
@@ -758,12 +758,6 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
if pool.currentState.GetBalance(from).Cmp(tx.Value()) < 0 {
return ErrInsufficientFunds
}
- } else if tx.IsMorphTx() {
- // MorphTx V1 with FeeTokenID=0 uses GasFee() instead of Cost()
- cost := new(big.Int).Add(tx.GasFee(), tx.Value())
- if pool.currentState.GetBalance(from).Cmp(cost) < 0 {
- return ErrInsufficientFunds
- }
} else {
if pool.currentState.GetBalance(from).Cmp(tx.Cost()) < 0 {
return ErrInsufficientFunds
@@ -796,12 +790,6 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
if limit.Cmp(erc20Amount) < 0 {
return errors.New("invalid transaction: insufficient funds for l1fee + gas * price or fee limit too small")
}
- } else if tx.IsMorphTx() {
- // MorphTx V1 with FeeTokenID=0 uses GasFee() instead of Cost()
- cost := new(big.Int).Add(tx.GasFee(), tx.Value())
- if b := pool.currentState.GetBalance(from); b.Cmp(new(big.Int).Add(cost, l1DataFee)) < 0 {
- return errors.New("invalid transaction: insufficient funds for l1fee + gas * price + value")
- }
} else {
// cost == L1 data fee + V + GP * GL
if b := pool.currentState.GetBalance(from); b.Cmp(new(big.Int).Add(tx.Cost(), l1DataFee)) < 0 {
@@ -1642,9 +1630,6 @@ func (pool *TxPool) executableTxFilter(addr common.Address, costLimit *big.Int,
if tx.IsMorphTxWithAltFee() {
// MorphTx with alt fee is handled separately
txCost = nil
- } else if tx.IsMorphTx() {
- // MorphTx V1 with FeeTokenID=0 uses GasFee() instead of Cost()
- txCost = new(big.Int).Add(tx.GasFee(), tx.Value())
} else {
txCost = tx.Cost()
}
@@ -1679,10 +1664,6 @@ func (pool *TxPool) executableTxFilter(addr common.Address, costLimit *big.Int,
limit = cmath.BigMin(altCostLimit[tx.FeeTokenID()], tx.FeeLimit())
}
return costLimit.Cmp(tx.Value()) < 0 || limit.Cmp(altAmount) < 0
- } else if tx.IsMorphTx() {
- // MorphTx V1 with FeeTokenID=0 uses GasFee() instead of Cost()
- cost := new(big.Int).Add(tx.GasFee(), tx.Value())
- return costLimit.Cmp(new(big.Int).Add(cost, l1DataFee)) < 0
} else {
return costLimit.Cmp(new(big.Int).Add(tx.Cost(), l1DataFee)) < 0
}
diff --git a/core/tx_pool_test.go b/core/tx_pool_test.go
index c33ef3f0c..0ee4c168b 100644
--- a/core/tx_pool_test.go
+++ b/core/tx_pool_test.go
@@ -2762,8 +2762,8 @@ func TestMorphTxValidation(t *testing.T) {
// 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.ErrMorphTxV0RequiresFeeToken) {
- t.Errorf("expected ErrMorphTxV0RequiresFeeToken, got %v", err)
+ if err := pool.AddRemote(tx); !errors.Is(err, types.ErrMorphTxV0IllegalExtraParams) {
+ t.Errorf("expected ErrMorphTxV0IllegalExtraParams, got %v", err)
}
})
@@ -2781,7 +2781,7 @@ func TestMorphTxValidation(t *testing.T) {
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.ErrMorphTxV0RequiresFeeToken) {
+ if errors.Is(err, types.ErrMorphTxV0IllegalExtraParams) {
t.Errorf("V0 with FeeTokenID > 0 should pass version validation, got %v", err)
}
})
@@ -2839,8 +2839,8 @@ func TestMorphTxValidation(t *testing.T) {
Version: types.MorphTxVersion0,
Reference: &ref,
})
- if err := pool.AddRemote(tx); !errors.Is(err, types.ErrMorphTxV0HasReference) {
- t.Errorf("expected ErrMorphTxV0HasReference, got %v", err)
+ if err := pool.AddRemote(tx); !errors.Is(err, types.ErrMorphTxV0IllegalExtraParams) {
+ t.Errorf("expected ErrMorphTxV0IllegalExtraParams, got %v", err)
}
})
@@ -2867,8 +2867,8 @@ func TestMorphTxValidation(t *testing.T) {
Version: types.MorphTxVersion0,
Memo: &memo,
})
- if err := pool.AddRemote(tx); !errors.Is(err, types.ErrMorphTxV0HasMemo) {
- t.Errorf("expected ErrMorphTxV0HasMemo, got %v", err)
+ if err := pool.AddRemote(tx); !errors.Is(err, types.ErrMorphTxV0IllegalExtraParams) {
+ t.Errorf("expected ErrMorphTxV0IllegalExtraParams, got %v", err)
}
})
@@ -2898,8 +2898,8 @@ func TestMorphTxValidation(t *testing.T) {
Reference: &ref,
Memo: &memo,
})
- if err := pool.AddRemote(tx); !errors.Is(err, types.ErrMorphTxV1FeeLimitWithoutFeeToken) {
- t.Errorf("expected ErrMorphTxV1FeeLimitWithoutFeeToken, got %v", err)
+ if err := pool.AddRemote(tx); !errors.Is(err, types.ErrMorphTxV1IllegalExtraParams) {
+ t.Errorf("expected ErrMorphTxV1IllegalExtraParams, got %v", err)
}
})
}
diff --git a/core/types/transaction.go b/core/types/transaction.go
index e229eccb8..35277cd09 100644
--- a/core/types/transaction.go
+++ b/core/types/transaction.go
@@ -33,24 +33,21 @@ 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 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")
- ErrMorphTxV0RequiresFeeToken = errors.New("version 0 MorphTx requires FeeTokenID > 0")
- ErrMorphTxV0HasReference = errors.New("version 0 MorphTx does not support Reference field")
- ErrMorphTxV0HasMemo = errors.New("version 0 MorphTx does not support Memo field")
- ErrMorphTxV1FeeLimitWithoutFeeToken = errors.New("version 1 MorphTx cannot have FeeLimit when FeeTokenID is 0")
- ErrMorphTxCannotDetermineVersion = errors.New("cannot determine MorphTx version: FeeTokenID=0 without Reference or Memo")
- ErrMorphTxUnsupportedVersion = errors.New("unsupported MorphTx version")
- 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")
+ 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.
@@ -440,22 +437,16 @@ func (tx *Transaction) ValidateMorphTxVersion() error {
switch morphTx.Version {
case MorphTxVersion0:
// Version 0 requires FeeTokenID > 0 (legacy format used for alt-fee transactions)
- if morphTx.FeeTokenID == 0 {
- return ErrMorphTxV0RequiresFeeToken
- }
- // Version 0 does not support Reference field
- if morphTx.Reference != nil && *morphTx.Reference != (common.Reference{}) {
- return ErrMorphTxV0HasReference
- }
- // Version 0 does not support Memo field
- if morphTx.Memo != nil && len(*morphTx.Memo) > 0 {
- return ErrMorphTxV0HasMemo
+ 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 ErrMorphTxV1FeeLimitWithoutFeeToken
+ if morphTx.FeeTokenID == 0 && morphTx.FeeLimit != nil && morphTx.FeeLimit.Sign() != 0 {
+ return ErrMorphTxV1IllegalExtraParams
}
default:
return ErrMorphTxUnsupportedVersion
@@ -465,10 +456,6 @@ func (tx *Transaction) ValidateMorphTxVersion() error {
// Cost returns gas * gasPrice + value.
func (tx *Transaction) Cost() *big.Int {
- // TODO: morph tx without fee token
- if tx.IsMorphTx() {
- panic(ErrCostNotSupported)
- }
total := tx.GasFee()
total.Add(total, tx.Value())
return total
diff --git a/core/types/transaction_test.go b/core/types/transaction_test.go
index 923b048b4..b20f1e7a5 100644
--- a/core/types/transaction_test.go
+++ b/core/types/transaction_test.go
@@ -112,15 +112,15 @@ var (
// 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,
+ 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)
@@ -480,11 +480,11 @@ func TestTransactionCoding(t *testing.T) {
t.Fatalf("could not generate key: %v", err)
}
var (
- 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}}}}
+ 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}}}}
)
morphTxRef := common.HexToReference("0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890")
morphTxMemo := []byte("test memo")
@@ -963,19 +963,19 @@ func TestMorphTxValidation(t *testing.T) {
}{
// Version 0 tests
{"V0 with FeeTokenID > 0", MorphTxVersion0, 1, nil, nil, nil, nil},
- {"V0 with FeeTokenID = 0", MorphTxVersion0, 0, nil, nil, nil, ErrMorphTxV0RequiresFeeToken},
- {"V0 with Reference", MorphTxVersion0, 1, nil, &ref, nil, ErrMorphTxV0HasReference},
+ {"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, ErrMorphTxV0HasMemo},
+ {"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, ErrMorphTxV0HasReference},
+ {"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, ErrMorphTxV1FeeLimitWithoutFeeToken},
+ {"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
diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go
index ce84c5a13..cffd10335 100644
--- a/internal/ethapi/transaction_args.go
+++ b/internal/ethapi/transaction_args.go
@@ -95,97 +95,46 @@ func (args *TransactionArgs) isMorphTxArgs() bool {
}
// validateMorphTxVersion validates the MorphTx version and its associated field requirements.
-// Rules when version is explicitly specified:
+// 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
//
-// Rules when version is not explicitly specified (auto-detection):
-// - (FeeTokenID > 0) && (no Reference) && (no Memo) -> Version 0
-// - (valid Reference) || (valid Memo with 0 < len < 64) -> Version 1
-// - (FeeTokenID == 0) && (empty Reference) && (empty Memo) -> Error
+// 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
}
- // Check if version is explicitly specified
- if args.Version != nil {
- // Version explicitly specified - validate according to version
- version := uint8(*args.Version)
- switch version {
- case types.MorphTxVersion0:
- // Version 0 requires FeeTokenID > 0
- if args.FeeTokenID == nil || *args.FeeTokenID == 0 {
- return types.ErrMorphTxV0RequiresFeeToken
- }
- // Version 0 does not support Reference field
- if args.Reference != nil && *args.Reference != (common.Reference{}) {
- return types.ErrMorphTxV0HasReference
- }
- // Version 0 does not support Memo field
- if args.Memo != nil && len(*args.Memo) > 0 {
- return types.ErrMorphTxV0HasMemo
- }
- 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.ErrMorphTxV1FeeLimitWithoutFeeToken
- }
- default:
- return types.ErrMorphTxUnsupportedVersion
- }
+ // Only validate when version is explicitly specified
+ if args.Version == nil {
return nil
}
- // Version not explicitly specified - auto-detect version
- hasFeeToken := args.FeeTokenID != nil && *args.FeeTokenID > 0
- hasReference := args.Reference != nil && *args.Reference != (common.Reference{})
- hasMemo := args.Memo != nil && len(*args.Memo) > 0
-
- // Auto-detect version based on fields
- if hasFeeToken && !hasReference && !hasMemo {
- // (FeeTokenID > 0) && (no Reference) && (no Memo) -> Version 0
- return nil
- }
- if hasReference || hasMemo {
- // (valid Reference) || (valid Memo) -> Version 1
- // For Version 1, if FeeTokenID is 0, FeeLimit must not be set
- if !hasFeeToken && args.FeeLimit != nil && args.FeeLimit.ToInt().Sign() > 0 {
- return types.ErrMorphTxV1FeeLimitWithoutFeeToken
+ 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
}
- return nil
- }
- // (FeeTokenID == 0) && (empty Reference) && (empty Memo) -> Error
- return types.ErrMorphTxCannotDetermineVersion
-}
-
-// determineMorphTxVersion determines the MorphTx version based on explicit setting or auto-detection.
-// This function should be called after validateMorphTxVersion() has passed.
-func (args *TransactionArgs) determineMorphTxVersion() uint8 {
- // If version is explicitly specified, use it
- if args.Version != nil {
- return uint8(*args.Version)
- }
-
- // Auto-detect version based on fields
- hasFeeToken := args.FeeTokenID != nil && *args.FeeTokenID > 0
- hasReference := args.Reference != nil && *args.Reference != (common.Reference{})
- hasMemo := args.Memo != nil && len(*args.Memo) > 0
-
- // (FeeTokenID > 0) && (no Reference) && (no Memo) -> Version 0
- if hasFeeToken && !hasReference && !hasMemo {
- return types.MorphTxVersion0
+ 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
}
-
- // (valid Reference) || (valid Memo) -> Version 1
- // Also fallback to Version 1 for other cases
- return types.MorphTxVersion1
+ return nil
}
// setDefaults fills in default values for unspecified tx fields.
@@ -441,8 +390,11 @@ func (args *TransactionArgs) ToMessage(globalGasCap uint64, baseFee *big.Int) (t
if args.FeeLimit != nil {
feeLimit = args.FeeLimit.ToInt()
}
- // Determine version based on explicit setting or auto-detection
- version = args.determineMorphTxVersion()
+ // Default to version 1 if version is not explicitly specified
+ version = types.MorphTxVersion1
+ if args.Version != nil {
+ version = uint8(*args.Version)
+ }
if args.Reference != nil {
reference = args.Reference
}
@@ -528,8 +480,11 @@ func (args *TransactionArgs) toTransaction() *types.Transaction {
if args.AccessList != nil {
al = *args.AccessList
}
- // Determine version based on explicit setting or auto-detection
- version := args.determineMorphTxVersion()
+ // Default to version 1 if version is not explicitly specified
+ version := types.MorphTxVersion1
+ if args.Version != nil {
+ version = uint8(*args.Version)
+ }
var feeTokenID uint16
if args.FeeTokenID != nil {
feeTokenID = uint16(*args.FeeTokenID)
diff --git a/signer/core/apitypes/types.go b/signer/core/apitypes/types.go
index 38de24bec..f790e772a 100644
--- a/signer/core/apitypes/types.go
+++ b/signer/core/apitypes/types.go
@@ -102,35 +102,6 @@ func (args SendTxArgs) String() string {
return err.Error()
}
-// determineMorphTxVersion determines the MorphTx version based on explicit setting or auto-detection.
-// Rules when version is explicitly specified:
-// - Use the specified version directly
-//
-// Rules when version is not explicitly specified (auto-detection):
-// - (FeeTokenID > 0) && (no Reference) && (no Memo) -> Version 0
-// - (valid Reference) || (valid Memo) -> Version 1
-// - Otherwise -> Version 1
-func (args *SendTxArgs) determineMorphTxVersion() uint8 {
- // If version is explicitly specified, use it
- if args.Version != nil {
- return uint8(*args.Version)
- }
-
- // Auto-detect version based on fields
- hasFeeToken := args.FeeTokenID != nil && *args.FeeTokenID > 0
- hasReference := args.Reference != nil && *args.Reference != (common.Reference{})
- hasMemo := args.Memo != nil && len(*args.Memo) > 0
-
- // (FeeTokenID > 0) && (no Reference) && (no Memo) -> Version 0
- if hasFeeToken && !hasReference && !hasMemo {
- return types.MorphTxVersion0
- }
-
- // (valid Reference) || (valid Memo) -> Version 1
- // Also fallback to Version 1 for other cases
- return types.MorphTxVersion1
-}
-
// ToTransaction converts the arguments to a transaction.
func (args *SendTxArgs) ToTransaction() *types.Transaction {
// Add the To-field, if specified
@@ -158,8 +129,11 @@ func (args *SendTxArgs) ToTransaction() *types.Transaction {
if args.AccessList != nil {
al = *args.AccessList
}
- // Determine version based on explicit setting or auto-detection
- version := args.determineMorphTxVersion()
+ // Default to version 1 if version is not explicitly specified
+ version := uint8(types.MorphTxVersion1)
+ if args.Version != nil {
+ version = uint8(*args.Version)
+ }
var feeTokenID uint16
if args.FeeTokenID != nil {
feeTokenID = uint16(*args.FeeTokenID)
From 86092109fae4cb1b957f79a0591c38bbe2851912 Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Fri, 13 Feb 2026 15:10:51 +0800
Subject: [PATCH 18/33] update
---
accounts/abi/bind/base.go | 3 ++
core/tx_list.go | 9 +---
core/tx_pool_test.go | 4 +-
ethclient/ethclient.go | 21 ++--------
internal/ethapi/api.go | 87 ++-------------------------------------
rpc/types.go | 15 +++++++
6 files changed, 28 insertions(+), 111 deletions(-)
diff --git a/accounts/abi/bind/base.go b/accounts/abi/bind/base.go
index 671dddbe8..fdf276455 100644
--- a/accounts/abi/bind/base.go
+++ b/accounts/abi/bind/base.go
@@ -376,6 +376,9 @@ func (c *BoundContract) morphTxVersion(opts *TransactOpts) (uint8, error) {
// If version is not explicitly specified, use the highest version
if opts.Version == nil {
+ if opts.FeeTokenID == 0 && opts.FeeLimit != nil && opts.FeeLimit.Sign() != 0 {
+ return 0, types.ErrMorphTxV1IllegalExtraParams
+ }
return types.MorphTxVersion1, nil
}
diff --git a/core/tx_list.go b/core/tx_list.go
index 20975592a..d36ccc594 100644
--- a/core/tx_list.go
+++ b/core/tx_list.go
@@ -404,14 +404,7 @@ func (l *txList) Filter(costLimit *big.Int, gasLimit uint64, altCostLimit map[ui
}
return tx.Gas() > gasLimit || tx.Value().Cmp(costLimit) > 0
}
- // Calculate cost based on transaction type
- var txCost *big.Int
- if tx.IsMorphTx() {
- // TODO
- } else {
- txCost = tx.Cost()
- }
- return !allLower || tx.Gas() > gasLimit || txCost.Cmp(costLimit) > 0
+ return !allLower || tx.Gas() > gasLimit || tx.Cost().Cmp(costLimit) > 0
})
if len(removed) == 0 {
diff --git a/core/tx_pool_test.go b/core/tx_pool_test.go
index 0ee4c168b..ea4d3d279 100644
--- a/core/tx_pool_test.go
+++ b/core/tx_pool_test.go
@@ -3005,8 +3005,8 @@ func TestMorphTxPoolManagement(t *testing.T) {
t.Fatalf("failed to add tx1: %v", err)
}
- // Try to replace with same gas price (should fail)
- tx2 := morphTxV1(0, 100000, big.NewInt(10), big.NewInt(1), key)
+ // 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")
}
diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go
index 41baaa5f2..22f997b2e 100644
--- a/ethclient/ethclient.go
+++ b/ethclient/ethclient.go
@@ -38,21 +38,6 @@ type Client struct {
c *rpc.Client
}
-// ReferenceTransactionResult contains transaction information for a reference query result.
-type ReferenceTransactionResult struct {
- TransactionHash common.Hash `json:"transactionHash"`
- BlockNumber hexutil.Uint64 `json:"blockNumber"`
- BlockTimestamp hexutil.Uint64 `json:"blockTimestamp"`
- TransactionIndex hexutil.Uint64 `json:"transactionIndex"`
-}
-
-// ReferenceQueryArgs contains 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"`
-}
-
// Dial connects a client to the given URL.
func Dial(rawurl string) (*Client, error) {
return DialContext(context.Background(), rawurl)
@@ -392,9 +377,9 @@ func (ec *Client) GetSkippedTransaction(ctx context.Context, txHash common.Hash)
// - 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) ([]ReferenceTransactionResult, error) {
- var result []ReferenceTransactionResult
- args := ReferenceQueryArgs{
+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,
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
index f01dd5254..cae75df37 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -1900,78 +1900,6 @@ func (s *PublicTransactionPoolAPI) GetTransactionCount(ctx context.Context, addr
return (*hexutil.Uint64)(&nonce), state.Error()
}
-// 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"`
-}
-
-// 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 (s *PublicTransactionPoolAPI) GetTransactionHashesByReference(
- ctx context.Context,
- reference common.Reference,
- offset *hexutil.Uint64,
- limit *hexutil.Uint64,
-) (
- []ReferenceTransactionResult,
- error,
-) {
- // Set default values
- offsetVal := uint64(0)
- if offset != nil {
- offsetVal = uint64(*offset)
- }
- limitVal := uint64(100)
- if limit != nil {
- limitVal = uint64(*limit)
- }
-
- // Validate limit (max 100)
- if limitVal > 100 {
- return nil, errors.New("limit exceeds maximum value of 100")
- }
-
- entries := rawdb.ReadReferenceIndexEntries(s.b.ChainDb(), reference)
- if len(entries) == 0 {
- return nil, nil
- }
-
- // Validate offset
- if offsetVal >= uint64(len(entries)) {
- return nil, fmt.Errorf("offset %d exceeds total results %d", offsetVal, len(entries))
- }
-
- // Apply pagination
- end := offsetVal + limitVal
- if end > uint64(len(entries)) {
- end = uint64(len(entries))
- }
- paginatedEntries := entries[offsetVal:end]
-
- // Build result
- result := make([]ReferenceTransactionResult, 0, len(paginatedEntries))
- for _, entry := range paginatedEntries {
- blockNumber := rawdb.ReadTxLookupEntry(s.b.ChainDb(), entry.TxHash)
- if blockNumber == nil {
- continue
- }
- result = append(result, ReferenceTransactionResult{
- TransactionHash: entry.TxHash,
- BlockNumber: hexutil.Uint64(*blockNumber),
- BlockTimestamp: hexutil.Uint64(entry.BlockTimestamp),
- TransactionIndex: hexutil.Uint64(entry.TxIndex),
- })
- }
- return result, nil
-}
-
// GetTransactionByHash returns the transaction for the given hash
func (s *PublicTransactionPoolAPI) GetTransactionByHash(ctx context.Context, hash common.Hash) (*RPCTransaction, error) {
// Try to return an already finalized transaction
@@ -2543,13 +2471,6 @@ func NewPublicMorphAPI(b Backend) *PublicMorphAPI {
return &PublicMorphAPI{b}
}
-// 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"`
-}
-
// GetTransactionHashesByReference returns transactions for the given reference with pagination.
// Results are sorted by blockTimestamp and txIndex (ascending order).
// Parameters:
@@ -2558,9 +2479,9 @@ type ReferenceQueryArgs struct {
// - args.limit: pagination limit (default: 100, max: 100)
func (s *PublicMorphAPI) GetTransactionHashesByReference(
ctx context.Context,
- args ReferenceQueryArgs,
+ args rpc.ReferenceQueryArgs,
) (
- []ReferenceTransactionResult,
+ []rpc.ReferenceTransactionResult,
error,
) {
// Set default values
@@ -2596,13 +2517,13 @@ func (s *PublicMorphAPI) GetTransactionHashesByReference(
paginatedEntries := entries[offsetVal:end]
// Build result
- result := make([]ReferenceTransactionResult, 0, len(paginatedEntries))
+ 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, ReferenceTransactionResult{
+ result = append(result, rpc.ReferenceTransactionResult{
TransactionHash: entry.TxHash,
BlockNumber: hexutil.Uint64(*blockNumber),
BlockTimestamp: hexutil.Uint64(entry.BlockTimestamp),
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"`
+}
From 73b8a050597a24b24de3da07cf1456084a923b00 Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Thu, 26 Feb 2026 14:32:14 +0800
Subject: [PATCH 19/33] update to jade
---
cmd/geth/chaincmd.go | 8 ++---
cmd/geth/config.go | 6 ++--
cmd/geth/main.go | 2 +-
cmd/utils/flags.go | 6 ++--
core/block_validator.go | 4 +--
core/block_validator_test.go | 44 ++++++++++++++-----------
core/genesis.go | 6 ++--
core/mpt_fork_test.go | 64 ++++++++++++++++++------------------
eth/backend.go | 4 +--
eth/catalyst/l2_api.go | 2 +-
eth/ethconfig/config.go | 4 +--
internal/ethapi/api.go | 8 ++---
les/client.go | 4 +--
params/config.go | 31 ++++++++---------
14 files changed, 99 insertions(+), 94 deletions(-)
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/core/block_validator.go b/core/block_validator.go
index c86225a37..7df2053e7 100644
--- a/core/block_validator.go
+++ b/core/block_validator.go
@@ -113,9 +113,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/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/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/internal/ethapi/api.go b/internal/ethapi/api.go
index cae75df37..eb7779cb4 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -653,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)
@@ -716,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{
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/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),
}
}
From 702d5cea9c91892b8ac4fcb625c49cb36908b362 Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Thu, 26 Feb 2026 15:28:36 +0800
Subject: [PATCH 20/33] active after jade fork time
---
core/block_validator.go | 9 ++-
core/tx_pool.go | 41 ++++++++--
core/tx_pool_test.go | 117 ++++++++++++++++++++++------
core/types/transaction.go | 23 +++---
core/types/transaction_test.go | 46 ++---------
internal/ethapi/transaction_args.go | 11 +++
6 files changed, 158 insertions(+), 89 deletions(-)
diff --git a/core/block_validator.go b/core/block_validator.go
index 7df2053e7..bbda92745 100644
--- a/core/block_validator.go
+++ b/core/block_validator.go
@@ -60,12 +60,13 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error {
return ErrInvalidBlockPayloadSize
}
// Validate MorphTx for all transactions
+ isJadeFork := v.config.IsJadeFork(block.Time())
for _, tx := range block.Transactions() {
- // Validate memo length
- if err := tx.ValidateMemo(); err != nil {
- return err
+ // Reject MorphTx V1 before jade fork is active
+ if !isJadeFork && tx.IsMorphTx() && tx.Version() == types.MorphTxVersion1 {
+ return types.ErrMorphTxV1NotYetActive
}
- // Validate version and associated field requirements
+ // Validate version, memo, and associated field requirements
if err := tx.ValidateMorphTxVersion(); err != nil {
return err
}
diff --git a/core/tx_pool.go b/core/tx_pool.go
index 4cd3d1e9d..83fd79549 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,22 +669,23 @@ 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.MorphTxType {
return ErrTxTypeNotSupported
}
- // Validate MorphTx memo length
- if err := tx.ValidateMemo(); err != nil {
- return err
+ if !pool.eip7702 && tx.Type() == types.SetCodeTxType {
+ return ErrTxTypeNotSupported
}
- // Validate MorphTx version and associated field requirements
+
+ // 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
}
- if !pool.eip7702 && tx.Type() == types.SetCodeTxType {
- return ErrTxTypeNotSupported
- }
// Reject transactions over defined size to prevent DOS attacks
if uint64(tx.Size()) > uint64(pool.txMaxSize) {
@@ -1269,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{} {
@@ -1541,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
diff --git a/core/tx_pool_test.go b/core/tx_pool_test.go
index ea4d3d279..432fc06b4 100644
--- a/core/tx_pool_test.go
+++ b/core/tx_pool_test.go
@@ -79,12 +79,13 @@ func init() {
eip1559NoL1feeConfig.BerlinBlock = common.Big0
eip1559NoL1feeConfig.LondonBlock = common.Big0
- // MorphTx config with Emerald fork enabled
+ // 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.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
@@ -215,29 +216,6 @@ func morphTxV1WithMemo(nonce uint64, gaslimit uint64, gasFee *big.Int, tip *big.
return tx
}
-// morphTxV1WithAltFee creates a MorphTx V1 with alt fee (FeeTokenID > 0).
-func morphTxV1WithAltFee(nonce uint64, gaslimit uint64, gasFee *big.Int, tip *big.Int, feeTokenID uint16, 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: feeTokenID,
- FeeLimit: big.NewInt(1000000),
- Version: types.MorphTxVersion1,
- Reference: &ref,
- Memo: &memo,
- })
- return tx
-}
-
func setupTxPool() (*TxPool, *ecdsa.PrivateKey) {
return setupTxPoolWithConfig(params.TestChainConfig)
}
@@ -3106,3 +3084,92 @@ func TestMorphTxAccessors(t *testing.T) {
}
})
}
+
+// 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/transaction.go b/core/types/transaction.go
index 35277cd09..6ced68b85 100644
--- a/core/types/transaction.go
+++ b/core/types/transaction.go
@@ -43,6 +43,7 @@ var (
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")
@@ -409,31 +410,21 @@ func (tx *Transaction) Memo() *[]byte {
return tx.AsMorphTx().Memo
}
-// ValidateMemo validates that the memo length does not exceed the maximum limit.
-// Returns nil if the transaction is not a MorphTx or if the memo length is valid.
-func (tx *Transaction) ValidateMemo() error {
- if !tx.IsMorphTx() {
- return nil
- }
- if tx.AsMorphTx().Memo != nil && len(*tx.AsMorphTx().Memo) > common.MaxMemoLength {
- return ErrMemoTooLong
- }
- return nil
-}
-
-// ValidateMorphTxVersion validates the MorphTx version and its associated field requirements.
+// 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 the version is valid.
+// 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)
@@ -448,6 +439,10 @@ func (tx *Transaction) ValidateMorphTxVersion() error {
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
}
diff --git a/core/types/transaction_test.go b/core/types/transaction_test.go
index b20f1e7a5..6270a86ae 100644
--- a/core/types/transaction_test.go
+++ b/core/types/transaction_test.go
@@ -910,41 +910,8 @@ func TestMorphTxEncoding(t *testing.T) {
}
}
-// TestMorphTxValidation tests ValidateMemo and ValidateMorphTxVersion.
+// TestMorphTxValidation tests ValidateMorphTxVersion (which includes memo validation).
func TestMorphTxValidation(t *testing.T) {
- t.Run("ValidateMemo", func(t *testing.T) {
- tests := []struct {
- name string
- memo *[]byte
- wantErr error
- }{
- {"nil memo", nil, nil},
- {"empty memo", func() *[]byte { m := []byte{}; return &m }(), nil},
- {"valid memo", func() *[]byte { m := []byte("hello"); return &m }(), nil},
- {"max length memo", func() *[]byte { m := make([]byte, common.MaxMemoLength); return &m }(), nil},
- {"over max length memo", func() *[]byte { m := make([]byte, common.MaxMemoLength+1); return &m }(), ErrMemoTooLong},
- }
-
- 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,
- Memo: tc.memo,
- })
- err := tx.ValidateMemo()
- if err != tc.wantErr {
- t.Errorf("ValidateMemo error mismatch, got %v, want %v", err, tc.wantErr)
- }
- })
- }
- })
-
t.Run("ValidateMorphTxVersion", func(t *testing.T) {
ref := common.HexToReference("0x1111111111111111111111111111111111111111111111111111111111111111")
emptyRef := common.Reference{}
@@ -961,6 +928,12 @@ func TestMorphTxValidation(t *testing.T) {
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},
@@ -1007,16 +980,13 @@ func TestMorphTxValidation(t *testing.T) {
})
t.Run("Non-MorphTx validation", func(t *testing.T) {
- // ValidateMemo on non-MorphTx should return nil
+ // ValidateMorphTxVersion on non-MorphTx should return nil
legacyTx := NewTx(&LegacyTx{
Nonce: 1,
GasPrice: big.NewInt(1),
Gas: 21000,
To: &testAddr,
})
- if err := legacyTx.ValidateMemo(); err != nil {
- t.Errorf("ValidateMemo on LegacyTx should return nil, got %v", err)
- }
if err := legacyTx.ValidateMorphTxVersion(); err != nil {
t.Errorf("ValidateMorphTxVersion on LegacyTx should return nil, got %v", err)
}
diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go
index cffd10335..de98870ce 100644
--- a/internal/ethapi/transaction_args.go
+++ b/internal/ethapi/transaction_args.go
@@ -222,6 +222,17 @@ func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend) error {
if err := args.validateMorphTxVersion(); err != nil {
return err
}
+ // Reject MorphTx V1 before jade fork is active
+ if args.isMorphTxArgs() && !b.ChainConfig().IsJadeFork(head.Time) {
+ // Determine the effective version (default to V1 if not specified)
+ version := types.MorphTxVersion1
+ if args.Version != nil {
+ version = uint8(*args.Version)
+ }
+ if version == types.MorphTxVersion1 {
+ return types.ErrMorphTxV1NotYetActive
+ }
+ }
// Estimate the gas usage if necessary.
if args.Gas == nil {
// These fields are immutable during the estimation, safe to
From 21d0c83ef05f114292dfa1fbfe10ff4bc3a4850e Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Thu, 26 Feb 2026 18:24:31 +0800
Subject: [PATCH 21/33] check geth version
---
cmd/evm/internal/t8ntool/execution.go | 10 +++---
core/state_processor.go | 10 +++---
core/types/l2trace.go | 13 +++++---
core/types/receipt.go | 10 ++++--
core/types/transaction.go | 1 -
core/types/transaction_marshalling.go | 13 +++++---
internal/ethapi/api.go | 11 ++++---
internal/ethapi/transaction_args.go | 44 ++++++++++++++++++---------
8 files changed, 72 insertions(+), 40 deletions(-)
diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go
index 225aa2fd8..2a26da15f 100644
--- a/cmd/evm/internal/t8ntool/execution.go
+++ b/cmd/evm/internal/t8ntool/execution.go
@@ -219,10 +219,12 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
receipt.FeeRate = msgResult.FeeRate
receipt.TokenScale = msgResult.TokenScale
}
- version := msg.Version()
- receipt.Version = version
- receipt.Reference = msg.Reference()
- receipt.Memo = msg.Memo()
+ // 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/core/state_processor.go b/core/state_processor.go
index 5679a8df7..06f98dd01 100644
--- a/core/state_processor.go
+++ b/core/state_processor.go
@@ -198,10 +198,12 @@ func ApplyTransactionWithEVM(msg Message, config *params.ChainConfig, gp *GasPoo
receipt.FeeLimit = tx.FeeLimit()
receipt.FeeRate = result.FeeRate
receipt.TokenScale = result.TokenScale
- receipt.Version = tx.Version()
- receipt.Reference = tx.Reference()
- memo := tx.Memo()
- receipt.Memo = memo
+ // 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/types/l2trace.go b/core/types/l2trace.go
index 76ce4a945..80f620b85 100644
--- a/core/types/l2trace.go
+++ b/core/types/l2trace.go
@@ -203,11 +203,14 @@ func NewTransactionData(tx *Transaction, blockNumber uint64, blockTime uint64, c
if feeLimit := tx.FeeLimit(); feeLimit != nil && feeLimit.Sign() > 0 {
result.FeeLimit = (*hexutil.Big)(feeLimit)
}
- result.Version = tx.Version()
- result.Reference = tx.Reference()
- if tx.Memo() != nil {
- memo := hexutil.Bytes(*tx.Memo())
- result.Memo = &memo
+ // 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
+ }
}
}
diff --git a/core/types/receipt.go b/core/types/receipt.go
index 6747ee83a..303459420 100644
--- a/core/types/receipt.go
+++ b/core/types/receipt.go
@@ -119,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
@@ -128,9 +132,9 @@ type storedReceiptRLP struct {
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, convert to *[]byte when needed
+ 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.
diff --git a/core/types/transaction.go b/core/types/transaction.go
index 6ced68b85..3856510fd 100644
--- a/core/types/transaction.go
+++ b/core/types/transaction.go
@@ -338,7 +338,6 @@ func (tx *Transaction) IsL1MessageTx() bool {
// IsMorphTx returns true if the transaction is morph tx.
func (tx *Transaction) IsMorphTx() bool {
- // TODO: check if altfee used
return tx.Type() == MorphTxType
}
diff --git a/core/types/transaction_marshalling.go b/core/types/transaction_marshalling.go
index 476ab8926..dbf285279 100644
--- a/core/types/transaction_marshalling.go
+++ b/core/types/transaction_marshalling.go
@@ -204,9 +204,12 @@ func (tx *Transaction) MarshalJSON() ([]byte, error) {
enc.AccessList = &itx.AccessList
enc.FeeTokenID = hexutil.Uint16(itx.FeeTokenID)
enc.FeeLimit = (*hexutil.Big)(itx.FeeLimit)
- enc.Version = (*uint8)(&itx.Version)
- enc.Reference = (*common.Reference)(itx.Reference)
- enc.Memo = (*hexutil.Bytes)(itx.Memo)
+ // Only include V1 fields (version, reference, memo) for V1+ transactions
+ if itx.Version >= MorphTxVersion1 {
+ enc.Version = (*uint8)(&itx.Version)
+ 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)
@@ -605,7 +608,9 @@ func (tx *Transaction) UnmarshalJSON(input []byte) error {
itx.FeeTokenID = uint16(dec.FeeTokenID)
itx.FeeLimit = (*big.Int)(dec.FeeLimit)
itx.Value = (*big.Int)(dec.Value)
- itx.Version = *dec.Version
+ if dec.Version != nil {
+ itx.Version = *dec.Version
+ }
itx.Reference = (*common.Reference)(dec.Reference)
itx.Memo = (*[]byte)(dec.Memo)
if dec.Input == nil {
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
index eb7779cb4..dc0a59340 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -1621,10 +1621,13 @@ func NewRPCTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber
feeTokenID := hexutil.Uint16(tx.FeeTokenID())
result.FeeTokenID = &feeTokenID
result.FeeLimit = (*hexutil.Big)(tx.FeeLimit())
- version := hexutil.Uint64(tx.Version())
- result.Version = &version
- result.Reference = (*common.Reference)(tx.Reference())
- result.Memo = (*hexutil.Bytes)(tx.Memo())
+ // 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())
diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go
index de98870ce..bb9e971dd 100644
--- a/internal/ethapi/transaction_args.go
+++ b/internal/ethapi/transaction_args.go
@@ -214,6 +214,31 @@ 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
+ }
+ }
+ // Set default version based on fork status when not explicitly specified
+ if args.Version == nil {
+ if isJadeFork {
+ 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")
@@ -222,17 +247,7 @@ func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend) error {
if err := args.validateMorphTxVersion(); err != nil {
return err
}
- // Reject MorphTx V1 before jade fork is active
- if args.isMorphTxArgs() && !b.ChainConfig().IsJadeFork(head.Time) {
- // Determine the effective version (default to V1 if not specified)
- version := types.MorphTxVersion1
- if args.Version != nil {
- version = uint8(*args.Version)
- }
- if version == types.MorphTxVersion1 {
- return types.ErrMorphTxV1NotYetActive
- }
- }
+
// Estimate the gas usage if necessary.
if args.Gas == nil {
// These fields are immutable during the estimation, safe to
@@ -401,8 +416,7 @@ func (args *TransactionArgs) ToMessage(globalGasCap uint64, baseFee *big.Int) (t
if args.FeeLimit != nil {
feeLimit = args.FeeLimit.ToInt()
}
- // Default to version 1 if version is not explicitly specified
- version = types.MorphTxVersion1
+ // Use version from args (set by setDefaults or explicitly by caller)
if args.Version != nil {
version = uint8(*args.Version)
}
@@ -491,8 +505,8 @@ func (args *TransactionArgs) toTransaction() *types.Transaction {
if args.AccessList != nil {
al = *args.AccessList
}
- // Default to version 1 if version is not explicitly specified
- version := types.MorphTxVersion1
+ // Use version from args (set by setDefaults or explicitly by caller)
+ var version uint8
if args.Version != nil {
version = uint8(*args.Version)
}
From a805e930447c05cc97ab54390985fd54b3fc5d22 Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Fri, 27 Feb 2026 11:59:37 +0800
Subject: [PATCH 22/33] fix sth
---
core/blockchain.go | 21 ++++++++++++++++++++-
core/rawdb/accessors_chain.go | 6 +++---
core/types/morph_tx.go | 3 +--
3 files changed, 24 insertions(+), 6 deletions(-)
diff --git a/core/blockchain.go b/core/blockchain.go
index 923c3073a..5342183cc 100644
--- a/core/blockchain.go
+++ b/core/blockchain.go
@@ -1985,9 +1985,28 @@ 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())
}
+ // Clean up reference index entries for truly deleted MorphTx transactions.
+ // We need the block context (timestamp, tx index) so we iterate oldChain.
+ if len(trulyDeletedTxs) > 0 {
+ trulyDeletedSet := make(map[common.Hash]struct{}, len(trulyDeletedTxs))
+ for _, tx := range trulyDeletedTxs {
+ trulyDeletedSet[tx.Hash()] = struct{}{}
+ }
+ for _, block := range oldChain {
+ for txIdx, tx := range block.Transactions() {
+ if _, ok := trulyDeletedSet[tx.Hash()]; !ok {
+ continue
+ }
+ if tx.IsMorphTx() && tx.Reference() != nil {
+ rawdb.DeleteReferenceIndexEntry(indexesBatch, *tx.Reference(), block.Time(), uint64(txIdx), tx.Hash())
+ }
+ }
+ }
+ }
// Delete any canonical number assignments above the new head
number := bc.CurrentBlock().NumberU64()
for i := number + 1; ; i++ {
diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go
index b2d54b760..f69d0bd80 100644
--- a/core/rawdb/accessors_chain.go
+++ b/core/rawdb/accessors_chain.go
@@ -639,9 +639,9 @@ type storedReceiptRLP struct {
FeeRate *big.Int
TokenScale *big.Int
FeeLimit *big.Int
- Version byte
- Reference *common.Reference
- Memo []byte
+ 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/types/morph_tx.go b/core/types/morph_tx.go
index 17b577594..c524d6e84 100644
--- a/core/types/morph_tx.go
+++ b/core/types/morph_tx.go
@@ -109,7 +109,6 @@ func (tx *MorphTx) copy() TxData {
Memo: copyBytesPtr(tx.Memo),
// These are copied below.
AccessList: make(AccessList, len(tx.AccessList)),
- FeeLimit: new(big.Int),
Value: new(big.Int),
ChainID: new(big.Int),
GasTipCap: new(big.Int),
@@ -132,7 +131,7 @@ func (tx *MorphTx) copy() TxData {
cpy.GasFeeCap.Set(tx.GasFeeCap)
}
if tx.FeeLimit != nil {
- cpy.FeeLimit.Set(tx.FeeLimit)
+ cpy.FeeLimit = new(big.Int).Set(tx.FeeLimit)
}
if tx.V != nil {
cpy.V.Set(tx.V)
From 711985cf21a961cc82e87c99319829defd8a08fd Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Fri, 27 Feb 2026 15:20:17 +0800
Subject: [PATCH 23/33] fix reference validate
---
core/types/morph_tx.go | 3 +++
1 file changed, 3 insertions(+)
diff --git a/core/types/morph_tx.go b/core/types/morph_tx.go
index c524d6e84..b193cd1ed 100644
--- a/core/types/morph_tx.go
+++ b/core/types/morph_tx.go
@@ -278,6 +278,9 @@ func decodeV1MorphTxRLP(tx *MorphTx, blob []byte) error {
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
From 93631251e7df28149ce7990ef6063a878a00ad58 Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Fri, 27 Feb 2026 15:21:55 +0800
Subject: [PATCH 24/33] fix memo validate
---
core/types/morph_tx.go | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/core/types/morph_tx.go b/core/types/morph_tx.go
index b193cd1ed..6e0143d5b 100644
--- a/core/types/morph_tx.go
+++ b/core/types/morph_tx.go
@@ -285,7 +285,10 @@ func decodeV1MorphTxRLP(tx *MorphTx, blob []byte) error {
ref := common.BytesToReference(v1.Reference)
tx.Reference = &ref
}
- // Convert []byte to *[]byte
+ // 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
}
From 7822427760bb89f7d2a499d98a7963ae6548baf2 Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Fri, 27 Feb 2026 15:26:08 +0800
Subject: [PATCH 25/33] fix tx filter
---
core/tx_pool.go | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/core/tx_pool.go b/core/tx_pool.go
index 83fd79549..76f96024e 100644
--- a/core/tx_pool.go
+++ b/core/tx_pool.go
@@ -1667,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.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
@@ -1682,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 {
From df6a3d6fbc3a5d35a249ae8fd13b9c0aa7f67ae3 Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Sat, 28 Feb 2026 11:26:37 +0800
Subject: [PATCH 26/33] fix rpc and ref index
---
core/blockchain.go | 28 +++---
core/blockchain_test.go | 108 ++++++++++++++++++++++++
core/rawdb/accessors_reference_index.go | 83 +++++++++++++-----
internal/ethapi/api.go | 19 ++---
4 files changed, 184 insertions(+), 54 deletions(-)
diff --git a/core/blockchain.go b/core/blockchain.go
index 5342183cc..2205a00f3 100644
--- a/core/blockchain.go
+++ b/core/blockchain.go
@@ -1109,7 +1109,7 @@ 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
@@ -1989,23 +1989,15 @@ func (bc *BlockChain) reorg(oldBlock, newBlock *types.Block) error {
for _, tx := range trulyDeletedTxs {
rawdb.DeleteTxLookupEntry(indexesBatch, tx.Hash())
}
- // Clean up reference index entries for truly deleted MorphTx transactions.
- // We need the block context (timestamp, tx index) so we iterate oldChain.
- if len(trulyDeletedTxs) > 0 {
- trulyDeletedSet := make(map[common.Hash]struct{}, len(trulyDeletedTxs))
- for _, tx := range trulyDeletedTxs {
- trulyDeletedSet[tx.Hash()] = struct{}{}
- }
- for _, block := range oldChain {
- for txIdx, tx := range block.Transactions() {
- if _, ok := trulyDeletedSet[tx.Hash()]; !ok {
- continue
- }
- if tx.IsMorphTx() && tx.Reference() != nil {
- rawdb.DeleteReferenceIndexEntry(indexesBatch, *tx.Reference(), block.Time(), uint64(txIdx), 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()
diff --git a/core/blockchain_test.go b/core/blockchain_test.go
index ad04a98a8..d06918993 100644
--- a/core/blockchain_test.go
+++ b/core/blockchain_test.go
@@ -3773,6 +3773,28 @@ func TestReferenceIndexBasicOperations(t *testing.T) {
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)
@@ -4061,3 +4083,89 @@ func TestReferenceIndicesMultipleTxsSameReference(t *testing.T) {
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) {
+ var (
+ db = 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(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, params.TestChainConfig, 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)
+ }
+
+ entries = rawdb.ReadReferenceIndexEntries(chain.db, ref)
+ if len(entries) != 1 {
+ t.Fatalf("expected exactly 1 entry after reorg cleanup, got %d", 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/rawdb/accessors_reference_index.go b/core/rawdb/accessors_reference_index.go
index c7531df99..b07456420 100644
--- a/core/rawdb/accessors_reference_index.go
+++ b/core/rawdb/accessors_reference_index.go
@@ -18,6 +18,7 @@ package rawdb
import (
"encoding/binary"
+ "math"
"math/big"
"github.com/morph-l2/go-ethereum/common"
@@ -73,42 +74,81 @@ func DeleteReferenceIndexEntriesForBlock(db ethdb.KeyValueWriter, block *types.B
}
}
-// ReadReferenceIndexEntries returns all transaction entries for a given reference.
+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).
-func ReadReferenceIndexEntries(db ethdb.Database, reference common.Reference) []ReferenceIndexEntry {
+// 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()
- var entries []ReferenceIndexEntry
+ end := uint64(math.MaxUint64)
+ if offset <= math.MaxUint64-limit {
+ end = offset + limit
+ }
+ entries := make([]ReferenceIndexEntry, 0, limit)
+ index := uint64(0)
for it.Next() {
- key := it.Key()
- // 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 {
+ select {
+ case <-interrupt:
+ return nil, true
+ default:
+ }
+ entry, ok := parseReferenceIndexEntry(it.Key())
+ if !ok {
continue
}
-
- 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:])
-
- entries = append(entries, ReferenceIndexEntry{
- BlockTimestamp: blockTimestamp,
- TxIndex: txIndex,
- TxHash: txHash,
- })
+ if index < offset {
+ index++
+ continue
+ }
+ if index >= end {
+ break
+ }
+ entries = append(entries, entry)
+ index++
}
-
if it.Error() != nil {
- log.Error("Failed to iterate reference index entries", "reference", reference.Hex(), "err", it.Error())
+ 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)
@@ -125,4 +165,3 @@ func WriteReferenceIndexTail(db ethdb.KeyValueWriter, blockNumber uint64) {
log.Crit("Failed to store reference index tail", "err", err)
}
}
-
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
index dc0a59340..9a733fb8a 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -2502,22 +2502,13 @@ func (s *PublicMorphAPI) GetTransactionHashesByReference(
return nil, errors.New("limit exceeds maximum value of 100")
}
- entries := rawdb.ReadReferenceIndexEntries(s.b.ChainDb(), args.Reference)
- if len(entries) == 0 {
- return nil, nil
- }
-
- // Validate offset
- if offsetVal >= uint64(len(entries)) {
- return nil, fmt.Errorf("offset %d exceeds total results %d", offsetVal, len(entries))
+ paginatedEntries, interrupted := rawdb.ReadReferenceIndexEntriesPaginatedWithInterrupt(s.b.ChainDb(), args.Reference, offsetVal, limitVal, ctx.Done())
+ if interrupted {
+ return nil, ctx.Err()
}
-
- // Apply pagination
- end := offsetVal + limitVal
- if end > uint64(len(entries)) {
- end = uint64(len(entries))
+ if len(paginatedEntries) == 0 {
+ return nil, nil
}
- paginatedEntries := entries[offsetVal:end]
// Build result
result := make([]rpc.ReferenceTransactionResult, 0, len(paginatedEntries))
From f006b23be79f8bfdf7381633b443904edd4d2d9b Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Sat, 28 Feb 2026 11:42:09 +0800
Subject: [PATCH 27/33] fix
---
core/blockchain.go | 13 +
core/blockchain_test.go | 14 +-
core/rawdb/accessors_reference_index.go | 7 +-
core/rawdb/accessors_reference_index_test.go | 261 +++++++++++++++++++
4 files changed, 291 insertions(+), 4 deletions(-)
create mode 100644 core/rawdb/accessors_reference_index_test.go
diff --git a/core/blockchain.go b/core/blockchain.go
index 2205a00f3..068611257 100644
--- a/core/blockchain.go
+++ b/core/blockchain.go
@@ -775,6 +775,19 @@ 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)
+ }
+ }
+
rawdb.WriteHeadHeaderHash(batch, block.Hash())
rawdb.WriteHeadFastBlockHash(batch, block.Hash())
rawdb.WriteCanonicalHash(batch, block.Hash(), block.NumberU64())
diff --git a/core/blockchain_test.go b/core/blockchain_test.go
index d06918993..48b4bccfa 100644
--- a/core/blockchain_test.go
+++ b/core/blockchain_test.go
@@ -4087,13 +4087,18 @@ func TestReferenceIndicesMultipleTxsSameReference(t *testing.T) {
// 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: params.TestChainConfig,
+ Config: &jadeCfg,
Alloc: GenesisAlloc{address: {Balance: funds}},
BaseFee: big.NewInt(params.InitialBaseFee),
}
@@ -4135,7 +4140,7 @@ func TestReferenceIndicesReorgCleanup(t *testing.T) {
})
l := uint64(0)
- chain, err := NewBlockChain(db, nil, params.TestChainConfig, ethash.NewFaker(), vm.Config{}, nil, &l)
+ chain, err := NewBlockChain(db, nil, &jadeCfg, ethash.NewFaker(), vm.Config{}, nil, &l)
if err != nil {
t.Fatalf("failed to create tester chain: %v", err)
}
@@ -4157,9 +4162,12 @@ func TestReferenceIndicesReorgCleanup(t *testing.T) {
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", len(entries))
+ 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 {
diff --git a/core/rawdb/accessors_reference_index.go b/core/rawdb/accessors_reference_index.go
index b07456420..65df2f36b 100644
--- a/core/rawdb/accessors_reference_index.go
+++ b/core/rawdb/accessors_reference_index.go
@@ -108,7 +108,12 @@ func ReadReferenceIndexEntriesPaginatedWithInterrupt(db ethdb.Database, referenc
if offset <= math.MaxUint64-limit {
end = offset + limit
}
- entries := make([]ReferenceIndexEntry, 0, 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 {
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")
+ }
+}
From cba42b48a53cce5bc8c5872e8b73c7f07dc3f341 Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Sat, 28 Feb 2026 15:27:22 +0800
Subject: [PATCH 28/33] fix version
---
accounts/abi/bind/base.go | 19 +-
accounts/abi/bind/morph_tx_version_test.go | 183 +++++++
common/types.go | 2 -
core/blockchain.go | 10 +
internal/ethapi/api.go | 26 +-
internal/ethapi/api_morph_test.go | 576 +++++++++++++++++++++
internal/ethapi/transaction_args.go | 6 +-
rollup/tracing/tracing.go | 23 +-
signer/core/apitypes/types.go | 7 +-
signer/core/apitypes/types_test.go | 219 ++++++++
10 files changed, 1045 insertions(+), 26 deletions(-)
create mode 100644 accounts/abi/bind/morph_tx_version_test.go
create mode 100644 internal/ethapi/api_morph_test.go
create mode 100644 signer/core/apitypes/types_test.go
diff --git a/accounts/abi/bind/base.go b/accounts/abi/bind/base.go
index fdf276455..2f7426a60 100644
--- a/accounts/abi/bind/base.go
+++ b/accounts/abi/bind/base.go
@@ -367,19 +367,28 @@ func (c *BoundContract) createMorphTx(opts *TransactOpts, contract *common.Addre
// - Version 1: FeeTokenID, Reference, Memo are all optional;
// if FeeTokenID is 0, FeeLimit must not be set
//
-// If version is not explicitly specified, default to the highest version.
+// 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, use the highest version
+ // 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 {
- if opts.FeeTokenID == 0 && opts.FeeLimit != nil && opts.FeeLimit.Sign() != 0 {
- return 0, types.ErrMorphTxV1IllegalExtraParams
+ 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.MorphTxVersion1, nil
+ return types.MorphTxVersion0, nil
}
// Version explicitly specified - validate parameters match
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/common/types.go b/common/types.go
index 3b2b3c7c4..a70f2162b 100644
--- a/common/types.go
+++ b/common/types.go
@@ -164,8 +164,6 @@ func (r Reference) MarshalText() ([]byte, error) {
return hexutil.Bytes(r[:]).MarshalText()
}
-// MarshalJSON marshals the original value
-
// MarshalJSON marshals the original value
func (r Reference) MarshalJSON() ([]byte, error) {
return json.Marshal(r.Hex())
diff --git a/core/blockchain.go b/core/blockchain.go
index 068611257..47f8c5202 100644
--- a/core/blockchain.go
+++ b/core/blockchain.go
@@ -623,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 {
@@ -785,6 +792,9 @@ func (bc *BlockChain) writeHeadBlock(block *types.Block) {
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)
}
}
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
index 9a733fb8a..08d82c6d2 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -1995,13 +1995,21 @@ func marshalReceipt(ctx context.Context, b Backend, receipt *types.Receipt, bigb
"logsBloom": receipt.Bloom,
"type": hexutil.Uint(tx.Type()),
"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),
+ }
+
+ // Include MorphTx-specific fields only for MorphTx transactions,
+ // consistent with NewRPCTransaction behavior.
+ if tx.Type() == types.MorphTxType {
+ fields["feeRate"] = (*hexutil.Big)(receipt.FeeRate)
+ fields["tokenScale"] = (*hexutil.Big)(receipt.TokenScale)
+ fields["feeTokenID"] = (*hexutil.Uint16)(receipt.FeeTokenID)
+ fields["feeLimit"] = (*hexutil.Big)(receipt.FeeLimit)
+ // Only include V1 fields (version, reference, memo) for V1+ transactions
+ if tx.Version() >= types.MorphTxVersion1 {
+ fields["version"] = hexutil.Uint(receipt.Version)
+ fields["reference"] = (*common.Reference)(receipt.Reference)
+ fields["memo"] = (*hexutil.Bytes)(receipt.Memo)
+ }
}
// Assign the effective gas price paid
@@ -2501,6 +2509,10 @@ func (s *PublicMorphAPI) GetTransactionHashesByReference(
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 {
diff --git a/internal/ethapi/api_morph_test.go b/internal/ethapi/api_morph_test.go
new file mode 100644
index 000000000..4fa896386
--- /dev/null
+++ b/internal/ethapi/api_morph_test.go
@@ -0,0 +1,576 @@
+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),
+ },
+ }
+
+ 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 MorphTx-specific fields are
+// conditionally included in marshalled receipts:
+// - Non-MorphTx: no MorphTx fields at all
+// - MorphTx V0: feeTokenID, feeLimit, feeRate, tokenScale present; version/reference/memo absent
+// - MorphTx V1: all MorphTx fields present
+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
+ // expected field presence
+ expectMorphTxFields bool // feeTokenID, feeLimit, feeRate, tokenScale
+ expectV1Fields bool // version, reference, memo
+ }{
+ {
+ 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),
+ }),
+ expectMorphTxFields: false,
+ expectV1Fields: false,
+ },
+ {
+ 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,
+ expectMorphTxFields: true,
+ expectV1Fields: false,
+ },
+ {
+ 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,
+ expectMorphTxFields: true,
+ expectV1Fields: true,
+ },
+ }
+
+ backend := &mockReceiptBackend{}
+ blockHash := common.HexToHash("0xdeadbeef")
+ bigblock := big.NewInt(1)
+
+ 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)
+ }
+
+ // MorphTx-specific fields (feeTokenID, feeLimit, feeRate, tokenScale)
+ morphFields := []string{"feeTokenID", "feeLimit", "feeRate", "tokenScale"}
+ for _, field := range morphFields {
+ _, exists := fields[field]
+ if tt.expectMorphTxFields && !exists {
+ t.Errorf("expected field %q to be present for %s, but it was absent", field, tt.name)
+ }
+ if !tt.expectMorphTxFields && exists {
+ t.Errorf("expected field %q to be absent for %s, but it was present", field, tt.name)
+ }
+ }
+
+ // V1-specific fields (version, reference, memo)
+ v1Fields := []string{"version", "reference", "memo"}
+ for _, field := range v1Fields {
+ _, exists := fields[field]
+ if tt.expectV1Fields && !exists {
+ t.Errorf("expected field %q to be present for %s, but it was absent", field, tt.name)
+ }
+ if !tt.expectV1Fields && exists {
+ t.Errorf("expected field %q to be absent for %s, but it was present", field, tt.name)
+ }
+ }
+
+ // l1Fee should always be present (common Morph field)
+ if _, exists := fields["l1Fee"]; !exists {
+ t.Errorf("expected field 'l1Fee' to always be present, but it was absent")
+ }
+ })
+ }
+}
diff --git a/internal/ethapi/transaction_args.go b/internal/ethapi/transaction_args.go
index bb9e971dd..30c5cb2c4 100644
--- a/internal/ethapi/transaction_args.go
+++ b/internal/ethapi/transaction_args.go
@@ -228,9 +228,11 @@ func (args *TransactionArgs) setDefaults(ctx context.Context, b Backend) error {
return types.ErrMorphTxV1NotYetActive
}
}
- // Set default version based on fork status when not explicitly specified
+ // Determine version: explicit > V1 if V1-specific fields present > V0 (backward compatible)
if args.Version == nil {
- if isJadeFork {
+ 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 {
diff --git a/rollup/tracing/tracing.go b/rollup/tracing/tracing.go
index 41bd52374..bc7ca443d 100644
--- a/rollup/tracing/tracing.go
+++ b/rollup/tracing/tracing.go
@@ -516,24 +516,31 @@ func (env *TraceEnv) getTxResult(statedb *state.StateDB, index int, block *types
}
getTxResultTracerResultTimer.UpdateSince(tracerResultTimer)
- env.ExecutionResults[index] = &types.ExecutionResult{
+ execResult := &types.ExecutionResult{
From: sender,
To: receiver,
AccountCreated: createdAcc,
AccountsAfter: after,
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,
Failed: receipt.Status == types.ReceiptStatusFailed,
ReturnValue: fmt.Sprintf("%x", receipt.ReturnValue),
CallTrace: callTrace,
}
+ // Include MorphTx-specific fields only for MorphTx transactions
+ if tx.Type() == types.MorphTxType {
+ execResult.FeeTokenID = receipt.FeeTokenID
+ execResult.FeeLimit = (*hexutil.Big)(receipt.FeeLimit)
+ execResult.FeeRate = (*hexutil.Big)(receipt.FeeRate)
+ execResult.TokenScale = (*hexutil.Big)(receipt.TokenScale)
+ // Only include V1 fields for V1+ transactions
+ if tx.Version() >= types.MorphTxVersion1 {
+ execResult.Version = &receipt.Version
+ execResult.Reference = receipt.Reference
+ execResult.Memo = (*hexutil.Bytes)(receipt.Memo)
+ }
+ }
+ env.ExecutionResults[index] = execResult
return nil
}
diff --git a/signer/core/apitypes/types.go b/signer/core/apitypes/types.go
index f790e772a..a6ba9b0c3 100644
--- a/signer/core/apitypes/types.go
+++ b/signer/core/apitypes/types.go
@@ -129,10 +129,13 @@ func (args *SendTxArgs) ToTransaction() *types.Transaction {
if args.AccessList != nil {
al = *args.AccessList
}
- // Default to version 1 if version is not explicitly specified
- version := uint8(types.MorphTxVersion1)
+ // 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 {
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)
+ }
+ }
+ })
+ }
+}
From 1887bfa7b338c15b34851856312bad127e1ad089 Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Sat, 28 Feb 2026 17:24:05 +0800
Subject: [PATCH 29/33] add validate
---
core/types/transaction.go | 5 +
core/types/transaction_test.go | 210 +++++++++++++++++++++++++++++++++
2 files changed, 215 insertions(+)
diff --git a/core/types/transaction.go b/core/types/transaction.go
index 3856510fd..74eda4c44 100644
--- a/core/types/transaction.go
+++ b/core/types/transaction.go
@@ -931,6 +931,11 @@ func (tx *Transaction) AsMessage(s Signer, baseFee *big.Int) (Message, error) {
if baseFee != nil {
msg.gasPrice = math.BigMin(msg.gasPrice.Add(msg.gasTipCap, baseFee), msg.gasFeeCap)
}
+
+ if err := tx.ValidateMorphTxVersion(); err != nil {
+ return Message{}, err
+ }
+
if tx.FeeLimit() != nil {
msg.feeLimit = tx.FeeLimit()
}
diff --git a/core/types/transaction_test.go b/core/types/transaction_test.go
index 6270a86ae..157d59928 100644
--- a/core/types/transaction_test.go
+++ b/core/types/transaction_test.go
@@ -993,6 +993,216 @@ func TestMorphTxValidation(t *testing.T) {
})
}
+// 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")
From ddd20a802cd6d94d5a982ff543740a1ac3b8a3a0 Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Tue, 3 Mar 2026 14:24:27 +0800
Subject: [PATCH 30/33] update returns receipt & result
---
internal/ethapi/api.go | 22 ++++------
internal/ethapi/api_morph_test.go | 69 +++++++++----------------------
rollup/tracing/tracing.go | 23 ++++-------
3 files changed, 35 insertions(+), 79 deletions(-)
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
index 08d82c6d2..1fcd600ec 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -1995,21 +1995,13 @@ func marshalReceipt(ctx context.Context, b Backend, receipt *types.Receipt, bigb
"logsBloom": receipt.Bloom,
"type": hexutil.Uint(tx.Type()),
"l1Fee": (*hexutil.Big)(receipt.L1Fee),
- }
-
- // Include MorphTx-specific fields only for MorphTx transactions,
- // consistent with NewRPCTransaction behavior.
- if tx.Type() == types.MorphTxType {
- fields["feeRate"] = (*hexutil.Big)(receipt.FeeRate)
- fields["tokenScale"] = (*hexutil.Big)(receipt.TokenScale)
- fields["feeTokenID"] = (*hexutil.Uint16)(receipt.FeeTokenID)
- fields["feeLimit"] = (*hexutil.Big)(receipt.FeeLimit)
- // Only include V1 fields (version, reference, memo) for V1+ transactions
- if tx.Version() >= types.MorphTxVersion1 {
- fields["version"] = hexutil.Uint(receipt.Version)
- fields["reference"] = (*common.Reference)(receipt.Reference)
- fields["memo"] = (*hexutil.Bytes)(receipt.Memo)
- }
+ "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),
}
// Assign the effective gas price paid
diff --git a/internal/ethapi/api_morph_test.go b/internal/ethapi/api_morph_test.go
index 4fa896386..ac6be8b6c 100644
--- a/internal/ethapi/api_morph_test.go
+++ b/internal/ethapi/api_morph_test.go
@@ -424,11 +424,10 @@ func signTx(t *testing.T, key *ecdsa.PrivateKey, signer types.Signer, inner type
return tx
}
-// TestMarshalReceipt_FieldPresence verifies that MorphTx-specific fields are
-// conditionally included in marshalled receipts:
-// - Non-MorphTx: no MorphTx fields at all
-// - MorphTx V0: feeTokenID, feeLimit, feeRate, tokenScale present; version/reference/memo absent
-// - MorphTx V1: all MorphTx fields present
+// 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")
@@ -452,9 +451,6 @@ func TestMarshalReceipt_FieldPresence(t *testing.T) {
receiptVersion uint8
receiptReference *common.Reference
receiptMemo *[]byte
- // expected field presence
- expectMorphTxFields bool // feeTokenID, feeLimit, feeRate, tokenScale
- expectV1Fields bool // version, reference, memo
}{
{
name: "DynamicFeeTx (non-MorphTx)",
@@ -467,8 +463,6 @@ func TestMarshalReceipt_FieldPresence(t *testing.T) {
To: &testAddr,
Value: big.NewInt(1),
}),
- expectMorphTxFields: false,
- expectV1Fields: false,
},
{
name: "MorphTx V0",
@@ -489,8 +483,6 @@ func TestMarshalReceipt_FieldPresence(t *testing.T) {
receiptTokenScale: tokenScale,
receiptFeeLimit: big.NewInt(1000),
receiptVersion: types.MorphTxVersion0,
- expectMorphTxFields: true,
- expectV1Fields: false,
},
{
name: "MorphTx V1",
@@ -514,8 +506,6 @@ func TestMarshalReceipt_FieldPresence(t *testing.T) {
receiptVersion: types.MorphTxVersion1,
receiptReference: &ref,
receiptMemo: &memo,
- expectMorphTxFields: true,
- expectV1Fields: true,
},
}
@@ -523,18 +513,21 @@ func TestMarshalReceipt_FieldPresence(t *testing.T) {
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,
+ 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))
@@ -543,34 +536,12 @@ func TestMarshalReceipt_FieldPresence(t *testing.T) {
t.Fatalf("marshalReceipt error: %v", err)
}
- // MorphTx-specific fields (feeTokenID, feeLimit, feeRate, tokenScale)
- morphFields := []string{"feeTokenID", "feeLimit", "feeRate", "tokenScale"}
- for _, field := range morphFields {
- _, exists := fields[field]
- if tt.expectMorphTxFields && !exists {
- t.Errorf("expected field %q to be present for %s, but it was absent", field, tt.name)
- }
- if !tt.expectMorphTxFields && exists {
- t.Errorf("expected field %q to be absent for %s, but it was present", field, tt.name)
- }
- }
-
- // V1-specific fields (version, reference, memo)
- v1Fields := []string{"version", "reference", "memo"}
- for _, field := range v1Fields {
- _, exists := fields[field]
- if tt.expectV1Fields && !exists {
- t.Errorf("expected field %q to be present for %s, but it was absent", field, tt.name)
- }
- if !tt.expectV1Fields && exists {
- t.Errorf("expected field %q to be absent for %s, but it was present", field, tt.name)
+ // 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)
}
}
-
- // l1Fee should always be present (common Morph field)
- if _, exists := fields["l1Fee"]; !exists {
- t.Errorf("expected field 'l1Fee' to always be present, but it was absent")
- }
})
}
}
diff --git a/rollup/tracing/tracing.go b/rollup/tracing/tracing.go
index bc7ca443d..41bd52374 100644
--- a/rollup/tracing/tracing.go
+++ b/rollup/tracing/tracing.go
@@ -516,31 +516,24 @@ func (env *TraceEnv) getTxResult(statedb *state.StateDB, index int, block *types
}
getTxResultTracerResultTimer.UpdateSince(tracerResultTimer)
- execResult := &types.ExecutionResult{
+ env.ExecutionResults[index] = &types.ExecutionResult{
From: sender,
To: receiver,
AccountCreated: createdAcc,
AccountsAfter: after,
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,
Failed: receipt.Status == types.ReceiptStatusFailed,
ReturnValue: fmt.Sprintf("%x", receipt.ReturnValue),
CallTrace: callTrace,
}
- // Include MorphTx-specific fields only for MorphTx transactions
- if tx.Type() == types.MorphTxType {
- execResult.FeeTokenID = receipt.FeeTokenID
- execResult.FeeLimit = (*hexutil.Big)(receipt.FeeLimit)
- execResult.FeeRate = (*hexutil.Big)(receipt.FeeRate)
- execResult.TokenScale = (*hexutil.Big)(receipt.TokenScale)
- // Only include V1 fields for V1+ transactions
- if tx.Version() >= types.MorphTxVersion1 {
- execResult.Version = &receipt.Version
- execResult.Reference = receipt.Reference
- execResult.Memo = (*hexutil.Bytes)(receipt.Memo)
- }
- }
- env.ExecutionResults[index] = execResult
return nil
}
From 25b00483bf8e055b8108f39087aaa3142136ba21 Mon Sep 17 00:00:00 2001
From: panos
Date: Mon, 2 Mar 2026 19:23:05 +0800
Subject: [PATCH 31/33] fix: use hexutil.Uint64 for MorphTx version JSON
encoding (#293)
txJSON.Version was *uint8 (plain number) while RPCTransaction.Version
uses *hexutil.Uint64 (hex string). This caused ethclient.BlockByNumber
to fail with "cannot unmarshal string into Go struct field
rpcBlock.transactions.version of type uint8" on non-sequencer nodes.
Align txJSON.Version with the Ethereum JSON-RPC convention by using
*hexutil.Uint64, consistent with all other numeric fields (FeeTokenID,
QueueIndex, etc.).
---
core/types/transaction_marshalling.go | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/core/types/transaction_marshalling.go b/core/types/transaction_marshalling.go
index dbf285279..bf5e72e08 100644
--- a/core/types/transaction_marshalling.go
+++ b/core/types/transaction_marshalling.go
@@ -65,7 +65,7 @@ type txJSON struct {
// MorphTx transaction fields:
FeeTokenID hexutil.Uint16 `json:"feeTokenID"`
FeeLimit *hexutil.Big `json:"feeLimit"`
- Version *uint8 `json:"version"`
+ Version *hexutil.Uint64 `json:"version"`
Reference *common.Reference `json:"reference"`
Memo *hexutil.Bytes `json:"memo"`
}
@@ -206,7 +206,8 @@ func (tx *Transaction) MarshalJSON() ([]byte, error) {
enc.FeeLimit = (*hexutil.Big)(itx.FeeLimit)
// Only include V1 fields (version, reference, memo) for V1+ transactions
if itx.Version >= MorphTxVersion1 {
- enc.Version = (*uint8)(&itx.Version)
+ v := hexutil.Uint64(itx.Version)
+ enc.Version = &v
enc.Reference = (*common.Reference)(itx.Reference)
enc.Memo = (*hexutil.Bytes)(itx.Memo)
}
@@ -609,7 +610,7 @@ func (tx *Transaction) UnmarshalJSON(input []byte) error {
itx.FeeLimit = (*big.Int)(dec.FeeLimit)
itx.Value = (*big.Int)(dec.Value)
if dec.Version != nil {
- itx.Version = *dec.Version
+ itx.Version = uint8(*dec.Version)
}
itx.Reference = (*common.Reference)(dec.Reference)
itx.Memo = (*[]byte)(dec.Memo)
From 7892e9b50be973e29811ce20711cc68a26be1263 Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Tue, 3 Mar 2026 19:41:54 +0800
Subject: [PATCH 32/33] fix hash
---
core/types/morph_tx.go | 13 ++
core/types/morph_tx_compat_test.go | 198 +++++++++++++++++++++++++++++
2 files changed, 211 insertions(+)
diff --git a/core/types/morph_tx.go b/core/types/morph_tx.go
index 6e0143d5b..d0e7a906b 100644
--- a/core/types/morph_tx.go
+++ b/core/types/morph_tx.go
@@ -3,6 +3,7 @@ package types
import (
"bytes"
"errors"
+ "io"
"math/big"
"strconv"
@@ -177,6 +178,18 @@ 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:
diff --git a/core/types/morph_tx_compat_test.go b/core/types/morph_tx_compat_test.go
index 2f3918e4f..464fcc96b 100644
--- a/core/types/morph_tx_compat_test.go
+++ b/core/types/morph_tx_compat_test.go
@@ -7,6 +7,8 @@ import (
"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
@@ -393,3 +395,199 @@ func TestMorphTxVersionDetection(t *testing.T) {
}
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))
+ }
+ })
+ }
+}
From dd7be943b13da83cb674ff8266f0c1d300f2fc8c Mon Sep 17 00:00:00 2001
From: segue <“huoda.china@gmail.com”>
Date: Thu, 5 Mar 2026 11:51:23 +0800
Subject: [PATCH 33/33] add version check and test
---
internal/ethapi/api_morph_test.go | 82 +++++++++++++++++++++++++++++++
light/txpool.go | 5 ++
2 files changed, 87 insertions(+)
diff --git a/internal/ethapi/api_morph_test.go b/internal/ethapi/api_morph_test.go
index ac6be8b6c..e2c5c0e98 100644
--- a/internal/ethapi/api_morph_test.go
+++ b/internal/ethapi/api_morph_test.go
@@ -357,6 +357,88 @@ func TestSetDefaults_MorphTxVersionHeuristic(t *testing.T) {
},
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 {
diff --git a/light/txpool.go b/light/txpool.go
index 2b737dd34..0756c2338 100644
--- a/light/txpool.go
+++ b/light/txpool.go
@@ -412,6 +412,11 @@ 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