triedb/pathdb: improve EOA account retrieval

Before: 612ns now 540ns
Layer 1 now: 42ns 17x speedup
Layer 127 4100ns
This commit is contained in:
MariusVanDerWijden 2026-06-22 17:47:25 +02:00
parent 03911b11eb
commit 987cfb98fb
No known key found for this signature in database
15 changed files with 297 additions and 124 deletions

View file

@ -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.

View file

@ -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

View file

@ -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(),
})
}

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)
}

View file

@ -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

View file

@ -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()

View file

@ -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()

View file

@ -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

View file

@ -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

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
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

View file

@ -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 {