diff --git a/core/state/database_history.go b/core/state/database_history.go
index fbf4ab5f9c..170f79da93 100644
--- a/core/state/database_history.go
+++ b/core/state/database_history.go
@@ -62,7 +62,7 @@ func (r *historicStateReader) Account(addr common.Address) (*types.StateAccount,
Root: common.BytesToHash(account.Root),
}
if len(acct.CodeHash) == 0 {
- acct.CodeHash = types.EmptyCodeHash.Bytes()
+ acct.CodeHash = types.EmptyCodeHash[:]
}
if acct.Root == (common.Hash{}) {
acct.Root = types.EmptyRootHash
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 be07cec0f9..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.Bytes()
- }
- 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
new file mode 100644
index 0000000000..1b221ab791
--- /dev/null
+++ b/core/state/reader_test.go
@@ -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 .
+
+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(),
+ })
+}
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 5d3099285f..3d95f2ca8a 100644
--- a/triedb/pathdb/buffer.go
+++ b/triedb/pathdb/buffer.go
@@ -24,6 +24,7 @@ import (
"github.com/VictoriaMetrics/fastcache"
"github.com/ethereum/go-ethereum/common"
"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/log"
"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.
-func (b *buffer) account(hash common.Hash) ([]byte, 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 996d3f3ce1..cb97594167 100644
--- a/triedb/pathdb/database.go
+++ b/triedb/pathdb/database.go
@@ -56,6 +56,17 @@ 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 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,
// within a particular account. An error will be returned if the read operation
// exits abnormally. Specifically, if the layer is already stale.
diff --git a/triedb/pathdb/difflayer.go b/triedb/pathdb/difflayer.go
index 8ca3a39cf3..bb584d7fcb 100644
--- a/triedb/pathdb/difflayer.go
+++ b/triedb/pathdb/difflayer.go
@@ -21,6 +21,7 @@ import (
"sync"
"github.com/ethereum/go-ethereum/common"
+ "github.com/ethereum/go-ethereum/core/types"
"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.
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
// state accessing.
dl.lock.RLock()
defer dl.lock.RUnlock()
- if blob, found := dl.states.account(hash); found {
+ if account, found := dl.states.account(hash); found {
dirtyStateHitMeter.Mark(1)
dirtyStateHitDepthHist.Update(int64(depth))
- dirtyStateReadMeter.Mark(int64(len(blob)))
+ dirtyStateReadMeter.Mark(int64(slimAccountSize(account)))
- if len(blob) == 0 {
+ if account == nil {
stateAccountInexMeter.Mark(1)
} else {
stateAccountExistMeter.Mark(1)
}
- return blob, nil
+ return account, nil
}
// 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,
diff --git a/triedb/pathdb/disklayer.go b/triedb/pathdb/disklayer.go
index 8c0a751932..fafe5b81a9 100644
--- a/triedb/pathdb/disklayer.go
+++ b/triedb/pathdb/disklayer.go
@@ -25,6 +25,7 @@ import (
"github.com/VictoriaMetrics/fastcache"
"github.com/ethereum/go-ethereum/common"
"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/ethdb"
"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.
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()
defer dl.lock.RUnlock()
if dl.stale {
return nil, errSnapshotStale
}
- // Try to retrieve the trie node from the not-yet-written node buffer first
- // (both the live one and the frozen one). Note the buffer is lock free since
- // it's impossible to mutate the buffer before tagging the layer as stale.
+ // Try to retrieve the account from the not-yet-written state buffer first
+ // (both the live one and the frozen one). The buffer holds accounts in
+ // 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} {
if buffer != nil {
- blob, found := buffer.account(hash)
+ account, found := buffer.account(hash)
if found {
dirtyStateHitMeter.Mark(1)
- dirtyStateReadMeter.Mark(int64(len(blob)))
+ dirtyStateReadMeter.Mark(int64(slimAccountSize(account)))
dirtyStateHitDepthHist.Update(int64(depth))
- if len(blob) == 0 {
+ if account == nil {
stateAccountInexMeter.Mark(1)
} else {
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 {
stateAccountExistMeter.Mark(1)
}
- return blob, nil
+ return decodeAccountBlob(hash, blob), nil
}
cleanStateMissMeter.Mark(1)
}
@@ -237,7 +251,7 @@ func (dl *diskLayer) account(hash common.Hash, depth int) ([]byte, error) {
stateAccountExistMeter.Mark(1)
stateAccountExistDiskMeter.Mark(1)
}
- return blob, nil
+ return decodeAccountBlob(hash, blob), nil
}
// 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()
writeNodes(batch, nodes, dl.nodes)
- // Provide the original values of modified accounts and storages for revert
- writeStates(batch, progress, accounts, storages, dl.states)
+ // Provide the original values of modified accounts and storages for revert.
+ // 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.WriteSnapshotRoot(batch, h.meta.parent)
if err := batch.Write(); err != nil {
diff --git a/triedb/pathdb/flush.go b/triedb/pathdb/flush.go
index 4f816cf6a6..288f5fc77f 100644
--- a/triedb/pathdb/flush.go
+++ b/triedb/pathdb/flush.go
@@ -22,6 +22,7 @@ import (
"github.com/VictoriaMetrics/fastcache"
"github.com/ethereum/go-ethereum/common"
"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/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
// 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][]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 (
accounts int
slots int
)
- for addrHash, blob := range accountData {
+ for addrHash, account := range accountData {
// Skip any account not yet covered by the snapshot. The account
// at the generation marker position (addrHash == genMarker[:common.HashLength])
// 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
}
accounts += 1
- if len(blob) == 0 {
+ if account == nil {
rawdb.DeleteAccountSnapshot(batch, addrHash)
if clean != nil {
clean.Set(addrHash[:], nil)
}
} 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)
if clean != nil {
clean.Set(addrHash[:], blob)
diff --git a/triedb/pathdb/generate_test.go b/triedb/pathdb/generate_test.go
index f38a1ed7c4..a3c13f7c42 100644
--- a/triedb/pathdb/generate_test.go
+++ b/triedb/pathdb/generate_test.go
@@ -69,7 +69,7 @@ func (t *genTester) addTrieAccount(acckey string, acc *types.StateAccount) {
)
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
}
diff --git a/triedb/pathdb/iterator_fast.go b/triedb/pathdb/iterator_fast.go
index 04bf89e874..0f8b931a1f 100644
--- a/triedb/pathdb/iterator_fast.go
+++ b/triedb/pathdb/iterator_fast.go
@@ -98,7 +98,11 @@ func newFastIterator(db *Database, root common.Hash, account common.Hash, seek c
if dl.stale {
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,
})
@@ -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,
// so the read lock protection is unnecessary.
accountList := dl.states.accountList()
+ states := dl.states
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,
})
}
diff --git a/triedb/pathdb/iterator_test.go b/triedb/pathdb/iterator_test.go
index 191c2fadf5..3bce1989ea 100644
--- a/triedb/pathdb/iterator_test.go
+++ b/triedb/pathdb/iterator_test.go
@@ -122,8 +122,7 @@ func TestAccountIteratorBasics(t *testing.T) {
// Fill up a parent
for i := 0; i < 100; i++ {
hash := testrand.Hash()
- data := testrand.Bytes(32)
- accounts[hash] = data
+ accounts[hash] = randomAccount()
if rand.Intn(2) == 0 {
accStorage := make(map[common.Hash][]byte)
@@ -153,7 +152,7 @@ func TestStorageIteratorBasics(t *testing.T) {
// Fill some random data
for i := 0; i < 10; i++ {
hash := testrand.Hash()
- accounts[hash] = testrand.Bytes(32)
+ accounts[hash] = randomAccount()
accStorage := make(map[common.Hash][]byte)
var nilstorage int
@@ -344,6 +343,18 @@ func TestAccountIteratorTraversalValues(t *testing.T) {
}
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
var (
a = make(map[common.Hash][]byte)
@@ -356,27 +367,27 @@ func TestAccountIteratorTraversalValues(t *testing.T) {
h = make(map[common.Hash][]byte)
)
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 {
- b[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 1, i)
+ b[common.Hash{i}] = layerAccount(1, i)
}
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 {
- d[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 3, i)
+ d[common.Hash{i}] = layerAccount(3, i)
}
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 {
- f[common.Hash{i}] = fmt.Appendf(nil, "layer-%d, key %d", 5, i)
+ f[common.Hash{i}] = layerAccount(5, i)
}
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 {
- 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
diff --git a/triedb/pathdb/reader.go b/triedb/pathdb/reader.go
index e087ef26ed..b1f9c1f443 100644
--- a/triedb/pathdb/reader.go
+++ b/triedb/pathdb/reader.go
@@ -120,25 +120,34 @@ 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) {
- blob, err := r.AccountRLP(hash)
+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
}
- 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
}
- account := new(types.SlimAccount)
- if err := rlp.DecodeBytes(blob, account); err != nil {
- panic(err)
- }
- return account, nil
+ // The diff layers retain the account in decoded form and the returned
+ // object is documented as safe to modify, so hand back a shallow copy to
+ // protect the cached instance from caller mutation.
+ cpy := *account
+ return &cpy, nil
}
// Storage directly retrieves the storage data associated with a particular hash,
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 27a6c1d422..017c56b0be 100644
--- a/triedb/pathdb/states.go
+++ b/triedb/pathdb/states.go
@@ -17,6 +17,7 @@
package pathdb
import (
+ "bytes"
"fmt"
"io"
"maps"
@@ -26,6 +27,7 @@ import (
"github.com/VictoriaMetrics/fastcache"
"github.com/ethereum/go-ethereum/common"
"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/log"
"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,
// state access and mutation won't happen at the same time with guarantee.
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)
size uint64 // Memory size of the state data (accountData and storageData)
@@ -75,17 +77,76 @@ type stateSet struct {
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.
+// 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 {
// Don't panic for the lazy callers, initialize the nil maps instead.
- if accounts == nil {
- accounts = make(map[common.Hash][]byte)
- }
if storages == nil {
storages = make(map[common.Hash]map[common.Hash][]byte)
}
s := &stateSet{
- accountData: accounts,
+ accountData: decodeAccounts(accounts),
storageData: storages,
rawStorageKey: rawStorageKey,
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.
-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 data, ok := s.accountData[hash]; ok {
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
// hash. The difference is this function will return an error if the account
// 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 data, ok := s.accountData[hash]; ok {
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.
func (s *stateSet) check() uint64 {
var size int
- for _, blob := range s.accountData {
- size += common.HashLength + len(blob)
+ for _, account := range s.accountData {
+ size += common.HashLength + slimAccountSize(account)
}
for accountHash, slots := range s.storageData {
if slots == nil {
@@ -235,11 +298,13 @@ func (s *stateSet) merge(other *stateSet) {
)
// Apply the updated account data
for accountHash, data := range other.accountData {
+ dataSize := slimAccountSize(data)
if origin, ok := s.accountData[accountHash]; ok {
- delta += len(data) - len(origin)
- accountOverwrites.add(common.HashLength + len(origin))
+ originSize := slimAccountSize(origin)
+ delta += dataSize - originSize
+ accountOverwrites.add(common.HashLength + originSize)
} else {
- delta += common.HashLength + len(data)
+ delta += common.HashLength + dataSize
}
s.accountData[accountHash] = data
}
@@ -293,11 +358,14 @@ func (s *stateSet) revertTo(accountOrigin map[common.Hash][]byte, storageOrigin
if !ok {
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))
}
- delta += len(blob) - len(data)
- s.accountData[addrHash] = blob
+ delta += slimAccountSize(origin) - slimAccountSize(data)
+ s.accountData[addrHash] = origin
}
// Overwrite the storage data with original value blindly
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)),
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.Accounts = append(enc.Accounts, blob)
+ enc.Accounts = append(enc.Accounts, encodeSlimAccount(account))
}
if err := rlp.Encode(w, enc); err != nil {
return err
@@ -395,7 +463,7 @@ func (s *stateSet) decode(r *rlp.Stream) error {
for i := range dec.AddrHashes {
accountSet[dec.AddrHashes[i]] = empty2nil(dec.Accounts[i])
}
- s.accountData = accountSet
+ s.accountData = decodeAccounts(accountSet)
// Decode storages
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
// may have been generated.
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.size = 0
s.accountListSorted = nil
diff --git a/triedb/pathdb/states_test.go b/triedb/pathdb/states_test.go
index 4d181cc914..3e67006e96 100644
--- a/triedb/pathdb/states_test.go
+++ b/triedb/pathdb/states_test.go
@@ -22,15 +22,39 @@ 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"
)
+// 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) {
a := newStates(
map[common.Hash][]byte{
- {0xa}: {0xa0},
- {0xb}: {0xb0},
- {0xc}: {0xc0},
+ {0xa}: testAccountRLP(0xa0),
+ {0xb}: testAccountRLP(0xb0),
+ {0xc}: testAccountRLP(0xc0),
},
map[common.Hash]map[common.Hash][]byte{
{0xa}: {
@@ -48,8 +72,8 @@ func TestStatesMerge(t *testing.T) {
)
b := newStates(
map[common.Hash][]byte{
- {0xa}: {0xa1},
- {0xb}: {0xb1},
+ {0xa}: testAccountRLP(0xa1),
+ {0xb}: testAccountRLP(0xb1),
{0xc}: nil, // delete account
},
map[common.Hash]map[common.Hash][]byte{
@@ -69,25 +93,25 @@ func TestStatesMerge(t *testing.T) {
)
a.merge(b)
- blob, exist := a.account(common.Hash{0xa})
- if !exist || !bytes.Equal(blob, []byte{0xa1}) {
+ account, exist := a.account(common.Hash{0xa})
+ if !exist || !accountEqualsTag(account, 0xa1) {
t.Error("Unexpected value for account a")
}
- blob, exist = a.account(common.Hash{0xb})
- if !exist || !bytes.Equal(blob, []byte{0xb1}) {
+ account, exist = a.account(common.Hash{0xb})
+ if !exist || !accountEqualsTag(account, 0xb1) {
t.Error("Unexpected value for account b")
}
- blob, exist = a.account(common.Hash{0xc})
- if !exist || len(blob) != 0 {
+ account, exist = a.account(common.Hash{0xc})
+ if !exist || account != nil {
t.Error("Unexpected value for account c")
}
// unknown account
- blob, exist = a.account(common.Hash{0xd})
- if exist || len(blob) != 0 {
+ account, exist = a.account(common.Hash{0xd})
+ if exist || account != nil {
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}) {
t.Error("Unexpected value for a's storage")
}
@@ -118,9 +142,9 @@ func TestStatesMerge(t *testing.T) {
func TestStatesRevert(t *testing.T) {
a := newStates(
map[common.Hash][]byte{
- {0xa}: {0xa0},
- {0xb}: {0xb0},
- {0xc}: {0xc0},
+ {0xa}: testAccountRLP(0xa0),
+ {0xb}: testAccountRLP(0xb0),
+ {0xc}: testAccountRLP(0xc0),
},
map[common.Hash]map[common.Hash][]byte{
{0xa}: {
@@ -138,8 +162,8 @@ func TestStatesRevert(t *testing.T) {
)
b := newStates(
map[common.Hash][]byte{
- {0xa}: {0xa1},
- {0xb}: {0xb1},
+ {0xa}: testAccountRLP(0xa1),
+ {0xb}: testAccountRLP(0xb1),
{0xc}: nil,
},
map[common.Hash]map[common.Hash][]byte{
@@ -160,9 +184,9 @@ func TestStatesRevert(t *testing.T) {
a.merge(b)
a.revertTo(
map[common.Hash][]byte{
- {0xa}: {0xa0},
- {0xb}: {0xb0},
- {0xc}: {0xc0},
+ {0xa}: testAccountRLP(0xa0),
+ {0xb}: testAccountRLP(0xb0),
+ {0xc}: testAccountRLP(0xc0),
},
map[common.Hash]map[common.Hash][]byte{
{0xa}: {
@@ -179,25 +203,25 @@ func TestStatesRevert(t *testing.T) {
},
)
- blob, exist := a.account(common.Hash{0xa})
- if !exist || !bytes.Equal(blob, []byte{0xa0}) {
+ account, exist := a.account(common.Hash{0xa})
+ if !exist || !accountEqualsTag(account, 0xa0) {
t.Error("Unexpected value for account a")
}
- blob, exist = a.account(common.Hash{0xb})
- if !exist || !bytes.Equal(blob, []byte{0xb0}) {
+ account, exist = a.account(common.Hash{0xb})
+ if !exist || !accountEqualsTag(account, 0xb0) {
t.Error("Unexpected value for account b")
}
- blob, exist = a.account(common.Hash{0xc})
- if !exist || !bytes.Equal(blob, []byte{0xc0}) {
+ account, exist = a.account(common.Hash{0xc})
+ if !exist || !accountEqualsTag(account, 0xc0) {
t.Error("Unexpected value for account c")
}
// unknown account
- blob, exist = a.account(common.Hash{0xd})
- if exist || len(blob) != 0 {
+ account, exist = a.account(common.Hash{0xd})
+ if exist || account != nil {
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}) {
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
b := newStates(
map[common.Hash][]byte{
- {0xa}: {0xa},
+ {0xa}: testAccountRLP(0xa),
},
nil,
false,
@@ -244,12 +268,12 @@ func TestStateRevertAccountNullMarker(t *testing.T) {
nil,
) // revert the transition b
- blob, exist := a.account(common.Hash{0xa})
+ account, exist := a.account(common.Hash{0xa})
if !exist {
t.Fatal("null marker is not found")
}
- if len(blob) != 0 {
- t.Fatalf("Unexpected value for account, %v", blob)
+ if account != nil {
+ t.Fatalf("Unexpected value for account, %v", account)
}
}
@@ -258,7 +282,7 @@ func TestStateRevertAccountNullMarker(t *testing.T) {
// entry in the set.
func TestStateRevertStorageNullMarker(t *testing.T) {
a := newStates(map[common.Hash][]byte{
- {0xa}: {0xa},
+ {0xa}: testAccountRLP(0xa),
}, nil, false) // initial state with account 0xa
b := newStates(
@@ -297,7 +321,7 @@ func TestStatesEncode(t *testing.T) {
func testStatesEncode(t *testing.T, rawStorageKey bool) {
s := newStates(
map[common.Hash][]byte{
- {0x1}: {0x1},
+ {0x1}: testAccountRLP(0x1),
},
map[common.Hash]map[common.Hash][]byte{
{0x1}: {
@@ -333,7 +357,7 @@ func TestStateWithOriginEncode(t *testing.T) {
func testStateWithOriginEncode(t *testing.T, rawStorageKey bool) {
s := NewStateSetWithOrigin(
map[common.Hash][]byte{
- {0x1}: {0x1},
+ {0x1}: testAccountRLP(0x1),
},
map[common.Hash]map[common.Hash][]byte{
{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) {
- 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*common.HashLength + 3 + /* storage data of 0xb */
2*common.HashLength + 1 /* storage data of 0xc */
a := newStates(
map[common.Hash][]byte{
- {0xa}: {0xa0}, // common.HashLength+1
- {0xb}: {0xb0}, // common.HashLength+1
- {0xc}: {0xc0}, // common.HashLength+1
+ {0xa}: testAccountRLP(0x1),
+ {0xb}: testAccountRLP(0x1),
+ {0xc}: testAccountRLP(0x1),
},
map[common.Hash]map[common.Hash][]byte{
{0xa}: {
@@ -405,15 +472,15 @@ func TestStateSizeTracking(t *testing.T) {
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 + 2 + 2*common.HashLength + 2 + /* storage data of 0xb */
3*2*common.HashLength /* storage data of 0xc */
b := newStates(
map[common.Hash][]byte{
- {0xa}: {0xa1, 0xa1}, // common.HashLength+2
- {0xb}: {0xb1, 0xb1, 0xb1}, // common.HashLength+3
- {0xc}: nil, // common.HashLength, account deletion
+ {0xa}: testAccountRLP(0x1), // account update (same size)
+ {0xb}: testAccountRLP(0x1), // account update (same size)
+ {0xc}: nil, // common.HashLength, account deletion
},
map[common.Hash]map[common.Hash][]byte{
{0xa}: {
@@ -438,10 +505,13 @@ func TestStateSizeTracking(t *testing.T) {
}
a.merge(b)
- mergeSize := expSizeA + 1 /* account a data change */ + 2 /* account b data change */ - 1 /* account c data change */
- 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 */
+ // Accounts a and b are overwritten with same-size accounts (no delta), but
+ // account c becomes a deletion marker, dropping its value contribution while
+ // retaining the key.
+ 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) {
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
a.revertTo(
map[common.Hash][]byte{
- {0xa}: {0xa0},
- {0xb}: {0xb0},
- {0xc}: {0xc0},
+ {0xa}: testAccountRLP(0x1),
+ {0xb}: testAccountRLP(0x1),
+ {0xc}: testAccountRLP(0x1),
},
map[common.Hash]map[common.Hash][]byte{
{0xa}: {