erigontech / erigontech/erigon
execution: block assembly publishes raw write-sets, so EIP-161 clearing does not apply on the builder path
- 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
Assessment
This issue has not been assessed yet.