diff --git a/trie/nomttrie/key_encoding.go b/trie/nomttrie/key_encoding.go new file mode 100644 index 0000000000..7e7b194b31 --- /dev/null +++ b/trie/nomttrie/key_encoding.go @@ -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[:])) +} diff --git a/trie/nomttrie/key_encoding_test.go b/trie/nomttrie/key_encoding_test.go new file mode 100644 index 0000000000..e356d6508c --- /dev/null +++ b/trie/nomttrie/key_encoding_test.go @@ -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) +} diff --git a/trie/nomttrie/trie.go b/trie/nomttrie/trie.go index a0df2d82d3..7c835edc66 100644 --- a/trie/nomttrie/trie.go +++ b/trie/nomttrie/trie.go @@ -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 -} diff --git a/trie/nomttrie/trie_test.go b/trie/nomttrie/trie_test.go deleted file mode 100644 index 3555691ca0..0000000000 --- a/trie/nomttrie/trie_test.go +++ /dev/null @@ -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") -} diff --git a/trie/nomttrie/value_encoding.go b/trie/nomttrie/value_encoding.go new file mode 100644 index 0000000000..550d994f8a --- /dev/null +++ b/trie/nomttrie/value_encoding.go @@ -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 +} diff --git a/trie/nomttrie/value_encoding_test.go b/trie/nomttrie/value_encoding_test.go new file mode 100644 index 0000000000..85643edcf4 --- /dev/null +++ b/trie/nomttrie/value_encoding_test.go @@ -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) + }) + } +}