Skip to content

Commit 135a5e1

Browse files
committed
triedb/pathdb: add regression test for zero-base-root lookup fallback
TestLookupZeroBaseRootFallback constructs a layer tree whose disk-layer root is common.Hash{} (mirroring the bintrie/verkle configuration where an empty trie hashes to EmptyVerkleHash), stacks diff layers on top, and exercises four cases: 1. lookupAccount on a never-written key must fall through to the disk layer and return (diskLayer, nil). Before the previous commit this returned errSnapshotStale because the disk-layer fallback hash collided with the stale sentinel. 2. Symmetric case for lookupStorage. 3. lookupAccount on a written account must still return the diff layer that holds it — pins the normal resolution path. 4. lookupAccount/lookupStorage for an unknown state root must still return errSnapshotStale. This pins the other half of the contract so a future refactor that always returned ok=true would be caught here rather than in production. Verified by reverse-applying the previous commit: the test fails with the exact pre-fix error ("layer stale") on cases 1 and 2, and passes once the fix is restored. The existing TestAccountLookup/TestStorageLookup tests use newTestLayerTree which hard-codes common.Hash{0x1} as the disk-layer root, so none of them could cover the zero-root case without a tailored helper; this test inlines newDiskLayer(common.Hash{}, …) directly rather than parameterize the shared helper.
1 parent 0d16a41 commit 135a5e1

File tree

1 file changed

+115
-0
lines changed

1 file changed

+115
-0
lines changed

triedb/pathdb/layertree_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -916,3 +916,118 @@ func TestStorageLookup(t *testing.T) {
916916
}
917917
}
918918
}
919+
920+
// TestLookupZeroBaseRootFallback is a regression test for a sentinel
921+
// collision in accountTip/storageTip: before the fix they returned
922+
// common.Hash{} as both the "stale" marker and the disk-layer fallback
923+
// when the disk root itself happened to be zero. lookupAccount/Storage
924+
// then misreported a legitimate fallback as errSnapshotStale.
925+
//
926+
// On the merkle path the collision was invisible because the empty
927+
// merkle trie hashes to types.EmptyRootHash (a concrete non-zero
928+
// keccak), so the disk layer's root was never the zero hash in
929+
// practice. The bug only surfaces once the disk layer root can
930+
// legitimately be zero (for example a fresh verkle/bintrie database
931+
// where the empty binary trie hashes to EmptyVerkleHash ==
932+
// common.Hash{}).
933+
//
934+
// The test constructs a layer tree whose base layer's root IS the zero
935+
// hash, stacks diff layers on top, and exercises four cases:
936+
//
937+
// 1. Look up an account NEVER written → should fall through to the
938+
// disk layer and return (diskLayer, nil). Before the fix this
939+
// returned errSnapshotStale because the fallback hash collided
940+
// with the sentinel.
941+
// 2. Symmetric case for lookupStorage.
942+
// 3. Look up an account written in a diff layer → should return that
943+
// diff layer (the normal happy path is unaffected by the fix).
944+
// 4. Look up any key at a state root that isn't part of the tree
945+
// (neither the disk root nor a descendant of it) → MUST still
946+
// return errSnapshotStale. This pins the "other half" of the
947+
// contract so a future refactor that always returns ok=true would
948+
// fail here.
949+
func TestLookupZeroBaseRootFallback(t *testing.T) {
950+
// Build a layer tree whose disk-layer root is common.Hash{} —
951+
// mirrors the bintrie/verkle configuration where the empty trie
952+
// hashes to EmptyVerkleHash. newTestLayerTree can't be reused
953+
// because it hard-codes common.Hash{0x1}.
954+
db := New(rawdb.NewMemoryDatabase(), nil, false)
955+
base := newDiskLayer(common.Hash{}, 0, db, nil, nil, newBuffer(0, nil, nil, 0), nil)
956+
tr := newLayerTree(base)
957+
958+
// Stack two diff layers on the zero-rooted disk layer, each
959+
// touching a known account and slot so we have something for the
960+
// happy-path lookups to find later.
961+
if err := tr.add(
962+
common.Hash{0x2}, common.Hash{},
963+
1,
964+
NewNodeSetWithOrigin(nil, nil),
965+
NewStateSetWithOrigin(
966+
randomAccountSet("0xa"),
967+
randomStorageSet([]string{"0xa"}, [][]string{{"0x1"}}, nil),
968+
nil, nil, false),
969+
); err != nil {
970+
t.Fatalf("add first diff layer: %v", err)
971+
}
972+
if err := tr.add(
973+
common.Hash{0x3}, common.Hash{0x2},
974+
2,
975+
NewNodeSetWithOrigin(nil, nil),
976+
NewStateSetWithOrigin(
977+
randomAccountSet("0xb"),
978+
nil, nil, nil, false),
979+
); err != nil {
980+
t.Fatalf("add second diff layer: %v", err)
981+
}
982+
983+
// Case 1: unknown account queried at the head. The lookup must
984+
// fall through the diff layers, hit the disk-layer fallback at
985+
// base=common.Hash{}, and return the disk layer with no error —
986+
// NOT errSnapshotStale.
987+
l, err := tr.lookupAccount(common.HexToHash("0xdead"), common.Hash{0x3})
988+
if err != nil {
989+
t.Fatalf("lookupAccount on zero-base disk layer: unexpected error %v", err)
990+
}
991+
if l.rootHash() != (common.Hash{}) {
992+
t.Errorf("expected fall-through to disk layer (root=0), got %x", l.rootHash())
993+
}
994+
995+
// Case 2: symmetric check for storage. Slot 0x99 was never written,
996+
// so the lookup must fall through to the disk layer just like
997+
// Case 1.
998+
l, err = tr.lookupStorage(
999+
common.HexToHash("0xdead"), common.HexToHash("0x99"), common.Hash{0x3})
1000+
if err != nil {
1001+
t.Fatalf("lookupStorage on zero-base disk layer: unexpected error %v", err)
1002+
}
1003+
if l.rootHash() != (common.Hash{}) {
1004+
t.Errorf("expected fall-through to disk layer (root=0), got %x", l.rootHash())
1005+
}
1006+
1007+
// Case 3: happy path. Account 0xa was written at diff layer 0x2.
1008+
// The lookup must return that layer, proving the fix didn't break
1009+
// the normal resolution path.
1010+
l, err = tr.lookupAccount(common.HexToHash("0xa"), common.Hash{0x3})
1011+
if err != nil {
1012+
t.Fatalf("lookupAccount(known): %v", err)
1013+
}
1014+
if l.rootHash() != (common.Hash{0x2}) {
1015+
t.Errorf("known account tip: want %x, got %x",
1016+
common.Hash{0x2}, l.rootHash())
1017+
}
1018+
1019+
// Case 4: truly stale state root. This pins the other half of the
1020+
// contract — the boolean must actually signal not-found for an
1021+
// unknown state, otherwise a refactor that always returned
1022+
// ok=true would still pass cases 1–3.
1023+
_, err = tr.lookupAccount(common.HexToHash("0xa"), common.HexToHash("0xdeadbeef"))
1024+
if !errors.Is(err, errSnapshotStale) {
1025+
t.Errorf("lookupAccount(stale state): want errSnapshotStale, got %v", err)
1026+
}
1027+
_, err = tr.lookupStorage(
1028+
common.HexToHash("0xa"), common.HexToHash("0x1"),
1029+
common.HexToHash("0xdeadbeef"))
1030+
if !errors.Is(err, errSnapshotStale) {
1031+
t.Errorf("lookupStorage(stale state): want errSnapshotStale, got %v", err)
1032+
}
1033+
}

0 commit comments

Comments
 (0)