From dfd33c77923986dc9c20f99c5b0db6ddc87751e8 Mon Sep 17 00:00:00 2001 From: lightclient <14004106+lightclient@users.noreply.github.com> Date: Wed, 4 Sep 2024 14:33:51 +0200 Subject: [PATCH] all: implement EIP-6110, execution layer triggered deposits (#29431) This PR implements EIP-6110: Supply validator deposits on chain. It also sketches out the base for Prague in the engine API types. --- beacon/engine/gen_ed.go | 6 + beacon/engine/types.go | 39 ++++- cmd/evm/internal/t8ntool/execution.go | 24 +++ core/block_validator.go | 20 ++- core/blockchain.go | 22 +-- core/blockchain_test.go | 96 +++++++++++- core/chain_makers.go | 13 +- core/genesis.go | 11 +- core/state_processor.go | 39 ++++- core/stateless.go | 6 +- core/types.go | 15 +- core/types/block.go | 39 ++++- core/types/deposit.go | 103 ++++++++++++ core/types/deposit_test.go | 93 +++++++++++ core/types/gen_deposit_json.go | 70 +++++++++ core/types/gen_header_json.go | 6 + core/types/gen_header_rlp.go | 18 ++- core/types/hashes.go | 3 + core/types/request.go | 157 +++++++++++++++++++ eth/catalyst/api.go | 112 +++++++++++-- eth/catalyst/api_test.go | 108 +++++++++---- eth/catalyst/simulated_beacon.go | 4 +- eth/downloader/downloader_test.go | 3 +- eth/downloader/fetchers_concurrent_bodies.go | 6 +- eth/downloader/queue.go | 16 +- eth/downloader/queue_test.go | 2 +- eth/protocols/eth/handlers.go | 6 +- eth/protocols/eth/protocol.go | 9 +- eth/state_accessor.go | 2 +- eth/tracers/logger/logger.go | 2 +- ethclient/ethclient.go | 2 + internal/ethapi/api.go | 6 + miner/worker.go | 13 ++ params/config.go | 3 + tests/init.go | 2 + 35 files changed, 974 insertions(+), 102 deletions(-) create mode 100644 core/types/deposit.go create mode 100644 core/types/deposit_test.go create mode 100644 core/types/gen_deposit_json.go create mode 100644 core/types/request.go diff --git a/beacon/engine/gen_ed.go b/beacon/engine/gen_ed.go index 0ae5a3b8f1..b2eb1dc982 100644 --- a/beacon/engine/gen_ed.go +++ b/beacon/engine/gen_ed.go @@ -34,6 +34,7 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) { 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"` } var enc ExecutableData @@ -59,6 +60,7 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) { enc.Withdrawals = e.Withdrawals enc.BlobGasUsed = (*hexutil.Uint64)(e.BlobGasUsed) enc.ExcessBlobGas = (*hexutil.Uint64)(e.ExcessBlobGas) + enc.Deposits = e.Deposits enc.ExecutionWitness = e.ExecutionWitness return json.Marshal(&enc) } @@ -83,6 +85,7 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error { 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"` } var dec ExecutableData @@ -157,6 +160,9 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error { if dec.ExcessBlobGas != nil { e.ExcessBlobGas = (*uint64)(dec.ExcessBlobGas) } + if dec.Deposits != nil { + e.Deposits = *dec.Deposits + } if dec.ExecutionWitness != nil { e.ExecutionWitness = dec.ExecutionWitness } diff --git a/beacon/engine/types.go b/beacon/engine/types.go index 8687a6f504..19d2b2823e 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -36,6 +36,7 @@ var ( PayloadV1 PayloadVersion = 0x1 PayloadV2 PayloadVersion = 0x2 PayloadV3 PayloadVersion = 0x3 + PayloadV4 PayloadVersion = 0x4 ) //go:generate go run github.com/fjl/gencodec -type PayloadAttributes -field-override payloadAttributesMarshaling -out gen_blockparams.go @@ -76,6 +77,7 @@ type ExecutableData struct { Withdrawals []*types.Withdrawal `json:"withdrawals"` BlobGasUsed *uint64 `json:"blobGasUsed"` ExcessBlobGas *uint64 `json:"excessBlobGas"` + Deposits types.Deposits `json:"depositRequests"` ExecutionWitness *types.ExecutionWitness `json:"executionWitness,omitempty"` } @@ -231,6 +233,19 @@ 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. + var ( + requestsHash *common.Hash + requests types.Requests + ) + if data.Deposits != nil { + requests = make(types.Requests, 0) + for _, d := range data.Deposits { + requests = append(requests, types.NewRequest(d)) + } + h := types.DeriveSha(requests, trie.NewStackTrie(nil)) + requestsHash = &h + } header := &types.Header{ ParentHash: data.ParentHash, UncleHash: types.EmptyUncleHash, @@ -251,9 +266,10 @@ func ExecutableDataToBlock(data ExecutableData, versionedHashes []common.Hash, b ExcessBlobGas: data.ExcessBlobGas, BlobGasUsed: data.BlobGasUsed, ParentBeaconRoot: beaconRoot, + RequestsHash: requestsHash, } block := types.NewBlockWithHeader(header) - block = block.WithBody(types.Body{Transactions: txs, Uncles: nil, Withdrawals: data.Withdrawals}) + block = block.WithBody(types.Body{Transactions: txs, Uncles: nil, Withdrawals: data.Withdrawals, Requests: requests}) block = block.WithWitness(data.ExecutionWitness) if block.Hash() != data.BlockHash { return nil, fmt.Errorf("blockhash mismatch, want %x, got %x", data.BlockHash, block.Hash()) @@ -296,13 +312,30 @@ func BlockToExecutableData(block *types.Block, fees *big.Int, sidecars []*types. bundle.Proofs = append(bundle.Proofs, hexutil.Bytes(sidecar.Proofs[j][:])) } } + setRequests(block.Requests(), data) return &ExecutionPayloadEnvelope{ExecutionPayload: data, BlockValue: fees, BlobsBundle: &bundle, Override: false} } -// ExecutionPayloadBodyV1 is used in the response to GetPayloadBodiesByHashV1 and GetPayloadBodiesByRangeV1 -type ExecutionPayloadBodyV1 struct { +// setRequests differentiates the different request types and +// 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. + data.Deposits = make(types.Deposits, 0) + } + for _, r := range requests { + if d, ok := r.Inner().(*types.Deposit); ok { + data.Deposits = append(data.Deposits, d) + } + } +} + +// 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"` } // Client identifiers to support ClientVersionV1. diff --git a/cmd/evm/internal/t8ntool/execution.go b/cmd/evm/internal/t8ntool/execution.go index d240d161a9..a0805722d6 100644 --- a/cmd/evm/internal/t8ntool/execution.go +++ b/cmd/evm/internal/t8ntool/execution.go @@ -66,6 +66,8 @@ type ExecutionResult struct { 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"` } type ommer struct { @@ -377,6 +379,28 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig, execRs.CurrentExcessBlobGas = (*math.HexOrDecimal64)(&excessBlobGas) execRs.CurrentBlobGasUsed = (*math.HexOrDecimal64)(&blobGasUsed) } + if chainConfig.IsPrague(vmContext.BlockNumber, vmContext.Time) { + // Parse the requests from the logs + var allLogs []*types.Log + for _, receipt := range receipts { + allLogs = append(allLogs, receipt.Logs...) + } + requests, err := core.ParseDepositLogs(allLogs, chainConfig) + if err != nil { + return nil, nil, nil, NewError(ErrorEVM, fmt.Errorf("could not parse requests logs: %v", err)) + } + // Calculate the requests root + h := types.DeriveSha(requests, trie.NewStackTrie(nil)) + execRs.RequestsHash = &h + // Get the deposits from the requests + deposits := make(types.Deposits, 0) + for _, req := range requests { + if dep, ok := req.Inner().(*types.Deposit); ok { + deposits = append(deposits, dep) + } + } + execRs.DepositRequests = &deposits + } // Re-create statedb instance with new root upon the updated database // for accessing latest states. statedb, err = state.New(root, statedb.Database(), nil) diff --git a/core/block_validator.go b/core/block_validator.go index 75f7f8a94b..a944db0bf8 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -121,14 +121,17 @@ func (v *BlockValidator) ValidateBody(block *types.Block) error { // ValidateState validates the various changes that happen after a state transition, // such as amount of used gas, the receipt roots and the state root itself. -func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateDB, receipts types.Receipts, usedGas uint64, stateless bool) error { +func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateDB, res *ProcessResult, stateless bool) error { + if res == nil { + return fmt.Errorf("nil ProcessResult value") + } header := block.Header() - if block.GasUsed() != usedGas { - return fmt.Errorf("invalid gas used (remote: %d local: %d)", block.GasUsed(), usedGas) + if block.GasUsed() != res.GasUsed { + return fmt.Errorf("invalid gas used (remote: %d local: %d)", block.GasUsed(), res.GasUsed) } // Validate the received block's bloom with the one derived from the generated receipts. // For valid blocks this should always validate to true. - rbloom := types.CreateBloom(receipts) + rbloom := types.CreateBloom(res.Receipts) if rbloom != header.Bloom { return fmt.Errorf("invalid bloom (remote: %x local: %x)", header.Bloom, rbloom) } @@ -138,10 +141,17 @@ func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateD return nil } // The receipt Trie's root (R = (Tr [[H1, R1], ... [Hn, Rn]])) - receiptSha := types.DeriveSha(receipts, trie.NewStackTrie(nil)) + receiptSha := types.DeriveSha(res.Receipts, trie.NewStackTrie(nil)) if receiptSha != header.ReceiptHash { return fmt.Errorf("invalid receipt root hash (remote: %x local: %x)", header.ReceiptHash, receiptSha) } + // Validate the parsed requests match the expected header value. + if header.RequestsHash != nil { + depositSha := types.DeriveSha(res.Requests, trie.NewStackTrie(nil)) + if depositSha != *header.RequestsHash { + return fmt.Errorf("invalid deposit root hash (remote: %x local: %x)", *header.RequestsHash, depositSha) + } + } // Validate the state root against the received state root and throw // an error if they don't match. if root := statedb.IntermediateRoot(v.config.IsEIP158(header.Number)); header.Root != root { diff --git a/core/blockchain.go b/core/blockchain.go index 733525bc70..397d917d73 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -1927,23 +1927,23 @@ func (bc *BlockChain) processBlock(block *types.Block, statedb *state.StateDB, s // Process block using the parent state as reference point pstart := time.Now() - receipts, logs, usedGas, err := bc.processor.Process(block, statedb, bc.vmConfig) + res, err := bc.processor.Process(block, statedb, bc.vmConfig) if err != nil { - bc.reportBlock(block, receipts, err) + bc.reportBlock(block, res, err) return nil, err } ptime := time.Since(pstart) vstart := time.Now() - if err := bc.validator.ValidateState(block, statedb, receipts, usedGas, false); err != nil { - bc.reportBlock(block, receipts, err) + if err := bc.validator.ValidateState(block, statedb, res, false); err != nil { + bc.reportBlock(block, res, err) return nil, err } vtime := time.Since(vstart) if witness := statedb.Witness(); witness != nil { if err = bc.validator.ValidateWitness(witness, block.ReceiptHash(), block.Root()); err != nil { - bc.reportBlock(block, receipts, err) + bc.reportBlock(block, res, err) return nil, fmt.Errorf("cross verification failed: %v", err) } } @@ -1978,9 +1978,9 @@ func (bc *BlockChain) processBlock(block *types.Block, statedb *state.StateDB, s ) if !setHead { // Don't set the head, only insert the block - err = bc.writeBlockWithState(block, receipts, statedb) + err = bc.writeBlockWithState(block, res.Receipts, statedb) } else { - status, err = bc.writeBlockAndSetHead(block, receipts, logs, statedb, false) + status, err = bc.writeBlockAndSetHead(block, res.Receipts, res.Logs, statedb, false) } if err != nil { return nil, err @@ -1994,7 +1994,7 @@ func (bc *BlockChain) processBlock(block *types.Block, statedb *state.StateDB, s blockWriteTimer.Update(time.Since(wstart) - max(statedb.AccountCommits, statedb.StorageCommits) /* concurrent */ - statedb.SnapshotCommits - statedb.TrieDBCommits) blockInsertTimer.UpdateSince(start) - return &blockProcessingResult{usedGas: usedGas, procTime: proctime, status: status}, nil + return &blockProcessingResult{usedGas: res.GasUsed, procTime: proctime, status: status}, nil } // insertSideChain is called when an import batch hits upon a pruned ancestor @@ -2491,7 +2491,11 @@ func (bc *BlockChain) skipBlock(err error, it *insertIterator) bool { } // reportBlock logs a bad block error. -func (bc *BlockChain) reportBlock(block *types.Block, receipts types.Receipts, err error) { +func (bc *BlockChain) reportBlock(block *types.Block, res *ProcessResult, err error) { + var receipts types.Receipts + if res != nil { + receipts = res.Receipts + } rawdb.WriteBadBlock(bc.db, block) log.Error(summarizeBadBlock(block, receipts, bc.Config(), err)) } diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 4f28c6f5e6..9148dd6f08 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -163,13 +163,14 @@ func testBlockChainImport(chain types.Blocks, blockchain *BlockChain) error { if err != nil { return err } - receipts, _, usedGas, err := blockchain.processor.Process(block, statedb, vm.Config{}) + res, err := blockchain.processor.Process(block, statedb, vm.Config{}) if err != nil { - blockchain.reportBlock(block, receipts, err) + blockchain.reportBlock(block, res, err) return err } - if err = blockchain.validator.ValidateState(block, statedb, receipts, usedGas, false); err != nil { - blockchain.reportBlock(block, receipts, err) + err = blockchain.validator.ValidateState(block, statedb, res, false) + if err != nil { + blockchain.reportBlock(block, res, err) return err } @@ -4220,3 +4221,90 @@ func TestEIP3651(t *testing.T) { t.Fatalf("sender balance incorrect: expected %d, got %d", expected, actual) } } + +func TestEIP6110(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}, + config.DepositContractAddress: { + // Simple deposit generator, source: https://gist.github.com/lightclient/54abb2af2465d6969fa6d1920b9ad9d7 + Code: common.Hex2Bytes("6080604052366103aa575f603067ffffffffffffffff811115610025576100246103ae565b5b6040519080825280601f01601f1916602001820160405280156100575781602001600182028036833780820191505090505b5090505f8054906101000a900460ff1660f81b815f8151811061007d5761007c6103db565b5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff191690815f1a9053505f602067ffffffffffffffff8111156100c7576100c66103ae565b5b6040519080825280601f01601f1916602001820160405280156100f95781602001600182028036833780820191505090505b5090505f8054906101000a900460ff1660f81b815f8151811061011f5761011e6103db565b5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff191690815f1a9053505f600867ffffffffffffffff811115610169576101686103ae565b5b6040519080825280601f01601f19166020018201604052801561019b5781602001600182028036833780820191505090505b5090505f8054906101000a900460ff1660f81b815f815181106101c1576101c06103db565b5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff191690815f1a9053505f606067ffffffffffffffff81111561020b5761020a6103ae565b5b6040519080825280601f01601f19166020018201604052801561023d5781602001600182028036833780820191505090505b5090505f8054906101000a900460ff1660f81b815f81518110610263576102626103db565b5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff191690815f1a9053505f600867ffffffffffffffff8111156102ad576102ac6103ae565b5b6040519080825280601f01601f1916602001820160405280156102df5781602001600182028036833780820191505090505b5090505f8054906101000a900460ff1660f81b815f81518110610305576103046103db565b5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff191690815f1a9053505f8081819054906101000a900460ff168092919061035090610441565b91906101000a81548160ff021916908360ff160217905550507f649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c585858585856040516103a09594939291906104d9565b60405180910390a1005b5f80fd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f60ff82169050919050565b5f61044b82610435565b915060ff820361045e5761045d610408565b5b600182019050919050565b5f81519050919050565b5f82825260208201905092915050565b8281835e5f83830152505050565b5f601f19601f8301169050919050565b5f6104ab82610469565b6104b58185610473565b93506104c5818560208601610483565b6104ce81610491565b840191505092915050565b5f60a0820190508181035f8301526104f181886104a1565b9050818103602083015261050581876104a1565b9050818103604083015261051981866104a1565b9050818103606083015261052d81856104a1565b9050818103608083015261054181846104a1565b9050969550505050505056fea26469706673582212208569967e58690162d7d6fe3513d07b393b4c15e70f41505cbbfd08f53eba739364736f6c63430008190033"), + Nonce: 0, + Balance: big.NewInt(0), + }, + }, + } + ) + + 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) + + _, blocks, _ := GenerateChainWithGenesis(gspec, engine, 1, func(i int, b *BlockGen) { + for i := 0; i < 5; i++ { + txdata := &types.DynamicFeeTx{ + ChainID: gspec.Config.ChainID, + Nonce: uint64(i), + To: &config.DepositContractAddress, + Gas: 500000, + GasFeeCap: newGwei(5), + GasTipCap: big.NewInt(2), + AccessList: nil, + Data: []byte{}, + } + 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{DisableStack: true}, 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 len(block.Requests()) != 5 { + t.Fatalf("failed to retrieve deposits: have %d, want %d", len(block.Requests()), 5) + } + + // Verify each index is correct. + for want, req := range block.Requests() { + d, ok := req.Inner().(*types.Deposit) + if !ok { + t.Fatalf("expected deposit object") + } + if got := int(d.PublicKey[0]); got != want { + t.Fatalf("invalid pubkey: have %d, want %d", got, want) + } + if got := int(d.WithdrawalCredentials[0]); got != want { + t.Fatalf("invalid withdrawal credentials: have %d, want %d", got, want) + } + if d.Amount != uint64(want) { + t.Fatalf("invalid amounbt: have %d, want %d", d.Amount, want) + } + if got := int(d.Signature[0]); got != want { + t.Fatalf("invalid signature: have %d, want %d", got, want) + } + if d.Index != uint64(want) { + t.Fatalf("invalid index: have %d, want %d", d.Index, want) + } + } +} diff --git a/core/chain_makers.go b/core/chain_makers.go index 6cee6fdc8a..dcaa36e08c 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -346,7 +346,18 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse gen(i, b) } - body := types.Body{Transactions: b.txs, Uncles: b.uncles, Withdrawals: b.withdrawals} + var requests types.Requests + if config.IsPrague(b.header.Number, b.header.Time) { + for _, r := range b.receipts { + d, err := ParseDepositLogs(r.Logs, config) + if err != nil { + panic(fmt.Sprintf("failed to parse deposit log: %v", err)) + } + requests = append(requests, d...) + } + } + + body := types.Body{Transactions: b.txs, Uncles: b.uncles, Withdrawals: b.withdrawals, Requests: requests} block, err := b.engine.FinalizeAndAssemble(cm, b.header, statedb, &body, b.receipts) if err != nil { panic(err) diff --git a/core/genesis.go b/core/genesis.go index d83aab0baf..67f5d56cfe 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -447,7 +447,10 @@ func (g *Genesis) toBlockWithRoot(root common.Hash) *types.Block { head.BaseFee = new(big.Int).SetUint64(params.InitialBaseFee) } } - var withdrawals []*types.Withdrawal + var ( + withdrawals []*types.Withdrawal + requests types.Requests + ) if conf := g.Config; conf != nil { num := big.NewInt(int64(g.Number)) if conf.IsShanghai(num, g.Timestamp) { @@ -469,8 +472,12 @@ func (g *Genesis) toBlockWithRoot(root common.Hash) *types.Block { head.BlobGasUsed = new(uint64) } } + if conf.IsPrague(num, g.Timestamp) { + head.RequestsHash = &types.EmptyRequestsHash + requests = make(types.Requests, 0) + } } - return types.NewBlock(head, &types.Body{Withdrawals: withdrawals}, nil, trie.NewStackTrie(nil)) + return types.NewBlock(head, &types.Body{Withdrawals: withdrawals, Requests: requests}, nil, trie.NewStackTrie(nil)) } // Commit writes the block and state of a genesis specification to the database. diff --git a/core/state_processor.go b/core/state_processor.go index c13b696022..2844693a9e 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -53,7 +53,7 @@ func NewStateProcessor(config *params.ChainConfig, chain *HeaderChain) *StatePro // Process returns the receipts and logs accumulated during the process and // returns the amount of gas that was used in the process. If any of the // transactions failed to execute due to insufficient gas it will return an error. -func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts, []*types.Log, uint64, error) { +func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (*ProcessResult, error) { var ( receipts types.Receipts usedGas = new(uint64) @@ -71,6 +71,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg var ( context vm.BlockContext signer = types.MakeSigner(p.config, header.Number, header.Time) + err error ) context = NewEVMBlockContext(header, p.chain, nil) vmenv := vm.NewEVM(context, vm.TxContext{}, statedb, p.config, cfg) @@ -84,21 +85,35 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg for i, tx := range block.Transactions() { msg, err := TransactionToMessage(tx, signer, header.BaseFee) if err != nil { - return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) + return nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } statedb.SetTxContext(tx.Hash(), i) receipt, err := ApplyTransactionWithEVM(msg, p.config, gp, statedb, blockNumber, blockHash, tx, usedGas, vmenv) if err != nil { - return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) + return nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } receipts = append(receipts, receipt) allLogs = append(allLogs, receipt.Logs...) } + // Read requests if Prague is enabled. + var requests types.Requests + if p.config.IsPrague(block.Number(), block.Time()) { + requests, err = ParseDepositLogs(allLogs, p.config) + if err != nil { + return nil, err + } + } + // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) p.chain.engine.Finalize(p.chain, header, statedb, block.Body()) - return receipts, allLogs, *usedGas, nil + return &ProcessResult{ + Receipts: receipts, + Requests: requests, + Logs: allLogs, + GasUsed: *usedGas, + }, nil } // ApplyTransactionWithEVM attempts to apply a transaction to the given state database @@ -239,3 +254,19 @@ func ProcessParentBlockHash(prevHash common.Hash, vmenv *vm.EVM, statedb *state. _, _, _ = vmenv.Call(vm.AccountRef(msg.From), *msg.To, msg.Data, 30_000_000, common.U2560) statedb.Finalise(true) } + +// ParseDepositLogs extracts the EIP-6110 deposit values from logs emitted by +// BeaconDepositContract. +func ParseDepositLogs(logs []*types.Log, config *params.ChainConfig) (types.Requests, error) { + deposits := make(types.Requests, 0) + for _, log := range logs { + if log.Address == config.DepositContractAddress { + d, err := types.UnpackIntoDeposit(log.Data) + if err != nil { + return nil, fmt.Errorf("unable to parse deposit data: %v", err) + } + deposits = append(deposits, types.NewRequest(d)) + } + } + return deposits, nil +} diff --git a/core/stateless.go b/core/stateless.go index 4c7e6f3102..e3168747cf 100644 --- a/core/stateless.go +++ b/core/stateless.go @@ -58,15 +58,15 @@ func ExecuteStateless(config *params.ChainConfig, witness *stateless.Witness) (c validator := NewBlockValidator(config, nil) // No chain, we only validate the state, not the block // Run the stateless blocks processing and self-validate certain fields - receipts, _, usedGas, err := processor.Process(witness.Block, db, vm.Config{}) + res, err := processor.Process(witness.Block, db, vm.Config{}) if err != nil { return common.Hash{}, common.Hash{}, err } - if err = validator.ValidateState(witness.Block, db, receipts, usedGas, true); err != nil { + if err = validator.ValidateState(witness.Block, db, res, true); err != nil { return common.Hash{}, common.Hash{}, err } // Almost everything validated, but receipt and state root needs to be returned - receiptRoot := types.DeriveSha(receipts, trie.NewStackTrie(nil)) + receiptRoot := types.DeriveSha(res.Receipts, trie.NewStackTrie(nil)) stateRoot := db.IntermediateRoot(config.IsEIP158(witness.Block.Number())) return receiptRoot, stateRoot, nil diff --git a/core/types.go b/core/types.go index dc13de52ce..b1808c9ffb 100644 --- a/core/types.go +++ b/core/types.go @@ -33,9 +33,8 @@ type Validator interface { // ValidateBody validates the given block's content. ValidateBody(block *types.Block) error - // ValidateState validates the given statedb and optionally the receipts and - // gas used. - ValidateState(block *types.Block, state *state.StateDB, receipts types.Receipts, usedGas uint64, stateless bool) error + // ValidateState validates the given statedb and optionally the process result. + ValidateState(block *types.Block, state *state.StateDB, res *ProcessResult, stateless bool) error // ValidateWitness cross validates a block execution with stateless remote clients. ValidateWitness(witness *stateless.Witness, receiptRoot common.Hash, stateRoot common.Hash) error @@ -54,5 +53,13 @@ type Processor interface { // Process processes the state changes according to the Ethereum rules by running // the transaction messages using the statedb and applying any rewards to both // the processor (coinbase) and any included uncles. - Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts, []*types.Log, uint64, error) + Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (*ProcessResult, error) +} + +// ProcessResult contains the values computed by Process. +type ProcessResult struct { + Receipts types.Receipts + Requests types.Requests + Logs []*types.Log + GasUsed uint64 } diff --git a/core/types/block.go b/core/types/block.go index e6ddf2012f..1c00658d5b 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -102,6 +102,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 @@ -163,10 +166,11 @@ func (h *Header) SanityCheck() error { // EmptyBody returns true if there is no additional 'body' to complete the header // that is: no transactions, no uncles and no withdrawals. func (h *Header) EmptyBody() bool { - if h.WithdrawalsHash != nil { - return h.TxHash == EmptyTxsHash && *h.WithdrawalsHash == EmptyWithdrawalsHash - } - return h.TxHash == EmptyTxsHash && h.UncleHash == EmptyUncleHash + var ( + emptyWithdrawals = h.WithdrawalsHash == nil || *h.WithdrawalsHash == EmptyWithdrawalsHash + emptyRequests = h.RequestsHash == nil || *h.RequestsHash == EmptyReceiptsHash + ) + return h.TxHash == EmptyTxsHash && h.UncleHash == EmptyUncleHash && emptyWithdrawals && emptyRequests } // EmptyReceipts returns true if there are no receipts for this header/block. @@ -180,6 +184,7 @@ type Body struct { Transactions []*Transaction Uncles []*Header Withdrawals []*Withdrawal `rlp:"optional"` + Requests []*Request `rlp:"optional"` } // Block represents an Ethereum block. @@ -204,6 +209,7 @@ type Block struct { uncles []*Header transactions Transactions withdrawals Withdrawals + requests Requests // witness is not an encoded part of the block body. // It is held in Block in order for easy relaying to the places @@ -226,6 +232,7 @@ type extblock struct { Txs []*Transaction Uncles []*Header Withdrawals []*Withdrawal `rlp:"optional"` + Requests []*Request `rlp:"optional"` } // NewBlock creates a new block. The input data is copied, changes to header and to the @@ -242,6 +249,7 @@ func NewBlock(header *Header, body *Body, receipts []*Receipt, hasher TrieHasher txs = body.Transactions uncles = body.Uncles withdrawals = body.Withdrawals + requests = body.Requests ) if len(txs) == 0 { @@ -280,6 +288,17 @@ func NewBlock(header *Header, body *Body, receipts []*Receipt, hasher TrieHasher b.withdrawals = slices.Clone(withdrawals) } + if requests == nil { + b.header.RequestsHash = nil + } else if len(requests) == 0 { + b.header.RequestsHash = &EmptyRequestsHash + b.requests = Requests{} + } else { + h := DeriveSha(Requests(requests), hasher) + b.header.RequestsHash = &h + b.requests = slices.Clone(requests) + } + return b } @@ -315,6 +334,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 } @@ -325,7 +348,7 @@ func (b *Block) DecodeRLP(s *rlp.Stream) error { if err := s.Decode(&eb); err != nil { return err } - b.header, b.uncles, b.transactions, b.withdrawals = eb.Header, eb.Uncles, eb.Txs, eb.Withdrawals + b.header, b.uncles, b.transactions, b.withdrawals, b.requests = eb.Header, eb.Uncles, eb.Txs, eb.Withdrawals, eb.Requests b.size.Store(rlp.ListSize(size)) return nil } @@ -337,13 +360,14 @@ func (b *Block) EncodeRLP(w io.Writer) error { Txs: b.transactions, Uncles: b.uncles, Withdrawals: b.withdrawals, + Requests: b.requests, }) } // Body returns the non-header content of the block. // Note the returned data is not an independent copy. func (b *Block) Body() *Body { - return &Body{b.transactions, b.uncles, b.withdrawals} + return &Body{b.transactions, b.uncles, b.withdrawals, b.requests} } // Accessors for body data. These do not return a copy because the content @@ -352,6 +376,7 @@ func (b *Block) Body() *Body { func (b *Block) Uncles() []*Header { return b.uncles } func (b *Block) Transactions() Transactions { return b.transactions } func (b *Block) Withdrawals() Withdrawals { return b.withdrawals } +func (b *Block) Requests() Requests { return b.requests } func (b *Block) Transaction(hash common.Hash) *Transaction { for _, transaction := range b.transactions { @@ -476,6 +501,7 @@ func (b *Block) WithBody(body Body) *Block { transactions: slices.Clone(body.Transactions), uncles: make([]*Header, len(body.Uncles)), withdrawals: slices.Clone(body.Withdrawals), + requests: slices.Clone(body.Requests), witness: b.witness, } for i := range body.Uncles { @@ -490,6 +516,7 @@ func (b *Block) WithWitness(witness *ExecutionWitness) *Block { transactions: b.transactions, uncles: b.uncles, withdrawals: b.withdrawals, + requests: b.requests, witness: witness, } } diff --git a/core/types/deposit.go b/core/types/deposit.go new file mode 100644 index 0000000000..172acc36ed --- /dev/null +++ b/core/types/deposit.go @@ -0,0 +1,103 @@ +// 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" + "fmt" + + "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 Deposit -field-override depositMarshaling -out gen_deposit_json.go + +// Deposit contains EIP-6110 deposit data. +type Deposit struct { + PublicKey [48]byte `json:"pubkey"` // public key of validator + WithdrawalCredentials common.Hash `json:"withdrawalCredentials"` // beneficiary of the validator funds + Amount uint64 `json:"amount"` // deposit size in Gwei + Signature [96]byte `json:"signature"` // signature over deposit msg + Index uint64 `json:"index"` // deposit count value +} + +// field type overrides for gencodec +type depositMarshaling struct { + PublicKey hexutil.Bytes + WithdrawalCredentials hexutil.Bytes + Amount hexutil.Uint64 + Signature hexutil.Bytes + Index hexutil.Uint64 +} + +// Deposits implements DerivableList for requests. +type Deposits []*Deposit + +// Len returns the length of s. +func (s Deposits) Len() int { return len(s) } + +// EncodeIndex encodes the i'th deposit to s. +func (s Deposits) EncodeIndex(i int, w *bytes.Buffer) { + rlp.Encode(w, s[i]) +} + +// UnpackIntoDeposit unpacks a serialized DepositEvent. +func UnpackIntoDeposit(data []byte) (*Deposit, error) { + if len(data) != 576 { + return nil, fmt.Errorf("deposit wrong length: want 576, have %d", len(data)) + } + var d Deposit + // The ABI encodes the position of dynamic elements first. Since there are 5 + // elements, skip over the positional data. The first 32 bytes of dynamic + // elements also encode their actual length. Skip over that value too. + b := 32*5 + 32 + // PublicKey is the first element. ABI encoding pads values to 32 bytes, so + // despite BLS public keys being length 48, the value length here is 64. Then + // skip over the next length value. + copy(d.PublicKey[:], data[b:b+48]) + b += 48 + 16 + 32 + // WithdrawalCredentials is 32 bytes. Read that value then skip over next + // length. + copy(d.WithdrawalCredentials[:], data[b:b+32]) + b += 32 + 32 + // Amount is 8 bytes, but it is padded to 32. Skip over it and the next + // length. + d.Amount = binary.LittleEndian.Uint64(data[b : b+8]) + b += 8 + 24 + 32 + // Signature is 96 bytes. Skip over it and the next length. + copy(d.Signature[:], data[b:b+96]) + b += 96 + 32 + // Amount is 8 bytes. + d.Index = binary.LittleEndian.Uint64(data[b : b+8]) + + return &d, nil +} + +func (d *Deposit) requestType() byte { return DepositRequestType } +func (d *Deposit) encode(b *bytes.Buffer) error { return rlp.Encode(b, d) } +func (d *Deposit) decode(input []byte) error { return rlp.DecodeBytes(input, d) } +func (d *Deposit) copy() RequestData { + return &Deposit{ + PublicKey: d.PublicKey, + WithdrawalCredentials: d.WithdrawalCredentials, + Amount: d.Amount, + Signature: d.Signature, + Index: d.Index, + } +} diff --git a/core/types/deposit_test.go b/core/types/deposit_test.go new file mode 100644 index 0000000000..ed2e18445d --- /dev/null +++ b/core/types/deposit_test.go @@ -0,0 +1,93 @@ +// 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 ( + "encoding/binary" + "reflect" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" +) + +var ( + depositABI = abi.ABI{Methods: map[string]abi.Method{"DepositEvent": depositEvent}} + bytesT, _ = abi.NewType("bytes", "", nil) + depositEvent = abi.NewMethod("DepositEvent", "DepositEvent", abi.Function, "", false, false, []abi.Argument{ + {Name: "pubkey", Type: bytesT, Indexed: false}, + {Name: "withdrawal_credentials", Type: bytesT, Indexed: false}, + {Name: "amount", Type: bytesT, Indexed: false}, + {Name: "signature", Type: bytesT, Indexed: false}, + {Name: "index", Type: bytesT, Indexed: false}}, nil, + ) +) + +// FuzzUnpackIntoDeposit tries roundtrip packing and unpacking of deposit events. +func FuzzUnpackIntoDeposit(f *testing.F) { + for _, tt := range []struct { + pubkey string + wxCred string + amount string + sig string + index string + }{ + { + pubkey: "111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", + wxCred: "2222222222222222222222222222222222222222222222222222222222222222", + amount: "3333333333333333", + sig: "444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444", + index: "5555555555555555", + }, + } { + f.Add(common.FromHex(tt.pubkey), common.FromHex(tt.wxCred), common.FromHex(tt.amount), common.FromHex(tt.sig), common.FromHex(tt.index)) + } + + f.Fuzz(func(t *testing.T, p []byte, w []byte, a []byte, s []byte, i []byte) { + var ( + pubkey [48]byte + wxCred [32]byte + amount [8]byte + sig [96]byte + index [8]byte + ) + copy(pubkey[:], p) + copy(wxCred[:], w) + copy(amount[:], a) + copy(sig[:], s) + copy(index[:], i) + + want := Deposit{ + PublicKey: pubkey, + WithdrawalCredentials: wxCred, + Amount: binary.LittleEndian.Uint64(amount[:]), + Signature: sig, + Index: binary.LittleEndian.Uint64(index[:]), + } + out, err := depositABI.Pack("DepositEvent", want.PublicKey[:], want.WithdrawalCredentials[:], amount[:], want.Signature[:], index[:]) + if err != nil { + t.Fatalf("error packing deposit: %v", err) + } + got, err := UnpackIntoDeposit(out[4:]) + if err != nil { + t.Errorf("error unpacking deposit: %v", err) + } + if !reflect.DeepEqual(want, *got) { + t.Errorf("roundtrip failed: want %v, got %v", want, got) + } + }) +} diff --git a/core/types/gen_deposit_json.go b/core/types/gen_deposit_json.go new file mode 100644 index 0000000000..a65691188f --- /dev/null +++ b/core/types/gen_deposit_json.go @@ -0,0 +1,70 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package types + +import ( + "encoding/json" + "errors" + + "github.com/ethereum/go-ethereum/common/hexutil" +) + +var _ = (*depositMarshaling)(nil) + +// MarshalJSON marshals as JSON. +func (d Deposit) MarshalJSON() ([]byte, error) { + type Deposit struct { + PublicKey hexutil.Bytes `json:"pubkey"` + WithdrawalCredentials hexutil.Bytes `json:"withdrawalCredentials"` + Amount hexutil.Uint64 `json:"amount"` + Signature hexutil.Bytes `json:"signature"` + Index hexutil.Uint64 `json:"index"` + } + var enc Deposit + enc.PublicKey = d.PublicKey[:] + enc.WithdrawalCredentials = d.WithdrawalCredentials[:] + enc.Amount = hexutil.Uint64(d.Amount) + enc.Signature = d.Signature[:] + enc.Index = hexutil.Uint64(d.Index) + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals from JSON. +func (d *Deposit) UnmarshalJSON(input []byte) error { + type Deposit struct { + PublicKey *hexutil.Bytes `json:"pubkey"` + WithdrawalCredentials *hexutil.Bytes `json:"withdrawalCredentials"` + Amount *hexutil.Uint64 `json:"amount"` + Signature *hexutil.Bytes `json:"signature"` + Index *hexutil.Uint64 `json:"index"` + } + var dec Deposit + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.PublicKey != nil { + if len(*dec.PublicKey) != len(d.PublicKey) { + return errors.New("field 'pubkey' has wrong length, need 48 items") + } + copy(d.PublicKey[:], *dec.PublicKey) + } + if dec.WithdrawalCredentials != nil { + if len(*dec.WithdrawalCredentials) != len(d.WithdrawalCredentials) { + return errors.New("field 'withdrawalCredentials' has wrong length, need 32 items") + } + copy(d.WithdrawalCredentials[:], *dec.WithdrawalCredentials) + } + if dec.Amount != nil { + d.Amount = uint64(*dec.Amount) + } + if dec.Signature != nil { + if len(*dec.Signature) != len(d.Signature) { + return errors.New("field 'signature' has wrong length, need 96 items") + } + copy(d.Signature[:], *dec.Signature) + } + if dec.Index != nil { + d.Index = uint64(*dec.Index) + } + 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 ed6a1a002c..c79aa8a250 100644 --- a/core/types/gen_header_rlp.go +++ b/core/types/gen_header_rlp.go @@ -42,7 +42,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 { @@ -52,34 +53,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/hashes.go b/core/types/hashes.go index 43e9130fd1..cbd197072e 100644 --- a/core/types/hashes.go +++ b/core/types/hashes.go @@ -41,6 +41,9 @@ var ( // EmptyWithdrawalsHash is the known hash of the empty withdrawal set. EmptyWithdrawalsHash = common.HexToHash("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") + // EmptyRequestsHash is the known hash of the empty requests set. + EmptyRequestsHash = common.HexToHash("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") + // EmptyVerkleHash is the known hash of an empty verkle trie. EmptyVerkleHash = common.Hash{} ) diff --git a/core/types/request.go b/core/types/request.go new file mode 100644 index 0000000000..7b1cade26e --- /dev/null +++ b/core/types/request.go @@ -0,0 +1,157 @@ +// 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" + "errors" + "fmt" + "io" + + "github.com/ethereum/go-ethereum/rlp" +) + +var ( + ErrRequestTypeNotSupported = errors.New("request type not supported") + errShortTypedRequest = errors.New("typed request too short") +) + +// Request types. +const ( + DepositRequestType = 0x00 +) + +// Request is an EIP-7685 request object. It represents execution layer +// triggered messages bound for the consensus layer. +type Request struct { + inner RequestData +} + +// Type returns the EIP-7685 type of the request. +func (r *Request) Type() byte { + return r.inner.requestType() +} + +// Inner returns the inner request data. +func (r *Request) Inner() RequestData { + return r.inner +} + +// NewRequest creates a new request. +func NewRequest(inner RequestData) *Request { + req := new(Request) + req.inner = inner.copy() + return req +} + +// Requests implements DerivableList for requests. +type Requests []*Request + +// Len returns the length of s. +func (s Requests) Len() int { return len(s) } + +// EncodeIndex encodes the i'th request to s. +func (s Requests) EncodeIndex(i int, w *bytes.Buffer) { + s[i].encode(w) +} + +// RequestData is the underlying data of a request. +type RequestData interface { + requestType() byte + encode(*bytes.Buffer) error + decode([]byte) error + copy() RequestData // creates a deep copy and initializes all fields +} + +// EncodeRLP implements rlp.Encoder +func (r *Request) EncodeRLP(w io.Writer) error { + buf := encodeBufferPool.Get().(*bytes.Buffer) + defer encodeBufferPool.Put(buf) + buf.Reset() + if err := r.encode(buf); err != nil { + return err + } + return rlp.Encode(w, buf.Bytes()) +} + +// encode writes the canonical encoding of a request to w. +func (r *Request) encode(w *bytes.Buffer) error { + w.WriteByte(r.Type()) + return r.inner.encode(w) +} + +// MarshalBinary returns the canonical encoding of the request. +func (r *Request) MarshalBinary() ([]byte, error) { + var buf bytes.Buffer + err := r.encode(&buf) + return buf.Bytes(), err +} + +// DecodeRLP implements rlp.Decoder +func (r *Request) DecodeRLP(s *rlp.Stream) error { + kind, size, err := s.Kind() + switch { + case err != nil: + return err + case kind == rlp.List: + return fmt.Errorf("untyped request") + case kind == rlp.Byte: + return errShortTypedRequest + default: + // First read the request payload bytes into a temporary buffer. + b, buf, err := getPooledBuffer(size) + if err != nil { + return err + } + defer encodeBufferPool.Put(buf) + if err := s.ReadBytes(b); err != nil { + return err + } + // Now decode the inner request. + inner, err := r.decode(b) + if err == nil { + r.inner = inner + } + return err + } +} + +// UnmarshalBinary decodes the canonical encoding of requests. +func (r *Request) UnmarshalBinary(b []byte) error { + inner, err := r.decode(b) + if err != nil { + return err + } + r.inner = inner + return nil +} + +// decode decodes a request from the canonical format. +func (r *Request) decode(b []byte) (RequestData, error) { + if len(b) <= 1 { + return nil, errShortTypedRequest + } + var inner RequestData + switch b[0] { + case DepositRequestType: + inner = new(Deposit) + default: + return nil, ErrRequestTypeNotSupported + } + err := inner.decode(b[1:]) + return inner, err +} diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index 48bfb021b9..7ea1c9ee07 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -86,11 +86,15 @@ var caps = []string{ "engine_getPayloadV1", "engine_getPayloadV2", "engine_getPayloadV3", + "engine_getPayloadV4", "engine_newPayloadV1", "engine_newPayloadV2", "engine_newPayloadV3", + "engine_newPayloadV4", "engine_getPayloadBodiesByHashV1", + "engine_getPayloadBodiesByHashV2", "engine_getPayloadBodiesByRangeV1", + "engine_getPayloadBodiesByRangeV2", "engine_getClientVersionV1", } @@ -220,7 +224,7 @@ func (api *ConsensusAPI) ForkchoiceUpdatedV3(update engine.ForkchoiceStateV1, pa if params.BeaconRoot == nil { return engine.STATUS_INVALID, engine.InvalidPayloadAttributes.With(errors.New("missing beacon root")) } - if api.eth.BlockChain().Config().LatestFork(params.Timestamp) != forks.Cancun { + if api.eth.BlockChain().Config().LatestFork(params.Timestamp) != forks.Cancun && api.eth.BlockChain().Config().LatestFork(params.Timestamp) != forks.Prague { return engine.STATUS_INVALID, engine.UnsupportedFork.With(errors.New("forkchoiceUpdatedV3 must only be called for cancun payloads")) } } @@ -443,6 +447,14 @@ func (api *ConsensusAPI) GetPayloadV3(payloadID engine.PayloadID) (*engine.Execu return api.getPayload(payloadID, false) } +// GetPayloadV4 returns a cached payload by id. +func (api *ConsensusAPI) GetPayloadV4(payloadID engine.PayloadID) (*engine.ExecutionPayloadEnvelope, error) { + if !payloadID.Is(engine.PayloadV4) { + return nil, engine.UnsupportedFork + } + return api.getPayload(payloadID, false) +} + func (api *ConsensusAPI) getPayload(payloadID engine.PayloadID, full bool) (*engine.ExecutionPayloadEnvelope, error) { log.Trace("Engine API request received", "method", "GetPayload", "id", payloadID) data := api.localBlocks.get(payloadID, full) @@ -508,6 +520,34 @@ func (api *ConsensusAPI) NewPayloadV3(params engine.ExecutableData, versionedHas return api.newPayload(params, versionedHashes, beaconRoot) } +// NewPayloadV4 creates an Eth1 block, inserts it in the chain, and returns the status of the chain. +func (api *ConsensusAPI) NewPayloadV4(params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash) (engine.PayloadStatusV1, error) { + if params.Withdrawals == nil { + return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("nil withdrawals post-shanghai")) + } + if params.ExcessBlobGas == nil { + return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("nil excessBlobGas post-cancun")) + } + if params.BlobGasUsed == nil { + return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("nil blobGasUsed post-cancun")) + } + if params.Deposits == nil { + return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("nil deposits post-prague")) + } + + if versionedHashes == nil { + return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("nil versionedHashes post-cancun")) + } + if beaconRoot == nil { + return engine.PayloadStatusV1{Status: engine.INVALID}, engine.InvalidParams.With(errors.New("nil beaconRoot post-cancun")) + } + + if api.eth.BlockChain().Config().LatestFork(params.Timestamp) != forks.Prague { + return engine.PayloadStatusV1{Status: engine.INVALID}, engine.UnsupportedFork.With(errors.New("newPayloadV4 must only be called for prague payloads")) + } + return api.newPayload(params, versionedHashes, beaconRoot) +} + func (api *ConsensusAPI) newPayload(params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash) (engine.PayloadStatusV1, error) { // The locking here is, strictly, not required. Without these locks, this can happen: // @@ -553,6 +593,7 @@ func (api *ConsensusAPI) newPayload(params engine.ExecutableData, versionedHashe "params.ExcessBlobGas", ebg, "len(params.Transactions)", len(params.Transactions), "len(params.Withdrawals)", len(params.Withdrawals), + "len(params.Deposits)", len(params.Deposits), "beaconRoot", beaconRoot, "error", err) return api.invalid(err, nil), nil @@ -826,8 +867,25 @@ func (api *ConsensusAPI) GetClientVersionV1(info engine.ClientVersionV1) []engin // GetPayloadBodiesByHashV1 implements engine_getPayloadBodiesByHashV1 which allows for retrieval of a list // of block bodies by the engine api. -func (api *ConsensusAPI) GetPayloadBodiesByHashV1(hashes []common.Hash) []*engine.ExecutionPayloadBodyV1 { - bodies := make([]*engine.ExecutionPayloadBodyV1, len(hashes)) +func (api *ConsensusAPI) GetPayloadBodiesByHashV1(hashes []common.Hash) []*engine.ExecutionPayloadBody { + bodies := make([]*engine.ExecutionPayloadBody, len(hashes)) + for i, hash := range hashes { + block := api.eth.BlockChain().GetBlockByHash(hash) + body := getBody(block) + if body != nil { + // Nil out the V2 values, clients should know to not request V1 objects + // after Prague. + body.Deposits = nil + } + bodies[i] = body + } + return bodies +} + +// GetPayloadBodiesByHashV2 implements engine_getPayloadBodiesByHashV1 which allows for retrieval of a list +// of block bodies by the engine api. +func (api *ConsensusAPI) GetPayloadBodiesByHashV2(hashes []common.Hash) []*engine.ExecutionPayloadBody { + bodies := make([]*engine.ExecutionPayloadBody, len(hashes)) for i, hash := range hashes { block := api.eth.BlockChain().GetBlockByHash(hash) bodies[i] = getBody(block) @@ -837,7 +895,28 @@ func (api *ConsensusAPI) GetPayloadBodiesByHashV1(hashes []common.Hash) []*engin // GetPayloadBodiesByRangeV1 implements engine_getPayloadBodiesByRangeV1 which allows for retrieval of a range // of block bodies by the engine api. -func (api *ConsensusAPI) GetPayloadBodiesByRangeV1(start, count hexutil.Uint64) ([]*engine.ExecutionPayloadBodyV1, error) { +func (api *ConsensusAPI) GetPayloadBodiesByRangeV1(start, count hexutil.Uint64) ([]*engine.ExecutionPayloadBody, error) { + bodies, err := api.getBodiesByRange(start, count) + if err != nil { + return nil, err + } + // Nil out the V2 values, clients should know to not request V1 objects + // after Prague. + for i := range bodies { + if bodies[i] != nil { + bodies[i].Deposits = nil + } + } + return bodies, nil +} + +// GetPayloadBodiesByRangeV2 implements engine_getPayloadBodiesByRangeV1 which allows for retrieval of a range +// of block bodies by the engine api. +func (api *ConsensusAPI) GetPayloadBodiesByRangeV2(start, count hexutil.Uint64) ([]*engine.ExecutionPayloadBody, error) { + return api.getBodiesByRange(start, count) +} + +func (api *ConsensusAPI) getBodiesByRange(start, count hexutil.Uint64) ([]*engine.ExecutionPayloadBody, error) { if start == 0 || count == 0 { return nil, engine.InvalidParams.With(fmt.Errorf("invalid start or count, start: %v count: %v", start, count)) } @@ -850,7 +929,7 @@ func (api *ConsensusAPI) GetPayloadBodiesByRangeV1(start, count hexutil.Uint64) if last > current { last = current } - bodies := make([]*engine.ExecutionPayloadBodyV1, 0, uint64(count)) + bodies := make([]*engine.ExecutionPayloadBody, 0, uint64(count)) for i := uint64(start); i <= last; i++ { block := api.eth.BlockChain().GetBlockByNumber(i) bodies = append(bodies, getBody(block)) @@ -858,15 +937,16 @@ func (api *ConsensusAPI) GetPayloadBodiesByRangeV1(start, count hexutil.Uint64) return bodies, nil } -func getBody(block *types.Block) *engine.ExecutionPayloadBodyV1 { +func getBody(block *types.Block) *engine.ExecutionPayloadBody { if block == nil { return nil } var ( - body = block.Body() - txs = make([]hexutil.Bytes, len(body.Transactions)) - withdrawals = body.Withdrawals + body = block.Body() + txs = make([]hexutil.Bytes, len(body.Transactions)) + withdrawals = body.Withdrawals + depositRequests types.Deposits ) for j, tx := range body.Transactions { @@ -878,8 +958,20 @@ func getBody(block *types.Block) *engine.ExecutionPayloadBodyV1 { withdrawals = make([]*types.Withdrawal, 0) } - return &engine.ExecutionPayloadBodyV1{ + if block.Header().RequestsHash != nil { + // TODO: this isn't future proof because we can't determine if a request + // 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) + } + } + } + + return &engine.ExecutionPayloadBody{ TransactionData: txs, Withdrawals: withdrawals, + Deposits: depositRequests, } } diff --git a/eth/catalyst/api_test.go b/eth/catalyst/api_test.go index 64e6684be1..0a58e1eaee 100644 --- a/eth/catalyst/api_test.go +++ b/eth/catalyst/api_test.go @@ -74,6 +74,12 @@ func generateMergeChain(n int, merged bool) (*core.Genesis, []*types.Block) { Alloc: types.GenesisAlloc{ testAddr: {Balance: testBalance}, params.BeaconRootsAddress: {Balance: common.Big0, Code: common.Hex2Bytes("3373fffffffffffffffffffffffffffffffffffffffe14604457602036146024575f5ffd5b620180005f350680545f35146037575f5ffd5b6201800001545f5260205ff35b6201800042064281555f359062018000015500")}, + config.DepositContractAddress: { + // Simple deposit generator, source: https://gist.github.com/lightclient/54abb2af2465d6969fa6d1920b9ad9d7 + Code: common.Hex2Bytes("6080604052366103aa575f603067ffffffffffffffff811115610025576100246103ae565b5b6040519080825280601f01601f1916602001820160405280156100575781602001600182028036833780820191505090505b5090505f8054906101000a900460ff1660f81b815f8151811061007d5761007c6103db565b5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff191690815f1a9053505f602067ffffffffffffffff8111156100c7576100c66103ae565b5b6040519080825280601f01601f1916602001820160405280156100f95781602001600182028036833780820191505090505b5090505f8054906101000a900460ff1660f81b815f8151811061011f5761011e6103db565b5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff191690815f1a9053505f600867ffffffffffffffff811115610169576101686103ae565b5b6040519080825280601f01601f19166020018201604052801561019b5781602001600182028036833780820191505090505b5090505f8054906101000a900460ff1660f81b815f815181106101c1576101c06103db565b5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff191690815f1a9053505f606067ffffffffffffffff81111561020b5761020a6103ae565b5b6040519080825280601f01601f19166020018201604052801561023d5781602001600182028036833780820191505090505b5090505f8054906101000a900460ff1660f81b815f81518110610263576102626103db565b5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff191690815f1a9053505f600867ffffffffffffffff8111156102ad576102ac6103ae565b5b6040519080825280601f01601f1916602001820160405280156102df5781602001600182028036833780820191505090505b5090505f8054906101000a900460ff1660f81b815f81518110610305576103046103db565b5b60200101907effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff191690815f1a9053505f8081819054906101000a900460ff168092919061035090610441565b91906101000a81548160ff021916908360ff160217905550507f649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c585858585856040516103a09594939291906104d9565b60405180910390a1005b5f80fd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52604160045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52603260045260245ffd5b7f4e487b71000000000000000000000000000000000000000000000000000000005f52601160045260245ffd5b5f60ff82169050919050565b5f61044b82610435565b915060ff820361045e5761045d610408565b5b600182019050919050565b5f81519050919050565b5f82825260208201905092915050565b8281835e5f83830152505050565b5f601f19601f8301169050919050565b5f6104ab82610469565b6104b58185610473565b93506104c5818560208601610483565b6104ce81610491565b840191505092915050565b5f60a0820190508181035f8301526104f181886104a1565b9050818103602083015261050581876104a1565b9050818103604083015261051981866104a1565b9050818103606083015261052d81856104a1565b9050818103608083015261054181846104a1565b9050969550505050505056fea26469706673582212208569967e58690162d7d6fe3513d07b393b4c15e70f41505cbbfd08f53eba739364736f6c63430008190033"), + Nonce: 0, + Balance: big.NewInt(0), + }, }, ExtraData: []byte("test genesis"), Timestamp: 9000, @@ -483,10 +489,10 @@ func TestFullAPI(t *testing.T) { ethservice.TxPool().Add([]*types.Transaction{tx}, true, false) } - setupBlocks(t, ethservice, 10, parent, callback, nil) + setupBlocks(t, ethservice, 10, parent, callback, nil, nil) } -func setupBlocks(t *testing.T, ethservice *eth.Ethereum, n int, parent *types.Header, callback func(parent *types.Header), withdrawals [][]*types.Withdrawal) []*types.Header { +func setupBlocks(t *testing.T, ethservice *eth.Ethereum, n int, parent *types.Header, callback func(parent *types.Header), withdrawals [][]*types.Withdrawal, beaconRoots []common.Hash) []*types.Header { api := NewConsensusAPI(ethservice) var blocks []*types.Header for i := 0; i < n; i++ { @@ -495,14 +501,18 @@ func setupBlocks(t *testing.T, ethservice *eth.Ethereum, n int, parent *types.He if withdrawals != nil { w = withdrawals[i] } + var h *common.Hash + if beaconRoots != nil { + h = &beaconRoots[i] + } - payload := getNewPayload(t, api, parent, w) - execResp, err := api.NewPayloadV2(*payload) + payload := getNewPayload(t, api, parent, w, h) + execResp, err := api.newPayload(*payload, []common.Hash{}, h) if err != nil { t.Fatalf("can't execute payload: %v", err) } if execResp.Status != engine.VALID { - t.Fatalf("invalid status: %v", execResp.Status) + t.Fatalf("invalid status: %v %s", execResp.Status, *execResp.ValidationError) } fcState := engine.ForkchoiceStateV1{ HeadBlockHash: payload.BlockHash, @@ -690,10 +700,10 @@ func TestEmptyBlocks(t *testing.T) { api := NewConsensusAPI(ethservice) // Setup 10 blocks on the canonical chain - setupBlocks(t, ethservice, 10, commonAncestor, func(parent *types.Header) {}, nil) + setupBlocks(t, ethservice, 10, commonAncestor, func(parent *types.Header) {}, nil, nil) // (1) check LatestValidHash by sending a normal payload (P1'') - payload := getNewPayload(t, api, commonAncestor, nil) + payload := getNewPayload(t, api, commonAncestor, nil, nil) status, err := api.NewPayloadV1(*payload) if err != nil { @@ -707,7 +717,7 @@ func TestEmptyBlocks(t *testing.T) { } // (2) Now send P1' which is invalid - payload = getNewPayload(t, api, commonAncestor, nil) + payload = getNewPayload(t, api, commonAncestor, nil, nil) payload.GasUsed += 1 payload = setBlockhash(payload) // Now latestValidHash should be the common ancestor @@ -725,7 +735,7 @@ func TestEmptyBlocks(t *testing.T) { } // (3) Now send a payload with unknown parent - payload = getNewPayload(t, api, commonAncestor, nil) + payload = getNewPayload(t, api, commonAncestor, nil, nil) payload.ParentHash = common.Hash{1} payload = setBlockhash(payload) // Now latestValidHash should be the common ancestor @@ -741,12 +751,13 @@ func TestEmptyBlocks(t *testing.T) { } } -func getNewPayload(t *testing.T, api *ConsensusAPI, parent *types.Header, withdrawals []*types.Withdrawal) *engine.ExecutableData { +func getNewPayload(t *testing.T, api *ConsensusAPI, parent *types.Header, withdrawals []*types.Withdrawal, beaconRoot *common.Hash) *engine.ExecutableData { params := engine.PayloadAttributes{ Timestamp: parent.Time + 1, Random: crypto.Keccak256Hash([]byte{byte(1)}), SuggestedFeeRecipient: parent.Coinbase, Withdrawals: withdrawals, + BeaconRoot: beaconRoot, } payload, err := assembleBlock(api, parent.Hash(), ¶ms) @@ -814,7 +825,7 @@ func TestTrickRemoteBlockCache(t *testing.T) { commonAncestor := ethserviceA.BlockChain().CurrentBlock() // Setup 10 blocks on the canonical chain - setupBlocks(t, ethserviceA, 10, commonAncestor, func(parent *types.Header) {}, nil) + setupBlocks(t, ethserviceA, 10, commonAncestor, func(parent *types.Header) {}, nil, nil) commonAncestor = ethserviceA.BlockChain().CurrentBlock() var invalidChain []*engine.ExecutableData @@ -823,7 +834,7 @@ func TestTrickRemoteBlockCache(t *testing.T) { //invalidChain = append(invalidChain, payload1) // create an invalid payload2 (P2) - payload2 := getNewPayload(t, apiA, commonAncestor, nil) + payload2 := getNewPayload(t, apiA, commonAncestor, nil, nil) //payload2.ParentHash = payload1.BlockHash payload2.GasUsed += 1 payload2 = setBlockhash(payload2) @@ -832,7 +843,7 @@ func TestTrickRemoteBlockCache(t *testing.T) { head := payload2 // create some valid payloads on top for i := 0; i < 10; i++ { - payload := getNewPayload(t, apiA, commonAncestor, nil) + payload := getNewPayload(t, apiA, commonAncestor, nil, nil) payload.ParentHash = head.BlockHash payload = setBlockhash(payload) invalidChain = append(invalidChain, payload) @@ -869,10 +880,10 @@ func TestInvalidBloom(t *testing.T) { api := NewConsensusAPI(ethservice) // Setup 10 blocks on the canonical chain - setupBlocks(t, ethservice, 10, commonAncestor, func(parent *types.Header) {}, nil) + setupBlocks(t, ethservice, 10, commonAncestor, func(parent *types.Header) {}, nil, nil) // (1) check LatestValidHash by sending a normal payload (P1'') - payload := getNewPayload(t, api, commonAncestor, nil) + payload := getNewPayload(t, api, commonAncestor, nil, nil) payload.LogsBloom = append(payload.LogsBloom, byte(1)) status, err := api.NewPayloadV1(*payload) if err != nil { @@ -1285,24 +1296,35 @@ func TestNilWithdrawals(t *testing.T) { func setupBodies(t *testing.T) (*node.Node, *eth.Ethereum, []*types.Block) { genesis, blocks := generateMergeChain(10, true) - // enable shanghai on the last block + + // Enable next forks on the last block. time := blocks[len(blocks)-1].Header().Time + 1 genesis.Config.ShanghaiTime = &time + genesis.Config.CancunTime = &time + genesis.Config.PragueTime = &time + n, ethservice := startEthService(t, genesis, blocks) var ( - parent = ethservice.BlockChain().CurrentBlock() // This EVM code generates a log when the contract is created. logCode = common.Hex2Bytes("60606040525b7f24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b60405180905060405180910390a15b600a8060416000396000f360606040526008565b00") + parent = ethservice.BlockChain().CurrentBlock() ) + // Each block, this callback will include two txs that generate body values like logs and requests. callback := func(parent *types.Header) { - statedb, _ := ethservice.BlockChain().StateAt(parent.Root) - nonce := statedb.GetNonce(testAddr) - tx, _ := types.SignTx(types.NewContractCreation(nonce, new(big.Int), 1000000, big.NewInt(2*params.InitialBaseFee), logCode), types.LatestSigner(ethservice.BlockChain().Config()), testKey) - ethservice.TxPool().Add([]*types.Transaction{tx}, false, false) + var ( + statedb, _ = ethservice.BlockChain().StateAt(parent.Root) + // Create tx to trigger log generator. + tx1, _ = types.SignTx(types.NewContractCreation(statedb.GetNonce(testAddr), new(big.Int), 1000000, big.NewInt(2*params.InitialBaseFee), logCode), types.LatestSigner(ethservice.BlockChain().Config()), testKey) + // Create tx to trigger deposit generator. + tx2, _ = types.SignTx(types.NewTransaction(statedb.GetNonce(testAddr)+1, ethservice.APIBackend.ChainConfig().DepositContractAddress, new(big.Int), 500000, big.NewInt(2*params.InitialBaseFee), nil), types.LatestSigner(ethservice.BlockChain().Config()), testKey) + ) + ethservice.TxPool().Add([]*types.Transaction{tx1}, false, false) + ethservice.TxPool().Add([]*types.Transaction{tx2}, false, false) } + // Make some withdrawals to include. withdrawals := make([][]*types.Withdrawal, 10) withdrawals[0] = nil // should be filtered out by miner withdrawals[1] = make([]*types.Withdrawal, 0) @@ -1314,12 +1336,20 @@ func setupBodies(t *testing.T) (*node.Node, *eth.Ethereum, []*types.Block) { } } - postShanghaiHeaders := setupBlocks(t, ethservice, 10, parent, callback, withdrawals) - postShanghaiBlocks := make([]*types.Block, len(postShanghaiHeaders)) - for i, header := range postShanghaiHeaders { - postShanghaiBlocks[i] = ethservice.BlockChain().GetBlock(header.Hash(), header.Number.Uint64()) + // Make beacon root update for each block. + beaconRoots := make([]common.Hash, 10) + for i := 0; i < 10; i++ { + beaconRoots[i] = common.Hash{byte(i)} + } + + // Create the blocks. + newHeaders := setupBlocks(t, ethservice, 10, parent, callback, withdrawals, beaconRoots) + newBlocks := make([]*types.Block, len(newHeaders)) + for i, header := range newHeaders { + newBlocks[i] = ethservice.BlockChain().GetBlock(header.Hash(), header.Number.Uint64()) } - return n, ethservice, append(blocks, postShanghaiBlocks...) + + return n, ethservice, append(blocks, newBlocks...) } func allHashes(blocks []*types.Block) []common.Hash { @@ -1384,7 +1414,7 @@ func TestGetBlockBodiesByHash(t *testing.T) { } for k, test := range tests { - result := api.GetPayloadBodiesByHashV1(test.hashes) + result := api.GetPayloadBodiesByHashV2(test.hashes) for i, r := range result { if !equalBody(test.results[i], r) { t.Fatalf("test %v: invalid response: expected %+v got %+v", k, test.results[i], r) @@ -1458,7 +1488,7 @@ func TestGetBlockBodiesByRange(t *testing.T) { } for k, test := range tests { - result, err := api.GetPayloadBodiesByRangeV1(test.start, test.count) + result, err := api.GetPayloadBodiesByRangeV2(test.start, test.count) if err != nil { t.Fatal(err) } @@ -1509,7 +1539,7 @@ func TestGetBlockBodiesByRangeInvalidParams(t *testing.T) { }, } for i, tc := range tests { - result, err := api.GetPayloadBodiesByRangeV1(tc.start, tc.count) + result, err := api.GetPayloadBodiesByRangeV2(tc.start, tc.count) if err == nil { t.Fatalf("test %d: expected error, got %v", i, result) } @@ -1519,7 +1549,7 @@ func TestGetBlockBodiesByRangeInvalidParams(t *testing.T) { } } -func equalBody(a *types.Body, b *engine.ExecutionPayloadBodyV1) bool { +func equalBody(a *types.Body, b *engine.ExecutionPayloadBody) bool { if a == nil && b == nil { return true } else if a == nil || b == nil { @@ -1534,7 +1564,23 @@ func equalBody(a *types.Body, b *engine.ExecutionPayloadBodyV1) bool { return false } } - return reflect.DeepEqual(a.Withdrawals, b.Withdrawals) + + if !reflect.DeepEqual(a.Withdrawals, b.Withdrawals) { + return false + } + + var deposits types.Deposits + if a.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. + deposits = make(types.Deposits, 0) + } + for _, r := range a.Requests { + if d, ok := r.Inner().(*types.Deposit); ok { + deposits = append(deposits, d) + } + } + return reflect.DeepEqual(deposits, b.Deposits) } func TestBlockToPayloadWithBlobs(t *testing.T) { diff --git a/eth/catalyst/simulated_beacon.go b/eth/catalyst/simulated_beacon.go index 86355a1533..cb27810aac 100644 --- a/eth/catalyst/simulated_beacon.go +++ b/eth/catalyst/simulated_beacon.go @@ -109,7 +109,7 @@ func NewSimulatedBeacon(period uint64, eth *eth.Ethereum) (*SimulatedBeacon, err // if genesis block, send forkchoiceUpdated to trigger transition to PoS if block.Number.Sign() == 0 { - if _, err := engineAPI.ForkchoiceUpdatedV2(current, nil); err != nil { + if _, err := engineAPI.ForkchoiceUpdatedV3(current, nil); err != nil { return nil, err } } @@ -226,7 +226,7 @@ func (c *SimulatedBeacon) sealBlock(withdrawals []*types.Withdrawal, timestamp u c.setCurrentState(payload.BlockHash, finalizedHash) // Mark the block containing the payload as canonical - if _, err = c.engineAPI.ForkchoiceUpdatedV2(c.curForkchoiceState, nil); err != nil { + if _, err = c.engineAPI.ForkchoiceUpdatedV3(c.curForkchoiceState, nil); err != nil { return err } c.lastBlockTime = payload.Timestamp diff --git a/eth/downloader/downloader_test.go b/eth/downloader/downloader_test.go index 0cbddee6bf..c440b40e9d 100644 --- a/eth/downloader/downloader_test.go +++ b/eth/downloader/downloader_test.go @@ -230,6 +230,7 @@ func (dlp *downloadTesterPeer) RequestBodies(hashes []common.Hash, sink chan *et txsHashes = make([]common.Hash, len(bodies)) uncleHashes = make([]common.Hash, len(bodies)) withdrawalHashes = make([]common.Hash, len(bodies)) + requestsHashes = make([]common.Hash, len(bodies)) ) hasher := trie.NewStackTrie(nil) for i, body := range bodies { @@ -248,7 +249,7 @@ func (dlp *downloadTesterPeer) RequestBodies(hashes []common.Hash, sink chan *et res := ð.Response{ Req: req, Res: (*eth.BlockBodiesResponse)(&bodies), - Meta: [][]common.Hash{txsHashes, uncleHashes, withdrawalHashes}, + Meta: [][]common.Hash{txsHashes, uncleHashes, withdrawalHashes, requestsHashes}, Time: 1, Done: make(chan error, 1), // Ignore the returned status } diff --git a/eth/downloader/fetchers_concurrent_bodies.go b/eth/downloader/fetchers_concurrent_bodies.go index 56359b33c9..709df77575 100644 --- a/eth/downloader/fetchers_concurrent_bodies.go +++ b/eth/downloader/fetchers_concurrent_bodies.go @@ -88,10 +88,10 @@ func (q *bodyQueue) request(peer *peerConnection, req *fetchRequest, resCh chan // deliver is responsible for taking a generic response packet from the concurrent // fetcher, unpacking the body data and delivering it to the downloader's queue. func (q *bodyQueue) deliver(peer *peerConnection, packet *eth.Response) (int, error) { - txs, uncles, withdrawals := packet.Res.(*eth.BlockBodiesResponse).Unpack() - hashsets := packet.Meta.([][]common.Hash) // {txs hashes, uncle hashes, withdrawal hashes} + txs, uncles, withdrawals, requests := packet.Res.(*eth.BlockBodiesResponse).Unpack() + hashsets := packet.Meta.([][]common.Hash) // {txs hashes, uncle hashes, withdrawal hashes, requests hashes} - accepted, err := q.queue.DeliverBodies(peer.id, txs, hashsets[0], uncles, hashsets[1], withdrawals, hashsets[2]) + accepted, err := q.queue.DeliverBodies(peer.id, txs, hashsets[0], uncles, hashsets[1], withdrawals, hashsets[2], requests, hashsets[3]) switch { case err == nil && len(txs) == 0: peer.log.Trace("Requested bodies delivered") diff --git a/eth/downloader/queue.go b/eth/downloader/queue.go index 5441ad1187..adad450200 100644 --- a/eth/downloader/queue.go +++ b/eth/downloader/queue.go @@ -784,7 +784,8 @@ func (q *queue) DeliverHeaders(id string, headers []*types.Header, hashes []comm // also wakes any threads waiting for data delivery. func (q *queue) DeliverBodies(id string, txLists [][]*types.Transaction, txListHashes []common.Hash, uncleLists [][]*types.Header, uncleListHashes []common.Hash, - withdrawalLists [][]*types.Withdrawal, withdrawalListHashes []common.Hash) (int, error) { + withdrawalLists [][]*types.Withdrawal, withdrawalListHashes []common.Hash, + requestsLists [][]*types.Request, requestsListHashes []common.Hash) (int, error) { q.lock.Lock() defer q.lock.Unlock() @@ -808,6 +809,19 @@ func (q *queue) DeliverBodies(id string, txLists [][]*types.Transaction, txListH return errInvalidBody } } + if header.RequestsHash == nil { + // nil hash means that requests should not be present in body + if requestsLists[index] != nil { + return errInvalidBody + } + } else { // non-nil hash: body must have requests + if requestsLists[index] == nil { + return errInvalidBody + } + if requestsListHashes[index] != *header.RequestsHash { + return errInvalidBody + } + } // Blocks must have a number of blobs corresponding to the header gas usage, // and zero before the Cancun hardfork. var blobs int diff --git a/eth/downloader/queue_test.go b/eth/downloader/queue_test.go index 857ac4813a..e29d23f80b 100644 --- a/eth/downloader/queue_test.go +++ b/eth/downloader/queue_test.go @@ -341,7 +341,7 @@ func XTestDelivery(t *testing.T) { uncleHashes[i] = types.CalcUncleHash(uncles) } time.Sleep(100 * time.Millisecond) - _, err := q.DeliverBodies(peer.id, txset, txsHashes, uncleset, uncleHashes, nil, nil) + _, err := q.DeliverBodies(peer.id, txset, txsHashes, uncleset, uncleHashes, nil, nil, nil, nil) if err != nil { fmt.Printf("delivered %d bodies %v\n", len(txset), err) } diff --git a/eth/protocols/eth/handlers.go b/eth/protocols/eth/handlers.go index b3886270f3..951352319f 100644 --- a/eth/protocols/eth/handlers.go +++ b/eth/protocols/eth/handlers.go @@ -316,6 +316,7 @@ func handleBlockBodies(backend Backend, msg Decoder, peer *Peer) error { txsHashes = make([]common.Hash, len(res.BlockBodiesResponse)) uncleHashes = make([]common.Hash, len(res.BlockBodiesResponse)) withdrawalHashes = make([]common.Hash, len(res.BlockBodiesResponse)) + requestsHashes = make([]common.Hash, len(res.BlockBodiesResponse)) ) hasher := trie.NewStackTrie(nil) for i, body := range res.BlockBodiesResponse { @@ -324,8 +325,11 @@ func handleBlockBodies(backend Backend, msg Decoder, peer *Peer) error { if body.Withdrawals != nil { withdrawalHashes[i] = types.DeriveSha(types.Withdrawals(body.Withdrawals), hasher) } + if body.Requests != nil { + requestsHashes[i] = types.DeriveSha(types.Requests(body.Requests), hasher) + } } - return [][]common.Hash{txsHashes, uncleHashes, withdrawalHashes} + return [][]common.Hash{txsHashes, uncleHashes, withdrawalHashes, requestsHashes} } return peer.dispatchResponse(&Response{ id: res.RequestId, diff --git a/eth/protocols/eth/protocol.go b/eth/protocols/eth/protocol.go index c5cb2dd1dc..cbc895eabb 100644 --- a/eth/protocols/eth/protocol.go +++ b/eth/protocols/eth/protocol.go @@ -224,21 +224,22 @@ type BlockBody struct { Transactions []*types.Transaction // Transactions contained within a block Uncles []*types.Header // Uncles contained within a block Withdrawals []*types.Withdrawal `rlp:"optional"` // Withdrawals contained within a block + Requests []*types.Request `rlp:"optional"` // Requests contained within a block } // Unpack retrieves the transactions and uncles from the range packet and returns // them in a split flat format that's more consistent with the internal data structures. -func (p *BlockBodiesResponse) Unpack() ([][]*types.Transaction, [][]*types.Header, [][]*types.Withdrawal) { - // TODO(matt): add support for withdrawals to fetchers +func (p *BlockBodiesResponse) Unpack() ([][]*types.Transaction, [][]*types.Header, [][]*types.Withdrawal, [][]*types.Request) { var ( txset = make([][]*types.Transaction, len(*p)) uncleset = make([][]*types.Header, len(*p)) withdrawalset = make([][]*types.Withdrawal, len(*p)) + requestset = make([][]*types.Request, len(*p)) ) for i, body := range *p { - txset[i], uncleset[i], withdrawalset[i] = body.Transactions, body.Uncles, body.Withdrawals + txset[i], uncleset[i], withdrawalset[i], requestset[i] = body.Transactions, body.Uncles, body.Withdrawals, body.Requests } - return txset, uncleset, withdrawalset + return txset, uncleset, withdrawalset, requestset } // GetReceiptsRequest represents a block receipts query. diff --git a/eth/state_accessor.go b/eth/state_accessor.go index 90f7c01391..7b6f5ba150 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -146,7 +146,7 @@ func (eth *Ethereum) hashState(ctx context.Context, block *types.Block, reexec u if current = eth.blockchain.GetBlockByNumber(next); current == nil { return nil, nil, fmt.Errorf("block #%d not found", next) } - _, _, _, err := eth.blockchain.Processor().Process(current, statedb, vm.Config{}) + _, err := eth.blockchain.Processor().Process(current, statedb, vm.Config{}) if err != nil { return nil, nil, fmt.Errorf("processing block %d failed: %v", current.NumberU64(), err) } diff --git a/eth/tracers/logger/logger.go b/eth/tracers/logger/logger.go index 7638c3cd09..b952c82286 100644 --- a/eth/tracers/logger/logger.go +++ b/eth/tracers/logger/logger.go @@ -390,7 +390,7 @@ func (t *mdLogger) OnExit(depth int, output []byte, gasUsed uint64, err error, r // OnOpcode also tracks SLOAD/SSTORE ops to track storage change. func (t *mdLogger) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) { stack := scope.StackData() - fmt.Fprintf(t.out, "| %4d | %10v | %3d |", pc, op, cost) + fmt.Fprintf(t.out, "| %4d | %10v | %3d |", pc, vm.OpCode(op).String(), cost) if !t.cfg.DisableStack { // format stack diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go index f769ee8475..a1148bcedb 100644 --- a/ethclient/ethclient.go +++ b/ethclient/ethclient.go @@ -123,6 +123,7 @@ type rpcBlock struct { Transactions []rpcTransaction `json:"transactions"` UncleHashes []common.Hash `json:"uncles"` Withdrawals []*types.Withdrawal `json:"withdrawals,omitempty"` + Requests []*types.Request `json:"requests,omitempty"` } func (ec *Client) getBlock(ctx context.Context, method string, args ...interface{}) (*types.Block, error) { @@ -196,6 +197,7 @@ func (ec *Client) getBlock(ctx context.Context, method string, args ...interface Transactions: txs, Uncles: uncles, Withdrawals: body.Withdrawals, + Requests: body.Requests, }), nil } diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 1c3cb4adf9..aeb3e8adc2 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1268,6 +1268,9 @@ func RPCMarshalHeader(head *types.Header) map[string]interface{} { if head.ParentBeaconRoot != nil { result["parentBeaconBlockRoot"] = head.ParentBeaconRoot } + if head.RequestsHash != nil { + result["requestsRoot"] = head.RequestsHash + } return result } @@ -1303,6 +1306,9 @@ func RPCMarshalBlock(block *types.Block, inclTx bool, fullTx bool, config *param if block.Header().WithdrawalsHash != nil { fields["withdrawals"] = block.Withdrawals() } + if block.Header().RequestsHash != nil { + fields["requests"] = block.Requests() + } return fields } diff --git a/miner/worker.go b/miner/worker.go index 4e566f1489..1f49118c47 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -105,7 +105,20 @@ func (miner *Miner) generateWork(params *generateParams) *newPayloadResult { log.Warn("Block building is interrupted", "allowance", common.PrettyDuration(miner.config.Recommit)) } } + body := types.Body{Transactions: work.txs, Withdrawals: params.withdrawals} + allLogs := make([]*types.Log, 0) + for _, r := range work.receipts { + allLogs = append(allLogs, r.Logs...) + } + // Read requests if Prague is enabled. + if miner.chainConfig.IsPrague(work.header.Number, work.header.Time) { + requests, err := core.ParseDepositLogs(allLogs, miner.chainConfig) + if err != nil { + return &newPayloadResult{err: err} + } + body.Requests = requests + } block, err := miner.engine.FinalizeAndAssemble(miner.chain, work.header, work.state, &body, work.receipts) if err != nil { return &newPayloadResult{err: err} diff --git a/params/config.go b/params/config.go index c4e6ba6948..9ecf465bb6 100644 --- a/params/config.go +++ b/params/config.go @@ -58,6 +58,7 @@ var ( TerminalTotalDifficultyPassed: true, ShanghaiTime: newUint64(1681338455), CancunTime: newUint64(1710338135), + DepositContractAddress: common.HexToAddress("0x00000000219ab540356cbb839cbe05303d7705fa"), Ethash: new(EthashConfig), } // HoleskyChainConfig contains the chain parameters to run a node on the Holesky test network. @@ -337,6 +338,8 @@ type ChainConfig struct { // TODO(karalabe): Drop this field eventually (always assuming PoS mode) TerminalTotalDifficultyPassed bool `json:"terminalTotalDifficultyPassed,omitempty"` + DepositContractAddress common.Address `json:"depositContractAddress,omitempty"` + // Various consensus engines Ethash *EthashConfig `json:"ethash,omitempty"` Clique *CliqueConfig `json:"clique,omitempty"` diff --git a/tests/init.go b/tests/init.go index c85e714c00..4bb83f9300 100644 --- a/tests/init.go +++ b/tests/init.go @@ -373,6 +373,7 @@ var Forks = map[string]*params.ChainConfig{ ShanghaiTime: u64(0), CancunTime: u64(0), PragueTime: u64(0), + DepositContractAddress: params.MainnetChainConfig.DepositContractAddress, }, "CancunToPragueAtTime15k": { ChainID: big.NewInt(1), @@ -393,6 +394,7 @@ var Forks = map[string]*params.ChainConfig{ ShanghaiTime: u64(0), CancunTime: u64(0), PragueTime: u64(15_000), + DepositContractAddress: params.MainnetChainConfig.DepositContractAddress, }, }