From 03911b11ebafdcf02e7519dcbb1211a33b0d076d Mon Sep 17 00:00:00 2001 From: MariusVanDerWijden Date: Mon, 22 Jun 2026 17:17:50 +0200 Subject: [PATCH 1/2] triedb/pathdb: improve hot account retrievals before: 595 ms after 48ms on layer 1 before: 6050 ms after: 4960ms on layer 127 --- core/state/database_history.go | 2 +- core/state/reader.go | 2 +- core/state/reader_test.go | 93 ++++++++++++++++++ triedb/pathdb/buffer.go | 6 +- triedb/pathdb/database.go | 10 ++ triedb/pathdb/difflayer.go | 27 +++++- triedb/pathdb/disklayer.go | 40 +++++--- triedb/pathdb/flush.go | 11 ++- triedb/pathdb/generate_test.go | 2 +- triedb/pathdb/iterator_fast.go | 15 ++- triedb/pathdb/iterator_test.go | 33 ++++--- triedb/pathdb/reader.go | 23 +++-- triedb/pathdb/states.go | 116 ++++++++++++++++++---- triedb/pathdb/states_test.go | 169 ++++++++++++++++++++++----------- 14 files changed, 433 insertions(+), 116 deletions(-) create mode 100644 core/state/reader_test.go diff --git a/core/state/database_history.go b/core/state/database_history.go index fbf4ab5f9c..170f79da93 100644 --- a/core/state/database_history.go +++ b/core/state/database_history.go @@ -62,7 +62,7 @@ func (r *historicStateReader) Account(addr common.Address) (*types.StateAccount, Root: common.BytesToHash(account.Root), } if len(acct.CodeHash) == 0 { - acct.CodeHash = types.EmptyCodeHash.Bytes() + acct.CodeHash = types.EmptyCodeHash[:] } if acct.Root == (common.Hash{}) { acct.Root = types.EmptyRootHash diff --git a/core/state/reader.go b/core/state/reader.go index be07cec0f9..2163da8a9f 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -112,7 +112,7 @@ func (r *flatReader) Account(addr common.Address) (*types.StateAccount, error) { Root: common.BytesToHash(account.Root), } if len(acct.CodeHash) == 0 { - acct.CodeHash = types.EmptyCodeHash.Bytes() + acct.CodeHash = types.EmptyCodeHash[:] } if acct.Root == (common.Hash{}) { acct.Root = types.EmptyRootHash diff --git a/core/state/reader_test.go b/core/state/reader_test.go new file mode 100644 index 0000000000..7a38971f28 --- /dev/null +++ b/core/state/reader_test.go @@ -0,0 +1,93 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package state + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/holiman/uint256" +) + +// benchStateReader is a minimal database.StateReader backed by an in-memory map. +// It isolates the conversion overhead in flatReader.Account (address hashing, +// the *types.StateAccount allocation and the slim->full fixups) from any trie, +// snapshot or disk access. +type benchStateReader struct { + account *types.SlimAccount +} + +func (r *benchStateReader) Account(hash common.Hash) (*types.SlimAccount, error) { + // Return a fresh copy on every call, mirroring the contract that the + // returned account is safe to modify by the caller. + if r.account == nil { + return nil, nil + } + cpy := *r.account + return &cpy, nil +} + +func (r *benchStateReader) Storage(accountHash, storageHash common.Hash) ([]byte, error) { + return nil, nil +} + +// benchmarkFlatReaderAccount measures flatReader.Account for a single address. +// The provided slim account dictates which branches of the conversion are hit: +// a slim account with nil Root/CodeHash exercises the EmptyCodeHash.Bytes() and +// EmptyRootHash fixups (the lines that dominate the production profile), whereas +// a fully populated one skips them. +func benchmarkFlatReaderAccount(b *testing.B, slim *types.SlimAccount) { + r := newFlatReader(&benchStateReader{account: slim}) + addr := common.HexToAddress("0x1234567890123456789012345678901234567890") + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + acct, err := r.Account(addr) + if err != nil { + b.Fatal(err) + } + if acct == nil { + b.Fatal("unexpected nil account") + } + } +} + +// BenchmarkFlatReaderAccountEmpty benchmarks the common EOA case: an account +// with no code and no storage. This is the hot path from the profile, hitting +// both the EmptyCodeHash and EmptyRootHash fixups. +func BenchmarkFlatReaderAccountEmpty(b *testing.B) { + benchmarkFlatReaderAccount(b, &types.SlimAccount{ + Nonce: 1, + Balance: uint256.NewInt(100), + // Root and CodeHash left nil: slim encoding of an EOA. + }) +} + +// BenchmarkFlatReaderAccountContract benchmarks a contract account with a +// non-empty storage root and code hash, skipping the empty-value fixups. +func BenchmarkFlatReaderAccountContract(b *testing.B) { + root := common.HexToHash("0xaabbccddeeff00112233445566778899aabbccddeeff00112233445566778899") + codeHash := common.HexToHash("0x112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00") + benchmarkFlatReaderAccount(b, &types.SlimAccount{ + Nonce: 7, + Balance: uint256.NewInt(1000), + Root: root.Bytes(), + CodeHash: codeHash.Bytes(), + }) +} diff --git a/triedb/pathdb/buffer.go b/triedb/pathdb/buffer.go index 5d3099285f..7afc1826c7 100644 --- a/triedb/pathdb/buffer.go +++ b/triedb/pathdb/buffer.go @@ -24,6 +24,7 @@ import ( "github.com/VictoriaMetrics/fastcache" "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/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/trie/trienode" @@ -64,8 +65,9 @@ func newBuffer(limit int, nodes *nodeSet, states *stateSet, layers uint64) *buff } } -// account retrieves the account blob with account address hash. -func (b *buffer) account(hash common.Hash) ([]byte, bool) { +// account retrieves the decoded slim account with account address hash. A nil +// account with found=true denotes a known-deleted account. +func (b *buffer) account(hash common.Hash) (*types.SlimAccount, bool) { return b.states.account(hash) } diff --git a/triedb/pathdb/database.go b/triedb/pathdb/database.go index e52949c93e..ca31647997 100644 --- a/triedb/pathdb/database.go +++ b/triedb/pathdb/database.go @@ -56,6 +56,16 @@ type layer interface { // - no error will be returned if the requested account is not found in database. account(hash common.Hash, depth int) ([]byte, error) + // accountObject directly retrieves the decoded slim account associated with + // a particular hash. It mirrors account but avoids re-encoding/decoding for + // the diff-layer hot read path, where accounts are already held in decoded + // form. An error will be returned if the read operation exits abnormally. + // + // Note: + // - the returned account is not a copy, please don't modify it. + // - a nil account is returned if the requested account is not found or was deleted. + accountObject(hash common.Hash, depth int) (*types.SlimAccount, error) + // storage directly retrieves the storage data associated with a particular hash, // within a particular account. An error will be returned if the read operation // exits abnormally. Specifically, if the layer is already stale. diff --git a/triedb/pathdb/difflayer.go b/triedb/pathdb/difflayer.go index 8ca3a39cf3..f92b380118 100644 --- a/triedb/pathdb/difflayer.go +++ b/triedb/pathdb/difflayer.go @@ -21,6 +21,7 @@ import ( "sync" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" ) @@ -102,25 +103,41 @@ func (dl *diffLayer) node(owner common.Hash, path []byte, depth int) ([]byte, co // // Note the returned account is not a copy, please don't modify it. func (dl *diffLayer) account(hash common.Hash, depth int) ([]byte, error) { + account, err := dl.accountObject(hash, depth) + if err != nil { + return nil, err + } + // Re-encode into slim-RLP form for the byte-oriented callers (e.g. the + // binary iterator and the public AccountRLP). The hot read path uses + // accountObject directly and avoids this encode. + return encodeSlimAccount(account), nil +} + +// accountObject directly retrieves the decoded slim account associated with a +// particular hash. A nil account is returned if the account does not exist or +// was deleted in this hierarchy. +// +// Note the returned account is not a copy, please don't modify it. +func (dl *diffLayer) accountObject(hash common.Hash, depth int) (*types.SlimAccount, error) { // Hold the lock, ensure the parent won't be changed during the // state accessing. dl.lock.RLock() defer dl.lock.RUnlock() - if blob, found := dl.states.account(hash); found { + if account, found := dl.states.account(hash); found { dirtyStateHitMeter.Mark(1) dirtyStateHitDepthHist.Update(int64(depth)) - dirtyStateReadMeter.Mark(int64(len(blob))) + dirtyStateReadMeter.Mark(int64(slimAccountSize(account))) - if len(blob) == 0 { + if account == nil { stateAccountInexMeter.Mark(1) } else { stateAccountExistMeter.Mark(1) } - return blob, nil + return account, nil } // Account is unknown to this layer, resolve from parent - return dl.parent.account(hash, depth+1) + return dl.parent.accountObject(hash, depth+1) } // storage directly retrieves the storage data associated with a particular hash, diff --git a/triedb/pathdb/disklayer.go b/triedb/pathdb/disklayer.go index 8c0a751932..27a73af53b 100644 --- a/triedb/pathdb/disklayer.go +++ b/triedb/pathdb/disklayer.go @@ -25,6 +25,7 @@ import ( "github.com/VictoriaMetrics/fastcache" "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/ethdb" "github.com/ethereum/go-ethereum/log" @@ -169,29 +170,42 @@ func (dl *diskLayer) node(owner common.Hash, path []byte, depth int) ([]byte, co // // Note the returned account is not a copy, please don't modify it. func (dl *diskLayer) account(hash common.Hash, depth int) ([]byte, error) { + account, err := dl.accountObject(hash, depth) + if err != nil { + return nil, err + } + return encodeSlimAccount(account), nil +} + +// accountObject directly retrieves the decoded slim account associated with a +// particular hash. A nil account is returned if the account does not exist or +// was deleted. +func (dl *diskLayer) accountObject(hash common.Hash, depth int) (*types.SlimAccount, error) { dl.lock.RLock() defer dl.lock.RUnlock() if dl.stale { return nil, errSnapshotStale } - // Try to retrieve the trie node from the not-yet-written node buffer first - // (both the live one and the frozen one). Note the buffer is lock free since - // it's impossible to mutate the buffer before tagging the layer as stale. + // Try to retrieve the account from the not-yet-written state buffer first + // (both the live one and the frozen one). The buffer holds accounts in + // decoded form, so no decoding is required here. Note the buffer is lock + // free since it's impossible to mutate the buffer before tagging the layer + // as stale. for _, buffer := range []*buffer{dl.buffer, dl.frozen} { if buffer != nil { - blob, found := buffer.account(hash) + account, found := buffer.account(hash) if found { dirtyStateHitMeter.Mark(1) - dirtyStateReadMeter.Mark(int64(len(blob))) + dirtyStateReadMeter.Mark(int64(slimAccountSize(account))) dirtyStateHitDepthHist.Update(int64(depth)) - if len(blob) == 0 { + if account == nil { stateAccountInexMeter.Mark(1) } else { stateAccountExistMeter.Mark(1) } - return blob, nil + return account, nil } } } @@ -214,7 +228,7 @@ func (dl *diskLayer) account(hash common.Hash, depth int) ([]byte, error) { } else { stateAccountExistMeter.Mark(1) } - return blob, nil + return decodeAccountBlob(hash, blob), nil } cleanStateMissMeter.Mark(1) } @@ -237,7 +251,7 @@ func (dl *diskLayer) account(hash common.Hash, depth int) ([]byte, error) { stateAccountExistMeter.Mark(1) stateAccountExistDiskMeter.Mark(1) } - return blob, nil + return decodeAccountBlob(hash, blob), nil } // storage directly retrieves the storage data associated with a particular hash, @@ -598,8 +612,12 @@ func (dl *diskLayer) revert(h *stateHistory) (*diskLayer, error) { batch := dl.db.diskdb.NewBatch() writeNodes(batch, nodes, dl.nodes) - // Provide the original values of modified accounts and storages for revert - writeStates(batch, progress, accounts, storages, dl.states) + // Provide the original values of modified accounts and storages for revert. + // The history-derived accounts are in slim-RLP form; decode them so they + // share the single encode point in writeStates. This path is only hit on a + // deep reorg that rolls back the persistent layer, so the round-trip cost + // is irrelevant. + writeStates(batch, progress, decodeAccounts(accounts), storages, dl.states) rawdb.WritePersistentStateID(batch, dl.id-1) rawdb.WriteSnapshotRoot(batch, h.meta.parent) if err := batch.Write(); err != nil { diff --git a/triedb/pathdb/flush.go b/triedb/pathdb/flush.go index 4f816cf6a6..8d7f791e3b 100644 --- a/triedb/pathdb/flush.go +++ b/triedb/pathdb/flush.go @@ -22,6 +22,7 @@ import ( "github.com/VictoriaMetrics/fastcache" "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/ethdb" "github.com/ethereum/go-ethereum/trie/trienode" ) @@ -74,12 +75,12 @@ func writeNodes(batch ethdb.Batch, nodes map[common.Hash]map[string]*trienode.No // TODO(rjl493456442) do we really need this generation marker? The state updates // after the marker can also be written and will be fixed by generator later if // it's outdated. -func writeStates(batch ethdb.Batch, genMarker []byte, accountData map[common.Hash][]byte, storageData map[common.Hash]map[common.Hash][]byte, clean *fastcache.Cache) (int, int) { +func writeStates(batch ethdb.Batch, genMarker []byte, accountData map[common.Hash]*types.SlimAccount, storageData map[common.Hash]map[common.Hash][]byte, clean *fastcache.Cache) (int, int) { var ( accounts int slots int ) - for addrHash, blob := range accountData { + for addrHash, account := range accountData { // Skip any account not yet covered by the snapshot. The account // at the generation marker position (addrHash == genMarker[:common.HashLength]) // should still be updated, as it would be skipped in the next @@ -88,12 +89,16 @@ func writeStates(batch ethdb.Batch, genMarker []byte, accountData map[common.Has continue } accounts += 1 - if len(blob) == 0 { + if account == nil { rawdb.DeleteAccountSnapshot(batch, addrHash) if clean != nil { clean.Set(addrHash[:], nil) } } else { + // Encode the decoded account back into slim-RLP form for storage. + // This is the single point where the in-memory decoded accounts + // are serialized for the on-disk snapshot. + blob := encodeSlimAccount(account) rawdb.WriteAccountSnapshot(batch, addrHash, blob) if clean != nil { clean.Set(addrHash[:], blob) diff --git a/triedb/pathdb/generate_test.go b/triedb/pathdb/generate_test.go index f38a1ed7c4..a3c13f7c42 100644 --- a/triedb/pathdb/generate_test.go +++ b/triedb/pathdb/generate_test.go @@ -69,7 +69,7 @@ func (t *genTester) addTrieAccount(acckey string, acc *types.StateAccount) { ) t.acctTrie.MustUpdate(key.Bytes(), val) - t.states.accountData[key] = val + t.states.accountData[key] = decodeAccountBlob(key, types.SlimAccountRLP(*acc)) t.states.accountOrigin[addr] = nil } diff --git a/triedb/pathdb/iterator_fast.go b/triedb/pathdb/iterator_fast.go index 04bf89e874..0f8b931a1f 100644 --- a/triedb/pathdb/iterator_fast.go +++ b/triedb/pathdb/iterator_fast.go @@ -98,7 +98,11 @@ func newFastIterator(db *Database, root common.Hash, account common.Hash, seek c if dl.stale { return nil, errSnapshotStale } - return dl.buffer.states.mustAccount(hash) + account, err := dl.buffer.states.mustAccount(hash) + if err != nil { + return nil, err + } + return encodeSlimAccount(account), nil }), priority: depth, }) @@ -110,8 +114,15 @@ func newFastIterator(db *Database, root common.Hash, account common.Hash, seek c // The state set in diff layer is immutable and will never be stale, // so the read lock protection is unnecessary. accountList := dl.states.accountList() + states := dl.states fi.iterators = append(fi.iterators, &weightedIterator{ - it: newDiffAccountIterator(seek, accountList, dl.states.mustAccount), + it: newDiffAccountIterator(seek, accountList, func(hash common.Hash) ([]byte, error) { + account, err := states.mustAccount(hash) + if err != nil { + return nil, err + } + return encodeSlimAccount(account), nil + }), priority: depth, }) } diff --git a/triedb/pathdb/iterator_test.go b/triedb/pathdb/iterator_test.go index 191c2fadf5..3bce1989ea 100644 --- a/triedb/pathdb/iterator_test.go +++ b/triedb/pathdb/iterator_test.go @@ -122,8 +122,7 @@ func TestAccountIteratorBasics(t *testing.T) { // Fill up a parent for i := 0; i < 100; i++ { hash := testrand.Hash() - data := testrand.Bytes(32) - accounts[hash] = data + accounts[hash] = randomAccount() if rand.Intn(2) == 0 { accStorage := make(map[common.Hash][]byte) @@ -153,7 +152,7 @@ func TestStorageIteratorBasics(t *testing.T) { // Fill some random data for i := 0; i < 10; i++ { hash := testrand.Hash() - accounts[hash] = testrand.Bytes(32) + accounts[hash] = randomAccount() accStorage := make(map[common.Hash][]byte) var nilstorage int @@ -344,6 +343,18 @@ func TestAccountIteratorTraversalValues(t *testing.T) { } db := New(rawdb.NewMemoryDatabase(), config, false) + // layerAccount returns a valid slim-RLP account whose nonce uniquely encodes + // the (layer, key) pair, so that values are distinct per layer and the + // iterator's "newest layer wins" behavior is exercised. + layerAccount := func(layer, key byte) []byte { + return types.SlimAccountRLP(types.StateAccount{ + Nonce: uint64(layer)<<8 | uint64(key), + Balance: uint256.NewInt(uint64(key)), + Root: types.EmptyRootHash, + CodeHash: types.EmptyCodeHash[:], + }) + } + // Create a batch of account sets to seed subsequent layers with var ( a = make(map[common.Hash][]byte) @@ -356,27 +367,27 @@ func TestAccountIteratorTraversalValues(t *testing.T) { h = make(map[common.Hash][]byte) ) for i := byte(2); i < 0xff; i++ { - a[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 0, i) + a[common.Hash{i}] = layerAccount(0, i) if i > 20 && i%2 == 0 { - b[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 1, i) + b[common.Hash{i}] = layerAccount(1, i) } if i%4 == 0 { - c[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 2, i) + c[common.Hash{i}] = layerAccount(2, i) } if i%7 == 0 { - d[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 3, i) + d[common.Hash{i}] = layerAccount(3, i) } if i%8 == 0 { - e[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 4, i) + e[common.Hash{i}] = layerAccount(4, i) } if i > 50 && i < 85 { - f[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 5, i) + f[common.Hash{i}] = layerAccount(5, i) } if i%64 == 0 { - g[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 6, i) + g[common.Hash{i}] = layerAccount(6, i) } if i%128 == 0 { - h[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 7, i) + h[common.Hash{i}] = layerAccount(7, i) } } // Assemble a stack of snapshots from the account layers diff --git a/triedb/pathdb/reader.go b/triedb/pathdb/reader.go index e087ef26ed..9a006a2dbd 100644 --- a/triedb/pathdb/reader.go +++ b/triedb/pathdb/reader.go @@ -127,18 +127,27 @@ func (r *reader) AccountRLP(hash common.Hash) ([]byte, error) { // - the returned account object is safe to modify // - no error will be returned if the requested account is not found in database func (r *reader) Account(hash common.Hash) (*types.SlimAccount, error) { - blob, err := r.AccountRLP(hash) + l, err := r.db.tree.lookupAccount(hash, r.state) if err != nil { return nil, err } - if len(blob) == 0 { + // If the located layer is stale, fall back to the slow path to retrieve + // the account. See AccountRLP for the rationale of this fallback. + account, err := l.accountObject(hash, 0) + if errors.Is(err, errSnapshotStale) { + account, err = r.layer.accountObject(hash, 0) + } + if err != nil { + return nil, err + } + if account == nil { return nil, nil } - account := new(types.SlimAccount) - if err := rlp.DecodeBytes(blob, account); err != nil { - panic(err) - } - return account, nil + // The diff layers retain the account in decoded form and the returned + // object is documented as safe to modify, so hand back a shallow copy to + // protect the cached instance from caller mutation. + cpy := *account + return &cpy, nil } // Storage directly retrieves the storage data associated with a particular hash, diff --git a/triedb/pathdb/states.go b/triedb/pathdb/states.go index 27a6c1d422..6c79e62bfb 100644 --- a/triedb/pathdb/states.go +++ b/triedb/pathdb/states.go @@ -26,6 +26,7 @@ import ( "github.com/VictoriaMetrics/fastcache" "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/ethdb" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" @@ -60,7 +61,7 @@ func (c *counter) report(count, size *metrics.Meter) { // subsequent state access will be denied due to the stale flag. Therefore, // state access and mutation won't happen at the same time with guarantee. type stateSet struct { - accountData map[common.Hash][]byte // Keyed accounts for direct retrieval (nil means deleted) + accountData map[common.Hash]*types.SlimAccount // Keyed accounts for direct retrieval (nil means deleted) storageData map[common.Hash]map[common.Hash][]byte // Keyed storage slots for direct retrieval. one per account (nil means deleted) size uint64 // Memory size of the state data (accountData and storageData) @@ -75,17 +76,81 @@ type stateSet struct { listLock sync.RWMutex } +// slimAccountSize returns the approximate encoded size of a slim account, used +// for memory accounting. It estimates the slim-RLP byte length without actually +// encoding the account. A nil account (deleted marker) contributes zero. +func slimAccountSize(account *types.SlimAccount) int { + if account == nil { + return 0 + } + // 9 bytes covers the RLP list header plus the nonce; the balance, root and + // code hash contribute their own byte lengths. This intentionally mirrors + // the previous behavior of accounting for the encoded blob length. + size := 9 + if account.Balance != nil { + size += account.Balance.ByteLen() + } + size += len(account.Root) + size += len(account.CodeHash) + return size +} + +// decodeAccountBlob decodes a single slim-RLP-encoded account blob. A nil/empty +// blob denotes a non-existent or deleted account and decodes to a nil account. +func decodeAccountBlob(hash common.Hash, blob []byte) *types.SlimAccount { + if len(blob) == 0 { + return nil + } + account := new(types.SlimAccount) + if err := rlp.DecodeBytes(blob, account); err != nil { + panic(fmt.Sprintf("failed to decode account %x: %v", hash, err)) + } + return account +} + +// decodeAccounts converts a map of slim-RLP-encoded accounts into decoded slim +// account objects. A nil/empty blob denotes a deleted account and is preserved +// as a nil entry in the resulting map. +func decodeAccounts(accounts map[common.Hash][]byte) map[common.Hash]*types.SlimAccount { + decoded := make(map[common.Hash]*types.SlimAccount, len(accounts)) + for hash, blob := range accounts { + if len(blob) == 0 { + decoded[hash] = nil // deleted account + continue + } + account := new(types.SlimAccount) + if err := rlp.DecodeBytes(blob, account); err != nil { + panic(fmt.Sprintf("failed to decode account %x: %v", hash, err)) + } + decoded[hash] = account + } + return decoded +} + +// encodeSlimAccount serializes a decoded slim account back into its slim-RLP +// byte form. A nil account (deleted marker) encodes to a nil blob. +func encodeSlimAccount(account *types.SlimAccount) []byte { + if account == nil { + return nil + } + blob, err := rlp.EncodeToBytes(account) + if err != nil { + panic(fmt.Sprintf("failed to encode account: %v", err)) + } + return blob +} + // newStates constructs the state set with the provided account and storage data. +// The accounts are supplied in slim-RLP form and are decoded once on ingest; +// they are kept in decoded form for the lifetime of the set and only re-encoded +// at the serialization boundaries (journal and disk flush). func newStates(accounts map[common.Hash][]byte, storages map[common.Hash]map[common.Hash][]byte, rawStorageKey bool) *stateSet { // Don't panic for the lazy callers, initialize the nil maps instead. - if accounts == nil { - accounts = make(map[common.Hash][]byte) - } if storages == nil { storages = make(map[common.Hash]map[common.Hash][]byte) } s := &stateSet{ - accountData: accounts, + accountData: decodeAccounts(accounts), storageData: storages, rawStorageKey: rawStorageKey, storageListSorted: make(map[common.Hash][]common.Hash), @@ -95,7 +160,9 @@ func newStates(accounts map[common.Hash][]byte, storages map[common.Hash]map[com } // account returns the account data associated with the specified address hash. -func (s *stateSet) account(hash common.Hash) ([]byte, bool) { +// A nil account with found=true denotes a known-deleted account, whereas +// found=false indicates the account is unknown in this set. +func (s *stateSet) account(hash common.Hash) (*types.SlimAccount, bool) { // If the account is known locally, return it if data, ok := s.accountData[hash]; ok { return data, true @@ -106,7 +173,7 @@ func (s *stateSet) account(hash common.Hash) ([]byte, bool) { // mustAccount returns the account data associated with the specified address // hash. The difference is this function will return an error if the account // is not found. -func (s *stateSet) mustAccount(hash common.Hash) ([]byte, error) { +func (s *stateSet) mustAccount(hash common.Hash) (*types.SlimAccount, error) { // If the account is known locally, return it if data, ok := s.accountData[hash]; ok { return data, nil @@ -143,8 +210,8 @@ func (s *stateSet) mustStorage(accountHash, storageHash common.Hash) ([]byte, er // Additionally, it computes the total memory size occupied by the maps. func (s *stateSet) check() uint64 { var size int - for _, blob := range s.accountData { - size += common.HashLength + len(blob) + for _, account := range s.accountData { + size += common.HashLength + slimAccountSize(account) } for accountHash, slots := range s.storageData { if slots == nil { @@ -235,11 +302,13 @@ func (s *stateSet) merge(other *stateSet) { ) // Apply the updated account data for accountHash, data := range other.accountData { + dataSize := slimAccountSize(data) if origin, ok := s.accountData[accountHash]; ok { - delta += len(data) - len(origin) - accountOverwrites.add(common.HashLength + len(origin)) + originSize := slimAccountSize(origin) + delta += dataSize - originSize + accountOverwrites.add(common.HashLength + originSize) } else { - delta += common.HashLength + len(data) + delta += common.HashLength + dataSize } s.accountData[accountHash] = data } @@ -293,11 +362,20 @@ func (s *stateSet) revertTo(accountOrigin map[common.Hash][]byte, storageOrigin if !ok { panic(fmt.Sprintf("non-existent account for reverting, %x", addrHash)) } - if len(data) == 0 && len(blob) == 0 { + // Decode the original slim-RLP value; an empty blob denotes a deleted + // account, represented as a nil entry. + var origin *types.SlimAccount + if len(blob) != 0 { + origin = new(types.SlimAccount) + if err := rlp.DecodeBytes(blob, origin); err != nil { + panic(fmt.Sprintf("failed to decode account %x: %v", addrHash, err)) + } + } + if data == nil && origin == nil { panic(fmt.Sprintf("invalid account mutation (null to null), %x", addrHash)) } - delta += len(blob) - len(data) - s.accountData[addrHash] = blob + delta += slimAccountSize(origin) - slimAccountSize(data) + s.accountData[addrHash] = origin } // Overwrite the storage data with original value blindly for addrHash, storage := range storageOrigin { @@ -346,9 +424,9 @@ func (s *stateSet) encode(w io.Writer) error { AddrHashes: make([]common.Hash, 0, len(s.accountData)), Accounts: make([][]byte, 0, len(s.accountData)), } - for addrHash, blob := range s.accountData { + for addrHash, account := range s.accountData { enc.AddrHashes = append(enc.AddrHashes, addrHash) - enc.Accounts = append(enc.Accounts, blob) + enc.Accounts = append(enc.Accounts, encodeSlimAccount(account)) } if err := rlp.Encode(w, enc); err != nil { return err @@ -395,7 +473,7 @@ func (s *stateSet) decode(r *rlp.Stream) error { for i := range dec.AddrHashes { accountSet[dec.AddrHashes[i]] = empty2nil(dec.Accounts[i]) } - s.accountData = accountSet + s.accountData = decodeAccounts(accountSet) // Decode storages type storage struct { @@ -431,7 +509,7 @@ func (s *stateSet) write(batch ethdb.Batch, genMarker []byte, clean *fastcache.C // reset clears all cached state data, including any optional sorted lists that // may have been generated. func (s *stateSet) reset() { - s.accountData = make(map[common.Hash][]byte) + s.accountData = make(map[common.Hash]*types.SlimAccount) s.storageData = make(map[common.Hash]map[common.Hash][]byte) s.size = 0 s.accountListSorted = nil diff --git a/triedb/pathdb/states_test.go b/triedb/pathdb/states_test.go index 4d181cc914..ff1a1078e0 100644 --- a/triedb/pathdb/states_test.go +++ b/triedb/pathdb/states_test.go @@ -22,15 +22,39 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/internal/testrand" "github.com/ethereum/go-ethereum/rlp" + "github.com/holiman/uint256" ) +// testAccountRLP returns the slim-RLP encoding of a deterministic account whose +// nonce is derived from the provided tag. It lets the state-set tests use real, +// decodable accounts while keeping the tag values readable. +func testAccountRLP(tag byte) []byte { + return types.SlimAccountRLP(types.StateAccount{ + Nonce: uint64(tag), + Balance: uint256.NewInt(uint64(tag)), + Root: types.EmptyRootHash, + CodeHash: types.EmptyCodeHash[:], + }) +} + +// accountEqualsTag reports whether the decoded account matches the account +// produced by testAccountRLP for the given tag. +func accountEqualsTag(account *types.SlimAccount, tag byte) bool { + if account == nil { + return false + } + return bytes.Equal(encodeSlimAccount(account), testAccountRLP(tag)) +} + func TestStatesMerge(t *testing.T) { a := newStates( map[common.Hash][]byte{ - {0xa}: {0xa0}, - {0xb}: {0xb0}, - {0xc}: {0xc0}, + {0xa}: testAccountRLP(0xa0), + {0xb}: testAccountRLP(0xb0), + {0xc}: testAccountRLP(0xc0), }, map[common.Hash]map[common.Hash][]byte{ {0xa}: { @@ -48,8 +72,8 @@ func TestStatesMerge(t *testing.T) { ) b := newStates( map[common.Hash][]byte{ - {0xa}: {0xa1}, - {0xb}: {0xb1}, + {0xa}: testAccountRLP(0xa1), + {0xb}: testAccountRLP(0xb1), {0xc}: nil, // delete account }, map[common.Hash]map[common.Hash][]byte{ @@ -69,25 +93,25 @@ func TestStatesMerge(t *testing.T) { ) a.merge(b) - blob, exist := a.account(common.Hash{0xa}) - if !exist || !bytes.Equal(blob, []byte{0xa1}) { + account, exist := a.account(common.Hash{0xa}) + if !exist || !accountEqualsTag(account, 0xa1) { t.Error("Unexpected value for account a") } - blob, exist = a.account(common.Hash{0xb}) - if !exist || !bytes.Equal(blob, []byte{0xb1}) { + account, exist = a.account(common.Hash{0xb}) + if !exist || !accountEqualsTag(account, 0xb1) { t.Error("Unexpected value for account b") } - blob, exist = a.account(common.Hash{0xc}) - if !exist || len(blob) != 0 { + account, exist = a.account(common.Hash{0xc}) + if !exist || account != nil { t.Error("Unexpected value for account c") } // unknown account - blob, exist = a.account(common.Hash{0xd}) - if exist || len(blob) != 0 { + account, exist = a.account(common.Hash{0xd}) + if exist || account != nil { t.Error("Unexpected value for account d") } - blob, exist = a.storage(common.Hash{0xa}, common.Hash{0x1}) + blob, exist := a.storage(common.Hash{0xa}, common.Hash{0x1}) if !exist || !bytes.Equal(blob, []byte{0x11}) { t.Error("Unexpected value for a's storage") } @@ -118,9 +142,9 @@ func TestStatesMerge(t *testing.T) { func TestStatesRevert(t *testing.T) { a := newStates( map[common.Hash][]byte{ - {0xa}: {0xa0}, - {0xb}: {0xb0}, - {0xc}: {0xc0}, + {0xa}: testAccountRLP(0xa0), + {0xb}: testAccountRLP(0xb0), + {0xc}: testAccountRLP(0xc0), }, map[common.Hash]map[common.Hash][]byte{ {0xa}: { @@ -138,8 +162,8 @@ func TestStatesRevert(t *testing.T) { ) b := newStates( map[common.Hash][]byte{ - {0xa}: {0xa1}, - {0xb}: {0xb1}, + {0xa}: testAccountRLP(0xa1), + {0xb}: testAccountRLP(0xb1), {0xc}: nil, }, map[common.Hash]map[common.Hash][]byte{ @@ -160,9 +184,9 @@ func TestStatesRevert(t *testing.T) { a.merge(b) a.revertTo( map[common.Hash][]byte{ - {0xa}: {0xa0}, - {0xb}: {0xb0}, - {0xc}: {0xc0}, + {0xa}: testAccountRLP(0xa0), + {0xb}: testAccountRLP(0xb0), + {0xc}: testAccountRLP(0xc0), }, map[common.Hash]map[common.Hash][]byte{ {0xa}: { @@ -179,25 +203,25 @@ func TestStatesRevert(t *testing.T) { }, ) - blob, exist := a.account(common.Hash{0xa}) - if !exist || !bytes.Equal(blob, []byte{0xa0}) { + account, exist := a.account(common.Hash{0xa}) + if !exist || !accountEqualsTag(account, 0xa0) { t.Error("Unexpected value for account a") } - blob, exist = a.account(common.Hash{0xb}) - if !exist || !bytes.Equal(blob, []byte{0xb0}) { + account, exist = a.account(common.Hash{0xb}) + if !exist || !accountEqualsTag(account, 0xb0) { t.Error("Unexpected value for account b") } - blob, exist = a.account(common.Hash{0xc}) - if !exist || !bytes.Equal(blob, []byte{0xc0}) { + account, exist = a.account(common.Hash{0xc}) + if !exist || !accountEqualsTag(account, 0xc0) { t.Error("Unexpected value for account c") } // unknown account - blob, exist = a.account(common.Hash{0xd}) - if exist || len(blob) != 0 { + account, exist = a.account(common.Hash{0xd}) + if exist || account != nil { t.Error("Unexpected value for account d") } - blob, exist = a.storage(common.Hash{0xa}, common.Hash{0x1}) + blob, exist := a.storage(common.Hash{0xa}, common.Hash{0x1}) if !exist || !bytes.Equal(blob, []byte{0x10}) { t.Error("Unexpected value for a's storage") } @@ -231,7 +255,7 @@ func TestStateRevertAccountNullMarker(t *testing.T) { a := newStates(nil, nil, false) // empty initial state b := newStates( map[common.Hash][]byte{ - {0xa}: {0xa}, + {0xa}: testAccountRLP(0xa), }, nil, false, @@ -244,12 +268,12 @@ func TestStateRevertAccountNullMarker(t *testing.T) { nil, ) // revert the transition b - blob, exist := a.account(common.Hash{0xa}) + account, exist := a.account(common.Hash{0xa}) if !exist { t.Fatal("null marker is not found") } - if len(blob) != 0 { - t.Fatalf("Unexpected value for account, %v", blob) + if account != nil { + t.Fatalf("Unexpected value for account, %v", account) } } @@ -258,7 +282,7 @@ func TestStateRevertAccountNullMarker(t *testing.T) { // entry in the set. func TestStateRevertStorageNullMarker(t *testing.T) { a := newStates(map[common.Hash][]byte{ - {0xa}: {0xa}, + {0xa}: testAccountRLP(0xa), }, nil, false) // initial state with account 0xa b := newStates( @@ -297,7 +321,7 @@ func TestStatesEncode(t *testing.T) { func testStatesEncode(t *testing.T, rawStorageKey bool) { s := newStates( map[common.Hash][]byte{ - {0x1}: {0x1}, + {0x1}: testAccountRLP(0x1), }, map[common.Hash]map[common.Hash][]byte{ {0x1}: { @@ -333,7 +357,7 @@ func TestStateWithOriginEncode(t *testing.T) { func testStateWithOriginEncode(t *testing.T, rawStorageKey bool) { s := NewStateSetWithOrigin( map[common.Hash][]byte{ - {0x1}: {0x1}, + {0x1}: testAccountRLP(0x1), }, map[common.Hash]map[common.Hash][]byte{ {0x1}: { @@ -375,17 +399,53 @@ func testStateWithOriginEncode(t *testing.T, rawStorageKey bool) { } } +// TestSlimAccountSize verifies the estimated memory size of a slim account for +// the empty/deleted, EOA and contract cases. The accountData map keys are +// accounted for separately (common.HashLength per entry), so this only covers +// the value contribution. +func TestSlimAccountSize(t *testing.T) { + // A nil account (deletion marker) contributes nothing. + if got := slimAccountSize(nil); got != 0 { + t.Fatalf("nil account size, want: 0, got: %d", got) + } + // An EOA with no code and empty storage root carries nil Root/CodeHash in + // slim form, so only the 9-byte list/nonce overhead plus the balance length + // is counted. + eoa := &types.SlimAccount{Nonce: 1, Balance: uint256.NewInt(0x100)} + if got, want := slimAccountSize(eoa), 9+eoa.Balance.ByteLen(); got != want { + t.Fatalf("EOA account size, want: %d, got: %d", want, got) + } + // A contract carries a 32-byte root and 32-byte code hash in addition. + contract := &types.SlimAccount{ + Nonce: 2, + Balance: uint256.NewInt(0x10000), + Root: testrand.Bytes(32), + CodeHash: testrand.Bytes(32), + } + if got, want := slimAccountSize(contract), 9+contract.Balance.ByteLen()+64; got != want { + t.Fatalf("contract account size, want: %d, got: %d", want, got) + } +} + +// TestStateSizeTracking verifies the storage-slot memory accounting through the +// merge and revert transitions. Accounts are all identical real accounts, so +// their size contribution is a constant (acctSize per entry) and the assertions +// focus on the storage deltas. func TestStateSizeTracking(t *testing.T) { - expSizeA := 3*(common.HashLength+1) + /* account data */ + // All accounts are identical, so each contributes the same estimated size. + acct := decodeAccountBlob(common.Hash{}, testAccountRLP(0x1)) + acctSize := common.HashLength + slimAccountSize(acct) + + expSizeA := 3*acctSize + /* account data: a, b, c */ 2*(2*common.HashLength+1) + /* storage data of 0xa */ 2*common.HashLength + 3 + /* storage data of 0xb */ 2*common.HashLength + 1 /* storage data of 0xc */ a := newStates( map[common.Hash][]byte{ - {0xa}: {0xa0}, // common.HashLength+1 - {0xb}: {0xb0}, // common.HashLength+1 - {0xc}: {0xc0}, // common.HashLength+1 + {0xa}: testAccountRLP(0x1), + {0xb}: testAccountRLP(0x1), + {0xc}: testAccountRLP(0x1), }, map[common.Hash]map[common.Hash][]byte{ {0xa}: { @@ -405,15 +465,15 @@ func TestStateSizeTracking(t *testing.T) { t.Fatalf("Unexpected size, want: %d, got: %d", expSizeA, a.size) } - expSizeB := common.HashLength + 2 + common.HashLength + 3 + common.HashLength + /* account data */ + expSizeB := 2*acctSize + common.HashLength + /* account data: a, b updated; c deleted (key only) */ 2*common.HashLength + 3 + 2*common.HashLength + 2 + /* storage data of 0xa */ 2*common.HashLength + 2 + 2*common.HashLength + 2 + /* storage data of 0xb */ 3*2*common.HashLength /* storage data of 0xc */ b := newStates( map[common.Hash][]byte{ - {0xa}: {0xa1, 0xa1}, // common.HashLength+2 - {0xb}: {0xb1, 0xb1, 0xb1}, // common.HashLength+3 - {0xc}: nil, // common.HashLength, account deletion + {0xa}: testAccountRLP(0x1), // account update (same size) + {0xb}: testAccountRLP(0x1), // account update (same size) + {0xc}: nil, // common.HashLength, account deletion }, map[common.Hash]map[common.Hash][]byte{ {0xa}: { @@ -438,10 +498,13 @@ func TestStateSizeTracking(t *testing.T) { } a.merge(b) - mergeSize := expSizeA + 1 /* account a data change */ + 2 /* account b data change */ - 1 /* account c data change */ - mergeSize += 2*common.HashLength + 2 + 2 /* storage a change */ - mergeSize += 2*common.HashLength + 2 - 1 /* storage b change */ - mergeSize += 2*2*common.HashLength - 1 /* storage data removal of 0xc */ + // Accounts a and b are overwritten with same-size accounts (no delta), but + // account c becomes a deletion marker, dropping its value contribution while + // retaining the key. + mergeSize := expSizeA - slimAccountSize(acct) /* account c value removed */ + mergeSize += 2*common.HashLength + 2 + 2 /* storage a change */ + mergeSize += 2*common.HashLength + 2 - 1 /* storage b change */ + mergeSize += 2*2*common.HashLength - 1 /* storage data removal of 0xc */ if a.size != uint64(mergeSize) { t.Fatalf("Unexpected size, want: %d, got: %d", mergeSize, a.size) @@ -450,9 +513,9 @@ func TestStateSizeTracking(t *testing.T) { // Revert the set to original status a.revertTo( map[common.Hash][]byte{ - {0xa}: {0xa0}, - {0xb}: {0xb0}, - {0xc}: {0xc0}, + {0xa}: testAccountRLP(0x1), + {0xb}: testAccountRLP(0x1), + {0xc}: testAccountRLP(0x1), }, map[common.Hash]map[common.Hash][]byte{ {0xa}: { From 987cfb98fb2af4bc853ef3fd940b883f4d88bfa8 Mon Sep 17 00:00:00 2001 From: MariusVanDerWijden Date: Mon, 22 Jun 2026 17:47:25 +0200 Subject: [PATCH 2/2] triedb/pathdb: improve EOA account retrieval Before: 612ns now 540ns Layer 1 now: 42ns 17x speedup Layer 127 4100ns --- core/state/database_mpt.go | 2 +- core/state/reader.go | 23 +---- core/state/reader_test.go | 40 ++++---- core/state/snapshot/snapshot.go | 48 ++++++++++ eth/protocols/snap/handlers.go | 4 +- triedb/database/database.go | 6 +- triedb/pathdb/buffer.go | 7 +- triedb/pathdb/database.go | 11 ++- triedb/pathdb/difflayer.go | 8 +- triedb/pathdb/disklayer.go | 8 +- triedb/pathdb/flush.go | 2 +- triedb/pathdb/reader.go | 6 +- triedb/pathdb/reader_bench_test.go | 145 +++++++++++++++++++++++++++++ triedb/pathdb/states.go | 82 +++++++--------- triedb/pathdb/states_test.go | 29 +++--- 15 files changed, 297 insertions(+), 124 deletions(-) create mode 100644 triedb/pathdb/reader_bench_test.go diff --git a/core/state/database_mpt.go b/core/state/database_mpt.go index 42c5f2e5ef..fd07523cef 100644 --- a/core/state/database_mpt.go +++ b/core/state/database_mpt.go @@ -66,7 +66,7 @@ func (db *MPTDatabase) StateReader(stateRoot common.Hash) (StateReader, error) { if db.TrieDB().Scheme() == rawdb.HashScheme && db.snap != nil { snap := db.snap.Snapshot(stateRoot) if snap != nil { - readers = append(readers, newFlatReader(snap)) + readers = append(readers, newFlatReader(snapshot.NewStateReader(snap))) } } // Configure the state reader using the path database in path mode. diff --git a/core/state/reader.go b/core/state/reader.go index 2163da8a9f..64930f17aa 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -98,26 +98,9 @@ func newFlatReader(reader database.StateReader) *flatReader { // // The returned account might be nil if it's not existent. func (r *flatReader) Account(addr common.Address) (*types.StateAccount, error) { - account, err := r.reader.Account(crypto.Keccak256Hash(addr[:])) - if err != nil { - return nil, err - } - if account == nil { - return nil, nil - } - acct := &types.StateAccount{ - Nonce: account.Nonce, - Balance: account.Balance, - CodeHash: account.CodeHash, - Root: common.BytesToHash(account.Root), - } - if len(acct.CodeHash) == 0 { - acct.CodeHash = types.EmptyCodeHash[:] - } - if acct.Root == (common.Hash{}) { - acct.Root = types.EmptyRootHash - } - return acct, nil + // The backing reader already returns accounts in the consensus (full) + // format, so no slim->full conversion is required here. + return r.reader.Account(crypto.Keccak256Hash(addr[:])) } // Storage implements StateReader, retrieving the storage slot specified by the diff --git a/core/state/reader_test.go b/core/state/reader_test.go index 7a38971f28..1b221ab791 100644 --- a/core/state/reader_test.go +++ b/core/state/reader_test.go @@ -24,15 +24,15 @@ import ( "github.com/holiman/uint256" ) -// benchStateReader is a minimal database.StateReader backed by an in-memory map. -// It isolates the conversion overhead in flatReader.Account (address hashing, -// the *types.StateAccount allocation and the slim->full fixups) from any trie, -// snapshot or disk access. +// benchStateReader is a minimal database.StateReader backed by a single account. +// It isolates flatReader.Account (just an address hash plus a forwarding call, +// now that the backing reader returns full accounts) from any trie, snapshot or +// disk access. type benchStateReader struct { - account *types.SlimAccount + account *types.StateAccount } -func (r *benchStateReader) Account(hash common.Hash) (*types.SlimAccount, error) { +func (r *benchStateReader) Account(hash common.Hash) (*types.StateAccount, error) { // Return a fresh copy on every call, mirroring the contract that the // returned account is safe to modify by the caller. if r.account == nil { @@ -47,12 +47,8 @@ func (r *benchStateReader) Storage(accountHash, storageHash common.Hash) ([]byte } // benchmarkFlatReaderAccount measures flatReader.Account for a single address. -// The provided slim account dictates which branches of the conversion are hit: -// a slim account with nil Root/CodeHash exercises the EmptyCodeHash.Bytes() and -// EmptyRootHash fixups (the lines that dominate the production profile), whereas -// a fully populated one skips them. -func benchmarkFlatReaderAccount(b *testing.B, slim *types.SlimAccount) { - r := newFlatReader(&benchStateReader{account: slim}) +func benchmarkFlatReaderAccount(b *testing.B, account *types.StateAccount) { + r := newFlatReader(&benchStateReader{account: account}) addr := common.HexToAddress("0x1234567890123456789012345678901234567890") b.ReportAllocs() @@ -69,25 +65,27 @@ func benchmarkFlatReaderAccount(b *testing.B, slim *types.SlimAccount) { } // BenchmarkFlatReaderAccountEmpty benchmarks the common EOA case: an account -// with no code and no storage. This is the hot path from the profile, hitting -// both the EmptyCodeHash and EmptyRootHash fixups. +// with no code and empty storage. Previously this path paid a slim->full +// conversion with an allocation; the backing reader now returns full accounts +// so flatReader only forwards. func BenchmarkFlatReaderAccountEmpty(b *testing.B) { - benchmarkFlatReaderAccount(b, &types.SlimAccount{ - Nonce: 1, - Balance: uint256.NewInt(100), - // Root and CodeHash left nil: slim encoding of an EOA. + benchmarkFlatReaderAccount(b, &types.StateAccount{ + Nonce: 1, + Balance: uint256.NewInt(100), + Root: types.EmptyRootHash, + CodeHash: types.EmptyCodeHash[:], }) } // BenchmarkFlatReaderAccountContract benchmarks a contract account with a -// non-empty storage root and code hash, skipping the empty-value fixups. +// non-empty storage root and code hash. func BenchmarkFlatReaderAccountContract(b *testing.B) { root := common.HexToHash("0xaabbccddeeff00112233445566778899aabbccddeeff00112233445566778899") codeHash := common.HexToHash("0x112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00") - benchmarkFlatReaderAccount(b, &types.SlimAccount{ + benchmarkFlatReaderAccount(b, &types.StateAccount{ Nonce: 7, Balance: uint256.NewInt(1000), - Root: root.Bytes(), + Root: root, CodeHash: codeHash.Bytes(), }) } diff --git a/core/state/snapshot/snapshot.go b/core/state/snapshot/snapshot.go index d3a5e9aa21..97744968e5 100644 --- a/core/state/snapshot/snapshot.go +++ b/core/state/snapshot/snapshot.go @@ -32,8 +32,56 @@ import ( "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/triedb" + "github.com/ethereum/go-ethereum/triedb/database" ) +// stateReader adapts a Snapshot, whose Account method returns accounts in the +// slim data format, to the database.StateReader interface which returns +// accounts in the consensus (full) format. +type stateReader struct { + snap Snapshot +} + +// NewStateReader wraps the given snapshot so it satisfies database.StateReader, +// converting accounts from the slim format to the consensus (full) format. It +// returns nil if the snapshot is nil. +func NewStateReader(snap Snapshot) database.StateReader { + if snap == nil { + return nil + } + return stateReader{snap: snap} +} + +// Account implements database.StateReader, converting the snapshot's slim +// account into the consensus (full) format. +func (r stateReader) Account(hash common.Hash) (*types.StateAccount, error) { + account, err := r.snap.Account(hash) + if err != nil { + return nil, err + } + if account == nil { + return nil, nil + } + acct := &types.StateAccount{ + Nonce: account.Nonce, + Balance: account.Balance, + CodeHash: account.CodeHash, + Root: common.BytesToHash(account.Root), + } + if len(acct.CodeHash) == 0 { + acct.CodeHash = types.EmptyCodeHash[:] + } + if acct.Root == (common.Hash{}) { + acct.Root = types.EmptyRootHash + } + return acct, nil +} + +// Storage implements database.StateReader. +func (r stateReader) Storage(accountHash, storageHash common.Hash) ([]byte, error) { + return r.snap.Storage(accountHash, storageHash) +} + var ( snapshotCleanAccountHitMeter = metrics.NewRegisteredMeter("state/snapshot/clean/account/hit", nil) snapshotCleanAccountMissMeter = metrics.NewRegisteredMeter("state/snapshot/clean/account/miss", nil) diff --git a/eth/protocols/snap/handlers.go b/eth/protocols/snap/handlers.go index 106322de90..f3dd7437a6 100644 --- a/eth/protocols/snap/handlers.go +++ b/eth/protocols/snap/handlers.go @@ -439,7 +439,7 @@ func ServiceGetTrieNodesQuery(chain *core.BlockChain, req *GetTrieNodesPacket) ( // via snapshot. var reader database.StateReader if chain.Snapshots() != nil { - reader = chain.Snapshots().Snapshot(req.Root) + reader = snapshot.NewStateReader(chain.Snapshots().Snapshot(req.Root)) } if reader == nil { reader, _ = triedb.StateReader(req.Root) @@ -499,7 +499,7 @@ func ServiceGetTrieNodesQuery(chain *core.BlockChain, req *GetTrieNodesPacket) ( if err != nil || account == nil { break } - stRoot = common.BytesToHash(account.Root) + stRoot = account.Root } id := trie.StorageTrieID(req.Root, common.BytesToHash(accKey), stRoot) diff --git a/triedb/database/database.go b/triedb/database/database.go index 8c61ea0293..56cc3beb25 100644 --- a/triedb/database/database.go +++ b/triedb/database/database.go @@ -44,13 +44,13 @@ type NodeDatabase interface { // StateReader wraps the Account and Storage method of a backing state reader. type StateReader interface { // Account directly retrieves the account associated with a particular hash in - // the slim data format. An error will be returned if the read operation exits - // abnormally. Specifically, if the layer is already stale. + // the consensus (full) data format. An error will be returned if the read + // operation exits abnormally. Specifically, if the layer is already stale. // // Note: // - the returned account object is safe to modify // - no error will be returned if the requested account is not found in database - Account(hash common.Hash) (*types.SlimAccount, error) + Account(hash common.Hash) (*types.StateAccount, error) // Storage directly retrieves the storage data associated with a particular hash, // within a particular account. An error will be returned if the read operation diff --git a/triedb/pathdb/buffer.go b/triedb/pathdb/buffer.go index 7afc1826c7..3d95f2ca8a 100644 --- a/triedb/pathdb/buffer.go +++ b/triedb/pathdb/buffer.go @@ -65,9 +65,10 @@ func newBuffer(limit int, nodes *nodeSet, states *stateSet, layers uint64) *buff } } -// account retrieves the decoded slim account with account address hash. A nil -// account with found=true denotes a known-deleted account. -func (b *buffer) account(hash common.Hash) (*types.SlimAccount, bool) { +// account retrieves the decoded account with account address hash, in the +// consensus (full) format. A nil account with found=true denotes a +// known-deleted account. +func (b *buffer) account(hash common.Hash) (*types.StateAccount, bool) { return b.states.account(hash) } diff --git a/triedb/pathdb/database.go b/triedb/pathdb/database.go index ca31647997..e1691349cf 100644 --- a/triedb/pathdb/database.go +++ b/triedb/pathdb/database.go @@ -56,15 +56,16 @@ type layer interface { // - no error will be returned if the requested account is not found in database. account(hash common.Hash, depth int) ([]byte, error) - // accountObject directly retrieves the decoded slim account associated with - // a particular hash. It mirrors account but avoids re-encoding/decoding for - // the diff-layer hot read path, where accounts are already held in decoded - // form. An error will be returned if the read operation exits abnormally. + // accountObject directly retrieves the decoded account associated with a + // particular hash, in the consensus (full) format. It mirrors account but + // avoids re-encoding/decoding for the diff-layer hot read path, where + // accounts are already held in decoded form. An error will be returned if + // the read operation exits abnormally. // // Note: // - the returned account is not a copy, please don't modify it. // - a nil account is returned if the requested account is not found or was deleted. - accountObject(hash common.Hash, depth int) (*types.SlimAccount, error) + accountObject(hash common.Hash, depth int) (*types.StateAccount, error) // storage directly retrieves the storage data associated with a particular hash, // within a particular account. An error will be returned if the read operation diff --git a/triedb/pathdb/difflayer.go b/triedb/pathdb/difflayer.go index f92b380118..bb584d7fcb 100644 --- a/triedb/pathdb/difflayer.go +++ b/triedb/pathdb/difflayer.go @@ -113,12 +113,12 @@ func (dl *diffLayer) account(hash common.Hash, depth int) ([]byte, error) { return encodeSlimAccount(account), nil } -// accountObject directly retrieves the decoded slim account associated with a -// particular hash. A nil account is returned if the account does not exist or -// was deleted in this hierarchy. +// accountObject directly retrieves the decoded account associated with a +// particular hash, in the consensus (full) format. A nil account is returned +// if the account does not exist or was deleted in this hierarchy. // // Note the returned account is not a copy, please don't modify it. -func (dl *diffLayer) accountObject(hash common.Hash, depth int) (*types.SlimAccount, error) { +func (dl *diffLayer) accountObject(hash common.Hash, depth int) (*types.StateAccount, error) { // Hold the lock, ensure the parent won't be changed during the // state accessing. dl.lock.RLock() diff --git a/triedb/pathdb/disklayer.go b/triedb/pathdb/disklayer.go index 27a73af53b..fafe5b81a9 100644 --- a/triedb/pathdb/disklayer.go +++ b/triedb/pathdb/disklayer.go @@ -177,10 +177,10 @@ func (dl *diskLayer) account(hash common.Hash, depth int) ([]byte, error) { return encodeSlimAccount(account), nil } -// accountObject directly retrieves the decoded slim account associated with a -// particular hash. A nil account is returned if the account does not exist or -// was deleted. -func (dl *diskLayer) accountObject(hash common.Hash, depth int) (*types.SlimAccount, error) { +// accountObject directly retrieves the decoded account associated with a +// particular hash, in the consensus (full) format. A nil account is returned +// if the account does not exist or was deleted. +func (dl *diskLayer) accountObject(hash common.Hash, depth int) (*types.StateAccount, error) { dl.lock.RLock() defer dl.lock.RUnlock() diff --git a/triedb/pathdb/flush.go b/triedb/pathdb/flush.go index 8d7f791e3b..288f5fc77f 100644 --- a/triedb/pathdb/flush.go +++ b/triedb/pathdb/flush.go @@ -75,7 +75,7 @@ func writeNodes(batch ethdb.Batch, nodes map[common.Hash]map[string]*trienode.No // TODO(rjl493456442) do we really need this generation marker? The state updates // after the marker can also be written and will be fixed by generator later if // it's outdated. -func writeStates(batch ethdb.Batch, genMarker []byte, accountData map[common.Hash]*types.SlimAccount, storageData map[common.Hash]map[common.Hash][]byte, clean *fastcache.Cache) (int, int) { +func writeStates(batch ethdb.Batch, genMarker []byte, accountData map[common.Hash]*types.StateAccount, storageData map[common.Hash]map[common.Hash][]byte, clean *fastcache.Cache) (int, int) { var ( accounts int slots int diff --git a/triedb/pathdb/reader.go b/triedb/pathdb/reader.go index 9a006a2dbd..b1f9c1f443 100644 --- a/triedb/pathdb/reader.go +++ b/triedb/pathdb/reader.go @@ -120,13 +120,13 @@ func (r *reader) AccountRLP(hash common.Hash) ([]byte, error) { } // Account directly retrieves the account associated with a particular hash in -// the slim data format. An error will be returned if the read operation exits -// abnormally. Specifically, if the layer is already stale. +// the consensus (full) data format. An error will be returned if the read +// operation exits abnormally. Specifically, if the layer is already stale. // // Note: // - the returned account object is safe to modify // - no error will be returned if the requested account is not found in database -func (r *reader) Account(hash common.Hash) (*types.SlimAccount, error) { +func (r *reader) Account(hash common.Hash) (*types.StateAccount, error) { l, err := r.db.tree.lookupAccount(hash, r.state) if err != nil { return nil, err diff --git a/triedb/pathdb/reader_bench_test.go b/triedb/pathdb/reader_bench_test.go new file mode 100644 index 0000000000..5a0b1bb964 --- /dev/null +++ b/triedb/pathdb/reader_bench_test.go @@ -0,0 +1,145 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package pathdb + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/internal/testrand" + "github.com/ethereum/go-ethereum/rlp" + "github.com/holiman/uint256" +) + +// buildAccountLayers constructs a stack of `total` diff layers on top of an +// empty disk layer. Each layer holds `perLayer` random slim-encoded accounts. +// A single "hot" account is injected into the layer at `depth` (0 == bottom-most +// diff layer, total-1 == top-most) and its hash is returned so a benchmark can +// repeatedly resolve it. This mirrors the production layout where a recently +// touched account lives in one of the in-memory diff layers and must be located +// by walking down from the top layer. +func buildAccountLayers(total, perLayer, depth int) (layer, common.Hash) { + hotHash := common.BytesToHash(testrand.Bytes(32)) + hotBlob := types.SlimAccountRLP(types.StateAccount{ + Nonce: 1, + Balance: uint256.NewInt(100), + Root: types.EmptyRootHash, + CodeHash: types.EmptyCodeHash[:], + }) + + fill := func(parent layer, index int) *diffLayer { + accounts := make(map[common.Hash][]byte, perLayer) + for i := 0; i < perLayer; i++ { + accounts[common.BytesToHash(testrand.Bytes(32))] = types.SlimAccountRLP(types.StateAccount{ + Nonce: uint64(i), + Balance: uint256.NewInt(uint64(i)), + Root: types.EmptyRootHash, + CodeHash: types.EmptyCodeHash[:], + }) + } + if index == depth { + accounts[hotHash] = hotBlob + } + states := NewStateSetWithOrigin(accounts, nil, nil, nil, false) + return newDiffLayer(parent, common.Hash{}, 0, 0, NewNodeSetWithOrigin(nil, nil), states) + } + + var l layer = emptyLayer() + for i := 0; i < total; i++ { + l = fill(l, i) + } + return l, hotHash +} + +// benchmarkAccountRead resolves the slim account blob by walking the layer stack +// and then RLP-decodes it into a *types.SlimAccount, exactly as reader.Account +// does. The decode is the cost the diff-layer refactor (storing decoded accounts +// instead of slim RLP) would move out of the read path. +func benchmarkAccountRead(b *testing.B, total, depth int) { + l, hotHash := buildAccountLayers(total, 1000, depth) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + blob, err := l.account(hotHash, 0) + if err != nil { + b.Fatal(err) + } + account := new(types.SlimAccount) + if err := rlp.DecodeBytes(blob, account); err != nil { + b.Fatal(err) + } + } +} + +// BenchmarkAccountReadTop reads an account living in the top-most diff layer: +// the lookup is cheap (depth 0) so the cost is dominated by the per-read RLP +// decode. +func BenchmarkAccountReadTop(b *testing.B) { benchmarkAccountRead(b, 128, 127) } + +// BenchmarkAccountReadBottom reads an account living in the bottom-most diff +// layer of a 128-deep stack: this pays both the full traversal and the decode. +func BenchmarkAccountReadBottom(b *testing.B) { benchmarkAccountRead(b, 128, 0) } + +// benchmarkAccountReadOnly isolates the layer traversal from the decode, to +// quantify how much of the read cost is the RLP decode (which the refactor +// removes) versus the lookup itself (which it does not). +func benchmarkAccountReadOnly(b *testing.B, total, depth int) { + l, hotHash := buildAccountLayers(total, 1000, depth) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, err := l.account(hotHash, 0); err != nil { + b.Fatal(err) + } + } +} + +// BenchmarkAccountLookupTop measures only the traversal+lookup for a top-layer +// account, with no decode. Compare against BenchmarkAccountReadTop to attribute +// cost to the decode. +func BenchmarkAccountLookupTop(b *testing.B) { benchmarkAccountReadOnly(b, 128, 127) } + +// BenchmarkAccountLookupBottom measures only the traversal+lookup for a +// bottom-layer account. Compare against BenchmarkAccountReadBottom. +func BenchmarkAccountLookupBottom(b *testing.B) { benchmarkAccountReadOnly(b, 128, 0) } + +// benchmarkAccountObject reads the decoded account directly via accountObject, +// which is the post-refactor hot path: the diff layers retain the account in +// decoded form, so no per-read RLP decode is performed. Compare against +// benchmarkAccountRead to measure the decode that was eliminated. +func benchmarkAccountObject(b *testing.B, total, depth int) { + l, hotHash := buildAccountLayers(total, 1000, depth) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + if _, err := l.accountObject(hotHash, 0); err != nil { + b.Fatal(err) + } + } +} + +// BenchmarkAccountObjectTop reads a top-layer account in decoded form (no +// decode). Compare against BenchmarkAccountReadTop. +func BenchmarkAccountObjectTop(b *testing.B) { benchmarkAccountObject(b, 128, 127) } + +// BenchmarkAccountObjectBottom reads a bottom-layer account in decoded form. +// Compare against BenchmarkAccountReadBottom. +func BenchmarkAccountObjectBottom(b *testing.B) { benchmarkAccountObject(b, 128, 0) } diff --git a/triedb/pathdb/states.go b/triedb/pathdb/states.go index 6c79e62bfb..017c56b0be 100644 --- a/triedb/pathdb/states.go +++ b/triedb/pathdb/states.go @@ -17,6 +17,7 @@ package pathdb import ( + "bytes" "fmt" "io" "maps" @@ -61,7 +62,7 @@ func (c *counter) report(count, size *metrics.Meter) { // subsequent state access will be denied due to the stale flag. Therefore, // state access and mutation won't happen at the same time with guarantee. type stateSet struct { - accountData map[common.Hash]*types.SlimAccount // Keyed accounts for direct retrieval (nil means deleted) + accountData map[common.Hash]*types.StateAccount // Keyed accounts for direct retrieval (nil means deleted) storageData map[common.Hash]map[common.Hash][]byte // Keyed storage slots for direct retrieval. one per account (nil means deleted) size uint64 // Memory size of the state data (accountData and storageData) @@ -76,74 +77,69 @@ type stateSet struct { listLock sync.RWMutex } -// slimAccountSize returns the approximate encoded size of a slim account, used +// slimAccountSize returns the approximate slim-encoded size of an account, used // for memory accounting. It estimates the slim-RLP byte length without actually -// encoding the account. A nil account (deleted marker) contributes zero. -func slimAccountSize(account *types.SlimAccount) int { +// encoding the account, mirroring how the account is serialized to disk: the +// empty storage root and code hash are omitted in slim form. A nil account +// (deleted marker) contributes zero. +func slimAccountSize(account *types.StateAccount) int { if account == nil { return 0 } // 9 bytes covers the RLP list header plus the nonce; the balance, root and - // code hash contribute their own byte lengths. This intentionally mirrors - // the previous behavior of accounting for the encoded blob length. + // code hash contribute their own byte lengths. The root and code hash are + // omitted in slim form when empty, matching SlimAccountRLP. size := 9 if account.Balance != nil { size += account.Balance.ByteLen() } - size += len(account.Root) - size += len(account.CodeHash) + if account.Root != types.EmptyRootHash { + size += len(account.Root) + } + if !bytes.Equal(account.CodeHash, types.EmptyCodeHash[:]) { + size += len(account.CodeHash) + } return size } -// decodeAccountBlob decodes a single slim-RLP-encoded account blob. A nil/empty -// blob denotes a non-existent or deleted account and decodes to a nil account. -func decodeAccountBlob(hash common.Hash, blob []byte) *types.SlimAccount { +// decodeAccountBlob decodes a single slim-RLP-encoded account blob into the +// consensus (full) account format. A nil/empty blob denotes a non-existent or +// deleted account and decodes to a nil account. +func decodeAccountBlob(hash common.Hash, blob []byte) *types.StateAccount { if len(blob) == 0 { return nil } - account := new(types.SlimAccount) - if err := rlp.DecodeBytes(blob, account); err != nil { + account, err := types.FullAccount(blob) + if err != nil { panic(fmt.Sprintf("failed to decode account %x: %v", hash, err)) } return account } -// decodeAccounts converts a map of slim-RLP-encoded accounts into decoded slim +// decodeAccounts converts a map of slim-RLP-encoded accounts into decoded full // account objects. A nil/empty blob denotes a deleted account and is preserved // as a nil entry in the resulting map. -func decodeAccounts(accounts map[common.Hash][]byte) map[common.Hash]*types.SlimAccount { - decoded := make(map[common.Hash]*types.SlimAccount, len(accounts)) +func decodeAccounts(accounts map[common.Hash][]byte) map[common.Hash]*types.StateAccount { + decoded := make(map[common.Hash]*types.StateAccount, len(accounts)) for hash, blob := range accounts { - if len(blob) == 0 { - decoded[hash] = nil // deleted account - continue - } - account := new(types.SlimAccount) - if err := rlp.DecodeBytes(blob, account); err != nil { - panic(fmt.Sprintf("failed to decode account %x: %v", hash, err)) - } - decoded[hash] = account + decoded[hash] = decodeAccountBlob(hash, blob) } return decoded } -// encodeSlimAccount serializes a decoded slim account back into its slim-RLP +// encodeSlimAccount serializes a decoded full account back into its slim-RLP // byte form. A nil account (deleted marker) encodes to a nil blob. -func encodeSlimAccount(account *types.SlimAccount) []byte { +func encodeSlimAccount(account *types.StateAccount) []byte { if account == nil { return nil } - blob, err := rlp.EncodeToBytes(account) - if err != nil { - panic(fmt.Sprintf("failed to encode account: %v", err)) - } - return blob + return types.SlimAccountRLP(*account) } // newStates constructs the state set with the provided account and storage data. // The accounts are supplied in slim-RLP form and are decoded once on ingest; -// they are kept in decoded form for the lifetime of the set and only re-encoded -// at the serialization boundaries (journal and disk flush). +// they are kept in decoded (full) form for the lifetime of the set and only +// re-encoded at the serialization boundaries (journal and disk flush). func newStates(accounts map[common.Hash][]byte, storages map[common.Hash]map[common.Hash][]byte, rawStorageKey bool) *stateSet { // Don't panic for the lazy callers, initialize the nil maps instead. if storages == nil { @@ -162,7 +158,7 @@ func newStates(accounts map[common.Hash][]byte, storages map[common.Hash]map[com // account returns the account data associated with the specified address hash. // A nil account with found=true denotes a known-deleted account, whereas // found=false indicates the account is unknown in this set. -func (s *stateSet) account(hash common.Hash) (*types.SlimAccount, bool) { +func (s *stateSet) account(hash common.Hash) (*types.StateAccount, bool) { // If the account is known locally, return it if data, ok := s.accountData[hash]; ok { return data, true @@ -173,7 +169,7 @@ func (s *stateSet) account(hash common.Hash) (*types.SlimAccount, bool) { // mustAccount returns the account data associated with the specified address // hash. The difference is this function will return an error if the account // is not found. -func (s *stateSet) mustAccount(hash common.Hash) (*types.SlimAccount, error) { +func (s *stateSet) mustAccount(hash common.Hash) (*types.StateAccount, error) { // If the account is known locally, return it if data, ok := s.accountData[hash]; ok { return data, nil @@ -362,15 +358,9 @@ func (s *stateSet) revertTo(accountOrigin map[common.Hash][]byte, storageOrigin if !ok { panic(fmt.Sprintf("non-existent account for reverting, %x", addrHash)) } - // Decode the original slim-RLP value; an empty blob denotes a deleted - // account, represented as a nil entry. - var origin *types.SlimAccount - if len(blob) != 0 { - origin = new(types.SlimAccount) - if err := rlp.DecodeBytes(blob, origin); err != nil { - panic(fmt.Sprintf("failed to decode account %x: %v", addrHash, err)) - } - } + // Decode the original slim-RLP value into the consensus (full) format; + // an empty blob denotes a deleted account, represented as a nil entry. + origin := decodeAccountBlob(addrHash, blob) if data == nil && origin == nil { panic(fmt.Sprintf("invalid account mutation (null to null), %x", addrHash)) } @@ -509,7 +499,7 @@ func (s *stateSet) write(batch ethdb.Batch, genMarker []byte, clean *fastcache.C // reset clears all cached state data, including any optional sorted lists that // may have been generated. func (s *stateSet) reset() { - s.accountData = make(map[common.Hash]*types.SlimAccount) + s.accountData = make(map[common.Hash]*types.StateAccount) s.storageData = make(map[common.Hash]map[common.Hash][]byte) s.size = 0 s.accountListSorted = nil diff --git a/triedb/pathdb/states_test.go b/triedb/pathdb/states_test.go index ff1a1078e0..3e67006e96 100644 --- a/triedb/pathdb/states_test.go +++ b/triedb/pathdb/states_test.go @@ -42,7 +42,7 @@ func testAccountRLP(tag byte) []byte { // accountEqualsTag reports whether the decoded account matches the account // produced by testAccountRLP for the given tag. -func accountEqualsTag(account *types.SlimAccount, tag byte) bool { +func accountEqualsTag(account *types.StateAccount, tag byte) bool { if account == nil { return false } @@ -399,27 +399,34 @@ func testStateWithOriginEncode(t *testing.T, rawStorageKey bool) { } } -// TestSlimAccountSize verifies the estimated memory size of a slim account for -// the empty/deleted, EOA and contract cases. The accountData map keys are -// accounted for separately (common.HashLength per entry), so this only covers -// the value contribution. +// TestSlimAccountSize verifies the estimated slim-encoded memory size of a full +// account for the empty/deleted, EOA and contract cases. The accountData map +// keys are accounted for separately (common.HashLength per entry), so this only +// covers the value contribution. slimAccountSize takes a full account but +// estimates the slim encoding, where an empty storage root and code hash are +// omitted. func TestSlimAccountSize(t *testing.T) { // A nil account (deletion marker) contributes nothing. if got := slimAccountSize(nil); got != 0 { t.Fatalf("nil account size, want: 0, got: %d", got) } - // An EOA with no code and empty storage root carries nil Root/CodeHash in - // slim form, so only the 9-byte list/nonce overhead plus the balance length - // is counted. - eoa := &types.SlimAccount{Nonce: 1, Balance: uint256.NewInt(0x100)} + // An EOA with empty code and empty storage root: Root/CodeHash are omitted + // in slim form, so only the 9-byte list/nonce overhead plus the balance + // length is counted. + eoa := &types.StateAccount{ + Nonce: 1, + Balance: uint256.NewInt(0x100), + Root: types.EmptyRootHash, + CodeHash: types.EmptyCodeHash[:], + } if got, want := slimAccountSize(eoa), 9+eoa.Balance.ByteLen(); got != want { t.Fatalf("EOA account size, want: %d, got: %d", want, got) } // A contract carries a 32-byte root and 32-byte code hash in addition. - contract := &types.SlimAccount{ + contract := &types.StateAccount{ Nonce: 2, Balance: uint256.NewInt(0x10000), - Root: testrand.Bytes(32), + Root: common.BytesToHash(testrand.Bytes(32)), CodeHash: testrand.Bytes(32), } if got, want := slimAccountSize(contract), 9+contract.Balance.ByteLen()+64; got != want {