mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-18 19:00:46 +00:00
triedb/pathdb: improve EOA account retrieval
Before: 612ns now 540ns Layer 1 now: 42ns 17x speedup Layer 127 4100ns
This commit is contained in:
parent
03911b11eb
commit
987cfb98fb
15 changed files with 297 additions and 124 deletions
|
|
@ -66,7 +66,7 @@ func (db *MPTDatabase) StateReader(stateRoot common.Hash) (StateReader, error) {
|
|||
if db.TrieDB().Scheme() == rawdb.HashScheme && db.snap != nil {
|
||||
snap := db.snap.Snapshot(stateRoot)
|
||||
if snap != nil {
|
||||
readers = append(readers, newFlatReader(snap))
|
||||
readers = append(readers, newFlatReader(snapshot.NewStateReader(snap)))
|
||||
}
|
||||
}
|
||||
// Configure the state reader using the path database in path mode.
|
||||
|
|
|
|||
|
|
@ -98,26 +98,9 @@ func newFlatReader(reader database.StateReader) *flatReader {
|
|||
//
|
||||
// The returned account might be nil if it's not existent.
|
||||
func (r *flatReader) Account(addr common.Address) (*types.StateAccount, error) {
|
||||
account, err := r.reader.Account(crypto.Keccak256Hash(addr[:]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if account == nil {
|
||||
return nil, nil
|
||||
}
|
||||
acct := &types.StateAccount{
|
||||
Nonce: account.Nonce,
|
||||
Balance: account.Balance,
|
||||
CodeHash: account.CodeHash,
|
||||
Root: common.BytesToHash(account.Root),
|
||||
}
|
||||
if len(acct.CodeHash) == 0 {
|
||||
acct.CodeHash = types.EmptyCodeHash[:]
|
||||
}
|
||||
if acct.Root == (common.Hash{}) {
|
||||
acct.Root = types.EmptyRootHash
|
||||
}
|
||||
return acct, nil
|
||||
// The backing reader already returns accounts in the consensus (full)
|
||||
// format, so no slim->full conversion is required here.
|
||||
return r.reader.Account(crypto.Keccak256Hash(addr[:]))
|
||||
}
|
||||
|
||||
// Storage implements StateReader, retrieving the storage slot specified by the
|
||||
|
|
|
|||
|
|
@ -24,15 +24,15 @@ import (
|
|||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
||||
// benchStateReader is a minimal database.StateReader backed by an in-memory map.
|
||||
// It isolates the conversion overhead in flatReader.Account (address hashing,
|
||||
// the *types.StateAccount allocation and the slim->full fixups) from any trie,
|
||||
// snapshot or disk access.
|
||||
// benchStateReader is a minimal database.StateReader backed by a single account.
|
||||
// It isolates flatReader.Account (just an address hash plus a forwarding call,
|
||||
// now that the backing reader returns full accounts) from any trie, snapshot or
|
||||
// disk access.
|
||||
type benchStateReader struct {
|
||||
account *types.SlimAccount
|
||||
account *types.StateAccount
|
||||
}
|
||||
|
||||
func (r *benchStateReader) Account(hash common.Hash) (*types.SlimAccount, error) {
|
||||
func (r *benchStateReader) Account(hash common.Hash) (*types.StateAccount, error) {
|
||||
// Return a fresh copy on every call, mirroring the contract that the
|
||||
// returned account is safe to modify by the caller.
|
||||
if r.account == nil {
|
||||
|
|
@ -47,12 +47,8 @@ func (r *benchStateReader) Storage(accountHash, storageHash common.Hash) ([]byte
|
|||
}
|
||||
|
||||
// benchmarkFlatReaderAccount measures flatReader.Account for a single address.
|
||||
// The provided slim account dictates which branches of the conversion are hit:
|
||||
// a slim account with nil Root/CodeHash exercises the EmptyCodeHash.Bytes() and
|
||||
// EmptyRootHash fixups (the lines that dominate the production profile), whereas
|
||||
// a fully populated one skips them.
|
||||
func benchmarkFlatReaderAccount(b *testing.B, slim *types.SlimAccount) {
|
||||
r := newFlatReader(&benchStateReader{account: slim})
|
||||
func benchmarkFlatReaderAccount(b *testing.B, account *types.StateAccount) {
|
||||
r := newFlatReader(&benchStateReader{account: account})
|
||||
addr := common.HexToAddress("0x1234567890123456789012345678901234567890")
|
||||
|
||||
b.ReportAllocs()
|
||||
|
|
@ -69,25 +65,27 @@ func benchmarkFlatReaderAccount(b *testing.B, slim *types.SlimAccount) {
|
|||
}
|
||||
|
||||
// BenchmarkFlatReaderAccountEmpty benchmarks the common EOA case: an account
|
||||
// with no code and no storage. This is the hot path from the profile, hitting
|
||||
// both the EmptyCodeHash and EmptyRootHash fixups.
|
||||
// with no code and empty storage. Previously this path paid a slim->full
|
||||
// conversion with an allocation; the backing reader now returns full accounts
|
||||
// so flatReader only forwards.
|
||||
func BenchmarkFlatReaderAccountEmpty(b *testing.B) {
|
||||
benchmarkFlatReaderAccount(b, &types.SlimAccount{
|
||||
Nonce: 1,
|
||||
Balance: uint256.NewInt(100),
|
||||
// Root and CodeHash left nil: slim encoding of an EOA.
|
||||
benchmarkFlatReaderAccount(b, &types.StateAccount{
|
||||
Nonce: 1,
|
||||
Balance: uint256.NewInt(100),
|
||||
Root: types.EmptyRootHash,
|
||||
CodeHash: types.EmptyCodeHash[:],
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkFlatReaderAccountContract benchmarks a contract account with a
|
||||
// non-empty storage root and code hash, skipping the empty-value fixups.
|
||||
// non-empty storage root and code hash.
|
||||
func BenchmarkFlatReaderAccountContract(b *testing.B) {
|
||||
root := common.HexToHash("0xaabbccddeeff00112233445566778899aabbccddeeff00112233445566778899")
|
||||
codeHash := common.HexToHash("0x112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00")
|
||||
benchmarkFlatReaderAccount(b, &types.SlimAccount{
|
||||
benchmarkFlatReaderAccount(b, &types.StateAccount{
|
||||
Nonce: 7,
|
||||
Balance: uint256.NewInt(1000),
|
||||
Root: root.Bytes(),
|
||||
Root: root,
|
||||
CodeHash: codeHash.Bytes(),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,8 +32,56 @@ import (
|
|||
"github.com/ethereum/go-ethereum/metrics"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
"github.com/ethereum/go-ethereum/triedb"
|
||||
"github.com/ethereum/go-ethereum/triedb/database"
|
||||
)
|
||||
|
||||
// stateReader adapts a Snapshot, whose Account method returns accounts in the
|
||||
// slim data format, to the database.StateReader interface which returns
|
||||
// accounts in the consensus (full) format.
|
||||
type stateReader struct {
|
||||
snap Snapshot
|
||||
}
|
||||
|
||||
// NewStateReader wraps the given snapshot so it satisfies database.StateReader,
|
||||
// converting accounts from the slim format to the consensus (full) format. It
|
||||
// returns nil if the snapshot is nil.
|
||||
func NewStateReader(snap Snapshot) database.StateReader {
|
||||
if snap == nil {
|
||||
return nil
|
||||
}
|
||||
return stateReader{snap: snap}
|
||||
}
|
||||
|
||||
// Account implements database.StateReader, converting the snapshot's slim
|
||||
// account into the consensus (full) format.
|
||||
func (r stateReader) Account(hash common.Hash) (*types.StateAccount, error) {
|
||||
account, err := r.snap.Account(hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if account == nil {
|
||||
return nil, nil
|
||||
}
|
||||
acct := &types.StateAccount{
|
||||
Nonce: account.Nonce,
|
||||
Balance: account.Balance,
|
||||
CodeHash: account.CodeHash,
|
||||
Root: common.BytesToHash(account.Root),
|
||||
}
|
||||
if len(acct.CodeHash) == 0 {
|
||||
acct.CodeHash = types.EmptyCodeHash[:]
|
||||
}
|
||||
if acct.Root == (common.Hash{}) {
|
||||
acct.Root = types.EmptyRootHash
|
||||
}
|
||||
return acct, nil
|
||||
}
|
||||
|
||||
// Storage implements database.StateReader.
|
||||
func (r stateReader) Storage(accountHash, storageHash common.Hash) ([]byte, error) {
|
||||
return r.snap.Storage(accountHash, storageHash)
|
||||
}
|
||||
|
||||
var (
|
||||
snapshotCleanAccountHitMeter = metrics.NewRegisteredMeter("state/snapshot/clean/account/hit", nil)
|
||||
snapshotCleanAccountMissMeter = metrics.NewRegisteredMeter("state/snapshot/clean/account/miss", nil)
|
||||
|
|
|
|||
|
|
@ -439,7 +439,7 @@ func ServiceGetTrieNodesQuery(chain *core.BlockChain, req *GetTrieNodesPacket) (
|
|||
// via snapshot.
|
||||
var reader database.StateReader
|
||||
if chain.Snapshots() != nil {
|
||||
reader = chain.Snapshots().Snapshot(req.Root)
|
||||
reader = snapshot.NewStateReader(chain.Snapshots().Snapshot(req.Root))
|
||||
}
|
||||
if reader == nil {
|
||||
reader, _ = triedb.StateReader(req.Root)
|
||||
|
|
@ -499,7 +499,7 @@ func ServiceGetTrieNodesQuery(chain *core.BlockChain, req *GetTrieNodesPacket) (
|
|||
if err != nil || account == nil {
|
||||
break
|
||||
}
|
||||
stRoot = common.BytesToHash(account.Root)
|
||||
stRoot = account.Root
|
||||
}
|
||||
|
||||
id := trie.StorageTrieID(req.Root, common.BytesToHash(accKey), stRoot)
|
||||
|
|
|
|||
|
|
@ -44,13 +44,13 @@ type NodeDatabase interface {
|
|||
// StateReader wraps the Account and Storage method of a backing state reader.
|
||||
type StateReader interface {
|
||||
// Account directly retrieves the account associated with a particular hash in
|
||||
// the slim data format. An error will be returned if the read operation exits
|
||||
// abnormally. Specifically, if the layer is already stale.
|
||||
// the consensus (full) data format. An error will be returned if the read
|
||||
// operation exits abnormally. Specifically, if the layer is already stale.
|
||||
//
|
||||
// Note:
|
||||
// - the returned account object is safe to modify
|
||||
// - no error will be returned if the requested account is not found in database
|
||||
Account(hash common.Hash) (*types.SlimAccount, error)
|
||||
Account(hash common.Hash) (*types.StateAccount, error)
|
||||
|
||||
// Storage directly retrieves the storage data associated with a particular hash,
|
||||
// within a particular account. An error will be returned if the read operation
|
||||
|
|
|
|||
|
|
@ -65,9 +65,10 @@ func newBuffer(limit int, nodes *nodeSet, states *stateSet, layers uint64) *buff
|
|||
}
|
||||
}
|
||||
|
||||
// account retrieves the decoded slim account with account address hash. A nil
|
||||
// account with found=true denotes a known-deleted account.
|
||||
func (b *buffer) account(hash common.Hash) (*types.SlimAccount, bool) {
|
||||
// account retrieves the decoded account with account address hash, in the
|
||||
// consensus (full) format. A nil account with found=true denotes a
|
||||
// known-deleted account.
|
||||
func (b *buffer) account(hash common.Hash) (*types.StateAccount, bool) {
|
||||
return b.states.account(hash)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,15 +56,16 @@ type layer interface {
|
|||
// - no error will be returned if the requested account is not found in database.
|
||||
account(hash common.Hash, depth int) ([]byte, error)
|
||||
|
||||
// accountObject directly retrieves the decoded slim account associated with
|
||||
// a particular hash. It mirrors account but avoids re-encoding/decoding for
|
||||
// the diff-layer hot read path, where accounts are already held in decoded
|
||||
// form. An error will be returned if the read operation exits abnormally.
|
||||
// accountObject directly retrieves the decoded account associated with a
|
||||
// particular hash, in the consensus (full) format. It mirrors account but
|
||||
// avoids re-encoding/decoding for the diff-layer hot read path, where
|
||||
// accounts are already held in decoded form. An error will be returned if
|
||||
// the read operation exits abnormally.
|
||||
//
|
||||
// Note:
|
||||
// - the returned account is not a copy, please don't modify it.
|
||||
// - a nil account is returned if the requested account is not found or was deleted.
|
||||
accountObject(hash common.Hash, depth int) (*types.SlimAccount, error)
|
||||
accountObject(hash common.Hash, depth int) (*types.StateAccount, error)
|
||||
|
||||
// storage directly retrieves the storage data associated with a particular hash,
|
||||
// within a particular account. An error will be returned if the read operation
|
||||
|
|
|
|||
|
|
@ -113,12 +113,12 @@ func (dl *diffLayer) account(hash common.Hash, depth int) ([]byte, error) {
|
|||
return encodeSlimAccount(account), nil
|
||||
}
|
||||
|
||||
// accountObject directly retrieves the decoded slim account associated with a
|
||||
// particular hash. A nil account is returned if the account does not exist or
|
||||
// was deleted in this hierarchy.
|
||||
// accountObject directly retrieves the decoded account associated with a
|
||||
// particular hash, in the consensus (full) format. A nil account is returned
|
||||
// if the account does not exist or was deleted in this hierarchy.
|
||||
//
|
||||
// Note the returned account is not a copy, please don't modify it.
|
||||
func (dl *diffLayer) accountObject(hash common.Hash, depth int) (*types.SlimAccount, error) {
|
||||
func (dl *diffLayer) accountObject(hash common.Hash, depth int) (*types.StateAccount, error) {
|
||||
// Hold the lock, ensure the parent won't be changed during the
|
||||
// state accessing.
|
||||
dl.lock.RLock()
|
||||
|
|
|
|||
|
|
@ -177,10 +177,10 @@ func (dl *diskLayer) account(hash common.Hash, depth int) ([]byte, error) {
|
|||
return encodeSlimAccount(account), nil
|
||||
}
|
||||
|
||||
// accountObject directly retrieves the decoded slim account associated with a
|
||||
// particular hash. A nil account is returned if the account does not exist or
|
||||
// was deleted.
|
||||
func (dl *diskLayer) accountObject(hash common.Hash, depth int) (*types.SlimAccount, error) {
|
||||
// accountObject directly retrieves the decoded account associated with a
|
||||
// particular hash, in the consensus (full) format. A nil account is returned
|
||||
// if the account does not exist or was deleted.
|
||||
func (dl *diskLayer) accountObject(hash common.Hash, depth int) (*types.StateAccount, error) {
|
||||
dl.lock.RLock()
|
||||
defer dl.lock.RUnlock()
|
||||
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ func writeNodes(batch ethdb.Batch, nodes map[common.Hash]map[string]*trienode.No
|
|||
// TODO(rjl493456442) do we really need this generation marker? The state updates
|
||||
// after the marker can also be written and will be fixed by generator later if
|
||||
// it's outdated.
|
||||
func writeStates(batch ethdb.Batch, genMarker []byte, accountData map[common.Hash]*types.SlimAccount, storageData map[common.Hash]map[common.Hash][]byte, clean *fastcache.Cache) (int, int) {
|
||||
func writeStates(batch ethdb.Batch, genMarker []byte, accountData map[common.Hash]*types.StateAccount, storageData map[common.Hash]map[common.Hash][]byte, clean *fastcache.Cache) (int, int) {
|
||||
var (
|
||||
accounts int
|
||||
slots int
|
||||
|
|
|
|||
|
|
@ -120,13 +120,13 @@ func (r *reader) AccountRLP(hash common.Hash) ([]byte, error) {
|
|||
}
|
||||
|
||||
// Account directly retrieves the account associated with a particular hash in
|
||||
// the slim data format. An error will be returned if the read operation exits
|
||||
// abnormally. Specifically, if the layer is already stale.
|
||||
// the consensus (full) data format. An error will be returned if the read
|
||||
// operation exits abnormally. Specifically, if the layer is already stale.
|
||||
//
|
||||
// Note:
|
||||
// - the returned account object is safe to modify
|
||||
// - no error will be returned if the requested account is not found in database
|
||||
func (r *reader) Account(hash common.Hash) (*types.SlimAccount, error) {
|
||||
func (r *reader) Account(hash common.Hash) (*types.StateAccount, error) {
|
||||
l, err := r.db.tree.lookupAccount(hash, r.state)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
145
triedb/pathdb/reader_bench_test.go
Normal file
145
triedb/pathdb/reader_bench_test.go
Normal 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) }
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
package pathdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
|
|
@ -61,7 +62,7 @@ func (c *counter) report(count, size *metrics.Meter) {
|
|||
// subsequent state access will be denied due to the stale flag. Therefore,
|
||||
// state access and mutation won't happen at the same time with guarantee.
|
||||
type stateSet struct {
|
||||
accountData map[common.Hash]*types.SlimAccount // Keyed accounts for direct retrieval (nil means deleted)
|
||||
accountData map[common.Hash]*types.StateAccount // Keyed accounts for direct retrieval (nil means deleted)
|
||||
storageData map[common.Hash]map[common.Hash][]byte // Keyed storage slots for direct retrieval. one per account (nil means deleted)
|
||||
size uint64 // Memory size of the state data (accountData and storageData)
|
||||
|
||||
|
|
@ -76,74 +77,69 @@ type stateSet struct {
|
|||
listLock sync.RWMutex
|
||||
}
|
||||
|
||||
// slimAccountSize returns the approximate encoded size of a slim account, used
|
||||
// slimAccountSize returns the approximate slim-encoded size of an account, used
|
||||
// for memory accounting. It estimates the slim-RLP byte length without actually
|
||||
// encoding the account. A nil account (deleted marker) contributes zero.
|
||||
func slimAccountSize(account *types.SlimAccount) int {
|
||||
// encoding the account, mirroring how the account is serialized to disk: the
|
||||
// empty storage root and code hash are omitted in slim form. A nil account
|
||||
// (deleted marker) contributes zero.
|
||||
func slimAccountSize(account *types.StateAccount) int {
|
||||
if account == nil {
|
||||
return 0
|
||||
}
|
||||
// 9 bytes covers the RLP list header plus the nonce; the balance, root and
|
||||
// code hash contribute their own byte lengths. This intentionally mirrors
|
||||
// the previous behavior of accounting for the encoded blob length.
|
||||
// code hash contribute their own byte lengths. The root and code hash are
|
||||
// omitted in slim form when empty, matching SlimAccountRLP.
|
||||
size := 9
|
||||
if account.Balance != nil {
|
||||
size += account.Balance.ByteLen()
|
||||
}
|
||||
size += len(account.Root)
|
||||
size += len(account.CodeHash)
|
||||
if account.Root != types.EmptyRootHash {
|
||||
size += len(account.Root)
|
||||
}
|
||||
if !bytes.Equal(account.CodeHash, types.EmptyCodeHash[:]) {
|
||||
size += len(account.CodeHash)
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
// decodeAccountBlob decodes a single slim-RLP-encoded account blob. A nil/empty
|
||||
// blob denotes a non-existent or deleted account and decodes to a nil account.
|
||||
func decodeAccountBlob(hash common.Hash, blob []byte) *types.SlimAccount {
|
||||
// decodeAccountBlob decodes a single slim-RLP-encoded account blob into the
|
||||
// consensus (full) account format. A nil/empty blob denotes a non-existent or
|
||||
// deleted account and decodes to a nil account.
|
||||
func decodeAccountBlob(hash common.Hash, blob []byte) *types.StateAccount {
|
||||
if len(blob) == 0 {
|
||||
return nil
|
||||
}
|
||||
account := new(types.SlimAccount)
|
||||
if err := rlp.DecodeBytes(blob, account); err != nil {
|
||||
account, err := types.FullAccount(blob)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to decode account %x: %v", hash, err))
|
||||
}
|
||||
return account
|
||||
}
|
||||
|
||||
// decodeAccounts converts a map of slim-RLP-encoded accounts into decoded slim
|
||||
// decodeAccounts converts a map of slim-RLP-encoded accounts into decoded full
|
||||
// account objects. A nil/empty blob denotes a deleted account and is preserved
|
||||
// as a nil entry in the resulting map.
|
||||
func decodeAccounts(accounts map[common.Hash][]byte) map[common.Hash]*types.SlimAccount {
|
||||
decoded := make(map[common.Hash]*types.SlimAccount, len(accounts))
|
||||
func decodeAccounts(accounts map[common.Hash][]byte) map[common.Hash]*types.StateAccount {
|
||||
decoded := make(map[common.Hash]*types.StateAccount, len(accounts))
|
||||
for hash, blob := range accounts {
|
||||
if len(blob) == 0 {
|
||||
decoded[hash] = nil // deleted account
|
||||
continue
|
||||
}
|
||||
account := new(types.SlimAccount)
|
||||
if err := rlp.DecodeBytes(blob, account); err != nil {
|
||||
panic(fmt.Sprintf("failed to decode account %x: %v", hash, err))
|
||||
}
|
||||
decoded[hash] = account
|
||||
decoded[hash] = decodeAccountBlob(hash, blob)
|
||||
}
|
||||
return decoded
|
||||
}
|
||||
|
||||
// encodeSlimAccount serializes a decoded slim account back into its slim-RLP
|
||||
// encodeSlimAccount serializes a decoded full account back into its slim-RLP
|
||||
// byte form. A nil account (deleted marker) encodes to a nil blob.
|
||||
func encodeSlimAccount(account *types.SlimAccount) []byte {
|
||||
func encodeSlimAccount(account *types.StateAccount) []byte {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
blob, err := rlp.EncodeToBytes(account)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to encode account: %v", err))
|
||||
}
|
||||
return blob
|
||||
return types.SlimAccountRLP(*account)
|
||||
}
|
||||
|
||||
// newStates constructs the state set with the provided account and storage data.
|
||||
// The accounts are supplied in slim-RLP form and are decoded once on ingest;
|
||||
// they are kept in decoded form for the lifetime of the set and only re-encoded
|
||||
// at the serialization boundaries (journal and disk flush).
|
||||
// they are kept in decoded (full) form for the lifetime of the set and only
|
||||
// re-encoded at the serialization boundaries (journal and disk flush).
|
||||
func newStates(accounts map[common.Hash][]byte, storages map[common.Hash]map[common.Hash][]byte, rawStorageKey bool) *stateSet {
|
||||
// Don't panic for the lazy callers, initialize the nil maps instead.
|
||||
if storages == nil {
|
||||
|
|
@ -162,7 +158,7 @@ func newStates(accounts map[common.Hash][]byte, storages map[common.Hash]map[com
|
|||
// account returns the account data associated with the specified address hash.
|
||||
// A nil account with found=true denotes a known-deleted account, whereas
|
||||
// found=false indicates the account is unknown in this set.
|
||||
func (s *stateSet) account(hash common.Hash) (*types.SlimAccount, bool) {
|
||||
func (s *stateSet) account(hash common.Hash) (*types.StateAccount, bool) {
|
||||
// If the account is known locally, return it
|
||||
if data, ok := s.accountData[hash]; ok {
|
||||
return data, true
|
||||
|
|
@ -173,7 +169,7 @@ func (s *stateSet) account(hash common.Hash) (*types.SlimAccount, bool) {
|
|||
// mustAccount returns the account data associated with the specified address
|
||||
// hash. The difference is this function will return an error if the account
|
||||
// is not found.
|
||||
func (s *stateSet) mustAccount(hash common.Hash) (*types.SlimAccount, error) {
|
||||
func (s *stateSet) mustAccount(hash common.Hash) (*types.StateAccount, error) {
|
||||
// If the account is known locally, return it
|
||||
if data, ok := s.accountData[hash]; ok {
|
||||
return data, nil
|
||||
|
|
@ -362,15 +358,9 @@ func (s *stateSet) revertTo(accountOrigin map[common.Hash][]byte, storageOrigin
|
|||
if !ok {
|
||||
panic(fmt.Sprintf("non-existent account for reverting, %x", addrHash))
|
||||
}
|
||||
// Decode the original slim-RLP value; an empty blob denotes a deleted
|
||||
// account, represented as a nil entry.
|
||||
var origin *types.SlimAccount
|
||||
if len(blob) != 0 {
|
||||
origin = new(types.SlimAccount)
|
||||
if err := rlp.DecodeBytes(blob, origin); err != nil {
|
||||
panic(fmt.Sprintf("failed to decode account %x: %v", addrHash, err))
|
||||
}
|
||||
}
|
||||
// Decode the original slim-RLP value into the consensus (full) format;
|
||||
// an empty blob denotes a deleted account, represented as a nil entry.
|
||||
origin := decodeAccountBlob(addrHash, blob)
|
||||
if data == nil && origin == nil {
|
||||
panic(fmt.Sprintf("invalid account mutation (null to null), %x", addrHash))
|
||||
}
|
||||
|
|
@ -509,7 +499,7 @@ func (s *stateSet) write(batch ethdb.Batch, genMarker []byte, clean *fastcache.C
|
|||
// reset clears all cached state data, including any optional sorted lists that
|
||||
// may have been generated.
|
||||
func (s *stateSet) reset() {
|
||||
s.accountData = make(map[common.Hash]*types.SlimAccount)
|
||||
s.accountData = make(map[common.Hash]*types.StateAccount)
|
||||
s.storageData = make(map[common.Hash]map[common.Hash][]byte)
|
||||
s.size = 0
|
||||
s.accountListSorted = nil
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ func testAccountRLP(tag byte) []byte {
|
|||
|
||||
// accountEqualsTag reports whether the decoded account matches the account
|
||||
// produced by testAccountRLP for the given tag.
|
||||
func accountEqualsTag(account *types.SlimAccount, tag byte) bool {
|
||||
func accountEqualsTag(account *types.StateAccount, tag byte) bool {
|
||||
if account == nil {
|
||||
return false
|
||||
}
|
||||
|
|
@ -399,27 +399,34 @@ func testStateWithOriginEncode(t *testing.T, rawStorageKey bool) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestSlimAccountSize verifies the estimated memory size of a slim account for
|
||||
// the empty/deleted, EOA and contract cases. The accountData map keys are
|
||||
// accounted for separately (common.HashLength per entry), so this only covers
|
||||
// the value contribution.
|
||||
// TestSlimAccountSize verifies the estimated slim-encoded memory size of a full
|
||||
// account for the empty/deleted, EOA and contract cases. The accountData map
|
||||
// keys are accounted for separately (common.HashLength per entry), so this only
|
||||
// covers the value contribution. slimAccountSize takes a full account but
|
||||
// estimates the slim encoding, where an empty storage root and code hash are
|
||||
// omitted.
|
||||
func TestSlimAccountSize(t *testing.T) {
|
||||
// A nil account (deletion marker) contributes nothing.
|
||||
if got := slimAccountSize(nil); got != 0 {
|
||||
t.Fatalf("nil account size, want: 0, got: %d", got)
|
||||
}
|
||||
// An EOA with no code and empty storage root carries nil Root/CodeHash in
|
||||
// slim form, so only the 9-byte list/nonce overhead plus the balance length
|
||||
// is counted.
|
||||
eoa := &types.SlimAccount{Nonce: 1, Balance: uint256.NewInt(0x100)}
|
||||
// An EOA with empty code and empty storage root: Root/CodeHash are omitted
|
||||
// in slim form, so only the 9-byte list/nonce overhead plus the balance
|
||||
// length is counted.
|
||||
eoa := &types.StateAccount{
|
||||
Nonce: 1,
|
||||
Balance: uint256.NewInt(0x100),
|
||||
Root: types.EmptyRootHash,
|
||||
CodeHash: types.EmptyCodeHash[:],
|
||||
}
|
||||
if got, want := slimAccountSize(eoa), 9+eoa.Balance.ByteLen(); got != want {
|
||||
t.Fatalf("EOA account size, want: %d, got: %d", want, got)
|
||||
}
|
||||
// A contract carries a 32-byte root and 32-byte code hash in addition.
|
||||
contract := &types.SlimAccount{
|
||||
contract := &types.StateAccount{
|
||||
Nonce: 2,
|
||||
Balance: uint256.NewInt(0x10000),
|
||||
Root: testrand.Bytes(32),
|
||||
Root: common.BytesToHash(testrand.Bytes(32)),
|
||||
CodeHash: testrand.Bytes(32),
|
||||
}
|
||||
if got, want := slimAccountSize(contract), 9+contract.Balance.ByteLen()+64; got != want {
|
||||
|
|
|
|||
Loading…
Reference in a new issue