mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-20 21:54:30 +00:00
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:
parent
556d6160df
commit
4a2a10ca7d
3 changed files with 581 additions and 21 deletions
505
trie/nomttrie/compat_test.go
Normal file
505
trie/nomttrie/compat_test.go
Normal 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")
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue