mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-21 06:04:33 +00:00
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:
parent
cf10f3d997
commit
53fd00926f
8 changed files with 753 additions and 3 deletions
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
226
trie/nomttrie/trie.go
Normal 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
249
trie/nomttrie/trie_test.go
Normal 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")
|
||||
}
|
||||
|
|
@ -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
22
triedb/nomtdb/config.go
Normal 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
109
triedb/nomtdb/database.go
Normal 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
84
triedb/nomtdb/reader.go
Normal 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
|
||||
}
|
||||
Loading…
Reference in a new issue