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