trie/bintrie: mark promoted stem dirty during splitStemValuesInsert

When splitStemValuesInsert inserts a new stem that shares a prefix with
an existing stem, it increments the existing stem's depth and inserts a
new internal node above it. The existing stem's on-disk path is derived
from its depth via collectChildGroups + extendPathToGroupLeaf, so
promoting its depth means it should be flushed at a new path.

Previously, only the new stem (created in the divergence branch) was
marked dirty. The promoted existing stem retained whatever dirty value
it had — false if it was just deserialized from disk via a HashedNode
resolve. collectNodes would then skip flushing the existing stem at its
new path, while the new ancestor internal blob (also dirty) overwrites
the existing stem's old blob at the prior path. The stem's data is
left with no on-disk home, breaking subsequent reads with
"missing trie node".

The bug surfaces in the integration-test harness (state-actor builds a
DB with single-stem-per-slot at depth 8, geth then mutates by adding a
new stem that shares ≥8 prefix bits with the existing stem). After
mutation, geth's `getValuesAtStem` resolves a HashedNode whose blob
should be at the extended-depth path but isn't on disk.

Mark `existing.dirty = true` when promoting the depth so collectNodes
re-flushes the stem at its new path.

Verification: the 100MB integration-test harness (which previously
failed at block 9-10 with "missing trie node bdaf89... (path c96010)")
now runs cleanly through 200+ blocks of ERC20 deploys and bloat
transactions without any missing-trie-node errors.
This commit is contained in:
weiihann 2026-05-13 09:39:31 +08:00
parent a1eaa21f24
commit bdb7b64173

View file

@ -263,6 +263,12 @@ func (s *nodeStore) splitStemValuesInsert(existingRef nodeRef, newStem []byte, v
nRef := s.newInternalRef(int(existing.depth))
nNode := s.getInternal(nRef.Index())
existing.depth++
// The existing stem's on-disk path is derived from its depth via
// extendPathToGroupLeaf. Promoting its depth changes that path, so the
// stem must be re-flushed at the new path; otherwise the old blob (at
// the prior path) gets overwritten by the new ancestor internal blob
// and the stem's data has no on-disk home.
existing.dirty = true
bitKey := newStem[nNode.depth/8] >> (7 - (nNode.depth % 8)) & 1
if bitKey == bitStem {