erigontech / erigontech/erigon

execution: block assembly publishes raw write-sets, so EIP-161 clearing does not apply on the builder path

Open
#22,828 1 comment 0 reactions 1 assignee Claimed by @yperbasis View on GitHub
tech debt reduction
Dominant language
Go
Stars
3.6k
Forks
1.5k
Avg merge
1d 16h
Merged PRs (30d)
455

Description

## What

`IntraBlockState.FlushWritesToVersionMap` publishes a transaction's raw
`versionedWrites` straight into the version map. The block-building and BAL
re-derivation paths use it:

| site | caller |
|---|---|
| `execution/exec/block_assembler.go:161` | block init |
| `execution/exec/block_assembler.go:220` | per-tx assembly |
| `execution/exec/block_assembler.go:416` | block finalize |
| `execution/bal/rederive.go:71`, `:97` | BAL re-derivation |

The parallel executor publishes through `FinalizedWrites` instead, which applies
the EIP-161 empty-account clearing (withhold a created-empty record, emit a
deletion for a pre-existing account that ends empty). `FlushWritesToVersionMap`
applies none of it, so on the assembly path an account a transaction leaves empty
stays in the version map as an account that exists — and the version map is what
the next transaction in the same block reads for existence.

`FlushWritesToVersionMap`'s own docstring already anticipates this:

> Safe under the strict-`>` gate in versionedReadCore and the committed
> fallback; a consumer leaning on the AccountLifecycle `>=` arm must account for
> it (or this should be unified onto `FinalizedWrites()`).

## Scope of the effect

Intra-block *visibility*, not the final root: `execution/builder/exec.go` runs the
assembled write-sets through `WriteSet.Normalize`, whose own empty-removal pass
converts an all-zero account record into a delete before the trie is touched. So
the account is still cleared from the state — the gap is that a later
transaction in the same block can observe it as existing while executing, which
is precisely the divergence fixed for the executor path.

Note the two paths also disagree on *when* they fire: `Normalize` requires
balance, nonce and code hash to all be present in the write-set, whereas the
executor-side pass resolves whichever fields the transaction did not write.

## Current exposure

Not the default path today — versioned assembly runs when the assembler has a
BAL (`ExperimentalBAL`, off by default, or Amsterdam rules), so a stock mainnet
builder does not hit it. EIP-7928 is scheduled for Glamsterdam, which is what
makes it worth closing before then rather than after.

## Suggested direction

Either unify onto `FinalizedWrites` as the docstring suggests, or apply the same
clearing before flushing, so one implementation covers every publisher. Adjacent
to #21138 and #22458, both of which move in the same direction; this is the
narrower, self-contained piece.

Came out of review of #22768, which added the clearing to `FinalizedWrites`.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.