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/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..96b15a1336 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,39 @@ 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.RawPayerSignatureValues() + 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.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 +106,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 +190,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 +215,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 +223,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..f731a85934 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) } @@ -147,6 +155,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 +205,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 +215,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 +277,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 +299,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 +316,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 +359,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 +370,115 @@ 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.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 +550,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 +623,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/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 }