nomt/merkle, nomt/db: adapt to EIP-7864 stem-based types (Phase D)

Remove leaf compaction from PageWalker.compactStep, replace KeyValue
with StemKeyValue throughout the merkle engine and worker, and update
DB.Update to accept pre-hashed stem key-value pairs.

Key change: singleThreadedUpdate now uses the same depth-7 child-index
partitioning as the parallel path, ensuring identical intermediate
hashes without leaf compaction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
weiihann 2026-02-12 22:23:57 +08:00
parent 2db24e85a3
commit 1051e7be0c
6 changed files with 336 additions and 365 deletions

View file

@ -127,15 +127,14 @@ func (db *DB) SyncSeqn() uint32 {
return db.syncSeqn return db.syncSeqn
} }
// Update applies a batch of leaf operations to the trie. // Update applies a sorted batch of stem key-value pairs to the trie.
// //
// Operations are sorted by key internally. The function: // The pairs must be pre-sorted by stem path. The function:
// 1. Builds a PageSet from Bitbox // 1. Builds a PageSet from Bitbox
// 2. Groups operations by their terminal node position // 2. Runs the parallel PageWalker to produce updated pages
// 3. Runs the PageWalker to produce updated pages // 3. Persists updated pages via Bitbox sync
// 4. Persists updated pages via Bitbox sync // 4. Returns the new root hash
// 5. Returns the new root hash func (db *DB) Update(ops []core.StemKeyValue) (core.Node, error) {
func (db *DB) Update(ops []core.LeafOp) (core.Node, error) {
if len(ops) == 0 { if len(ops) == 0 {
return db.Root(), nil return db.Root(), nil
} }
@ -143,26 +142,15 @@ func (db *DB) Update(ops []core.LeafOp) (core.Node, error) {
db.mu.Lock() db.mu.Lock()
defer db.mu.Unlock() defer db.mu.Unlock()
// Sort ops by key path. // Sort by stem path.
sort.Slice(ops, func(i, j int) bool { sort.Slice(ops, func(i, j int) bool {
return ops[i].Key != ops[j].Key && keyLess(&ops[i].Key, &ops[j].Key) return stemLess(&ops[i].Stem, &ops[j].Stem)
}) })
// Convert to KeyValue (filter out deletes).
kvs := make([]core.KeyValue, 0, len(ops))
for _, op := range ops {
if op.Value != nil {
kvs = append(kvs, core.KeyValue{Key: op.Key, Value: *op.Value})
}
}
if len(kvs) == 0 {
return db.root, nil
}
pageSetFactory := func() merkle.PageSet { pageSetFactory := func() merkle.PageSet {
return newBitboxPageSet(db.bb) return newBitboxPageSet(db.bb)
} }
out := merkle.ParallelUpdate(db.root, kvs, db.numWorkers, pageSetFactory) out := merkle.ParallelUpdate(db.root, ops, db.numWorkers, pageSetFactory)
// Persist updated pages. // Persist updated pages.
walPath := filepath.Join(db.dataDir, walFileName) walPath := filepath.Join(db.dataDir, walFileName)
@ -260,7 +248,7 @@ func pageIDKey(id core.PageID) string {
return string(encoded[:]) return string(encoded[:])
} }
func keyLess(a, b *core.KeyPath) bool { func stemLess(a, b *core.StemPath) bool {
for i := range a { for i := range a {
if a[i] < b[i] { if a[i] < b[i] {
return true return true

View file

@ -31,9 +31,8 @@ func TestReopenPreservesState(t *testing.T) {
db, err := Open(dir, DefaultConfig()) db, err := Open(dir, DefaultConfig())
require.NoError(t, err) require.NoError(t, err)
v := core.ValueHash{0x01} newRoot, err := db.Update([]core.StemKeyValue{
newRoot, err := db.Update([]core.LeafOp{ {Stem: makeStem(0x10), Hash: makeHash(0x01)},
{Key: makeKey(0x10), Value: &v},
}) })
require.NoError(t, err) require.NoError(t, err)
require.False(t, core.IsTerminator(&newRoot)) require.False(t, core.IsTerminator(&newRoot))
@ -55,15 +54,12 @@ func TestUpdateSingleKey(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
defer db.Close() defer db.Close()
v := core.ValueHash{0x42} newRoot, err := db.Update([]core.StemKeyValue{
kp := makeKey(0x10) // starts with 0 bit {Stem: makeStem(0x10), Hash: makeHash(0x42)},
newRoot, err := db.Update([]core.LeafOp{
{Key: kp, Value: &v},
}) })
require.NoError(t, err) require.NoError(t, err)
assert.True(t, core.IsLeaf(&newRoot)) assert.False(t, core.IsTerminator(&newRoot))
assert.Equal(t, newRoot, db.Root()) assert.Equal(t, newRoot, db.Root())
} }
@ -73,22 +69,20 @@ func TestUpdateMultipleKeys(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
defer db.Close() defer db.Close()
v := core.ValueHash{0x01} ops := []core.StemKeyValue{
ops := []core.LeafOp{ {Stem: makeStem(0x10), Hash: makeHash(0x01)},
{Key: makeKey(0x10), Value: &v}, {Stem: makeStem(0x80), Hash: makeHash(0x02)},
{Key: makeKey(0x80), Value: &v},
} }
newRoot, err := db.Update(ops) newRoot, err := db.Update(ops)
require.NoError(t, err) require.NoError(t, err)
assert.True(t, core.IsInternal(&newRoot)) assert.False(t, core.IsTerminator(&newRoot))
} }
func TestUpdateDeterministic(t *testing.T) { func TestUpdateDeterministic(t *testing.T) {
v := core.ValueHash{0x01} ops := []core.StemKeyValue{
ops := []core.LeafOp{ {Stem: makeStem(0x10), Hash: makeHash(0x01)},
{Key: makeKey(0x10), Value: &v}, {Stem: makeStem(0x80), Hash: makeHash(0x02)},
{Key: makeKey(0x80), Value: &v},
} }
run := func() core.Node { run := func() core.Node {
@ -118,22 +112,21 @@ func TestUpdateEmptyOps(t *testing.T) {
assert.Equal(t, core.Terminator, root) assert.Equal(t, core.Terminator, root)
} }
func TestUpdateSortsByKey(t *testing.T) { func TestUpdateSortsByStem(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
db, err := Open(dir, DefaultConfig()) db, err := Open(dir, DefaultConfig())
require.NoError(t, err) require.NoError(t, err)
defer db.Close() defer db.Close()
v := core.ValueHash{0x01} // Provide stems in reverse order — should still work.
// Provide keys in reverse order — should still work. ops := []core.StemKeyValue{
ops := []core.LeafOp{ {Stem: makeStem(0x80), Hash: makeHash(0x01)},
{Key: makeKey(0x80), Value: &v}, {Stem: makeStem(0x10), Hash: makeHash(0x02)},
{Key: makeKey(0x10), Value: &v},
} }
root, err := db.Update(ops) root, err := db.Update(ops)
require.NoError(t, err) require.NoError(t, err)
assert.True(t, core.IsInternal(&root)) assert.False(t, core.IsTerminator(&root))
} }
func TestSyncSeqnIncrements(t *testing.T) { func TestSyncSeqnIncrements(t *testing.T) {
@ -144,24 +137,33 @@ func TestSyncSeqnIncrements(t *testing.T) {
assert.Equal(t, uint32(0), db.SyncSeqn()) assert.Equal(t, uint32(0), db.SyncSeqn())
v := core.ValueHash{0x01} _, err = db.Update([]core.StemKeyValue{
_, err = db.Update([]core.LeafOp{ {Stem: makeStem(0x10), Hash: makeHash(0x01)},
{Key: makeKey(0x10), Value: &v},
}) })
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, uint32(1), db.SyncSeqn()) assert.Equal(t, uint32(1), db.SyncSeqn())
_, err = db.Update([]core.LeafOp{ _, err = db.Update([]core.StemKeyValue{
{Key: makeKey(0x80), Value: &v}, {Stem: makeStem(0x80), Hash: makeHash(0x02)},
}) })
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, uint32(2), db.SyncSeqn()) assert.Equal(t, uint32(2), db.SyncSeqn())
} }
func makeKey(b byte) core.KeyPath { func makeStem(b byte) core.StemPath {
var kp core.KeyPath var sp core.StemPath
for i := range kp { for i := range sp {
kp[i] = b sp[i] = b
} }
return kp return sp
}
func makeHash(b byte) core.Node {
var h core.Node
for i := range h {
h[i] = b ^ byte(i)
}
// Ensure non-zero to avoid terminator.
h[0] |= 0x01
return h
} }

View file

@ -76,7 +76,7 @@ func NewPageWalker(root core.Node, parentPage *core.PageID) *PageWalker {
} }
// AdvanceAndReplace advances to the given position and replaces the terminal // AdvanceAndReplace advances to the given position and replaces the terminal
// node there with a sub-trie built from the provided key-value pairs. // node there with a sub-trie built from the provided stem key-value pairs.
// //
// The pairs must be sorted and must all share the prefix corresponding to // The pairs must be sorted and must all share the prefix corresponding to
// the position. An empty slice deletes the existing terminal node. // the position. An empty slice deletes the existing terminal node.
@ -85,7 +85,7 @@ func NewPageWalker(root core.Node, parentPage *core.PageID) *PageWalker {
func (w *PageWalker) AdvanceAndReplace( func (w *PageWalker) AdvanceAndReplace(
pageSet PageSet, pageSet PageSet,
newPos core.TriePosition, newPos core.TriePosition,
ops []core.KeyValue, ops []core.StemKeyValue,
) { ) {
if w.lastPosition != nil { if w.lastPosition != nil {
w.assertForward(&newPos) w.assertForward(&newPos)
@ -152,7 +152,7 @@ func (w *PageWalker) placeNode(node core.Node) {
} }
} }
func (w *PageWalker) replaceTerminal(pageSet PageSet, ops []core.KeyValue) { func (w *PageWalker) replaceTerminal(pageSet PageSet, ops []core.StemKeyValue) {
var existingNode core.Node var existingNode core.Node
if w.position.IsRoot() { if w.position.IsRoot() {
existingNode = w.root existingNode = w.root
@ -163,7 +163,7 @@ func (w *PageWalker) replaceTerminal(pageSet PageSet, ops []core.KeyValue) {
startDepth := w.position.Depth() startDepth := w.position.Depth()
core.BuildTrie(int(w.position.Depth()), ops, func(wn core.WriteNode) { core.BuildInternalTree(int(w.position.Depth()), ops, func(wn core.WriteNode) {
node := wn.Node node := wn.Node
// For internal nodes, clear garbage in the sibling slot if the // For internal nodes, clear garbage in the sibling slot if the
@ -443,41 +443,24 @@ func (w *PageWalker) compactUpToRoot() {
} }
// compactStep performs one layer of compaction: reads the current node and // compactStep performs one layer of compaction: reads the current node and
// its sibling, then either compacts terminators/leaves upward or hashes // its sibling, then hashes them as an internal node. In EIP-7864, there is
// an internal node. // no leaf compaction — stem hashes are opaque values that never float up.
func (w *PageWalker) compactStep() core.Node { func (w *PageWalker) compactStep() core.Node {
node := w.node() node := w.node()
sibling := w.siblingNode() sibling := w.siblingNode()
bit := w.position.PeekLastBit()
nodeKind := core.NodeKindOf(&node) if core.IsTerminator(&node) && core.IsTerminator(&sibling) {
sibKind := core.NodeKindOf(&sibling)
switch {
case nodeKind == core.NodeTerminator && sibKind == core.NodeTerminator:
return core.Terminator return core.Terminator
case nodeKind == core.NodeLeaf && sibKind == core.NodeTerminator:
// Compact: clear this node, move leaf up.
w.setNode(core.Terminator)
return node
case nodeKind == core.NodeTerminator && sibKind == core.NodeLeaf:
// Compact: clear sibling, move leaf up.
w.position.Sibling()
w.setNode(core.Terminator)
return sibling
default:
// Internal: hash the two children together.
var id core.InternalData
if bit {
id = core.InternalData{Left: sibling, Right: node}
} else {
id = core.InternalData{Left: node, Right: sibling}
}
return core.HashInternal(&id)
} }
bit := w.position.PeekLastBit()
var id core.InternalData
if bit {
id = core.InternalData{Left: sibling, Right: node}
} else {
id = core.InternalData{Left: node, Right: sibling}
}
return core.HashInternal(&id)
} }
// --- page node access --- // --- page node access ---

View file

@ -17,30 +17,47 @@ func triePos(bits ...bool) core.TriePosition {
return p return p
} }
// Helper: create a KeyPath with the given bits set at the MSB positions. // makeStemPath creates a StemPath filled with a single byte.
func keyPath(bits ...bool) core.KeyPath { func makeStemPath(b byte) core.StemPath {
var kp core.KeyPath var sp core.StemPath
for i, b := range bits { for i := range sp {
if b { sp[i] = b
kp[i/8] |= 1 << (7 - i%8) }
return sp
}
// makeSKV creates a StemKeyValue where the stem is filled with b
// and the hash is a deterministic non-zero value derived from b.
func makeSKV(b byte) core.StemKeyValue {
stem := makeStemPath(b)
var hash core.Node
for i := range hash {
hash[i] = b ^ byte(i)
}
return core.StemKeyValue{Stem: stem, Hash: hash}
}
// expectedRoot computes the expected root hash for a set of stem key-values
// using BuildInternalTree as the oracle. This splits at bit 0 (left/right)
// and hashes up, matching the PageWalker's singleThreadedUpdate approach.
func expectedRoot(skvs []core.StemKeyValue) core.Node {
if len(skvs) == 0 {
return core.Terminator
}
var left, right []core.StemKeyValue
for _, skv := range skvs {
if skv.Stem[0]&0x80 == 0 {
left = append(left, skv)
} else {
right = append(right, skv)
} }
} }
return kp leftHash := core.BuildInternalTree(1, left, func(_ core.WriteNode) {})
} rightHash := core.BuildInternalTree(1, right, func(_ core.WriteNode) {})
if core.IsTerminator(&leftHash) && core.IsTerminator(&rightHash) {
// Helper: create a ValueHash filled with a single byte. return core.Terminator
func val(v byte) core.ValueHash {
var vh core.ValueHash
for i := range vh {
vh[i] = v
} }
return vh return core.HashInternal(&core.InternalData{Left: leftHash, Right: rightHash})
}
// Helper: compute the expected root from a set of key-value pairs using
// BuildTrie directly (the "oracle").
func expectedRoot(kvs []core.KeyValue) core.Node {
return core.BuildTrie(0, kvs, func(_ core.WriteNode) {})
} }
func TestPageWalkerEmptyTrie(t *testing.T) { func TestPageWalkerEmptyTrie(t *testing.T) {
@ -56,40 +73,31 @@ func TestPageWalkerSingleInsert(t *testing.T) {
ps := NewMemoryPageSet(true) ps := NewMemoryPageSet(true)
walker := NewPageWalker(core.Terminator, nil) walker := NewPageWalker(core.Terminator, nil)
kp := keyPath(false, false) skv := makeSKV(0x00) // stem 0x00..., first bit = 0
v := val(1) pos := triePos(false)
pos := triePos(false, false)
walker.AdvanceAndReplace(ps, pos, []core.KeyValue{{Key: kp, Value: v}}) walker.AdvanceAndReplace(ps, pos, []core.StemKeyValue{skv})
out := walker.Conclude() out := walker.Conclude()
expected := expectedRoot([]core.KeyValue{{Key: kp, Value: v}}) expected := expectedRoot([]core.StemKeyValue{skv})
assert.Equal(t, expected, out.Root) assert.Equal(t, expected, out.Root)
assert.True(t, core.IsLeaf(&out.Root)) assert.False(t, core.IsTerminator(&out.Root))
} }
func TestPageWalkerTwoInsertsSameAdvance(t *testing.T) { func TestPageWalkerTwoInsertsSameAdvance(t *testing.T) {
// Two keys that share a common prefix at position [0,0], then diverge. // Two stems that share a common prefix, then diverge.
ps := NewMemoryPageSet(true) ps := NewMemoryPageSet(true)
walker := NewPageWalker(core.Terminator, nil) walker := NewPageWalker(core.Terminator, nil)
kp1 := keyPath(false, false, true, false) // 0010... skv1 := makeSKV(0x00) // 0000 0000...
kp2 := keyPath(false, false, true, true) // 0011... skv2 := makeSKV(0x01) // 0000 0001... (differ at bit 7)
v1, v2 := val(1), val(2)
pos := triePos(false, false) pos := triePos(false) // advance to left subtree
walker.AdvanceAndReplace(ps, pos, []core.KeyValue{ walker.AdvanceAndReplace(ps, pos, []core.StemKeyValue{skv1, skv2})
{Key: kp1, Value: v1},
{Key: kp2, Value: v2},
})
out := walker.Conclude() out := walker.Conclude()
expected := expectedRoot([]core.KeyValue{ expected := expectedRoot([]core.StemKeyValue{skv1, skv2})
{Key: kp1, Value: v1},
{Key: kp2, Value: v2},
})
assert.Equal(t, expected, out.Root) assert.Equal(t, expected, out.Root)
assert.True(t, core.IsInternal(&out.Root))
} }
func TestPageWalkerTwoAdvances(t *testing.T) { func TestPageWalkerTwoAdvances(t *testing.T) {
@ -97,71 +105,55 @@ func TestPageWalkerTwoAdvances(t *testing.T) {
ps := NewMemoryPageSet(true) ps := NewMemoryPageSet(true)
walker := NewPageWalker(core.Terminator, nil) walker := NewPageWalker(core.Terminator, nil)
kp0 := keyPath(false, false) // 00... skv0 := makeSKV(0x00) // first bit 0
kp1 := keyPath(true, false) // 10... skv1 := makeSKV(0x80) // first bit 1
v0, v1 := val(1), val(2)
walker.AdvanceAndReplace(ps, triePos(false), []core.KeyValue{ walker.AdvanceAndReplace(ps, triePos(false), []core.StemKeyValue{skv0})
{Key: kp0, Value: v0}, walker.AdvanceAndReplace(ps, triePos(true), []core.StemKeyValue{skv1})
})
walker.AdvanceAndReplace(ps, triePos(true), []core.KeyValue{
{Key: kp1, Value: v1},
})
out := walker.Conclude() out := walker.Conclude()
expected := expectedRoot([]core.KeyValue{ expected := expectedRoot([]core.StemKeyValue{skv0, skv1})
{Key: kp0, Value: v0},
{Key: kp1, Value: v1},
})
assert.Equal(t, expected, out.Root) assert.Equal(t, expected, out.Root)
} }
func TestPageWalkerMultipleAdvances(t *testing.T) { func TestPageWalkerMultipleAdvances(t *testing.T) {
// Match the Rust multi_value test pattern: // Match the multi_value test pattern.
// 0b00010000 = 0x10, 0b00100000 = 0x20, 0b01000000 = 0x40,
// 0b10100000 = 0xA0, 0b10110000 = 0xB0
ps := NewMemoryPageSet(true) ps := NewMemoryPageSet(true)
walker := NewPageWalker(core.Terminator, nil) walker := NewPageWalker(core.Terminator, nil)
kvA := core.KeyValue{Key: makeKVKey(0x10), Value: makeKVVal(0x10)} skvA := makeSKV(0x10) // 0001...
kvB := core.KeyValue{Key: makeKVKey(0x20), Value: makeKVVal(0x20)} skvB := makeSKV(0x20) // 0010...
kvC := core.KeyValue{Key: makeKVKey(0x40), Value: makeKVVal(0x40)} skvC := makeSKV(0x40) // 0100...
kvD := core.KeyValue{Key: makeKVKey(0xA0), Value: makeKVVal(0xA0)} skvD := makeSKV(0xA0) // 1010...
kvE := core.KeyValue{Key: makeKVKey(0xB0), Value: makeKVVal(0xB0)} skvE := makeSKV(0xB0) // 1011...
allOps := []core.KeyValue{kvA, kvB, kvC, kvD, kvE} allOps := []core.StemKeyValue{skvA, skvB, skvC, skvD, skvE}
expected := expectedRoot(allOps) expected := expectedRoot(allOps)
// Group by terminal: A and B share prefix 00, C is at 01, D and E // Group by terminal: A and B share prefix 00, C is at 01, D and E
// share prefix 101. // share prefix 101.
// Terminal at [0,0]: A, B
walker.AdvanceAndReplace(ps, triePos(false, false), walker.AdvanceAndReplace(ps, triePos(false, false),
[]core.KeyValue{kvA, kvB}) []core.StemKeyValue{skvA, skvB})
// Terminal at [0,1]: C
walker.AdvanceAndReplace(ps, triePos(false, true), walker.AdvanceAndReplace(ps, triePos(false, true),
[]core.KeyValue{kvC}) []core.StemKeyValue{skvC})
// Terminal at [1]: D, E
walker.AdvanceAndReplace(ps, triePos(true), walker.AdvanceAndReplace(ps, triePos(true),
[]core.KeyValue{kvD, kvE}) []core.StemKeyValue{skvD, skvE})
out := walker.Conclude() out := walker.Conclude()
assert.Equal(t, expected, out.Root) assert.Equal(t, expected, out.Root)
} }
func TestPageWalkerDeleteToTerminator(t *testing.T) { func TestPageWalkerDeleteToTerminator(t *testing.T) {
// Insert a leaf, then delete it in a second walker pass. // Insert a stem, then delete it in a second walker pass.
ps := NewMemoryPageSet(true) ps := NewMemoryPageSet(true)
kp := keyPath(false) skv := makeSKV(0x00) // left child
v := val(1)
// First: insert a leaf. // First: insert a stem.
walker1 := NewPageWalker(core.Terminator, nil) walker1 := NewPageWalker(core.Terminator, nil)
walker1.AdvanceAndReplace(ps, triePos(false), []core.KeyValue{ walker1.AdvanceAndReplace(ps, triePos(false), []core.StemKeyValue{skv})
{Key: kp, Value: v},
})
out1 := walker1.Conclude() out1 := walker1.Conclude()
require.True(t, core.IsLeaf(&out1.Root)) require.False(t, core.IsTerminator(&out1.Root))
ps.Apply(out1.Pages) ps.Apply(out1.Pages)
// Second: delete it (empty ops = terminator replacement). // Second: delete it (empty ops = terminator replacement).
@ -171,45 +163,37 @@ func TestPageWalkerDeleteToTerminator(t *testing.T) {
assert.Equal(t, core.Terminator, out2.Root) assert.Equal(t, core.Terminator, out2.Root)
} }
func TestPageWalkerCompactionLeafUp(t *testing.T) { func TestPageWalkerNoLeafCompaction(t *testing.T) {
// When one sibling becomes a terminator and the other is a leaf, // In EIP-7864, when one sibling becomes a terminator and the other is
// the leaf should be compacted upward. // a stem hash, they form an internal node (no compaction upward).
ps := NewMemoryPageSet(true) ps := NewMemoryPageSet(true)
kp0 := keyPath(false) skv0 := makeSKV(0x00) // left
kp1 := keyPath(true) skv1 := makeSKV(0x80) // right
v := val(1)
// Insert two leaves. // Insert two stems.
walker1 := NewPageWalker(core.Terminator, nil) walker1 := NewPageWalker(core.Terminator, nil)
walker1.AdvanceAndReplace(ps, triePos(false), []core.KeyValue{ walker1.AdvanceAndReplace(ps, triePos(false), []core.StemKeyValue{skv0})
{Key: kp0, Value: v}, walker1.AdvanceAndReplace(ps, triePos(true), []core.StemKeyValue{skv1})
})
walker1.AdvanceAndReplace(ps, triePos(true), []core.KeyValue{
{Key: kp1, Value: v},
})
out1 := walker1.Conclude() out1 := walker1.Conclude()
ps.Apply(out1.Pages) ps.Apply(out1.Pages)
// Delete the right leaf — left leaf should compact up to root. // Delete the right stem — root should be HashInternal(stemHash, Terminator),
// NOT just stemHash (no compaction).
walker2 := NewPageWalker(out1.Root, nil) walker2 := NewPageWalker(out1.Root, nil)
walker2.AdvanceAndReplace(ps, triePos(true), nil) walker2.AdvanceAndReplace(ps, triePos(true), nil)
out2 := walker2.Conclude() out2 := walker2.Conclude()
expectedLeaf := core.HashLeaf(&core.LeafData{KeyPath: kp0, ValueHash: v}) expected := core.HashInternal(&core.InternalData{Left: skv0.Hash, Right: core.Terminator})
assert.Equal(t, expectedLeaf, out2.Root) assert.Equal(t, expected, out2.Root)
} }
func TestPageWalkerOutputPages(t *testing.T) { func TestPageWalkerOutputPages(t *testing.T) {
ps := NewMemoryPageSet(true) ps := NewMemoryPageSet(true)
walker := NewPageWalker(core.Terminator, nil) walker := NewPageWalker(core.Terminator, nil)
kp := keyPath(false) skv := makeSKV(0x00)
v := val(1) walker.AdvanceAndReplace(ps, triePos(false), []core.StemKeyValue{skv})
walker.AdvanceAndReplace(ps, triePos(false), []core.KeyValue{
{Key: kp, Value: v},
})
out := walker.Conclude() out := walker.Conclude()
// Should output at least the root page. // Should output at least the root page.
@ -221,31 +205,25 @@ func TestPageWalkerAdvanceBackwardsPanics(t *testing.T) {
ps := NewMemoryPageSet(true) ps := NewMemoryPageSet(true)
walker := NewPageWalker(core.Terminator, nil) walker := NewPageWalker(core.Terminator, nil)
walker.AdvanceAndReplace(ps, triePos(true), []core.KeyValue{ walker.AdvanceAndReplace(ps, triePos(true), []core.StemKeyValue{makeSKV(0x80)})
{Key: keyPath(true), Value: val(1)},
})
assert.Panics(t, func() { assert.Panics(t, func() {
walker.AdvanceAndReplace(ps, triePos(false), []core.KeyValue{ walker.AdvanceAndReplace(ps, triePos(false), []core.StemKeyValue{makeSKV(0x00)})
{Key: keyPath(false), Value: val(2)},
})
}) })
} }
func TestPageWalkerDeterministic(t *testing.T) { func TestPageWalkerDeterministic(t *testing.T) {
// Same inputs should produce the same root. skvs := []core.StemKeyValue{
kvs := []core.KeyValue{ makeSKV(0x10),
{Key: makeKVKey(0x10), Value: makeKVVal(0x10)}, makeSKV(0x50),
{Key: makeKVKey(0x50), Value: makeKVVal(0x50)}, makeSKV(0xA0),
{Key: makeKVKey(0xA0), Value: makeKVVal(0xA0)},
} }
run := func() core.Node { run := func() core.Node {
ps := NewMemoryPageSet(true) ps := NewMemoryPageSet(true)
w := NewPageWalker(core.Terminator, nil) w := NewPageWalker(core.Terminator, nil)
// All at root terminal since trie is empty. w.AdvanceAndReplace(ps, triePos(false), skvs[:2])
w.AdvanceAndReplace(ps, triePos(false), kvs[:2]) w.AdvanceAndReplace(ps, triePos(true), skvs[2:])
w.AdvanceAndReplace(ps, triePos(true), kvs[2:])
return w.Conclude().Root return w.Conclude().Root
} }
@ -259,102 +237,60 @@ func TestPageWalkerIncrementalUpdates(t *testing.T) {
// building from scratch. // building from scratch.
ps := NewMemoryPageSet(true) ps := NewMemoryPageSet(true)
kp0 := makeKVKey(0x10) skv0 := makeSKV(0x10) // left
kp1 := makeKVKey(0x80) skv1 := makeSKV(0x80) // right
v1, v2 := makeKVVal(0x01), makeKVVal(0x02) skv1Updated := core.StemKeyValue{
Stem: skv1.Stem,
Hash: core.Node{0xFF, 0xFE, 0xFD}, // different hash
}
// Pass 1: insert two keys. // Pass 1: insert two stems.
w1 := NewPageWalker(core.Terminator, nil) w1 := NewPageWalker(core.Terminator, nil)
w1.AdvanceAndReplace(ps, triePos(false), []core.KeyValue{ w1.AdvanceAndReplace(ps, triePos(false), []core.StemKeyValue{skv0})
{Key: kp0, Value: v1}, w1.AdvanceAndReplace(ps, triePos(true), []core.StemKeyValue{skv1})
})
w1.AdvanceAndReplace(ps, triePos(true), []core.KeyValue{
{Key: kp1, Value: v1},
})
out1 := w1.Conclude() out1 := w1.Conclude()
ps.Apply(out1.Pages) ps.Apply(out1.Pages)
// Pass 2: update the second key's value. // Pass 2: update the second stem's hash.
w2 := NewPageWalker(out1.Root, nil) w2 := NewPageWalker(out1.Root, nil)
w2.AdvanceAndReplace(ps, triePos(true), []core.KeyValue{ w2.AdvanceAndReplace(ps, triePos(true), []core.StemKeyValue{skv1Updated})
{Key: kp1, Value: v2},
})
out2 := w2.Conclude() out2 := w2.Conclude()
// The expected root should match building the whole trie from scratch // The expected root should match building from scratch with updated hash.
// with the updated value. expected := expectedRoot([]core.StemKeyValue{skv0, skv1Updated})
expected := expectedRoot([]core.KeyValue{
{Key: kp0, Value: v1},
{Key: kp1, Value: v2},
})
assert.Equal(t, expected, out2.Root) assert.Equal(t, expected, out2.Root)
} }
func TestPageWalkerAdvanceWithoutModify(t *testing.T) { func TestPageWalkerAdvanceWithoutModify(t *testing.T) {
// Test the Advance (read-only) method.
ps := NewMemoryPageSet(true) ps := NewMemoryPageSet(true)
walker := NewPageWalker(core.Terminator, nil) walker := NewPageWalker(core.Terminator, nil)
kp0 := keyPath(false, false, true, false) // 0010... skvA := makeSKV(0x00) // 0000...
kp1 := keyPath(false, false, true, true) // 0011... skvB := makeSKV(0x01) // 0000 0001... (share prefix with A)
kp2 := keyPath(true) // 1... skvC := makeSKV(0x80) // 1000...
v := val(1)
walker.AdvanceAndReplace(ps, triePos(false, false), []core.KeyValue{ walker.AdvanceAndReplace(ps, triePos(false), []core.StemKeyValue{skvA, skvB})
{Key: kp0, Value: v},
{Key: kp1, Value: v},
})
// Advance past [0,1] without modifying — the walker should still // Advance past [0,1] without modifying.
// compact correctly.
walker.Advance(triePos(false, true)) walker.Advance(triePos(false, true))
walker.AdvanceAndReplace(ps, triePos(true), []core.KeyValue{ walker.AdvanceAndReplace(ps, triePos(true), []core.StemKeyValue{skvC})
{Key: kp2, Value: v},
})
out := walker.Conclude() out := walker.Conclude()
expected := expectedRoot([]core.KeyValue{ expected := expectedRoot([]core.StemKeyValue{skvA, skvB, skvC})
{Key: kp0, Value: v},
{Key: kp1, Value: v},
{Key: kp2, Value: v},
})
assert.Equal(t, expected, out.Root) assert.Equal(t, expected, out.Root)
} }
func TestPageWalkerPageDiffs(t *testing.T) { func TestPageWalkerPageDiffs(t *testing.T) {
// Verify that output pages have non-empty diffs.
ps := NewMemoryPageSet(true) ps := NewMemoryPageSet(true)
walker := NewPageWalker(core.Terminator, nil) walker := NewPageWalker(core.Terminator, nil)
kp := keyPath(false, false) skv := makeSKV(0x00)
v := val(1) walker.AdvanceAndReplace(ps, triePos(false), []core.StemKeyValue{skv})
walker.AdvanceAndReplace(ps, triePos(false, false), []core.KeyValue{
{Key: kp, Value: v},
})
out := walker.Conclude() out := walker.Conclude()
require.NotEmpty(t, out.Pages) require.NotEmpty(t, out.Pages)
// The root page should have at least one changed node.
assert.True(t, out.Pages[0].Diff.Count() > 0, assert.True(t, out.Pages[0].Diff.Count() > 0,
"page diff should track changed nodes") "page diff should track changed nodes")
} }
// --- helpers ---
func makeKVKey(b byte) core.KeyPath {
var kp core.KeyPath
for i := range kp {
kp[i] = b
}
return kp
}
func makeKVVal(b byte) core.ValueHash {
var vh core.ValueHash
for i := range vh {
vh[i] = b
}
return vh
}

View file

@ -8,10 +8,10 @@ import (
"github.com/ethereum/go-ethereum/nomt/core" "github.com/ethereum/go-ethereum/nomt/core"
) )
// childBucket groups key-value operations for a single root page child index. // childBucket groups stem key-value operations for a single root page child index.
type childBucket struct { type childBucket struct {
childIndex uint8 childIndex uint8
kvs []core.KeyValue kvs []core.StemKeyValue
} }
// workerTask describes the work assigned to a single worker goroutine. // workerTask describes the work assigned to a single worker goroutine.
@ -26,15 +26,15 @@ type workerResult struct {
err error err error
} }
// ParallelUpdate applies sorted key-value operations to the trie using // ParallelUpdate applies sorted stem key-value operations to the trie using
// multiple worker goroutines. Each worker processes a disjoint set of root // multiple worker goroutines. Each worker processes a disjoint set of root
// page child subtrees (partitioned by the first 6 bits of each key path). // page child subtrees (partitioned by the first 6 bits of each stem path).
// //
// If numWorkers <= 1 or the batch is small, falls back to single-threaded. // If numWorkers <= 1 or the batch is small, falls back to single-threaded.
// The pageSetFactory is called once per worker to create independent PageSets. // The pageSetFactory is called once per worker to create independent PageSets.
func ParallelUpdate( func ParallelUpdate(
root core.Node, root core.Node,
kvs []core.KeyValue, kvs []core.StemKeyValue,
numWorkers int, numWorkers int,
pageSetFactory func() PageSet, pageSetFactory func() PageSet,
) Output { ) Output {
@ -115,42 +115,49 @@ func ParallelUpdate(
// singleThreadedUpdate runs the trie update with a single PageWalker. // singleThreadedUpdate runs the trie update with a single PageWalker.
// This is the fallback for small batches or single-worker configurations. // This is the fallback for small batches or single-worker configurations.
//
// Uses the same child-index partitioning as the parallel path to ensure
// identical hash results. Without leaf compaction, splitting at depth 1
// vs depth 7 produces different intermediate hashes, so both paths must
// use the same splitting strategy.
func singleThreadedUpdate( func singleThreadedUpdate(
root core.Node, root core.Node,
kvs []core.KeyValue, kvs []core.StemKeyValue,
pageSet PageSet, pageSet PageSet,
) Output { ) Output {
walker := NewPageWalker(root, nil) walker := NewPageWalker(root, nil)
var leftKVs, rightKVs []core.KeyValue buckets := partitionByChildIndex(kvs)
for i := range kvs { for childIdx, childKVs := range buckets {
if kvs[i].Key[0]&0x80 == 0 { if len(childKVs) == 0 {
leftKVs = append(leftKVs, kvs[i]) continue
} else { }
rightKVs = append(rightKVs, kvs[i]) var leftKVs, rightKVs []core.StemKeyValue
for i := range childKVs {
if (childKVs[i].Stem[0]>>1)&1 == 0 {
leftKVs = append(leftKVs, childKVs[i])
} else {
rightKVs = append(rightKVs, childKVs[i])
}
} }
}
if len(leftKVs) > 0 { if len(leftKVs) > 0 {
leftPos := core.NewTriePosition() walker.AdvanceAndReplace(pageSet, childPosition(uint8(childIdx), false), leftKVs)
leftPos.Down(false) }
walker.AdvanceAndReplace(pageSet, leftPos, leftKVs) if len(rightKVs) > 0 {
} walker.AdvanceAndReplace(pageSet, childPosition(uint8(childIdx), true), rightKVs)
if len(rightKVs) > 0 { }
rightPos := core.NewTriePosition()
rightPos.Down(true)
walker.AdvanceAndReplace(pageSet, rightPos, rightKVs)
} }
return walker.Conclude() return walker.Conclude()
} }
// partitionByChildIndex buckets sorted KVs by the first 6 bits of each key // partitionByChildIndex buckets sorted SKVs by the first 6 bits of each stem
// path (the root page's child index: 0-63). // path (the root page's child index: 0-63).
func partitionByChildIndex(kvs []core.KeyValue) [64][]core.KeyValue { func partitionByChildIndex(kvs []core.StemKeyValue) [64][]core.StemKeyValue {
var buckets [64][]core.KeyValue var buckets [64][]core.StemKeyValue
for i := range kvs { for i := range kvs {
childIdx := kvs[i].Key[0] >> 2 childIdx := kvs[i].Stem[0] >> 2
buckets[childIdx] = append(buckets[childIdx], kvs[i]) buckets[childIdx] = append(buckets[childIdx], kvs[i])
} }
return buckets return buckets
@ -159,7 +166,7 @@ func partitionByChildIndex(kvs []core.KeyValue) [64][]core.KeyValue {
// assignToWorkers distributes non-empty child buckets across numWorkers // assignToWorkers distributes non-empty child buckets across numWorkers
// contiguous ranges. // contiguous ranges.
func assignToWorkers( func assignToWorkers(
buckets [64][]core.KeyValue, buckets [64][]core.StemKeyValue,
numWorkers int, numWorkers int,
) []workerTask { ) []workerTask {
var nonEmpty []childBucket var nonEmpty []childBucket
@ -205,9 +212,9 @@ func runWorker(
walker := NewPageWalker(root, &rootPageID) walker := NewPageWalker(root, &rootPageID)
for _, child := range task.children { for _, child := range task.children {
var leftKVs, rightKVs []core.KeyValue var leftKVs, rightKVs []core.StemKeyValue
for i := range child.kvs { for i := range child.kvs {
if (child.kvs[i].Key[0]>>1)&1 == 0 { if (child.kvs[i].Stem[0]>>1)&1 == 0 {
leftKVs = append(leftKVs, child.kvs[i]) leftKVs = append(leftKVs, child.kvs[i])
} else { } else {
rightKVs = append(rightKVs, child.kvs[i]) rightKVs = append(rightKVs, child.kvs[i])

View file

@ -13,13 +13,13 @@ import (
// --- Unit tests for helpers --- // --- Unit tests for helpers ---
func TestPartitionByChildIndex(t *testing.T) { func TestPartitionByChildIndex(t *testing.T) {
// Key 0x00... → child 0, key 0xFC... → child 63. // Stem 0x00... → child 0, stem 0x04... → child 1, stem 0xFC... → child 63.
kvs := []core.KeyValue{ skvs := []core.StemKeyValue{
{Key: makeKVKey(0x00), Value: makeKVVal(1)}, makeSKV(0x00),
{Key: makeKVKey(0x04), Value: makeKVVal(2)}, // 0x04 >> 2 = 1 makeSKV(0x04), // 0x04 >> 2 = 1
{Key: makeKVKey(0xFC), Value: makeKVVal(3)}, // 0xFC >> 2 = 63 makeSKV(0xFC), // 0xFC >> 2 = 63
} }
buckets := partitionByChildIndex(kvs) buckets := partitionByChildIndex(skvs)
assert.Len(t, buckets[0], 1) assert.Len(t, buckets[0], 1)
assert.Len(t, buckets[1], 1) assert.Len(t, buckets[1], 1)
@ -69,10 +69,10 @@ func TestChildPosition(t *testing.T) {
func TestAssignToWorkers(t *testing.T) { func TestAssignToWorkers(t *testing.T) {
// 3 non-empty buckets, 2 workers. // 3 non-empty buckets, 2 workers.
var buckets [64][]core.KeyValue var buckets [64][]core.StemKeyValue
buckets[0] = []core.KeyValue{{Key: makeKVKey(0x00), Value: makeKVVal(1)}} buckets[0] = []core.StemKeyValue{makeSKV(0x00)}
buckets[10] = []core.KeyValue{{Key: makeKVKey(0x28), Value: makeKVVal(2)}} // 0x28>>2=10 buckets[10] = []core.StemKeyValue{makeSKV(0x28)} // 0x28>>2=10
buckets[63] = []core.KeyValue{{Key: makeKVKey(0xFC), Value: makeKVVal(3)}} buckets[63] = []core.StemKeyValue{makeSKV(0xFC)}
tasks := assignToWorkers(buckets, 2) tasks := assignToWorkers(buckets, 2)
require.Len(t, tasks, 2) require.Len(t, tasks, 2)
@ -85,9 +85,9 @@ func TestAssignToWorkers(t *testing.T) {
} }
func TestAssignToWorkersMoreWorkersThanChildren(t *testing.T) { func TestAssignToWorkersMoreWorkersThanChildren(t *testing.T) {
var buckets [64][]core.KeyValue var buckets [64][]core.StemKeyValue
buckets[5] = []core.KeyValue{{Key: makeKVKey(0x14), Value: makeKVVal(1)}} // 0x14>>2=5 buckets[5] = []core.StemKeyValue{makeSKV(0x14)} // 0x14>>2=5
buckets[6] = []core.KeyValue{{Key: makeKVKey(0x18), Value: makeKVVal(2)}} // 0x18>>2=6 buckets[6] = []core.StemKeyValue{makeSKV(0x18)} // 0x18>>2=6
tasks := assignToWorkers(buckets, 8) tasks := assignToWorkers(buckets, 8)
// Only 2 non-empty, so cap to 2 workers. // Only 2 non-empty, so cap to 2 workers.
@ -118,99 +118,152 @@ func memoryPageSetFactory() PageSet {
return &permissivePageSet{NewMemoryPageSet(true)} return &permissivePageSet{NewMemoryPageSet(true)}
} }
// expectedWorkerRoot computes the expected root hash matching the depth-7
// child-index partitioning used by both singleThreadedUpdate and ParallelUpdate.
// This differs from expectedRoot (which splits at depth 1) because without
// leaf compaction, the splitting depth affects intermediate hashes.
func expectedWorkerRoot(skvs []core.StemKeyValue) core.Node {
if len(skvs) == 0 {
return core.Terminator
}
// Partition into 128 subtree roots (64 child indices × 2 sides).
buckets := partitionByChildIndex(skvs)
var roots [128]core.Node
for ci := range 64 {
if len(buckets[ci]) == 0 {
continue
}
var leftKVs, rightKVs []core.StemKeyValue
for i := range buckets[ci] {
if (buckets[ci][i].Stem[0]>>1)&1 == 0 {
leftKVs = append(leftKVs, buckets[ci][i])
} else {
rightKVs = append(rightKVs, buckets[ci][i])
}
}
if len(leftKVs) > 0 {
roots[ci*2] = core.BuildInternalTree(7, leftKVs, func(_ core.WriteNode) {})
}
if len(rightKVs) > 0 {
roots[ci*2+1] = core.BuildInternalTree(7, rightKVs, func(_ core.WriteNode) {})
}
}
// Hash up 7 levels: 128 → 64 → 32 → 16 → 8 → 4 → 2 → 1.
nodes := make([]core.Node, 128)
copy(nodes, roots[:])
for len(nodes) > 1 {
half := len(nodes) / 2
next := make([]core.Node, half)
for i := range half {
left := nodes[i*2]
right := nodes[i*2+1]
if core.IsTerminator(&left) && core.IsTerminator(&right) {
next[i] = core.Terminator
} else {
next[i] = core.HashInternal(&core.InternalData{Left: left, Right: right})
}
}
nodes = next
}
return nodes[0]
}
func TestParallelUpdateEmpty(t *testing.T) { func TestParallelUpdateEmpty(t *testing.T) {
out := ParallelUpdate(core.Terminator, nil, 4, memoryPageSetFactory) out := ParallelUpdate(core.Terminator, nil, 4, memoryPageSetFactory)
assert.Equal(t, core.Terminator, out.Root) assert.Equal(t, core.Terminator, out.Root)
} }
func TestParallelUpdateSingleKey(t *testing.T) { func TestParallelUpdateSingleKey(t *testing.T) {
kv := core.KeyValue{Key: makeKVKey(0x50), Value: makeKVVal(1)} skv := makeSKV(0x50)
kvs := []core.KeyValue{kv} skvs := []core.StemKeyValue{skv}
out := ParallelUpdate(core.Terminator, kvs, 4, memoryPageSetFactory) out := ParallelUpdate(core.Terminator, skvs, 4, memoryPageSetFactory)
expected := expectedRoot(kvs) expected := expectedWorkerRoot(skvs)
assert.Equal(t, expected, out.Root) assert.Equal(t, expected, out.Root)
} }
func TestParallelUpdateTwoKeysDifferentChildren(t *testing.T) { func TestParallelUpdateTwoKeysDifferentChildren(t *testing.T) {
// 0x00 → child 0, 0x80 → child 32. // 0x00 → child 0, 0x80 → child 32.
kvs := []core.KeyValue{ skvs := []core.StemKeyValue{
{Key: makeKVKey(0x00), Value: makeKVVal(1)}, makeSKV(0x00),
{Key: makeKVKey(0x80), Value: makeKVVal(2)}, makeSKV(0x80),
} }
out := ParallelUpdate(core.Terminator, kvs, 4, memoryPageSetFactory) out := ParallelUpdate(core.Terminator, skvs, 4, memoryPageSetFactory)
expected := expectedRoot(kvs) expected := expectedWorkerRoot(skvs)
assert.Equal(t, expected, out.Root) assert.Equal(t, expected, out.Root)
} }
func TestParallelUpdateSparseChildren(t *testing.T) { func TestParallelUpdateSparseChildren(t *testing.T) {
// Only children 0 and 63 have ops. // Only children 0 and 63 have ops.
kvs := []core.KeyValue{ skvs := []core.StemKeyValue{
{Key: makeKVKey(0x00), Value: makeKVVal(1)}, makeSKV(0x00),
{Key: makeKVKey(0xFC), Value: makeKVVal(2)}, makeSKV(0xFC),
} }
out := ParallelUpdate(core.Terminator, kvs, 4, memoryPageSetFactory) out := ParallelUpdate(core.Terminator, skvs, 4, memoryPageSetFactory)
expected := expectedRoot(kvs) expected := expectedWorkerRoot(skvs)
assert.Equal(t, expected, out.Root) assert.Equal(t, expected, out.Root)
} }
func TestParallelUpdateSingleChild(t *testing.T) { func TestParallelUpdateSingleChild(t *testing.T) {
// All keys land in child 0 (first 6 bits = 000000). // All stems land in child 0 (first 6 bits = 000000).
kvs := []core.KeyValue{ skvs := []core.StemKeyValue{
{Key: makeKVKey(0x00), Value: makeKVVal(1)}, makeSKV(0x00),
{Key: makeKVKey(0x01), Value: makeKVVal(2)}, makeSKV(0x01),
{Key: makeKVKey(0x02), Value: makeKVVal(3)}, makeSKV(0x02),
{Key: makeKVKey(0x03), Value: makeKVVal(4)}, makeSKV(0x03),
} }
sort.Slice(kvs, func(i, j int) bool { return kvLess(&kvs[i], &kvs[j]) }) sort.Slice(skvs, func(i, j int) bool { return skvLess(&skvs[i], &skvs[j]) })
out := ParallelUpdate(core.Terminator, kvs, 4, memoryPageSetFactory) out := ParallelUpdate(core.Terminator, skvs, 4, memoryPageSetFactory)
expected := expectedRoot(kvs) expected := expectedWorkerRoot(skvs)
assert.Equal(t, expected, out.Root) assert.Equal(t, expected, out.Root)
} }
func TestParallelUpdateFallbackSmallBatch(t *testing.T) { func TestParallelUpdateFallbackSmallBatch(t *testing.T) {
// Less than 64 ops → single-threaded fallback. // Less than 64 ops → single-threaded fallback.
kvs := randomKVs(10, 42) skvs := randomSKVs(10, 42)
out := ParallelUpdate(core.Terminator, kvs, 8, memoryPageSetFactory) out := ParallelUpdate(core.Terminator, skvs, 8, memoryPageSetFactory)
expected := expectedRoot(kvs) expected := expectedWorkerRoot(skvs)
assert.Equal(t, expected, out.Root) assert.Equal(t, expected, out.Root)
} }
func TestParallelUpdateDeterministic(t *testing.T) { func TestParallelUpdateDeterministic(t *testing.T) {
kvs := randomKVs(200, 99) skvs := randomSKVs(200, 99)
r1 := ParallelUpdate(core.Terminator, kvs, 4, memoryPageSetFactory).Root r1 := ParallelUpdate(core.Terminator, skvs, 4, memoryPageSetFactory).Root
r2 := ParallelUpdate(core.Terminator, kvs, 4, memoryPageSetFactory).Root r2 := ParallelUpdate(core.Terminator, skvs, 4, memoryPageSetFactory).Root
assert.Equal(t, r1, r2, "same inputs should produce same root") assert.Equal(t, r1, r2, "same inputs should produce same root")
} }
func TestParallelUpdateMatchesSingleThreaded(t *testing.T) { func TestParallelUpdateMatchesSingleThreaded(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
numKVs int numSKVs int
workers int workers int
}{ }{
{"1kv_2w", 1, 2}, {"1skv_2w", 1, 2},
{"10kv_2w", 10, 2}, {"10skv_2w", 10, 2},
{"100kv_2w", 100, 2}, {"100skv_2w", 100, 2},
{"100kv_4w", 100, 4}, {"100skv_4w", 100, 4},
{"100kv_8w", 100, 8}, {"100skv_8w", 100, 8},
{"500kv_4w", 500, 4}, {"500skv_4w", 500, 4},
{"1000kv_8w", 1000, 8}, {"1000skv_8w", 1000, 8},
} }
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
kvs := randomKVs(tc.numKVs, 12345) skvs := randomSKVs(tc.numSKVs, 12345)
single := singleThreadedUpdate( single := singleThreadedUpdate(
core.Terminator, kvs, NewMemoryPageSet(true), core.Terminator, skvs, memoryPageSetFactory(),
) )
parallel := ParallelUpdate( parallel := ParallelUpdate(
core.Terminator, kvs, tc.workers, memoryPageSetFactory, core.Terminator, skvs, tc.workers, memoryPageSetFactory,
) )
assert.Equal(t, single.Root, parallel.Root, assert.Equal(t, single.Root, parallel.Root,
@ -221,36 +274,38 @@ func TestParallelUpdateMatchesSingleThreaded(t *testing.T) {
// --- helpers --- // --- helpers ---
func randomKVs(n int, seed int64) []core.KeyValue { func randomSKVs(n int, seed int64) []core.StemKeyValue {
rng := rand.New(rand.NewSource(seed)) rng := rand.New(rand.NewSource(seed))
kvs := make([]core.KeyValue, n) skvs := make([]core.StemKeyValue, n)
seen := make(map[core.KeyPath]bool, n) seen := make(map[core.StemPath]bool, n)
for i := range n { for i := range n {
for { for {
var kp core.KeyPath var stem core.StemPath
rng.Read(kp[:]) rng.Read(stem[:])
if seen[kp] { if seen[stem] {
continue continue
} }
seen[kp] = true seen[stem] = true
var vh core.ValueHash var hash core.Node
rng.Read(vh[:]) rng.Read(hash[:])
kvs[i] = core.KeyValue{Key: kp, Value: vh} // Ensure non-zero hash (avoid terminator).
hash[0] |= 0x01
skvs[i] = core.StemKeyValue{Stem: stem, Hash: hash}
break break
} }
} }
sort.Slice(kvs, func(i, j int) bool { return kvLess(&kvs[i], &kvs[j]) }) sort.Slice(skvs, func(i, j int) bool { return skvLess(&skvs[i], &skvs[j]) })
return kvs return skvs
} }
func kvLess(a, b *core.KeyValue) bool { func skvLess(a, b *core.StemKeyValue) bool {
for i := range a.Key { for i := range a.Stem {
if a.Key[i] < b.Key[i] { if a.Stem[i] < b.Stem[i] {
return true return true
} }
if a.Key[i] > b.Key[i] { if a.Stem[i] > b.Stem[i] {
return false return false
} }
} }