diff --git a/core/state/database_mpt.go b/core/state/database_mpt.go index 42c5f2e5ef..fd07523cef 100644 --- a/core/state/database_mpt.go +++ b/core/state/database_mpt.go @@ -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. diff --git a/core/state/reader.go b/core/state/reader.go index 2163da8a9f..64930f17aa 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -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 diff --git a/core/state/reader_test.go b/core/state/reader_test.go index 7a38971f28..1b221ab791 100644 --- a/core/state/reader_test.go +++ b/core/state/reader_test.go @@ -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(), }) } diff --git a/core/state/snapshot/snapshot.go b/core/state/snapshot/snapshot.go index d3a5e9aa21..97744968e5 100644 --- a/core/state/snapshot/snapshot.go +++ b/core/state/snapshot/snapshot.go @@ -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) diff --git a/eth/protocols/snap/handlers.go b/eth/protocols/snap/handlers.go index 106322de90..f3dd7437a6 100644 --- a/eth/protocols/snap/handlers.go +++ b/eth/protocols/snap/handlers.go @@ -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) diff --git a/triedb/database/database.go b/triedb/database/database.go index 8c61ea0293..56cc3beb25 100644 --- a/triedb/database/database.go +++ b/triedb/database/database.go @@ -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 diff --git a/triedb/pathdb/buffer.go b/triedb/pathdb/buffer.go index 7afc1826c7..3d95f2ca8a 100644 --- a/triedb/pathdb/buffer.go +++ b/triedb/pathdb/buffer.go @@ -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) } diff --git a/triedb/pathdb/database.go b/triedb/pathdb/database.go index ca31647997..e1691349cf 100644 --- a/triedb/pathdb/database.go +++ b/triedb/pathdb/database.go @@ -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 diff --git a/triedb/pathdb/difflayer.go b/triedb/pathdb/difflayer.go index f92b380118..bb584d7fcb 100644 --- a/triedb/pathdb/difflayer.go +++ b/triedb/pathdb/difflayer.go @@ -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() diff --git a/triedb/pathdb/disklayer.go b/triedb/pathdb/disklayer.go index 27a73af53b..fafe5b81a9 100644 --- a/triedb/pathdb/disklayer.go +++ b/triedb/pathdb/disklayer.go @@ -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() diff --git a/triedb/pathdb/flush.go b/triedb/pathdb/flush.go index 8d7f791e3b..288f5fc77f 100644 --- a/triedb/pathdb/flush.go +++ b/triedb/pathdb/flush.go @@ -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 diff --git a/triedb/pathdb/reader.go b/triedb/pathdb/reader.go index 9a006a2dbd..b1f9c1f443 100644 --- a/triedb/pathdb/reader.go +++ b/triedb/pathdb/reader.go @@ -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 diff --git a/triedb/pathdb/reader_bench_test.go b/triedb/pathdb/reader_bench_test.go new file mode 100644 index 0000000000..5a0b1bb964 --- /dev/null +++ b/triedb/pathdb/reader_bench_test.go @@ -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 . + +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) } diff --git a/triedb/pathdb/states.go b/triedb/pathdb/states.go index 6c79e62bfb..017c56b0be 100644 --- a/triedb/pathdb/states.go +++ b/triedb/pathdb/states.go @@ -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 diff --git a/triedb/pathdb/states_test.go b/triedb/pathdb/states_test.go index ff1a1078e0..3e67006e96 100644 --- a/triedb/pathdb/states_test.go +++ b/triedb/pathdb/states_test.go @@ -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 {