nomt: Phase F — cross-validation tests proving NOMT root == bintrie root

Two root causes of hash mismatch fixed:

1. Canonical root computation: Hash() now uses BuildInternalTree(skip=0)
   over all known stems instead of the page tree's depth-7 internal root.
   The page tree is still updated for persistent storage, but the canonical
   root bypasses its depth-7 worker split that added extra wrapping levels.

2. Code chunk grouping: UpdateContractCode now matches bintrie's group-based
   key derivation exactly — computing the stem key only at group boundaries
   and using groupOffset as the suffix, instead of computing a separate
   GetBinaryTreeKey per chunk (which produced different stems).

Cross-validation tests (compat_test.go) assert strict equality:
- TestSingleAccountRootMatch
- TestMultiAccountRootMatch
- TestStorageRootMatch
- TestCodeChunkRootMatch
- TestMixedOpsRootMatch
- 4 BuildInternalTree vs bintrie diagnostic tests

48 tests passing, race-detector clean.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
weiihann 2026-02-12 23:42:04 +08:00
parent 556d6160df
commit 4a2a10ca7d
3 changed files with 581 additions and 21 deletions

View file

@ -0,0 +1,505 @@
package nomttrie
import (
"encoding/binary"
"sort"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/nomt/core"
"github.com/ethereum/go-ethereum/trie/bintrie"
"github.com/ethereum/go-ethereum/triedb"
"github.com/ethereum/go-ethereum/triedb/nomtdb"
"github.com/holiman/uint256"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// newBintrie creates a fresh in-memory BinaryTrie for testing.
func newBintrie(t *testing.T) *bintrie.BinaryTrie {
t.Helper()
diskdb := rawdb.NewMemoryDatabase()
trieDB := triedb.NewDatabase(diskdb, nil)
t.Cleanup(func() { trieDB.Close() })
bt, err := bintrie.NewBinaryTrie(types.EmptyRootHash, trieDB)
require.NoError(t, err)
return bt
}
// newNomtTrieForCompat creates a NomtTrie with in-memory ethdb and temp Bitbox.
func newNomtTrieForCompat(t *testing.T) *NomtTrie {
t.Helper()
diskdb := rawdb.NewMemoryDatabase()
backend := nomtdb.New(diskdb, &nomtdb.Config{
DataDir: t.TempDir(),
HTCapacity: 1 << 16,
})
t.Cleanup(func() { backend.Close() })
tr, err := New(common.Hash{}, backend)
require.NoError(t, err)
return tr
}
// TestSingleAccountRootMatch verifies that a single account produces
// the same state root on both BinaryTrie and NomtTrie.
func TestSingleAccountRootMatch(t *testing.T) {
addr := common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
acc := &types.StateAccount{
Nonce: 42,
Balance: uint256.NewInt(1_000_000),
CodeHash: common.FromHex("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"),
}
// BinaryTrie path.
bt := newBintrie(t)
require.NoError(t, bt.UpdateAccount(addr, acc, 0))
binRoot := bt.Hash()
// NomtTrie path.
nt := newNomtTrieForCompat(t)
require.NoError(t, nt.UpdateAccount(addr, acc, 0))
nomtRoot := nt.Hash()
t.Logf("bintrie root: %x", binRoot)
t.Logf("nomt root: %x", nomtRoot)
assert.NotEqual(t, common.Hash{}, binRoot)
assert.NotEqual(t, common.Hash{}, nomtRoot)
assert.Equal(t, binRoot, nomtRoot, "single-account root must match bintrie")
}
// TestMultiAccountRootMatch tests whether multiple accounts produce
// matching roots between the two trie implementations.
func TestMultiAccountRootMatch(t *testing.T) {
addrs := []common.Address{
common.HexToAddress("0x1111111111111111111111111111111111111111"),
common.HexToAddress("0x2222222222222222222222222222222222222222"),
common.HexToAddress("0x3333333333333333333333333333333333333333"),
}
bt := newBintrie(t)
nt := newNomtTrieForCompat(t)
for i, addr := range addrs {
acc := &types.StateAccount{
Nonce: uint64(i + 1),
Balance: uint256.NewInt(uint64((i + 1) * 1000)),
CodeHash: make([]byte, 32),
}
require.NoError(t, bt.UpdateAccount(addr, acc, 0))
require.NoError(t, nt.UpdateAccount(addr, acc, 0))
}
binRoot := bt.Hash()
nomtRoot := nt.Hash()
t.Logf("bintrie root: %x", binRoot)
t.Logf("nomt root: %x", nomtRoot)
assert.NotEqual(t, common.Hash{}, binRoot)
assert.NotEqual(t, common.Hash{}, nomtRoot)
assert.Equal(t, binRoot, nomtRoot, "multi-account root must match bintrie")
}
// TestStorageRootMatch tests storage slot updates on both tries.
func TestStorageRootMatch(t *testing.T) {
addr := common.HexToAddress("0xaaaa")
acc := &types.StateAccount{
Nonce: 1,
Balance: uint256.NewInt(100),
CodeHash: make([]byte, 32),
}
slot := common.Hex2Bytes(
"0000000000000000000000000000000000000000000000000000000000000001",
)
val := common.Hex2Bytes("ff")
bt := newBintrie(t)
require.NoError(t, bt.UpdateAccount(addr, acc, 0))
require.NoError(t, bt.UpdateStorage(addr, slot, val))
binRoot := bt.Hash()
nt := newNomtTrieForCompat(t)
require.NoError(t, nt.UpdateAccount(addr, acc, 0))
require.NoError(t, nt.UpdateStorage(addr, slot, val))
nomtRoot := nt.Hash()
t.Logf("bintrie root: %x", binRoot)
t.Logf("nomt root: %x", nomtRoot)
assert.NotEqual(t, common.Hash{}, binRoot)
assert.NotEqual(t, common.Hash{}, nomtRoot)
assert.Equal(t, binRoot, nomtRoot, "storage root must match bintrie")
}
// TestCodeChunkRootMatch tests contract code updates on both tries.
func TestCodeChunkRootMatch(t *testing.T) {
addr := common.HexToAddress("0xbbbb")
acc := &types.StateAccount{
Nonce: 1,
Balance: uint256.NewInt(0),
CodeHash: common.FromHex("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"),
}
code := make([]byte, 100)
for i := range code {
code[i] = byte(i)
}
bt := newBintrie(t)
require.NoError(t, bt.UpdateAccount(addr, acc, len(code)))
require.NoError(t, bt.UpdateContractCode(addr, common.Hash{}, code))
binRoot := bt.Hash()
nt := newNomtTrieForCompat(t)
require.NoError(t, nt.UpdateAccount(addr, acc, len(code)))
require.NoError(t, nt.UpdateContractCode(addr, common.Hash{}, code))
nomtRoot := nt.Hash()
t.Logf("bintrie root: %x", binRoot)
t.Logf("nomt root: %x", nomtRoot)
assert.NotEqual(t, common.Hash{}, binRoot)
assert.NotEqual(t, common.Hash{}, nomtRoot)
assert.Equal(t, binRoot, nomtRoot, "code chunk root must match bintrie")
}
// TestNomtTrieDeterministic verifies that the same operations always
// produce the same root hash in NomtTrie.
func TestNomtTrieDeterministic(t *testing.T) {
makeAndHash := func() common.Hash {
tr := newNomtTrieForCompat(t)
addr := common.HexToAddress("0x1234")
acc := &types.StateAccount{
Nonce: 7,
Balance: uint256.NewInt(42),
CodeHash: make([]byte, 32),
}
require.NoError(t, tr.UpdateAccount(addr, acc, 0))
return tr.Hash()
}
root1 := makeAndHash()
root2 := makeAndHash()
assert.Equal(t, root1, root2, "same operations must produce same root")
}
// TestNomtTrieRootChangesOnUpdate verifies that different state changes
// produce different roots.
func TestNomtTrieRootChangesOnUpdate(t *testing.T) {
addr := common.HexToAddress("0x5678")
tr1 := newNomtTrieForCompat(t)
acc1 := &types.StateAccount{
Nonce: 1,
Balance: uint256.NewInt(100),
CodeHash: make([]byte, 32),
}
require.NoError(t, tr1.UpdateAccount(addr, acc1, 0))
root1 := tr1.Hash()
tr2 := newNomtTrieForCompat(t)
acc2 := &types.StateAccount{
Nonce: 2, // different nonce
Balance: uint256.NewInt(100),
CodeHash: make([]byte, 32),
}
require.NoError(t, tr2.UpdateAccount(addr, acc2, 0))
root2 := tr2.Hash()
assert.NotEqual(t, root1, root2,
"different state should produce different roots")
}
// TestNomtTrieSequentialConsistency applies two blocks of changes and
// verifies the final root is consistent.
func TestNomtTrieSequentialConsistency(t *testing.T) {
tr := newNomtTrieForCompat(t)
addr := common.HexToAddress("0xabcd")
acc := &types.StateAccount{
Nonce: 1,
Balance: uint256.NewInt(1000),
CodeHash: make([]byte, 32),
}
// Block 1.
require.NoError(t, tr.UpdateAccount(addr, acc, 0))
root1 := tr.Hash()
assert.NotEqual(t, common.Hash{}, root1)
// Block 2: update balance.
acc.Nonce = 2
acc.Balance = uint256.NewInt(2000)
require.NoError(t, tr.UpdateAccount(addr, acc, 0))
root2 := tr.Hash()
assert.NotEqual(t, common.Hash{}, root2)
assert.NotEqual(t, root1, root2)
// Re-reading the account should show updated values.
got, err := tr.GetAccount(addr)
require.NoError(t, err)
require.NotNil(t, got)
assert.Equal(t, uint64(2), got.Nonce)
assert.Equal(t, uint64(2000), got.Balance.Uint64())
}
// TestMixedOpsRootMatch performs account, storage, and code updates on
// both tries and compares results.
func TestMixedOpsRootMatch(t *testing.T) {
addr := common.HexToAddress("0xffff")
acc := &types.StateAccount{
Nonce: 10,
Balance: uint256.NewInt(5000),
CodeHash: common.FromHex("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"),
}
code := []byte{0x60, 0x00, 0x60, 0x00, 0xFD}
slot := make([]byte, 32)
slot[31] = 1
val := make([]byte, 32)
val[31] = 0x42
bt := newBintrie(t)
require.NoError(t, bt.UpdateAccount(addr, acc, len(code)))
require.NoError(t, bt.UpdateStorage(addr, slot, val))
require.NoError(t, bt.UpdateContractCode(addr, common.Hash{}, code))
binRoot := bt.Hash()
nt := newNomtTrieForCompat(t)
require.NoError(t, nt.UpdateAccount(addr, acc, len(code)))
require.NoError(t, nt.UpdateStorage(addr, slot, val))
require.NoError(t, nt.UpdateContractCode(addr, common.Hash{}, code))
nomtRoot := nt.Hash()
t.Logf("bintrie root: %x", binRoot)
t.Logf("nomt root: %x", nomtRoot)
assert.NotEqual(t, common.Hash{}, binRoot)
assert.NotEqual(t, common.Hash{}, nomtRoot)
assert.Equal(t, binRoot, nomtRoot, "mixed-ops root must match bintrie")
}
// TestBintrieRawInsertRootVector validates the known bintrie test vectors
// to confirm our understanding of the expected hashing.
func TestBintrieRawInsertRootVector(t *testing.T) {
// This test directly uses bintrie's low-level Insert API to verify
// known test vectors from trie/bintrie/trie_test.go.
tree := bintrie.NewBinaryNode()
zeroKey := [bintrie.HashSize]byte{}
oneKey := common.HexToHash(
"0101010101010101010101010101010101010101010101010101010101010101",
)
var err error
tree, err = tree.Insert(zeroKey[:], oneKey[:], nil, 0)
require.NoError(t, err)
expected := common.HexToHash(
"aab1060e04cb4f5dc6f697ae93156a95714debbf77d54238766adc5709282b6f",
)
assert.Equal(t, expected, tree.Hash(),
"single entry root should match known test vector")
}
// TestBintrieMerkleizeVector validates the 4-entry merkle test vector.
func TestBintrieMerkleizeVector(t *testing.T) {
tree := bintrie.NewBinaryNode()
keys := [][]byte{
common.HexToHash("0000000000000000000000000000000000000000000000000000000000000000").Bytes(),
common.HexToHash("8000000000000000000000000000000000000000000000000000000000000000").Bytes(),
common.HexToHash("0100000000000000000000000000000000000000000000000000000000000000").Bytes(),
common.HexToHash("8100000000000000000000000000000000000000000000000000000000000000").Bytes(),
}
for i, key := range keys {
var v [bintrie.HashSize]byte
binary.LittleEndian.PutUint64(v[:8], uint64(i))
var err error
tree, err = tree.Insert(key, v[:], nil, 0)
require.NoError(t, err)
}
expected := common.HexToHash(
"9317155862f7a3867660ddd0966ff799a3d16aa4df1e70a7516eaa4a675191b5",
)
assert.Equal(t, expected, tree.Hash(),
"4-entry merkle root should match known test vector")
}
// buildInternalTreeRoot computes the root hash using BuildInternalTree at
// skip=0, bypassing the depth-7 page walker split.
func buildInternalTreeRoot(kvs []core.StemKeyValue) core.Node {
sort.Slice(kvs, func(i, j int) bool {
return kvs[i].Stem != kvs[j].Stem && stemLess(&kvs[i].Stem, &kvs[j].Stem)
})
return core.BuildInternalTree(0, kvs, func(_ core.WriteNode) {})
}
// TestBuildInternalTreeSingleStemMatchesBintrie verifies that
// BuildInternalTree(skip=0) with a single stem produces the same root
// as bintrie's InsertValuesAtStem.
func TestBuildInternalTreeSingleStemMatchesBintrie(t *testing.T) {
var stem core.StemPath
stem[0] = 0xAA
stem[1] = 0xBB
var values [core.StemNodeWidth][]byte
values[0] = make([]byte, 32)
values[0][0] = 0x42
values[1] = make([]byte, 32)
values[1][31] = 0xFF
// NOMT path: compute stem hash, then BuildInternalTree at skip=0.
stemHash := core.HashStem(stem, values)
nomtRoot := buildInternalTreeRoot([]core.StemKeyValue{
{Stem: stem, Hash: stemHash},
})
// Bintrie path: InsertValuesAtStem on an empty tree.
tree := bintrie.NewBinaryNode()
var binValues [bintrie.StemNodeWidth][]byte
for i, v := range values {
if v != nil {
binValues[i] = v
}
}
var err error
tree, err = tree.InsertValuesAtStem(stem[:], binValues[:], nil, 0)
require.NoError(t, err)
binRoot := tree.Hash()
t.Logf("BuildInternalTree root: %x", nomtRoot)
t.Logf("bintrie root: %x", binRoot)
assert.Equal(t, binRoot, common.Hash(nomtRoot),
"BuildInternalTree(skip=0) should match bintrie for a single stem")
}
// TestBuildInternalTreeTwoStemsMatchesBintrie verifies root match with two
// stems that diverge early (bit 0).
func TestBuildInternalTreeTwoStemsMatchesBintrie(t *testing.T) {
var stemA, stemB core.StemPath
stemA[0] = 0x00 // bit 0 = 0
stemB[0] = 0x80 // bit 0 = 1
valA := make([]byte, 32)
valA[0] = 0x11
valB := make([]byte, 32)
valB[0] = 0x22
var valsA, valsB [core.StemNodeWidth][]byte
valsA[0] = valA
valsB[0] = valB
hashA := core.HashStem(stemA, valsA)
hashB := core.HashStem(stemB, valsB)
nomtRoot := buildInternalTreeRoot([]core.StemKeyValue{
{Stem: stemA, Hash: hashA},
{Stem: stemB, Hash: hashB},
})
// Bintrie: insert both stems.
tree := bintrie.NewBinaryNode()
var err error
tree, err = tree.InsertValuesAtStem(stemA[:], valsA[:], nil, 0)
require.NoError(t, err)
tree, err = tree.InsertValuesAtStem(stemB[:], valsB[:], nil, 0)
require.NoError(t, err)
binRoot := tree.Hash()
t.Logf("BuildInternalTree root: %x", nomtRoot)
t.Logf("bintrie root: %x", binRoot)
assert.Equal(t, binRoot, common.Hash(nomtRoot),
"BuildInternalTree(skip=0) should match bintrie for two diverging stems")
}
// TestBuildInternalTreeLongPrefixMatchesBintrie verifies root match with two
// stems sharing a long common prefix (bits 0-7 identical, diverge at bit 8).
func TestBuildInternalTreeLongPrefixMatchesBintrie(t *testing.T) {
var stemA, stemB core.StemPath
stemA[0] = 0xAA // 10101010
stemA[1] = 0x00 // bit 8 = 0
stemB[0] = 0xAA // 10101010 (same first byte)
stemB[1] = 0x80 // bit 8 = 1
valA := make([]byte, 32)
valA[0] = 0x33
valB := make([]byte, 32)
valB[0] = 0x44
var valsA, valsB [core.StemNodeWidth][]byte
valsA[0] = valA
valsB[0] = valB
hashA := core.HashStem(stemA, valsA)
hashB := core.HashStem(stemB, valsB)
nomtRoot := buildInternalTreeRoot([]core.StemKeyValue{
{Stem: stemA, Hash: hashA},
{Stem: stemB, Hash: hashB},
})
tree := bintrie.NewBinaryNode()
var err error
tree, err = tree.InsertValuesAtStem(stemA[:], valsA[:], nil, 0)
require.NoError(t, err)
tree, err = tree.InsertValuesAtStem(stemB[:], valsB[:], nil, 0)
require.NoError(t, err)
binRoot := tree.Hash()
t.Logf("BuildInternalTree root: %x", nomtRoot)
t.Logf("bintrie root: %x", binRoot)
assert.Equal(t, binRoot, common.Hash(nomtRoot),
"BuildInternalTree(skip=0) should match bintrie for stems with long shared prefix")
}
// TestBuildInternalTreeFourStemsMatchesBintrie validates the 4-stem case
// using the same keys as TestBintrieMerkleizeVector.
func TestBuildInternalTreeFourStemsMatchesBintrie(t *testing.T) {
keys := [][32]byte{
common.HexToHash("0000000000000000000000000000000000000000000000000000000000000000"),
common.HexToHash("8000000000000000000000000000000000000000000000000000000000000000"),
common.HexToHash("0100000000000000000000000000000000000000000000000000000000000000"),
common.HexToHash("8100000000000000000000000000000000000000000000000000000000000000"),
}
tree := bintrie.NewBinaryNode()
var kvs []core.StemKeyValue
for i, key := range keys {
var v [bintrie.HashSize]byte
binary.LittleEndian.PutUint64(v[:8], uint64(i))
// Bintrie: full 32-byte key insert.
var err error
tree, err = tree.Insert(key[:], v[:], nil, 0)
require.NoError(t, err)
// NOMT: stem is first 31 bytes, suffix is byte 31.
var stem core.StemPath
copy(stem[:], key[:31])
var vals [core.StemNodeWidth][]byte
vals[key[31]] = v[:]
kvs = append(kvs, core.StemKeyValue{
Stem: stem,
Hash: core.HashStem(stem, vals),
})
}
binRoot := tree.Hash()
nomtRoot := buildInternalTreeRoot(kvs)
t.Logf("bintrie root: %x", binRoot)
t.Logf("BuildInternalTree root: %x", nomtRoot)
assert.Equal(t, binRoot, common.Hash(nomtRoot),
"BuildInternalTree(skip=0) should match bintrie 4-entry merkle vector")
}

View file

@ -8,6 +8,7 @@ package nomttrie
import (
"encoding/binary"
"sort"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
@ -33,21 +34,33 @@ type stemUpdate struct {
// NomtTrie implements the state.Trie interface using NOMT's page-based binary
// merkle trie. It accumulates stem updates during block execution and flushes
// them to the NOMT engine on Hash()/Commit().
//
// The canonical root hash is computed via BuildInternalTree(skip=0) over all
// stems, producing roots identical to bintrie. The NOMT page tree is updated
// separately for persistent storage (its internal root may differ due to the
// depth-7 worker split, but the canonical root returned by Hash() matches
// bintrie exactly).
type NomtTrie struct {
nomtDB *db.DB // NOMT trie engine (page storage + walker)
backend *nomtdb.Database // NOMT triedb backend (for flat state access)
root common.Hash // current trie root
root common.Hash // current canonical trie root
pending []stemUpdate // accumulated stem updates
dirty bool // whether pending updates exist
// allStems tracks the stem hash for every active stem in the trie.
// Updated on each Hash() with results from groupAndHashStems.
// Used to compute the canonical root via BuildInternalTree(skip=0).
allStems map[core.StemPath]core.Node
}
// New creates a new NomtTrie. The root parameter is the current state root.
func New(root common.Hash, backend *nomtdb.Database) (*NomtTrie, error) {
return &NomtTrie{
nomtDB: backend.NomtDB(),
backend: backend,
root: root,
pending: make([]stemUpdate, 0, 64),
nomtDB: backend.NomtDB(),
backend: backend,
root: root,
pending: make([]stemUpdate, 0, 64),
allStems: make(map[core.StemPath]core.Node, 64),
}, nil
}
@ -176,16 +189,25 @@ func (t *NomtTrie) DeleteStorage(addr common.Address, key []byte) error {
}
// UpdateContractCode chunks the bytecode using EIP-7864's ChunkifyCode and
// queues stem updates for each chunk at offset 128+.
// queues stem updates for each chunk, matching bintrie's group-based key
// derivation exactly. Chunks are grouped into stems of 256 slots; the stem
// key is computed only at group boundaries, and groupOffset is the suffix.
func (t *NomtTrie) UpdateContractCode(addr common.Address, _ common.Hash, code []byte) error {
chunks := bintrie.ChunkifyCode(code)
var stem core.StemPath
for i, chunknr := 0, uint64(0); i < len(chunks); i, chunknr = i+bintrie.HashSize, chunknr+1 {
stem, suffix := codeChunkStemAndSuffix(addr, chunknr)
groupOffset := byte((chunknr + 128) % bintrie.StemNodeWidth)
if groupOffset == 0 || chunknr == 0 {
var offset [bintrie.HashSize]byte
binary.LittleEndian.PutUint64(offset[24:], chunknr+128)
key := bintrie.GetBinaryTreeKey(addr, offset[:])
copy(stem[:], key[:core.StemSize])
}
val := make([]byte, bintrie.HashSize)
copy(val, chunks[i:i+bintrie.HashSize])
t.pending = append(t.pending, stemUpdate{
Stem: stem,
Suffix: suffix,
Suffix: groupOffset,
Value: val,
})
}
@ -197,6 +219,10 @@ func (t *NomtTrie) UpdateContractCode(addr common.Address, _ common.Hash, code [
// Hash flushes pending updates to flat state and the NOMT page tree,
// returning the new trie root hash.
//
// The canonical root is computed via BuildInternalTree(skip=0) over all known
// stems, producing roots identical to bintrie (EIP-7864). The NOMT page tree
// is also updated for persistent storage.
func (t *NomtTrie) Hash() common.Hash {
if !t.dirty {
return t.root
@ -208,20 +234,45 @@ func (t *NomtTrie) Hash() common.Hash {
return t.root
}
// Update allStems with new/changed stem hashes.
for _, kv := range stemKVs {
t.allStems[kv.Stem] = kv.Hash
}
// Update the page tree for persistent storage.
if len(stemKVs) > 0 {
newRoot, err := t.nomtDB.Update(stemKVs)
if err != nil {
if _, err := t.nomtDB.Update(stemKVs); err != nil {
log.Error("NOMT page tree update failed", "err", err)
return t.root
}
t.root = common.Hash(newRoot)
}
// Compute the canonical root via BuildInternalTree(skip=0).
// This produces roots identical to bintrie by avoiding the depth-7
// worker split that adds extra wrapping levels.
t.root = common.Hash(t.canonicalRoot())
t.pending = t.pending[:0]
t.dirty = false
return t.root
}
// canonicalRoot computes the bintrie-compatible root hash from all known stems
// using BuildInternalTree at skip=0.
func (t *NomtTrie) canonicalRoot() core.Node {
if len(t.allStems) == 0 {
return core.Terminator
}
sorted := make([]core.StemKeyValue, 0, len(t.allStems))
for stem, hash := range t.allStems {
sorted = append(sorted, core.StemKeyValue{Stem: stem, Hash: hash})
}
sort.Slice(sorted, func(i, j int) bool {
return stemLess(&sorted[i].Stem, &sorted[j].Stem)
})
return core.BuildInternalTree(0, sorted, func(_ core.WriteNode) {})
}
// Commit flushes pending operations and returns the root hash.
func (t *NomtTrie) Commit(_ bool) (common.Hash, *trienode.NodeSet) {
root := t.Hash()
@ -253,11 +304,16 @@ func (t *NomtTrie) IsVerkle() bool {
func (t *NomtTrie) Copy() *NomtTrie {
pending := make([]stemUpdate, len(t.pending))
copy(pending, t.pending)
allStems := make(map[core.StemPath]core.Node, len(t.allStems))
for k, v := range t.allStems {
allStems[k] = v
}
return &NomtTrie{
nomtDB: t.nomtDB,
backend: t.backend,
root: t.root,
pending: pending,
dirty: t.dirty,
nomtDB: t.nomtDB,
backend: t.backend,
root: t.root,
pending: pending,
dirty: t.dirty,
allStems: allStems,
}
}

View file

@ -346,10 +346,9 @@ func TestHashProducesCorrectStemHash(t *testing.T) {
values[bintrie.CodeHashLeafKey] = codeHashVal
stemHash := core.HashStem(stem, values)
// The trie has one stem → the root is the stem hash placed at depth 248,
// surrounded by terminators. The exact root depends on the page tree
// hashing, but it should be deterministic.
// A single-stem trie's canonical root equals the stem hash directly,
// matching bintrie's behavior (StemNode hash IS the root).
assert.NotEqual(t, common.Hash{}, root)
assert.NotEqual(t, common.Hash(stemHash), root,
"root != stemHash because the page tree hashes internal nodes above the stem")
assert.Equal(t, common.Hash(stemHash), root,
"single-stem trie root should equal the stem hash")
}