trie/nomttrie: add EIP-7864 key derivation and value encoding (Phase B)

Add stem-aware key encoding wrappers delegating to bintrie for identical
SHA256 key derivation. Add packBasicData/packStorageValue matching
bintrie's exact encoding layout. Stub trie.go with stemUpdate type
pending Phase E implementation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
weiihann 2026-02-12 22:30:14 +08:00
parent 1051e7be0c
commit 84fff73c8f
6 changed files with 371 additions and 382 deletions

View file

@ -0,0 +1,48 @@
// Package nomttrie implements a state.Trie backed by the NOMT binary merkle
// trie engine, targeting EIP-7864 compatibility.
//
// Key derivation functions delegate to trie/bintrie to guarantee identical
// key generation. This file provides stem-aware wrappers that split keys
// into the 31-byte stem path and 1-byte suffix used by the NOMT page tree.
package nomttrie
import (
"encoding/binary"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/nomt/core"
"github.com/ethereum/go-ethereum/trie/bintrie"
)
// stemAndSuffix splits a 32-byte EIP-7864 trie key into its 31-byte stem
// and 1-byte suffix. The stem identifies the stem node, and the suffix
// identifies the value slot (0-255) within that stem.
func stemAndSuffix(key []byte) (core.StemPath, byte) {
var stem core.StemPath
copy(stem[:], key[:core.StemSize])
return stem, key[core.StemSize]
}
// accountStem returns the stem path for an account's EIP-7864 key.
// All account-level values (basic data at suffix 0, code hash at suffix 1)
// share the same stem.
func accountStem(addr common.Address) core.StemPath {
stem, _ := stemAndSuffix(bintrie.GetBinaryTreeKeyBasicData(addr))
return stem
}
// storageStemAndSuffix returns the stem path and suffix for a storage slot.
func storageStemAndSuffix(addr common.Address, storageKey []byte) (core.StemPath, byte) {
return stemAndSuffix(bintrie.GetBinaryTreeKeyStorageSlot(addr, storageKey))
}
// codeChunkStemAndSuffix returns the stem path and suffix for a code chunk.
//
// This matches bintrie.UpdateContractCode's key construction (not the buggy
// GetBinaryTreeKeyCodeChunk which passes a variable-length uint256.Bytes()
// to GetBinaryTreeKey).
func codeChunkStemAndSuffix(addr common.Address, chunkNr uint64) (core.StemPath, byte) {
var offset [bintrie.HashSize]byte
binary.LittleEndian.PutUint64(offset[24:], chunkNr+128)
return stemAndSuffix(bintrie.GetBinaryTreeKey(addr, offset[:]))
}

View file

@ -0,0 +1,108 @@
package nomttrie
import (
"encoding/binary"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/nomt/core"
"github.com/ethereum/go-ethereum/trie/bintrie"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStemAndSuffix(t *testing.T) {
key := make([]byte, 32)
for i := range key {
key[i] = byte(i)
}
stem, suffix := stemAndSuffix(key)
// Stem should be first 31 bytes.
for i := range core.StemSize {
assert.Equal(t, byte(i), stem[i])
}
// Suffix should be byte 31.
assert.Equal(t, byte(31), suffix)
}
func TestAccountStemSharedBetweenBasicDataAndCodeHash(t *testing.T) {
addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678")
basicDataKey := bintrie.GetBinaryTreeKeyBasicData(addr)
codeHashKey := bintrie.GetBinaryTreeKeyCodeHash(addr)
// First 31 bytes (stem) should be identical.
assert.Equal(t, basicDataKey[:core.StemSize], codeHashKey[:core.StemSize])
// Suffixes should differ: 0 for basic data, 1 for code hash.
assert.Equal(t, byte(bintrie.BasicDataLeafKey), basicDataKey[core.StemSize])
assert.Equal(t, byte(bintrie.CodeHashLeafKey), codeHashKey[core.StemSize])
}
func TestAccountStemMatchesBintrie(t *testing.T) {
addr := common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
stem := accountStem(addr)
bintrieKey := bintrie.GetBinaryTreeKeyBasicData(addr)
var expectedStem core.StemPath
copy(expectedStem[:], bintrieKey[:core.StemSize])
assert.Equal(t, expectedStem, stem)
}
func TestStorageStemAndSuffix(t *testing.T) {
addr := common.HexToAddress("0xaaaa")
slot := common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000001")
stem, suffix := storageStemAndSuffix(addr, slot)
bintrieKey := bintrie.GetBinaryTreeKeyStorageSlot(addr, slot)
var expectedStem core.StemPath
copy(expectedStem[:], bintrieKey[:core.StemSize])
assert.Equal(t, expectedStem, stem)
assert.Equal(t, bintrieKey[core.StemSize], suffix)
}
func TestStorageHeaderSlotMapsToAccountStem(t *testing.T) {
// Storage keys < 64 live in the account header (same stem as basic data).
addr := common.HexToAddress("0xbbbb")
// Slot 0 is in the header.
headerSlot := make([]byte, 32)
headerSlot[31] = 2 // slot 2 (< 64)
stem, suffix := storageStemAndSuffix(addr, headerSlot)
accountSt := accountStem(addr)
assert.Equal(t, accountSt, stem,
"header storage slot should share stem with account")
assert.Equal(t, byte(64+2), suffix,
"header slot suffix = 64 + slot number")
}
func TestCodeChunkStemAndSuffix(t *testing.T) {
addr := common.HexToAddress("0xcccc")
stem, suffix := codeChunkStemAndSuffix(addr, 5)
// Reproduce the key construction from bintrie.UpdateContractCode.
var offset [bintrie.HashSize]byte
binary.LittleEndian.PutUint64(offset[24:], 5+128)
expectedKey := bintrie.GetBinaryTreeKey(addr, offset[:])
var expectedStem core.StemPath
copy(expectedStem[:], expectedKey[:core.StemSize])
assert.Equal(t, expectedStem, stem)
assert.Equal(t, expectedKey[core.StemSize], suffix)
}
func TestDifferentAddressesProduceDifferentStems(t *testing.T) {
addr1 := common.HexToAddress("0x1111111111111111111111111111111111111111")
addr2 := common.HexToAddress("0x2222222222222222222222222222222222222222")
stem1 := accountStem(addr1)
stem2 := accountStem(addr2)
require.NotEqual(t, stem1, stem2)
}

View file

@ -1,209 +1,142 @@
// Package nomttrie implements a state.Trie backed by the NOMT binary merkle
// trie engine. It accumulates leaf operations during block execution and
// commits them to the NOMT page-based storage on Hash()/Commit().
// trie engine, targeting EIP-7864 compatibility.
//
// Read operations (GetAccount, GetStorage) delegate to geth's ethdb flat state
// since NOMT's trie stores only hashes, not actual values.
// Read operations delegate to geth's ethdb flat state. Write operations
// accumulate stem updates and flush them to the NOMT page tree on Hash()/Commit().
package nomttrie
import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/nomt/core"
"github.com/ethereum/go-ethereum/nomt/db"
"github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/trie/trienode"
"github.com/ethereum/go-ethereum/triedb/nomtdb"
)
// stemUpdate represents a pending value change at a specific (stem, suffix)
// position in the EIP-7864 trie.
type stemUpdate struct {
Stem [31]byte // stem path
Suffix byte // value slot index (0-255)
Value []byte // 32-byte value, nil = delete
}
// NomtTrie implements the state.Trie interface using NOMT's page-based binary
// merkle trie. It accumulates changes as LeafOps during block execution and
// flushes them to the NOMT engine on Hash()/Commit().
// merkle trie. It accumulates stem updates during block execution and flushes
// them to the NOMT engine on Hash()/Commit().
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 as common.Hash
pending []core.LeafOp // accumulated leaf operations
dirty bool // whether pending ops exist
root common.Hash // current trie root
pending []stemUpdate // accumulated stem updates
dirty bool // whether pending updates exist
}
// New creates a new NomtTrie. The root parameter is the current state root.
// If root is empty or the zero hash, the trie starts empty.
func New(root common.Hash, backend *nomtdb.Database) (*NomtTrie, error) {
return &NomtTrie{
nomtDB: backend.NomtDB(),
backend: backend,
root: root,
pending: make([]core.LeafOp, 0, 64),
pending: make([]stemUpdate, 0, 64),
}, nil
}
// GetKey returns the sha3 preimage of a hashed key. NOMT doesn't maintain
// preimage mappings; returns the key as-is.
// GetKey returns the sha3 preimage of a hashed key.
func (t *NomtTrie) GetKey(key []byte) []byte {
return key
}
// GetAccount reads an account from flat state storage (ethdb).
// NOMT's trie stores only hashes, so actual data comes from flat KV.
func (t *NomtTrie) GetAccount(address common.Address) (*types.StateAccount, error) {
data, err := t.backend.DiskDB().Get(nomtdb.NomtAccountKey(crypto.Keccak256Hash(address.Bytes())))
if err != nil {
return nil, nil // not found
}
if len(data) == 0 {
return nil, nil
}
return types.FullAccount(data)
// GetAccount reads an account from flat state storage.
// TODO(Phase E): implement using EIP-7864 key encoding.
func (t *NomtTrie) GetAccount(_ common.Address) (*types.StateAccount, error) {
return nil, nil
}
// PrefetchAccount is a no-op for NOMT — flat state reads are fast by design.
func (t *NomtTrie) PrefetchAccount(addresses []common.Address) error {
// PrefetchAccount is a no-op.
func (t *NomtTrie) PrefetchAccount(_ []common.Address) error {
return nil
}
// GetStorage reads a storage slot from flat state storage (ethdb).
func (t *NomtTrie) GetStorage(addr common.Address, key []byte) ([]byte, error) {
addrHash := crypto.Keccak256Hash(addr.Bytes())
slotHash := crypto.Keccak256Hash(key)
data, err := t.backend.DiskDB().Get(nomtdb.NomtStorageKey(addrHash, slotHash))
if err != nil {
return nil, nil // not found
}
return data, nil
// GetStorage reads a storage slot from flat state storage.
// TODO(Phase E): implement using EIP-7864 key encoding.
func (t *NomtTrie) GetStorage(_ common.Address, _ []byte) ([]byte, error) {
return nil, nil
}
// PrefetchStorage is a no-op for NOMT.
func (t *NomtTrie) PrefetchStorage(addr common.Address, keys [][]byte) error {
// PrefetchStorage is a no-op.
func (t *NomtTrie) PrefetchStorage(_ common.Address, _ [][]byte) error {
return nil
}
// UpdateAccount encodes the account and adds a leaf op to the pending batch.
// The trie keypath is keccak256(address), value hash is keccak256(slimRLP).
func (t *NomtTrie) UpdateAccount(address common.Address, account *types.StateAccount, codeLen int) error {
slimData := types.SlimAccountRLP(*account)
keyPath := crypto.Keccak256Hash(address.Bytes())
valueHash := crypto.Keccak256Hash(slimData)
var kp core.KeyPath
copy(kp[:], keyPath[:])
var vh core.ValueHash
copy(vh[:], valueHash[:])
t.pending = append(t.pending, core.LeafOp{
Key: kp,
Value: &vh,
})
t.dirty = true
// UpdateAccount encodes the account and queues stem updates.
// TODO(Phase E): implement using packBasicData + stem grouping.
func (t *NomtTrie) UpdateAccount(_ common.Address, _ *types.StateAccount, _ int) error {
return nil
}
// UpdateStorage adds a storage update leaf op to the pending batch.
// The trie keypath is keccak256(keccak256(address) || keccak256(slot)),
// value hash is keccak256(value).
func (t *NomtTrie) UpdateStorage(addr common.Address, key, value []byte) error {
kp := storageKeyPath(addr, key)
if len(value) == 0 {
// Empty value = delete.
t.pending = append(t.pending, core.LeafOp{
Key: kp,
Value: nil,
})
} else {
valueHash := crypto.Keccak256Hash(value)
var vh core.ValueHash
copy(vh[:], valueHash[:])
t.pending = append(t.pending, core.LeafOp{
Key: kp,
Value: &vh,
})
}
t.dirty = true
// UpdateStorage queues a storage value update.
// TODO(Phase E): implement using storageStemAndSuffix + packStorageValue.
func (t *NomtTrie) UpdateStorage(_ common.Address, _, _ []byte) error {
return nil
}
// DeleteAccount adds a delete leaf op for the account.
func (t *NomtTrie) DeleteAccount(address common.Address) error {
keyPath := crypto.Keccak256Hash(address.Bytes())
var kp core.KeyPath
copy(kp[:], keyPath[:])
t.pending = append(t.pending, core.LeafOp{
Key: kp,
Value: nil,
})
t.dirty = true
// DeleteAccount queues deletion of account values.
// TODO(Phase E): implement.
func (t *NomtTrie) DeleteAccount(_ common.Address) error {
return nil
}
// DeleteStorage adds a delete leaf op for a storage slot.
func (t *NomtTrie) DeleteStorage(addr common.Address, key []byte) error {
kp := storageKeyPath(addr, key)
t.pending = append(t.pending, core.LeafOp{
Key: kp,
Value: nil,
})
t.dirty = true
// DeleteStorage queues deletion of a storage slot.
// TODO(Phase E): implement.
func (t *NomtTrie) DeleteStorage(_ common.Address, _ []byte) error {
return nil
}
// UpdateContractCode is a no-op for NOMT — code is stored in ethdb by the
// state layer, not in the trie.
func (t *NomtTrie) UpdateContractCode(address common.Address, codeHash common.Hash, code []byte) error {
// UpdateContractCode queues code chunk updates.
// TODO(Phase E): implement using ChunkifyCode + codeChunkStemAndSuffix.
func (t *NomtTrie) UpdateContractCode(_ common.Address, _ common.Hash, _ []byte) error {
return nil
}
// Hash returns the current root hash. If there are pending operations, they
// are flushed to the NOMT engine first.
// Hash returns the current root hash. Flushes pending updates first.
// TODO(Phase E): implement using groupAndHashStems + nomtDB.Update.
func (t *NomtTrie) Hash() common.Hash {
if !t.dirty || len(t.pending) == 0 {
return t.root
}
newRoot, err := t.nomtDB.Update(t.pending)
if err != nil {
// On error, return current root without updating.
return t.root
}
t.root = common.Hash(newRoot)
t.pending = t.pending[:0]
t.dirty = false
return t.root
}
// Commit flushes pending operations and returns the root hash.
// The returned NodeSet is empty since NOMT uses page-based storage, not
// individual node persistence.
func (t *NomtTrie) Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet) {
func (t *NomtTrie) Commit(_ bool) (common.Hash, *trienode.NodeSet) {
root := t.Hash()
return root, trienode.NewNodeSet(common.Hash{})
}
// Witness returns accessed trie nodes. NOMT doesn't track witnesses yet.
// Witness returns accessed trie nodes. Not yet implemented.
func (t *NomtTrie) Witness() map[string][]byte {
return nil
}
// NodeIterator returns an iterator over trie nodes. Not yet implemented.
func (t *NomtTrie) NodeIterator(startKey []byte) (trie.NodeIterator, error) {
func (t *NomtTrie) NodeIterator(_ []byte) (trie.NodeIterator, error) {
return nil, nil
}
// Prove constructs a merkle proof. Not yet implemented.
func (t *NomtTrie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error {
func (t *NomtTrie) Prove(_ []byte, _ ethdb.KeyValueWriter) error {
return nil
}
// IsVerkle returns false — NOMT is a binary merkle trie, not verkle.
// IsVerkle returns true — NOMT uses EIP-7864's single-trie semantics
// which requires the verkle-like statedb path (no separate storage tries).
func (t *NomtTrie) IsVerkle() bool {
return false
return true
}
// Copy creates a deep copy of the trie.
func (t *NomtTrie) Copy() *NomtTrie {
pending := make([]core.LeafOp, len(t.pending))
pending := make([]stemUpdate, len(t.pending))
copy(pending, t.pending)
return &NomtTrie{
nomtDB: t.nomtDB,
@ -213,14 +146,3 @@ func (t *NomtTrie) Copy() *NomtTrie {
dirty: t.dirty,
}
}
// storageKeyPath computes the NOMT keypath for a storage slot.
// Format: keccak256(keccak256(address) || keccak256(slot))
func storageKeyPath(addr common.Address, slot []byte) core.KeyPath {
addrHash := crypto.Keccak256Hash(addr.Bytes())
slotHash := crypto.Keccak256Hash(slot)
combined := crypto.Keccak256Hash(addrHash.Bytes(), slotHash.Bytes())
var kp core.KeyPath
copy(kp[:], combined[:])
return kp
}

View file

@ -1,249 +0,0 @@
package nomttrie
import (
"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/crypto"
"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"
)
func newTestBackend(t *testing.T) *nomtdb.Database {
t.Helper()
dir := t.TempDir()
diskdb := rawdb.NewMemoryDatabase()
config := &triedb.Config{
NomtDB: &nomtdb.Config{
DataDir: dir,
HTCapacity: 1024,
},
}
tdb := triedb.NewDatabase(diskdb, config)
t.Cleanup(func() { tdb.Close() })
return tdb.NomtBackend()
}
func TestNewEmptyTrie(t *testing.T) {
backend := newTestBackend(t)
tr, err := New(common.Hash{}, backend)
require.NoError(t, err)
// Empty trie should hash to zero (NOMT Terminator).
root := tr.Hash()
assert.Equal(t, common.Hash{}, root)
}
func TestUpdateSingleAccount(t *testing.T) {
backend := newTestBackend(t)
tr, err := New(common.Hash{}, backend)
require.NoError(t, err)
addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678")
acc := &types.StateAccount{
Nonce: 42,
Balance: uint256.NewInt(1000),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
require.NoError(t, tr.UpdateAccount(addr, acc, 0))
root := tr.Hash()
assert.NotEqual(t, common.Hash{}, root, "root should be non-zero after insert")
}
func TestUpdateTwoAccounts(t *testing.T) {
backend := newTestBackend(t)
tr, err := New(common.Hash{}, backend)
require.NoError(t, err)
addr1 := common.HexToAddress("0x1111111111111111111111111111111111111111")
addr2 := common.HexToAddress("0x2222222222222222222222222222222222222222")
acc := &types.StateAccount{
Nonce: 1,
Balance: uint256.NewInt(100),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
require.NoError(t, tr.UpdateAccount(addr1, acc, 0))
require.NoError(t, tr.UpdateAccount(addr2, acc, 0))
root := tr.Hash()
assert.NotEqual(t, common.Hash{}, root)
}
func TestDeterministicRoot(t *testing.T) {
addr := common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
acc := &types.StateAccount{
Nonce: 7,
Balance: uint256.NewInt(999),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
run := func() common.Hash {
backend := newTestBackend(t)
tr, err := New(common.Hash{}, backend)
require.NoError(t, err)
require.NoError(t, tr.UpdateAccount(addr, acc, 0))
return tr.Hash()
}
r1 := run()
r2 := run()
assert.Equal(t, r1, r2, "same ops should produce same root")
}
func TestCommitReturnsRoot(t *testing.T) {
backend := newTestBackend(t)
tr, err := New(common.Hash{}, backend)
require.NoError(t, err)
addr := common.HexToAddress("0xaaaa")
acc := &types.StateAccount{
Nonce: 1,
Balance: uint256.NewInt(1),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
require.NoError(t, tr.UpdateAccount(addr, acc, 0))
root, nodeSet := tr.Commit(false)
assert.NotEqual(t, common.Hash{}, root)
assert.NotNil(t, nodeSet)
}
func TestDeleteAccount(t *testing.T) {
backend := newTestBackend(t)
tr, err := New(common.Hash{}, backend)
require.NoError(t, err)
addr := common.HexToAddress("0xbbbb")
acc := &types.StateAccount{
Nonce: 1,
Balance: uint256.NewInt(100),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
require.NoError(t, tr.UpdateAccount(addr, acc, 0))
root1 := tr.Hash()
assert.NotEqual(t, common.Hash{}, root1)
// DeleteAccount accumulates a nil-value LeafOp. Verify it's queued.
require.NoError(t, tr.DeleteAccount(addr))
assert.True(t, tr.dirty)
assert.Len(t, tr.pending, 1)
}
func TestUpdateStorage(t *testing.T) {
backend := newTestBackend(t)
tr, err := New(common.Hash{}, backend)
require.NoError(t, err)
addr := common.HexToAddress("0xcccc")
slot := common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000001")
value := common.Hex2Bytes("00000000000000000000000000000000000000000000000000000000000000ff")
require.NoError(t, tr.UpdateStorage(addr, slot, value))
root := tr.Hash()
assert.NotEqual(t, common.Hash{}, root)
}
func TestStorageKeyPathDeterminism(t *testing.T) {
addr := common.HexToAddress("0xdddd")
slot := common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000001")
kp1 := storageKeyPath(addr, slot)
kp2 := storageKeyPath(addr, slot)
assert.Equal(t, kp1, kp2, "same inputs should produce same keypath")
// Different slot should produce different keypath.
slot2 := common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000002")
kp3 := storageKeyPath(addr, slot2)
assert.NotEqual(t, kp1, kp3)
}
func TestAccountKeyPathMatchesCryptoKeccak(t *testing.T) {
addr := common.HexToAddress("0xeeee")
expected := crypto.Keccak256Hash(addr.Bytes())
backend := newTestBackend(t)
tr, err := New(common.Hash{}, backend)
require.NoError(t, err)
acc := &types.StateAccount{
Nonce: 1,
Balance: uint256.NewInt(1),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
require.NoError(t, tr.UpdateAccount(addr, acc, 0))
// Pending op should have keypath = keccak256(addr).
require.Len(t, tr.pending, 1)
assert.Equal(t, [32]byte(expected), tr.pending[0].Key)
// After Hash(), pending should be flushed.
tr.Hash()
assert.Len(t, tr.pending, 0)
}
func TestIsVerkle(t *testing.T) {
backend := newTestBackend(t)
tr, err := New(common.Hash{}, backend)
require.NoError(t, err)
assert.False(t, tr.IsVerkle())
}
func TestCopy(t *testing.T) {
backend := newTestBackend(t)
tr, err := New(common.Hash{}, backend)
require.NoError(t, err)
addr := common.HexToAddress("0xffff")
acc := &types.StateAccount{
Nonce: 5,
Balance: uint256.NewInt(500),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
require.NoError(t, tr.UpdateAccount(addr, acc, 0))
cp := tr.Copy()
assert.Equal(t, len(tr.pending), len(cp.pending))
assert.Equal(t, tr.root, cp.root)
assert.Equal(t, tr.dirty, cp.dirty)
}
func TestMultipleHashCalls(t *testing.T) {
backend := newTestBackend(t)
tr, err := New(common.Hash{}, backend)
require.NoError(t, err)
addr1 := common.HexToAddress("0x1111")
addr2 := common.HexToAddress("0x2222")
acc := &types.StateAccount{
Nonce: 1,
Balance: uint256.NewInt(1),
Root: types.EmptyRootHash,
CodeHash: types.EmptyCodeHash.Bytes(),
}
// First update + hash.
require.NoError(t, tr.UpdateAccount(addr1, acc, 0))
root1 := tr.Hash()
assert.NotEqual(t, common.Hash{}, root1)
// Second update + hash (simulating per-transaction root computation).
require.NoError(t, tr.UpdateAccount(addr2, acc, 0))
root2 := tr.Hash()
assert.NotEqual(t, common.Hash{}, root2)
assert.NotEqual(t, root1, root2, "root should change after second update")
}

View file

@ -0,0 +1,43 @@
package nomttrie
import (
"encoding/binary"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/trie/bintrie"
)
// packBasicData encodes account metadata into a 32-byte value matching the
// EIP-7864 basic data layout used by bintrie.UpdateAccount:
//
// [4:8] code size (uint32 big-endian)
// [8:16] nonce (uint64 big-endian)
// [16:32] balance (up to 16 bytes, right-aligned big-endian)
func packBasicData(acc *types.StateAccount, codeLen int) [bintrie.HashSize]byte {
var data [bintrie.HashSize]byte
binary.BigEndian.PutUint32(data[bintrie.BasicDataCodeSizeOffset-1:], uint32(codeLen))
binary.BigEndian.PutUint64(data[bintrie.BasicDataNonceOffset:], acc.Nonce)
// Truncate balance to 16 bytes (matching bintrie behavior for devmode
// accounts that exceed 128-bit balance).
balanceBytes := acc.Balance.Bytes()
if len(balanceBytes) > 16 {
balanceBytes = balanceBytes[16:]
}
copy(data[bintrie.HashSize-len(balanceBytes):], balanceBytes)
return data
}
// packStorageValue encodes a storage value into a 32-byte slot matching
// bintrie.UpdateStorage: right-pad short values, truncate long values.
func packStorageValue(value []byte) [bintrie.HashSize]byte {
var v [bintrie.HashSize]byte
if len(value) >= bintrie.HashSize {
copy(v[:], value[:bintrie.HashSize])
} else {
copy(v[bintrie.HashSize-len(value):], value)
}
return v
}

View file

@ -0,0 +1,117 @@
package nomttrie
import (
"encoding/binary"
"testing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/trie/bintrie"
"github.com/holiman/uint256"
"github.com/stretchr/testify/assert"
)
func TestPackBasicDataNonce(t *testing.T) {
acc := &types.StateAccount{
Nonce: 42,
Balance: uint256.NewInt(0),
}
data := packBasicData(acc, 0)
got := binary.BigEndian.Uint64(data[bintrie.BasicDataNonceOffset:])
assert.Equal(t, uint64(42), got)
}
func TestPackBasicDataCodeSize(t *testing.T) {
acc := &types.StateAccount{
Nonce: 0,
Balance: uint256.NewInt(0),
}
data := packBasicData(acc, 1234)
got := binary.BigEndian.Uint32(data[bintrie.BasicDataCodeSizeOffset-1:])
assert.Equal(t, uint32(1234), got)
}
func TestPackBasicDataBalance(t *testing.T) {
acc := &types.StateAccount{
Nonce: 0,
Balance: uint256.NewInt(1000),
}
data := packBasicData(acc, 0)
// Balance is right-aligned in the last 16 bytes.
// 1000 = 0x03E8, occupies 2 bytes.
assert.Equal(t, byte(0x03), data[30])
assert.Equal(t, byte(0xE8), data[31])
}
func TestPackBasicDataMatchesBintrie(t *testing.T) {
// Match the exact encoding bintrie uses in UpdateAccount.
acc := &types.StateAccount{
Nonce: 7,
Balance: uint256.NewInt(999),
CodeHash: types.EmptyCodeHash.Bytes(),
}
codeLen := 512
data := packBasicData(acc, codeLen)
// Manually reproduce bintrie encoding.
var expected [bintrie.HashSize]byte
binary.BigEndian.PutUint32(expected[bintrie.BasicDataCodeSizeOffset-1:], uint32(codeLen))
binary.BigEndian.PutUint64(expected[bintrie.BasicDataNonceOffset:], acc.Nonce)
balBytes := acc.Balance.Bytes()
copy(expected[bintrie.HashSize-len(balBytes):], balBytes)
assert.Equal(t, expected, data)
}
func TestPackStorageValue(t *testing.T) {
tests := []struct {
name string
value []byte
expected [32]byte
}{
{
name: "small value right-padded",
value: []byte{0xFF},
expected: func() [32]byte {
var v [32]byte
v[31] = 0xFF
return v
}(),
},
{
name: "full 32 bytes",
value: func() []byte {
v := make([]byte, 32)
for i := range v {
v[i] = byte(i)
}
return v
}(),
expected: func() [32]byte {
var v [32]byte
for i := range v {
v[i] = byte(i)
}
return v
}(),
},
{
name: "empty value",
value: nil,
expected: [32]byte{},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := packStorageValue(tc.value)
assert.Equal(t, tc.expected, got)
})
}
}