From dbdecb55dc327007d2c2272b42d3e1d506b3f5f0 Mon Sep 17 00:00:00 2001 From: lightclient Date: Wed, 4 Sep 2024 14:41:24 +0200 Subject: [PATCH] all: implement eip-7002 EL triggered withdrawal requests --- beacon/engine/gen_ed.go | 82 ++++++++-------- beacon/engine/types.go | 70 ++++++++------ cmd/evm/internal/t8ntool/execution.go | 109 +++++++++++++--------- core/blockchain_test.go | 99 ++++++++++++++++++++ core/chain_makers.go | 7 ++ core/state_processor.go | 40 ++++++++ core/types/gen_withdrawal_request_json.go | 53 +++++++++++ core/types/request.go | 5 +- core/types/withdrawal_request.go | 80 ++++++++++++++++ eth/catalyst/api.go | 32 ++++--- miner/worker.go | 10 +- params/protocol_params.go | 4 +- 12 files changed, 463 insertions(+), 128 deletions(-) create mode 100644 core/types/gen_withdrawal_request_json.go create mode 100644 core/types/withdrawal_request.go diff --git a/beacon/engine/gen_ed.go b/beacon/engine/gen_ed.go index b2eb1dc982..51595644b7 100644 --- a/beacon/engine/gen_ed.go +++ b/beacon/engine/gen_ed.go @@ -17,25 +17,26 @@ var _ = (*executableDataMarshaling)(nil) // MarshalJSON marshals as JSON. func (e ExecutableData) MarshalJSON() ([]byte, error) { type ExecutableData struct { - ParentHash common.Hash `json:"parentHash" gencodec:"required"` - FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` - StateRoot common.Hash `json:"stateRoot" gencodec:"required"` - ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` - LogsBloom hexutil.Bytes `json:"logsBloom" gencodec:"required"` - Random common.Hash `json:"prevRandao" gencodec:"required"` - Number hexutil.Uint64 `json:"blockNumber" gencodec:"required"` - GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` - GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` - Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"` - ExtraData hexutil.Bytes `json:"extraData" gencodec:"required"` - BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` - BlockHash common.Hash `json:"blockHash" gencodec:"required"` - Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` - BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` - ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` - Deposits types.Deposits `json:"depositRequests"` - ExecutionWitness *types.ExecutionWitness `json:"executionWitness,omitempty"` + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom hexutil.Bytes `json:"logsBloom" gencodec:"required"` + Random common.Hash `json:"prevRandao" gencodec:"required"` + Number hexutil.Uint64 `json:"blockNumber" gencodec:"required"` + GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"` + ExtraData hexutil.Bytes `json:"extraData" gencodec:"required"` + BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` + BlockHash common.Hash `json:"blockHash" gencodec:"required"` + Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` + ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` + Deposits types.Deposits `json:"depositRequests"` + WithdrawalRequests types.WithdrawalRequests `json:"withdrawalRequests"` + ExecutionWitness *types.ExecutionWitness `json:"executionWitness,omitempty"` } var enc ExecutableData enc.ParentHash = e.ParentHash @@ -61,6 +62,7 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) { enc.BlobGasUsed = (*hexutil.Uint64)(e.BlobGasUsed) enc.ExcessBlobGas = (*hexutil.Uint64)(e.ExcessBlobGas) enc.Deposits = e.Deposits + enc.WithdrawalRequests = e.WithdrawalRequests enc.ExecutionWitness = e.ExecutionWitness return json.Marshal(&enc) } @@ -68,25 +70,26 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) { // UnmarshalJSON unmarshals from JSON. func (e *ExecutableData) UnmarshalJSON(input []byte) error { type ExecutableData struct { - ParentHash *common.Hash `json:"parentHash" gencodec:"required"` - FeeRecipient *common.Address `json:"feeRecipient" gencodec:"required"` - StateRoot *common.Hash `json:"stateRoot" gencodec:"required"` - ReceiptsRoot *common.Hash `json:"receiptsRoot" gencodec:"required"` - LogsBloom *hexutil.Bytes `json:"logsBloom" gencodec:"required"` - Random *common.Hash `json:"prevRandao" gencodec:"required"` - Number *hexutil.Uint64 `json:"blockNumber" gencodec:"required"` - GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` - GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` - Timestamp *hexutil.Uint64 `json:"timestamp" gencodec:"required"` - ExtraData *hexutil.Bytes `json:"extraData" gencodec:"required"` - BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` - BlockHash *common.Hash `json:"blockHash" gencodec:"required"` - Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` - BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` - ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` - Deposits *types.Deposits `json:"depositRequests"` - ExecutionWitness *types.ExecutionWitness `json:"executionWitness,omitempty"` + ParentHash *common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient *common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot *common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot *common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom *hexutil.Bytes `json:"logsBloom" gencodec:"required"` + Random *common.Hash `json:"prevRandao" gencodec:"required"` + Number *hexutil.Uint64 `json:"blockNumber" gencodec:"required"` + GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Timestamp *hexutil.Uint64 `json:"timestamp" gencodec:"required"` + ExtraData *hexutil.Bytes `json:"extraData" gencodec:"required"` + BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` + BlockHash *common.Hash `json:"blockHash" gencodec:"required"` + Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` + ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` + Deposits *types.Deposits `json:"depositRequests"` + WithdrawalRequests *types.WithdrawalRequests `json:"withdrawalRequests"` + ExecutionWitness *types.ExecutionWitness `json:"executionWitness,omitempty"` } var dec ExecutableData if err := json.Unmarshal(input, &dec); err != nil { @@ -163,6 +166,9 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error { if dec.Deposits != nil { e.Deposits = *dec.Deposits } + if dec.WithdrawalRequests != nil { + e.WithdrawalRequests = *dec.WithdrawalRequests + } if dec.ExecutionWitness != nil { e.ExecutionWitness = dec.ExecutionWitness } diff --git a/beacon/engine/types.go b/beacon/engine/types.go index 19d2b2823e..cb735c561d 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -60,25 +60,26 @@ type payloadAttributesMarshaling struct { // ExecutableData is the data necessary to execute an EL payload. type ExecutableData struct { - ParentHash common.Hash `json:"parentHash" gencodec:"required"` - FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` - StateRoot common.Hash `json:"stateRoot" gencodec:"required"` - ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` - LogsBloom []byte `json:"logsBloom" gencodec:"required"` - Random common.Hash `json:"prevRandao" gencodec:"required"` - Number uint64 `json:"blockNumber" gencodec:"required"` - GasLimit uint64 `json:"gasLimit" gencodec:"required"` - GasUsed uint64 `json:"gasUsed" gencodec:"required"` - Timestamp uint64 `json:"timestamp" gencodec:"required"` - ExtraData []byte `json:"extraData" gencodec:"required"` - BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"` - BlockHash common.Hash `json:"blockHash" gencodec:"required"` - Transactions [][]byte `json:"transactions" gencodec:"required"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` - BlobGasUsed *uint64 `json:"blobGasUsed"` - ExcessBlobGas *uint64 `json:"excessBlobGas"` - Deposits types.Deposits `json:"depositRequests"` - ExecutionWitness *types.ExecutionWitness `json:"executionWitness,omitempty"` + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom []byte `json:"logsBloom" gencodec:"required"` + Random common.Hash `json:"prevRandao" gencodec:"required"` + Number uint64 `json:"blockNumber" gencodec:"required"` + GasLimit uint64 `json:"gasLimit" gencodec:"required"` + GasUsed uint64 `json:"gasUsed" gencodec:"required"` + Timestamp uint64 `json:"timestamp" gencodec:"required"` + ExtraData []byte `json:"extraData" gencodec:"required"` + BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"` + BlockHash common.Hash `json:"blockHash" gencodec:"required"` + Transactions [][]byte `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + BlobGasUsed *uint64 `json:"blobGasUsed"` + ExcessBlobGas *uint64 `json:"excessBlobGas"` + Deposits types.Deposits `json:"depositRequests"` + WithdrawalRequests types.WithdrawalRequests `json:"withdrawalRequests"` + ExecutionWitness *types.ExecutionWitness `json:"executionWitness,omitempty"` } // JSON type overrides for executableData. @@ -233,7 +234,9 @@ func ExecutableDataToBlock(data ExecutableData, versionedHashes []common.Hash, b h := types.DeriveSha(types.Withdrawals(data.Withdrawals), trie.NewStackTrie(nil)) withdrawalsRoot = &h } - // Compute requestsHash if any requests are non-nil. + // Only set requestsHash if there exists requests in the ExecutableData. This + // allows CLs to continue using the data structure before requests are + // enabled. var ( requestsHash *common.Hash requests types.Requests @@ -243,9 +246,17 @@ func ExecutableDataToBlock(data ExecutableData, versionedHashes []common.Hash, b for _, d := range data.Deposits { requests = append(requests, types.NewRequest(d)) } + } + if data.WithdrawalRequests != nil { + for _, w := range data.WithdrawalRequests { + requests = append(requests, types.NewRequest(w)) + } + } + if requests != nil { h := types.DeriveSha(requests, trie.NewStackTrie(nil)) requestsHash = &h } + header := &types.Header{ ParentHash: data.ParentHash, UncleHash: types.EmptyUncleHash, @@ -320,22 +331,27 @@ func BlockToExecutableData(block *types.Block, fees *big.Int, sidecars []*types. // assigns them to the associated fields in ExecutableData. func setRequests(requests types.Requests, data *ExecutableData) { if requests != nil { - // If requests is non-nil, it means deposits are available in block and we - // should return an empty slice instead of nil if there are no deposits. + // If requests is non-nil, it means the requests are available in block and + // we should return an empty slice instead of nil. data.Deposits = make(types.Deposits, 0) + data.WithdrawalRequests = make(types.WithdrawalRequests, 0) } for _, r := range requests { - if d, ok := r.Inner().(*types.Deposit); ok { - data.Deposits = append(data.Deposits, d) + switch v := r.Inner().(type) { + case *types.Deposit: + data.Deposits = append(data.Deposits, v) + case *types.WithdrawalRequest: + data.WithdrawalRequests = append(data.WithdrawalRequests, v) } } } // ExecutionPayloadBody is used in the response to GetPayloadBodiesByHash and GetPayloadBodiesByRange type ExecutionPayloadBody struct { - TransactionData []hexutil.Bytes `json:"transactions"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` - Deposits types.Deposits `json:"depositRequests"` + TransactionData []hexutil.Bytes `json:"transactions"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + Deposits types.Deposits `json:"depositRequests"` + WithdrawalRequests types.WithdrawalRequests `json:"withdrawalRequests"` } // Client identifiers to support ClientVersionV1. diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go index 5fd1d6a4a6..68d9a97e26 100644 --- a/cmd/evm/internal/t8ntool/execution.go +++ b/cmd/evm/internal/t8ntool/execution.go @@ -53,21 +53,22 @@ type Prestate struct { // ExecutionResult contains the execution status after running a state test, any // error that might have occurred and a dump of the final state if requested. type ExecutionResult struct { - StateRoot common.Hash `json:"stateRoot"` - TxRoot common.Hash `json:"txRoot"` - ReceiptRoot common.Hash `json:"receiptsRoot"` - LogsHash common.Hash `json:"logsHash"` - Bloom types.Bloom `json:"logsBloom" gencodec:"required"` - Receipts types.Receipts `json:"receipts"` - Rejected []*rejectedTx `json:"rejected,omitempty"` - Difficulty *math.HexOrDecimal256 `json:"currentDifficulty" gencodec:"required"` - GasUsed math.HexOrDecimal64 `json:"gasUsed"` - BaseFee *math.HexOrDecimal256 `json:"currentBaseFee,omitempty"` - WithdrawalsRoot *common.Hash `json:"withdrawalsRoot,omitempty"` - CurrentExcessBlobGas *math.HexOrDecimal64 `json:"currentExcessBlobGas,omitempty"` - CurrentBlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed,omitempty"` - RequestsHash *common.Hash `json:"requestsRoot,omitempty"` - DepositRequests *types.Deposits `json:"depositRequests,omitempty"` + StateRoot common.Hash `json:"stateRoot"` + TxRoot common.Hash `json:"txRoot"` + ReceiptRoot common.Hash `json:"receiptsRoot"` + LogsHash common.Hash `json:"logsHash"` + Bloom types.Bloom `json:"logsBloom" gencodec:"required"` + Receipts types.Receipts `json:"receipts"` + Rejected []*rejectedTx `json:"rejected,omitempty"` + Difficulty *math.HexOrDecimal256 `json:"currentDifficulty" gencodec:"required"` + GasUsed math.HexOrDecimal64 `json:"gasUsed"` + BaseFee *math.HexOrDecimal256 `json:"currentBaseFee,omitempty"` + WithdrawalsRoot *common.Hash `json:"withdrawalsRoot,omitempty"` + CurrentExcessBlobGas *math.HexOrDecimal64 `json:"currentExcessBlobGas,omitempty"` + CurrentBlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed,omitempty"` + RequestsHash *common.Hash `json:"requestsRoot,omitempty"` + DepositRequests types.Deposits `json:"depositRequests,omitempty"` + WithdrawalRequests types.WithdrawalRequests `json:"withdrawalRequests,omitempty"` } type ommer struct { @@ -354,33 +355,14 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, amount := new(big.Int).Mul(new(big.Int).SetUint64(w.Amount), big.NewInt(params.GWei)) statedb.AddBalance(w.Address, uint256.MustFromBig(amount), tracing.BalanceIncreaseWithdrawal) } - // Commit block - root, err := statedb.Commit(vmContext.BlockNumber.Uint64(), chainConfig.IsEIP158(vmContext.BlockNumber)) - if err != nil { - return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("could not commit state: %v", err)) - } - execRs := &ExecutionResult{ - StateRoot: root, - TxRoot: types.DeriveSha(includedTxs, trie.NewStackTrie(nil)), - ReceiptRoot: types.DeriveSha(receipts, trie.NewStackTrie(nil)), - Bloom: types.CreateBloom(receipts), - LogsHash: rlpHash(statedb.Logs()), - Receipts: receipts, - Rejected: rejectedTxs, - Difficulty: (*math.HexOrDecimal256)(vmContext.Difficulty), - GasUsed: (math.HexOrDecimal64)(gasUsed), - BaseFee: (*math.HexOrDecimal256)(vmContext.BaseFee), - } - if pre.Env.Withdrawals != nil { - h := types.DeriveSha(types.Withdrawals(pre.Env.Withdrawals), trie.NewStackTrie(nil)) - execRs.WithdrawalsRoot = &h - } - if vmContext.BlobBaseFee != nil { - execRs.CurrentExcessBlobGas = (*math.HexOrDecimal64)(&excessBlobGas) - execRs.CurrentBlobGasUsed = (*math.HexOrDecimal64)(&blobGasUsed) - } + // Retrieve deposit and withdrawal requests + var ( + depositRequests types.Deposits + withdrawalRequests types.WithdrawalRequests + requestsHash *common.Hash + ) if chainConfig.IsPrague(vmContext.BlockNumber, vmContext.Time) { - // Parse the requests from the logs + // Parse deposit requests from the logs var allLogs []*types.Log for _, receipt := range receipts { allLogs = append(allLogs, receipt.Logs...) @@ -389,17 +371,52 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, if err != nil { return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("could not parse requests logs: %v", err)) } + // Process the withdrawal requests contract execution + vmenv := vm.NewEVM(vmContext, vm.TxContext{}, statedb, chainConfig, vmConfig) + wxs := core.ProcessDequeueWithdrawalRequests(vmenv, statedb) + requests = append(requests, wxs...) // Calculate the requests root h := types.DeriveSha(requests, trie.NewStackTrie(nil)) - execRs.RequestsHash = &h + requestsHash = &h // Get the deposits from the requests - deposits := make(types.Deposits, 0) + depositRequests = make(types.Deposits, 0) + withdrawalRequests = make(types.WithdrawalRequests, 0) for _, req := range requests { - if dep, ok := req.Inner().(*types.Deposit); ok { - deposits = append(deposits, dep) + switch v := req.Inner().(type) { + case *types.Deposit: + depositRequests = append(depositRequests, v) + case *types.WithdrawalRequest: + withdrawalRequests = append(withdrawalRequests, v) } } - execRs.DepositRequests = &deposits + } + // Commit block + root, err := statedb.Commit(vmContext.BlockNumber.Uint64(), chainConfig.IsEIP158(vmContext.BlockNumber)) + if err != nil { + return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("could not commit state: %v", err)) + } + execRs := &ExecutionResult{ + StateRoot: root, + TxRoot: types.DeriveSha(includedTxs, trie.NewStackTrie(nil)), + ReceiptRoot: types.DeriveSha(receipts, trie.NewStackTrie(nil)), + Bloom: types.CreateBloom(receipts), + LogsHash: rlpHash(statedb.Logs()), + Receipts: receipts, + Rejected: rejectedTxs, + Difficulty: (*math.HexOrDecimal256)(vmContext.Difficulty), + GasUsed: (math.HexOrDecimal64)(gasUsed), + BaseFee: (*math.HexOrDecimal256)(vmContext.BaseFee), + RequestsHash: requestsHash, + DepositRequests: depositRequests, + WithdrawalRequests: withdrawalRequests, + } + if pre.Env.Withdrawals != nil { + h := types.DeriveSha(types.Withdrawals(pre.Env.Withdrawals), trie.NewStackTrie(nil)) + execRs.WithdrawalsRoot = &h + } + if vmContext.BlobBaseFee != nil { + execRs.CurrentExcessBlobGas = (*math.HexOrDecimal64)(&excessBlobGas) + execRs.CurrentBlobGasUsed = (*math.HexOrDecimal64)(&blobGasUsed) } // Re-create statedb instance with new root upon the updated database // for accessing latest states. diff --git a/core/blockchain_test.go b/core/blockchain_test.go index d8c436fcd6..92611d5661 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -17,6 +17,7 @@ package core import ( + "encoding/binary" "errors" "fmt" "math/big" @@ -4314,3 +4315,101 @@ func TestEIP6110(t *testing.T) { } } } + +// TestEIP7002 verifies that withdrawal requests are processed correctly in the +// pre-deploy and parsed out correctly via the system call. +func TestEIP7002(t *testing.T) { + var ( + engine = beacon.NewFaker() + + // A sender who makes transactions, has some funds + key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + addr = crypto.PubkeyToAddress(key.PublicKey) + funds = new(big.Int).Mul(common.Big1, big.NewInt(params.Ether)) + config = *params.AllEthashProtocolChanges + gspec = &Genesis{ + Config: &config, + Alloc: types.GenesisAlloc{ + addr: {Balance: funds}, + params.WithdrawalRequestsAddress: {Code: common.FromHex("3373fffffffffffffffffffffffffffffffffffffffe146090573615156028575f545f5260205ff35b366038141561012e5760115f54600182026001905f5b5f82111560595781019083028483029004916001019190603e565b90939004341061012e57600154600101600155600354806003026004013381556001015f3581556001016020359055600101600355005b6003546002548082038060101160a4575060105b5f5b81811460dd5780604c02838201600302600401805490600101805490600101549160601b83528260140152906034015260010160a6565b910180921460ed579060025560f8565b90505f6002555f6003555b5f548061049d141561010757505f5b60015460028282011161011c5750505f610122565b01600290035b5f555f600155604c025ff35b5f5ffd")}, + }, + } + ) + gspec.Config.BerlinBlock = common.Big0 + gspec.Config.LondonBlock = common.Big0 + gspec.Config.TerminalTotalDifficulty = common.Big0 + gspec.Config.TerminalTotalDifficultyPassed = true + gspec.Config.ShanghaiTime = u64(0) + gspec.Config.CancunTime = u64(0) + gspec.Config.PragueTime = u64(0) + signer := types.LatestSigner(gspec.Config) + + // Withdrawal requests to send. + wxs := types.WithdrawalRequests{ + { + Source: addr, + PublicKey: [48]byte{42}, + Amount: 42, + }, + { + Source: addr, + PublicKey: [48]byte{13, 37}, + Amount: 1337, + }, + } + + _, blocks, _ := GenerateChainWithGenesis(gspec, engine, 1, func(i int, b *BlockGen) { + for i, wx := range wxs { + data := make([]byte, 56) + copy(data, wx.PublicKey[:]) + binary.BigEndian.PutUint64(data[48:], wx.Amount) + txdata := &types.DynamicFeeTx{ + ChainID: gspec.Config.ChainID, + Nonce: uint64(i), + To: ¶ms.WithdrawalRequestsAddress, + Value: big.NewInt(1), + Gas: 500000, + GasFeeCap: newGwei(5), + GasTipCap: big.NewInt(2), + AccessList: nil, + Data: data, + } + tx := types.NewTx(txdata) + tx, _ = types.SignTx(tx, signer, key) + b.AddTx(tx) + } + }) + chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), nil, gspec, nil, engine, vm.Config{Tracer: logger.NewMarkdownLogger(&logger.Config{}, os.Stderr).Hooks()}, nil, nil) + if err != nil { + t.Fatalf("failed to create tester chain: %v", err) + } + defer chain.Stop() + if n, err := chain.InsertChain(blocks); err != nil { + t.Fatalf("block %d: failed to insert into chain: %v", n, err) + } + block := chain.GetBlockByNumber(1) + if block == nil { + t.Fatalf("failed to retrieve block 1") + } + + // Verify the withdrawal requests match. + got := block.Requests() + if len(got) != 2 { + t.Fatalf("wrong number of withdrawal requests: wanted 2, got %d", len(wxs)) + } + for i, want := range wxs { + got, ok := got[i].Inner().(*types.WithdrawalRequest) + if !ok { + t.Fatalf("expected withdrawal request") + } + if want.Source != got.Source { + t.Fatalf("wrong source address: want %s, got %s", want.Source, got.Source) + } + if want.PublicKey != got.PublicKey { + t.Fatalf("wrong public key: want %s, got %s", common.Bytes2Hex(want.PublicKey[:]), common.Bytes2Hex(got.PublicKey[:])) + } + if want.Amount != got.Amount { + t.Fatalf("wrong amount: want %d, got %d", want.Amount, got.Amount) + } + } +} diff --git a/core/chain_makers.go b/core/chain_makers.go index 8e75abdea0..9bacc480ac 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -355,6 +355,13 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse } requests = append(requests, d...) } + + var ( + blockContext = NewEVMBlockContext(b.header, b.cm, &b.header.Coinbase) + vmenv = vm.NewEVM(blockContext, vm.TxContext{}, b.statedb, b.cm.config, vm.Config{}) + ) + wxs := ProcessDequeueWithdrawalRequests(vmenv, statedb) + requests = append(requests, wxs...) } body := types.Body{Transactions: b.txs, Uncles: b.uncles, Withdrawals: b.withdrawals, Requests: requests} diff --git a/core/state_processor.go b/core/state_processor.go index c74a33c378..64722c4875 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -17,6 +17,7 @@ package core import ( + "encoding/binary" "fmt" "math/big" @@ -103,6 +104,8 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg if err != nil { return nil, err } + wxs := ProcessDequeueWithdrawalRequests(vmenv, statedb) + requests = append(requests, wxs...) } // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) @@ -275,3 +278,40 @@ func ParseDepositLogs(logs []*types.Log, config *params.ChainConfig) (types.Requ } return deposits, nil } + +// ProcessDequeueWithdrawalRequests applies the EIP-7002 system call to the withdrawal requests contract. +func ProcessDequeueWithdrawalRequests(vmenv *vm.EVM, statedb *state.StateDB) types.Requests { + if vmenv.Config.Tracer != nil && vmenv.Config.Tracer.OnSystemCallStart != nil { + vmenv.Config.Tracer.OnSystemCallStart() + } + if vmenv.Config.Tracer != nil && vmenv.Config.Tracer.OnSystemCallEnd != nil { + defer vmenv.Config.Tracer.OnSystemCallEnd() + } + msg := &Message{ + From: params.SystemAddress, + GasLimit: 30_000_000, + GasPrice: common.Big0, + GasFeeCap: common.Big0, + GasTipCap: common.Big0, + To: ¶ms.WithdrawalRequestsAddress, + } + vmenv.Reset(NewEVMTxContext(msg), statedb) + statedb.AddAddressToAccessList(params.WithdrawalRequestsAddress) + ret, _, _ := vmenv.Call(vm.AccountRef(msg.From), *msg.To, msg.Data, 30_000_000, common.U2560) + statedb.Finalise(true) + + // Parse out the exits. + var reqs types.Requests + for i := 0; i < len(ret)/76; i++ { + start := i * 76 + var pubkey [48]byte + copy(pubkey[:], ret[start+20:start+68]) + wx := &types.WithdrawalRequest{ + Source: common.BytesToAddress(ret[start : start+20]), + PublicKey: pubkey, + Amount: binary.BigEndian.Uint64(ret[start+68:]), + } + reqs = append(reqs, types.NewRequest(wx)) + } + return reqs +} diff --git a/core/types/gen_withdrawal_request_json.go b/core/types/gen_withdrawal_request_json.go new file mode 100644 index 0000000000..655e119917 --- /dev/null +++ b/core/types/gen_withdrawal_request_json.go @@ -0,0 +1,53 @@ +// 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" +) + +var _ = (*withdrawalRequestMarshaling)(nil) + +// MarshalJSON marshals as JSON. +func (w WithdrawalRequest) MarshalJSON() ([]byte, error) { + type WithdrawalRequest struct { + Source common.Address `json:"sourceAddress"` + PublicKey hexutil.Bytes `json:"validatorPubkey"` + Amount hexutil.Uint64 `json:"amount"` + } + var enc WithdrawalRequest + enc.Source = w.Source + enc.PublicKey = w.PublicKey[:] + enc.Amount = hexutil.Uint64(w.Amount) + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals from JSON. +func (w *WithdrawalRequest) UnmarshalJSON(input []byte) error { + type WithdrawalRequest struct { + Source *common.Address `json:"sourceAddress"` + PublicKey *hexutil.Bytes `json:"validatorPubkey"` + Amount *hexutil.Uint64 `json:"amount"` + } + var dec WithdrawalRequest + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.Source != nil { + w.Source = *dec.Source + } + if dec.PublicKey != nil { + if len(*dec.PublicKey) != len(w.PublicKey) { + return errors.New("field 'validatorPubkey' has wrong length, need 48 items") + } + copy(w.PublicKey[:], *dec.PublicKey) + } + if dec.Amount != nil { + w.Amount = uint64(*dec.Amount) + } + return nil +} diff --git a/core/types/request.go b/core/types/request.go index 7b1cade26e..c3f6417e49 100644 --- a/core/types/request.go +++ b/core/types/request.go @@ -32,7 +32,8 @@ var ( // Request types. const ( - DepositRequestType = 0x00 + DepositRequestType = 0x00 + WithdrawalRequestType = 0x01 ) // Request is an EIP-7685 request object. It represents execution layer @@ -149,6 +150,8 @@ func (r *Request) decode(b []byte) (RequestData, error) { switch b[0] { case DepositRequestType: inner = new(Deposit) + case WithdrawalRequestType: + inner = new(WithdrawalRequest) default: return nil, ErrRequestTypeNotSupported } diff --git a/core/types/withdrawal_request.go b/core/types/withdrawal_request.go new file mode 100644 index 0000000000..8a451f3f5f --- /dev/null +++ b/core/types/withdrawal_request.go @@ -0,0 +1,80 @@ +// 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" + "encoding/binary" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/rlp" +) + +//go:generate go run github.com/fjl/gencodec -type WithdrawalRequest -field-override withdrawalRequestMarshaling -out gen_withdrawal_request_json.go + +// WithdrawalRequest represents an EIP-7002 withdrawal request from source for +// the validator associated with the public key for amount. +type WithdrawalRequest struct { + Source common.Address `json:"sourceAddress"` + PublicKey [48]byte `json:"validatorPubkey"` + Amount uint64 `json:"amount"` +} + +// field type overrides for gencodec +type withdrawalRequestMarshaling struct { + PublicKey hexutil.Bytes + Amount hexutil.Uint64 +} + +func (w *WithdrawalRequest) Bytes() []byte { + out := make([]byte, 76) + copy(out, w.Source.Bytes()) + copy(out[20:], w.PublicKey[:]) + binary.LittleEndian.PutUint64(out, w.Amount) + return out +} + +// WithdrawalRequests implements DerivableList for withdrawal requests. +type WithdrawalRequests []*WithdrawalRequest + +// Len returns the length of s. +func (s WithdrawalRequests) Len() int { return len(s) } + +// EncodeIndex encodes the i'th withdrawal request to w. +func (s WithdrawalRequests) EncodeIndex(i int, w *bytes.Buffer) { + rlp.Encode(w, s[i]) +} + +// Requests creates a deep copy of each deposit and returns a slice of the +// withdrwawal requests as Request objects. +func (s WithdrawalRequests) Requests() (reqs Requests) { + for _, d := range s { + reqs = append(reqs, NewRequest(d)) + } + return +} + +func (w *WithdrawalRequest) requestType() byte { return WithdrawalRequestType } +func (w *WithdrawalRequest) encode(b *bytes.Buffer) error { return rlp.Encode(b, w) } +func (w *WithdrawalRequest) decode(input []byte) error { return rlp.DecodeBytes(input, w) } +func (w *WithdrawalRequest) copy() RequestData { + return &WithdrawalRequest{ + Source: w.Source, + PublicKey: w.PublicKey, + Amount: w.Amount, + } +} diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index 7ea1c9ee07..3515799854 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -872,12 +872,13 @@ func (api *ConsensusAPI) GetPayloadBodiesByHashV1(hashes []common.Hash) []*engin for i, hash := range hashes { block := api.eth.BlockChain().GetBlockByHash(hash) body := getBody(block) + // Nil out the V2 values, clients should know to not request V1 objects + // after Prague. if body != nil { - // Nil out the V2 values, clients should know to not request V1 objects - // after Prague. body.Deposits = nil + bodies[i].WithdrawalRequests = nil + bodies[i] = body } - bodies[i] = body } return bodies } @@ -905,6 +906,7 @@ func (api *ConsensusAPI) GetPayloadBodiesByRangeV1(start, count hexutil.Uint64) for i := range bodies { if bodies[i] != nil { bodies[i].Deposits = nil + bodies[i].WithdrawalRequests = nil } } return bodies, nil @@ -943,10 +945,11 @@ func getBody(block *types.Block) *engine.ExecutionPayloadBody { } var ( - body = block.Body() - txs = make([]hexutil.Bytes, len(body.Transactions)) - withdrawals = body.Withdrawals - depositRequests types.Deposits + body = block.Body() + txs = make([]hexutil.Bytes, len(body.Transactions)) + withdrawals = body.Withdrawals + depositRequests types.Deposits + withdrawalRequests types.WithdrawalRequests ) for j, tx := range body.Transactions { @@ -963,15 +966,18 @@ func getBody(block *types.Block) *engine.ExecutionPayloadBody { // type has activated yet or if there are just no requests of that type from // only the block. for _, req := range block.Requests() { - if d, ok := req.Inner().(*types.Deposit); ok { - depositRequests = append(depositRequests, d) + switch v := req.Inner().(type) { + case *types.Deposit: + depositRequests = append(depositRequests, v) + case *types.WithdrawalRequest: + withdrawalRequests = append(withdrawalRequests, v) } } } - return &engine.ExecutionPayloadBody{ - TransactionData: txs, - Withdrawals: withdrawals, - Deposits: depositRequests, + TransactionData: txs, + Withdrawals: withdrawals, + Deposits: depositRequests, + WithdrawalRequests: withdrawalRequests, } } diff --git a/miner/worker.go b/miner/worker.go index 1f49118c47..f0bdd02771 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -113,10 +113,18 @@ func (miner *Miner) generateWork(params *generateParams) *newPayloadResult { } // Read requests if Prague is enabled. if miner.chainConfig.IsPrague(work.header.Number, work.header.Time) { - requests, err := core.ParseDepositLogs(allLogs, miner.chainConfig) + // Parse Deposits + requests := make([]*types.Request, 0) + reqs, err := core.ParseDepositLogs(allLogs, miner.chainConfig) if err != nil { return &newPayloadResult{err: err} } + requests = append(requests, reqs...) + // Process WithdrawalRequests + context := core.NewEVMBlockContext(work.header, miner.chain, nil) + vmenv := vm.NewEVM(context, vm.TxContext{}, work.state, miner.chainConfig, vm.Config{}) + wxs := core.ProcessDequeueWithdrawalRequests(vmenv, work.state) + requests = append(requests, wxs...) body.Requests = requests } block, err := miner.engine.FinalizeAndAssemble(miner.chain, work.header, work.state, &body, work.receipts) diff --git a/params/protocol_params.go b/params/protocol_params.go index 638f58a339..7da77b18eb 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -189,10 +189,10 @@ var ( // BeaconRootsAddress is the address where historical beacon roots are stored as per EIP-4788 BeaconRootsAddress = common.HexToAddress("0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02") - // BeaconRootsCode is the code where historical beacon roots are stored as per EIP-4788 BeaconRootsCode = common.FromHex("3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500") - + // WithdrawalRequests is the address where the EIP-7002 withdrawal requests queue is maintained. + WithdrawalRequestsAddress = common.HexToAddress("0x00A3ca265EBcb825B45F985A16CEFB49958cE017") // SystemAddress is where the system-transaction is sent from as per EIP-4788 SystemAddress = common.HexToAddress("0xfffffffffffffffffffffffffffffffffffffffe") // HistoryStorageAddress is where the historical block hashes are stored.