Skip to content

Commit c1c65b9

Browse files
committed
trie/bintrie: fix DeleteAccount no-op
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
1 parent acdd139 commit c1c65b9

File tree

2 files changed

+266
-6
lines changed

2 files changed

+266
-6
lines changed

trie/bintrie/trie.go

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -216,10 +216,15 @@ func (t *BinaryTrie) GetAccount(addr common.Address) (*types.StateAccount, error
216216
return nil, nil
217217
}
218218

219-
// If the account has been deleted, then values[10] will be 0 and not nil. If it has
220-
// been recreated after that, then its code keccak will NOT be 0. So return `nil` if
221-
// the nonce, and values[10], and code keccak is 0.
222-
if bytes.Equal(values[BasicDataLeafKey], zero[:]) && len(values) > 10 && len(values[10]) > 0 && bytes.Equal(values[CodeHashLeafKey], zero[:]) {
219+
// If the account has been deleted, BasicData and CodeHash will both be
220+
// 32-byte zero blobs (not nil) and the accountDeletedMarkerKey sentinel
221+
// will be a non-empty 32-byte zero blob written by DeleteAccount. If the
222+
// account has been recreated since, UpdateAccount will have overwritten
223+
// BasicData and CodeHash with non-zero values, so this branch won't hit.
224+
if bytes.Equal(values[BasicDataLeafKey], zero[:]) &&
225+
len(values) > accountDeletedMarkerKey &&
226+
len(values[accountDeletedMarkerKey]) > 0 &&
227+
bytes.Equal(values[CodeHashLeafKey], zero[:]) {
223228
return nil, nil
224229
}
225230

@@ -294,11 +299,47 @@ func (t *BinaryTrie) UpdateStorage(address common.Address, key, value []byte) er
294299
return nil
295300
}
296301

297-
// DeleteAccount is a no-op as it is disabled in stateless.
302+
// DeleteAccount removes the account metadata (basic data and code hash) for
303+
// the given address from the trie.
304+
//
305+
// Binary trie leaves cannot be "physically" removed — there is no delete
306+
// primitive on StemNode. Instead, the bintrie uses a tombstone convention:
307+
// BasicData (offset 0) and CodeHash (offset 1) are overwritten with 32 zero
308+
// bytes, and a non-nil 32-byte sentinel is written at the deletion marker
309+
// offset that GetAccount consults. This matches the detection logic at
310+
// GetAccount around trie.go:219 which treats "BasicData and CodeHash are
311+
// zero AND the sentinel is present" as an absent account while still
312+
// distinguishing it from "never existed" (all-nil slots).
313+
//
314+
// Storage slots and code chunks are NOT touched by this function. If the
315+
// caller needs to wipe storage on self-destruct, it must walk the relevant
316+
// slots explicitly — bintrie's unified keyspace has no cheap way to
317+
// enumerate every slot of a given account.
298318
func (t *BinaryTrie) DeleteAccount(addr common.Address) error {
299-
return nil
319+
var (
320+
err error
321+
zeroBlob [HashSize]byte
322+
values = make([][]byte, StemNodeWidth)
323+
stem = GetBinaryTreeKey(addr, zero[:])
324+
)
325+
// Clear BasicData (nonce, balance, code size) and CodeHash.
326+
values[BasicDataLeafKey] = zeroBlob[:]
327+
values[CodeHashLeafKey] = zeroBlob[:]
328+
// Write a non-nil 32-byte sentinel at the deletion marker offset so
329+
// GetAccount can tell "deleted" apart from "never existed" on a
330+
// subsequent read. See GetAccount's deletion branch around trie.go:219.
331+
values[accountDeletedMarkerKey] = zeroBlob[:]
332+
333+
t.root, err = t.root.InsertValuesAtStem(stem, values, t.nodeResolver, 0)
334+
return err
300335
}
301336

337+
// accountDeletedMarkerKey is the stem offset used as a "this account was
338+
// deleted" sentinel. It lives in the EIP-7864 reserved range (offsets 2-63)
339+
// and is consulted only by GetAccount's deletion-detection branch. Keep this
340+
// in sync with the check in GetAccount (trie.go:219).
341+
const accountDeletedMarkerKey = 10
342+
302343
// DeleteStorage removes any existing value for key from the trie. If a node was not
303344
// found in the database, a trie.MissingNodeError is returned.
304345
func (t *BinaryTrie) DeleteStorage(addr common.Address, key []byte) error {

trie/bintrie/trie_test.go

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,225 @@ func TestStorageRoundTrip(t *testing.T) {
267267
}
268268
}
269269

270+
// newTestTrie creates a fresh BinaryTrie with an empty root and a default
271+
// prevalue tracer. Used by tests that need a realistic trie instance.
272+
func newTestTrie(t *testing.T) *BinaryTrie {
273+
t.Helper()
274+
return &BinaryTrie{
275+
root: NewBinaryNode(),
276+
tracer: trie.NewPrevalueTracer(),
277+
}
278+
}
279+
280+
// makeAccount constructs a StateAccount with the given fields. The Root is
281+
// zeroed out because the bintrie has no per-account storage root.
282+
func makeAccount(nonce uint64, balance uint64, codeHash common.Hash) *types.StateAccount {
283+
return &types.StateAccount{
284+
Nonce: nonce,
285+
Balance: uint256.NewInt(balance),
286+
CodeHash: codeHash.Bytes(),
287+
}
288+
}
289+
290+
// TestDeleteAccountRoundTrip verifies the basic delete path: create an
291+
// account, read it back, delete it, confirm subsequent reads return nil.
292+
// Regression test for the no-op DeleteAccount bug where the deletion was
293+
// silently ignored and the old values remained in the trie.
294+
func TestDeleteAccountRoundTrip(t *testing.T) {
295+
tr := newTestTrie(t)
296+
addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678")
297+
codeHash := common.HexToHash("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470")
298+
299+
// Create: write account, verify round-trip.
300+
acc := makeAccount(42, 1000, codeHash)
301+
if err := tr.UpdateAccount(addr, acc, 0); err != nil {
302+
t.Fatalf("UpdateAccount: %v", err)
303+
}
304+
got, err := tr.GetAccount(addr)
305+
if err != nil {
306+
t.Fatalf("GetAccount: %v", err)
307+
}
308+
if got == nil {
309+
t.Fatal("GetAccount returned nil after UpdateAccount")
310+
}
311+
if got.Nonce != 42 {
312+
t.Fatalf("Nonce: got %d, want 42", got.Nonce)
313+
}
314+
if got.Balance.Uint64() != 1000 {
315+
t.Fatalf("Balance: got %s, want 1000", got.Balance)
316+
}
317+
if !bytes.Equal(got.CodeHash, codeHash[:]) {
318+
t.Fatalf("CodeHash: got %x, want %x", got.CodeHash, codeHash)
319+
}
320+
321+
// Delete: verify GetAccount returns nil afterwards.
322+
if err := tr.DeleteAccount(addr); err != nil {
323+
t.Fatalf("DeleteAccount: %v", err)
324+
}
325+
got, err = tr.GetAccount(addr)
326+
if err != nil {
327+
t.Fatalf("GetAccount after delete: %v", err)
328+
}
329+
if got != nil {
330+
t.Fatalf("GetAccount after delete: got %+v, want nil", got)
331+
}
332+
}
333+
334+
// TestDeleteAccountOnMissingAccount verifies that deleting an account that
335+
// was never created does not error and subsequent reads still return nil.
336+
func TestDeleteAccountOnMissingAccount(t *testing.T) {
337+
tr := newTestTrie(t)
338+
addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678")
339+
340+
// Delete without any prior create. Should not panic or error on an
341+
// empty root, and GetAccount should still return nil.
342+
if err := tr.DeleteAccount(addr); err != nil {
343+
t.Fatalf("DeleteAccount on empty trie: %v", err)
344+
}
345+
got, err := tr.GetAccount(addr)
346+
if err != nil {
347+
t.Fatalf("GetAccount after delete on empty trie: %v", err)
348+
}
349+
if got != nil {
350+
t.Fatalf("GetAccount on deleted missing account: got %+v, want nil", got)
351+
}
352+
}
353+
354+
// TestDeleteAccountPreservesOtherAccounts verifies that deleting one account
355+
// does not affect accounts at different stems.
356+
func TestDeleteAccountPreservesOtherAccounts(t *testing.T) {
357+
tr := newTestTrie(t)
358+
addrA := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678")
359+
addrB := common.HexToAddress("0xabcdef1234567890abcdef1234567890abcdef12")
360+
codeHashA := common.HexToHash("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470")
361+
codeHashB := common.HexToHash("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff0102030405060708090a0b0c0d0e0f10")
362+
363+
// Create two distinct accounts.
364+
if err := tr.UpdateAccount(addrA, makeAccount(1, 100, codeHashA), 0); err != nil {
365+
t.Fatalf("UpdateAccount(A): %v", err)
366+
}
367+
if err := tr.UpdateAccount(addrB, makeAccount(2, 200, codeHashB), 0); err != nil {
368+
t.Fatalf("UpdateAccount(B): %v", err)
369+
}
370+
371+
// Delete A.
372+
if err := tr.DeleteAccount(addrA); err != nil {
373+
t.Fatalf("DeleteAccount(A): %v", err)
374+
}
375+
376+
// A should be gone.
377+
if got, err := tr.GetAccount(addrA); err != nil {
378+
t.Fatalf("GetAccount(A): %v", err)
379+
} else if got != nil {
380+
t.Fatalf("GetAccount(A) after delete: got %+v, want nil", got)
381+
}
382+
383+
// B should still be readable with its original values.
384+
got, err := tr.GetAccount(addrB)
385+
if err != nil {
386+
t.Fatalf("GetAccount(B): %v", err)
387+
}
388+
if got == nil {
389+
t.Fatal("GetAccount(B) returned nil after unrelated delete")
390+
}
391+
if got.Nonce != 2 {
392+
t.Fatalf("Account B Nonce: got %d, want 2", got.Nonce)
393+
}
394+
if got.Balance.Uint64() != 200 {
395+
t.Fatalf("Account B Balance: got %s, want 200", got.Balance)
396+
}
397+
if !bytes.Equal(got.CodeHash, codeHashB[:]) {
398+
t.Fatalf("Account B CodeHash: got %x, want %x", got.CodeHash, codeHashB)
399+
}
400+
}
401+
402+
// TestDeleteAccountThenRecreate verifies that an account can be deleted and
403+
// then recreated with different values; the second read must return the new
404+
// values, not the stale ones from before deletion.
405+
func TestDeleteAccountThenRecreate(t *testing.T) {
406+
tr := newTestTrie(t)
407+
addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678")
408+
codeHash1 := common.HexToHash("1111111111111111111111111111111111111111111111111111111111111111")
409+
codeHash2 := common.HexToHash("2222222222222222222222222222222222222222222222222222222222222222")
410+
411+
// Create.
412+
if err := tr.UpdateAccount(addr, makeAccount(1, 100, codeHash1), 0); err != nil {
413+
t.Fatalf("UpdateAccount #1: %v", err)
414+
}
415+
// Delete.
416+
if err := tr.DeleteAccount(addr); err != nil {
417+
t.Fatalf("DeleteAccount: %v", err)
418+
}
419+
// Recreate with new values.
420+
if err := tr.UpdateAccount(addr, makeAccount(7, 9999, codeHash2), 0); err != nil {
421+
t.Fatalf("UpdateAccount #2: %v", err)
422+
}
423+
// Read: must observe the new values, not the originals.
424+
got, err := tr.GetAccount(addr)
425+
if err != nil {
426+
t.Fatalf("GetAccount: %v", err)
427+
}
428+
if got == nil {
429+
t.Fatal("GetAccount returned nil after recreate")
430+
}
431+
if got.Nonce != 7 {
432+
t.Fatalf("Nonce: got %d, want 7", got.Nonce)
433+
}
434+
if got.Balance.Uint64() != 9999 {
435+
t.Fatalf("Balance: got %s, want 9999", got.Balance)
436+
}
437+
if !bytes.Equal(got.CodeHash, codeHash2[:]) {
438+
t.Fatalf("CodeHash: got %x, want %x", got.CodeHash, codeHash2)
439+
}
440+
}
441+
442+
// TestDeleteAccountDoesNotAffectMainStorage verifies that DeleteAccount only
443+
// clears the account's BasicData and CodeHash, leaving main storage slots
444+
// (which live at different stems) untouched. Wiping storage on self-destruct
445+
// is a separate concern handled at the StateDB level.
446+
func TestDeleteAccountDoesNotAffectMainStorage(t *testing.T) {
447+
tr := newTestTrie(t)
448+
addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678")
449+
codeHash := common.HexToHash("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470")
450+
451+
// Create account.
452+
if err := tr.UpdateAccount(addr, makeAccount(1, 100, codeHash), 0); err != nil {
453+
t.Fatalf("UpdateAccount: %v", err)
454+
}
455+
// Write a main storage slot — i.e. key[31] >= 64 or key[:31] != 0 — so
456+
// it lives at a different stem from the account header.
457+
slot := common.HexToHash("0000000000000000000000000000000000000000000000000000000000000080")
458+
value := common.TrimLeftZeroes(common.HexToHash("00000000000000000000000000000000000000000000000000000000deadbeef").Bytes())
459+
if err := tr.UpdateStorage(addr, slot[:], value); err != nil {
460+
t.Fatalf("UpdateStorage: %v", err)
461+
}
462+
463+
// Delete the account.
464+
if err := tr.DeleteAccount(addr); err != nil {
465+
t.Fatalf("DeleteAccount: %v", err)
466+
}
467+
468+
// Account should be absent.
469+
if got, _ := tr.GetAccount(addr); got != nil {
470+
t.Fatalf("GetAccount after delete: got %+v, want nil", got)
471+
}
472+
473+
// Main storage slot should still be readable — DeleteAccount must not
474+
// have touched it.
475+
got, err := tr.GetStorage(addr, slot[:])
476+
if err != nil {
477+
t.Fatalf("GetStorage after DeleteAccount: %v", err)
478+
}
479+
if len(got) == 0 {
480+
t.Fatal("main storage slot was wiped by DeleteAccount, expected it to survive")
481+
}
482+
var expected [HashSize]byte
483+
copy(expected[HashSize-len(value):], value)
484+
if !bytes.Equal(got, expected[:]) {
485+
t.Fatalf("main storage slot: got %x, want %x", got, expected)
486+
}
487+
}
488+
270489
func TestBinaryTrieWitness(t *testing.T) {
271490
tracer := trie.NewPrevalueTracer()
272491

0 commit comments

Comments
 (0)