diff --git a/common/hexutil/json.go b/common/hexutil/json.go index 50db208118..e0ac98f52d 100644 --- a/common/hexutil/json.go +++ b/common/hexutil/json.go @@ -23,6 +23,8 @@ import ( "math/big" "reflect" "strconv" + + "github.com/holiman/uint256" ) var ( @@ -30,6 +32,7 @@ var ( bigT = reflect.TypeOf((*Big)(nil)) uintT = reflect.TypeOf(Uint(0)) uint64T = reflect.TypeOf(Uint64(0)) + u256T = reflect.TypeOf((*uint256.Int)(nil)) ) // Bytes marshals/unmarshals as a JSON string with 0x prefix. @@ -225,6 +228,48 @@ func (b *Big) UnmarshalGraphQL(input interface{}) error { return err } +// U256 marshals/unmarshals as a JSON string with 0x prefix. +// The zero value marshals as "0x0". +type U256 uint256.Int + +// MarshalText implements encoding.TextMarshaler +func (b U256) MarshalText() ([]byte, error) { + u256 := (*uint256.Int)(&b) + return []byte(u256.Hex()), nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (b *U256) UnmarshalJSON(input []byte) error { + // The uint256.Int.UnmarshalJSON method accepts "dec", "0xhex"; we must be + // more strict, hence we check string and invoke SetFromHex directly. + if !isString(input) { + return errNonString(u256T) + } + // The hex decoder needs to accept empty string ("") as '0', which uint256.Int + // would reject. + if len(input) == 2 { + (*uint256.Int)(b).Clear() + return nil + } + err := (*uint256.Int)(b).SetFromHex(string(input[1 : len(input)-1])) + if err != nil { + return &json.UnmarshalTypeError{Value: err.Error(), Type: u256T} + } + return nil +} + +// UnmarshalText implements encoding.TextUnmarshaler +func (b *U256) UnmarshalText(input []byte) error { + // The uint256.Int.UnmarshalText method accepts "dec", "0xhex"; we must be + // more strict, hence we check string and invoke SetFromHex directly. + return (*uint256.Int)(b).SetFromHex(string(input)) +} + +// String returns the hex encoding of b. +func (b *U256) String() string { + return (*uint256.Int)(b).Hex() +} + // Uint64 marshals/unmarshals as a JSON string with 0x prefix. // The zero value marshals as "0x0". type Uint64 uint64 diff --git a/core/txpool/txpool.go b/core/txpool/txpool.go index 1a300d95db..b80fe656bd 100644 --- a/core/txpool/txpool.go +++ b/core/txpool/txpool.go @@ -637,6 +637,9 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error { if tx.Type() == types.BlobTxType { return errors.New("BlobTxType of transaction is currently not supported.") } + if tx.Type() == types.SetCodeTxType { + return errors.New("SetCodeTxType of transaction is currently not supported.") + } // Accept only legacy transactions until EIP-2718/2930 activates. if !pool.eip2718 && tx.Type() != types.LegacyTxType { return core.ErrTxTypeNotSupported diff --git a/core/types/block.go b/core/types/block.go index 35a7c853d6..dc970ffd51 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -93,6 +93,9 @@ type Header struct { // ParentBeaconRoot was added by EIP-4788 and is ignored in legacy headers. ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` + + // RequestsHash was added by EIP-7685 and is ignored in legacy headers. + RequestsHash *common.Hash `json:"requestsRoot" rlp:"optional"` } // field type overrides for gencodec @@ -299,6 +302,10 @@ func CopyHeader(h *Header) *Header { cpy.ParentBeaconRoot = new(common.Hash) *cpy.ParentBeaconRoot = *h.ParentBeaconRoot } + if h.RequestsHash != nil { + cpy.RequestsHash = new(common.Hash) + *cpy.RequestsHash = *h.RequestsHash + } return &cpy } @@ -372,7 +379,8 @@ func (b *Block) Header() *Header { return CopyHeader(b.header) } // Body returns the non-header content of the block. func (b *Block) Body() *Body { return &Body{b.transactions, b.uncles, b.withdrawals} } -func (b *Block) BeaconRoot() *common.Hash { return b.header.ParentBeaconRoot } +func (b *Block) BeaconRoot() *common.Hash { return b.header.ParentBeaconRoot } +func (b *Block) RequestsHash() *common.Hash { return b.header.RequestsHash } func (b *Block) ExcessBlobGas() *uint64 { var excessBlobGas *uint64 diff --git a/core/types/gen_authorization.go b/core/types/gen_authorization.go new file mode 100644 index 0000000000..01995c2ae7 --- /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 (s SetCodeAuthorization) MarshalJSON() ([]byte, error) { + type SetCodeAuthorization struct { + ChainID hexutil.U256 `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 hexutil.U256 `json:"r" gencodec:"required"` + S hexutil.U256 `json:"s" gencodec:"required"` + } + var enc SetCodeAuthorization + enc.ChainID = hexutil.U256(s.ChainID) + enc.Address = s.Address + enc.Nonce = hexutil.Uint64(s.Nonce) + enc.V = hexutil.Uint64(s.V) + enc.R = hexutil.U256(s.R) + enc.S = hexutil.U256(s.S) + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals from JSON. +func (s *SetCodeAuthorization) UnmarshalJSON(input []byte) error { + type SetCodeAuthorization struct { + ChainID *hexutil.U256 `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 *hexutil.U256 `json:"r" gencodec:"required"` + S *hexutil.U256 `json:"s" gencodec:"required"` + } + var dec SetCodeAuthorization + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.ChainID == nil { + return errors.New("missing required field 'chainId' for SetCodeAuthorization") + } + s.ChainID = uint256.Int(*dec.ChainID) + if dec.Address == nil { + return errors.New("missing required field 'address' for SetCodeAuthorization") + } + s.Address = *dec.Address + if dec.Nonce == nil { + return errors.New("missing required field 'nonce' for SetCodeAuthorization") + } + s.Nonce = uint64(*dec.Nonce) + if dec.V == nil { + return errors.New("missing required field 'v' for SetCodeAuthorization") + } + s.V = uint8(*dec.V) + if dec.R == nil { + return errors.New("missing required field 'r' for SetCodeAuthorization") + } + s.R = uint256.Int(*dec.R) + if dec.S == nil { + return errors.New("missing required field 's' for SetCodeAuthorization") + } + s.S = uint256.Int(*dec.S) + return nil +} diff --git a/core/types/gen_header_json.go b/core/types/gen_header_json.go index fb1f915d01..322c5d5642 100644 --- a/core/types/gen_header_json.go +++ b/core/types/gen_header_json.go @@ -36,6 +36,7 @@ func (h Header) MarshalJSON() ([]byte, error) { BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed" rlp:"optional"` ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"` ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` + RequestsHash *common.Hash `json:"requestsRoot" rlp:"optional"` Hash common.Hash `json:"hash"` } var enc Header @@ -59,6 +60,7 @@ func (h Header) MarshalJSON() ([]byte, error) { enc.BlobGasUsed = (*hexutil.Uint64)(h.BlobGasUsed) enc.ExcessBlobGas = (*hexutil.Uint64)(h.ExcessBlobGas) enc.ParentBeaconRoot = h.ParentBeaconRoot + enc.RequestsHash = h.RequestsHash enc.Hash = h.Hash() return json.Marshal(&enc) } @@ -86,6 +88,7 @@ func (h *Header) UnmarshalJSON(input []byte) error { BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed" rlp:"optional"` ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas" rlp:"optional"` ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"` + RequestsHash *common.Hash `json:"requestsRoot" rlp:"optional"` } var dec Header if err := json.Unmarshal(input, &dec); err != nil { @@ -163,5 +166,8 @@ func (h *Header) UnmarshalJSON(input []byte) error { if dec.ParentBeaconRoot != nil { h.ParentBeaconRoot = dec.ParentBeaconRoot } + if dec.RequestsHash != nil { + h.RequestsHash = dec.RequestsHash + } return nil } diff --git a/core/types/gen_header_rlp.go b/core/types/gen_header_rlp.go index b91a255a55..f42e6d996a 100644 --- a/core/types/gen_header_rlp.go +++ b/core/types/gen_header_rlp.go @@ -45,7 +45,8 @@ func (obj *Header) EncodeRLP(_w io.Writer) error { _tmp3 := obj.BlobGasUsed != nil _tmp4 := obj.ExcessBlobGas != nil _tmp5 := obj.ParentBeaconRoot != nil - if _tmp1 || _tmp2 || _tmp3 || _tmp4 || _tmp5 { + _tmp6 := obj.RequestsHash != nil + if _tmp1 || _tmp2 || _tmp3 || _tmp4 || _tmp5 || _tmp6 { if obj.BaseFee == nil { w.Write(rlp.EmptyString) } else { @@ -55,34 +56,41 @@ func (obj *Header) EncodeRLP(_w io.Writer) error { w.WriteBigInt(obj.BaseFee) } } - if _tmp2 || _tmp3 || _tmp4 || _tmp5 { + if _tmp2 || _tmp3 || _tmp4 || _tmp5 || _tmp6 { if obj.WithdrawalsHash == nil { w.Write([]byte{0x80}) } else { w.WriteBytes(obj.WithdrawalsHash[:]) } } - if _tmp3 || _tmp4 || _tmp5 { + if _tmp3 || _tmp4 || _tmp5 || _tmp6 { if obj.BlobGasUsed == nil { w.Write([]byte{0x80}) } else { w.WriteUint64((*obj.BlobGasUsed)) } } - if _tmp4 || _tmp5 { + if _tmp4 || _tmp5 || _tmp6 { if obj.ExcessBlobGas == nil { w.Write([]byte{0x80}) } else { w.WriteUint64((*obj.ExcessBlobGas)) } } - if _tmp5 { + if _tmp5 || _tmp6 { if obj.ParentBeaconRoot == nil { w.Write([]byte{0x80}) } else { w.WriteBytes(obj.ParentBeaconRoot[:]) } } + if _tmp6 { + if obj.RequestsHash == nil { + w.Write([]byte{0x80}) + } else { + w.WriteBytes(obj.RequestsHash[:]) + } + } w.ListEnd(_tmp0) return w.Flush() } diff --git a/core/types/receipt.go b/core/types/receipt.go index 73a6a79641..93afb85014 100644 --- a/core/types/receipt.go +++ b/core/types/receipt.go @@ -312,7 +312,7 @@ func (r *Receipt) decodeTyped(b []byte) error { return errShortTypedReceipt } switch b[0] { - case DynamicFeeTxType, AccessListTxType, BlobTxType: + case DynamicFeeTxType, AccessListTxType, BlobTxType, SetCodeTxType: var data receiptRLP err := rlp.DecodeBytes(b[1:], &data) if err != nil { @@ -509,6 +509,9 @@ func (rs Receipts) EncodeIndex(i int, w *bytes.Buffer) { case BlobTxType: w.WriteByte(BlobTxType) rlp.Encode(w, data) + case SetCodeTxType: + w.WriteByte(SetCodeTxType) + rlp.Encode(w, data) default: // For unsupported types, write nothing. Since this is for // DeriveSha, the error will be caught matching the derived hash diff --git a/core/types/transaction.go b/core/types/transaction.go index a29eeee835..f3189c47b3 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -50,6 +50,7 @@ const ( AccessListTxType = 0x01 DynamicFeeTxType = 0x02 BlobTxType = 0x03 + SetCodeTxType = 0x04 ) // Transaction is an Ethereum transaction. @@ -214,6 +215,10 @@ func (tx *Transaction) decodeTyped(b []byte) (TxData, error) { var inner BlobTx err := rlp.DecodeBytes(b[1:], &inner) return &inner, err + case SetCodeTxType: + var inner SetCodeTx + err := rlp.DecodeBytes(b[1:], &inner) + return &inner, err default: return nil, ErrTxTypeNotSupported } @@ -540,6 +545,15 @@ func (tx *Transaction) WithoutBlobTxSidecar() *Transaction { return cpy } +// SetCodeAuthorizations returns the authorizations list of the transaction. +func (tx *Transaction) SetCodeAuthorizations() []SetCodeAuthorization { + setcodetx, ok := tx.inner.(*SetCodeTx) + if !ok { + return nil + } + return setcodetx.AuthList +} + // Hash returns the transaction hash. func (tx *Transaction) Hash() common.Hash { if hash := tx.hash.Load(); hash != nil { diff --git a/core/types/transaction_marshalling.go b/core/types/transaction_marshalling.go index 4253380ec4..9a48af7e1f 100644 --- a/core/types/transaction_marshalling.go +++ b/core/types/transaction_marshalling.go @@ -24,6 +24,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/ethereum/go-ethereum/rlp" "github.com/holiman/uint256" @@ -34,20 +35,26 @@ type txJSON struct { Type hexutil.Uint64 `json:"type"` // Common transaction fields: - Nonce *hexutil.Uint64 `json:"nonce"` - GasPrice *hexutil.Big `json:"gasPrice"` - MaxPriorityFeePerGas *hexutil.Big `json:"maxPriorityFeePerGas"` - MaxFeePerGas *hexutil.Big `json:"maxFeePerGas"` - MaxFeePerBlobGas *hexutil.Big `json:"maxFeePerBlobGas,omitempty"` - Gas *hexutil.Uint64 `json:"gas"` - Value *hexutil.Big `json:"value"` - Data *hexutil.Bytes `json:"input"` - BlobVersionedHashes []common.Hash `json:"blobVersionedHashes,omitempty"` - V *hexutil.Big `json:"v"` - R *hexutil.Big `json:"r"` - S *hexutil.Big `json:"s"` - YParity *hexutil.Uint64 `json:"yParity,omitempty"` - To *common.Address `json:"to"` + Nonce *hexutil.Uint64 `json:"nonce"` + GasPrice *hexutil.Big `json:"gasPrice"` + MaxPriorityFeePerGas *hexutil.Big `json:"maxPriorityFeePerGas"` + MaxFeePerGas *hexutil.Big `json:"maxFeePerGas"` + MaxFeePerBlobGas *hexutil.Big `json:"maxFeePerBlobGas,omitempty"` + Gas *hexutil.Uint64 `json:"gas"` + Value *hexutil.Big `json:"value"` + Data *hexutil.Bytes `json:"input"` + BlobVersionedHashes []common.Hash `json:"blobVersionedHashes,omitempty"` + AuthorizationList []SetCodeAuthorization `json:"authorizationList,omitempty"` + V *hexutil.Big `json:"v"` + R *hexutil.Big `json:"r"` + S *hexutil.Big `json:"s"` + YParity *hexutil.Uint64 `json:"yParity,omitempty"` + To *common.Address `json:"to"` + + // Blob transaction sidecar encoding: + Blobs []kzg4844.Blob `json:"blobs,omitempty"` + Commitments []kzg4844.Commitment `json:"commitments,omitempty"` + Proofs []kzg4844.Proof `json:"proofs,omitempty"` // Deposit transaction fields SourceHash *common.Hash `json:"sourceHash,omitempty"` @@ -129,6 +136,44 @@ func (tx *Transaction) MarshalJSON() ([]byte, error) { enc.V = (*hexutil.Big)(itx.V) enc.R = (*hexutil.Big)(itx.R) enc.S = (*hexutil.Big)(itx.S) + case *BlobTx: + enc.ChainID = (*hexutil.Big)(itx.ChainID.ToBig()) + enc.Nonce = (*hexutil.Uint64)(&itx.Nonce) + enc.Gas = (*hexutil.Uint64)(&itx.Gas) + enc.MaxFeePerGas = (*hexutil.Big)(itx.GasFeeCap.ToBig()) + enc.MaxPriorityFeePerGas = (*hexutil.Big)(itx.GasTipCap.ToBig()) + enc.MaxFeePerBlobGas = (*hexutil.Big)(itx.BlobFeeCap.ToBig()) + enc.Value = (*hexutil.Big)(itx.Value.ToBig()) + enc.Data = (*hexutil.Bytes)(&itx.Data) + enc.AccessList = &itx.AccessList + enc.BlobVersionedHashes = itx.BlobHashes + enc.To = tx.To() + enc.V = (*hexutil.Big)(itx.V.ToBig()) + enc.R = (*hexutil.Big)(itx.R.ToBig()) + enc.S = (*hexutil.Big)(itx.S.ToBig()) + yparity := itx.V.Uint64() + enc.YParity = (*hexutil.Uint64)(&yparity) + if sidecar := itx.Sidecar; sidecar != nil { + enc.Blobs = itx.Sidecar.Blobs + enc.Commitments = itx.Sidecar.Commitments + enc.Proofs = itx.Sidecar.Proofs + } + case *SetCodeTx: + enc.ChainID = (*hexutil.Big)(itx.ChainID.ToBig()) + enc.Nonce = (*hexutil.Uint64)(&itx.Nonce) + enc.To = tx.To() + enc.Gas = (*hexutil.Uint64)(&itx.Gas) + enc.MaxFeePerGas = (*hexutil.Big)(itx.GasFeeCap.ToBig()) + enc.MaxPriorityFeePerGas = (*hexutil.Big)(itx.GasTipCap.ToBig()) + enc.Value = (*hexutil.Big)(itx.Value.ToBig()) + enc.Data = (*hexutil.Bytes)(&itx.Data) + enc.AccessList = &itx.AccessList + enc.AuthorizationList = itx.AuthList + enc.V = (*hexutil.Big)(itx.V.ToBig()) + enc.R = (*hexutil.Big)(itx.R.ToBig()) + enc.S = (*hexutil.Big)(itx.S.ToBig()) + yparity := itx.V.Uint64() + enc.YParity = (*hexutil.Uint64)(&yparity) case *DepositTx: enc.Gas = (*hexutil.Uint64)(&itx.Gas) enc.Value = (*hexutil.Big)(itx.Value) @@ -393,6 +438,83 @@ func (tx *Transaction) UnmarshalJSON(input []byte) error { return err } } + case SetCodeTxType: + var itx SetCodeTx + inner = &itx + if dec.ChainID == nil { + return errors.New("missing required field 'chainId' in transaction") + } + var overflow bool + itx.ChainID, overflow = uint256.FromBig(dec.ChainID.ToInt()) + if overflow { + return errors.New("'chainId' value overflows uint256") + } + if dec.Nonce == nil { + return errors.New("missing required field 'nonce' in transaction") + } + itx.Nonce = uint64(*dec.Nonce) + if dec.To == nil { + return errors.New("missing required field 'to' in transaction") + } + itx.To = *dec.To + if dec.Gas == nil { + return errors.New("missing required field 'gas' for txdata") + } + itx.Gas = uint64(*dec.Gas) + 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.Value == nil { + return errors.New("missing required field 'value' in transaction") + } + itx.Value = uint256.MustFromBig((*big.Int)(dec.Value)) + if dec.Data == nil { + return errors.New("missing required field 'input' in transaction") + } + itx.Data = *dec.Data + 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 + if dec.R == nil { + return errors.New("missing required field 'r' in transaction") + } + itx.R, overflow = uint256.FromBig((*big.Int)(dec.R)) + if overflow { + return errors.New("'r' value overflows uint256") + } + // signature S + if dec.S == nil { + return errors.New("missing required field 's' in transaction") + } + itx.S, overflow = uint256.FromBig((*big.Int)(dec.S)) + if overflow { + return errors.New("'s' value overflows uint256") + } + // signature V + vbig, err := dec.yParityValue() + if err != nil { + return err + } + itx.V, overflow = uint256.FromBig(vbig) + 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(vbig, itx.R.ToBig(), itx.S.ToBig(), false); err != nil { + return err + } + } case DepositTxType: if dec.AccessList != nil || dec.MaxFeePerGas != nil || dec.MaxPriorityFeePerGas != nil { diff --git a/core/types/transaction_marshalling_test.go b/core/types/transaction_marshalling_test.go index 497dd8b020..1e98a5dd8d 100644 --- a/core/types/transaction_marshalling_test.go +++ b/core/types/transaction_marshalling_test.go @@ -9,6 +9,68 @@ import ( "github.com/stretchr/testify/require" ) +var ( + setCodeTx = ` + { + "blockHash": "0x9e77f369a3415aef97a2577ee10d341572d6343f5204551a1ff0f0c2b57e7b3f", + "blockNumber": "0x86236", + "from": "0x1220d2767171ea3a6f4a545eff23efaad4c80221", + "gas": "0xf4240", + "gasPrice": "0xe078996", + "maxFeePerGas": "0xe078996", + "maxPriorityFeePerGas": "0xe07898f", + "hash": "0xa61cdf2dde1acc00b4fcd9e550d660b07ffe77082b411425cddd5fe6cc824e5f", + "input": "0xbeabacc8000000000000000000000000792c9e49df017c00cc265cc9c09d4130c0eaddc6000000000000000000000000cdd5134b4be40f679eac600fae02600004c94e0a0000000000000000000000000000000000000000000000000de0b6b3a7640000", + "nonce": "0x6", + "to": "0x13023d2f562b656b3c35e65956cf527c893d5fdf", + "transactionIndex": "0x0", + "value": "0x0", + "type": "0x4", + "accessList": [], + "chainId": "0x1a5ee289c", + "authorizationList": [ + { + "chainId": "0x1a5ee289c", + "address": "0xd7eb2c2b3c979fb14b94a523a102bc2d593b9080", + "nonce": "0x2", + "v": "0x0", + "r": "0x928c8a2a359bd28d3b94ab450589fc34f27677d605dc37c9300572844d059275", + "s": "0x4b9b4025c47e9ef04c99ad702aeb8056bc4f08c960c69226c3605466f316ca27" + } + ], + "v": "0x0", + "r": "0x733c3cf5e701e6873b72c65273b290a993dc4b0612545cdc5b41950848182874", + "s": "0x306ce694afab15d06344334e1e89960834ec691e863753d9aea53a22902f2cdd", + "yParity": "0x0" + } +` +) + +func TestUnmarshalSetCodeTx(t *testing.T) { + tests := []struct { + name string + json string + expectedError string + }{ + { + name: "UnmarshalSetCodeTx", + json: setCodeTx, + expectedError: "", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var parsedTx = &Transaction{} + err := json.Unmarshal([]byte(test.json), &parsedTx) + if test.expectedError == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, test.expectedError) + } + }) + } +} + func TestTransactionUnmarshalJsonDeposit(t *testing.T) { tx := NewTx(&DepositTx{ SourceHash: common.HexToHash("0x1234"), diff --git a/core/types/transaction_signing.go b/core/types/transaction_signing.go index 7b5b0e0786..e057254867 100644 --- a/core/types/transaction_signing.go +++ b/core/types/transaction_signing.go @@ -87,7 +87,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. @@ -170,6 +170,77 @@ type Signer interface { Equal(Signer) bool } +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.Sign() != 0 && txdata.ChainID.CmpBig(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.SetCodeAuthorizations(), + }) +} + type cancunSigner struct{ londonSigner } // NewCancunSigner returns a signer that accepts diff --git a/core/types/tx_setcoce.go b/core/types/tx_setcoce.go new file mode 100644 index 0000000000..9711cf7f15 --- /dev/null +++ b/core/types/tx_setcoce.go @@ -0,0 +1,226 @@ +// 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 *uint256.Int + 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 []SetCodeAuthorization + + // Signature values + V *uint256.Int + R *uint256.Int + S *uint256.Int +} + +//go:generate go run github.com/fjl/gencodec -type SetCodeAuthorization -field-override authorizationMarshaling -out gen_authorization.go + +// SetCodeAuthorization is an authorization from an account to deploy code at its address. +type SetCodeAuthorization struct { + ChainID uint256.Int `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.U256 + Nonce hexutil.Uint64 + V hexutil.Uint64 + R hexutil.U256 + S hexutil.U256 +} + +// SignSetCode creates a signed the SetCode authorization. +func SignSetCode(prv *ecdsa.PrivateKey, auth SetCodeAuthorization) (SetCodeAuthorization, error) { + sighash := auth.sigHash() + sig, err := crypto.Sign(sighash[:], prv) + if err != nil { + return SetCodeAuthorization{}, err + } + r, s, _ := decodeSignature(sig) + return SetCodeAuthorization{ + ChainID: auth.ChainID, + Address: auth.Address, + Nonce: auth.Nonce, + V: sig[64], + R: *uint256.MustFromBig(r), + S: *uint256.MustFromBig(s), + }, nil +} + +func (a *SetCodeAuthorization) sigHash() common.Hash { + return prefixedRlpHash(0x05, []any{ + a.ChainID, + a.Address, + a.Nonce, + }) +} + +// Authority recovers the the authorizing account of an authorization. +func (a *SetCodeAuthorization) 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([]SetCodeAuthorization, len(tx.AuthList)), + Value: new(uint256.Int), + ChainID: new(uint256.Int), + 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.ChainID != nil { + cpy.ChainID.Set(tx.ChainID) + } + 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 tx.ChainID.ToBig() } +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) isSystemTx() bool { return false } + +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 = uint256.MustFromBig(chainID) + 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/tx_setcode_test.go b/core/types/tx_setcode_test.go new file mode 100644 index 0000000000..d0544573cf --- /dev/null +++ b/core/types/tx_setcode_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/graphql/graphql.go b/graphql/graphql.go index 65d2b627f4..ada2def439 100644 --- a/graphql/graphql.go +++ b/graphql/graphql.go @@ -281,7 +281,7 @@ func (t *Transaction) MaxFeePerGas(ctx context.Context) (*hexutil.Big, error) { switch tx.Type() { case types.AccessListTxType: return nil, nil - case types.DynamicFeeTxType: + case types.DynamicFeeTxType, types.BlobTxType, types.SetCodeTxType: return (*hexutil.Big)(tx.GasFeeCap()), nil default: return nil, nil @@ -296,13 +296,25 @@ func (t *Transaction) MaxPriorityFeePerGas(ctx context.Context) (*hexutil.Big, e switch tx.Type() { case types.AccessListTxType: return nil, nil - case types.DynamicFeeTxType: + case types.DynamicFeeTxType, types.BlobTxType, types.SetCodeTxType: return (*hexutil.Big)(tx.GasTipCap()), nil default: return nil, nil } } +func (t *Transaction) BlobVersionedHashes(ctx context.Context) *[]common.Hash { + tx, _ := t.resolve(ctx) + if tx == nil { + return nil + } + if tx.Type() != types.BlobTxType { + return nil + } + blobHashes := tx.BlobHashes() + return &blobHashes +} + func (t *Transaction) EffectiveTip(ctx context.Context) (*hexutil.Big, error) { tx, err := t.resolve(ctx) if err != nil || tx == nil { @@ -439,6 +451,40 @@ func (t *Transaction) CumulativeGasUsed(ctx context.Context) (*Long, error) { return &ret, nil } +func (t *Transaction) BlobGasUsed(ctx context.Context) (*hexutil.Uint64, error) { + tx, _ := t.resolve(ctx) + if tx == nil { + return nil, nil + } + if tx.Type() != types.BlobTxType { + return nil, nil + } + + receipt, err := t.getReceipt(ctx) + if err != nil || receipt == nil { + return nil, err + } + ret := hexutil.Uint64(receipt.BlobGasUsed) + return &ret, nil +} + +func (t *Transaction) BlobGasPrice(ctx context.Context) (*hexutil.Big, error) { + tx, _ := t.resolve(ctx) + if tx == nil { + return nil, nil + } + if tx.Type() != types.BlobTxType { + return nil, nil + } + + receipt, err := t.getReceipt(ctx) + if err != nil || receipt == nil { + return nil, err + } + ret := (*hexutil.Big)(receipt.BlobGasPrice) + return ret, nil +} + func (t *Transaction) CreatedContract(ctx context.Context, args BlockNumberArgs) (*Account, error) { receipt, err := t.getReceipt(ctx) if err != nil || receipt == nil || receipt.ContractAddress == (common.Address{}) {