Skip to content

Commit ff3b684

Browse files
authored
execution: treat empty BAL as valid access list (EIP-7928) (#20776)
This update aligns BAL handling with the requirements described in [EIP-7928](https://eips.ethereum.org/EIPS/eip-7928), which requires that even an empty BAL be recognized as a legitimate access list. According to the specification, the header should include the `rlp.encode([])` hash, while the payload should have the standard empty RLP list (`0xc0`). Previously, even though the header hash was established correctly, the BAL bytes themselves were not saved. This effectively regarded an empty BAL as if it did not exist, contradicting the desired behavior indicated in the EIP. With this change, empty BALs are now handled consistently like any other BAL. The RLP decoder now returns an initialized empty slice instead of nil, so `0xc0` naturally round-trips through the standard encode/decode path without special-case logic. The `len(block.BlockAccessList) > 0` guard in the DB inserter correctly persists the `[]byte{0xc0}` sidecar (length 1), and the engine API response encodes it back via standard RLP rules. The payload response path also retains the `br.BlockAccessList != nil` guard so that a genuinely missing BAL is never silently emitted as `0xc0`. This removes any ambiguity between a valid empty BAL and cases where BAL data is missing or pruned.
1 parent d0c8dd4 commit ff3b684

6 files changed

Lines changed: 81 additions & 15 deletions

File tree

db/rawdb/accessors_chain_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1171,7 +1171,10 @@ func TestBlockAccessListStorage(t *testing.T) {
11711171

11721172
decoded, err = types.DecodeBlockAccessListBytes(data)
11731173
require.NoError(t, err)
1174-
require.Nil(t, decoded)
1174+
// EIP-7928: Decoding 0xc0 (empty RLP list) should return an initialized empty slice,
1175+
// rather than nil, to distinguish a valid empty BAL from a missing/pruned one.
1176+
require.NotNil(t, decoded)
1177+
require.Empty(t, decoded)
11751178
require.NoError(t, decoded.Validate())
11761179
require.Equal(t, empty.BlockAccessListHash, decoded.Hash())
11771180
}

execution/engineapi/engine_server.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,8 +347,13 @@ func (s *EngineServer) newPayload(ctx context.Context, req *engine_types.Executi
347347
return nil, &rpc.InvalidParamsError{Message: "blockAccessList missing"}
348348
}
349349
if len(req.BlockAccessList) == 0 {
350-
blockAccessList = nil
351-
header.BlockAccessListHash = &empty.BlockAccessListHash
350+
blockAccessList = make(types.BlockAccessList, 0)
351+
hash := empty.BlockAccessListHash
352+
header.BlockAccessListHash = &hash
353+
blockAccessListBytes, err = types.EncodeBlockAccessListBytes(blockAccessList)
354+
if err != nil {
355+
return nil, &rpc.InvalidParamsError{Message: fmt.Sprintf("encode empty blockAccessList: %v", err)}
356+
}
352357
} else {
353358
blockAccessList, err = types.DecodeBlockAccessListBytes(req.BlockAccessList)
354359
if err != nil {
@@ -1079,6 +1084,9 @@ func assembledBlockToPayloadResponse(br *types.BlockWithReceipts, blockValue *ui
10791084
ep.SlotNumber = &sn
10801085
}
10811086
if header.BlockAccessListHash != nil && br.BlockAccessList != nil {
1087+
// EIP-7928: encode even when br.BlockAccessList is empty — an empty
1088+
// BAL serializes to 0xc0 via standard RLP rules. A nil BAL means
1089+
// the data is missing/not-populated and should not be emitted.
10821090
encoded, encErr := types.EncodeBlockAccessListBytes(br.BlockAccessList)
10831091
if encErr == nil {
10841092
ep.BlockAccessList = encoded

execution/engineapi/engine_server_getpayload_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ import (
2424
"github.com/holiman/uint256"
2525
"github.com/stretchr/testify/require"
2626

27+
"github.com/erigontech/erigon/cl/clparams"
2728
"github.com/erigontech/erigon/common"
29+
"github.com/erigontech/erigon/common/empty"
2830
"github.com/erigontech/erigon/common/hexutil"
2931
"github.com/erigontech/erigon/common/log/v3"
3032
"github.com/erigontech/erigon/execution/builder"
@@ -74,6 +76,29 @@ func TestGetPayloadV4AcceptsEmptyRequestsBundle(t *testing.T) {
7476
require.Len(t, resp.ExecutionRequests, 0)
7577
}
7678

79+
func TestAssembledBlockToPayloadResponseIncludesCanonicalEmptyBAL(t *testing.T) {
80+
t.Parallel()
81+
82+
baseFee := uint256.NewInt(1_000_000_000)
83+
emptyBALHash := empty.BlockAccessListHash
84+
header := &types.Header{
85+
Number: *uint256.NewInt(101),
86+
Time: 1,
87+
BaseFee: baseFee,
88+
GasLimit: 30_000_000,
89+
BlockAccessListHash: &emptyBALHash,
90+
}
91+
block := types.NewBlockWithHeader(header)
92+
br := &types.BlockWithReceipts{Block: block, BlockAccessList: make(types.BlockAccessList, 0), Requests: make(types.FlatRequests, 0)}
93+
94+
resp, err := assembledBlockToPayloadResponse(br, uint256.NewInt(0), clparams.GloasVersion)
95+
require.NoError(t, err)
96+
97+
emptyBAL, err := types.EncodeBlockAccessListBytes(make(types.BlockAccessList, 0))
98+
require.NoError(t, err)
99+
require.Equal(t, hexutil.Bytes(emptyBAL), resp.ExecutionPayload.BlockAccessList)
100+
}
101+
77102
func newProposingEngineServerForGetPayloadTests(stub execmodule.ExecutionModule) *EngineServer {
78103
cfg := allForksChainConfig()
79104
// GetPayloadV4 is valid on Prague but invalid once Osaka activates.

execution/execmodule/inserters.go

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,7 @@ func (e *ExecModule) InsertBlocks(ctx context.Context, blocks []*types.RawBlock)
118118
if header.BlockAccessListHash == nil {
119119
return 0, fmt.Errorf("ethereumExecutionModule.InsertBlocks: block access list provided without hash for block %d", height)
120120
}
121-
balBytes := block.BlockAccessList
122-
if len(balBytes) == 0 {
123-
balBytes, err = types.EncodeBlockAccessListBytes(nil)
124-
if err != nil {
125-
return 0, fmt.Errorf("ethereumExecutionModule.InsertBlocks: encode empty block access list, block %d: %s", height, err)
126-
}
127-
}
128-
if err := rawdb.WriteBlockAccessListBytes(blockOverlay, header.Hash(), height, balBytes); err != nil {
121+
if err := rawdb.WriteBlockAccessListBytes(blockOverlay, header.Hash(), height, block.BlockAccessList); err != nil {
129122
return 0, fmt.Errorf("ethereumExecutionModule.InsertBlocks: writeBlockAccessList, block %d: %s", height, err)
130123
}
131124
}

execution/types/block_access_list.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,8 @@ func decodeBlockAccessList(out *BlockAccessList, s *rlp.Stream) error {
523523
var size uint64
524524
if size, err = s.List(); err != nil {
525525
if errors.Is(err, rlp.EOL) {
526+
// EOL at List() time means the BAL value is missing/pruned,
527+
// not that an empty list (0xc0) was decoded. Return nil.
526528
*out = nil
527529
return nil
528530
}
@@ -558,7 +560,9 @@ func decodeBlockAccessList(out *BlockAccessList, s *rlp.Stream) error {
558560
return err
559561
}
560562
if len(changes) == 0 {
561-
*out = nil
563+
// EIP-7928: a genuine empty list (0xc0) was successfully decoded.
564+
// Return an initialized empty slice, not nil.
565+
*out = make(BlockAccessList, 0)
562566
return nil
563567
}
564568
*out = changes
@@ -577,9 +581,6 @@ func DecodeBlockAccessListBytes(data []byte) (BlockAccessList, error) {
577581

578582
// EncodeBlockAccessListBytes encodes a block access list into RLP bytes.
579583
func EncodeBlockAccessListBytes(bal BlockAccessList) ([]byte, error) {
580-
if len(bal) == 0 {
581-
return []byte{0xc0}, nil
582-
}
583584
if err := bal.Validate(); err != nil {
584585
return nil, err
585586
}

execution/types/block_access_list_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,39 @@ func TestBlockAccessListHashEmpty(t *testing.T) {
180180
t.Fatalf("empty BAL should be valid: %v", err)
181181
}
182182
}
183+
184+
// TestBlockAccessListEmptyRoundTrip verifies that an empty BAL encodes to the
185+
// canonical empty RLP list (0xc0) and decodes back to a non-nil empty slice.
186+
// EIP-7928 requires: "When no state changes are present, this field is the
187+
// empty RLP list 0xc0, i.e. rlp.encode([])."
188+
func TestBlockAccessListEmptyRoundTrip(t *testing.T) {
189+
// Encode nil BAL — must produce 0xc0.
190+
encoded, err := EncodeBlockAccessListBytes(nil)
191+
if err != nil {
192+
t.Fatalf("encode nil BAL: %v", err)
193+
}
194+
if !bytes.Equal(encoded, []byte{0xc0}) {
195+
t.Fatalf("nil BAL encoding: got %x, want c0", encoded)
196+
}
197+
198+
// Encode empty (non-nil) BAL — must also produce 0xc0.
199+
encoded2, err := EncodeBlockAccessListBytes(make(BlockAccessList, 0))
200+
if err != nil {
201+
t.Fatalf("encode empty BAL: %v", err)
202+
}
203+
if !bytes.Equal(encoded2, []byte{0xc0}) {
204+
t.Fatalf("empty BAL encoding: got %x, want c0", encoded2)
205+
}
206+
207+
// Decode 0xc0 — must produce non-nil empty slice (not nil).
208+
decoded, err := DecodeBlockAccessListBytes(encoded)
209+
if err != nil {
210+
t.Fatalf("decode empty BAL: %v", err)
211+
}
212+
if decoded == nil {
213+
t.Fatal("decoded empty BAL must be non-nil")
214+
}
215+
if len(decoded) != 0 {
216+
t.Fatalf("decoded empty BAL length: got %d, want 0", len(decoded))
217+
}
218+
}

0 commit comments

Comments
 (0)