go-ethereum/nomt/merkle/worker_test.go
2026-03-09 21:19:27 +08:00

313 lines
8.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package merkle
import (
"math/rand"
"sort"
"testing"
"github.com/ethereum/go-ethereum/nomt/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- Unit tests for helpers ---
func TestPartitionByChildIndex(t *testing.T) {
// Stem 0x00... → child 0, stem 0x04... → child 1, stem 0xFC... → child 63.
skvs := []core.StemKeyValue{
makeSKV(0x00),
makeSKV(0x04), // 0x04 >> 2 = 1
makeSKV(0xFC), // 0xFC >> 2 = 63
}
buckets := partitionByChildIndex(skvs)
assert.Len(t, buckets[0], 1)
assert.Len(t, buckets[1], 1)
assert.Len(t, buckets[63], 1)
// All other buckets should be empty.
nonEmpty := 0
for _, b := range buckets {
if len(b) > 0 {
nonEmpty++
}
}
assert.Equal(t, 3, nonEmpty)
}
func TestChildPosition(t *testing.T) {
// Child 0, left: 7 bits all false → depth 7.
pos := childPosition(0, false)
assert.Equal(t, uint16(7), pos.Depth())
for i := range 7 {
assert.False(t, pos.Bit(i), "bit %d should be 0", i)
}
// Child 0, right: 6 false + 1 true → depth 7.
pos = childPosition(0, true)
assert.Equal(t, uint16(7), pos.Depth())
for i := range 6 {
assert.False(t, pos.Bit(i), "bit %d should be 0", i)
}
assert.True(t, pos.Bit(6))
// Child 63 (0b111111), left: 6 true + 1 false → depth 7.
pos = childPosition(63, false)
assert.Equal(t, uint16(7), pos.Depth())
for i := range 6 {
assert.True(t, pos.Bit(i), "bit %d should be 1", i)
}
assert.False(t, pos.Bit(6))
// Child 63 (0b111111), right: 7 true → depth 7.
pos = childPosition(63, true)
assert.Equal(t, uint16(7), pos.Depth())
for i := range 7 {
assert.True(t, pos.Bit(i), "bit %d should be 1", i)
}
}
func TestAssignToWorkers(t *testing.T) {
// 3 non-empty buckets, 2 workers.
var buckets [64][]core.StemKeyValue
buckets[0] = []core.StemKeyValue{makeSKV(0x00)}
buckets[10] = []core.StemKeyValue{makeSKV(0x28)} // 0x28>>2=10
buckets[63] = []core.StemKeyValue{makeSKV(0xFC)}
tasks := assignToWorkers(buckets, 2)
require.Len(t, tasks, 2)
// 3 items / 2 workers: first gets 2, second gets 1.
assert.Len(t, tasks[0].children, 2)
assert.Len(t, tasks[1].children, 1)
assert.Equal(t, uint8(0), tasks[0].children[0].childIndex)
assert.Equal(t, uint8(10), tasks[0].children[1].childIndex)
assert.Equal(t, uint8(63), tasks[1].children[0].childIndex)
}
func TestAssignToWorkersMoreWorkersThanChildren(t *testing.T) {
var buckets [64][]core.StemKeyValue
buckets[5] = []core.StemKeyValue{makeSKV(0x14)} // 0x14>>2=5
buckets[6] = []core.StemKeyValue{makeSKV(0x18)} // 0x18>>2=6
tasks := assignToWorkers(buckets, 8)
// Only 2 non-empty, so cap to 2 workers.
require.Len(t, tasks, 2)
assert.Len(t, tasks[0].children, 1)
assert.Len(t, tasks[1].children, 1)
}
// --- Integration tests ---
// permissivePageSet wraps MemoryPageSet to return fresh pages for missing
// entries (matching pebblePageSet behavior). This is needed because the
// parallel workers descend into child pages that may not exist yet.
type permissivePageSet struct {
*MemoryPageSet
}
func (ps *permissivePageSet) Get(pageID core.PageID) (*core.RawPage, PageOrigin, bool) {
page, origin, ok := ps.MemoryPageSet.Get(pageID)
if !ok {
fresh := new(core.RawPage)
return fresh, PageOrigin{Kind: PageOriginFresh}, true
}
return page, origin, true
}
func memoryPageSetFactory() PageSet {
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) {
out := ParallelUpdate(core.Terminator, nil, 4, memoryPageSetFactory)
assert.Equal(t, core.Terminator, out.Root)
}
func TestParallelUpdateSingleKey(t *testing.T) {
skv := makeSKV(0x50)
skvs := []core.StemKeyValue{skv}
out := ParallelUpdate(core.Terminator, skvs, 4, memoryPageSetFactory)
expected := expectedWorkerRoot(skvs)
assert.Equal(t, expected, out.Root)
}
func TestParallelUpdateTwoKeysDifferentChildren(t *testing.T) {
// 0x00 → child 0, 0x80 → child 32.
skvs := []core.StemKeyValue{
makeSKV(0x00),
makeSKV(0x80),
}
out := ParallelUpdate(core.Terminator, skvs, 4, memoryPageSetFactory)
expected := expectedWorkerRoot(skvs)
assert.Equal(t, expected, out.Root)
}
func TestParallelUpdateSparseChildren(t *testing.T) {
// Only children 0 and 63 have ops.
skvs := []core.StemKeyValue{
makeSKV(0x00),
makeSKV(0xFC),
}
out := ParallelUpdate(core.Terminator, skvs, 4, memoryPageSetFactory)
expected := expectedWorkerRoot(skvs)
assert.Equal(t, expected, out.Root)
}
func TestParallelUpdateSingleChild(t *testing.T) {
// All stems land in child 0 (first 6 bits = 000000).
skvs := []core.StemKeyValue{
makeSKV(0x00),
makeSKV(0x01),
makeSKV(0x02),
makeSKV(0x03),
}
sort.Slice(skvs, func(i, j int) bool { return skvLess(&skvs[i], &skvs[j]) })
out := ParallelUpdate(core.Terminator, skvs, 4, memoryPageSetFactory)
expected := expectedWorkerRoot(skvs)
assert.Equal(t, expected, out.Root)
}
func TestParallelUpdateFallbackSmallBatch(t *testing.T) {
// Less than 64 ops → single-threaded fallback.
skvs := randomSKVs(10, 42)
out := ParallelUpdate(core.Terminator, skvs, 8, memoryPageSetFactory)
expected := expectedWorkerRoot(skvs)
assert.Equal(t, expected, out.Root)
}
func TestParallelUpdateDeterministic(t *testing.T) {
skvs := randomSKVs(200, 99)
r1 := ParallelUpdate(core.Terminator, skvs, 4, memoryPageSetFactory).Root
r2 := ParallelUpdate(core.Terminator, skvs, 4, memoryPageSetFactory).Root
assert.Equal(t, r1, r2, "same inputs should produce same root")
}
func TestParallelUpdateMatchesSingleThreaded(t *testing.T) {
tests := []struct {
name string
numSKVs int
workers int
}{
{"1skv_2w", 1, 2},
{"10skv_2w", 10, 2},
{"100skv_2w", 100, 2},
{"100skv_4w", 100, 4},
{"100skv_8w", 100, 8},
{"500skv_4w", 500, 4},
{"1000skv_8w", 1000, 8},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
skvs := randomSKVs(tc.numSKVs, 12345)
single := singleThreadedUpdate(
core.Terminator, skvs, memoryPageSetFactory(),
)
parallel := ParallelUpdate(
core.Terminator, skvs, tc.workers, memoryPageSetFactory,
)
assert.Equal(t, single.Root, parallel.Root,
"parallel root should match single-threaded root")
})
}
}
// --- helpers ---
func randomSKVs(n int, seed int64) []core.StemKeyValue {
rng := rand.New(rand.NewSource(seed))
skvs := make([]core.StemKeyValue, n)
seen := make(map[core.StemPath]bool, n)
for i := range n {
for {
var stem core.StemPath
rng.Read(stem[:])
if seen[stem] {
continue
}
seen[stem] = true
var hash core.Node
rng.Read(hash[:])
// Ensure non-zero hash (avoid terminator).
hash[0] |= 0x01
skvs[i] = core.StemKeyValue{Stem: stem, Hash: hash}
break
}
}
sort.Slice(skvs, func(i, j int) bool { return skvLess(&skvs[i], &skvs[j]) })
return skvs
}
func skvLess(a, b *core.StemKeyValue) bool {
for i := range a.Stem {
if a.Stem[i] < b.Stem[i] {
return true
}
if a.Stem[i] > b.Stem[i] {
return false
}
}
return false
}