From b34a925e9e5819d345a938ee2779fea9d59ce482 Mon Sep 17 00:00:00 2001 From: Stefan <22667037+qu0b@users.noreply.github.com> Date: Mon, 13 Jul 2026 09:55:39 +0200 Subject: [PATCH] beacon/engine, eth/catalyst: return block access lists in payload bodies v2 (#35347) --- beacon/engine/types.go | 6 ++++ eth/catalyst/api.go | 31 +++++++++++------ eth/catalyst/api_test.go | 74 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 99 insertions(+), 12 deletions(-) diff --git a/beacon/engine/types.go b/beacon/engine/types.go index 980e8f9771..abe46bb351 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -415,6 +415,12 @@ type ExecutionPayloadBody struct { Withdrawals []*types.Withdrawal `json:"withdrawals"` } +// ExecutionPayloadBodyV2 extends ExecutionPayloadBody with the block access list. +type ExecutionPayloadBodyV2 struct { + ExecutionPayloadBody + BlockAccessList *bal.BlockAccessList `json:"blockAccessList"` +} + // Client identifiers to support ClientVersionV1. const ( ClientCode = "GE" diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index 4b0c3f6ea3..5119051806 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -1176,13 +1176,13 @@ func (api *ConsensusAPI) GetPayloadBodiesByHashV1(hashes []common.Hash) []*engin return bodies } -// GetPayloadBodiesByHashV2 implements engine_getPayloadBodiesByHashV1 which allows for retrieval of a list +// GetPayloadBodiesByHashV2 implements engine_getPayloadBodiesByHashV2 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)) +func (api *ConsensusAPI) GetPayloadBodiesByHashV2(hashes []common.Hash) []*engine.ExecutionPayloadBodyV2 { + bodies := make([]*engine.ExecutionPayloadBodyV2, len(hashes)) for i, hash := range hashes { block := api.eth.BlockChain().GetBlockByHash(hash) - bodies[i] = getBody(block) + bodies[i] = getBodyV2(block) } return bodies } @@ -1190,16 +1190,16 @@ func (api *ConsensusAPI) GetPayloadBodiesByHashV2(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.ExecutionPayloadBody, error) { - return api.getBodiesByRange(start, count) + return getBodiesByRange(api, start, count, getBody) } -// GetPayloadBodiesByRangeV2 implements engine_getPayloadBodiesByRangeV1 which allows for retrieval of a range +// GetPayloadBodiesByRangeV2 implements engine_getPayloadBodiesByRangeV2 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) GetPayloadBodiesByRangeV2(start, count hexutil.Uint64) ([]*engine.ExecutionPayloadBodyV2, error) { + return getBodiesByRange(api, start, count, getBodyV2) } -func (api *ConsensusAPI) getBodiesByRange(start, count hexutil.Uint64) ([]*engine.ExecutionPayloadBody, error) { +func getBodiesByRange[T any](api *ConsensusAPI, start, count hexutil.Uint64, getBody func(*types.Block) *T) ([]*T, error) { if start == 0 || count == 0 { return nil, engine.InvalidParams.With(fmt.Errorf("invalid start or count, start: %v count: %v", start, count)) } @@ -1212,7 +1212,7 @@ func (api *ConsensusAPI) getBodiesByRange(start, count hexutil.Uint64) ([]*engin if last > current { last = current } - bodies := make([]*engine.ExecutionPayloadBody, 0, uint64(count)) + bodies := make([]*T, 0, uint64(count)) for i := uint64(start); i <= last; i++ { block := api.eth.BlockChain().GetBlockByNumber(i) bodies = append(bodies, getBody(block)) @@ -1241,6 +1241,17 @@ func getBody(block *types.Block) *engine.ExecutionPayloadBody { return &result } +func getBodyV2(block *types.Block) *engine.ExecutionPayloadBodyV2 { + body := getBody(block) + if body == nil { + return nil + } + return &engine.ExecutionPayloadBodyV2{ + ExecutionPayloadBody: *body, + BlockAccessList: block.AccessList(), + } +} + // convertRequests converts a hex requests slice to plain [][]byte. func convertRequests(hex []hexutil.Bytes) [][]byte { if hex == nil { diff --git a/eth/catalyst/api_test.go b/eth/catalyst/api_test.go index 849c123979..783dda59b0 100644 --- a/eth/catalyst/api_test.go +++ b/eth/catalyst/api_test.go @@ -39,6 +39,7 @@ import ( "github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/ethereum/go-ethereum/eth" @@ -1366,7 +1367,7 @@ func TestGetBlockBodiesByHash(t *testing.T) { for k, test := range tests { result := api.GetPayloadBodiesByHashV2(test.hashes) for i, r := range result { - if err := checkEqualBody(test.results[i], r); err != nil { + if err := checkEqualBodyV2(test.results[i], r); err != nil { t.Fatalf("test %v: invalid response: %v\nexpected %+v\ngot %+v", k, err, test.results[i], r) } } @@ -1444,7 +1445,7 @@ func TestGetBlockBodiesByRange(t *testing.T) { } if len(result) == len(test.results) { for i, r := range result { - if err := checkEqualBody(test.results[i], r); err != nil { + if err := checkEqualBodyV2(test.results[i], r); err != nil { t.Fatalf("test %d: invalid response: %v\nexpected %+v\ngot %+v", k, err, test.results[i], r) } } @@ -1520,6 +1521,75 @@ func checkEqualBody(a *types.Body, b *engine.ExecutionPayloadBody) error { return nil } +func checkEqualBodyV2(a *types.Body, b *engine.ExecutionPayloadBodyV2) error { + if b == nil { + return checkEqualBody(a, nil) + } + return checkEqualBody(a, &b.ExecutionPayloadBody) +} + +func TestGetPayloadBodyV2BlockAccessList(t *testing.T) { + empty := bal.BlockAccessList{} + emptyHash := empty.Hash() + tests := []struct { + name string + header *types.Header + accessList *bal.BlockAccessList + want string + }{ + { + name: "retained empty BAL", + header: &types.Header{BlockAccessListHash: &emptyHash}, + accessList: &empty, + want: "[]", + }, + { + name: "pruned BAL", + header: &types.Header{BlockAccessListHash: &emptyHash}, + want: "null", + }, + { + name: "pre-Amsterdam block", + header: new(types.Header), + want: "null", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + block := types.NewBlockWithHeader(test.header).WithAccessListUnsafe(test.accessList) + body := getBodyV2(block) + encoded, err := json.Marshal(body) + if err != nil { + t.Fatal(err) + } + var fields map[string]json.RawMessage + if err := json.Unmarshal(encoded, &fields); err != nil { + t.Fatal(err) + } + if got := string(fields["blockAccessList"]); got != test.want { + t.Fatalf("unexpected blockAccessList: got %s, want %s", got, test.want) + } + }) + } +} + +func TestGetPayloadBodyV1OmitsBlockAccessList(t *testing.T) { + empty := bal.BlockAccessList{} + emptyHash := empty.Hash() + block := types.NewBlockWithHeader(&types.Header{BlockAccessListHash: &emptyHash}).WithAccessListUnsafe(&empty) + encoded, err := json.Marshal(getBody(block)) + if err != nil { + t.Fatal(err) + } + var fields map[string]json.RawMessage + if err := json.Unmarshal(encoded, &fields); err != nil { + t.Fatal(err) + } + if _, ok := fields["blockAccessList"]; ok { + t.Fatal("V1 payload body contains blockAccessList") + } +} + func TestBlockToPayloadWithBlobs(t *testing.T) { header := types.Header{} var txs []*types.Transaction