go-ethereum/trie/nomttrie/compat_test.go
2026-03-09 21:19:27 +08:00

502 lines
15 KiB
Go

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.
func newNomtTrieForCompat(t *testing.T) *NomtTrie {
t.Helper()
diskdb := rawdb.NewMemoryDatabase()
backend := nomtdb.New(diskdb, nil)
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")
}