mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-18 19:00:46 +00:00
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:
parent
1051e7be0c
commit
84fff73c8f
6 changed files with 371 additions and 382 deletions
48
trie/nomttrie/key_encoding.go
Normal file
48
trie/nomttrie/key_encoding.go
Normal 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[:]))
|
||||||
|
}
|
||||||
108
trie/nomttrie/key_encoding_test.go
Normal file
108
trie/nomttrie/key_encoding_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
|
@ -1,209 +1,142 @@
|
||||||
// Package nomttrie implements a state.Trie backed by the NOMT binary merkle
|
// Package nomttrie implements a state.Trie backed by the NOMT binary merkle
|
||||||
// trie engine. It accumulates leaf operations during block execution and
|
// trie engine, targeting EIP-7864 compatibility.
|
||||||
// commits them to the NOMT page-based storage on Hash()/Commit().
|
|
||||||
//
|
//
|
||||||
// Read operations (GetAccount, GetStorage) delegate to geth's ethdb flat state
|
// Read operations delegate to geth's ethdb flat state. Write operations
|
||||||
// since NOMT's trie stores only hashes, not actual values.
|
// accumulate stem updates and flush them to the NOMT page tree on Hash()/Commit().
|
||||||
package nomttrie
|
package nomttrie
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
"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/nomt/db"
|
||||||
"github.com/ethereum/go-ethereum/trie"
|
"github.com/ethereum/go-ethereum/trie"
|
||||||
"github.com/ethereum/go-ethereum/trie/trienode"
|
"github.com/ethereum/go-ethereum/trie/trienode"
|
||||||
"github.com/ethereum/go-ethereum/triedb/nomtdb"
|
"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
|
// NomtTrie implements the state.Trie interface using NOMT's page-based binary
|
||||||
// merkle trie. It accumulates changes as LeafOps during block execution and
|
// merkle trie. It accumulates stem updates during block execution and flushes
|
||||||
// flushes them to the NOMT engine on Hash()/Commit().
|
// them to the NOMT engine on Hash()/Commit().
|
||||||
type NomtTrie struct {
|
type NomtTrie struct {
|
||||||
nomtDB *db.DB // NOMT trie engine (page storage + walker)
|
nomtDB *db.DB // NOMT trie engine (page storage + walker)
|
||||||
backend *nomtdb.Database // NOMT triedb backend (for flat state access)
|
backend *nomtdb.Database // NOMT triedb backend (for flat state access)
|
||||||
root common.Hash // current trie root as common.Hash
|
root common.Hash // current trie root
|
||||||
pending []core.LeafOp // accumulated leaf operations
|
pending []stemUpdate // accumulated stem updates
|
||||||
dirty bool // whether pending ops exist
|
dirty bool // whether pending updates exist
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new NomtTrie. The root parameter is the current state root.
|
// 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) {
|
func New(root common.Hash, backend *nomtdb.Database) (*NomtTrie, error) {
|
||||||
return &NomtTrie{
|
return &NomtTrie{
|
||||||
nomtDB: backend.NomtDB(),
|
nomtDB: backend.NomtDB(),
|
||||||
backend: backend,
|
backend: backend,
|
||||||
root: root,
|
root: root,
|
||||||
pending: make([]core.LeafOp, 0, 64),
|
pending: make([]stemUpdate, 0, 64),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetKey returns the sha3 preimage of a hashed key. NOMT doesn't maintain
|
// GetKey returns the sha3 preimage of a hashed key.
|
||||||
// preimage mappings; returns the key as-is.
|
|
||||||
func (t *NomtTrie) GetKey(key []byte) []byte {
|
func (t *NomtTrie) GetKey(key []byte) []byte {
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAccount reads an account from flat state storage (ethdb).
|
// GetAccount reads an account from flat state storage.
|
||||||
// NOMT's trie stores only hashes, so actual data comes from flat KV.
|
// TODO(Phase E): implement using EIP-7864 key encoding.
|
||||||
func (t *NomtTrie) GetAccount(address common.Address) (*types.StateAccount, error) {
|
func (t *NomtTrie) GetAccount(_ 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 nil, nil
|
||||||
}
|
|
||||||
return types.FullAccount(data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrefetchAccount is a no-op for NOMT — flat state reads are fast by design.
|
// PrefetchAccount is a no-op.
|
||||||
func (t *NomtTrie) PrefetchAccount(addresses []common.Address) error {
|
func (t *NomtTrie) PrefetchAccount(_ []common.Address) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStorage reads a storage slot from flat state storage (ethdb).
|
// GetStorage reads a storage slot from flat state storage.
|
||||||
func (t *NomtTrie) GetStorage(addr common.Address, key []byte) ([]byte, error) {
|
// TODO(Phase E): implement using EIP-7864 key encoding.
|
||||||
addrHash := crypto.Keccak256Hash(addr.Bytes())
|
func (t *NomtTrie) GetStorage(_ common.Address, _ []byte) ([]byte, error) {
|
||||||
slotHash := crypto.Keccak256Hash(key)
|
return nil, nil
|
||||||
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.
|
// PrefetchStorage is a no-op.
|
||||||
func (t *NomtTrie) PrefetchStorage(addr common.Address, keys [][]byte) error {
|
func (t *NomtTrie) PrefetchStorage(_ common.Address, _ [][]byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAccount encodes the account and adds a leaf op to the pending batch.
|
// UpdateAccount encodes the account and queues stem updates.
|
||||||
// The trie keypath is keccak256(address), value hash is keccak256(slimRLP).
|
// TODO(Phase E): implement using packBasicData + stem grouping.
|
||||||
func (t *NomtTrie) UpdateAccount(address common.Address, account *types.StateAccount, codeLen int) error {
|
func (t *NomtTrie) UpdateAccount(_ common.Address, _ *types.StateAccount, _ 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateStorage adds a storage update leaf op to the pending batch.
|
// UpdateStorage queues a storage value update.
|
||||||
// The trie keypath is keccak256(keccak256(address) || keccak256(slot)),
|
// TODO(Phase E): implement using storageStemAndSuffix + packStorageValue.
|
||||||
// value hash is keccak256(value).
|
func (t *NomtTrie) UpdateStorage(_ common.Address, _, _ []byte) error {
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteAccount adds a delete leaf op for the account.
|
// DeleteAccount queues deletion of account values.
|
||||||
func (t *NomtTrie) DeleteAccount(address common.Address) error {
|
// TODO(Phase E): implement.
|
||||||
keyPath := crypto.Keccak256Hash(address.Bytes())
|
func (t *NomtTrie) DeleteAccount(_ common.Address) error {
|
||||||
var kp core.KeyPath
|
|
||||||
copy(kp[:], keyPath[:])
|
|
||||||
|
|
||||||
t.pending = append(t.pending, core.LeafOp{
|
|
||||||
Key: kp,
|
|
||||||
Value: nil,
|
|
||||||
})
|
|
||||||
t.dirty = true
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteStorage adds a delete leaf op for a storage slot.
|
// DeleteStorage queues deletion of a storage slot.
|
||||||
func (t *NomtTrie) DeleteStorage(addr common.Address, key []byte) error {
|
// TODO(Phase E): implement.
|
||||||
kp := storageKeyPath(addr, key)
|
func (t *NomtTrie) DeleteStorage(_ common.Address, _ []byte) error {
|
||||||
t.pending = append(t.pending, core.LeafOp{
|
|
||||||
Key: kp,
|
|
||||||
Value: nil,
|
|
||||||
})
|
|
||||||
t.dirty = true
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateContractCode is a no-op for NOMT — code is stored in ethdb by the
|
// UpdateContractCode queues code chunk updates.
|
||||||
// state layer, not in the trie.
|
// TODO(Phase E): implement using ChunkifyCode + codeChunkStemAndSuffix.
|
||||||
func (t *NomtTrie) UpdateContractCode(address common.Address, codeHash common.Hash, code []byte) error {
|
func (t *NomtTrie) UpdateContractCode(_ common.Address, _ common.Hash, _ []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash returns the current root hash. If there are pending operations, they
|
// Hash returns the current root hash. Flushes pending updates first.
|
||||||
// are flushed to the NOMT engine first.
|
// TODO(Phase E): implement using groupAndHashStems + nomtDB.Update.
|
||||||
func (t *NomtTrie) Hash() common.Hash {
|
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
|
return t.root
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit flushes pending operations and returns the root hash.
|
// Commit flushes pending operations and returns the root hash.
|
||||||
// The returned NodeSet is empty since NOMT uses page-based storage, not
|
func (t *NomtTrie) Commit(_ bool) (common.Hash, *trienode.NodeSet) {
|
||||||
// individual node persistence.
|
|
||||||
func (t *NomtTrie) Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet) {
|
|
||||||
root := t.Hash()
|
root := t.Hash()
|
||||||
return root, trienode.NewNodeSet(common.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 {
|
func (t *NomtTrie) Witness() map[string][]byte {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NodeIterator returns an iterator over trie nodes. Not yet implemented.
|
// 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
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prove constructs a merkle proof. Not yet implemented.
|
// 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
|
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 {
|
func (t *NomtTrie) IsVerkle() bool {
|
||||||
return false
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy creates a deep copy of the trie.
|
// Copy creates a deep copy of the trie.
|
||||||
func (t *NomtTrie) Copy() *NomtTrie {
|
func (t *NomtTrie) Copy() *NomtTrie {
|
||||||
pending := make([]core.LeafOp, len(t.pending))
|
pending := make([]stemUpdate, len(t.pending))
|
||||||
copy(pending, t.pending)
|
copy(pending, t.pending)
|
||||||
return &NomtTrie{
|
return &NomtTrie{
|
||||||
nomtDB: t.nomtDB,
|
nomtDB: t.nomtDB,
|
||||||
|
|
@ -213,14 +146,3 @@ func (t *NomtTrie) Copy() *NomtTrie {
|
||||||
dirty: t.dirty,
|
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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")
|
|
||||||
}
|
|
||||||
43
trie/nomttrie/value_encoding.go
Normal file
43
trie/nomttrie/value_encoding.go
Normal 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
|
||||||
|
}
|
||||||
117
trie/nomttrie/value_encoding_test.go
Normal file
117
trie/nomttrie/value_encoding_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue