From 6f1bf68a7baf61550b154bcf0321da752caf0803 Mon Sep 17 00:00:00 2001 From: Bui Quang Minh Date: Wed, 25 Oct 2023 15:00:56 +0700 Subject: [PATCH] core/types: add new sponsored transaction types This commit adds new sponsored transaction type 0x64 (100) following EIP-2718: Typed Transaction Envelope. The new transaction type is the same as legacy with additional expiredTime and payer's signature field. --- accounts/abi/bind/backends/simulated.go | 4 + core/types/access_list_tx.go | 5 + core/types/dynamic_fee_tx.go | 5 + core/types/legacy_tx.go | 5 + core/types/receipt.go | 11 +- core/types/receipt_test.go | 47 +++++ core/types/sponsored_tx.go | 99 +++++++++ core/types/transaction.go | 135 ++++++++---- core/types/transaction_marshalling.go | 270 ++++++++++++------------ core/types/transaction_signing.go | 207 ++++++++++++++++-- core/types/transaction_signing_test.go | 168 +++++++++++++++ core/types/transaction_test.go | 19 +- ethclient/signer.go | 3 + internal/ethapi/api.go | 25 ++- 14 files changed, 810 insertions(+), 193 deletions(-) create mode 100644 core/types/sponsored_tx.go diff --git a/accounts/abi/bind/backends/simulated.go b/accounts/abi/bind/backends/simulated.go index 5fd3f4600a..15e865b3cf 100644 --- a/accounts/abi/bind/backends/simulated.go +++ b/accounts/abi/bind/backends/simulated.go @@ -811,6 +811,10 @@ func (m callMsg) Value() *big.Int { return m.CallMsg.Value } func (m callMsg) Data() []byte { return m.CallMsg.Data } func (m callMsg) AccessList() types.AccessList { return m.CallMsg.AccessList } +// FIXME: support sponsored transaction in callMsg +func (m callMsg) Payer() common.Address { return m.CallMsg.From } +func (m callMsg) ExpiredTime() uint64 { return 0 } + // filterBackend implements filters.Backend to support filtering for logs without // taking bloom-bits acceleration structures into account. type filterBackend struct { diff --git a/core/types/access_list_tx.go b/core/types/access_list_tx.go index ee5f194b77..90ea043cb2 100644 --- a/core/types/access_list_tx.go +++ b/core/types/access_list_tx.go @@ -105,6 +105,11 @@ func (tx *AccessListTx) gasFeeCap() *big.Int { return tx.GasPrice } func (tx *AccessListTx) value() *big.Int { return tx.Value } func (tx *AccessListTx) nonce() uint64 { return tx.Nonce } func (tx *AccessListTx) to() *common.Address { return tx.To } +func (tx *AccessListTx) expiredTime() uint64 { return 0 } + +func (tx *AccessListTx) rawPayerSignatureValues() (v, r, s *big.Int) { + return nil, nil, nil +} func (tx *AccessListTx) rawSignatureValues() (v, r, s *big.Int) { return tx.V, tx.R, tx.S diff --git a/core/types/dynamic_fee_tx.go b/core/types/dynamic_fee_tx.go index 585c029d89..ffab45df66 100644 --- a/core/types/dynamic_fee_tx.go +++ b/core/types/dynamic_fee_tx.go @@ -93,6 +93,11 @@ func (tx *DynamicFeeTx) gasPrice() *big.Int { return tx.GasFeeCap } func (tx *DynamicFeeTx) value() *big.Int { return tx.Value } func (tx *DynamicFeeTx) nonce() uint64 { return tx.Nonce } func (tx *DynamicFeeTx) to() *common.Address { return tx.To } +func (tx *DynamicFeeTx) expiredTime() uint64 { return 0 } + +func (tx *DynamicFeeTx) rawPayerSignatureValues() (v, r, s *big.Int) { + return nil, nil, nil +} func (tx *DynamicFeeTx) rawSignatureValues() (v, r, s *big.Int) { return tx.V, tx.R, tx.S diff --git a/core/types/legacy_tx.go b/core/types/legacy_tx.go index cb86bed772..3c2d007be6 100644 --- a/core/types/legacy_tx.go +++ b/core/types/legacy_tx.go @@ -102,6 +102,11 @@ func (tx *LegacyTx) gasFeeCap() *big.Int { return tx.GasPrice } func (tx *LegacyTx) value() *big.Int { return tx.Value } func (tx *LegacyTx) nonce() uint64 { return tx.Nonce } func (tx *LegacyTx) to() *common.Address { return tx.To } +func (tx *LegacyTx) expiredTime() uint64 { return 0 } + +func (tx *LegacyTx) rawPayerSignatureValues() (v, r, s *big.Int) { + return nil, nil, nil +} func (tx *LegacyTx) rawSignatureValues() (v, r, s *big.Int) { return tx.V, tx.R, tx.S diff --git a/core/types/receipt.go b/core/types/receipt.go index c3588990c0..264ccb4515 100644 --- a/core/types/receipt.go +++ b/core/types/receipt.go @@ -192,7 +192,7 @@ func (r *Receipt) DecodeRLP(s *rlp.Stream) error { return errEmptyTypedReceipt } r.Type = b[0] - if r.Type == AccessListTxType || r.Type == DynamicFeeTxType { + if r.Type == AccessListTxType || r.Type == DynamicFeeTxType || r.Type == SponsoredTxType { var dec receiptRLP if err := rlp.DecodeBytes(b[1:], &dec); err != nil { return err @@ -228,7 +228,7 @@ func (r *Receipt) decodeTyped(b []byte) error { return errEmptyTypedReceipt } switch b[0] { - case DynamicFeeTxType, AccessListTxType: + case DynamicFeeTxType, AccessListTxType, SponsoredTxType: var data receiptRLP err := rlp.DecodeBytes(b[1:], &data) if err != nil { @@ -391,11 +391,8 @@ func (rs Receipts) EncodeIndex(i int, w *bytes.Buffer) { switch r.Type { case LegacyTxType: rlp.Encode(w, data) - case AccessListTxType: - w.WriteByte(AccessListTxType) - rlp.Encode(w, data) - case DynamicFeeTxType: - w.WriteByte(DynamicFeeTxType) + case AccessListTxType, DynamicFeeTxType, SponsoredTxType: + w.WriteByte(r.Type) rlp.Encode(w, data) default: // For unsupported types, write nothing. Since this is for diff --git a/core/types/receipt_test.go b/core/types/receipt_test.go index 613559a658..ee9308395e 100644 --- a/core/types/receipt_test.go +++ b/core/types/receipt_test.go @@ -80,6 +80,23 @@ var ( }, Type: DynamicFeeTxType, } + mikoReceipt = &Receipt{ + Status: ReceiptStatusFailed, + CumulativeGasUsed: 1, + Logs: []*Log{ + { + Address: common.BytesToAddress([]byte{0x11}), + Topics: []common.Hash{common.HexToHash("dead"), common.HexToHash("beef")}, + Data: []byte{0x01, 0x00, 0xff}, + }, + { + Address: common.BytesToAddress([]byte{0x01, 0x11}), + Topics: []common.Hash{common.HexToHash("dead"), common.HexToHash("beef")}, + Data: []byte{0x01, 0x00, 0xff}, + }, + }, + Type: SponsoredTxType, + } ) func TestDecodeEmptyTypedReceipt(t *testing.T) { @@ -427,6 +444,25 @@ func TestReceiptMarshalBinary(t *testing.T) { if !bytes.Equal(have, eip1559Want) { t.Errorf("encoded RLP mismatch, got %x want %x", have, eip1559Want) } + + // Miko Receipt + buf.Reset() + mikoReceipt.Bloom = CreateBloom(Receipts{mikoReceipt}) + have, err = mikoReceipt.MarshalBinary() + if err != nil { + t.Fatalf("marshal binary error: %v", err) + } + mikoReceipts := Receipts{mikoReceipt} + mikoReceipts.EncodeIndex(0, buf) + haveEncodeIndex = buf.Bytes() + if !bytes.Equal(have, haveEncodeIndex) { + t.Errorf("BinaryMarshal and EncodeIndex mismatch, got %x want %x", have, haveEncodeIndex) + } + mikoWant := common.FromHex("64f901c58001b9010000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000010000080000000000000000000004000000000000000000000000000040000000000000000000000000000800000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000f8bef85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100fff85d940000000000000000000000000000000000000111f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff") + if !bytes.Equal(have, mikoWant) { + t.Errorf("encoded RLP mismatch, got %x want %x", have, mikoWant) + } + } func TestReceiptUnmarshalBinary(t *testing.T) { @@ -462,6 +498,17 @@ func TestReceiptUnmarshalBinary(t *testing.T) { if !reflect.DeepEqual(got1559Receipt, eip1559Receipt) { t.Errorf("receipt unmarshalled from binary mismatch, got %v want %v", got1559Receipt, eip1559Receipt) } + + // miko Receipt + mikoRctBinary := common.FromHex("64f901c58001b9010000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000010000080000000000000000000004000000000000000000000000000040000000000000000000000000000800000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000f8bef85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100fff85d940000000000000000000000000000000000000111f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff") + gotMikoReceipt := new(Receipt) + if err := gotMikoReceipt.UnmarshalBinary(mikoRctBinary); err != nil { + t.Fatalf("unmarshal binary error: %v", err) + } + mikoReceipt.Bloom = CreateBloom(Receipts{mikoReceipt}) + if !reflect.DeepEqual(gotMikoReceipt, mikoReceipt) { + t.Errorf("receipt unmarshalled from binary mismatch, got %v want %v", gotMikoReceipt, mikoReceipt) + } } func clearComputedFieldsOnReceipts(t *testing.T, receipts Receipts) { diff --git a/core/types/sponsored_tx.go b/core/types/sponsored_tx.go new file mode 100644 index 0000000000..ebf7620e27 --- /dev/null +++ b/core/types/sponsored_tx.go @@ -0,0 +1,99 @@ +package types + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +type SponsoredTx struct { + ChainID *big.Int // destination chain ID + Nonce uint64 // nonce of sender account + GasTipCap *big.Int // maximum tip to the miner + GasFeeCap *big.Int // maximum gas fee want to pay + Gas uint64 // gas limit + To *common.Address `rlp:"nil"` // nil means contract creation + Value *big.Int // wei amount + Data []byte // contract invocation input data + ExpiredTime uint64 // the expired time of payer's signature + PayerV, PayerR, PayerS *big.Int // payer's signature values + V, R, S *big.Int // sender's signature values +} + +func (tx *SponsoredTx) copy() TxData { + cpy := &SponsoredTx{ + Nonce: tx.Nonce, + To: copyAddressPtr(tx.To), + Data: common.CopyBytes(tx.Data), + Gas: tx.Gas, + ExpiredTime: tx.ExpiredTime, + // These are initialized below. + ChainID: new(big.Int), + Value: new(big.Int), + GasTipCap: new(big.Int), + GasFeeCap: new(big.Int), + PayerV: new(big.Int), + PayerR: new(big.Int), + PayerS: new(big.Int), + V: new(big.Int), + R: new(big.Int), + S: new(big.Int), + } + if tx.ChainID != nil { + cpy.ChainID.Set(tx.ChainID) + } + if tx.Value != nil { + cpy.Value.Set(tx.Value) + } + if tx.GasTipCap != nil { + cpy.GasTipCap.Set(tx.GasTipCap) + } + if tx.GasFeeCap != nil { + cpy.GasFeeCap.Set(tx.GasFeeCap) + } + if tx.PayerV != nil { + cpy.PayerV.Set(tx.PayerV) + } + if tx.PayerR != nil { + cpy.PayerR.Set(tx.PayerR) + } + if tx.PayerS != nil { + cpy.PayerS.Set(tx.PayerS) + } + if tx.V != nil { + cpy.V.Set(tx.V) + } + if tx.R != nil { + cpy.R.Set(tx.R) + } + if tx.S != nil { + cpy.S.Set(tx.S) + } + return cpy +} + +// accessors for innerTx. +func (tx *SponsoredTx) txType() byte { return SponsoredTxType } +func (tx *SponsoredTx) chainID() *big.Int { return tx.ChainID } +func (tx *SponsoredTx) accessList() AccessList { return nil } +func (tx *SponsoredTx) data() []byte { return tx.Data } +func (tx *SponsoredTx) gas() uint64 { return tx.Gas } +func (tx *SponsoredTx) gasPrice() *big.Int { return tx.GasFeeCap } +func (tx *SponsoredTx) gasTipCap() *big.Int { return tx.GasTipCap } +func (tx *SponsoredTx) gasFeeCap() *big.Int { return tx.GasFeeCap } +func (tx *SponsoredTx) value() *big.Int { return tx.Value } +func (tx *SponsoredTx) nonce() uint64 { return tx.Nonce } +func (tx *SponsoredTx) to() *common.Address { return tx.To } +func (tx *SponsoredTx) expiredTime() uint64 { return tx.ExpiredTime } + +func (tx *SponsoredTx) rawPayerSignatureValues() (v, r, s *big.Int) { + return tx.PayerV, tx.PayerR, tx.PayerS +} + +func (tx *SponsoredTx) rawSignatureValues() (v, r, s *big.Int) { + return tx.V, tx.R, tx.S +} + +func (tx *SponsoredTx) setSignatureValues(chainID, v, r, s *big.Int) { + tx.V, tx.R, tx.S = v, r, s +} diff --git a/core/types/transaction.go b/core/types/transaction.go index 4e3c4fad8a..b097961abc 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -45,6 +45,7 @@ const ( LegacyTxType = iota AccessListTxType DynamicFeeTxType + SponsoredTxType = 100 ) // Transaction is an Ethereum transaction. @@ -53,9 +54,10 @@ type Transaction struct { time time.Time // Time first seen locally (spam avoidance) // caches - hash atomic.Value - size atomic.Value - from atomic.Value + hash atomic.Value + size atomic.Value + from atomic.Value + payer atomic.Value } // NewTx creates a new transaction. @@ -67,7 +69,7 @@ func NewTx(inner TxData) *Transaction { // TxData is the underlying data of a transaction. // -// This is implemented by DynamicFeeTx, LegacyTx and AccessListTx. +// This is implemented by DynamicFeeTx, LegacyTx, SponsoredTx and AccessListTx. type TxData interface { txType() byte // returns the type ID copy() TxData // creates a deep copy and initializes all fields @@ -82,7 +84,9 @@ type TxData interface { value() *big.Int nonce() uint64 to() *common.Address + expiredTime() uint64 + rawPayerSignatureValues() (v, r, s *big.Int) rawSignatureValues() (v, r, s *big.Int) setSignatureValues(chainID, v, r, s *big.Int) } @@ -186,6 +190,10 @@ func (tx *Transaction) decodeTyped(b []byte) (TxData, error) { var inner DynamicFeeTx err := rlp.DecodeBytes(b[1:], &inner) return &inner, err + case SponsoredTxType: + var inner SponsoredTx + err := rlp.DecodeBytes(b[1:], &inner) + return &inner, err default: return nil, ErrTxTypeNotSupported } @@ -287,6 +295,11 @@ func (tx *Transaction) To() *common.Address { return copyAddressPtr(tx.inner.to()) } +// ExpiredTime returns the expired time of the sponsored transaction +func (tx *Transaction) ExpiredTime() uint64 { + return tx.inner.expiredTime() +} + // Cost returns gas * gasPrice + value. func (tx *Transaction) Cost() *big.Int { total := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) @@ -294,6 +307,12 @@ func (tx *Transaction) Cost() *big.Int { return total } +// RawSignatureValues returns the V, R, S payer signature values of the transaction. +// The return values should not be modified by the caller. +func (tx *Transaction) RawPayerSignatureValues() (v, r, s *big.Int) { + return tx.inner.rawPayerSignatureValues() +} + // RawSignatureValues returns the V, R, S signature values of the transaction. // The return values should not be modified by the caller. func (tx *Transaction) RawSignatureValues() (v, r, s *big.Int) { @@ -567,48 +586,64 @@ func (t *TransactionsByPriceAndNonce) Size() int { // // NOTE: In a future PR this will be removed. type Message struct { - to *common.Address - from common.Address - nonce uint64 - amount *big.Int - gasLimit uint64 - gasPrice *big.Int - gasFeeCap *big.Int - gasTipCap *big.Int - data []byte - accessList AccessList - isFake bool -} - -func NewMessage(from common.Address, to *common.Address, nonce uint64, amount *big.Int, gasLimit uint64, gasPrice, gasFeeCap, gasTipCap *big.Int, data []byte, accessList AccessList, isFake bool) Message { + to *common.Address + from common.Address + nonce uint64 + amount *big.Int + gasLimit uint64 + gasPrice *big.Int + gasFeeCap *big.Int + gasTipCap *big.Int + data []byte + accessList AccessList + isFake bool + payer common.Address + expiredTime uint64 +} + +// Create a new message with payer is the same as from, expired time = 0 +func NewMessage( + from common.Address, + to *common.Address, + nonce uint64, + amount *big.Int, + gasLimit uint64, + gasPrice, gasFeeCap, gasTipCap *big.Int, + data []byte, + accessList AccessList, + isFake bool, +) Message { return Message{ - from: from, - to: to, - nonce: nonce, - amount: amount, - gasLimit: gasLimit, - gasPrice: gasPrice, - gasFeeCap: gasFeeCap, - gasTipCap: gasTipCap, - data: data, - accessList: accessList, - isFake: isFake, + from: from, + to: to, + nonce: nonce, + amount: amount, + gasLimit: gasLimit, + gasPrice: gasPrice, + gasFeeCap: gasFeeCap, + gasTipCap: gasTipCap, + data: data, + accessList: accessList, + isFake: isFake, + payer: from, + expiredTime: 0, } } // AsMessage returns the transaction as a core.Message. func (tx *Transaction) AsMessage(s Signer, baseFee *big.Int) (Message, error) { msg := Message{ - nonce: tx.Nonce(), - gasLimit: tx.Gas(), - gasPrice: new(big.Int).Set(tx.GasPrice()), - gasFeeCap: new(big.Int).Set(tx.GasFeeCap()), - gasTipCap: new(big.Int).Set(tx.GasTipCap()), - to: tx.To(), - amount: tx.Value(), - data: tx.Data(), - accessList: tx.AccessList(), - isFake: false, + nonce: tx.Nonce(), + gasLimit: tx.Gas(), + gasPrice: new(big.Int).Set(tx.GasPrice()), + gasFeeCap: new(big.Int).Set(tx.GasFeeCap()), + gasTipCap: new(big.Int).Set(tx.GasTipCap()), + to: tx.To(), + amount: tx.Value(), + data: tx.Data(), + accessList: tx.AccessList(), + isFake: false, + expiredTime: tx.ExpiredTime(), } // If baseFee provided, set gasPrice to effectiveGasPrice. if baseFee != nil { @@ -616,7 +651,25 @@ func (tx *Transaction) AsMessage(s Signer, baseFee *big.Int) (Message, error) { } var err error msg.from, err = Sender(s, tx) - return msg, err + if err != nil { + return Message{}, err + } + + msg.payer, err = Payer(s, tx) + if err != nil && errors.Is(err, errMissingPayerField) { + if errors.Is(err, errMissingPayerField) { + // This is not a sponsored transaction, the payer is the same as from + msg.payer = msg.from + return msg, nil + } + + return Message{}, err + } else if msg.payer == msg.from { + // Reject sponsored transaction with identical payer and sender + return Message{}, errors.New("payer = sender in sponsored transaction") + } + + return msg, nil } func (m Message) From() common.Address { return m.from } @@ -630,6 +683,8 @@ func (m Message) Nonce() uint64 { return m.nonce } func (m Message) Data() []byte { return m.data } func (m Message) AccessList() AccessList { return m.accessList } func (m Message) IsFake() bool { return m.isFake } +func (m Message) Payer() common.Address { return m.payer } +func (m Message) ExpiredTime() uint64 { return m.expiredTime } // 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 aad31a5a97..678c36fa3e 100644 --- a/core/types/transaction_marshalling.go +++ b/core/types/transaction_marshalling.go @@ -46,6 +46,12 @@ type txJSON struct { ChainID *hexutil.Big `json:"chainId,omitempty"` AccessList *AccessList `json:"accessList,omitempty"` + // Sponsored transaction fields + ExpiredTime *hexutil.Uint64 `json:"expiredTime,omitempty"` + PayerV *hexutil.Big `json:"payerV,omitempty"` + PayerR *hexutil.Big `json:"payerR,omitempty"` + PayerS *hexutil.Big `json:"payerS,omitempty"` + // Only used for encoding: Hash common.Hash `json:"hash"` } @@ -56,44 +62,40 @@ func (t *Transaction) MarshalJSON() ([]byte, error) { // These are set for all tx types. enc.Hash = t.Hash() enc.Type = hexutil.Uint64(t.Type()) + nonce := t.Nonce() + gas := t.Gas() + data := t.Data() + v, r, s := t.RawSignatureValues() + enc.Nonce = (*hexutil.Uint64)(&nonce) + enc.Gas = (*hexutil.Uint64)(&gas) + enc.Value = (*hexutil.Big)(t.Value()) + enc.Data = (*hexutil.Bytes)(&data) + enc.To = t.To() + enc.V = (*hexutil.Big)(v) + enc.R = (*hexutil.Big)(r) + enc.S = (*hexutil.Big)(s) // Other fields are set conditionally depending on tx type. switch tx := t.inner.(type) { case *LegacyTx: - enc.Nonce = (*hexutil.Uint64)(&tx.Nonce) - enc.Gas = (*hexutil.Uint64)(&tx.Gas) enc.GasPrice = (*hexutil.Big)(tx.GasPrice) - enc.Value = (*hexutil.Big)(tx.Value) - enc.Data = (*hexutil.Bytes)(&tx.Data) - enc.To = t.To() - enc.V = (*hexutil.Big)(tx.V) - enc.R = (*hexutil.Big)(tx.R) - enc.S = (*hexutil.Big)(tx.S) case *AccessListTx: + enc.GasPrice = (*hexutil.Big)(tx.GasPrice) enc.ChainID = (*hexutil.Big)(tx.ChainID) enc.AccessList = &tx.AccessList - enc.Nonce = (*hexutil.Uint64)(&tx.Nonce) - enc.Gas = (*hexutil.Uint64)(&tx.Gas) - enc.GasPrice = (*hexutil.Big)(tx.GasPrice) - enc.Value = (*hexutil.Big)(tx.Value) - enc.Data = (*hexutil.Bytes)(&tx.Data) - enc.To = t.To() - enc.V = (*hexutil.Big)(tx.V) - enc.R = (*hexutil.Big)(tx.R) - enc.S = (*hexutil.Big)(tx.S) case *DynamicFeeTx: enc.ChainID = (*hexutil.Big)(tx.ChainID) enc.AccessList = &tx.AccessList - enc.Nonce = (*hexutil.Uint64)(&tx.Nonce) - enc.Gas = (*hexutil.Uint64)(&tx.Gas) enc.MaxFeePerGas = (*hexutil.Big)(tx.GasFeeCap) enc.MaxPriorityFeePerGas = (*hexutil.Big)(tx.GasTipCap) - enc.Value = (*hexutil.Big)(tx.Value) - enc.Data = (*hexutil.Bytes)(&tx.Data) - enc.To = t.To() - enc.V = (*hexutil.Big)(tx.V) - enc.R = (*hexutil.Big)(tx.R) - enc.S = (*hexutil.Big)(tx.S) + case *SponsoredTx: + enc.ChainID = (*hexutil.Big)(tx.ChainID) + enc.MaxFeePerGas = (*hexutil.Big)(tx.GasFeeCap) + enc.MaxPriorityFeePerGas = (*hexutil.Big)(tx.GasTipCap) + enc.ExpiredTime = (*hexutil.Uint64)(&tx.ExpiredTime) + enc.PayerV = (*hexutil.Big)(tx.PayerV) + enc.PayerR = (*hexutil.Big)(tx.PayerR) + enc.PayerS = (*hexutil.Big)(tx.PayerS) } return json.Marshal(&enc) } @@ -105,56 +107,81 @@ func (t *Transaction) UnmarshalJSON(input []byte) error { return err } + var to *common.Address + if dec.To != nil { + to = dec.To + } + if dec.Nonce == nil { + return errors.New("missing required field 'nonce' in transaction") + } + nonce := uint64(*dec.Nonce) + if dec.Gas == nil { + return errors.New("missing required field 'gas' in transaction") + } + gas := uint64(*dec.Gas) + if dec.Value == nil { + return errors.New("missing required field 'value' in transaction") + } + value := (*big.Int)(dec.Value) + if dec.Data == nil { + return errors.New("missing required field 'input' in transaction") + } + data := *dec.Data + if dec.V == nil { + return errors.New("missing required field 'v' in transaction") + } + v := (*big.Int)(dec.V) + if dec.R == nil { + return errors.New("missing required field 'r' in transaction") + } + r := (*big.Int)(dec.R) + if dec.S == nil { + return errors.New("missing required field 's' in transaction") + } + s := (*big.Int)(dec.S) + withSignature := v.Sign() != 0 || r.Sign() != 0 || s.Sign() != 0 + if withSignature { + maybeProtected := false + if dec.Type == LegacyTxType { + maybeProtected = true + } + + if err := sanityCheckSignature(v, r, s, maybeProtected); err != nil { + return err + } + } + // Decode / verify fields according to transaction type. var inner TxData switch dec.Type { case LegacyTxType: - var itx LegacyTx - inner = &itx - if dec.To != nil { - itx.To = dec.To - } - if dec.Nonce == nil { - return errors.New("missing required field 'nonce' in transaction") + itx := LegacyTx{ + Nonce: nonce, + Gas: gas, + To: to, + Value: value, + Data: data, + V: v, + R: r, + S: s, } - itx.Nonce = uint64(*dec.Nonce) + inner = &itx if dec.GasPrice == nil { return errors.New("missing required field 'gasPrice' in transaction") } itx.GasPrice = (*big.Int)(dec.GasPrice) - if dec.Gas == nil { - return errors.New("missing required field 'gas' in transaction") - } - itx.Gas = uint64(*dec.Gas) - if dec.Value == nil { - return errors.New("missing required field 'value' in transaction") - } - itx.Value = (*big.Int)(dec.Value) - if dec.Data == nil { - return errors.New("missing required field 'input' in transaction") - } - itx.Data = *dec.Data - if dec.V == nil { - return errors.New("missing required field 'v' in transaction") - } - itx.V = (*big.Int)(dec.V) - if dec.R == nil { - return errors.New("missing required field 'r' in transaction") - } - itx.R = (*big.Int)(dec.R) - if dec.S == nil { - return errors.New("missing required field 's' in transaction") - } - itx.S = (*big.Int)(dec.S) - withSignature := itx.V.Sign() != 0 || itx.R.Sign() != 0 || itx.S.Sign() != 0 - if withSignature { - if err := sanityCheckSignature(itx.V, itx.R, itx.S, true); err != nil { - return err - } - } case AccessListTxType: - var itx AccessListTx + itx := AccessListTx{ + Nonce: nonce, + Gas: gas, + To: to, + Value: value, + Data: data, + V: v, + R: r, + S: s, + } inner = &itx // Access list is optional for now. if dec.AccessList != nil { @@ -164,50 +191,22 @@ func (t *Transaction) UnmarshalJSON(input []byte) error { return errors.New("missing required field 'chainId' in transaction") } itx.ChainID = (*big.Int)(dec.ChainID) - if dec.To != nil { - itx.To = dec.To - } - if dec.Nonce == nil { - return errors.New("missing required field 'nonce' in transaction") - } - itx.Nonce = uint64(*dec.Nonce) if dec.GasPrice == nil { return errors.New("missing required field 'gasPrice' in transaction") } itx.GasPrice = (*big.Int)(dec.GasPrice) - if dec.Gas == nil { - return errors.New("missing required field 'gas' in transaction") - } - itx.Gas = uint64(*dec.Gas) - if dec.Value == nil { - return errors.New("missing required field 'value' in transaction") - } - itx.Value = (*big.Int)(dec.Value) - if dec.Data == nil { - return errors.New("missing required field 'input' in transaction") - } - itx.Data = *dec.Data - if dec.V == nil { - return errors.New("missing required field 'v' in transaction") - } - itx.V = (*big.Int)(dec.V) - if dec.R == nil { - return errors.New("missing required field 'r' in transaction") - } - itx.R = (*big.Int)(dec.R) - if dec.S == nil { - return errors.New("missing required field 's' in transaction") - } - itx.S = (*big.Int)(dec.S) - withSignature := itx.V.Sign() != 0 || itx.R.Sign() != 0 || itx.S.Sign() != 0 - if withSignature { - if err := sanityCheckSignature(itx.V, itx.R, itx.S, false); err != nil { - return err - } - } case DynamicFeeTxType: - var itx DynamicFeeTx + itx := DynamicFeeTx{ + Nonce: nonce, + Gas: gas, + To: to, + Value: value, + Data: data, + V: v, + R: r, + S: s, + } inner = &itx // Access list is optional for now. if dec.AccessList != nil { @@ -217,13 +216,6 @@ func (t *Transaction) UnmarshalJSON(input []byte) error { return errors.New("missing required field 'chainId' in transaction") } itx.ChainID = (*big.Int)(dec.ChainID) - if dec.To != nil { - itx.To = dec.To - } - if dec.Nonce == nil { - return errors.New("missing required field 'nonce' in transaction") - } - itx.Nonce = uint64(*dec.Nonce) if dec.MaxPriorityFeePerGas == nil { return errors.New("missing required field 'maxPriorityFeePerGas' for txdata") } @@ -232,35 +224,49 @@ func (t *Transaction) UnmarshalJSON(input []byte) error { return errors.New("missing required field 'maxFeePerGas' for txdata") } itx.GasFeeCap = (*big.Int)(dec.MaxFeePerGas) - if dec.Gas == nil { - return errors.New("missing required field 'gas' for txdata") + + case SponsoredTxType: + itx := SponsoredTx{ + Nonce: nonce, + Gas: gas, + To: to, + Value: value, + Data: data, + V: v, + R: r, + S: s, } - itx.Gas = uint64(*dec.Gas) - if dec.Value == nil { - return errors.New("missing required field 'value' in transaction") + inner = &itx + if dec.ChainID == nil { + return errors.New("missing required field 'chainId' in transaction") } - itx.Value = (*big.Int)(dec.Value) - if dec.Data == nil { - return errors.New("missing required field 'input' in transaction") + itx.ChainID = (*big.Int)(dec.ChainID) + if dec.MaxPriorityFeePerGas == nil { + return errors.New("missing required field 'maxPriorityFeePerGas' for txdata") + } + itx.GasTipCap = (*big.Int)(dec.MaxPriorityFeePerGas) + if dec.MaxFeePerGas == nil { + return errors.New("missing required field 'maxFeePerGas' for txdata") + } + itx.GasFeeCap = (*big.Int)(dec.MaxFeePerGas) + if dec.ExpiredTime == nil { + return errors.New("missing required field 'expiredTime' in transaction") } - itx.Data = *dec.Data - if dec.V == nil { - return errors.New("missing required field 'v' in transaction") + itx.ExpiredTime = uint64(*dec.ExpiredTime) + if dec.PayerV == nil { + return errors.New("missing required field 'payerV' in transaction") } - itx.V = (*big.Int)(dec.V) - if dec.R == nil { - return errors.New("missing required field 'r' in transaction") + itx.PayerV = (*big.Int)(dec.PayerV) + if dec.PayerR == nil { + return errors.New("missing required field 'payerR' in transaction") } - itx.R = (*big.Int)(dec.R) - if dec.S == nil { - return errors.New("missing required field 's' in transaction") + itx.PayerR = (*big.Int)(dec.PayerR) + if dec.PayerS == nil { + return errors.New("missing required field 'payerS' in transaction") } - itx.S = (*big.Int)(dec.S) - withSignature := itx.V.Sign() != 0 || itx.R.Sign() != 0 || itx.S.Sign() != 0 - if withSignature { - if err := sanityCheckSignature(itx.V, itx.R, itx.S, false); err != nil { - return err - } + itx.PayerS = (*big.Int)(dec.PayerS) + if err := sanityCheckSignature(itx.PayerV, itx.PayerR, itx.PayerS, false); err != nil { + return err } default: diff --git a/core/types/transaction_signing.go b/core/types/transaction_signing.go index 1d0d2a4c75..72b983fb09 100644 --- a/core/types/transaction_signing.go +++ b/core/types/transaction_signing.go @@ -27,7 +27,10 @@ import ( "github.com/ethereum/go-ethereum/params" ) -var ErrInvalidChainId = errors.New("invalid chain id for signer") +var ( + ErrInvalidChainId = errors.New("invalid chain id for signer") + errMissingPayerField = errors.New("transaction has no payer field") +) // sigCache is used to cache the derived sender and contains // the signer used to derive it. @@ -44,6 +47,8 @@ func MakeSigner(config *params.ChainConfig, blockNumber *big.Int) Signer { signer = NewLondonSigner(config.ChainID) case config.IsBerlin(blockNumber): signer = NewEIP2930Signer(config.ChainID) + case config.IsMiko(blockNumber): + signer = NewMikoSigner(config.ChainID) case config.IsEIP155(blockNumber): signer = NewEIP155Signer(config.ChainID) case config.IsHomestead(blockNumber): @@ -69,6 +74,9 @@ func LatestSigner(config *params.ChainConfig) Signer { if config.BerlinBlock != nil { return NewEIP2930Signer(config.ChainID) } + if config.MikoBlock != nil { + return NewMikoSigner(config.ChainID) + } if config.EIP155Block != nil { return NewEIP155Signer(config.ChainID) } @@ -111,6 +119,30 @@ func SignNewTx(prv *ecdsa.PrivateKey, s Signer, txdata TxData) (*Transaction, er return tx.WithSignature(s, sig) } +func PayerSign(prv *ecdsa.PrivateKey, signer Signer, sender common.Address, txdata TxData) (r, s, v *big.Int, err error) { + payerHash := rlpHash([]interface{}{ + signer.ChainID(), + sender, + txdata.nonce(), + txdata.gasTipCap(), + txdata.gasFeeCap(), + txdata.gas(), + txdata.to(), + txdata.value(), + txdata.data(), + txdata.expiredTime(), + }) + + sig, err := crypto.Sign(payerHash[:], prv) + if err != nil { + return nil, nil, nil, err + } + + r, s, _ = decodeSignature(sig) + v = big.NewInt(int64(sig[64])) + return r, s, v, nil +} + // MustSignNewTx creates a transaction and signs it. // This panics if the transaction cannot be signed. func MustSignNewTx(prv *ecdsa.PrivateKey, s Signer, txdata TxData) *Transaction { @@ -147,6 +179,35 @@ func Sender(signer Signer, tx *Transaction) (common.Address, error) { return addr, nil } +// Payer returns the address derived from payer's signature in sponsored +// transaction or nil in other transaction types. +// +// Payer may cache the address, allowing it to be used regardless of +// signing method. The cache is invalidated if the cached signer does +// not match the signer used in the current call. +func Payer(signer Signer, tx *Transaction) (common.Address, error) { + if tx.Type() != SponsoredTxType { + return common.Address{}, errMissingPayerField + } + + if sc := tx.payer.Load(); sc != nil { + sigCache := sc.(sigCache) + // If the signer used to derive from in a previous + // call is not the same as used current, invalidate + // the cache. + if sigCache.signer.Equal(signer) { + return sigCache.from, nil + } + } + + addr, err := signer.Payer(tx) + if err != nil { + return common.Address{}, err + } + tx.payer.Store(sigCache{signer: signer, from: addr}) + return addr, nil +} + // Signer encapsulates transaction signature handling. The name of this type is slightly // misleading because Signers don't actually sign, they're just for validating and // processing of signatures. @@ -168,6 +229,9 @@ type Signer interface { // Equal returns true if the given signer is the same as the receiver. Equal(Signer) bool + + // Payer returns the payer address of sponsored transaction + Payer(tx *Transaction) (common.Address, error) } type londonSigner struct{ eip2930Signer } @@ -175,10 +239,11 @@ type londonSigner struct{ eip2930Signer } // NewLondonSigner returns a signer that accepts // - EIP-1559 dynamic fee transactions // - EIP-2930 access list transactions, +// - Sponsored transactions, // - EIP-155 replay protected transactions, and // - legacy Homestead transactions. func NewLondonSigner(chainId *big.Int) Signer { - return londonSigner{eip2930Signer{NewEIP155Signer(chainId)}} + return londonSigner{eip2930Signer{MikoSigner{NewEIP155Signer(chainId)}}} } func (s londonSigner) Sender(tx *Transaction) (common.Address, error) { @@ -236,12 +301,14 @@ func (s londonSigner) Hash(tx *Transaction) common.Hash { }) } -type eip2930Signer struct{ EIP155Signer } +// londonSigner.Payer is the same as eip2930Signer.Payer + +type eip2930Signer struct{ MikoSigner } // NewEIP2930Signer returns a signer that accepts EIP-2930 access list transactions, // EIP-155 replay protected transactions, and legacy Homestead transactions. func NewEIP2930Signer(chainId *big.Int) Signer { - return eip2930Signer{NewEIP155Signer(chainId)} + return eip2930Signer{MikoSigner{NewEIP155Signer(chainId)}} } func (s eip2930Signer) ChainID() *big.Int { @@ -256,12 +323,8 @@ func (s eip2930Signer) Equal(s2 Signer) bool { func (s eip2930Signer) Sender(tx *Transaction) (common.Address, error) { V, R, S := tx.RawSignatureValues() switch tx.Type() { - case LegacyTxType: - if !tx.Protected() { - return HomesteadSigner{}.Sender(tx) - } - V = new(big.Int).Sub(V, s.chainIdMul) - V.Sub(V, big8) + case LegacyTxType, SponsoredTxType: + return s.MikoSigner.Sender(tx) case AccessListTxType: // AL txs are defined to use 0 and 1 as their recovery // id, add 27 to become equivalent to unprotected Homestead signatures. @@ -277,8 +340,8 @@ func (s eip2930Signer) Sender(tx *Transaction) (common.Address, error) { func (s eip2930Signer) SignatureValues(tx *Transaction, sig []byte) (R, S, V *big.Int, err error) { switch txdata := tx.inner.(type) { - case *LegacyTx: - return s.EIP155Signer.SignatureValues(tx, sig) + case *LegacyTx, *SponsoredTx: + return s.MikoSigner.SignatureValues(tx, sig) case *AccessListTx: // Check that chain ID of tx matches the signer. We also accept ID zero here, // because it indicates that the chain ID was not specified in the tx. @@ -320,6 +383,8 @@ func (s eip2930Signer) Hash(tx *Transaction) common.Hash { tx.Data(), tx.AccessList(), }) + case SponsoredTxType: + return s.MikoSigner.Hash(tx) default: // This _should_ not happen, but in case someone sends in a bad // json struct via RPC, it's probably more prudent to return an @@ -329,6 +394,116 @@ func (s eip2930Signer) Hash(tx *Transaction) common.Hash { } } +func (s eip2930Signer) Payer(tx *Transaction) (common.Address, error) { + if tx.Type() == SponsoredTxType { + return s.MikoSigner.Payer(tx) + } + + return common.Address{}, ErrInvalidTxType +} + +type MikoSigner struct { + EIP155Signer +} + +func NewMikoSigner(chainId *big.Int) MikoSigner { + return MikoSigner{NewEIP155Signer(chainId)} +} + +func (s MikoSigner) Equal(s2 Signer) bool { + miko, ok := s2.(MikoSigner) + return ok && miko.chainId.Cmp(s.chainId) == 0 +} + +func (s MikoSigner) Sender(tx *Transaction) (common.Address, error) { + switch tx.Type() { + case LegacyTxType: + return s.EIP155Signer.Sender(tx) + case SponsoredTxType: + if tx.ChainId().Cmp(s.chainId) != 0 { + return common.Address{}, ErrInvalidChainId + } + // V in sponsored signature is {0, 1}, but the recoverPlain expects + // {0, 1} + 27, so we need to add 27 to V + V, R, S := tx.RawSignatureValues() + V = new(big.Int).Add(V, big.NewInt(27)) + return recoverPlain(s.Hash(tx), R, S, V, true) + default: + return common.Address{}, ErrTxTypeNotSupported + } +} + +func (s MikoSigner) SignatureValues(tx *Transaction, sig []byte) (R, S, V *big.Int, err error) { + switch tx.Type() { + case LegacyTxType: + return s.EIP155Signer.SignatureValues(tx, sig) + case SponsoredTxType: + // V in sponsored signature is {0, 1}, get it directly from raw signature + // because decodeSignature returns {0, 1} + 27 + R, S, _ := decodeSignature(sig) + V := big.NewInt(int64(sig[64])) + return R, S, V, nil + default: + return nil, nil, nil, ErrTxTypeNotSupported + } +} + +func (s MikoSigner) Hash(tx *Transaction) common.Hash { + switch tx.Type() { + case LegacyTxType: + return s.EIP155Signer.Hash(tx) + case SponsoredTxType: + payerV, payerR, payerS := tx.RawPayerSignatureValues() + return prefixedRlpHash( + tx.Type(), + []interface{}{ + s.chainId, + tx.Nonce(), + tx.GasTipCap(), + tx.GasFeeCap(), + tx.Gas(), + tx.To(), + tx.Value(), + tx.Data(), + tx.ExpiredTime(), + payerV, payerR, payerS, + }, + ) + default: + return common.Hash{} + } +} + +func (s MikoSigner) Payer(tx *Transaction) (common.Address, error) { + if tx.Type() != SponsoredTxType { + return common.Address{}, ErrInvalidTxType + } + + sender, err := Sender(s, tx) + if err != nil { + return common.Address{}, err + } + + payerV, payerR, payerS := tx.RawPayerSignatureValues() + payerHash := rlpHash([]interface{}{ + s.chainId, + sender, + tx.Nonce(), + tx.GasTipCap(), + tx.GasFeeCap(), + tx.Gas(), + tx.To(), + tx.Value(), + tx.Data(), + tx.ExpiredTime(), + }) + + // V in payer signature is {0, 1}, but the recoverPlain expects + // {0, 1} + 27, so we need to add 27 to V + payerV = new(big.Int).Add(payerV, big.NewInt(27)) + return recoverPlain(payerHash, payerR, payerS, payerV, true) +} + // EIP155Signer implements Signer using the EIP-155 rules. This accepts transactions which // are replay-protected as well as unprotected homestead transactions. type EIP155Signer struct { @@ -400,6 +575,10 @@ func (s EIP155Signer) Hash(tx *Transaction) common.Hash { }) } +func (s EIP155Signer) Payer(tx *Transaction) (common.Address, error) { + return common.Address{}, ErrInvalidTxType +} + // HomesteadTransaction implements TransactionInterface using the // homestead rules. type HomesteadSigner struct{ FrontierSigner } @@ -469,6 +648,10 @@ func (fs FrontierSigner) Hash(tx *Transaction) common.Hash { }) } +func (fs FrontierSigner) Payer(tx *Transaction) (common.Address, error) { + return common.Address{}, ErrInvalidTxType +} + func decodeSignature(sig []byte) (r, s, v *big.Int) { if len(sig) != crypto.SignatureLength { panic(fmt.Sprintf("wrong size for signature: got %d, want %d", len(sig), crypto.SignatureLength)) diff --git a/core/types/transaction_signing_test.go b/core/types/transaction_signing_test.go index 689fc38a9b..f256da886e 100644 --- a/core/types/transaction_signing_test.go +++ b/core/types/transaction_signing_test.go @@ -17,6 +17,7 @@ package types import ( + "errors" "math/big" "testing" @@ -136,3 +137,170 @@ func TestChainId(t *testing.T) { t.Error("expected no error") } } + +func TestSponsoredTransactionSigner(t *testing.T) { + recipient := common.HexToAddress("0000000000000000000000000000000000000001") + sender, err := crypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + senderAddr := crypto.PubkeyToAddress(sender.PublicKey) + + payer, err := crypto.GenerateKey() + if err != nil { + t.Fatal(err) + } + payerAddr := crypto.PubkeyToAddress(payer.PublicKey) + + mikoSigner := NewMikoSigner(big.NewInt(2020)) + eip155Signer := NewEIP155Signer(big.NewInt(2020)) + latestSigner := LatestSignerForChainID(big.NewInt(2020)) + + innerTx := SponsoredTx{ + ChainID: big.NewInt(2020), + Nonce: 1, + GasTipCap: big.NewInt(100000), + GasFeeCap: big.NewInt(100000), + Gas: 1000, + To: &recipient, + Value: big.NewInt(10), + Data: []byte("abcd"), + ExpiredTime: 100000, + } + + innerTx.PayerR, innerTx.PayerS, innerTx.PayerV, err = PayerSign(payer, mikoSigner, senderAddr, &innerTx) + if err != nil { + t.Fatalf("Payer fails to sign, err %s", err) + } + + // 1. Check hash + tx := NewTx(&innerTx) + + expectedHash := prefixedRlpHash(0x64, []interface{}{ + big.NewInt(2020), + innerTx.Nonce, + innerTx.GasTipCap, + innerTx.GasFeeCap, + innerTx.Gas, + innerTx.To, + innerTx.Value, + innerTx.Data, + innerTx.ExpiredTime, + innerTx.PayerV, + innerTx.PayerR, + innerTx.PayerS, + }) + + hash := mikoSigner.Hash(tx) + if hash != expectedHash { + t.Fatalf("Tx hash mismatches, get %s expect %s", hash, expectedHash) + } + + hash = latestSigner.Hash(tx) + if hash != expectedHash { + t.Fatalf("Tx hash mismatches, get %s expect %s", hash, expectedHash) + } + + tx, err = SignTx(tx, mikoSigner, sender) + if err != nil { + t.Fatalf("Failed to sign tx, err %s", err) + } + v, r, s := tx.RawSignatureValues() + transactionHash := tx.Hash() + + expectedTxHash := prefixedRlpHash(0x64, []interface{}{ + big.NewInt(2020), + innerTx.Nonce, + innerTx.GasTipCap, + innerTx.GasFeeCap, + innerTx.Gas, + innerTx.To, + innerTx.Value, + innerTx.Data, + innerTx.ExpiredTime, + innerTx.PayerV, + innerTx.PayerR, + innerTx.PayerS, + v, r, s, + }) + if transactionHash != expectedTxHash { + t.Fatalf("Transaction hash mismatches, get %s expect %s", transactionHash, expectedTxHash) + } + + // 2. Check sender + tx, err = SignTx(tx, mikoSigner, sender) + if err != nil { + t.Fatalf("Failed to sign tx, err %s", err) + } + + v, _, _ = tx.RawSignatureValues() + if v.Cmp(big.NewInt(0)) != 0 && v.Cmp(big.NewInt(1)) != 0 { + t.Fatalf("V is expected to be {0, 1}, get %d", v.Uint64()) + } + + recoveredSender, err := Sender(mikoSigner, tx) + if err != nil { + t.Fatalf("Failed to recover sender, err %s", err) + } + + if recoveredSender != senderAddr { + t.Fatalf("Sender mismatches, get %s expect %s", recoveredSender, senderAddr) + } + + // London signer should be able to recover sender too + recoveredSender, err = Sender(latestSigner, tx) + if err != nil { + t.Fatalf("Failed to recover sender, err %s", err) + } + + if recoveredSender != senderAddr { + t.Fatalf("Sender mismatches, get %s expect %s", recoveredSender, senderAddr) + } + + // EIP155 should not accept sponsored tx + _, err = Sender(eip155Signer, tx) + if err == nil || !errors.Is(err, ErrTxTypeNotSupported) { + t.Fatalf("Expect %s, get %s", ErrTxTypeNotSupported, err) + } + + // 3. Check payer + payerV, _, _ := tx.RawPayerSignatureValues() + if payerV.Cmp(big.NewInt(0)) != 0 && payerV.Cmp(big.NewInt(1)) != 0 { + t.Fatalf("payerV is expected to be {0, 1}, get %d", v.Uint64()) + } + recoveredPayerAddr, err := Payer(mikoSigner, tx) + if err != nil { + t.Fatalf("Failed to recover payer, err %s", err) + } + + if recoveredPayerAddr != payerAddr { + t.Fatalf("Payer mismatches, get %s expect %s", recoveredPayerAddr, payerAddr) + } + + // London signer should be able to recover sender too + recoveredPayerAddr, err = Payer(latestSigner, tx) + if err != nil { + t.Fatalf("Failed to recover payer, err %s", err) + } + + if recoveredPayerAddr != payerAddr { + t.Fatalf("Payer mismatches, get %s expect %s", recoveredPayerAddr, payerAddr) + } + + // 4. Check chainId + innerTx.ChainID = big.NewInt(1000) + innerTx.PayerR, innerTx.PayerS, innerTx.PayerV, err = PayerSign(payer, mikoSigner, senderAddr, &innerTx) + if err != nil { + t.Fatalf("Payer fails to sign, err %s", err) + } + tx = NewTx(&innerTx) + tx, err = SignTx(tx, mikoSigner, sender) + if err != nil { + t.Fatalf("Failed to sign tx, err %s", err) + } + + _, err = Sender(mikoSigner, tx) + if err == nil || !errors.Is(err, ErrInvalidChainId) { + t.Fatalf("Expect %s, get %s", ErrInvalidChainId, err) + } +} diff --git a/core/types/transaction_test.go b/core/types/transaction_test.go index 58c95071b2..2c124559ee 100644 --- a/core/types/transaction_test.go +++ b/core/types/transaction_test.go @@ -419,7 +419,7 @@ func TestTransactionCoding(t *testing.T) { ) for i := uint64(0); i < 500; i++ { var txdata TxData - switch i % 5 { + switch i % 6 { case 0: // Legacy tx. txdata = &LegacyTx{ @@ -467,6 +467,23 @@ func TestTransactionCoding(t *testing.T) { GasPrice: big.NewInt(10), AccessList: accesses, } + case 5: + // Sponsored tx + itx := SponsoredTx{ + ChainID: big.NewInt(1), + Nonce: i, + To: &recipient, + Gas: 123457, + GasTipCap: big.NewInt(10), + GasFeeCap: big.NewInt(10), + Data: []byte("abcdef"), + ExpiredTime: 100000, + } + txdata = &itx + itx.PayerR, itx.PayerS, itx.PayerV, err = PayerSign(key, signer, crypto.PubkeyToAddress(key.PublicKey), txdata) + if err != nil { + t.Fatal(err) + } } tx, err := SignNewTx(key, signer, txdata) if err != nil { diff --git a/ethclient/signer.go b/ethclient/signer.go index f827d4eb56..ac1ce39e9f 100644 --- a/ethclient/signer.go +++ b/ethclient/signer.go @@ -60,3 +60,6 @@ func (s *senderFromServer) Hash(tx *types.Transaction) common.Hash { func (s *senderFromServer) SignatureValues(tx *types.Transaction, sig []byte) (R, S, V *big.Int, err error) { panic("can't sign with senderFromServer") } +func (s *senderFromServer) Payer(tx *types.Transaction) (common.Address, error) { + panic("can't sign with senderFromServer") +} diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 219388ee12..74e7330d57 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -20,11 +20,12 @@ import ( "context" "errors" "fmt" - "github.com/ethereum/go-ethereum/eth/tracers/logger" "math/big" "strings" "time" + "github.com/ethereum/go-ethereum/eth/tracers/logger" + "github.com/davecgh/go-spew/spew" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/abi" @@ -1332,6 +1333,10 @@ type RPCTransaction struct { V *hexutil.Big `json:"v"` R *hexutil.Big `json:"r"` S *hexutil.Big `json:"s"` + ExpiredTime *hexutil.Uint64 `json:"expiredTime,omitempty"` + PayerV *hexutil.Big `json:"payerV,omitempty"` + PayerR *hexutil.Big `json:"payerR,omitempty"` + PayerS *hexutil.Big `json:"payerS,omitempty"` } // newRPCTransaction returns a transaction that will serialize to the RPC @@ -1378,6 +1383,24 @@ func newRPCTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber } else { result.GasPrice = (*hexutil.Big)(tx.GasFeeCap()) } + case types.SponsoredTxType: + result.ChainID = (*hexutil.Big)(tx.ChainId()) + result.GasFeeCap = (*hexutil.Big)(tx.GasFeeCap()) + result.GasTipCap = (*hexutil.Big)(tx.GasTipCap()) + // if the transaction has been mined, compute the effective gas price + if baseFee != nil && blockHash != (common.Hash{}) { + // price = min(tip, gasFeeCap - baseFee) + baseFee + price := math.BigMin(new(big.Int).Add(tx.GasTipCap(), baseFee), tx.GasFeeCap()) + result.GasPrice = (*hexutil.Big)(price) + } else { + result.GasPrice = (*hexutil.Big)(tx.GasFeeCap()) + } + expiredTime := tx.ExpiredTime() + result.ExpiredTime = (*hexutil.Uint64)(&expiredTime) + v, r, s := tx.RawPayerSignatureValues() + result.PayerR = (*hexutil.Big)(r) + result.PayerS = (*hexutil.Big)(s) + result.PayerV = (*hexutil.Big)(v) } return result }