erigontech / erigontech/erigon

execution/state: the reader-side and EVM-side destruct scans use different lower bounds

Open
#23,559 0 comments 0 reactions 0 assignees View on GitHub
tech debt reduction
Dominant language
Go
Stars
3.6k
Forks
1.5k
Avg merge
1d 16h
Merged PRs (30d)
455

Description

`versionedStateReader`'s wipe scan and `versionedReadCore`'s no-cell scan answer the same question — did an in-block SELFDESTRUCT erase this field — from two different lower bounds.

- Reader side: `destructScanFloor` returns `scanEverything` (-1) when the value has no in-block cell, so a destruct the block-begin system tx recorded at TxIndex -1 counts (execution/state/versionmap.go:674-693).
- EVM side: `versionedReadCore`'s no-cell branch calls `FindDoneSelfDestructInRange(addr, 0, s.txIndex, true)` (execution/state/read_paths.go:740), so the same destruct does not.

Both also carry their own copy of the per-path bump — reader side `CodeHashPath` only, EVM side `CodeHashPath` and `BalancePath` — and their own "no cell" test.

## Why it is inert today

The only writer of a `SelfDestruct` cell at -1 is `Normalize`'s EIP-161 pass (writeset_normalize.go:479), and it fires only for an address whose write set carries Balance, Nonce and CodeHash and ends `balance == 0 && nonce == 0 && codeHash.IsEmpty()` (writeset_normalize.go:470-472). Every field the reader-side scan wipes off that destruct already holds the value the wipe writes.

`DeleteAccountFields` (versionedio.go:1062) drops balance/nonce/incarnation/codeHash and leaves Code and Storage, so those are the two paths where the bounds could differ on a value. Code has to be empty for the pass to fire at all. Storage would need the block-begin system tx to write a slot for an address that ends that tx EIP-161-empty, and the system contracts it calls carry code and nonce >= 1.

## What to do

One function for both, with `scanEverything` as the no-cell floor, the per-path bump, and `UnknownDep` as the only "no cell". Moving the EVM bound changes what the validator sees, so it needs its own test: a block-begin destruct followed by an in-block revival, read back through both paths and compared.

## Also from the same review

Test hygiene in the same files, all in #23072: the genesis preamble in `execution/tests/statedb_chain_test.go` is worth sharing, the `calcFees` fixture in `exec3_finalize_test.go` is copied three times, and `preBlockStateReader` (revival_reader_test.go:33) duplicates `fallthroughStateReader` (versionedio_test.go:545) in the same package.

Found by Copilot on #23072, https://github.com/erigontech/erigon/pull/23072#discussion_r3842602175.

Contributor guide

Open the contributing guide

Research direction

Start with destructScanFloor in execution/state/versionmap.go and the no-cell branch of versionedReadCore in execution/state/read_paths.go, then inspect the related normalization and account-field code in writeset_normalize.go and versionedio.go. Add the described block-begin destruct and in-block revival coverage, and verify both read paths return the same result.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
blockchain
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.