mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-18 19:00:46 +00:00
Merge 987cfb98fb into dddbaa4bf3
This commit is contained in:
commit
d1b67beecb
19 changed files with 634 additions and 144 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
91
core/state/reader_test.go
Normal 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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
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
|
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
|
||||||
|
|
|
||||||
|
|
@ -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}: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue