From 96aae3d99707999142807411f959c2895007cf44 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Thu, 24 Feb 2022 16:12:48 -0700 Subject: [PATCH] core, params: prototype withdrawal transactions --- core/block_validator.go | 52 ++++++++++++++++++-- core/state_processor.go | 21 ++++++++- core/types/transaction.go | 44 ++++++++++++++++- core/types/transaction_marshalling.go | 14 ++++++ core/types/withdrawal_tx.go | 68 +++++++++++++++++++++++++++ params/config.go | 6 +++ 6 files changed, 200 insertions(+), 5 deletions(-) create mode 100644 core/types/withdrawal_tx.go diff --git a/core/block_validator.go b/core/block_validator.go index 3763be0be08d..8449070e4b4c 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -17,6 +17,7 @@ package core import ( + "errors" "fmt" "github.com/ethereum/go-ethereum/consensus" @@ -46,9 +47,53 @@ func NewBlockValidator(config *params.ChainConfig, blockchain *BlockChain, engin return validator } +func validateOnlyWithdrawalTransactions(txs []*types.Transaction) bool { + for _, tx := range txs { + if tx.Type() != types.WithdrawalTxType { + return false + } + } + return true +} + +func validateNoWithdrawalTransactions(txs []*types.Transaction) bool { + for _, tx := range txs { + if tx.Type() == types.WithdrawalTxType { + return false + } + } + return true +} + +// Return an error if the `block` fails EIP-4863 transaction ordering validation, or `nil` otherwise. +func validateCorrectTransactionOrdering(block *types.Block) error { + firstUserTx := -1 + txs := block.Transactions() + for i, tx := range txs { + if tx.Type() != types.WithdrawalTxType { + firstUserTx = i + break + } + } + if firstUserTx == -1 { + return nil + } else { + hasCorrectWithdrawalTx := validateOnlyWithdrawalTransactions(txs[:firstUserTx]) + if !hasCorrectWithdrawalTx { + return errors.New("has user transactions where only withdrawal transactions should be in block body") + } + hasCorrectUserTx := validateNoWithdrawalTransactions(txs[firstUserTx:]) + if !hasCorrectUserTx { + return errors.New("has withdrawal transactions where only user transactions should be in block body") + } + } + + return nil +} + // ValidateBody validates the given block's uncles and verifies the block -// header's transaction and uncle roots. The headers are assumed to be already -// validated at this point. +// header's transaction and uncle roots, along with transaction ordering. +// The headers are assumed to be already validated at this point. func (v *BlockValidator) ValidateBody(block *types.Block) error { // Check whether the block's known, and if not, that it's linkable if v.bc.HasBlockAndState(block.Hash(), block.NumberU64()) { @@ -71,7 +116,8 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error { } return consensus.ErrPrunedAncestor } - return nil + // Ensure any withdrawal transactions are before any other type of transaction. + return validateCorrectTransactionOrdering(block) } // ValidateState validates the various changes that happen after a state diff --git a/core/state_processor.go b/core/state_processor.go index d4c77ae41042..1793b2f48f6c 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -92,13 +92,32 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg return receipts, allLogs, *usedGas, nil } +func applyWithdrawalTx(tx *types.Transaction, statedb *state.StateDB) (*ExecutionResult, error) { + statedb.AddBalance(*tx.To(), tx.Value()) + + // TODO emit logs? + // statedb.AddLog(...) + + return &ExecutionResult{ + UsedGas: 0, + Err: nil, + ReturnData: nil, + }, nil +} + func applyTransaction(msg types.Message, config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, evm *vm.EVM) (*types.Receipt, error) { // Create a new context to be used in the EVM environment. txContext := NewEVMTxContext(msg) evm.Reset(txContext, statedb) // Apply the transaction to the current state (included in the env). - result, err := ApplyMessage(evm, msg, gp) + var result *ExecutionResult + var err error + if config.IsShanghai(blockNumber) && tx.Type() == types.WithdrawalTxType { + result, err = applyWithdrawalTx(tx, statedb) + } else { + result, err = ApplyMessage(evm, msg, gp) + } if err != nil { return nil, err } diff --git a/core/types/transaction.go b/core/types/transaction.go index 83f1766e67e2..5e9c9e823cfd 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -19,6 +19,7 @@ package types import ( "bytes" "container/heap" + "encoding/binary" "errors" "io" "math/big" @@ -45,6 +46,7 @@ const ( LegacyTxType = iota AccessListTxType DynamicFeeTxType + WithdrawalTxType ) // Transaction is an Ethereum transaction. @@ -102,10 +104,27 @@ func (tx *Transaction) EncodeRLP(w io.Writer) error { return rlp.Encode(w, buf.Bytes()) } +func encodeWithdrawalTxSSZ(w *bytes.Buffer, tx TxData) error { + to := tx.to() + _, err := w.Write(to[:]) + if err != nil { + return err + } + + value := tx.value() + valueBuf := make([]byte, 32) + value.FillBytes(valueBuf) + return binary.Write(w, binary.LittleEndian, valueBuf) +} + // encodeTyped writes the canonical encoding of a typed transaction to w. func (tx *Transaction) encodeTyped(w *bytes.Buffer) error { w.WriteByte(tx.Type()) - return rlp.Encode(w, tx.inner) + if tx.Type() == WithdrawalTxType { + return encodeWithdrawalTxSSZ(w, tx.inner) + } else { + return rlp.Encode(w, tx.inner) + } } // MarshalBinary returns the canonical encoding of the transaction. @@ -172,6 +191,25 @@ func (tx *Transaction) UnmarshalBinary(b []byte) error { return nil } +func decodeWithdrawalTxSSZ(b []byte, tx *WithdrawalTx) error { + if len(b) != 52 { + return errors.New("withdrawal tx data is incorrectly sized") + } + + to := common.BytesToAddress(b[:20]) + tx.To = &to + + buf := bytes.NewReader(b[20:]) + valueBuf := make([]byte, 32) + err := binary.Read(buf, binary.LittleEndian, &valueBuf) + if err != nil { + return err + } + + tx.Value = new(big.Int).SetBytes(valueBuf) + return nil +} + // decodeTyped decodes a typed transaction from the canonical format. func (tx *Transaction) decodeTyped(b []byte) (TxData, error) { if len(b) == 0 { @@ -186,6 +224,10 @@ func (tx *Transaction) decodeTyped(b []byte) (TxData, error) { var inner DynamicFeeTx err := rlp.DecodeBytes(b[1:], &inner) return &inner, err + case WithdrawalTxType: + var inner WithdrawalTx + err := decodeWithdrawalTxSSZ(b[1:], &inner) + return &inner, err default: return nil, ErrTxTypeNotSupported } diff --git a/core/types/transaction_marshalling.go b/core/types/transaction_marshalling.go index aad31a5a97e2..fd653838d6b6 100644 --- a/core/types/transaction_marshalling.go +++ b/core/types/transaction_marshalling.go @@ -94,6 +94,9 @@ func (t *Transaction) MarshalJSON() ([]byte, error) { enc.V = (*hexutil.Big)(tx.V) enc.R = (*hexutil.Big)(tx.R) enc.S = (*hexutil.Big)(tx.S) + case *WithdrawalTx: + enc.To = t.To() + enc.Value = (*hexutil.Big)(tx.Value) } return json.Marshal(&enc) } @@ -263,6 +266,17 @@ func (t *Transaction) UnmarshalJSON(input []byte) error { } } + case WithdrawalTxType: + var itx WithdrawalTx + inner = &itx + if dec.To == nil { + return errors.New("missing required field 'to' in transaction") + } + itx.To = dec.To + if dec.Value == nil { + return errors.New("missing required field 'value' in transaction") + } + itx.Value = (*big.Int)(dec.Value) default: return ErrTxTypeNotSupported } diff --git a/core/types/withdrawal_tx.go b/core/types/withdrawal_tx.go new file mode 100644 index 000000000000..4c06a4588d4e --- /dev/null +++ b/core/types/withdrawal_tx.go @@ -0,0 +1,68 @@ +// Copyright 2021 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 ( + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +// `WithdrawalTx` mirrors a withdrawal receipt committed to +// on the consensus layer. +// This receipt is guaranteed to have some execution address +// and an amount in Gwei. +// NOTE: the amount is converted to Wei when the transaction data is +// unmarshalled into this struct. +type WithdrawalTx struct { + To *common.Address + Value *big.Int +} + +// copy creates a deep copy of the transaction data and initializes all fields. +func (tx *WithdrawalTx) copy() TxData { + cpy := &WithdrawalTx{ + To: copyAddressPtr(tx.To), + Value: new(big.Int), + } + if tx.Value != nil { + cpy.Value.Set(tx.Value) + } + return cpy +} + +// accessors for innerTx. +func (tx *WithdrawalTx) txType() byte { return WithdrawalTxType } +func (tx *WithdrawalTx) chainID() *big.Int { return new(big.Int) } +func (tx *WithdrawalTx) accessList() AccessList { return nil } +func (tx *WithdrawalTx) data() []byte { return nil } +func (tx *WithdrawalTx) gas() uint64 { return 0 } +func (tx *WithdrawalTx) gasFeeCap() *big.Int { return new(big.Int) } +func (tx *WithdrawalTx) gasTipCap() *big.Int { return new(big.Int) } +func (tx *WithdrawalTx) gasPrice() *big.Int { return new(big.Int) } +func (tx *WithdrawalTx) value() *big.Int { return tx.Value } +func (tx *WithdrawalTx) nonce() uint64 { return 0 } +func (tx *WithdrawalTx) to() *common.Address { return tx.To } + +func (tx *WithdrawalTx) rawSignatureValues() (v, r, s *big.Int) { + // return "zero" values, this method should not be called + return new(big.Int), new(big.Int), new(big.Int) +} + +func (tx *WithdrawalTx) setSignatureValues(chainID, v, r, s *big.Int) { + // no-op, keep for broader compatibility +} diff --git a/params/config.go b/params/config.go index 7f52472ec9dc..5e276b161c1b 100644 --- a/params/config.go +++ b/params/config.go @@ -347,6 +347,7 @@ type ChainConfig struct { LondonBlock *big.Int `json:"londonBlock,omitempty"` // London switch block (nil = no fork, 0 = already on london) ArrowGlacierBlock *big.Int `json:"arrowGlacierBlock,omitempty"` // Eip-4345 (bomb delay) switch block (nil = no fork, 0 = already activated) MergeForkBlock *big.Int `json:"mergeForkBlock,omitempty"` // EIP-3675 (TheMerge) switch block (nil = no fork, 0 = already in merge proceedings) + ShanghaiBlock *big.Int `json:"shanghaiForkBlock,omitempty"` // Shanghai switch block (nil = no fork, 0 = already on shanghai) // TerminalTotalDifficulty is the amount of total difficulty reached by // the network that triggers the consensus upgrade. @@ -475,6 +476,11 @@ func (c *ChainConfig) IsArrowGlacier(num *big.Int) bool { return isForked(c.ArrowGlacierBlock, num) } +// IsShanghai returns whether num is either equal to the Shanghai (EIP-TBD) fork block or greater. +func (c *ChainConfig) IsShanghai(num *big.Int) bool { + return isForked(c.ShanghaiBlock, num) +} + // IsTerminalPoWBlock returns whether the given block is the last block of PoW stage. func (c *ChainConfig) IsTerminalPoWBlock(parentTotalDiff *big.Int, totalDiff *big.Int) bool { if c.TerminalTotalDifficulty == nil {