@@ -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