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),
}
}