triedb/nomtdb, trie/nomttrie: add Phase 6 geth integration for NOMT

Wire the NOMT binary merkle trie engine into geth's triedb/state
framework. This adds two new packages:

- triedb/nomtdb: backend implementing triedb.backend interface, manages
  flat state persistence in ethdb and delegates trie ops to nomt/db
- trie/nomttrie: NomtTrie implementing state.Trie, accumulates LeafOps
  during block execution and flushes to NOMT engine on Hash()/Commit()

Key design choices:
- Single flat keyspace: accounts use keccak256(addr), storage uses
  keccak256(keccak256(addr) || keccak256(slot)) as 256-bit trie paths
- OpenStorageTrie returns the account trie itself (no separate tries)
- Flat state (account/storage values) stored in ethdb with prefixed keys
- NOMT trie stores only hashes; reads delegate to ethdb flat state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
weiihann 2026-02-12 17:36:57 +08:00
parent cf10f3d997
commit 53fd00926f
8 changed files with 753 additions and 3 deletions

View file

@ -44,6 +44,11 @@ const HashScheme = "hash"
// on extra state diffs to survive deep reorg.
const PathScheme = "path"
// NomtScheme is the NOMT page-based state scheme with which trie nodes are stored
// in pages (126 nodes each) using the Bitbox hash table. This scheme offers
// optimal I/O for binary merkle tries by batching nodes into fixed-size pages.
const NomtScheme = "nomt"
// ReadAccountTrieNode retrieves the account trie node with the specified node path.
func ReadAccountTrieNode(db ethdb.KeyValueReader, path []byte) []byte {
data, _ := db.Get(accountTrieNodeKey(path))

View file

@ -29,6 +29,7 @@ import (
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/trie/bintrie"
"github.com/ethereum/go-ethereum/trie/nomttrie"
"github.com/ethereum/go-ethereum/trie/transitiontrie"
"github.com/ethereum/go-ethereum/trie/trienode"
"github.com/ethereum/go-ethereum/triedb"
@ -200,6 +201,13 @@ func (db *CachingDB) StateReader(stateRoot common.Hash) (StateReader, error) {
readers = append(readers, newFlatReader(reader))
}
}
// Configure the state reader for NOMT mode.
if db.TrieDB().Scheme() == rawdb.NomtScheme {
reader, err := db.triedb.StateReader(stateRoot)
if err == nil {
readers = append(readers, newFlatReader(reader))
}
}
// Configure the trie reader, which is expected to be available as the
// gatekeeper unless the state is corrupted.
tr, err := newTrieReader(stateRoot, db.triedb)
@ -238,6 +246,10 @@ func (db *CachingDB) ReadersWithCacheStats(stateRoot common.Hash) (ReaderWithSta
// OpenTrie opens the main account trie at a specific root hash.
func (db *CachingDB) OpenTrie(root common.Hash) (Trie, error) {
// NOMT uses its own page-based binary trie.
if db.triedb.IsNomt() {
return nomttrie.New(root, db.triedb.NomtBackend())
}
if db.triedb.IsVerkle() {
ts := overlay.LoadTransitionState(db.TrieDB().Disk(), root, db.triedb.IsVerkle())
if ts.InTransition() {
@ -258,6 +270,10 @@ func (db *CachingDB) OpenTrie(root common.Hash) (Trie, error) {
// OpenStorageTrie opens the storage trie of an account.
func (db *CachingDB) OpenStorageTrie(stateRoot common.Hash, address common.Address, root common.Hash, self Trie) (Trie, error) {
// NOMT uses a single flat keyspace — no separate storage tries.
if db.triedb.IsNomt() {
return self, nil
}
if db.triedb.IsVerkle() {
return self, nil
}
@ -301,6 +317,8 @@ func mustCopyTrie(t Trie) Trie {
return t.Copy()
case *transitiontrie.TransitionTrie:
return t.Copy()
case *nomttrie.NomtTrie:
return t.Copy()
default:
panic(fmt.Errorf("unknown trie type %T", t))
}

226
trie/nomttrie/trie.go Normal file
View file

@ -0,0 +1,226 @@
// 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().
//
// Read operations (GetAccount, GetStorage) delegate to geth's ethdb flat state
// since NOMT's trie stores only hashes, not actual values.
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"
)
// 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().
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
}
// 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),
}, nil
}
// GetKey returns the sha3 preimage of a hashed key. NOMT doesn't maintain
// preimage mappings; returns the key as-is.
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)
}
// PrefetchAccount is a no-op for NOMT — flat state reads are fast by design.
func (t *NomtTrie) PrefetchAccount(addresses []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
}
// PrefetchStorage is a no-op for NOMT.
func (t *NomtTrie) PrefetchStorage(addr common.Address, keys [][]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
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
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
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
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 {
return nil
}
// Hash returns the current root hash. If there are pending operations, they
// are flushed to the NOMT engine first.
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) {
root := t.Hash()
return root, trienode.NewNodeSet(common.Hash{})
}
// Witness returns accessed trie nodes. NOMT doesn't track witnesses yet.
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) {
return nil, nil
}
// Prove constructs a merkle proof. Not yet implemented.
func (t *NomtTrie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error {
return nil
}
// IsVerkle returns false — NOMT is a binary merkle trie, not verkle.
func (t *NomtTrie) IsVerkle() bool {
return false
}
// Copy creates a deep copy of the trie.
func (t *NomtTrie) Copy() *NomtTrie {
pending := make([]core.LeafOp, len(t.pending))
copy(pending, t.pending)
return &NomtTrie{
nomtDB: t.nomtDB,
backend: t.backend,
root: t.root,
pending: pending,
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
}

249
trie/nomttrie/trie_test.go Normal file
View file

@ -0,0 +1,249 @@
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

@ -26,6 +26,7 @@ import (
"github.com/ethereum/go-ethereum/trie/trienode"
"github.com/ethereum/go-ethereum/triedb/database"
"github.com/ethereum/go-ethereum/triedb/hashdb"
"github.com/ethereum/go-ethereum/triedb/nomtdb"
"github.com/ethereum/go-ethereum/triedb/pathdb"
)
@ -35,6 +36,7 @@ type Config struct {
IsVerkle bool // Flag whether the db is holding a verkle tree
HashDB *hashdb.Config // Configs for hash-based scheme
PathDB *pathdb.Config // Configs for experimental path-based scheme
NomtDB *nomtdb.Config // Configs for NOMT page-based scheme
}
// HashDefaults represents a config for using hash-based scheme with
@ -105,12 +107,26 @@ func NewDatabase(diskdb ethdb.Database, config *Config) *Database {
config: config,
preimages: preimages,
}
if config.HashDB != nil && config.PathDB != nil {
log.Crit("Both 'hash' and 'path' mode are configured")
// Ensure at most one backend is configured.
configured := 0
if config.HashDB != nil {
configured++
}
if config.PathDB != nil {
configured++
}
if config.NomtDB != nil {
configured++
}
if configured > 1 {
log.Crit("Multiple trie backends configured (only one of hash/path/nomt allowed)")
}
switch {
case config.NomtDB != nil:
db.backend = nomtdb.New(diskdb, config.NomtDB)
case config.PathDB != nil:
db.backend = pathdb.New(diskdb, config.PathDB, config.IsVerkle)
} else {
default:
db.backend = hashdb.New(diskdb, config.HashDB)
}
return db
@ -163,6 +179,13 @@ func (db *Database) Update(root common.Hash, parent common.Hash, block uint64, n
return b.Update(root, parent, block, nodes)
case *pathdb.Database:
return b.Update(root, parent, block, nodes, states.internal())
case *nomtdb.Database:
// For NOMT, trie pages are already committed during NomtTrie.Hash()/Commit().
// Here we only need to persist the flat state changes.
if states == nil {
return nil
}
return b.Update(states.Accounts, states.Storages)
}
return errors.New("unknown backend")
}
@ -194,12 +217,26 @@ func (db *Database) Size() (common.StorageSize, common.StorageSize, common.Stora
// Scheme returns the node scheme used in the database.
func (db *Database) Scheme() string {
if db.config.NomtDB != nil {
return rawdb.NomtScheme
}
if db.config.PathDB != nil {
return rawdb.PathScheme
}
return rawdb.HashScheme
}
// IsNomt returns true if the database is using the NOMT backend.
func (db *Database) IsNomt() bool {
return db.config.NomtDB != nil
}
// NomtBackend returns the NOMT backend, or nil if not using NOMT.
func (db *Database) NomtBackend() *nomtdb.Database {
ndb, _ := db.backend.(*nomtdb.Database)
return ndb
}
// Close flushes the dangling preimages to disk and closes the trie database.
// It is meant to be called when closing the blockchain object, so that all
// resources held can be released correctly.

22
triedb/nomtdb/config.go Normal file
View file

@ -0,0 +1,22 @@
// Package nomtdb implements the triedb backend for NOMT (Near-Optimal Merkle
// Trie), a page-based binary merkle trie engine.
//
// NOMT handles only the trie structure (merkle pages). Flat key-value storage
// (accounts, storage slots) is stored in geth's existing ethdb (PebbleDB)
// under NOMT-specific key prefixes.
package nomtdb
// Config holds configuration for the NOMT triedb backend.
type Config struct {
// DataDir is the directory for NOMT's Bitbox storage files.
DataDir string
// HTCapacity is the number of hash table buckets. Must be a power of 2.
// Defaults to 1<<20 (~1M buckets) if zero.
HTCapacity uint64
}
// Defaults is the default configuration for the NOMT backend.
var Defaults = &Config{
HTCapacity: 1 << 20,
}

109
triedb/nomtdb/database.go Normal file
View file

@ -0,0 +1,109 @@
package nomtdb
import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/nomt/db"
"github.com/ethereum/go-ethereum/triedb/database"
)
// Database is the NOMT triedb backend. It manages the NOMT trie engine for
// page-based merkle storage and delegates flat state to geth's ethdb.
type Database struct {
diskdb ethdb.Database // geth's existing PebbleDB for flat state + metadata
nomt *db.DB // NOMT trie engine (Bitbox page storage)
config *Config
}
// New creates a new NOMT backend. The diskdb is used for flat state storage
// (accounts, storage slots) and NOMT metadata. The NOMT engine opens its own
// Bitbox files under config.DataDir.
func New(diskdb ethdb.Database, config *Config) *Database {
if config.HTCapacity == 0 {
config.HTCapacity = Defaults.HTCapacity
}
nomtDB, err := db.Open(config.DataDir, db.Config{
HTCapacity: config.HTCapacity,
})
if err != nil {
log.Crit("Failed to open NOMT database", "err", err)
}
return &Database{
diskdb: diskdb,
nomt: nomtDB,
config: config,
}
}
// NomtDB returns the underlying NOMT trie engine.
func (d *Database) NomtDB() *db.DB {
return d.nomt
}
// DiskDB returns the underlying ethdb for flat state access.
func (d *Database) DiskDB() ethdb.Database {
return d.diskdb
}
// NodeReader returns a reader for accessing trie nodes within the specified state.
func (d *Database) NodeReader(root common.Hash) (database.NodeReader, error) {
return &nodeReader{nomt: d.nomt}, nil
}
// StateReader returns a reader for accessing flat states within the specified state.
func (d *Database) StateReader(root common.Hash) (database.StateReader, error) {
return &stateReader{diskdb: d.diskdb}, nil
}
// Size returns the current storage size of the NOMT database.
// First return is diff layer size (always 0 for NOMT), second is disk size.
func (d *Database) Size() (common.StorageSize, common.StorageSize) {
return 0, 0
}
// Commit is a no-op for NOMT — pages are synced during trie Hash()/Commit().
func (d *Database) Commit(root common.Hash, report bool) error {
return nil
}
// Close closes the NOMT database backend.
func (d *Database) Close() error {
return d.nomt.Close()
}
// Update writes flat state changes to ethdb. The trie pages have already been
// persisted by the NomtTrie during Hash()/Commit().
func (d *Database) Update(accounts map[common.Hash][]byte, storages map[common.Hash]map[common.Hash][]byte) error {
batch := d.diskdb.NewBatch()
for accountHash, data := range accounts {
key := NomtAccountKey(accountHash)
if len(data) == 0 {
if err := batch.Delete(key); err != nil {
return err
}
} else {
if err := batch.Put(key, data); err != nil {
return err
}
}
}
for accountHash, slots := range storages {
for slotHash, value := range slots {
key := NomtStorageKey(accountHash, slotHash)
if len(value) == 0 {
if err := batch.Delete(key); err != nil {
return err
}
} else {
if err := batch.Put(key, value); err != nil {
return err
}
}
}
}
return batch.Write()
}

84
triedb/nomtdb/reader.go Normal file
View file

@ -0,0 +1,84 @@
package nomtdb
import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/nomt/db"
"github.com/ethereum/go-ethereum/rlp"
)
// Key prefixes for NOMT flat state in ethdb.
const (
nomtAccountPrefix byte = 0x01
nomtStoragePrefix byte = 0x02
)
// NomtAccountKey returns the ethdb key for an account's flat state.
// Format: 0x01 || accountHash (32 bytes)
func NomtAccountKey(accountHash common.Hash) []byte {
key := make([]byte, 1+common.HashLength)
key[0] = nomtAccountPrefix
copy(key[1:], accountHash[:])
return key
}
// NomtStorageKey returns the ethdb key for a storage slot's flat state.
// Format: 0x02 || accountHash (32 bytes) || slotHash (32 bytes)
func NomtStorageKey(accountHash, slotHash common.Hash) []byte {
key := make([]byte, 1+2*common.HashLength)
key[0] = nomtStoragePrefix
copy(key[1:], accountHash[:])
copy(key[1+common.HashLength:], slotHash[:])
return key
}
// nodeReader implements database.NodeReader backed by the NOMT page store.
//
// In NOMT, trie data is stored as pages (126 nodes each), not individual nodes.
// This reader extracts individual nodes from pages for compatibility with
// geth's node-oriented interfaces.
type nodeReader struct {
nomt *db.DB
}
// Node retrieves a trie node blob. For NOMT, this is a compatibility shim —
// the NomtTrie accesses pages directly rather than going through NodeReader.
func (r *nodeReader) Node(owner common.Hash, path []byte, hash common.Hash) ([]byte, error) {
// NOMT trie operations go through the page-based engine directly.
// This reader exists to satisfy the interface; the NomtTrie doesn't
// use it for normal trie operations.
return nil, nil
}
// stateReader implements database.StateReader backed by NOMT's flat state
// stored in geth's ethdb.
type stateReader struct {
diskdb ethdb.Database
}
// Account retrieves an account from NOMT's flat state storage.
func (r *stateReader) Account(hash common.Hash) (*types.SlimAccount, error) {
data, err := r.diskdb.Get(NomtAccountKey(hash))
if err != nil {
// Key not found is not an error — return nil account.
return nil, nil
}
if len(data) == 0 {
return nil, nil
}
account := new(types.SlimAccount)
if err := rlp.DecodeBytes(data, account); err != nil {
return nil, err
}
return account, nil
}
// Storage retrieves a storage slot from NOMT's flat state storage.
func (r *stateReader) Storage(accountHash, storageHash common.Hash) ([]byte, error) {
data, err := r.diskdb.Get(NomtStorageKey(accountHash, storageHash))
if err != nil {
return nil, nil
}
return data, nil
}