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