Skip to content

trie/bintrie: fix DeleteAccount no-op#34676

Open
CPerezz wants to merge 1 commit intoethereum:masterfrom
CPerezz:fix/bintrie-delete-account
Open

trie/bintrie: fix DeleteAccount no-op#34676
CPerezz wants to merge 1 commit intoethereum:masterfrom
CPerezz:fix/bintrie-delete-account

Conversation

@CPerezz
Copy link
Copy Markdown
Contributor

@CPerezz CPerezz commented Apr 7, 2026

Summary

BinaryTrie.DeleteAccount was a no-op, silently ignoring the caller's deletion request and leaving the old BasicData and CodeHash in the trie. The GetAccount deletion-detection branch (around trie.go:219) already expected a tombstone convention — "BasicData and CodeHash are 32-byte zero blobs AND a non-nil 32-byte sentinel is present at a reserved offset" — but nothing was writing that sentinel, so the check was effectively dead code.

This PR implements the deletion as an InsertValuesAtStem that:

  • writes a 32-byte zero blob to BasicData (offset 0)
  • writes a 32-byte zero blob to CodeHash (offset 1)
  • writes a 32-byte zero blob to a deletion sentinel offset in the EIP-7864 reserved range (offset 10, promoted to the named constant accountDeletedMarkerKey for cross-referencing with GetAccount)

This matches the bintrie's existing "write zeros to delete" convention already used by DeleteStorage, keeps GetAccount's deletion branch consistent, and still distinguishes deleted from never existed (the latter has all-nil slots so the empty-account check fires first).

This becomes critical once any flat-state layer is enabled for the bintrie (in progress in [state-hasher-iface-2](https://github.com/rjl493456442/go-ethereum/tree/state-hasher-iface-2 as a first abstraction) and the follow-up flat-state PR stack): the flat-state layer records deletions correctly, but the trie keeps the stale values, leading to state-root divergence.

BinaryTrie.DeleteAccount was a no-op, silently ignoring the caller's
deletion request and leaving the old BasicData and CodeHash in the trie.
The GetAccount deletion-detection branch (trie.go:219) already expected
a tombstone convention — "BasicData and CodeHash are 32-byte zero blobs
AND a non-nil 32-byte sentinel is present at a reserved offset" — but
nothing was writing that sentinel, so the check was effectively dead
code.

Implement the deletion as an InsertValuesAtStem that:

  - writes a 32-byte zero blob to BasicData (offset 0)
  - writes a 32-byte zero blob to CodeHash (offset 1)
  - writes a 32-byte zero blob to a deletion sentinel offset in the
    EIP-7864 reserved range (offset 10, promoted to the named constant
    accountDeletedMarkerKey for cross-referencing with GetAccount)

This matches the bintrie's existing "write zeros to delete" convention
seen in DeleteStorage, keeps GetAccount's deletion branch consistent,
and still distinguishes "deleted" from "never existed" (the latter has
all-nil slots so the empty-account check fires first).

Storage slots and code chunks are intentionally left untouched. Wiping
storage on self-destruct is a separate concern handled at the StateDB
level — the bintrie's unified keyspace has no cheap way to enumerate
every slot of a given account, so a blanket wipe is not possible here.

Regression tests cover:

  - round-trip: UpdateAccount -> GetAccount -> DeleteAccount -> GetAccount nil
  - delete on missing account: no panic, subsequent read still nil
  - unrelated accounts at different stems are preserved
  - delete + recreate: second read sees the new values, not the old ones
  - main storage slots at different stems survive DeleteAccount
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant