diff --git a/.github/workflows/test-pr.yml b/.github/workflows/test-pr.yml index 1a61cffce..ae0e7a97e 100644 --- a/.github/workflows/test-pr.yml +++ b/.github/workflows/test-pr.yml @@ -7,6 +7,7 @@ on: pull_request: branches: - master + - eip-7702 concurrency: group: ${{ github.head_ref || github.run_id }} diff --git a/core/types/gen_authorization.go b/core/types/gen_authorization.go new file mode 100644 index 000000000..b598b64ff --- /dev/null +++ b/core/types/gen_authorization.go @@ -0,0 +1,75 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package types + +import ( + "encoding/json" + "errors" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/holiman/uint256" +) + +var _ = (*authorizationMarshaling)(nil) + +// MarshalJSON marshals as JSON. +func (a Authorization) MarshalJSON() ([]byte, error) { + type Authorization struct { + ChainID hexutil.Uint64 `json:"chainId" gencodec:"required"` + Address common.Address `json:"address" gencodec:"required"` + Nonce hexutil.Uint64 `json:"nonce" gencodec:"required"` + V hexutil.Uint64 `json:"v" gencodec:"required"` + R uint256.Int `json:"r" gencodec:"required"` + S uint256.Int `json:"s" gencodec:"required"` + } + var enc Authorization + enc.ChainID = hexutil.Uint64(a.ChainID) + enc.Address = a.Address + enc.Nonce = hexutil.Uint64(a.Nonce) + enc.V = hexutil.Uint64(a.V) + enc.R = a.R + enc.S = a.S + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals from JSON. +func (a *Authorization) UnmarshalJSON(input []byte) error { + type Authorization struct { + ChainID *hexutil.Uint64 `json:"chainId" gencodec:"required"` + Address *common.Address `json:"address" gencodec:"required"` + Nonce *hexutil.Uint64 `json:"nonce" gencodec:"required"` + V *hexutil.Uint64 `json:"v" gencodec:"required"` + R *uint256.Int `json:"r" gencodec:"required"` + S *uint256.Int `json:"s" gencodec:"required"` + } + var dec Authorization + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.ChainID == nil { + return errors.New("missing required field 'chainId' for Authorization") + } + a.ChainID = uint64(*dec.ChainID) + if dec.Address == nil { + return errors.New("missing required field 'address' for Authorization") + } + a.Address = *dec.Address + if dec.Nonce == nil { + return errors.New("missing required field 'nonce' for Authorization") + } + a.Nonce = uint64(*dec.Nonce) + if dec.V == nil { + return errors.New("missing required field 'v' for Authorization") + } + a.V = uint8(*dec.V) + if dec.R == nil { + return errors.New("missing required field 'r' for Authorization") + } + a.R = *dec.R + if dec.S == nil { + return errors.New("missing required field 's' for Authorization") + } + a.S = *dec.S + return nil +} diff --git a/core/types/receipt.go b/core/types/receipt.go index 90ab341f4..6535ee5d3 100644 --- a/core/types/receipt.go +++ b/core/types/receipt.go @@ -196,7 +196,7 @@ func (r *Receipt) DecodeRLP(s *rlp.Stream) error { return errEmptyTypedReceipt } r.Type = b[0] - if r.Type == AccessListTxType || r.Type == DynamicFeeTxType || r.Type == BlobTxType || r.Type == SponsoredTxType { + if r.Type == AccessListTxType || r.Type == DynamicFeeTxType || r.Type == BlobTxType || r.Type == SetCodeTxType || r.Type == SponsoredTxType { var dec receiptRLP if err := rlp.DecodeBytes(b[1:], &dec); err != nil { return err @@ -232,7 +232,7 @@ func (r *Receipt) decodeTyped(b []byte) error { return errEmptyTypedReceipt } switch b[0] { - case DynamicFeeTxType, AccessListTxType, BlobTxType, SponsoredTxType: + case DynamicFeeTxType, AccessListTxType, BlobTxType, SponsoredTxType, SetCodeTxType: var data receiptRLP err := rlp.DecodeBytes(b[1:], &data) if err != nil { @@ -395,7 +395,7 @@ func (rs Receipts) EncodeIndex(i int, w *bytes.Buffer) { switch r.Type { case LegacyTxType: rlp.Encode(w, data) - case AccessListTxType, DynamicFeeTxType, BlobTxType, SponsoredTxType: + case AccessListTxType, DynamicFeeTxType, BlobTxType, SponsoredTxType, SetCodeTxType: w.WriteByte(r.Type) rlp.Encode(w, data) default: diff --git a/core/types/setcode_tx.go b/core/types/setcode_tx.go new file mode 100644 index 000000000..fcfdc2f00 --- /dev/null +++ b/core/types/setcode_tx.go @@ -0,0 +1,231 @@ +// 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 types + +import ( + "bytes" + "crypto/ecdsa" + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rlp" + "github.com/holiman/uint256" +) + +// DelegationPrefix is used by code to denote the account is delegating to +// another account. +var DelegationPrefix = []byte{0xef, 0x01, 0x00} + +// ParseDelegation tries to parse the address from a delegation slice. +func ParseDelegation(b []byte) (common.Address, bool) { + if len(b) != 23 || !bytes.HasPrefix(b, DelegationPrefix) { + return common.Address{}, false + } + return common.BytesToAddress(b[len(DelegationPrefix):]), true +} + +// AddressToDelegation adds the delegation prefix to the specified address. +func AddressToDelegation(addr common.Address) []byte { + return append(DelegationPrefix, addr.Bytes()...) +} + +// SetCodeTx implements the EIP-7702 transaction type which temporarily installs +// the code at the signer's address. +type SetCodeTx struct { + ChainID uint64 + Nonce uint64 + GasTipCap *uint256.Int // a.k.a. maxPriorityFeePerGas + GasFeeCap *uint256.Int // a.k.a. maxFeePerGas + Gas uint64 + To common.Address + Value *uint256.Int + Data []byte + AccessList AccessList + AuthList []Authorization + + // Signature values + V *uint256.Int `json:"v" gencodec:"required"` + R *uint256.Int `json:"r" gencodec:"required"` + S *uint256.Int `json:"s" gencodec:"required"` +} + +//go:generate go run github.com/fjl/gencodec -type Authorization -field-override authorizationMarshaling -out gen_authorization.go + +// Authorization is an authorization from an account to deploy code at its address. +type Authorization struct { + ChainID uint64 `json:"chainId" gencodec:"required"` + Address common.Address `json:"address" gencodec:"required"` + Nonce uint64 `json:"nonce" gencodec:"required"` + V uint8 `json:"v" gencodec:"required"` + R uint256.Int `json:"r" gencodec:"required"` + S uint256.Int `json:"s" gencodec:"required"` +} + +// field type overrides for gencodec +type authorizationMarshaling struct { + ChainID hexutil.Uint64 + Nonce hexutil.Uint64 + V hexutil.Uint64 +} + +// SignAuth signs the provided authorization. +func SignAuth(auth Authorization, prv *ecdsa.PrivateKey) (Authorization, error) { + sighash := auth.sigHash() + sig, err := crypto.Sign(sighash[:], prv) + if err != nil { + return Authorization{}, err + } + return auth.withSignature(sig), nil +} + +// withSignature updates the signature of an Authorization to be equal the +// decoded signature provided in sig. +func (a *Authorization) withSignature(sig []byte) Authorization { + r, s, _ := decodeSignature(sig) + return Authorization{ + ChainID: a.ChainID, + Address: a.Address, + Nonce: a.Nonce, + V: sig[64], + R: *uint256.MustFromBig(r), + S: *uint256.MustFromBig(s), + } +} + +func (a *Authorization) sigHash() common.Hash { + return prefixedRlpHash(0x05, []any{ + a.ChainID, + a.Address, + a.Nonce, + }) +} + +// Authority recovers the the authorizing account of an authorization. +func (a *Authorization) Authority() (common.Address, error) { + sighash := a.sigHash() + if !crypto.ValidateSignatureValues(a.V, a.R.ToBig(), a.S.ToBig(), true) { + return common.Address{}, ErrInvalidSig + } + // encode the signature in uncompressed format + var sig [crypto.SignatureLength]byte + a.R.WriteToSlice(sig[:32]) + a.S.WriteToSlice(sig[32:64]) + sig[64] = a.V + // recover the public key from the signature + pub, err := crypto.Ecrecover(sighash[:], sig[:]) + if err != nil { + return common.Address{}, err + } + if len(pub) == 0 || pub[0] != 4 { + return common.Address{}, errors.New("invalid public key") + } + var addr common.Address + copy(addr[:], crypto.Keccak256(pub[1:])[12:]) + return addr, nil +} + +// copy creates a deep copy of the transaction data and initializes all fields. +func (tx *SetCodeTx) copy() TxData { + cpy := &SetCodeTx{ + Nonce: tx.Nonce, + To: tx.To, + Data: common.CopyBytes(tx.Data), + Gas: tx.Gas, + // These are copied below. + AccessList: make(AccessList, len(tx.AccessList)), + AuthList: make([]Authorization, len(tx.AuthList)), + Value: new(uint256.Int), + ChainID: tx.ChainID, + GasTipCap: new(uint256.Int), + GasFeeCap: new(uint256.Int), + V: new(uint256.Int), + R: new(uint256.Int), + S: new(uint256.Int), + } + copy(cpy.AccessList, tx.AccessList) + copy(cpy.AuthList, tx.AuthList) + 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.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 *SetCodeTx) txType() byte { return SetCodeTxType } +func (tx *SetCodeTx) chainID() *big.Int { return big.NewInt(int64(tx.ChainID)) } +func (tx *SetCodeTx) accessList() AccessList { return tx.AccessList } +func (tx *SetCodeTx) data() []byte { return tx.Data } +func (tx *SetCodeTx) gas() uint64 { return tx.Gas } +func (tx *SetCodeTx) gasFeeCap() *big.Int { return tx.GasFeeCap.ToBig() } +func (tx *SetCodeTx) gasTipCap() *big.Int { return tx.GasTipCap.ToBig() } +func (tx *SetCodeTx) gasPrice() *big.Int { return tx.GasFeeCap.ToBig() } +func (tx *SetCodeTx) value() *big.Int { return tx.Value.ToBig() } +func (tx *SetCodeTx) nonce() uint64 { return tx.Nonce } +func (tx *SetCodeTx) to() *common.Address { tmp := tx.To; return &tmp } +func (tx *SetCodeTx) expiredTime() uint64 { return 0 } + +func (tx *SetCodeTx) rawPayerSignatureValues() (v, r, s *big.Int) { + return nil, nil, nil +} + +func (tx *SetCodeTx) effectiveGasPrice(dst *big.Int, baseFee *big.Int) *big.Int { + if baseFee == nil { + return dst.Set(tx.GasFeeCap.ToBig()) + } + tip := dst.Sub(tx.GasFeeCap.ToBig(), baseFee) + if tip.Cmp(tx.GasTipCap.ToBig()) > 0 { + tip.Set(tx.GasTipCap.ToBig()) + } + return tip.Add(tip, baseFee) +} + +func (tx *SetCodeTx) rawSignatureValues() (v, r, s *big.Int) { + return tx.V.ToBig(), tx.R.ToBig(), tx.S.ToBig() +} + +func (tx *SetCodeTx) setSignatureValues(chainID, v, r, s *big.Int) { + tx.ChainID = chainID.Uint64() + tx.V.SetFromBig(v) + tx.R.SetFromBig(r) + tx.S.SetFromBig(s) +} + +func (tx *SetCodeTx) encode(b *bytes.Buffer) error { + return rlp.Encode(b, tx) +} + +func (tx *SetCodeTx) decode(input []byte) error { + return rlp.DecodeBytes(input, tx) +} diff --git a/core/types/setcode_tx_test.go b/core/types/setcode_tx_test.go new file mode 100644 index 000000000..d0544573c --- /dev/null +++ b/core/types/setcode_tx_test.go @@ -0,0 +1,70 @@ +// 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 types + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +// TestParseDelegation tests a few possible delegation designator values and +// ensures they are parsed correctly. +func TestParseDelegation(t *testing.T) { + addr := common.Address{0x42} + for _, tt := range []struct { + val []byte + want *common.Address + }{ + { // simple correct delegation + val: append(DelegationPrefix, addr.Bytes()...), + want: &addr, + }, + { // wrong address size + val: append(DelegationPrefix, addr.Bytes()[0:19]...), + }, + { // short address + val: append(DelegationPrefix, 0x42), + }, + { // long address + val: append(append(DelegationPrefix, addr.Bytes()...), 0x42), + }, + { // wrong prefix size + val: append(DelegationPrefix[:2], addr.Bytes()...), + }, + { // wrong prefix + val: append([]byte{0xef, 0x01, 0x01}, addr.Bytes()...), + }, + { // wrong prefix + val: append([]byte{0xef, 0x00, 0x00}, addr.Bytes()...), + }, + { // no prefix + val: addr.Bytes(), + }, + { // no address + val: DelegationPrefix, + }, + } { + got, ok := ParseDelegation(tt.val) + if ok && tt.want == nil { + t.Fatalf("expected fail, got %s", got.Hex()) + } + if !ok && tt.want != nil { + t.Fatalf("failed to parse, want %s", tt.want.Hex()) + } + } +} diff --git a/core/types/transaction.go b/core/types/transaction.go index d300c4adf..21ef6ec0e 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -38,6 +38,9 @@ var ( ErrGasFeeCapTooLow = errors.New("fee cap less than base fee") ErrSamePayerSenderSponsoredTx = errors.New("payer = sender in sponsored transaction") errEmptyTypedTx = errors.New("empty typed transaction bytes") + 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. @@ -46,6 +49,7 @@ const ( AccessListTxType DynamicFeeTxType BlobTxType + SetCodeTxType SponsoredTxType = 100 ) @@ -196,6 +200,8 @@ func (tx *Transaction) decodeTyped(b []byte) (TxData, error) { inner = new(SponsoredTx) case BlobTxType: inner = new(BlobTx) + case SetCodeTxType: + inner = new(SetCodeTx) default: return nil, ErrTxTypeNotSupported } @@ -456,6 +462,15 @@ func (tx *Transaction) WithBlobTxSidecar(sideCar *BlobTxSidecar) *Transaction { return cpy } +// AuthList returns the authorizations list of the transaction. +func (tx *Transaction) AuthList() []Authorization { + setcodetx, ok := tx.inner.(*SetCodeTx) + if !ok { + return nil + } + return setcodetx.AuthList +} + // BlobGasFeeCapCmp compares the blob fee cap of two transactions. func (tx *Transaction) BlobGasFeeCapCmp(other *Transaction) int { return tx.BlobGasFeeCap().Cmp(other.BlobGasFeeCap()) @@ -596,6 +611,7 @@ type Message struct { expiredTime uint64 blobGasFeeCap *big.Int blobHashes []common.Hash + authList []Authorization } // Create a new message with payer is the same as from, expired time = 0 @@ -647,6 +663,7 @@ func (tx *Transaction) AsMessage(s Signer, baseFee *big.Int) (Message, error) { expiredTime: tx.ExpiredTime(), blobGasFeeCap: tx.BlobGasFeeCap(), blobHashes: tx.BlobHashes(), + authList: tx.AuthList(), } // If baseFee provided, set gasPrice to effectiveGasPrice. if baseFee != nil { @@ -691,6 +708,7 @@ func (m Message) ExpiredTime() uint64 { return m.expiredTime } func (m Message) BlobHashes() []common.Hash { return m.blobHashes } func (m Message) BlobGasFeeCap() *big.Int { return m.blobGasFeeCap } +func (m Message) AuthList() []Authorization { return m.authList } // 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 1711a941d..46f802fe3 100644 --- a/core/types/transaction_marshalling.go +++ b/core/types/transaction_marshalling.go @@ -44,6 +44,8 @@ type txJSON struct { R *hexutil.Big `json:"r"` S *hexutil.Big `json:"s"` To *common.Address `json:"to"` + AuthorizationList []Authorization `json:"authorizationList,omitempty"` + YParity *hexutil.Uint64 `json:"yParity,omitempty"` // Access list transaction fields: ChainID *hexutil.Big `json:"chainId,omitempty"` @@ -65,6 +67,26 @@ type txJSON struct { Hash common.Hash `json:"hash"` } +// yParityValue returns the YParity value from JSON. For backwards-compatibility reasons, +// this can be given in the 'v' field or the 'yParity' field. If both exist, they must match. +func (tx *txJSON) yParityValue() (*big.Int, error) { + if tx.YParity != nil { + val := uint64(*tx.YParity) + if val != 0 && val != 1 { + return nil, errInvalidYParity + } + bigval := new(big.Int).SetUint64(val) + if tx.V != nil && tx.V.ToInt().Cmp(bigval) != 0 { + return nil, errVYParityMismatch + } + return bigval, nil + } + if tx.V != nil { + return tx.V.ToInt(), nil + } + return nil, errVYParityMissing +} + // MarshalJSON marshals as JSON with a hash. func (t *Transaction) MarshalJSON() ([]byte, error) { var enc txJSON @@ -117,6 +139,14 @@ func (t *Transaction) MarshalJSON() ([]byte, error) { enc.Commitments = tx.Sidecar.Commitments enc.Proofs = tx.Sidecar.Proofs } + case *SetCodeTx: + enc.ChainID = (*hexutil.Big)(new(big.Int).SetUint64(tx.ChainID)) + enc.MaxFeePerGas = (*hexutil.Big)(tx.GasFeeCap.ToBig()) + enc.MaxPriorityFeePerGas = (*hexutil.Big)(tx.GasTipCap.ToBig()) + enc.AccessList = &tx.AccessList + enc.AuthorizationList = tx.AuthList + yparity := tx.V.Uint64() + enc.YParity = (*hexutil.Uint64)(&yparity) } return json.Marshal(&enc) } @@ -148,10 +178,10 @@ func (t *Transaction) UnmarshalJSON(input []byte) error { 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, err := dec.yParityValue() + if err != nil { + return err } - v := (*big.Int)(dec.V) if dec.R == nil { return errors.New("missing required field 'r' in transaction") } @@ -176,6 +206,10 @@ func (t *Transaction) UnmarshalJSON(input []byte) error { var inner TxData switch dec.Type { case LegacyTxType: + if dec.V == nil { + return errors.New("missing required field 'v' in transaction") + } + itx := LegacyTx{ Nonce: nonce, Gas: gas, @@ -295,9 +329,6 @@ func (t *Transaction) UnmarshalJSON(input []byte) error { Gas: gas, Value: uint256.MustFromBig(value), Data: data, - V: uint256.MustFromBig(v), - R: uint256.MustFromBig(r), - S: uint256.MustFromBig(s), } inner = &itx if dec.ChainID == nil { @@ -316,7 +347,7 @@ func (t *Transaction) UnmarshalJSON(input []byte) error { return errors.New("missing required field 'to' in transaction") } itx.To = *to - + if dec.MaxFeePerBlobGas == nil { return errors.New("missing required field 'maxFeePerBlobGas' in transaction") } @@ -328,6 +359,73 @@ func (t *Transaction) UnmarshalJSON(input []byte) error { if dec.AccessList != nil { itx.AccessList = *dec.AccessList } + // signature R + var overflow bool + itx.R, overflow = uint256.FromBig(r) + if overflow { + return errors.New("'r' value overflows uint256") + } + // signature S + itx.S, overflow = uint256.FromBig(s) + if overflow { + return errors.New("'s' value overflows uint256") + } + itx.V, overflow = uint256.FromBig(v) + if overflow { + return errors.New("'v' value overflows uint256") + } + case SetCodeTxType: + itx := SetCodeTx{ + Nonce: nonce, + Gas: gas, + Value: uint256.MustFromBig(value), + Data: data, + } + inner = &itx + if dec.ChainID == nil { + return errors.New("missing required field 'chainId' in transaction") + } + itx.ChainID = dec.ChainID.ToInt().Uint64() + if dec.To == nil { + return errors.New("missing required field 'to' in transaction") + } + itx.To = *dec.To + if dec.MaxPriorityFeePerGas == nil { + return errors.New("missing required field 'maxPriorityFeePerGas' for txdata") + } + itx.GasTipCap = uint256.MustFromBig((*big.Int)(dec.MaxPriorityFeePerGas)) + if dec.MaxFeePerGas == nil { + return errors.New("missing required field 'maxFeePerGas' for txdata") + } + itx.GasFeeCap = uint256.MustFromBig((*big.Int)(dec.MaxFeePerGas)) + if dec.AccessList != nil { + itx.AccessList = *dec.AccessList + } + if dec.AuthorizationList == nil { + return errors.New("missing required field 'authorizationList' in transaction") + } + itx.AuthList = dec.AuthorizationList + + // signature R + var overflow bool + itx.R, overflow = uint256.FromBig(r) + if overflow { + return errors.New("'r' value overflows uint256") + } + // signature S + itx.S, overflow = uint256.FromBig(s) + if overflow { + return errors.New("'s' value overflows uint256") + } + itx.V, overflow = uint256.FromBig(v) + if overflow { + return errors.New("'v' value overflows uint256") + } + if itx.V.Sign() != 0 || itx.R.Sign() != 0 || itx.S.Sign() != 0 { + if err := sanityCheckSignature(v, itx.R.ToBig(), itx.S.ToBig(), false); err != nil { + return err + } + } default: return ErrTxTypeNotSupported } diff --git a/core/types/transaction_signing.go b/core/types/transaction_signing.go index dd4c4a020..ce9fe342f 100644 --- a/core/types/transaction_signing.go +++ b/core/types/transaction_signing.go @@ -43,6 +43,8 @@ type sigCache struct { func MakeSigner(config *params.ChainConfig, blockNumber *big.Int) Signer { var signer Signer switch { + case config.IsPrague(blockNumber): + signer = NewPragueSigner(config.ChainID) case config.IsCancun(blockNumber): signer = NewCancunSigner(config.ChainID) case config.IsLondon(blockNumber): @@ -70,6 +72,9 @@ func MakeSigner(config *params.ChainConfig, blockNumber *big.Int) Signer { // have the current block number available, use MakeSigner instead. func LatestSigner(config *params.ChainConfig) Signer { if config.ChainID != nil { + if config.PragueBlock != nil { + return NewPragueSigner(config.ChainID) + } if config.CancunBlock != nil { return NewCancunSigner(config.ChainID) } @@ -100,7 +105,7 @@ func LatestSignerForChainID(chainID *big.Int) Signer { if chainID == nil { return HomesteadSigner{} } - return NewCancunSigner(chainID) + return NewPragueSigner(chainID) } // SignTx signs the transaction using the given signer and private key. @@ -239,6 +244,81 @@ type Signer interface { Payer(tx *Transaction) (common.Address, error) } +type pragueSigner struct{ cancunSigner } + +// NewPragueSigner returns a signer that accepts +// - EIP-7702 set code transactions +// - EIP-4844 blob transactions +// - EIP-1559 dynamic fee transactions +// - EIP-2930 access list transactions, +// - EIP-155 replay protected transactions, and +// - legacy Homestead transactions. +func NewPragueSigner(chainId *big.Int) Signer { + signer, _ := NewCancunSigner(chainId).(cancunSigner) + return pragueSigner{signer} +} + +func (s pragueSigner) Sender(tx *Transaction) (common.Address, error) { + if tx.Type() != SetCodeTxType { + return s.cancunSigner.Sender(tx) + } + V, R, S := tx.RawSignatureValues() + + // Set code txs are defined to use 0 and 1 as their recovery + // id, add 27 to become equivalent to unprotected Homestead signatures. + V = new(big.Int).Add(V, big.NewInt(27)) + if tx.ChainId().Cmp(s.chainId) != 0 { + return common.Address{}, fmt.Errorf("%w: have %d want %d", ErrInvalidChainId, tx.ChainId(), s.chainId) + } + return recoverPlain(s.Hash(tx), R, S, V, true) +} + +func (s pragueSigner) Equal(s2 Signer) bool { + x, ok := s2.(pragueSigner) + return ok && x.chainId.Cmp(s.chainId) == 0 +} + +func (s pragueSigner) SignatureValues(tx *Transaction, sig []byte) (R, S, V *big.Int, err error) { + txdata, ok := tx.inner.(*SetCodeTx) + if !ok { + return s.cancunSigner.SignatureValues(tx, sig) + } + // 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. + if txdata.ChainID != 0 && new(big.Int).SetUint64(txdata.ChainID).Cmp(s.chainId) != 0 { + return nil, nil, nil, fmt.Errorf("%w: have %d want %d", ErrInvalidChainId, txdata.ChainID, s.chainId) + } + R, S, _ = decodeSignature(sig) + V = big.NewInt(int64(sig[64])) + return R, S, V, nil +} + +// Hash returns the hash to be signed by the sender. +// It does not uniquely identify the transaction. +func (s pragueSigner) Hash(tx *Transaction) common.Hash { + if tx.Type() != SetCodeTxType { + return s.cancunSigner.Hash(tx) + } + return prefixedRlpHash( + tx.Type(), + []interface{}{ + s.chainId, + tx.Nonce(), + tx.GasTipCap(), + tx.GasFeeCap(), + tx.Gas(), + tx.To(), + tx.Value(), + tx.Data(), + tx.AccessList(), + tx.AuthList(), + }) +} + +func (s pragueSigner) Payer(tx *Transaction) (common.Address, error) { + return payerInternal(s, tx) +} + type cancunSigner struct{ londonSigner } // NewCancunSigner returns a signer that accepts diff --git a/core/types/transaction_test.go b/core/types/transaction_test.go index ff2a4c012..ec40fff17 100644 --- a/core/types/transaction_test.go +++ b/core/types/transaction_test.go @@ -22,6 +22,7 @@ import ( "encoding/json" "errors" "fmt" + "maps" "math/big" "reflect" "testing" @@ -375,6 +376,97 @@ func TestTransactionCoding(t *testing.T) { } } +func TestYParityJSONUnmarshalling(t *testing.T) { + baseJson := map[string]interface{}{ + // type is filled in by the test + "chainId": "0x7", + "nonce": "0x0", + "to": "0x1b442286e32ddcaa6e2570ce9ed85f4b4fc87425", + "gas": "0x124f8", + "gasPrice": "0x693d4ca8", + "maxPriorityFeePerGas": "0x3b9aca00", + "maxFeePerGas": "0x6fc23ac00", + "maxFeePerBlobGas": "0x3b9aca00", + "value": "0x0", + "input": "0x", + "accessList": []interface{}{}, + "blobVersionedHashes": []string{ + "0x010657f37554c781402a22917dee2f75def7ab966d7b770905398eba3c444014", + }, + "authorizationList": []string{}, + + // v and yParity are filled in by the test + "r": "0x2a922afc784d07e98012da29f2f37cae1f73eda78aa8805d3df6ee5dbb41ec1", + "s": "0x4f1f75ae6bcdf4970b4f305da1a15d8c5ddb21f555444beab77c9af2baab14", + } + + tests := []struct { + name string + v string + yParity string + wantErr error + }{ + // Valid v and yParity + {"valid v and yParity, 0x0", "0x0", "0x0", nil}, + {"valid v and yParity, 0x1", "0x1", "0x1", nil}, + + // Valid v, missing yParity + {"valid v, missing yParity, 0x0", "0x0", "", nil}, + {"valid v, missing yParity, 0x1", "0x1", "", nil}, + + // Valid yParity, missing v + {"valid yParity, missing v, 0x0", "", "0x0", nil}, + {"valid yParity, missing v, 0x1", "", "0x1", nil}, + + // Invalid yParity + {"invalid yParity, 0x2", "", "0x2", errInvalidYParity}, + + // Conflicting v and yParity + {"conflicting v and yParity", "0x1", "0x0", errVYParityMismatch}, + + // Missing v and yParity + {"missing v and yParity", "", "", errVYParityMissing}, + } + + // Run for all types that accept yParity + t.Parallel() + for _, txType := range []uint64{ + AccessListTxType, + DynamicFeeTxType, + BlobTxType, + SetCodeTxType, + } { + for _, test := range tests { + t.Run(fmt.Sprintf("txType=%d: %s", txType, test.name), func(t *testing.T) { + // Copy the base json + testJson := maps.Clone(baseJson) + + // Set v, yParity and type + if test.v != "" { + testJson["v"] = test.v + } + if test.yParity != "" { + testJson["yParity"] = test.yParity + } + testJson["type"] = fmt.Sprintf("0x%x", txType) + + // Marshal the JSON + jsonBytes, err := json.Marshal(testJson) + if err != nil { + t.Fatal(err) + } + + // Unmarshal the tx + var tx Transaction + err = tx.UnmarshalJSON(jsonBytes) + if err != test.wantErr { + t.Fatalf("wrong error: got %v, want %v", err, test.wantErr) + } + }) + } + } +} + func encodeDecodeJSON(tx *Transaction) (*Transaction, error) { data, err := json.Marshal(tx) if err != nil { diff --git a/params/config.go b/params/config.go index 7b63a3671..9ee639cec 100644 --- a/params/config.go +++ b/params/config.go @@ -307,8 +307,8 @@ var ( RoninTestnetProfileContractAddress = common.HexToAddress("0x3b67c8D22a91572a6AB18acC9F70787Af04A4043") RoninTestnetFinalityTrackingAddress = common.HexToAddress("0x41aCDFe786171824a037f2Cd6224c5916A58969a") RoninTestnetWhiteListDeployerContractV2Address = common.HexToAddress("0x50a7e07Aa75eB9C04281713224f50403cA79851F") - RoninTestnetTreasuryAddress = common.HexToAddress("0x5cfca565c09cc32bb7ba7222a648f1b014d6c30b") - RoninTestnetChainConfig = &ChainConfig{ + RoninTestnetTreasuryAddress = common.HexToAddress("0x5cfca565c09cc32bb7ba7222a648f1b014d6c30b") + RoninTestnetChainConfig = &ChainConfig{ ChainID: big.NewInt(2021), HomesteadBlock: big.NewInt(0), EIP150Block: big.NewInt(0), @@ -468,6 +468,7 @@ var ( BerlinBlock: big.NewInt(0), LondonBlock: big.NewInt(0), CancunBlock: big.NewInt(0), + PragueBlock: big.NewInt(0), ArrowGlacierBlock: nil, OdysseusBlock: nil, FenixBlock: nil, @@ -612,6 +613,7 @@ type ChainConfig struct { ShanghaiBlock *big.Int `json:"shanghaiBlock,omitempty"` // Shanghai switch block (nil = no fork, 0 = already on activated) CancunBlock *big.Int `json:"cancunBlock,omitempty"` // Cancun switch block (nil = no fork, 0 = already on activated) VenokiBlock *big.Int `json:"venokiBlock,omitempty"` // Venoki switch block (nil = no fork, 0 = already on activated) + PragueBlock *big.Int `json:"pragueBlock,omitempty"` // Prague switch block (nil = no fork, 0 = already on activated) BlacklistContractAddress *common.Address `json:"blacklistContractAddress,omitempty"` // Address of Blacklist Contract (nil = no blacklist) FenixValidatorContractAddress *common.Address `json:"fenixValidatorContractAddress,omitempty"` // Address of Ronin Contract in the Fenix hardfork (nil = no blacklist) @@ -745,7 +747,7 @@ func (c *ChainConfig) String() string { chainConfigFmt += "Engine: %v, Blacklist Contract: %v, Fenix Validator Contract: %v, ConsortiumV2: %v, ConsortiumV2.RoninValidatorSet: %v, " chainConfigFmt += "ConsortiumV2.SlashIndicator: %v, ConsortiumV2.StakingContract: %v, Puffy: %v, Buba: %v, Olek: %v, Shillin: %v, Antenna: %v, " chainConfigFmt += "ConsortiumV2.ProfileContract: %v, ConsortiumV2.FinalityTracking: %v, whiteListDeployerContractV2Address: %v, roninTreasuryAddress: %v, " - chainConfigFmt += "Miko: %v, Tripp: %v, TrippPeriod: %v, Aaron: %v, Shanghai: %v, Cancun: %v, Venoki: %v}" + chainConfigFmt += "Miko: %v, Tripp: %v, TrippPeriod: %v, Aaron: %v, Shanghai: %v, Cancun: %v, Venoki: %v, Prague: %v}" return fmt.Sprintf(chainConfigFmt, c.ChainID, @@ -788,6 +790,7 @@ func (c *ChainConfig) String() string { c.ShanghaiBlock, c.CancunBlock, c.VenokiBlock, + c.PragueBlock, ) } @@ -950,6 +953,11 @@ func (c *ChainConfig) IsVenoki(num *big.Int) bool { return isForked(c.VenokiBlock, num) } +// IsPrague returns whether the num is equals to or larger than the prague fork block. +func (c *ChainConfig) IsPrague(num *big.Int) bool { + return isForked(c.PragueBlock, num) +} + // CheckCompatible checks whether scheduled fork transitions have been imported // with a mismatching chain configuration. func (c *ChainConfig) CheckCompatible(newcfg *ChainConfig, height uint64) *ConfigCompatError { @@ -1175,7 +1183,7 @@ type Rules struct { IsBerlin, IsLondon, IsOdysseusFork bool IsFenix, IsShillin, IsConsortiumV2, IsAntenna bool IsMiko, IsTripp, IsAaron, IsShanghai, IsCancun bool - IsVenoki, IsLastConsortiumV1Block bool + IsVenoki, IsLastConsortiumV1Block, IsPrague bool } // Rules ensures c's ChainID is not nil. @@ -1208,5 +1216,6 @@ func (c *ChainConfig) Rules(num *big.Int) Rules { IsShanghai: c.IsShanghai(num), IsCancun: c.IsCancun(num), IsVenoki: c.IsVenoki(num), + IsPrague: c.IsPrague(num), } }