Skip to content

execution: carry the block access list as a first-class block object - #22894

Merged
AskAlexSharov merged 3 commits into
mainfrom
mh/bal-first-class-block
Jul 31, 2026
Merged

execution: carry the block access list as a first-class block object#22894
AskAlexSharov merged 3 commits into
mainfrom
mh/bal-first-class-block

Conversation

@mh0lt

@mh0lt mh0lt commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Independent refactor split out of #21414 (per the request to break it up). Stands alone on main.

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 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; SetBlockAccessList copies the input so a tx-owned slice can't alias it. Regression test pins it out of RLP/hash.
  • newPayload attaches the payload's BAL to the block; exec consumes it from the block, falling back to the DB sidecar for snapshot/forward-sync blocks.

Behaviour-preserving: the !dbg.IgnoreBAL gate and BAL validation are unchanged — this only changes where exec sources the BAL (block vs redundant DB re-read). It's the plumbing that lets the parallel/fold-ahead commitment (in #21414) consume the BAL; nothing computes commitment in parallel from it by itself. Part of the split recorded on #21414.

… 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.
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
@AskAlexSharov

Copy link
Copy Markdown
Collaborator

@mh0lt AccessList must be field of Block or field of Body?

@AskAlexSharov
AskAlexSharov added this pull request to the merge queue Jul 31, 2026
@AskAlexSharov

Copy link
Copy Markdown
Collaborator

@mh0lt nits:

  1. RawBlock.AsBlock bypasses the copy invariant. b.blockAccessList = r.BlockAccessList assigns without cloning, in the same PR whose description
    says "SetBlockAccessList copies the input so a tx-owned slice can't alias it". AsBlock has no production callers today (only its definition and
    one polygon test), so it's latent — but it's precisely the aliasing hole the setter exists to close. Use the setter.

  2. WithSeal shares the slice while Copy clones it. WithSeal is on the sealing path. Sharing is safe only under the "nobody mutates it in place"
    assumption that the setter deliberately refuses to make. Pick one rule.

  3. ReadBlock swallows the read error. if bal, err := ...; err == nil && len(bal) > 0 turns a real MDBX failure into "no BAL". Not fatal — exec's
    fallback re-reads and surfaces it — but ReadBlock already signals failure by returning nil elsewhere.

Merged via the queue into main with commit d33d977 Jul 31, 2026
111 checks passed
@AskAlexSharov
AskAlexSharov deleted the mh/bal-first-class-block branch July 31, 2026 06:19
@yperbasis yperbasis added the Glamsterdam https://eips.ethereum.org/EIPS/eip-7773 label Jul 31, 2026
// BlockOverlay is active. ProcessBAL still computes+validates the BAL
// from the write-set as the ultimate fallback.
data := b.BlockAccessList()
if len(data) == 0 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if len(data) == 0 && header.HasBAL() to save an unnecessary call to DB

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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

header.HasBAL()

pull Bot pushed a commit to Dustin4444/erigon that referenced this pull request Jul 31, 2026
Follow-up to erigontech#22894 addressing the [staged-sync review
comment](https://github.com/erigontech/erigon/pull/22894/changes#r3689214008)
and the [rawdb review
comment](https://github.com/erigontech/erigon/pull/22894/changes#r3689241839).

The original `Header.HasBAL()` predicate conflated two states: whether
the BAL commitment field is present and whether it commits to a
non-empty BAL. Those meanings need different behavior for a canonical
empty BAL encoded as `0xc0`.

This change:

- defines `Header.HasBAL()` as presence of the BAL commitment field;
- adds `Header.HasNonEmptyBAL()` for a commitment other than the
canonical empty-list hash;
- uses `HasNonEmptyBAL()` in staged sync and backward BAL fetching to
avoid unnecessary database and network work;
- keeps `rawdb.ReadBlock` on `HasBAL()` so a stored empty `0xc0` sidecar
is preserved;
- covers missing, empty, and non-empty commitments, including the
required single DB lookup for a missing non-empty sidecar;
- makes `TestAssembleBlockWithFreshlyAddedTxns` wait for observed txpool
polls instead of a fixed delay.

This keeps block reconstruction faithful while retaining the intended
optimization for BAL consumers that do not need an empty sidecar. The
builder-test change removes timing dependence and does not change
production block-building behavior.

TDD note: the CI stabilization is a test-only synchronization refactor,
so no production behavior was changed. The failed race-test job is the
original red signal.

Validation:

- `go test ./execution/types ./db/rawdb ./execution/stagedsync
./execution/p2p -count=1`
- `go test ./execution/stagedsync ./execution/execmodule -run
'^(TestBlockAccessListBytes|TestAssembleBlockWithFreshlyAddedTxns)$'
-count=20`
- `ERIGON_EXEC3_PARALLEL=true GOMAXPROCS=4 go test -race -timeout=10m
./execution/stagedsync ./execution/execmodule -run
'^(TestBlockAccessListBytes|TestAssembleBlockWithFreshlyAddedTxns)$'
-count=50`
- `ERIGON_EXEC3_PARALLEL=true GOMAXPROCS=4 go test -race -timeout=60m
./execution/execmodule -count=3`
- `go test ./execution/stagedsync ./execution/execmodule -count=1`
- `make lint` (four consecutive clean runs)
- `make erigon integration`
SoarinSkySagar pushed a commit to SoarinSkySagar/erigon that referenced this pull request Aug 6, 2026
- removes `block.SetBlockAccessList` in favour of always passing it via
block constructors as we do for withdrawals, transactions, header, etc
- we can now also simplify our API by removing `bal` from
`execModule.InsertBlocks` because they are now attached to the block
after erigontech#22894
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Glamsterdam https://eips.ethereum.org/EIPS/eip-7773

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants