From 9826cd65bc708792a34ff4ef02b7279c2d3263cb Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Mon, 6 Feb 2023 10:21:40 +0100 Subject: [PATCH] eth/catalyst: implement engine_getPayloadBodiesByHash/Range methods (#26232) This change implements engine_getPayloadBodiesByHash and engine_getPayloadBodiesByRange, according to the specification at https://github.com/ethereum/execution-apis/blob/main/src/engine/shanghai.md#specification-4 . Co-authored-by: Martin Holst Swende --- core/beacon/types.go | 6 ++ eth/catalyst/api.go | 60 ++++++++++++ eth/catalyst/api_test.go | 195 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 260 insertions(+), 1 deletion(-) diff --git a/core/beacon/types.go b/core/beacon/types.go index 656115c78..4c1218d16 100644 --- a/core/beacon/types.go +++ b/core/beacon/types.go @@ -229,3 +229,9 @@ func BlockToExecutableData(block *types.Block, fees *big.Int) *ExecutionPayloadE } return &ExecutionPayloadEnvelope{ExecutionPayload: data, BlockValue: fees} } + +// ExecutionPayloadBodyV1 is used in the response to GetPayloadBodiesByHashV1 and GetPayloadBodiesByRangeV1 +type ExecutionPayloadBodyV1 struct { + TransactionData []hexutil.Bytes `json:"transactions"` + Withdrawals []*types.Withdrawal `json:"withdrawals,omitempty"` +} diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index c56e8bbac..0706cccdc 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -88,6 +88,8 @@ var caps = []string{ "engine_getPayloadV2", "engine_newPayloadV1", "engine_newPayloadV2", + "engine_getPayloadBodiesByHashV1", + "engine_getPayloadBodiesByRangeV1", } type ConsensusAPI struct { @@ -756,3 +758,61 @@ func (api *ConsensusAPI) heartbeat() { func (api *ConsensusAPI) ExchangeCapabilities([]string) []string { return caps } + +// GetPayloadBodiesV1 implements engine_getPayloadBodiesByHashV1 which allows for retrieval of a list +// of block bodies by the engine api. +func (api *ConsensusAPI) GetPayloadBodiesByHashV1(hashes []common.Hash) []*beacon.ExecutionPayloadBodyV1 { + var bodies = make([]*beacon.ExecutionPayloadBodyV1, len(hashes)) + for i, hash := range hashes { + block := api.eth.BlockChain().GetBlockByHash(hash) + bodies[i] = getBody(block) + } + return bodies +} + +// GetPayloadBodiesByRangeV1 implements engine_getPayloadBodiesByRangeV1 which allows for retrieval of a range +// of block bodies by the engine api. +func (api *ConsensusAPI) GetPayloadBodiesByRangeV1(start, count uint64) ([]*beacon.ExecutionPayloadBodyV1, error) { + if start == 0 || count == 0 || count > 1024 { + return nil, beacon.InvalidParams.With(fmt.Errorf("invalid start or count, start: %v count: %v", start, count)) + } + // limit count up until current + current := api.eth.BlockChain().CurrentBlock().NumberU64() + end := start + count + if end > current { + end = current + } + var bodies []*beacon.ExecutionPayloadBodyV1 + for i := start; i < end; i++ { + block := api.eth.BlockChain().GetBlockByNumber(i) + bodies = append(bodies, getBody(block)) + } + return bodies, nil +} + +func getBody(block *types.Block) *beacon.ExecutionPayloadBodyV1 { + if block == nil { + return nil + } + + var ( + body = block.Body() + txs = make([]hexutil.Bytes, len(body.Transactions)) + withdrawals = body.Withdrawals + ) + + for j, tx := range body.Transactions { + data, _ := tx.MarshalBinary() + txs[j] = hexutil.Bytes(data) + } + + // Post-shanghai withdrawals MUST be set to empty slice instead of nil + if withdrawals == nil && block.Header().WithdrawalsHash != nil { + withdrawals = make([]*types.Withdrawal, 0) + } + + return &beacon.ExecutionPayloadBodyV1{ + TransactionData: txs, + Withdrawals: withdrawals, + } +} diff --git a/eth/catalyst/api_test.go b/eth/catalyst/api_test.go index d9280e99d..372557559 100644 --- a/eth/catalyst/api_test.go +++ b/eth/catalyst/api_test.go @@ -41,6 +41,7 @@ import ( "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/trie" ) @@ -475,8 +476,9 @@ func TestFullAPI(t *testing.T) { setupBlocks(t, ethservice, 10, parent, callback) } -func setupBlocks(t *testing.T, ethservice *eth.Ethereum, n int, parent *types.Block, callback func(parent *types.Block)) { +func setupBlocks(t *testing.T, ethservice *eth.Ethereum, n int, parent *types.Block, callback func(parent *types.Block)) []*types.Block { api := NewConsensusAPI(ethservice) + var blocks []*types.Block for i := 0; i < n; i++ { callback(parent) @@ -504,7 +506,9 @@ func setupBlocks(t *testing.T, ethservice *eth.Ethereum, n int, parent *types.Bl t.Fatal("Finalized block should be updated") } parent = ethservice.BlockChain().CurrentBlock() + blocks = append(blocks, parent) } + return blocks } func TestExchangeTransitionConfig(t *testing.T) { @@ -1225,3 +1229,192 @@ func TestNilWithdrawals(t *testing.T) { } } } + +func setupBodies(t *testing.T) (*node.Node, *eth.Ethereum, []*types.Block) { + genesis, preMergeBlocks := generateMergeChain(10, false) + n, ethservice := startEthService(t, genesis, preMergeBlocks) + + var ( + parent = ethservice.BlockChain().CurrentBlock() + // This EVM code generates a log when the contract is created. + logCode = common.Hex2Bytes("60606040525b7f24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b60405180905060405180910390a15b600a8060416000396000f360606040526008565b00") + ) + + callback := func(parent *types.Block) { + 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().AddLocal(tx) + } + + postMergeBlocks := setupBlocks(t, ethservice, 10, parent, callback) + return n, ethservice, append(preMergeBlocks, postMergeBlocks...) +} + +func TestGetBlockBodiesByHash(t *testing.T) { + node, eth, blocks := setupBodies(t) + api := NewConsensusAPI(eth) + defer node.Close() + + tests := []struct { + results []*types.Body + hashes []common.Hash + }{ + // First pow block + { + results: []*types.Body{eth.BlockChain().GetBlockByNumber(0).Body()}, + hashes: []common.Hash{eth.BlockChain().GetBlockByNumber(0).Hash()}, + }, + // Last pow block + { + results: []*types.Body{blocks[9].Body()}, + hashes: []common.Hash{blocks[9].Hash()}, + }, + // First post-merge block + { + results: []*types.Body{blocks[10].Body()}, + hashes: []common.Hash{blocks[10].Hash()}, + }, + // Pre & post merge blocks + { + results: []*types.Body{blocks[0].Body(), blocks[9].Body(), blocks[14].Body()}, + hashes: []common.Hash{blocks[0].Hash(), blocks[9].Hash(), blocks[14].Hash()}, + }, + // unavailable block + { + results: []*types.Body{blocks[0].Body(), nil, blocks[14].Body()}, + hashes: []common.Hash{blocks[0].Hash(), {1, 2}, blocks[14].Hash()}, + }, + // same block multiple times + { + results: []*types.Body{blocks[0].Body(), nil, blocks[0].Body(), blocks[0].Body()}, + hashes: []common.Hash{blocks[0].Hash(), {1, 2}, blocks[0].Hash(), blocks[0].Hash()}, + }, + } + + for k, test := range tests { + result := api.GetPayloadBodiesByHashV1(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) + } + } + } +} + +func TestGetBlockBodiesByRange(t *testing.T) { + node, eth, blocks := setupBodies(t) + api := NewConsensusAPI(eth) + defer node.Close() + + tests := []struct { + results []*types.Body + start uint64 + count uint64 + }{ + // Genesis + { + results: []*types.Body{blocks[0].Body()}, + start: 1, + count: 1, + }, + // First post-merge block + { + results: []*types.Body{blocks[9].Body()}, + start: 10, + count: 1, + }, + // Pre & post merge blocks + { + results: []*types.Body{blocks[7].Body(), blocks[8].Body(), blocks[9].Body(), blocks[10].Body()}, + start: 8, + count: 4, + }, + // unavailable block + { + results: []*types.Body{blocks[18].Body()}, + start: 19, + count: 3, + }, + // after range + { + results: make([]*types.Body, 0), + start: 20, + count: 2, + }, + } + + for k, test := range tests { + result, err := api.GetPayloadBodiesByRangeV1(test.start, test.count) + if err != nil { + t.Fatal(err) + } + if len(result) == len(test.results) { + 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) + } + } + } else { + t.Fatalf("invalid length want %v got %v", len(test.results), len(result)) + } + } +} + +func TestGetBlockBodiesByRangeInvalidParams(t *testing.T) { + node, eth, _ := setupBodies(t) + api := NewConsensusAPI(eth) + defer node.Close() + + tests := []struct { + start uint64 + count uint64 + }{ + // Genesis + { + start: 0, + count: 1, + }, + // No block requested + { + start: 1, + count: 0, + }, + // Genesis & no block + { + start: 0, + count: 0, + }, + // More than 1024 blocks + { + start: 1, + count: 1025, + }, + } + + for _, test := range tests { + result, err := api.GetPayloadBodiesByRangeV1(test.start, test.count) + if err == nil { + t.Fatalf("expected error, got %v", result) + } + } +} + +func equalBody(a *types.Body, b *beacon.ExecutionPayloadBodyV1) bool { + if a == nil && b == nil { + return true + } else if a == nil || b == nil { + return false + } + var want []hexutil.Bytes + for _, tx := range a.Transactions { + data, _ := tx.MarshalBinary() + want = append(want, hexutil.Bytes(data)) + } + aBytes, errA := rlp.EncodeToBytes(want) + bBytes, errB := rlp.EncodeToBytes(b.TransactionData) + if errA != errB { + return false + } + return bytes.Equal(aBytes, bBytes) +}