Skip to content

Commit 0d16a41

Browse files
committed
triedb/pathdb: fix lookup sentinel collision with zero disk layer root
lookup.accountTip and storageTip used common.Hash{} as the "state is stale" sentinel while ALSO returning common.Hash{} when the disk layer itself happened to be keyed by the zero hash. lookupAccount/Storage then blindly compared the returned value against common.Hash{} and misreported a legitimate disk-layer fallback as errSnapshotStale. For a merkle path database this sentinel collision is invisible: an empty merkle trie hashes to types.EmptyRootHash which is a concrete non-zero keccak, so the disk layer's root never equals common.Hash{}. The collision only shows up once the disk layer root can legitimately be zero — for example, a fresh verkle/bintrie database where the empty binary trie hashes to EmptyVerkleHash = common.Hash{}. In that configuration, any Account/Storage lookup for a key that has never been written ends up taking the disk-layer fallback branch, which correctly returns base=common.Hash{}, which lookupAccount then misreads as "stale" and bubbles an error up to the caller. Fix: change accountTip/storageTip to return (common.Hash, bool) so the "found the tip" signal is carried out of band from the hash value. lookupAccount/Storage now consult the boolean rather than comparing the returned hash to zero. The returned hash itself may still be zero (that is a valid disk-layer root on the bintrie path) and callers must not treat it as a sentinel. Noticed while wiring the bintrie flat-state reader in a separate branch; the fix is scheme-agnostic and lands here so it can flow into master independently of that work.
1 parent 04e4099 commit 0d16a41

File tree

2 files changed

+22
-23
lines changed

2 files changed

+22
-23
lines changed

triedb/pathdb/layertree.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -319,8 +319,8 @@ func (tree *layerTree) lookupAccount(accountHash common.Hash, state common.Hash)
319319
tree.lock.RLock()
320320
defer tree.lock.RUnlock()
321321

322-
tip := tree.lookup.accountTip(accountHash, state, tree.base.root)
323-
if tip == (common.Hash{}) {
322+
tip, ok := tree.lookup.accountTip(accountHash, state, tree.base.root)
323+
if !ok {
324324
return nil, fmt.Errorf("[%#x] %w", state, errSnapshotStale)
325325
}
326326
l := tree.layers[tip]
@@ -337,8 +337,8 @@ func (tree *layerTree) lookupStorage(accountHash common.Hash, slotHash common.Ha
337337
tree.lock.RLock()
338338
defer tree.lock.RUnlock()
339339

340-
tip := tree.lookup.storageTip(accountHash, slotHash, state, tree.base.root)
341-
if tip == (common.Hash{}) {
340+
tip, ok := tree.lookup.storageTip(accountHash, slotHash, state, tree.base.root)
341+
if !ok {
342342
return nil, fmt.Errorf("[%#x] %w", state, errSnapshotStale)
343343
}
344344
l := tree.layers[tip]

triedb/pathdb/lookup.go

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,16 @@ func newLookup(head layer, descendant func(state common.Hash, ancestor common.Ha
9292
// stateID or is a descendant of it.
9393
//
9494
// If found, the account data corresponding to the supplied stateID resides
95-
// in that layer. Otherwise, two scenarios are possible:
95+
// in the layer identified by the returned hash (ok=true). Otherwise,
96+
// (common.Hash{}, false) is returned to signal that the supplied stateID is
97+
// stale.
9698
//
97-
// (a) the account remains unmodified from the current disk layer up to the state
98-
// layer specified by the stateID: fallback to the disk layer for data retrieval,
99-
// (b) or the layer specified by the stateID is stale: reject the data retrieval.
100-
func (l *lookup) accountTip(accountHash common.Hash, stateID common.Hash, base common.Hash) common.Hash {
99+
// Note the returned hash may itself be common.Hash{} when the disk layer's
100+
// root is zero — as is the case for a fresh verkle/bintrie database whose
101+
// empty trie hashes to EmptyVerkleHash. Callers must therefore consult the
102+
// boolean rather than comparing the returned hash against common.Hash{}
103+
// directly.
104+
func (l *lookup) accountTip(accountHash common.Hash, stateID common.Hash, base common.Hash) (common.Hash, bool) {
101105
// Traverse the mutation history from latest to oldest one. Several
102106
// scenarios are possible:
103107
//
@@ -123,49 +127,44 @@ func (l *lookup) accountTip(accountHash common.Hash, stateID common.Hash, base c
123127
// containing the modified data. Otherwise, the current state may be ahead
124128
// of the requested one or belong to a different branch.
125129
if list[i] == stateID || l.descendant(stateID, list[i]) {
126-
return list[i]
130+
return list[i], true
127131
}
128132
}
129133
// No layer matching the stateID or its descendants was found. Use the
130134
// current disk layer as a fallback.
131135
if base == stateID || l.descendant(stateID, base) {
132-
return base
136+
return base, true
133137
}
134138
// The layer associated with 'stateID' is not the descendant of the current
135139
// disk layer, it's already stale, return nothing.
136-
return common.Hash{}
140+
return common.Hash{}, false
137141
}
138142

139143
// storageTip traverses the layer list associated with the given account and
140144
// slot hash in reverse order to locate the first entry that either matches
141145
// the specified stateID or is a descendant of it.
142146
//
143-
// If found, the storage data corresponding to the supplied stateID resides
144-
// in that layer. Otherwise, two scenarios are possible:
145-
//
146-
// (a) the storage slot remains unmodified from the current disk layer up to
147-
// the state layer specified by the stateID: fallback to the disk layer for
148-
// data retrieval, (b) or the layer specified by the stateID is stale: reject
149-
// the data retrieval.
150-
func (l *lookup) storageTip(accountHash common.Hash, slotHash common.Hash, stateID common.Hash, base common.Hash) common.Hash {
147+
// See accountTip for the returned-hash / ok convention — the same
148+
// bintrie-zero-root caveat applies here.
149+
func (l *lookup) storageTip(accountHash common.Hash, slotHash common.Hash, stateID common.Hash, base common.Hash) (common.Hash, bool) {
151150
list := l.storages[storageKey(accountHash, slotHash)]
152151
for i := len(list) - 1; i >= 0; i-- {
153152
// If the current state matches the stateID, or the requested state is a
154153
// descendant of it, return the current state as the most recent one
155154
// containing the modified data. Otherwise, the current state may be ahead
156155
// of the requested one or belong to a different branch.
157156
if list[i] == stateID || l.descendant(stateID, list[i]) {
158-
return list[i]
157+
return list[i], true
159158
}
160159
}
161160
// No layer matching the stateID or its descendants was found. Use the
162161
// current disk layer as a fallback.
163162
if base == stateID || l.descendant(stateID, base) {
164-
return base
163+
return base, true
165164
}
166165
// The layer associated with 'stateID' is not the descendant of the current
167166
// disk layer, it's already stale, return nothing.
168-
return common.Hash{}
167+
return common.Hash{}, false
169168
}
170169

171170
// addLayer traverses the state data retained in the specified diff layer and

0 commit comments

Comments
 (0)