go-ethereum/trie/nomttrie/trie.go
2026-02-13 10:40:05 +08:00

380 lines
12 KiB
Go

// Package nomttrie implements a state.Trie backed by the NOMT binary merkle
// trie engine, targeting EIP-7864 compatibility.
//
// Read operations delegate to geth's ethdb flat state (stem value keys).
// Write operations accumulate stem updates and flush them to flat state + the
// NOMT page tree on Hash()/Commit().
package nomttrie
import (
"bytes"
"encoding/binary"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
"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/bintrie"
"github.com/ethereum/go-ethereum/trie/trienode"
"github.com/ethereum/go-ethereum/triedb/nomtdb"
"github.com/holiman/uint256"
)
// stemUpdate represents a pending value change at a specific (stem, suffix)
// position in the EIP-7864 trie.
type stemUpdate struct {
Stem core.StemPath // 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 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 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,
// kept sorted by stem path. Updated on each Hash() via sorted merge
// with results from groupAndHashStems.
allStems []core.StemKeyValue
// mergeBuf is reused across Hash() calls to avoid allocating a new
// slice on every merge. After merge, allStems and mergeBuf swap roles.
mergeBuf []core.StemKeyValue
}
// 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),
}, nil
}
// 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 using EIP-7864 stem keys.
// Reads basic data (slot 0) and code hash (slot 1) from the account stem.
func (t *NomtTrie) GetAccount(addr common.Address) (*types.StateAccount, error) {
stem := accountStem(addr)
diskdb := t.backend.DiskDB()
basicData, err := diskdb.Get(stemValueDBKey(stem, bintrie.BasicDataLeafKey))
if err != nil {
basicData = nil
}
codeHash, err := diskdb.Get(stemValueDBKey(stem, bintrie.CodeHashLeafKey))
if err != nil {
codeHash = nil
}
if basicData == nil && codeHash == nil {
return nil, nil
}
acc := &types.StateAccount{
Balance: new(uint256.Int),
}
// Unpack basic data: nonce at [8:16], balance at [16:32].
if len(basicData) >= bintrie.HashSize {
acc.Nonce = binary.BigEndian.Uint64(
basicData[bintrie.BasicDataNonceOffset:],
)
var balance [16]byte
copy(balance[:], basicData[bintrie.BasicDataBalanceOffset:])
acc.Balance = new(uint256.Int).SetBytes(balance[:])
}
if len(codeHash) > 0 {
acc.CodeHash = make([]byte, len(codeHash))
copy(acc.CodeHash, codeHash)
}
return acc, nil
}
// PrefetchAccount is a no-op for NOMT (flat state reads are already fast).
func (t *NomtTrie) PrefetchAccount(_ []common.Address) error {
return nil
}
// GetStorage reads a storage slot from flat state using EIP-7864 stem keys.
func (t *NomtTrie) GetStorage(addr common.Address, key []byte) ([]byte, error) {
stem, suffix := storageStemAndSuffix(addr, key)
data, err := t.backend.DiskDB().Get(stemValueDBKey(stem, suffix))
if err != nil {
return nil, nil
}
return data, nil
}
// PrefetchStorage is a no-op for NOMT.
func (t *NomtTrie) PrefetchStorage(_ common.Address, _ [][]byte) error {
return nil
}
// UpdateAccount encodes account metadata and queues stem updates for basic
// data (slot 0) and code hash (slot 1) matching bintrie.UpdateAccount.
func (t *NomtTrie) UpdateAccount(addr common.Address, acc *types.StateAccount, codeLen int) error {
stem := accountStem(addr)
basicData := packBasicData(acc, codeLen)
t.pending = append(t.pending, stemUpdate{
Stem: stem,
Suffix: bintrie.BasicDataLeafKey,
Value: basicData[:],
})
codeHashVal := make([]byte, bintrie.HashSize)
copy(codeHashVal, acc.CodeHash)
t.pending = append(t.pending, stemUpdate{
Stem: stem,
Suffix: bintrie.CodeHashLeafKey,
Value: codeHashVal,
})
t.dirty = true
return nil
}
// UpdateStorage queues a storage value update. The value is right-aligned
// and padded to 32 bytes, matching bintrie.UpdateStorage.
func (t *NomtTrie) UpdateStorage(addr common.Address, key, value []byte) error {
stem, suffix := storageStemAndSuffix(addr, key)
v := packStorageValue(value)
t.pending = append(t.pending, stemUpdate{
Stem: stem,
Suffix: suffix,
Value: v[:],
})
t.dirty = true
return nil
}
// DeleteAccount is a no-op, matching bintrie behavior.
func (t *NomtTrie) DeleteAccount(_ common.Address) error {
return nil
}
// DeleteStorage queues a zero-value write for the storage slot,
// matching bintrie.DeleteStorage which inserts 32 zero bytes.
func (t *NomtTrie) DeleteStorage(addr common.Address, key []byte) error {
stem, suffix := storageStemAndSuffix(addr, key)
t.pending = append(t.pending, stemUpdate{
Stem: stem,
Suffix: suffix,
Value: make([]byte, bintrie.HashSize),
})
t.dirty = true
return nil
}
// UpdateContractCode chunks the bytecode using EIP-7864's ChunkifyCode and
// 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 {
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: groupOffset,
Value: val,
})
}
if len(chunks) > 0 {
t.dirty = true
}
return nil
}
// 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
}
stemKVs, err := groupAndHashStems(t.pending, t.backend.DiskDB())
if err != nil {
log.Error("NOMT groupAndHashStems failed", "err", err)
return t.root
}
// Merge sorted stemKVs into allStems (both are sorted by stem path).
// Swap allStems and mergeBuf to reuse backing arrays across calls.
merged := mergeStemKVs(t.allStems, stemKVs, t.mergeBuf)
t.mergeBuf = t.allStems
t.allStems = merged
// Update the page tree for persistent storage.
// stemKVs is already sorted, so skip the redundant sort in db.Update.
if len(stemKVs) > 0 {
if _, err := t.nomtDB.Update(stemKVs); err != nil {
log.Error("NOMT page tree update failed", "err", err)
return t.root
}
}
// Compute the canonical root via BuildInternalTree(skip=0).
// allStems is already sorted, so no additional sort needed.
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. allStems is already sorted.
func (t *NomtTrie) canonicalRoot() core.Node {
if len(t.allStems) == 0 {
return core.Terminator
}
return core.BuildInternalTree(0, t.allStems, func(_ core.WriteNode) {})
}
// mergeStemKVs merges sorted new stemKVs into sorted existing allStems.
// Existing entries with the same stem are replaced. The result is sorted.
// The buf parameter is reused for the result to avoid allocation when new
// stems need to be inserted.
func mergeStemKVs(existing, updates, buf []core.StemKeyValue) []core.StemKeyValue {
if len(updates) == 0 {
return existing
}
if len(existing) == 0 {
return updates
}
// Fast path: check if all updates are in-place replacements (no new stems).
// This is the common case for incremental block updates where accounts
// already exist in the trie.
allInPlace := true
ei := 0
for _, u := range updates {
for ei < len(existing) && bytes.Compare(existing[ei].Stem[:], u.Stem[:]) < 0 {
ei++
}
if ei >= len(existing) || existing[ei].Stem != u.Stem {
allInPlace = false
break
}
}
if allInPlace {
// Update hashes in place — zero allocation.
ei = 0
for _, u := range updates {
for existing[ei].Stem != u.Stem {
ei++
}
existing[ei].Hash = u.Hash
}
return existing
}
// Slow path: some new stems need inserting. Use merge with buffer.
needed := len(existing) + len(updates)
if cap(buf) < needed {
buf = make([]core.StemKeyValue, 0, needed)
}
result := buf[:0]
i, j := 0, 0
for i < len(existing) && j < len(updates) {
cmp := bytes.Compare(existing[i].Stem[:], updates[j].Stem[:])
switch {
case cmp < 0:
result = append(result, existing[i])
i++
case cmp > 0:
result = append(result, updates[j])
j++
default:
result = append(result, updates[j])
i++
j++
}
}
result = append(result, existing[i:]...)
result = append(result, updates[j:]...)
return result
}
// Commit flushes pending operations and returns the root hash.
func (t *NomtTrie) Commit(_ bool) (common.Hash, *trienode.NodeSet) {
root := t.Hash()
return root, trienode.NewNodeSet(common.Hash{})
}
// Witness returns accessed trie nodes. Not yet implemented for NOMT.
func (t *NomtTrie) Witness() map[string][]byte {
return nil
}
// NodeIterator returns an iterator over trie nodes. Not yet implemented.
func (t *NomtTrie) NodeIterator(_ []byte) (trie.NodeIterator, error) {
return nil, nil
}
// Prove constructs a merkle proof. Not yet implemented.
func (t *NomtTrie) Prove(_ []byte, _ ethdb.KeyValueWriter) error {
return nil
}
// 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 true
}
// Copy creates a deep copy of the trie.
func (t *NomtTrie) Copy() *NomtTrie {
pending := make([]stemUpdate, len(t.pending))
copy(pending, t.pending)
allStems := make([]core.StemKeyValue, len(t.allStems))
copy(allStems, t.allStems)
return &NomtTrie{
nomtDB: t.nomtDB,
backend: t.backend,
root: t.root,
pending: pending,
dirty: t.dirty,
allStems: allStems,
}
}