triedb/pathdb: improve hot account retrievals

before: 595 ms after 48ms on layer 1
before: 6050 ms after: 4960ms on layer 127
This commit is contained in:
MariusVanDerWijden 2026-06-22 17:17:50 +02:00
parent e3b6d0c86f
commit 03911b11eb
No known key found for this signature in database
14 changed files with 433 additions and 116 deletions

View file

@ -62,7 +62,7 @@ func (r *historicStateReader) Account(addr common.Address) (*types.StateAccount,
Root: common.BytesToHash(account.Root), Root: common.BytesToHash(account.Root),
} }
if len(acct.CodeHash) == 0 { if len(acct.CodeHash) == 0 {
acct.CodeHash = types.EmptyCodeHash.Bytes() acct.CodeHash = types.EmptyCodeHash[:]
} }
if acct.Root == (common.Hash{}) { if acct.Root == (common.Hash{}) {
acct.Root = types.EmptyRootHash acct.Root = types.EmptyRootHash

View file

@ -112,7 +112,7 @@ func (r *flatReader) Account(addr common.Address) (*types.StateAccount, error) {
Root: common.BytesToHash(account.Root), Root: common.BytesToHash(account.Root),
} }
if len(acct.CodeHash) == 0 { if len(acct.CodeHash) == 0 {
acct.CodeHash = types.EmptyCodeHash.Bytes() acct.CodeHash = types.EmptyCodeHash[:]
} }
if acct.Root == (common.Hash{}) { if acct.Root == (common.Hash{}) {
acct.Root = types.EmptyRootHash acct.Root = types.EmptyRootHash

93
core/state/reader_test.go Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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(),
})
}

View file

@ -24,6 +24,7 @@ import (
"github.com/VictoriaMetrics/fastcache" "github.com/VictoriaMetrics/fastcache"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb" "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/ethdb"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/trie/trienode" "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. // account retrieves the decoded slim account with account address hash. A nil
func (b *buffer) account(hash common.Hash) ([]byte, bool) { // account with found=true denotes a known-deleted account.
func (b *buffer) account(hash common.Hash) (*types.SlimAccount, bool) {
return b.states.account(hash) return b.states.account(hash)
} }

View file

@ -56,6 +56,16 @@ type layer interface {
// - no error will be returned if the requested account is not found in database. // - no error will be returned if the requested account is not found in database.
account(hash common.Hash, depth int) ([]byte, error) 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, // storage directly retrieves the storage data associated with a particular hash,
// within a particular account. An error will be returned if the read operation // within a particular account. An error will be returned if the read operation
// exits abnormally. Specifically, if the layer is already stale. // exits abnormally. Specifically, if the layer is already stale.

View file

@ -21,6 +21,7 @@ import (
"sync" "sync"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log" "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. // Note the returned account is not a copy, please don't modify it.
func (dl *diffLayer) account(hash common.Hash, depth int) ([]byte, error) { 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 // Hold the lock, ensure the parent won't be changed during the
// state accessing. // state accessing.
dl.lock.RLock() dl.lock.RLock()
defer dl.lock.RUnlock() defer dl.lock.RUnlock()
if blob, found := dl.states.account(hash); found { if account, found := dl.states.account(hash); found {
dirtyStateHitMeter.Mark(1) dirtyStateHitMeter.Mark(1)
dirtyStateHitDepthHist.Update(int64(depth)) dirtyStateHitDepthHist.Update(int64(depth))
dirtyStateReadMeter.Mark(int64(len(blob))) dirtyStateReadMeter.Mark(int64(slimAccountSize(account)))
if len(blob) == 0 { if account == nil {
stateAccountInexMeter.Mark(1) stateAccountInexMeter.Mark(1)
} else { } else {
stateAccountExistMeter.Mark(1) stateAccountExistMeter.Mark(1)
} }
return blob, nil return account, nil
} }
// Account is unknown to this layer, resolve from parent // 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, // storage directly retrieves the storage data associated with a particular hash,

View file

@ -25,6 +25,7 @@ import (
"github.com/VictoriaMetrics/fastcache" "github.com/VictoriaMetrics/fastcache"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb" "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/crypto"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log" "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. // Note the returned account is not a copy, please don't modify it.
func (dl *diskLayer) account(hash common.Hash, depth int) ([]byte, error) { 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() dl.lock.RLock()
defer dl.lock.RUnlock() defer dl.lock.RUnlock()
if dl.stale { if dl.stale {
return nil, errSnapshotStale return nil, errSnapshotStale
} }
// Try to retrieve the trie node from the not-yet-written node buffer first // Try to retrieve the account from the not-yet-written state buffer first
// (both the live one and the frozen one). Note the buffer is lock free since // (both the live one and the frozen one). The buffer holds accounts in
// it's impossible to mutate the buffer before tagging the layer as stale. // 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} { for _, buffer := range []*buffer{dl.buffer, dl.frozen} {
if buffer != nil { if buffer != nil {
blob, found := buffer.account(hash) account, found := buffer.account(hash)
if found { if found {
dirtyStateHitMeter.Mark(1) dirtyStateHitMeter.Mark(1)
dirtyStateReadMeter.Mark(int64(len(blob))) dirtyStateReadMeter.Mark(int64(slimAccountSize(account)))
dirtyStateHitDepthHist.Update(int64(depth)) dirtyStateHitDepthHist.Update(int64(depth))
if len(blob) == 0 { if account == nil {
stateAccountInexMeter.Mark(1) stateAccountInexMeter.Mark(1)
} else { } else {
stateAccountExistMeter.Mark(1) 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 { } else {
stateAccountExistMeter.Mark(1) stateAccountExistMeter.Mark(1)
} }
return blob, nil return decodeAccountBlob(hash, blob), nil
} }
cleanStateMissMeter.Mark(1) cleanStateMissMeter.Mark(1)
} }
@ -237,7 +251,7 @@ func (dl *diskLayer) account(hash common.Hash, depth int) ([]byte, error) {
stateAccountExistMeter.Mark(1) stateAccountExistMeter.Mark(1)
stateAccountExistDiskMeter.Mark(1) stateAccountExistDiskMeter.Mark(1)
} }
return blob, nil return decodeAccountBlob(hash, blob), nil
} }
// storage directly retrieves the storage data associated with a particular hash, // 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() batch := dl.db.diskdb.NewBatch()
writeNodes(batch, nodes, dl.nodes) writeNodes(batch, nodes, dl.nodes)
// Provide the original values of modified accounts and storages for revert // Provide the original values of modified accounts and storages for revert.
writeStates(batch, progress, accounts, storages, dl.states) // 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.WritePersistentStateID(batch, dl.id-1)
rawdb.WriteSnapshotRoot(batch, h.meta.parent) rawdb.WriteSnapshotRoot(batch, h.meta.parent)
if err := batch.Write(); err != nil { if err := batch.Write(); err != nil {

View file

@ -22,6 +22,7 @@ import (
"github.com/VictoriaMetrics/fastcache" "github.com/VictoriaMetrics/fastcache"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb" "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/ethdb"
"github.com/ethereum/go-ethereum/trie/trienode" "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 // 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 // after the marker can also be written and will be fixed by generator later if
// it's outdated. // 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 ( var (
accounts int accounts int
slots int slots int
) )
for addrHash, blob := range accountData { for addrHash, account := range accountData {
// Skip any account not yet covered by the snapshot. The account // Skip any account not yet covered by the snapshot. The account
// at the generation marker position (addrHash == genMarker[:common.HashLength]) // at the generation marker position (addrHash == genMarker[:common.HashLength])
// should still be updated, as it would be skipped in the next // 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 continue
} }
accounts += 1 accounts += 1
if len(blob) == 0 { if account == nil {
rawdb.DeleteAccountSnapshot(batch, addrHash) rawdb.DeleteAccountSnapshot(batch, addrHash)
if clean != nil { if clean != nil {
clean.Set(addrHash[:], nil) clean.Set(addrHash[:], nil)
} }
} else { } 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) rawdb.WriteAccountSnapshot(batch, addrHash, blob)
if clean != nil { if clean != nil {
clean.Set(addrHash[:], blob) clean.Set(addrHash[:], blob)

View file

@ -69,7 +69,7 @@ func (t *genTester) addTrieAccount(acckey string, acc *types.StateAccount) {
) )
t.acctTrie.MustUpdate(key.Bytes(), val) 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 t.states.accountOrigin[addr] = nil
} }

View file

@ -98,7 +98,11 @@ func newFastIterator(db *Database, root common.Hash, account common.Hash, seek c
if dl.stale { if dl.stale {
return nil, errSnapshotStale 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, 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, // The state set in diff layer is immutable and will never be stale,
// so the read lock protection is unnecessary. // so the read lock protection is unnecessary.
accountList := dl.states.accountList() accountList := dl.states.accountList()
states := dl.states
fi.iterators = append(fi.iterators, &weightedIterator{ 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, priority: depth,
}) })
} }

View file

@ -122,8 +122,7 @@ func TestAccountIteratorBasics(t *testing.T) {
// Fill up a parent // Fill up a parent
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
hash := testrand.Hash() hash := testrand.Hash()
data := testrand.Bytes(32) accounts[hash] = randomAccount()
accounts[hash] = data
if rand.Intn(2) == 0 { if rand.Intn(2) == 0 {
accStorage := make(map[common.Hash][]byte) accStorage := make(map[common.Hash][]byte)
@ -153,7 +152,7 @@ func TestStorageIteratorBasics(t *testing.T) {
// Fill some random data // Fill some random data
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
hash := testrand.Hash() hash := testrand.Hash()
accounts[hash] = testrand.Bytes(32) accounts[hash] = randomAccount()
accStorage := make(map[common.Hash][]byte) accStorage := make(map[common.Hash][]byte)
var nilstorage int var nilstorage int
@ -344,6 +343,18 @@ func TestAccountIteratorTraversalValues(t *testing.T) {
} }
db := New(rawdb.NewMemoryDatabase(), config, false) 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 // Create a batch of account sets to seed subsequent layers with
var ( var (
a = make(map[common.Hash][]byte) a = make(map[common.Hash][]byte)
@ -356,27 +367,27 @@ func TestAccountIteratorTraversalValues(t *testing.T) {
h = make(map[common.Hash][]byte) h = make(map[common.Hash][]byte)
) )
for i := byte(2); i < 0xff; i++ { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 // Assemble a stack of snapshots from the account layers

View file

@ -127,18 +127,27 @@ func (r *reader) AccountRLP(hash common.Hash) ([]byte, error) {
// - the returned account object is safe to modify // - the returned account object is safe to modify
// - no error will be returned if the requested account is not found in database // - 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.SlimAccount, error) {
blob, err := r.AccountRLP(hash) l, err := r.db.tree.lookupAccount(hash, r.state)
if err != nil { if err != nil {
return nil, err 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 return nil, nil
} }
account := new(types.SlimAccount) // The diff layers retain the account in decoded form and the returned
if err := rlp.DecodeBytes(blob, account); err != nil { // object is documented as safe to modify, so hand back a shallow copy to
panic(err) // protect the cached instance from caller mutation.
} cpy := *account
return account, nil return &cpy, nil
} }
// Storage directly retrieves the storage data associated with a particular hash, // Storage directly retrieves the storage data associated with a particular hash,

View file

@ -26,6 +26,7 @@ import (
"github.com/VictoriaMetrics/fastcache" "github.com/VictoriaMetrics/fastcache"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb" "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/ethdb"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics" "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, // 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. // state access and mutation won't happen at the same time with guarantee.
type stateSet struct { 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) 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) size uint64 // Memory size of the state data (accountData and storageData)
@ -75,17 +76,81 @@ type stateSet struct {
listLock sync.RWMutex 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. // 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 { 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. // Don't panic for the lazy callers, initialize the nil maps instead.
if accounts == nil {
accounts = make(map[common.Hash][]byte)
}
if storages == nil { if storages == nil {
storages = make(map[common.Hash]map[common.Hash][]byte) storages = make(map[common.Hash]map[common.Hash][]byte)
} }
s := &stateSet{ s := &stateSet{
accountData: accounts, accountData: decodeAccounts(accounts),
storageData: storages, storageData: storages,
rawStorageKey: rawStorageKey, rawStorageKey: rawStorageKey,
storageListSorted: make(map[common.Hash][]common.Hash), 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. // 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 the account is known locally, return it
if data, ok := s.accountData[hash]; ok { if data, ok := s.accountData[hash]; ok {
return data, true 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 // mustAccount returns the account data associated with the specified address
// hash. The difference is this function will return an error if the account // hash. The difference is this function will return an error if the account
// is not found. // 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 the account is known locally, return it
if data, ok := s.accountData[hash]; ok { if data, ok := s.accountData[hash]; ok {
return data, nil 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. // Additionally, it computes the total memory size occupied by the maps.
func (s *stateSet) check() uint64 { func (s *stateSet) check() uint64 {
var size int var size int
for _, blob := range s.accountData { for _, account := range s.accountData {
size += common.HashLength + len(blob) size += common.HashLength + slimAccountSize(account)
} }
for accountHash, slots := range s.storageData { for accountHash, slots := range s.storageData {
if slots == nil { if slots == nil {
@ -235,11 +302,13 @@ func (s *stateSet) merge(other *stateSet) {
) )
// Apply the updated account data // Apply the updated account data
for accountHash, data := range other.accountData { for accountHash, data := range other.accountData {
dataSize := slimAccountSize(data)
if origin, ok := s.accountData[accountHash]; ok { if origin, ok := s.accountData[accountHash]; ok {
delta += len(data) - len(origin) originSize := slimAccountSize(origin)
accountOverwrites.add(common.HashLength + len(origin)) delta += dataSize - originSize
accountOverwrites.add(common.HashLength + originSize)
} else { } else {
delta += common.HashLength + len(data) delta += common.HashLength + dataSize
} }
s.accountData[accountHash] = data s.accountData[accountHash] = data
} }
@ -293,11 +362,20 @@ func (s *stateSet) revertTo(accountOrigin map[common.Hash][]byte, storageOrigin
if !ok { if !ok {
panic(fmt.Sprintf("non-existent account for reverting, %x", addrHash)) 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)) panic(fmt.Sprintf("invalid account mutation (null to null), %x", addrHash))
} }
delta += len(blob) - len(data) delta += slimAccountSize(origin) - slimAccountSize(data)
s.accountData[addrHash] = blob s.accountData[addrHash] = origin
} }
// Overwrite the storage data with original value blindly // Overwrite the storage data with original value blindly
for addrHash, storage := range storageOrigin { 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)), AddrHashes: make([]common.Hash, 0, len(s.accountData)),
Accounts: make([][]byte, 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.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 { if err := rlp.Encode(w, enc); err != nil {
return err return err
@ -395,7 +473,7 @@ func (s *stateSet) decode(r *rlp.Stream) error {
for i := range dec.AddrHashes { for i := range dec.AddrHashes {
accountSet[dec.AddrHashes[i]] = empty2nil(dec.Accounts[i]) accountSet[dec.AddrHashes[i]] = empty2nil(dec.Accounts[i])
} }
s.accountData = accountSet s.accountData = decodeAccounts(accountSet)
// Decode storages // Decode storages
type storage struct { 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 // reset clears all cached state data, including any optional sorted lists that
// may have been generated. // may have been generated.
func (s *stateSet) reset() { 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.storageData = make(map[common.Hash]map[common.Hash][]byte)
s.size = 0 s.size = 0
s.accountListSorted = nil s.accountListSorted = nil

View file

@ -22,15 +22,39 @@ import (
"testing" "testing"
"github.com/ethereum/go-ethereum/common" "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/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) { func TestStatesMerge(t *testing.T) {
a := newStates( a := newStates(
map[common.Hash][]byte{ map[common.Hash][]byte{
{0xa}: {0xa0}, {0xa}: testAccountRLP(0xa0),
{0xb}: {0xb0}, {0xb}: testAccountRLP(0xb0),
{0xc}: {0xc0}, {0xc}: testAccountRLP(0xc0),
}, },
map[common.Hash]map[common.Hash][]byte{ map[common.Hash]map[common.Hash][]byte{
{0xa}: { {0xa}: {
@ -48,8 +72,8 @@ func TestStatesMerge(t *testing.T) {
) )
b := newStates( b := newStates(
map[common.Hash][]byte{ map[common.Hash][]byte{
{0xa}: {0xa1}, {0xa}: testAccountRLP(0xa1),
{0xb}: {0xb1}, {0xb}: testAccountRLP(0xb1),
{0xc}: nil, // delete account {0xc}: nil, // delete account
}, },
map[common.Hash]map[common.Hash][]byte{ map[common.Hash]map[common.Hash][]byte{
@ -69,25 +93,25 @@ func TestStatesMerge(t *testing.T) {
) )
a.merge(b) a.merge(b)
blob, exist := a.account(common.Hash{0xa}) account, exist := a.account(common.Hash{0xa})
if !exist || !bytes.Equal(blob, []byte{0xa1}) { if !exist || !accountEqualsTag(account, 0xa1) {
t.Error("Unexpected value for account a") t.Error("Unexpected value for account a")
} }
blob, exist = a.account(common.Hash{0xb}) account, exist = a.account(common.Hash{0xb})
if !exist || !bytes.Equal(blob, []byte{0xb1}) { if !exist || !accountEqualsTag(account, 0xb1) {
t.Error("Unexpected value for account b") t.Error("Unexpected value for account b")
} }
blob, exist = a.account(common.Hash{0xc}) account, exist = a.account(common.Hash{0xc})
if !exist || len(blob) != 0 { if !exist || account != nil {
t.Error("Unexpected value for account c") t.Error("Unexpected value for account c")
} }
// unknown account // unknown account
blob, exist = a.account(common.Hash{0xd}) account, exist = a.account(common.Hash{0xd})
if exist || len(blob) != 0 { if exist || account != nil {
t.Error("Unexpected value for account d") 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}) { if !exist || !bytes.Equal(blob, []byte{0x11}) {
t.Error("Unexpected value for a's storage") t.Error("Unexpected value for a's storage")
} }
@ -118,9 +142,9 @@ func TestStatesMerge(t *testing.T) {
func TestStatesRevert(t *testing.T) { func TestStatesRevert(t *testing.T) {
a := newStates( a := newStates(
map[common.Hash][]byte{ map[common.Hash][]byte{
{0xa}: {0xa0}, {0xa}: testAccountRLP(0xa0),
{0xb}: {0xb0}, {0xb}: testAccountRLP(0xb0),
{0xc}: {0xc0}, {0xc}: testAccountRLP(0xc0),
}, },
map[common.Hash]map[common.Hash][]byte{ map[common.Hash]map[common.Hash][]byte{
{0xa}: { {0xa}: {
@ -138,8 +162,8 @@ func TestStatesRevert(t *testing.T) {
) )
b := newStates( b := newStates(
map[common.Hash][]byte{ map[common.Hash][]byte{
{0xa}: {0xa1}, {0xa}: testAccountRLP(0xa1),
{0xb}: {0xb1}, {0xb}: testAccountRLP(0xb1),
{0xc}: nil, {0xc}: nil,
}, },
map[common.Hash]map[common.Hash][]byte{ map[common.Hash]map[common.Hash][]byte{
@ -160,9 +184,9 @@ func TestStatesRevert(t *testing.T) {
a.merge(b) a.merge(b)
a.revertTo( a.revertTo(
map[common.Hash][]byte{ map[common.Hash][]byte{
{0xa}: {0xa0}, {0xa}: testAccountRLP(0xa0),
{0xb}: {0xb0}, {0xb}: testAccountRLP(0xb0),
{0xc}: {0xc0}, {0xc}: testAccountRLP(0xc0),
}, },
map[common.Hash]map[common.Hash][]byte{ map[common.Hash]map[common.Hash][]byte{
{0xa}: { {0xa}: {
@ -179,25 +203,25 @@ func TestStatesRevert(t *testing.T) {
}, },
) )
blob, exist := a.account(common.Hash{0xa}) account, exist := a.account(common.Hash{0xa})
if !exist || !bytes.Equal(blob, []byte{0xa0}) { if !exist || !accountEqualsTag(account, 0xa0) {
t.Error("Unexpected value for account a") t.Error("Unexpected value for account a")
} }
blob, exist = a.account(common.Hash{0xb}) account, exist = a.account(common.Hash{0xb})
if !exist || !bytes.Equal(blob, []byte{0xb0}) { if !exist || !accountEqualsTag(account, 0xb0) {
t.Error("Unexpected value for account b") t.Error("Unexpected value for account b")
} }
blob, exist = a.account(common.Hash{0xc}) account, exist = a.account(common.Hash{0xc})
if !exist || !bytes.Equal(blob, []byte{0xc0}) { if !exist || !accountEqualsTag(account, 0xc0) {
t.Error("Unexpected value for account c") t.Error("Unexpected value for account c")
} }
// unknown account // unknown account
blob, exist = a.account(common.Hash{0xd}) account, exist = a.account(common.Hash{0xd})
if exist || len(blob) != 0 { if exist || account != nil {
t.Error("Unexpected value for account d") 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}) { if !exist || !bytes.Equal(blob, []byte{0x10}) {
t.Error("Unexpected value for a's storage") 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 a := newStates(nil, nil, false) // empty initial state
b := newStates( b := newStates(
map[common.Hash][]byte{ map[common.Hash][]byte{
{0xa}: {0xa}, {0xa}: testAccountRLP(0xa),
}, },
nil, nil,
false, false,
@ -244,12 +268,12 @@ func TestStateRevertAccountNullMarker(t *testing.T) {
nil, nil,
) // revert the transition b ) // revert the transition b
blob, exist := a.account(common.Hash{0xa}) account, exist := a.account(common.Hash{0xa})
if !exist { if !exist {
t.Fatal("null marker is not found") t.Fatal("null marker is not found")
} }
if len(blob) != 0 { if account != nil {
t.Fatalf("Unexpected value for account, %v", blob) t.Fatalf("Unexpected value for account, %v", account)
} }
} }
@ -258,7 +282,7 @@ func TestStateRevertAccountNullMarker(t *testing.T) {
// entry in the set. // entry in the set.
func TestStateRevertStorageNullMarker(t *testing.T) { func TestStateRevertStorageNullMarker(t *testing.T) {
a := newStates(map[common.Hash][]byte{ a := newStates(map[common.Hash][]byte{
{0xa}: {0xa}, {0xa}: testAccountRLP(0xa),
}, nil, false) // initial state with account 0xa }, nil, false) // initial state with account 0xa
b := newStates( b := newStates(
@ -297,7 +321,7 @@ func TestStatesEncode(t *testing.T) {
func testStatesEncode(t *testing.T, rawStorageKey bool) { func testStatesEncode(t *testing.T, rawStorageKey bool) {
s := newStates( s := newStates(
map[common.Hash][]byte{ map[common.Hash][]byte{
{0x1}: {0x1}, {0x1}: testAccountRLP(0x1),
}, },
map[common.Hash]map[common.Hash][]byte{ map[common.Hash]map[common.Hash][]byte{
{0x1}: { {0x1}: {
@ -333,7 +357,7 @@ func TestStateWithOriginEncode(t *testing.T) {
func testStateWithOriginEncode(t *testing.T, rawStorageKey bool) { func testStateWithOriginEncode(t *testing.T, rawStorageKey bool) {
s := NewStateSetWithOrigin( s := NewStateSetWithOrigin(
map[common.Hash][]byte{ map[common.Hash][]byte{
{0x1}: {0x1}, {0x1}: testAccountRLP(0x1),
}, },
map[common.Hash]map[common.Hash][]byte{ map[common.Hash]map[common.Hash][]byte{
{0x1}: { {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) { 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*(2*common.HashLength+1) + /* storage data of 0xa */
2*common.HashLength + 3 + /* storage data of 0xb */ 2*common.HashLength + 3 + /* storage data of 0xb */
2*common.HashLength + 1 /* storage data of 0xc */ 2*common.HashLength + 1 /* storage data of 0xc */
a := newStates( a := newStates(
map[common.Hash][]byte{ map[common.Hash][]byte{
{0xa}: {0xa0}, // common.HashLength+1 {0xa}: testAccountRLP(0x1),
{0xb}: {0xb0}, // common.HashLength+1 {0xb}: testAccountRLP(0x1),
{0xc}: {0xc0}, // common.HashLength+1 {0xc}: testAccountRLP(0x1),
}, },
map[common.Hash]map[common.Hash][]byte{ map[common.Hash]map[common.Hash][]byte{
{0xa}: { {0xa}: {
@ -405,15 +465,15 @@ func TestStateSizeTracking(t *testing.T) {
t.Fatalf("Unexpected size, want: %d, got: %d", expSizeA, a.size) 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 + 3 + 2*common.HashLength + 2 + /* storage data of 0xa */
2*common.HashLength + 2 + 2*common.HashLength + 2 + /* storage data of 0xb */ 2*common.HashLength + 2 + 2*common.HashLength + 2 + /* storage data of 0xb */
3*2*common.HashLength /* storage data of 0xc */ 3*2*common.HashLength /* storage data of 0xc */
b := newStates( b := newStates(
map[common.Hash][]byte{ map[common.Hash][]byte{
{0xa}: {0xa1, 0xa1}, // common.HashLength+2 {0xa}: testAccountRLP(0x1), // account update (same size)
{0xb}: {0xb1, 0xb1, 0xb1}, // common.HashLength+3 {0xb}: testAccountRLP(0x1), // account update (same size)
{0xc}: nil, // common.HashLength, account deletion {0xc}: nil, // common.HashLength, account deletion
}, },
map[common.Hash]map[common.Hash][]byte{ map[common.Hash]map[common.Hash][]byte{
{0xa}: { {0xa}: {
@ -438,10 +498,13 @@ func TestStateSizeTracking(t *testing.T) {
} }
a.merge(b) a.merge(b)
mergeSize := expSizeA + 1 /* account a data change */ + 2 /* account b data change */ - 1 /* account c data change */ // Accounts a and b are overwritten with same-size accounts (no delta), but
mergeSize += 2*common.HashLength + 2 + 2 /* storage a change */ // account c becomes a deletion marker, dropping its value contribution while
mergeSize += 2*common.HashLength + 2 - 1 /* storage b change */ // retaining the key.
mergeSize += 2*2*common.HashLength - 1 /* storage data removal of 0xc */ 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) { if a.size != uint64(mergeSize) {
t.Fatalf("Unexpected size, want: %d, got: %d", mergeSize, a.size) 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 // Revert the set to original status
a.revertTo( a.revertTo(
map[common.Hash][]byte{ map[common.Hash][]byte{
{0xa}: {0xa0}, {0xa}: testAccountRLP(0x1),
{0xb}: {0xb0}, {0xb}: testAccountRLP(0x1),
{0xc}: {0xc0}, {0xc}: testAccountRLP(0x1),
}, },
map[common.Hash]map[common.Hash][]byte{ map[common.Hash]map[common.Hash][]byte{
{0xa}: { {0xa}: {