From 695974b7267e9e44970cdff4c2973964bf914237 Mon Sep 17 00:00:00 2001 From: mh0lt Date: Tue, 14 Jul 2026 11:09:20 +0000 Subject: [PATCH 1/2] execution/types, execution/engineapi, execution/stagedsync: carry the block access list as a first-class block object EIP-7928 Block Access Lists are part of the payload and should be consumed by execution from the block it is handed, the same model as headers and bodies. Previously the BAL only existed transiently on RawBlock during InsertBlocks and RawBlock.AsBlock dropped it, so the block object execution processes never carried its own BAL and exec re-read it from the DB. - types.Block gains an unexported blockAccessList sidecar (BlockAccessList / SetBlockAccessList), carried out-of-band and never in the block RLP/hash; RawBlock.AsBlock/Copy/WithSeal transfer it. SetBlockAccessList copies the input so a transaction-owned slice cannot alias it. Regression test pins it out of RLP/hash. - newPayload attaches the payload's BAL to the block. - Execution consumes the BAL from the block, falling back to the DB sidecar for snapshot/forward-sync blocks that don't carry it. - rawdb.ReadBlock populates the BAL sidecar as secondary storage. --- db/rawdb/accessors_chain.go | 12 +++++++++- execution/engineapi/engine_server.go | 4 ++++ execution/stagedsync/exec3.go | 19 +++++++++++----- execution/types/block.go | 33 +++++++++++++++++++++------- execution/types/block_test.go | 32 +++++++++++++++++++++++++++ 5 files changed, 85 insertions(+), 15 deletions(-) diff --git a/db/rawdb/accessors_chain.go b/db/rawdb/accessors_chain.go index fc7c6df8e7e..6b67a46cd0f 100644 --- a/db/rawdb/accessors_chain.go +++ b/db/rawdb/accessors_chain.go @@ -609,7 +609,10 @@ func ReadBlockAccessListBytes(db kv.Getter, hash common.Hash, number uint64) ([] return data, nil } -// WriteBlockAccessListBytes stores the RLP-encoded block access list sidecar for a block. +// WriteBlockAccessListBytes stores the RLP-encoded block access list sidecar for +// a block. This is secondary storage (serving, backfill, unwind); the primary +// carry into execution is Block.BlockAccessList(), written via the block overlay +// in InsertBlocks and flushed at commit. func WriteBlockAccessListBytes(db kv.Putter, hash common.Hash, number uint64, data []byte) error { if err := db.Put(kv.BlockAccessList, dbutils.BlockBodyKey(number, hash), data); err != nil { return fmt.Errorf("failed to store block access list: %w", err) @@ -807,6 +810,13 @@ func ReadBlock(tx kv.Getter, hash common.Hash, number uint64) *types.Block { return nil } block := types.NewBlockFromStorage(hash, header, body.Transactions, body.Uncles, body.Withdrawals) + // Carry the BAL sidecar (secondary storage) so a block reconstructed from the + // DB carries its BAL like its header/body. Only Amsterdam+ blocks have one. + if header.BlockAccessListHash != nil { + if bal, err := ReadBlockAccessListBytes(tx, hash, number); err == nil && len(bal) > 0 { + block.SetBlockAccessList(bal) + } + } return block } diff --git a/execution/engineapi/engine_server.go b/execution/engineapi/engine_server.go index 1ce2cad4be0..59c3a08cec8 100644 --- a/execution/engineapi/engine_server.go +++ b/execution/engineapi/engine_server.go @@ -483,6 +483,10 @@ func (s *EngineServer) newPayload(ctx context.Context, req *engine_types.Executi // via rlp.EncodeToBytes. Both slices reference the same underlying // byte buffers from req.Transactions. block := types.NewBlockFromStorageWithBinaryTxs(blockHash, &header, transactions, txs, nil /* uncles */, withdrawals) + // Carry the payload's BAL on the block so execution consumes it from the + // in-memory payload; InsertBlocks still stores it in the overlay (flushed at + // commit) as secondary storage. + block.SetBlockAccessList(blockAccessListBytes) payloadStatus, err := s.HandleNewPayload(ctx, "NewPayload", block, expectedBlobHashes, blockAccessListBytes) if err != nil { if errors.Is(err, rules.ErrInvalidBlock) { diff --git a/execution/stagedsync/exec3.go b/execution/stagedsync/exec3.go index c8250d4ef43..88b1ccb1cad 100644 --- a/execution/stagedsync/exec3.go +++ b/execution/stagedsync/exec3.go @@ -612,12 +612,19 @@ func (te *txExecutor) executeBlocks(ctx context.Context, startBlockNum uint64, m go warmTxsHashes(b) var dbBAL types.BlockAccessList - // Read BAL through blockTx (overlay or execRoTx) — do NOT open - // a separate db.View() as it can deadlock with the stageloop's - // RW transaction when BlockOverlay is active. - data, err := rawdb.ReadBlockAccessListBytes(blockTx, b.Hash(), blockNum) - if err != nil { - return err + // Prefer the BAL carried on the block (the payload) — the newPayload / + // backward-sync paths attach it, so no read is needed. Fall back to the + // BAL sidecar in the DB (via blockTx: overlay or execRoTx) for blocks + // that don't carry it (snapshot / forward-sync); do NOT open a separate + // db.View() as it can deadlock with the stageloop's RW transaction when + // BlockOverlay is active. ProcessBAL still computes+validates the BAL + // from the write-set as the ultimate fallback. + data := b.BlockAccessList() + if len(data) == 0 { + data, err = rawdb.ReadBlockAccessListBytes(blockTx, b.Hash(), blockNum) + if err != nil { + return err + } } if len(data) > 0 && !dbg.IgnoreBAL { dbBAL, err = types.DecodeBlockAccessListBytes(data) diff --git a/execution/types/block.go b/execution/types/block.go index 17ed9fb2afe..c31f91f4909 100644 --- a/execution/types/block.go +++ b/execution/types/block.go @@ -808,6 +808,7 @@ func (r RawBlock) AsBlock() (*Block, error) { } } b.transactions = txs + b.blockAccessList = r.BlockAccessList return b, nil } @@ -819,6 +820,12 @@ type Block struct { transactions Transactions withdrawals []*Withdrawal + // blockAccessList is the RLP-encoded EIP-7928 Block Access List sidecar + // carried with the payload (nil pre-Amsterdam). It is NOT part of the block's + // RLP/consensus encoding or hash — never add it to EncodeRLP/DecodeRLP/ + // payloadSize. The header's BlockAccessListHash is the consensus commitment. + blockAccessList []byte + // binaryTransactions optionally caches the transactions' encodings (e.g. from // an engine_newPayload payload) so RawBody() can skip re-encoding them. binaryTransactions BinaryTransactions @@ -1380,6 +1387,14 @@ func (b *Block) ParentBeaconBlockRoot() *common.Hash { return b.header.ParentBea func (b *Block) RequestsHash() *common.Hash { return b.header.RequestsHash } func (b *Block) BlockAccessListHash() *common.Hash { return b.header.BlockAccessListHash } +// BlockAccessList returns the RLP-encoded EIP-7928 BAL sidecar carried with the +// payload (nil when absent). It is not part of the block's RLP encoding or hash. +func (b *Block) BlockAccessList() []byte { return b.blockAccessList } + +// SetBlockAccessList attaches the RLP-encoded BAL sidecar to the block, copying +// the input so a transaction-owned or later-mutated source cannot alias it. +func (b *Block) SetBlockAccessList(bal []byte) { b.blockAccessList = bytes.Clone(bal) } + // Header returns a deep-copy of the entire block header using CopyHeader() func (b *Block) Header() *Header { return CopyHeader(b.header) } func (b *Block) HeaderNoCopy() *Header { return b.header } @@ -1540,10 +1555,11 @@ func (b *Block) Copy() *Block { } newB := &Block{ - header: CopyHeader(b.header), - uncles: uncles, - transactions: CopyTxs(b.transactions), - withdrawals: withdrawals, + header: CopyHeader(b.header), + uncles: uncles, + transactions: CopyTxs(b.transactions), + withdrawals: withdrawals, + blockAccessList: common.Copy(b.blockAccessList), } szCopy := b.size.Load() newB.size.Store(szCopy) @@ -1557,10 +1573,11 @@ func (b *Block) WithSeal(header *Header) *Block { headerCopy.mutable = false headerCopy.hash.Store(nil) // invalidate cached hash return &Block{ - header: headerCopy, - transactions: b.transactions, - uncles: b.uncles, - withdrawals: b.withdrawals, + header: headerCopy, + transactions: b.transactions, + uncles: b.uncles, + withdrawals: b.withdrawals, + blockAccessList: b.blockAccessList, } } diff --git a/execution/types/block_test.go b/execution/types/block_test.go index 2983c19967c..63100628f6f 100644 --- a/execution/types/block_test.go +++ b/execution/types/block_test.go @@ -162,6 +162,38 @@ func TestBlockEncoding(t *testing.T) { } } +// TestBlockAccessListNotInEncoding pins the invariant that the BAL sidecar is +// carried out-of-band: it must never enter the block's RLP encoding or hash. +func TestBlockAccessListNotInEncoding(t *testing.T) { + t.Parallel() + blockEnc := common.FromHex("f90260f901f9a083cafc574e1f51ba9dc0568fc617a08ea2429fb384059c972f13b19fa1c8dd55a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347948888f1f195afa192cfee860698584c030f4c9db1a0ef1552a40b7165c3cd773806b9e0c165b75356e0314bf0706f279c729f51e017a05fe50b260da6308036625b850b5d6ced6d0a9f814c0688bc91ffb7b7a3a54b67a0bc37d79753ad738a6dac4921e57392f145d8887476de3f783dfa7edae9283e52b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008302000001832fefd8825208845506eb0780a0bd4472abb6659ebe3ee06ee4d7b72a00a9f4d001caca51342001075469aff49888a13a5a8c8f2bb1c4f861f85f800a82c35094095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba09bea4c4daac7c7c52e093e6a4c35dbbcf8856f1af7b059ba20253e70848d094fa08a8fae537ce25ed8cb5af9adac3f141af69bd515bd2ba031522df09b97dd72b1c0") + var block Block + if err := rlp.DecodeBytes(blockEnc, &block); err != nil { + t.Fatal("decode error: ", err) + } + + hashBefore := block.Hash() + block.SetBlockAccessList([]byte{0x01, 0x02, 0x03}) + if got := block.Hash(); got != hashBefore { + t.Errorf("BAL changed block hash: got %x want %x", got, hashBefore) + } + enc, err := rlp.EncodeToBytes(&block) + if err != nil { + t.Fatal("encode error: ", err) + } + if !bytes.Equal(enc, blockEnc) { + t.Errorf("BAL leaked into block RLP:\ngot: %x\nwant: %x", enc, blockEnc) + } + + var decoded Block + if err := rlp.DecodeBytes(enc, &decoded); err != nil { + t.Fatal("decode error: ", err) + } + if decoded.BlockAccessList() != nil { + t.Errorf("BAL survived RLP round-trip (must be a non-encoded sidecar): %x", decoded.BlockAccessList()) + } +} + func TestEIP1559BlockEncoding(t *testing.T) { t.Parallel() blockEnc := common.FromHex("f9030bf901fea083cafc574e1f51ba9dc0568fc617a08ea2429fb384059c972f13b19fa1c8dd55a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347948888f1f195afa192cfee860698584c030f4c9db1a0ef1552a40b7165c3cd773806b9e0c165b75356e0314bf0706f279c729f51e017a05fe50b260da6308036625b850b5d6ced6d0a9f814c0688bc91ffb7b7a3a54b67a0bc37d79753ad738a6dac4921e57392f145d8887476de3f783dfa7edae9283e52b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008302000001832fefd8825208845506eb0780a0bd4472abb6659ebe3ee06ee4d7b72a00a9f4d001caca51342001075469aff49888a13a5a8c8f2bb1c4843b9aca00f90106f85f800a82c35094095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba09bea4c4daac7c7c52e093e6a4c35dbbcf8856f1af7b059ba20253e70848d094fa08a8fae537ce25ed8cb5af9adac3f141af69bd515bd2ba031522df09b97dd72b1b8a302f8a0018080843b9aca008301e24194095e7baea6a6c7c4c2dfeb977efac326af552d878080f838f7940000000000000000000000000000000000000001e1a0000000000000000000000000000000000000000000000000000000000000000080a0fe38ca4e44a30002ac54af7cf922a6ac2ba11b7d22f548e8ecb3f51f41cb31b0a06de6a5cbae13c0c856e33acf021b51819636cfc009d39eafb9f606d546e305a8c0") From 64e4840f9f36e925816d6c7691469740a1d44452 Mon Sep 17 00:00:00 2001 From: Alexey Sharov Date: Fri, 31 Jul 2026 09:51:04 +0700 Subject: [PATCH 2/2] execution/types: inline common.Copy in Block.Copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit common.Copy is marked //go:fix inline, so vet's inline analyzer flags any call to it. The rest of the tree already uses bytes.Clone directly — this was the only remaining call site. Claude-Session: https://claude.ai/code/session_01UYCsHj9HYTJnqUCy8a7W91 --- execution/types/block.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/execution/types/block.go b/execution/types/block.go index c31f91f4909..e0b5d631d2d 100644 --- a/execution/types/block.go +++ b/execution/types/block.go @@ -1559,7 +1559,7 @@ func (b *Block) Copy() *Block { uncles: uncles, transactions: CopyTxs(b.transactions), withdrawals: withdrawals, - blockAccessList: common.Copy(b.blockAccessList), + blockAccessList: bytes.Clone(b.blockAccessList), } szCopy := b.size.Load() newB.size.Store(szCopy)