This commit is contained in:
Marius van der Wijden 2026-07-17 21:53:16 -07:00 committed by GitHub
commit d1b67beecb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 634 additions and 144 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

@ -66,7 +66,7 @@ func (db *MPTDatabase) StateReader(stateRoot common.Hash) (StateReader, error) {
if db.TrieDB().Scheme() == rawdb.HashScheme && db.snap != nil { if db.TrieDB().Scheme() == rawdb.HashScheme && db.snap != nil {
snap := db.snap.Snapshot(stateRoot) snap := db.snap.Snapshot(stateRoot)
if snap != nil { 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. // Configure the state reader using the path database in path mode.

View file

@ -98,26 +98,9 @@ func newFlatReader(reader database.StateReader) *flatReader {
// //
// The returned account might be nil if it's not existent. // The returned account might be nil if it's not existent.
func (r *flatReader) Account(addr common.Address) (*types.StateAccount, error) { func (r *flatReader) Account(addr common.Address) (*types.StateAccount, error) {
account, err := r.reader.Account(crypto.Keccak256Hash(addr[:])) // The backing reader already returns accounts in the consensus (full)
if err != nil { // format, so no slim->full conversion is required here.
return nil, err return r.reader.Account(crypto.Keccak256Hash(addr[:]))
}
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.Bytes()
}
if acct.Root == (common.Hash{}) {
acct.Root = types.EmptyRootHash
}
return acct, nil
} }
// Storage implements StateReader, retrieving the storage slot specified by the // Storage implements StateReader, retrieving the storage slot specified by the

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

@ -0,0 +1,91 @@
// 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 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.StateAccount
}
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 {
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.
func benchmarkFlatReaderAccount(b *testing.B, account *types.StateAccount) {
r := newFlatReader(&benchStateReader{account: account})
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 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.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.
func BenchmarkFlatReaderAccountContract(b *testing.B) {
root := common.HexToHash("0xaabbccddeeff00112233445566778899aabbccddeeff00112233445566778899")
codeHash := common.HexToHash("0x112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00")
benchmarkFlatReaderAccount(b, &types.StateAccount{
Nonce: 7,
Balance: uint256.NewInt(1000),
Root: root,
CodeHash: codeHash.Bytes(),
})
}

View file

@ -32,8 +32,56 @@ import (
"github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/triedb" "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 ( var (
snapshotCleanAccountHitMeter = metrics.NewRegisteredMeter("state/snapshot/clean/account/hit", nil) snapshotCleanAccountHitMeter = metrics.NewRegisteredMeter("state/snapshot/clean/account/hit", nil)
snapshotCleanAccountMissMeter = metrics.NewRegisteredMeter("state/snapshot/clean/account/miss", nil) snapshotCleanAccountMissMeter = metrics.NewRegisteredMeter("state/snapshot/clean/account/miss", nil)

View file

@ -439,7 +439,7 @@ func ServiceGetTrieNodesQuery(chain *core.BlockChain, req *GetTrieNodesPacket) (
// via snapshot. // via snapshot.
var reader database.StateReader var reader database.StateReader
if chain.Snapshots() != nil { if chain.Snapshots() != nil {
reader = chain.Snapshots().Snapshot(req.Root) reader = snapshot.NewStateReader(chain.Snapshots().Snapshot(req.Root))
} }
if reader == nil { if reader == nil {
reader, _ = triedb.StateReader(req.Root) reader, _ = triedb.StateReader(req.Root)
@ -499,7 +499,7 @@ func ServiceGetTrieNodesQuery(chain *core.BlockChain, req *GetTrieNodesPacket) (
if err != nil || account == nil { if err != nil || account == nil {
break break
} }
stRoot = common.BytesToHash(account.Root) stRoot = account.Root
} }
id := trie.StorageTrieID(req.Root, common.BytesToHash(accKey), stRoot) id := trie.StorageTrieID(req.Root, common.BytesToHash(accKey), stRoot)

View file

@ -44,13 +44,13 @@ type NodeDatabase interface {
// StateReader wraps the Account and Storage method of a backing state reader. // StateReader wraps the Account and Storage method of a backing state reader.
type StateReader interface { type StateReader interface {
// Account directly retrieves the account associated with a particular hash in // 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 // the consensus (full) data format. An error will be returned if the read
// abnormally. Specifically, if the layer is already stale. // operation exits abnormally. Specifically, if the layer is already stale.
// //
// Note: // Note:
// - 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
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, // 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

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,10 @@ func newBuffer(limit int, nodes *nodeSet, states *stateSet, layers uint64) *buff
} }
} }
// account retrieves the account blob with account address hash. // account retrieves the decoded account with account address hash, in the
func (b *buffer) account(hash common.Hash) ([]byte, bool) { // 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) return b.states.account(hash)
} }

View file

@ -56,6 +56,17 @@ 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 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.StateAccount, 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 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.StateAccount, 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 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() 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.StateAccount, 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

@ -120,25 +120,34 @@ func (r *reader) AccountRLP(hash common.Hash) ([]byte, error) {
} }
// Account directly retrieves the account associated with a particular hash in // 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 // the consensus (full) data format. An error will be returned if the read
// abnormally. Specifically, if the layer is already stale. // operation exits abnormally. Specifically, if the layer is already stale.
// //
// Note: // Note:
// - 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.StateAccount, 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

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

View file

@ -17,6 +17,7 @@
package pathdb package pathdb
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"maps" "maps"
@ -26,6 +27,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 +62,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.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) 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 +77,76 @@ type stateSet struct {
listLock sync.RWMutex listLock sync.RWMutex
} }
// 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, 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. The root and code hash are
// omitted in slim form when empty, matching SlimAccountRLP.
size := 9
if account.Balance != nil {
size += account.Balance.ByteLen()
}
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 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, 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 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.StateAccount {
decoded := make(map[common.Hash]*types.StateAccount, len(accounts))
for hash, blob := range accounts {
decoded[hash] = decodeAccountBlob(hash, blob)
}
return decoded
}
// 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.StateAccount) []byte {
if account == nil {
return nil
}
return types.SlimAccountRLP(*account)
}
// 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 (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 { 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 +156,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.StateAccount, 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 +169,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.StateAccount, 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 +206,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 +298,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 +358,14 @@ 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 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)) 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 +414,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 +463,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 +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 // 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.StateAccount)
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.StateAccount, 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,60 @@ func testStateWithOriginEncode(t *testing.T, rawStorageKey bool) {
} }
} }
// 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 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.StateAccount{
Nonce: 2,
Balance: uint256.NewInt(0x10000),
Root: common.BytesToHash(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 +472,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 +505,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 +520,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}: {