go-ethereum/eth/protocols/snap/bal_apply_test.go
Jonny Rhea 17aab1ac9a
Some checks are pending
/ Linux Build (push) Waiting to run
/ Linux Build (arm) (push) Waiting to run
/ Keeper Build (push) Waiting to run
/ Windows Build (push) Waiting to run
/ Docker Image (push) Waiting to run
core, eth/protocols/snap, eth/downloader: snap/2 sync logic (#34626)
Adds snap/2 (EIP-8189), a block-access-list (BAL) based state sync, and
wires it to run side by side with snap/1. It's opt-in (for now) behind a
new --snap.v2 flag and chosen at startup.

https://eips.ethereum.org/EIPS/eip-8189

---------

Co-authored-by: Toni Wahrstätter <info@toniwahrstaetter.com>
Co-authored-by: Gary Rong <garyrong0905@gmail.com>
2026-06-11 14:45:07 +08:00

591 lines
21 KiB
Go

// Copyright 2026 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package snap
import (
"bytes"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rlp"
"github.com/holiman/uint256"
)
// buildTestBAL constructs a BlockAccessList from a ConstructionBlockAccessList
// by RLP round-tripping (construction types use unexported encoding types).
func buildTestBAL(t *testing.T, cb *bal.ConstructionBlockAccessList) *bal.BlockAccessList {
t.Helper()
var buf bytes.Buffer
if err := cb.EncodeRLP(&buf); err != nil {
t.Fatalf("failed to encode BAL: %v", err)
}
var b bal.BlockAccessList
if err := rlp.DecodeBytes(buf.Bytes(), &b); err != nil {
t.Fatalf("failed to decode BAL: %v", err)
}
return &b
}
// applyBAL applies b to the syncer's flat state and commits it, mirroring the
// per-block batch flow used during catch-up: applyAccessList writes into a batch
// that the caller commits.
func applyBAL(t *testing.T, s *syncerV2, b *bal.BlockAccessList) {
t.Helper()
batch := s.db.NewBatch()
if err := s.applyAccessList(b, batch); err != nil {
t.Fatalf("applyAccessList failed: %v", err)
}
if err := batch.Write(); err != nil {
t.Fatalf("failed to commit BAL batch: %v", err)
}
}
// TestAccessListVerification checks that verifyAccessList accepts valid BALs
// and rejects tampered ones.
func TestAccessListVerification(t *testing.T) {
t.Parallel()
cb := bal.NewConstructionBlockAccessList()
addr := common.HexToAddress("0x01")
cb.BalanceChange(0, addr, uint256.NewInt(100))
b := buildTestBAL(t, cb)
correctHash := b.Hash()
// Valid: hash matches header
header := &types.Header{
Number: big.NewInt(1),
BlockAccessListHash: &correctHash,
}
if err := verifyAccessList(b, header); err != nil {
t.Fatalf("valid access list rejected: %v", err)
}
// Invalid: wrong hash in header
wrongHash := common.HexToHash("0xdead")
badHeader := &types.Header{
Number: big.NewInt(1),
BlockAccessListHash: &wrongHash,
}
if err := verifyAccessList(b, badHeader); err == nil {
t.Fatal("tampered access list accepted")
}
// Invalid: no hash in header
noHashHeader := &types.Header{
Number: big.NewInt(1),
}
if err := verifyAccessList(b, noHashHeader); err == nil {
t.Fatal("header without access list hash accepted")
}
}
// TestAccessListApplication verifies that applyAccessList correctly updates
// flat state (balance, nonce, code, storage) and leaves storageRoot stale.
func TestAccessListApplication(t *testing.T) {
t.Parallel()
db := rawdb.NewMemoryDatabase()
syncer := newSyncerV2(db, rawdb.HashScheme)
addr := common.HexToAddress("0x01")
accountHash := crypto.Keccak256Hash(addr[:])
// Write an existing account to flat state
original := types.StateAccount{
Nonce: 5,
Balance: uint256.NewInt(1000),
Root: common.HexToHash("0xbeef"), // intentionally non-empty
CodeHash: types.EmptyCodeHash[:],
}
rawdb.WriteAccountSnapshot(db, accountHash, types.SlimAccountRLP(original))
// Write an existing storage slot. The BAL uses raw slot keys, but the
// snapshot layer stores slots under keccak256(slot).
rawSlot := common.HexToHash("0xaa")
slotHash := crypto.Keccak256Hash(rawSlot[:])
rawdb.WriteStorageSnapshot(db, accountHash, slotHash, common.HexToHash("0x01").Bytes())
// Build a BAL that changes balance, nonce, code, and storage
cb := bal.NewConstructionBlockAccessList()
cb.BalanceChange(0, addr, uint256.NewInt(2000))
cb.NonceChange(addr, 0, 6)
cb.CodeChange(addr, 0, []byte{0x60, 0x00}) // PUSH1 0x00
cb.StorageWrite(0, addr, rawSlot, common.HexToHash("0x02"))
b := buildTestBAL(t, cb)
applyBAL(t, syncer, b)
// Verify account fields updated
data := rawdb.ReadAccountSnapshot(db, accountHash)
if len(data) == 0 {
t.Fatal("account snapshot missing after apply")
}
updated, err := types.FullAccount(data)
if err != nil {
t.Fatalf("failed to decode updated account: %v", err)
}
if updated.Balance.Cmp(uint256.NewInt(2000)) != 0 {
t.Errorf("balance wrong: got %v, want 2000", updated.Balance)
}
if updated.Nonce != 6 {
t.Errorf("nonce wrong: got %d, want 6", updated.Nonce)
}
wantCodeHash := crypto.Keccak256([]byte{0x60, 0x00})
if !bytes.Equal(updated.CodeHash, wantCodeHash) {
t.Errorf("code hash wrong: got %x, want %x", updated.CodeHash, wantCodeHash)
}
// Verify code was written
if code := rawdb.ReadCode(db, common.BytesToHash(wantCodeHash)); !bytes.Equal(code, []byte{0x60, 0x00}) {
t.Errorf("code wrong: got %x, want 6000", code)
}
// Verify storage updated. Slots are stored in the canonical snapshot
// encoding (RLP of the value with leading zeros trimmed), the same form
// the download path writes and the trie rebuild consumes.
storageVal := rawdb.ReadStorageSnapshot(db, accountHash, slotHash)
wantStorage, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(common.HexToHash("0x02").Bytes()))
if !bytes.Equal(storageVal, wantStorage) {
t.Errorf("storage wrong: got %x, want %x", storageVal, wantStorage)
}
// Verify storageRoot left stale (unchanged from original)
if updated.Root != original.Root {
t.Errorf("storageRoot should be stale: got %v, want %v", updated.Root, original.Root)
}
}
// TestAccessListApplicationMultiTx verifies that when an account has multiple
// changes at different transaction indices, only the highest index (post-block
// state) is applied.
func TestAccessListApplicationMultiTx(t *testing.T) {
t.Parallel()
db := rawdb.NewMemoryDatabase()
syncer := newSyncerV2(db, rawdb.HashScheme)
addr := common.HexToAddress("0x02")
accountHash := crypto.Keccak256Hash(addr[:])
// Write initial account
original := types.StateAccount{
Nonce: 0,
Balance: uint256.NewInt(100),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash[:],
}
rawdb.WriteAccountSnapshot(db, accountHash, types.SlimAccountRLP(original))
// Build BAL with multiple balance/nonce changes at different tx indices
cb := bal.NewConstructionBlockAccessList()
cb.BalanceChange(0, addr, uint256.NewInt(200)) // tx 0
cb.BalanceChange(3, addr, uint256.NewInt(500)) // tx 3
cb.BalanceChange(7, addr, uint256.NewInt(9999)) // tx 7 (final)
cb.NonceChange(addr, 0, 1) // tx 0
cb.NonceChange(addr, 3, 2) // tx 3
cb.NonceChange(addr, 7, 3) // tx 7 (final)
b := buildTestBAL(t, cb)
applyBAL(t, syncer, b)
data := rawdb.ReadAccountSnapshot(db, accountHash)
updated, err := types.FullAccount(data)
if err != nil {
t.Fatalf("failed to decode updated account: %v", err)
}
// Only the highest tx index values should be applied
if updated.Balance.Cmp(uint256.NewInt(9999)) != 0 {
t.Errorf("balance wrong: got %v, want 9999", updated.Balance)
}
if updated.Nonce != 3 {
t.Errorf("nonce wrong: got %d, want 3", updated.Nonce)
}
}
// TestAccessListApplicationZeroStorage verifies that a BAL slot write with a
// zero post-value deletes the snapshot entry instead of writing 32 zero
// bytes.
func TestAccessListApplicationZeroStorage(t *testing.T) {
t.Parallel()
db := rawdb.NewMemoryDatabase()
syncer := newSyncerV2(db, rawdb.HashScheme)
addr := common.HexToAddress("0x06")
accountHash := crypto.Keccak256Hash(addr[:])
// Existing account with a non-zero storage slot.
original := types.StateAccount{
Nonce: 1,
Balance: uint256.NewInt(1),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash[:],
}
rawdb.WriteAccountSnapshot(db, accountHash, types.SlimAccountRLP(original))
rawSlot := common.HexToHash("0xaa")
slotHash := crypto.Keccak256Hash(rawSlot[:])
rawdb.WriteStorageSnapshot(db, accountHash, slotHash, common.HexToHash("0x42").Bytes())
// BAL writes the slot to zero (deletion).
cb := bal.NewConstructionBlockAccessList()
cb.StorageWrite(0, addr, rawSlot, common.Hash{})
b := buildTestBAL(t, cb)
applyBAL(t, syncer, b)
if val := rawdb.ReadStorageSnapshot(db, accountHash, slotHash); len(val) != 0 {
t.Errorf("zeroed slot should have been deleted, got %x", val)
}
}
// TestAccessListApplicationNewAccount verifies that applyAccessList creates
// new accounts that don't exist in the DB yet.
func TestAccessListApplicationNewAccount(t *testing.T) {
t.Parallel()
db := rawdb.NewMemoryDatabase()
syncer := newSyncerV2(db, rawdb.HashScheme)
addr := common.HexToAddress("0x03")
accountHash := crypto.Keccak256Hash(addr[:])
// Verify account doesn't exist
if data := rawdb.ReadAccountSnapshot(db, accountHash); len(data) > 0 {
t.Fatal("account should not exist yet")
}
// Build BAL for a new account. BAL uses raw slot keys.
cb := bal.NewConstructionBlockAccessList()
cb.BalanceChange(0, addr, uint256.NewInt(42))
cb.NonceChange(addr, 0, 1)
rawSlot := common.HexToHash("0xbb")
cb.StorageWrite(0, addr, rawSlot, common.HexToHash("0xff"))
b := buildTestBAL(t, cb)
applyBAL(t, syncer, b)
// Verify account was created
data := rawdb.ReadAccountSnapshot(db, accountHash)
if len(data) == 0 {
t.Fatal("account should exist after apply")
}
account, err := types.FullAccount(data)
if err != nil {
t.Fatalf("failed to decode new account: %v", err)
}
if account.Balance.Cmp(uint256.NewInt(42)) != 0 {
t.Errorf("balance wrong: got %v, want 42", account.Balance)
}
if account.Nonce != 1 {
t.Errorf("nonce wrong: got %d, want 1", account.Nonce)
}
if account.Root != types.EmptyRootHash {
t.Errorf("root should be empty for new account: got %v", account.Root)
}
// Verify storage was written under keccak256(rawSlot) in the canonical
// snapshot encoding (RLP of the value with leading zeros trimmed).
slotHash := crypto.Keccak256Hash(rawSlot[:])
storageVal := rawdb.ReadStorageSnapshot(db, accountHash, slotHash)
wantStorage, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(common.HexToHash("0xff").Bytes()))
if !bytes.Equal(storageVal, wantStorage) {
t.Errorf("storage wrong: got %x, want %x", storageVal, wantStorage)
}
}
// TestAccessListApplicationSkipsUnfetched verifies that applyAccessList does
// not write account entries for addresses whose hash falls in a range that
// hasn't been downloaded yet.
func TestAccessListApplicationSkipsUnfetched(t *testing.T) {
t.Parallel()
db := rawdb.NewMemoryDatabase()
syncer := newSyncerV2(db, rawdb.HashScheme)
// Pick two addresses and order them by hash.
addrA := common.HexToAddress("0x01")
addrB := common.HexToAddress("0x02")
hashA := crypto.Keccak256Hash(addrA[:])
hashB := crypto.Keccak256Hash(addrB[:])
fetchedAddr, fetchedHash := addrA, hashA
unfetchedAddr, unfetchedHash := addrB, hashB
if bytes.Compare(hashA[:], hashB[:]) > 0 {
fetchedAddr, fetchedHash = addrB, hashB
unfetchedAddr, unfetchedHash = addrA, hashA
}
// One remaining task covering [unfetchedHash, MaxHash]: the fetched hash
// is below Next so isFetched returns true; the unfetched hash equals Next
// so isFetched returns false.
syncer.tasks = []*accountTaskV2{{
Next: unfetchedHash,
Last: common.MaxHash,
SubTasks: make(map[common.Hash][]*storageTaskV2),
stateCompleted: make(map[common.Hash]struct{}),
}}
cb := bal.NewConstructionBlockAccessList()
cb.BalanceChange(0, fetchedAddr, uint256.NewInt(100))
cb.BalanceChange(0, unfetchedAddr, uint256.NewInt(200))
b := buildTestBAL(t, cb)
applyBAL(t, syncer, b)
// The fetched account should have been written.
if data := rawdb.ReadAccountSnapshot(db, fetchedHash); len(data) == 0 {
t.Error("expected fetched account to be written")
}
// The unfetched account should not have been touched.
if data := rawdb.ReadAccountSnapshot(db, unfetchedHash); len(data) != 0 {
t.Errorf("unfetched account should not be written, got %x", data)
}
}
// TestAccessListApplicationSkipsUnfetchedStorage verifies that storage writes
// are also skipped when the parent account's hash range isn't downloaded yet.
func TestAccessListApplicationSkipsUnfetchedStorage(t *testing.T) {
t.Parallel()
db := rawdb.NewMemoryDatabase()
syncer := newSyncerV2(db, rawdb.HashScheme)
addrA := common.HexToAddress("0x01")
addrB := common.HexToAddress("0x02")
hashA := crypto.Keccak256Hash(addrA[:])
hashB := crypto.Keccak256Hash(addrB[:])
unfetchedAddr, unfetchedHash := addrB, hashB
if bytes.Compare(hashA[:], hashB[:]) > 0 {
unfetchedAddr, unfetchedHash = addrA, hashA
}
syncer.tasks = []*accountTaskV2{{
Next: unfetchedHash,
Last: common.MaxHash,
SubTasks: make(map[common.Hash][]*storageTaskV2),
stateCompleted: make(map[common.Hash]struct{}),
}}
// BAL touches an unfetched account with a storage write AND an empty
// balance mutation. Neither should result in any flat-state writes.
rawSlot := common.HexToHash("0xaa")
slotHash := crypto.Keccak256Hash(rawSlot[:])
cb := bal.NewConstructionBlockAccessList()
cb.BalanceChange(0, unfetchedAddr, uint256.NewInt(0)) // empty mutation
cb.StorageWrite(0, unfetchedAddr, rawSlot, common.HexToHash("0xff"))
b := buildTestBAL(t, cb)
applyBAL(t, syncer, b)
if data := rawdb.ReadAccountSnapshot(db, unfetchedHash); len(data) != 0 {
t.Errorf("unfetched account should not be written, got %x", data)
}
if val := rawdb.ReadStorageSnapshot(db, unfetchedHash, slotHash); len(val) != 0 {
t.Errorf("storage for unfetched account should not be written, got %x", val)
}
}
// TestAccessListApplicationPartialStorage verifies that for a large contract
// whose account hasn't been committed yet but whose storage is partially downloaded,
// applyAccessList rolls forward the slots below the active subtask frontier
// while skipping the ones above it and the account-level fields.
func TestAccessListApplicationPartialStorage(t *testing.T) {
t.Parallel()
db := rawdb.NewMemoryDatabase()
syncer := newSyncerV2(db, rawdb.HashScheme)
addr := common.HexToAddress("0xc0")
accountHash := crypto.Keccak256Hash(addr[:])
// Two slots, ordered by their storage hash. The subtask frontier sits at
// the higher one so the lower slot is fetched and the higher is not.
loRaw := common.HexToHash("0xaa")
hiRaw := common.HexToHash("0xbb")
loHash := crypto.Keccak256Hash(loRaw[:])
hiHash := crypto.Keccak256Hash(hiRaw[:])
if bytes.Compare(loHash[:], hiHash[:]) > 0 {
loRaw, hiRaw = hiRaw, loRaw
loHash, hiHash = hiHash, loHash
}
// The account sits exactly at Next (held back behind storage retrieval), so
// isFetched returns false. Its subtask has fetched everything below hiHash.
syncer.tasks = []*accountTaskV2{{
Next: accountHash,
Last: common.MaxHash,
SubTasks: map[common.Hash][]*storageTaskV2{
accountHash: {{
Next: hiHash,
Last: common.MaxHash,
}},
},
stateCompleted: make(map[common.Hash]struct{}),
}}
cb := bal.NewConstructionBlockAccessList()
cb.BalanceChange(0, addr, uint256.NewInt(500)) // account update must be skipped
cb.StorageWrite(0, addr, loRaw, common.HexToHash("0x11"))
cb.StorageWrite(0, addr, hiRaw, common.HexToHash("0x22"))
b := buildTestBAL(t, cb)
applyBAL(t, syncer, b)
// Account must not be written: it's still being filled.
if data := rawdb.ReadAccountSnapshot(db, accountHash); len(data) != 0 {
t.Errorf("account below Next should not be written, got %x", data)
}
// Slot below the frontier must be rolled forward.
wantLo, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(common.HexToHash("0x11").Bytes()))
if val := rawdb.ReadStorageSnapshot(db, accountHash, loHash); !bytes.Equal(val, wantLo) {
t.Errorf("fetched slot wrong: got %x, want %x", val, wantLo)
}
// Slot above the frontier must be skipped.
if val := rawdb.ReadStorageSnapshot(db, accountHash, hiHash); len(val) != 0 {
t.Errorf("unfetched slot should not be written, got %x", val)
}
}
func TestIsStorageFetched(t *testing.T) {
t.Parallel()
db := rawdb.NewMemoryDatabase()
syncer := newSyncerV2(db, rawdb.HashScheme)
var (
fetchedAcct = common.HexToHash("0x10") // below Next
fillingAcct = common.HexToHash("0x40") // == Next
beyondAcct = common.HexToHash("0x90") // above Last
)
prunedHiAcct := common.HexToHash("0x70") // in [Next, Last], still filling
completeAcct := common.HexToHash("0x71") // in [Next, Last], storage is complete
syncer.tasks = []*accountTaskV2{{
Next: fillingAcct,
Last: common.HexToHash("0x80"),
SubTasks: map[common.Hash][]*storageTaskV2{
fillingAcct: {{
Next: common.HexToHash("0x50"),
Last: common.MaxHash,
}},
prunedHiAcct: {{
Next: common.HexToHash("0x30"),
Last: common.HexToHash("0x60"),
}},
},
stateCompleted: map[common.Hash]struct{}{
completeAcct: {},
},
}}
noSubAcct := common.HexToHash("0x60") // in [Next,Last] but no subtasks yet
tests := []struct {
name string
account common.Hash
slot common.Hash
want bool
}{
{"account fully synced", fetchedAcct, common.HexToHash("0xff"), true},
{"storage fully synced", completeAcct, common.HexToHash("0xff"), true},
{"account before all tasks", common.HexToHash("0x01"), common.HexToHash("0xff"), true},
{"account beyond all tasks", beyondAcct, common.HexToHash("0xff"), true},
{"slot below storage frontier", fillingAcct, common.HexToHash("0x20"), true},
{"slot at storage frontier", fillingAcct, common.HexToHash("0x50"), false},
{"slot above storage frontier", fillingAcct, common.HexToHash("0x70"), false},
{"account filling, no subtasks", noSubAcct, common.HexToHash("0x01"), false},
{"slot in pruned low range", prunedHiAcct, common.HexToHash("0x10"), true},
{"slot at remaining frontier", prunedHiAcct, common.HexToHash("0x30"), false},
{"slot within remaining range", prunedHiAcct, common.HexToHash("0x50"), false},
{"slot in pruned high range", prunedHiAcct, common.HexToHash("0x90"), true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := syncer.isStorageFetched(tt.account, tt.slot); got != tt.want {
t.Errorf("isStorageFetched(%v, %v) = %v, want %v", tt.account, tt.slot, got, tt.want)
}
})
}
}
// TestAccessListApplicationSameTxCreateDestroy tests the edge case where an
// account is created and self-destructed in the same transaction during the
// pivot gap. Per EIP-7928, such accounts appear in the BAL with a balance
// change to zero but no nonce or code changes. Since the account didn't exist
// at the old pivot and doesn't exist at the new pivot (destroyed),
// applyAccessList should not leave a zero-balance account in the snapshot.
// Per EIP-161, empty accounts (zero balance, zero nonce, no code) must not exist
// in state.
func TestAccessListApplicationSameTxCreateDestroy(t *testing.T) {
t.Parallel()
db := rawdb.NewMemoryDatabase()
syncer := newSyncerV2(db, rawdb.HashScheme)
addr := common.HexToAddress("0x04")
accountHash := crypto.Keccak256Hash(addr[:])
// Verify account doesn't exist before apply
if data := rawdb.ReadAccountSnapshot(db, accountHash); len(data) > 0 {
t.Fatal("account should not exist yet")
}
// Build a BAL mimicking same-tx create+destroy: the account appears
// with a balance change to zero and nothing else.
cb := bal.NewConstructionBlockAccessList()
cb.BalanceChange(0, addr, uint256.NewInt(0))
b := buildTestBAL(t, cb)
applyBAL(t, syncer, b)
// Check if applyAccessList created an account.
data := rawdb.ReadAccountSnapshot(db, accountHash)
if len(data) > 0 {
// Account was created
account, err := types.FullAccount(data)
if err != nil {
t.Fatalf("failed to decode account: %v", err)
}
t.Errorf("account created for same-tx create+destroy: "+
"balance=%v, nonce=%d, codeHash=%x, root=%v",
account.Balance, account.Nonce, account.CodeHash, account.Root)
}
}
// TestAccessListApplicationDestroyExisting verifies that when a BAL reduces
// an existing flat-state account to nonce=0, balance=0, empty code (the
// pre-funded destruction pattern), applyAccessList deletes the entry rather
// than leaving it zereod.
func TestAccessListApplicationDestroyExisting(t *testing.T) {
t.Parallel()
db := rawdb.NewMemoryDatabase()
syncer := newSyncerV2(db, rawdb.HashScheme)
addr := common.HexToAddress("0x05")
accountHash := crypto.Keccak256Hash(addr[:])
// Pre-funded account: has balance, no nonce, no code.
original := types.StateAccount{
Nonce: 0,
Balance: uint256.NewInt(1000),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash[:],
}
rawdb.WriteAccountSnapshot(db, accountHash, types.SlimAccountRLP(original))
// The BAL zeros the balance. Nonce and code were already empty, so
// the account ends up fully empty after applying.
cb := bal.NewConstructionBlockAccessList()
cb.BalanceChange(0, addr, uint256.NewInt(0))
b := buildTestBAL(t, cb)
applyBAL(t, syncer, b)
if data := rawdb.ReadAccountSnapshot(db, accountHash); len(data) != 0 {
account, _ := types.FullAccount(data)
t.Errorf("destroyed account should have been deleted from flat state, "+
"got balance=%v, nonce=%d, codeHash=%x",
account.Balance, account.Nonce, account.CodeHash)
}
}