trie/bintrie: move dirty+mustRecompute flip into setValue

The invariant "mutating a value slot must mark the stem for re-hash
and re-flush" was enforced by every caller remembering to set both
flags after setValue. Moving the flip into setValue itself makes it
structurally impossible to forget, and drops the duplicate flag-sets
at each callsite.

decodeNode's on-disk load path still writes directly to sn.values
because loaded stems must retain whatever mustRecompute/dirty state
the caller asked for (typically both false).
This commit is contained in:
CPerezz 2026-04-19 22:16:50 +02:00
parent d216942b7c
commit f676f04706
No known key found for this signature in database
GPG key ID: 62045F34B97177DD
2 changed files with 11 additions and 5 deletions

View file

@ -51,8 +51,15 @@ func (sn *StemNode) allValues() [][]byte {
return sn.values[:] return sn.values[:]
} }
// setValue mutates a value slot and marks the stem for re-hash and
// re-flush. This is the only API for post-load value mutation; direct
// values[...] writes are reserved for the on-disk load path in
// decodeNode, which must leave mustRecompute/dirty at their loaded
// state.
func (sn *StemNode) setValue(suffix byte, value []byte) { func (sn *StemNode) setValue(suffix byte, value []byte) {
sn.values[suffix] = value sn.values[suffix] = value
sn.mustRecompute = true
sn.dirty = true
} }
func (sn *StemNode) Hash() common.Hash { func (sn *StemNode) Hash() common.Hash {

View file

@ -199,12 +199,10 @@ func (s *NodeStore) insertValuesAtStem(ref nodeRef, stem []byte, values [][]byte
case kindStem: case kindStem:
sn := s.getStem(ref.Index()) sn := s.getStem(ref.Index())
if sn.Stem == [StemSize]byte(stem[:StemSize]) { if sn.Stem == [StemSize]byte(stem[:StemSize]) {
// Same stem — merge values // Same stem — merge values (setValue marks dirty+mustRecompute)
for i, v := range values { for i, v := range values {
if v != nil { if v != nil {
sn.setValue(byte(i), v) sn.setValue(byte(i), v)
sn.mustRecompute = true
sn.dirty = true
} }
} }
return ref, nil return ref, nil
@ -233,7 +231,8 @@ func (s *NodeStore) insertValuesAtStem(ref nodeRef, stem []byte, values [][]byte
return s.insertValuesAtStem(resolved, stem, values, resolver, depth) return s.insertValuesAtStem(resolved, stem, values, resolver, depth)
case kindEmpty: case kindEmpty:
// Create new StemNode // Create new StemNode. Flag flips before the value loop so an
// all-nil values input still marks the newly-created stem dirty.
stemIdx := s.allocStem() stemIdx := s.allocStem()
sn := s.getStem(stemIdx) sn := s.getStem(stemIdx)
copy(sn.Stem[:], stem[:StemSize]) copy(sn.Stem[:], stem[:StemSize])
@ -242,7 +241,7 @@ func (s *NodeStore) insertValuesAtStem(ref nodeRef, stem []byte, values [][]byte
sn.dirty = true sn.dirty = true
for i, v := range values { for i, v := range values {
if v != nil { if v != nil {
sn.values[i] = v sn.setValue(byte(i), v)
} }
} }
return makeRef(kindStem, stemIdx), nil return makeRef(kindStem, stemIdx), nil