core/state: export StateUpdate struct

This commit is contained in:
Gary Rong 2026-04-01 14:02:41 +08:00
parent 5af5510b1e
commit 028ccebd23
13 changed files with 428 additions and 350 deletions

View file

@ -70,7 +70,7 @@ type Database interface {
// Commit flushes all pending writes and finalizes the state transition, // Commit flushes all pending writes and finalizes the state transition,
// committing the changes to the underlying storage. It returns an error // committing the changes to the underlying storage. It returns an error
// if the commit fails. // if the commit fails.
Commit(update *stateUpdate) error Commit(update *StateUpdate) error
} }
// Trie is a Ethereum Merkle Patricia trie. // Trie is a Ethereum Merkle Patricia trie.

View file

@ -297,7 +297,7 @@ func (db *HistoricDB) TrieDB() *triedb.Database {
// Commit flushes all pending writes and finalizes the state transition, // Commit flushes all pending writes and finalizes the state transition,
// committing the changes to the underlying storage. It returns an error // committing the changes to the underlying storage. It returns an error
// if the commit fails. // if the commit fails.
func (db *HistoricDB) Commit(update *stateUpdate) error { func (db *HistoricDB) Commit(update *StateUpdate) error {
return errors.New("not implemented") return errors.New("not implemented")
} }

View file

@ -23,6 +23,7 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/state/snapshot" "github.com/ethereum/go-ethereum/core/state/snapshot"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/triedb" "github.com/ethereum/go-ethereum/triedb"
) )
@ -57,9 +58,9 @@ type AccountIterator interface {
// An error will be returned if the preimage is not available. // An error will be returned if the preimage is not available.
Address() (common.Address, error) Address() (common.Address, error)
// Account returns the RLP encoded account the iterator is currently at. // Account returns the account the iterator is currently at.
// An error will be retained if the iterator becomes invalid. // An error will be retained if the iterator becomes invalid.
Account() []byte Account() *types.StateAccount
} }
// StorageIterator is an iterator to step over the specific storage in the // StorageIterator is an iterator to step over the specific storage in the
@ -73,7 +74,7 @@ type StorageIterator interface {
// Slot returns the storage slot the iterator is currently at. An error will // Slot returns the storage slot the iterator is currently at. An error will
// be retained if the iterator becomes invalid. // be retained if the iterator becomes invalid.
Slot() []byte Slot() common.Hash
} }
// Iteratee wraps the NewIterator methods for traversing the accounts and // Iteratee wraps the NewIterator methods for traversing the accounts and
@ -131,10 +132,7 @@ func (ai *flatAccountIterator) Next() bool {
// Error returns any failure that occurred during iteration, which might have // Error returns any failure that occurred during iteration, which might have
// caused a premature iteration exit. // caused a premature iteration exit.
func (ai *flatAccountIterator) Error() error { func (ai *flatAccountIterator) Error() error {
if ai.err != nil { return errors.Join(ai.err, ai.it.Error())
return ai.err
}
return ai.it.Error()
} }
// Hash returns the hash of the account or storage slot the iterator is // Hash returns the hash of the account or storage slot the iterator is
@ -165,8 +163,8 @@ func (ai *flatAccountIterator) Address() (common.Address, error) {
// Account returns the account data the iterator is currently at. The account // Account returns the account data the iterator is currently at. The account
// data is encoded as slim format from the underlying iterator, the conversion // data is encoded as slim format from the underlying iterator, the conversion
// is required. // is required.
func (ai *flatAccountIterator) Account() []byte { func (ai *flatAccountIterator) Account() *types.StateAccount {
data, err := types.FullAccountRLP(ai.it.Account()) data, err := types.FullAccount(ai.it.Account())
if err != nil { if err != nil {
ai.err = err ai.err = err
return nil return nil
@ -176,6 +174,7 @@ func (ai *flatAccountIterator) Account() []byte {
// flatStorageIterator is a wrapper around the underlying flat state iterator. // flatStorageIterator is a wrapper around the underlying flat state iterator.
type flatStorageIterator struct { type flatStorageIterator struct {
err error
it snapshot.StorageIterator it snapshot.StorageIterator
preimage PreimageReader preimage PreimageReader
} }
@ -196,7 +195,7 @@ func (si *flatStorageIterator) Next() bool {
// Error returns any failure that occurred during iteration, which might have // Error returns any failure that occurred during iteration, which might have
// caused a premature iteration exit. // caused a premature iteration exit.
func (si *flatStorageIterator) Error() error { func (si *flatStorageIterator) Error() error {
return si.it.Error() return errors.Join(si.err, si.it.Error())
} }
// Hash returns the hash of the account or storage slot the iterator is // Hash returns the hash of the account or storage slot the iterator is
@ -225,14 +224,24 @@ func (si *flatStorageIterator) Key() (common.Hash, error) {
} }
// Slot returns the storage slot data the iterator is currently at. // Slot returns the storage slot data the iterator is currently at.
func (si *flatStorageIterator) Slot() []byte { func (si *flatStorageIterator) Slot() common.Hash {
return si.it.Slot() // Perform the rlp-decode as the slot value is RLP-encoded
blob := si.it.Slot()
_, content, _, err := rlp.Split(blob)
if err != nil {
si.err = err
return common.Hash{}
}
var value common.Hash
value.SetBytes(content)
return value
} }
// merkleIterator implements the Iterator interface, providing functions to traverse // merkleIterator implements the Iterator interface, providing functions to traverse
// the accounts or storages with the manner of Merkle-Patricia-Trie. // the accounts or storages with the manner of Merkle-Patricia-Trie.
type merkleIterator struct { type merkleIterator struct {
tr Trie tr Trie
err error
it *trie.Iterator it *trie.Iterator
account bool account bool
} }
@ -260,7 +269,7 @@ func (ti *merkleIterator) Next() bool {
// Error returns any failure that occurred during iteration, which might have // Error returns any failure that occurred during iteration, which might have
// caused a premature iteration exit. // caused a premature iteration exit.
func (ti *merkleIterator) Error() error { func (ti *merkleIterator) Error() error {
return ti.it.Err return errors.Join(ti.err, ti.it.Err)
} }
// Hash returns the hash of the account or storage slot the iterator is // Hash returns the hash of the account or storage slot the iterator is
@ -287,11 +296,16 @@ func (ti *merkleIterator) Address() (common.Address, error) {
} }
// Account returns the account data the iterator is currently at. // Account returns the account data the iterator is currently at.
func (ti *merkleIterator) Account() []byte { func (ti *merkleIterator) Account() *types.StateAccount {
if !ti.account { if !ti.account {
return nil return nil
} }
return ti.it.Value var account types.StateAccount
if err := rlp.DecodeBytes(ti.it.Value, &account); err != nil {
ti.err = err
return nil
}
return &account
} }
// Key returns the raw storage slot key the iterator is currently at. // Key returns the raw storage slot key the iterator is currently at.
@ -308,11 +322,19 @@ func (ti *merkleIterator) Key() (common.Hash, error) {
} }
// Slot returns the storage slot the iterator is currently at. // Slot returns the storage slot the iterator is currently at.
func (ti *merkleIterator) Slot() []byte { func (ti *merkleIterator) Slot() common.Hash {
if ti.account { if ti.account {
return nil return common.Hash{}
} }
return ti.it.Value // Perform the rlp-decode as the slot value is RLP-encoded
_, content, _, err := rlp.Split(ti.it.Value)
if err != nil {
ti.err = err
return common.Hash{}
}
var value common.Hash
value.SetBytes(content)
return value
} }
// stateIteratee implements Iteratee interface, providing the state traversal // stateIteratee implements Iteratee interface, providing the state traversal
@ -430,6 +452,6 @@ func (e exhaustedIterator) Key() (common.Hash, error) {
return common.Hash{}, nil return common.Hash{}, nil
} }
func (e exhaustedIterator) Slot() []byte { func (e exhaustedIterator) Slot() common.Hash {
return nil return common.Hash{}
} }

View file

@ -24,7 +24,6 @@ import (
"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/core/types"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie"
) )
@ -45,7 +44,7 @@ func TestExhaustedIterator(t *testing.T) {
if key, err := it.Key(); key != (common.Hash{}) || err != nil { if key, err := it.Key(); key != (common.Hash{}) || err != nil {
t.Fatalf("Key() = %x, %v; want zero, nil", key, err) t.Fatalf("Key() = %x, %v; want zero, nil", key, err)
} }
if slot := it.Slot(); slot != nil { if slot := it.Slot(); slot != (common.Hash{}) {
t.Fatalf("Slot() = %x, want nil", slot) t.Fatalf("Slot() = %x, want nil", slot)
} }
it.Release() it.Release()
@ -95,20 +94,16 @@ func testAccountIterator(t *testing.T, scheme string) {
hashes = append(hashes, hash) hashes = append(hashes, hash)
// Decode and verify account data. // Decode and verify account data.
blob := acctIt.Account() got := acctIt.Account()
if blob == nil { if got == nil {
t.Fatalf("(%s) nil account at %x", scheme, hash) t.Fatalf("(%s) nil account at %x", scheme, hash)
} }
var decoded types.StateAccount
if err := rlp.DecodeBytes(blob, &decoded); err != nil {
t.Fatalf("(%s) bad RLP at %x: %v", scheme, hash, err)
}
acc := addrByHash[hash] acc := addrByHash[hash]
if decoded.Nonce != acc.nonce { if got.Nonce != acc.nonce {
t.Fatalf("(%s) nonce %x: got %d, want %d", scheme, hash, decoded.Nonce, acc.nonce) t.Fatalf("(%s) nonce %x: got %d, want %d", scheme, hash, got.Nonce, acc.nonce)
} }
if decoded.Balance.Cmp(acc.balance) != 0 { if got.Balance.Cmp(acc.balance) != 0 {
t.Fatalf("(%s) balance %x: got %v, want %v", scheme, hash, decoded.Balance, acc.balance) t.Fatalf("(%s) balance %x: got %v, want %v", scheme, hash, got.Balance, acc.balance)
} }
// Verify address preimage resolution. // Verify address preimage resolution.
addr, err := acctIt.Address() addr, err := acctIt.Address()
@ -183,7 +178,7 @@ func testStorageIterator(t *testing.T, scheme string) {
t.Fatalf("(%s) storage hashes not ascending for %x", scheme, acc.address) t.Fatalf("(%s) storage hashes not ascending for %x", scheme, acc.address)
} }
prevHash = hash prevHash = hash
if storageIt.Slot() == nil { if storageIt.Slot() == (common.Hash{}) {
t.Fatalf("(%s) nil slot at %x", scheme, hash) t.Fatalf("(%s) nil slot at %x", scheme, hash)
} }
// Check key preimage resolution on first slot. // Check key preimage resolution on first slot.

View file

@ -140,35 +140,44 @@ func (db *MPTDatabase) TrieDB() *triedb.Database {
// Commit flushes all pending writes and finalizes the state transition, // Commit flushes all pending writes and finalizes the state transition,
// committing the changes to the underlying storage. It returns an error // committing the changes to the underlying storage. It returns an error
// if the commit fails. // if the commit fails.
func (db *MPTDatabase) Commit(update *stateUpdate) error { func (db *MPTDatabase) Commit(update *StateUpdate) error {
// Short circuit if nothing to commit // Short circuit if nothing to commit
if update.empty() { if update.Empty() {
return nil return nil
} }
// Commit dirty contract code if any exists // Commit dirty contract code if any exists
if len(update.codes) > 0 { if len(update.Codes) > 0 {
batch := db.codedb.NewBatchWithSize(len(update.codes)) batch := db.codedb.NewBatchWithSize(len(update.Codes))
for _, code := range update.codes { for _, code := range update.Codes {
batch.Put(code.hash, code.blob) batch.Put(code.Hash, code.Blob)
} }
if err := batch.Commit(); err != nil { if err := batch.Commit(); err != nil {
return err return err
} }
} }
// Encode the state mutations in the MPT format
accounts, accountOrigin, storages, storageOrigin := update.EncodeMPTState()
// If snapshotting is enabled, update the snapshot tree with this new version // If snapshotting is enabled, update the snapshot tree with this new version
if db.snap != nil && db.snap.Snapshot(update.originRoot) != nil { if db.snap != nil && db.snap.Snapshot(update.OriginRoot) != nil {
if err := db.snap.Update(update.root, update.originRoot, update.accounts, update.storages); err != nil { if err := db.snap.Update(update.Root, update.OriginRoot, accounts, storages); err != nil {
log.Warn("Failed to update snapshot tree", "from", update.originRoot, "to", update.root, "err", err) log.Warn("Failed to update snapshot tree", "from", update.OriginRoot, "to", update.Root, "err", err)
} }
// Keep 128 diff layers in the memory, persistent layer is 129th. // Keep 128 diff layers in the memory, persistent layer is 129th.
// - head layer is paired with HEAD state // - head layer is paired with HEAD state
// - head-1 layer is paired with HEAD-1 state // - head-1 layer is paired with HEAD-1 state
// - head-127 layer(bottom-most diff layer) is paired with HEAD-127 state // - head-127 layer(bottom-most diff layer) is paired with HEAD-127 state
if err := db.snap.Cap(update.root, TriesInMemory); err != nil { if err := db.snap.Cap(update.Root, TriesInMemory); err != nil {
log.Warn("Failed to cap snapshot tree", "root", update.root, "layers", TriesInMemory, "err", err) log.Warn("Failed to cap snapshot tree", "root", update.Root, "layers", TriesInMemory, "err", err)
} }
} }
return db.triedb.Update(update.root, update.originRoot, update.blockNumber, update.nodes, update.stateSet()) return db.triedb.Update(update.Root, update.OriginRoot, update.BlockNumber, update.Nodes, &triedb.StateSet{
Accounts: accounts,
AccountsOrigin: accountOrigin,
Storages: storages,
StoragesOrigin: storageOrigin,
RawStorageKey: update.StorageKeyType == StorageKeyPlain,
})
} }
// Iteratee returns a state iteratee associated with the specified state root, // Iteratee returns a state iteratee associated with the specified state root,

View file

@ -113,22 +113,31 @@ func (db *UBTDatabase) TrieDB() *triedb.Database {
// Commit flushes all pending writes and finalizes the state transition, // Commit flushes all pending writes and finalizes the state transition,
// committing the changes to the underlying storage. It returns an error // committing the changes to the underlying storage. It returns an error
// if the commit fails. // if the commit fails.
func (db *UBTDatabase) Commit(update *stateUpdate) error { func (db *UBTDatabase) Commit(update *StateUpdate) error {
// Short circuit if nothing to commit // Short circuit if nothing to commit
if update.empty() { if update.Empty() {
return nil return nil
} }
// Commit dirty contract code if any exists // Commit dirty contract code if any exists
if len(update.codes) > 0 { if len(update.Codes) > 0 {
batch := db.codedb.NewBatchWithSize(len(update.codes)) batch := db.codedb.NewBatchWithSize(len(update.Codes))
for _, code := range update.codes { for _, code := range update.Codes {
batch.Put(code.hash, code.blob) batch.Put(code.Hash, code.Blob)
} }
if err := batch.Commit(); err != nil { if err := batch.Commit(); err != nil {
return err return err
} }
} }
return db.triedb.Update(update.root, update.originRoot, update.blockNumber, update.nodes, update.stateSet()) // Encode the state mutations in the UBT format
accounts, accountOrigin, storages, storageOrigin := update.EncodeUBTState()
return db.triedb.Update(update.Root, update.OriginRoot, update.BlockNumber, update.Nodes, &triedb.StateSet{
Accounts: accounts,
AccountsOrigin: accountOrigin,
Storages: storages,
StoragesOrigin: storageOrigin,
RawStorageKey: update.StorageKeyType == StorageKeyPlain,
})
} }
// Iteratee returns a state iteratee associated with the specified state root, // Iteratee returns a state iteratee associated with the specified state root,

View file

@ -24,9 +24,7 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie/bintrie" "github.com/ethereum/go-ethereum/trie/bintrie"
) )
@ -144,11 +142,8 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
defer acctIt.Release() defer acctIt.Release()
for acctIt.Next() { for acctIt.Next() {
var data types.StateAccount
if err := rlp.DecodeBytes(acctIt.Account(), &data); err != nil {
panic(err)
}
var ( var (
data = acctIt.Account()
account = DumpAccount{ account = DumpAccount{
Balance: data.Balance.String(), Balance: data.Balance.String(),
Nonce: data.Nonce, Nonce: data.Nonce,
@ -168,7 +163,7 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
address = &addrBytes address = &addrBytes
account.Address = address account.Address = address
} }
obj := newObject(s, addrBytes, &data) obj := newObject(s, addrBytes, data)
if !conf.SkipCode { if !conf.SkipCode {
account.Code = obj.Code() account.Code = obj.Code()
} }
@ -181,16 +176,11 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
continue continue
} }
for storageIt.Next() { for storageIt.Next() {
_, content, _, err := rlp.Split(storageIt.Slot())
if err != nil {
log.Error("Failed to decode the value returned by iterator", "error", err)
continue
}
key, err := storageIt.Key() key, err := storageIt.Key()
if err != nil { if err != nil {
continue continue
} }
account.Storage[key] = common.Bytes2Hex(content) account.Storage[key] = storageIt.Slot().String()
} }
storageIt.Release() storageIt.Release()
} }

View file

@ -27,7 +27,6 @@ import (
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/trie/bintrie" "github.com/ethereum/go-ethereum/trie/bintrie"
"github.com/ethereum/go-ethereum/trie/transitiontrie" "github.com/ethereum/go-ethereum/trie/transitiontrie"
@ -398,17 +397,8 @@ func (s *stateObject) updateRoot() {
} }
// commitStorage overwrites the clean storage with the storage changes and // commitStorage overwrites the clean storage with the storage changes and
// fulfills the storage diffs into the given accountUpdate struct. // fulfills the storage diffs into the given AccountUpdate struct.
func (s *stateObject) commitStorage(op *accountUpdate) { func (s *stateObject) commitStorage(op *AccountUpdate) {
var (
encode = func(val common.Hash) []byte {
if val == (common.Hash{}) {
return nil
}
blob, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(val[:]))
return blob
}
)
for key, val := range s.pendingStorage { for key, val := range s.pendingStorage {
// Skip the noop storage changes, it might be possible the value // Skip the noop storage changes, it might be possible the value
// of tracked slot is same in originStorage and pendingStorage // of tracked slot is same in originStorage and pendingStorage
@ -418,20 +408,20 @@ func (s *stateObject) commitStorage(op *accountUpdate) {
continue continue
} }
hash := crypto.Keccak256Hash(key[:]) hash := crypto.Keccak256Hash(key[:])
if op.storages == nil { if op.Storages == nil {
op.storages = make(map[common.Hash][]byte) op.Storages = make(map[common.Hash]common.Hash)
} }
op.storages[hash] = encode(val) op.Storages[hash] = val
if op.storagesOriginByKey == nil { if op.StoragesOriginByKey == nil {
op.storagesOriginByKey = make(map[common.Hash][]byte) op.StoragesOriginByKey = make(map[common.Hash]common.Hash)
} }
if op.storagesOriginByHash == nil { if op.StoragesOriginByHash == nil {
op.storagesOriginByHash = make(map[common.Hash][]byte) op.StoragesOriginByHash = make(map[common.Hash]common.Hash)
} }
origin := encode(s.originStorage[key]) origin := s.originStorage[key]
op.storagesOriginByKey[key] = origin op.StoragesOriginByKey[key] = origin
op.storagesOriginByHash[hash] = origin op.StoragesOriginByHash[hash] = origin
// Overwrite the clean value of storage slots // Overwrite the clean value of storage slots
s.originStorage[key] = val s.originStorage[key] = val
@ -444,32 +434,32 @@ func (s *stateObject) commitStorage(op *accountUpdate) {
// //
// Note, commit may run concurrently across all the state objects. Do not assume // Note, commit may run concurrently across all the state objects. Do not assume
// thread-safe access to the statedb. // thread-safe access to the statedb.
func (s *stateObject) commit() (*accountUpdate, *trienode.NodeSet, error) { func (s *stateObject) commit() (*AccountUpdate, *trienode.NodeSet, error) {
// commit the account metadata changes // commit the account metadata changes, the data must be deep-copied
op := &accountUpdate{ // to prevent accidental mutations later on (in practice the stateDB
address: s.address, // won't be modified after commit). The origin is safe to use directly.
data: types.SlimAccountRLP(s.data), op := &AccountUpdate{
} Address: s.address,
if s.origin != nil { Data: s.data.Copy(),
op.origin = types.SlimAccountRLP(*s.origin) Origin: s.origin,
} }
// commit the contract code if it's modified // commit the contract code if it's modified
if s.dirtyCode { if s.dirtyCode {
op.code = &contractCode{ op.Code = &ContractCode{
hash: common.BytesToHash(s.CodeHash()), Hash: common.BytesToHash(s.CodeHash()),
blob: s.code, Blob: s.code,
} }
s.dirtyCode = false // reset the dirty flag s.dirtyCode = false // reset the dirty flag
if s.origin == nil { if s.origin == nil {
op.code.originHash = types.EmptyCodeHash op.Code.OriginHash = types.EmptyCodeHash
} else { } else {
op.code.originHash = common.BytesToHash(s.origin.CodeHash) op.Code.OriginHash = common.BytesToHash(s.origin.CodeHash)
} }
} }
// Commit storage changes and the associated storage trie // Commit storage changes and the associated storage trie
s.commitStorage(op) s.commitStorage(op)
if len(op.storages) == 0 { if len(op.Storages) == 0 {
// nothing changed, don't bother to commit the trie // nothing changed, don't bother to commit the trie
s.origin = s.data.Copy() s.origin = s.data.Copy()
return op, nil, nil return op, nil, nil

View file

@ -125,16 +125,17 @@ func (s SizeStats) add(diff SizeStats) SizeStats {
} }
// calSizeStats measures the state size changes of the provided state update. // calSizeStats measures the state size changes of the provided state update.
func calSizeStats(update *stateUpdate) (SizeStats, error) { func calSizeStats(update *StateUpdate) (SizeStats, error) {
stats := SizeStats{ stats := SizeStats{
BlockNumber: update.blockNumber, BlockNumber: update.BlockNumber,
StateRoot: update.root, StateRoot: update.Root,
} }
accounts, accountOrigin, storages, storageOrigin := update.EncodeMPTState()
// Measure the account changes // Measure the account changes
for addr, oldValue := range update.accountsOrigin { for addr, oldValue := range accountOrigin {
addrHash := crypto.Keccak256Hash(addr.Bytes()) addrHash := crypto.Keccak256Hash(addr.Bytes())
newValue, exists := update.accounts[addrHash] newValue, exists := accounts[addrHash]
if !exists { if !exists {
return SizeStats{}, fmt.Errorf("account %x not found", addr) return SizeStats{}, fmt.Errorf("account %x not found", addr)
} }
@ -156,9 +157,9 @@ func calSizeStats(update *stateUpdate) (SizeStats, error) {
} }
// Measure storage changes // Measure storage changes
for addr, slots := range update.storagesOrigin { for addr, slots := range storageOrigin {
addrHash := crypto.Keccak256Hash(addr.Bytes()) addrHash := crypto.Keccak256Hash(addr.Bytes())
subset, exists := update.storages[addrHash] subset, exists := storages[addrHash]
if !exists { if !exists {
return SizeStats{}, fmt.Errorf("storage %x not found", addr) return SizeStats{}, fmt.Errorf("storage %x not found", addr)
} }
@ -167,7 +168,7 @@ func calSizeStats(update *stateUpdate) (SizeStats, error) {
exists bool exists bool
newValue []byte newValue []byte
) )
if update.rawStorageKey { if update.StorageKeyType == StorageKeyPlain {
newValue, exists = subset[crypto.Keccak256Hash(key.Bytes())] newValue, exists = subset[crypto.Keccak256Hash(key.Bytes())]
} else { } else {
newValue, exists = subset[key] newValue, exists = subset[key]
@ -194,7 +195,7 @@ func calSizeStats(update *stateUpdate) (SizeStats, error) {
} }
// Measure trienode changes // Measure trienode changes
for owner, subset := range update.nodes.Sets { for owner, subset := range update.Nodes.Sets {
var ( var (
keyPrefix int64 keyPrefix int64
isAccount = owner == (common.Hash{}) isAccount = owner == (common.Hash{})
@ -244,13 +245,13 @@ func calSizeStats(update *stateUpdate) (SizeStats, error) {
} }
codeExists := make(map[common.Hash]struct{}) codeExists := make(map[common.Hash]struct{})
for _, code := range update.codes { for _, code := range update.Codes {
if _, ok := codeExists[code.hash]; ok || code.duplicate { if _, ok := codeExists[code.Hash]; ok || code.Duplicate {
continue continue
} }
stats.ContractCodes += 1 stats.ContractCodes += 1
stats.ContractCodeBytes += codeKeySize + int64(len(code.blob)) stats.ContractCodeBytes += codeKeySize + int64(len(code.Blob))
codeExists[code.hash] = struct{}{} codeExists[code.Hash] = struct{}{}
} }
return stats, nil return stats, nil
} }
@ -267,7 +268,7 @@ type SizeTracker struct {
triedb *triedb.Database triedb *triedb.Database
abort chan struct{} abort chan struct{}
aborted chan struct{} aborted chan struct{}
updateCh chan *stateUpdate updateCh chan *StateUpdate
queryCh chan *stateSizeQuery queryCh chan *stateSizeQuery
} }
@ -281,7 +282,7 @@ func NewSizeTracker(db ethdb.KeyValueStore, triedb *triedb.Database) (*SizeTrack
triedb: triedb, triedb: triedb,
abort: make(chan struct{}), abort: make(chan struct{}),
aborted: make(chan struct{}), aborted: make(chan struct{}),
updateCh: make(chan *stateUpdate), updateCh: make(chan *StateUpdate),
queryCh: make(chan *stateSizeQuery), queryCh: make(chan *stateSizeQuery),
} }
go t.run() go t.run()
@ -328,9 +329,9 @@ func (t *SizeTracker) run() {
for { for {
select { select {
case u := <-t.updateCh: case u := <-t.updateCh:
base, found := stats[u.originRoot] base, found := stats[u.OriginRoot]
if !found { if !found {
log.Debug("Ignored the state size without parent", "parent", u.originRoot, "root", u.root, "number", u.blockNumber) log.Debug("Ignored the state size without parent", "parent", u.OriginRoot, "root", u.Root, "number", u.BlockNumber)
continue continue
} }
diff, err := calSizeStats(u) diff, err := calSizeStats(u)
@ -338,15 +339,15 @@ func (t *SizeTracker) run() {
continue continue
} }
stat := base.add(diff) stat := base.add(diff)
stats[u.root] = stat stats[u.Root] = stat
last = u.root last = u.Root
// Publish statistics to metric system // Publish statistics to metric system
stat.publish() stat.publish()
// Evict the stale statistics // Evict the stale statistics
heap.Push(&h, stats[u.root]) heap.Push(&h, stats[u.Root])
for len(h) > 0 && u.blockNumber-h[0].BlockNumber > statEvictThreshold { for len(h) > 0 && u.BlockNumber-h[0].BlockNumber > statEvictThreshold {
delete(stats, h[0].StateRoot) delete(stats, h[0].StateRoot)
heap.Pop(&h) heap.Pop(&h)
} }
@ -402,7 +403,7 @@ wait:
} }
var ( var (
updates = make(map[common.Hash]*stateUpdate) updates = make(map[common.Hash]*StateUpdate)
children = make(map[common.Hash][]common.Hash) children = make(map[common.Hash][]common.Hash)
done chan buildResult done chan buildResult
) )
@ -410,9 +411,9 @@ wait:
for { for {
select { select {
case u := <-t.updateCh: case u := <-t.updateCh:
updates[u.root] = u updates[u.Root] = u
children[u.originRoot] = append(children[u.originRoot], u.root) children[u.OriginRoot] = append(children[u.OriginRoot], u.Root)
log.Debug("Received state update", "root", u.root, "blockNumber", u.blockNumber) log.Debug("Received state update", "root", u.Root, "blockNumber", u.BlockNumber)
case r := <-t.queryCh: case r := <-t.queryCh:
r.err = errors.New("state size is not initialized yet") r.err = errors.New("state size is not initialized yet")
@ -432,8 +433,8 @@ wait:
continue continue
} }
done = make(chan buildResult) done = make(chan buildResult)
go t.build(entry.root, entry.blockNumber, done) go t.build(entry.Root, entry.BlockNumber, done)
log.Info("Measuring persistent state size", "root", root.Hex(), "number", entry.blockNumber) log.Info("Measuring persistent state size", "root", root.Hex(), "number", entry.BlockNumber)
case result := <-done: case result := <-done:
if result.err != nil { if result.err != nil {
@ -646,8 +647,8 @@ func (t *SizeTracker) iterateTableParallel(closed chan struct{}, prefix []byte,
// Notify is an async method used to send the state update to the size tracker. // Notify is an async method used to send the state update to the size tracker.
// It ignores empty updates (where no state changes occurred). // It ignores empty updates (where no state changes occurred).
// If the channel is full, it drops the update to avoid blocking. // If the channel is full, it drops the update to avoid blocking.
func (t *SizeTracker) Notify(update *stateUpdate) { func (t *SizeTracker) Notify(update *StateUpdate) {
if update == nil || update.empty() { if update == nil || update.Empty() {
return return
} }
select { select {

View file

@ -160,7 +160,7 @@ func TestSizeTracker(t *testing.T) {
} }
tracker.Notify(ret) tracker.Notify(ret)
if err := tdb.Commit(ret.root, false); err != nil { if err := tdb.Commit(ret.Root, false); err != nil {
t.Fatalf("Failed to commit trie at block %d: %v", blockNum, err) t.Fatalf("Failed to commit trie at block %d: %v", blockNum, err)
} }
@ -169,7 +169,7 @@ func TestSizeTracker(t *testing.T) {
t.Fatalf("Failed to calculate size stats for block %d: %v", blockNum, err) t.Fatalf("Failed to calculate size stats for block %d: %v", blockNum, err)
} }
trackedUpdates = append(trackedUpdates, diff) trackedUpdates = append(trackedUpdates, diff)
currentRoot = ret.root currentRoot = ret.Root
} }
finalRoot := rawdb.ReadSnapshotRoot(db) finalRoot := rawdb.ReadSnapshotRoot(db)

View file

@ -1045,11 +1045,11 @@ func (s *StateDB) clearJournalAndRefund() {
} }
// deleteStorage is designed to delete the storage trie of a designated account. // deleteStorage is designed to delete the storage trie of a designated account.
func (s *StateDB) deleteStorage(addrHash common.Hash, root common.Hash) (map[common.Hash][]byte, map[common.Hash][]byte, *trienode.NodeSet, error) { func (s *StateDB) deleteStorage(addrHash common.Hash, root common.Hash) (map[common.Hash]common.Hash, map[common.Hash]common.Hash, *trienode.NodeSet, error) {
var ( var (
nodes = trienode.NewNodeSet(addrHash) // the set for trie node mutations (value is nil) nodes = trienode.NewNodeSet(addrHash) // the set for trie node mutations (value is nil)
storages = make(map[common.Hash][]byte) // the set for storage mutations (value is nil) storages = make(map[common.Hash]common.Hash) // the set for storage mutations (value is nil)
storageOrigins = make(map[common.Hash][]byte) // the set for tracking the original value of slot storageOrigins = make(map[common.Hash]common.Hash) // the set for tracking the original value of slot
) )
iteratee, err := s.db.Iteratee(s.originalRoot) iteratee, err := s.db.Iteratee(s.originalRoot)
if err != nil { if err != nil {
@ -1065,19 +1065,24 @@ func (s *StateDB) deleteStorage(addrHash common.Hash, root common.Hash) (map[com
nodes.AddNode(path, trienode.NewDeletedWithPrev(blob)) nodes.AddNode(path, trienode.NewDeletedWithPrev(blob))
}) })
for it.Next() { for it.Next() {
slot := common.CopyBytes(it.Slot()) slot := it.Slot()
if err := it.Error(); err != nil { // error might occur after Slot function // Error might occur after Slot function
if err := it.Error(); err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
if slot == (common.Hash{}) {
return nil, nil, nil, fmt.Errorf("unexpected empty storage slot, addr: %x, slot: %x", addrHash, it.Hash())
}
key := it.Hash() key := it.Hash()
storages[key] = nil storages[key] = common.Hash{}
storageOrigins[key] = slot storageOrigins[key] = slot
if err := stack.Update(key.Bytes(), slot); err != nil { if err := stack.Update(key.Bytes(), encodeSlot(slot)); err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
} }
if err := it.Error(); err != nil { // error might occur during iteration // Error might occur during iteration
if err := it.Error(); err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
if stack.Hash() != root { if stack.Hash() != root {
@ -1104,10 +1109,10 @@ func (s *StateDB) deleteStorage(addrHash common.Hash, root common.Hash) (map[com
// with their values be tracked as original value. // with their values be tracked as original value.
// In case (d), **original** account along with its storages should be deleted, // In case (d), **original** account along with its storages should be deleted,
// with their values be tracked as original value. // with their values be tracked as original value.
func (s *StateDB) handleDestruction(noStorageWiping bool) (map[common.Hash]*accountDelete, []*trienode.NodeSet, error) { func (s *StateDB) handleDestruction(noStorageWiping bool) (map[common.Hash]*AccountDelete, []*trienode.NodeSet, error) {
var ( var (
nodes []*trienode.NodeSet nodes []*trienode.NodeSet
deletes = make(map[common.Hash]*accountDelete) deletes = make(map[common.Hash]*AccountDelete)
) )
for addr, prevObj := range s.stateObjectsDestruct { for addr, prevObj := range s.stateObjectsDestruct {
prev := prevObj.origin prev := prevObj.origin
@ -1122,9 +1127,9 @@ func (s *StateDB) handleDestruction(noStorageWiping bool) (map[common.Hash]*acco
} }
// The account was existent, it can be either case (c) or (d). // The account was existent, it can be either case (c) or (d).
addrHash := crypto.Keccak256Hash(addr.Bytes()) addrHash := crypto.Keccak256Hash(addr.Bytes())
op := &accountDelete{ op := &AccountDelete{
address: addr, Address: addr,
origin: types.SlimAccountRLP(*prev), Origin: prev,
} }
deletes[addrHash] = op deletes[addrHash] = op
@ -1140,8 +1145,8 @@ func (s *StateDB) handleDestruction(noStorageWiping bool) (map[common.Hash]*acco
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("failed to delete storage, err: %w", err) return nil, nil, fmt.Errorf("failed to delete storage, err: %w", err)
} }
op.storages = storages op.Storages = storages
op.storagesOrigin = storagesOrigin op.StoragesOrigin = storagesOrigin
// Aggregate the associated trie node changes. // Aggregate the associated trie node changes.
nodes = append(nodes, set) nodes = append(nodes, set)
@ -1156,7 +1161,7 @@ func (s *StateDB) GetTrie() Trie {
// commit gathers the state mutations accumulated along with the associated // commit gathers the state mutations accumulated along with the associated
// trie changes, resetting all internal flags with the new state as the base. // trie changes, resetting all internal flags with the new state as the base.
func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNumber uint64) (*stateUpdate, error) { func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNumber uint64) (*StateUpdate, error) {
// Short circuit in case any database failure occurred earlier. // Short circuit in case any database failure occurred earlier.
if s.dbErr != nil { if s.dbErr != nil {
return nil, fmt.Errorf("commit aborted due to earlier error: %v", s.dbErr) return nil, fmt.Errorf("commit aborted due to earlier error: %v", s.dbErr)
@ -1177,7 +1182,7 @@ func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNum
lock sync.Mutex // protect two maps below lock sync.Mutex // protect two maps below
nodes = trienode.NewMergedNodeSet() // aggregated trie nodes nodes = trienode.NewMergedNodeSet() // aggregated trie nodes
updates = make(map[common.Hash]*accountUpdate, len(s.mutations)) // aggregated account updates updates = make(map[common.Hash]*AccountUpdate, len(s.mutations)) // aggregated account updates
// merge aggregates the dirty trie nodes into the global set. // merge aggregates the dirty trie nodes into the global set.
// //
@ -1305,12 +1310,16 @@ func (s *StateDB) commit(deleteEmptyObjects bool, noStorageWiping bool, blockNum
origin := s.originalRoot origin := s.originalRoot
s.originalRoot = root s.originalRoot = root
return newStateUpdate(noStorageWiping, origin, root, blockNumber, deletes, updates, nodes), nil typ := StorageKeyHashed
if noStorageWiping {
typ = StorageKeyPlain
}
return NewStateUpdate(typ, origin, root, blockNumber, deletes, updates, nodes), nil
} }
// commitAndFlush is a wrapper of commit which also commits the state mutations // commitAndFlush is a wrapper of commit which also commits the state mutations
// to the configured data stores. // to the configured data stores.
func (s *StateDB) commitAndFlush(block uint64, deleteEmptyObjects bool, noStorageWiping bool, deriveCodeFields bool) (*stateUpdate, error) { func (s *StateDB) commitAndFlush(block uint64, deleteEmptyObjects bool, noStorageWiping bool, deriveCodeFields bool) (*StateUpdate, error) {
ret, err := s.commit(deleteEmptyObjects, noStorageWiping, block) ret, err := s.commit(deleteEmptyObjects, noStorageWiping, block)
if err != nil { if err != nil {
return nil, err return nil, err
@ -1351,17 +1360,17 @@ func (s *StateDB) Commit(block uint64, deleteEmptyObjects bool, noStorageWiping
if err != nil { if err != nil {
return common.Hash{}, err return common.Hash{}, err
} }
return ret.root, nil return ret.Root, nil
} }
// CommitWithUpdate writes the state mutations and returns the state update for // CommitWithUpdate writes the state mutations and returns the state update for
// external processing (e.g., live tracing hooks or size tracker). // external processing (e.g., live tracing hooks or size tracker).
func (s *StateDB) CommitWithUpdate(block uint64, deleteEmptyObjects bool, noStorageWiping bool) (common.Hash, *stateUpdate, error) { func (s *StateDB) CommitWithUpdate(block uint64, deleteEmptyObjects bool, noStorageWiping bool) (common.Hash, *StateUpdate, error) {
ret, err := s.commitAndFlush(block, deleteEmptyObjects, noStorageWiping, true) ret, err := s.commitAndFlush(block, deleteEmptyObjects, noStorageWiping, true)
if err != nil { if err != nil {
return common.Hash{}, nil, err return common.Hash{}, nil, err
} }
return ret.root, ret, nil return ret.Root, ret, nil
} }
// Prepare handles the preparatory steps for executing a state transition with. // Prepare handles the preparatory steps for executing a state transition with.

View file

@ -182,11 +182,12 @@ func (test *stateTest) run() bool {
accountOrigin []map[common.Address][]byte accountOrigin []map[common.Address][]byte
storages []map[common.Hash]map[common.Hash][]byte storages []map[common.Hash]map[common.Hash][]byte
storageOrigin []map[common.Address]map[common.Hash][]byte storageOrigin []map[common.Address]map[common.Hash][]byte
copyUpdate = func(update *stateUpdate) { copyUpdate = func(update *StateUpdate) {
accounts = append(accounts, maps.Clone(update.accounts)) accts, acctOrigin, slots, slotOrigin := update.EncodeMPTState()
accountOrigin = append(accountOrigin, maps.Clone(update.accountsOrigin)) accounts = append(accounts, maps.Clone(accts))
storages = append(storages, maps.Clone(update.storages)) accountOrigin = append(accountOrigin, maps.Clone(acctOrigin))
storageOrigin = append(storageOrigin, maps.Clone(update.storagesOrigin)) storages = append(storages, maps.Clone(slots))
storageOrigin = append(storageOrigin, maps.Clone(slotOrigin))
} }
disk = rawdb.NewMemoryDatabase() disk = rawdb.NewMemoryDatabase()
tdb = triedb.NewDatabase(disk, &triedb.Config{PathDB: pathdb.Defaults}) tdb = triedb.NewDatabase(disk, &triedb.Config{PathDB: pathdb.Defaults})
@ -232,11 +233,11 @@ func (test *stateTest) run() bool {
if err != nil { if err != nil {
panic(err) panic(err)
} }
if ret.empty() { if ret.Empty() {
return true return true
} }
copyUpdate(ret) copyUpdate(ret)
roots = append(roots, ret.root) roots = append(roots, ret.Root)
} }
for i := 0; i < len(test.actions); i++ { for i := 0; i < len(test.actions); i++ {
root := types.EmptyRootHash root := types.EmptyRootHash

View file

@ -26,139 +26,148 @@ import (
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie/trienode" "github.com/ethereum/go-ethereum/trie/trienode"
"github.com/ethereum/go-ethereum/triedb"
) )
// contractCode represents contract bytecode along with its associated metadata. // ContractCode represents contract bytecode mutation along with its
type contractCode struct { // associated metadata.
hash common.Hash // hash is the cryptographic hash of the current contract code. type ContractCode struct {
blob []byte // blob is the binary representation of the current contract code. Hash common.Hash // Hash is the cryptographic hash of the current contract code.
originHash common.Hash // originHash is the cryptographic hash of the code before mutation. Blob []byte // Blob is the binary representation of the current contract code.
OriginHash common.Hash // OriginHash is the cryptographic hash of the code before mutation.
// Derived fields, populated only when state tracking is enabled. // Derived fields, populated only when state tracking is enabled.
duplicate bool // duplicate indicates whether the updated code already exists. Duplicate bool // Duplicate indicates whether the updated code already exists.
originBlob []byte // originBlob is the original binary representation of the contract code. OriginBlob []byte // OriginBlob is the original binary representation of the contract code.
} }
// accountDelete represents an operation for deleting an Ethereum account. // AccountDelete represents a deletion operation for an Ethereum account.
type accountDelete struct { type AccountDelete struct {
address common.Address // address is the unique account identifier Address common.Address // Address uniquely identifies the account.
origin []byte // origin is the original value of account data in slim-RLP encoding. Origin *types.StateAccount // Origin is the account state prior to deletion (never be null).
Storages map[common.Hash]common.Hash // Storages contains mutated storage slots.
// storages stores mutated slots, the value should be nil. StoragesOrigin map[common.Hash]common.Hash // StoragesOrigin holds original values of mutated slots; keys are hashes of raw storage slot keys.
storages map[common.Hash][]byte
// storagesOrigin stores the original values of mutated slots in
// prefix-zero-trimmed RLP format. The map key refers to the **HASH**
// of the raw storage slot key.
storagesOrigin map[common.Hash][]byte
} }
// accountUpdate represents an operation for updating an Ethereum account. // AccountUpdate represents an update operation for an Ethereum account.
type accountUpdate struct { type AccountUpdate struct {
address common.Address // address is the unique account identifier Address common.Address // Address uniquely identifies the account.
data []byte // data is the slim-RLP encoded account data. Data *types.StateAccount // Data is the updated account state; nil indicates deletion.
origin []byte // origin is the original value of account data in slim-RLP encoding. Origin *types.StateAccount // Origin is the previous account state; nil indicates non-existence.
code *contractCode // code represents mutated contract code; nil means it's not modified. Code *ContractCode // Code contains updated contract code; nil if unchanged.
storages map[common.Hash][]byte // storages stores mutated slots in prefix-zero-trimmed RLP format. Storages map[common.Hash]common.Hash // Storages contains updated storage slots.
// storagesOriginByKey and storagesOriginByHash both store the original values // StoragesOriginByKey and StoragesOriginByHash both record original values
// of mutated slots in prefix-zero-trimmed RLP format. The difference is that // of mutated storage slots:
// storagesOriginByKey uses the **raw** storage slot key as the map ID, while // - StoragesOriginByKey uses raw storage slot keys.
// storagesOriginByHash uses the **hash** of the storage slot key instead. // - StoragesOriginByHash uses hashed storage slot keys.
storagesOriginByKey map[common.Hash][]byte StoragesOriginByKey map[common.Hash]common.Hash
storagesOriginByHash map[common.Hash][]byte StoragesOriginByHash map[common.Hash]common.Hash
} }
// stateUpdate represents the difference between two states resulting from state // StorageKeyEncoding specifies the encoding scheme of a storage key.
type StorageKeyEncoding int
const (
// StorageKeyHashed represents a hashed key (e.g. Keccak256).
StorageKeyHashed StorageKeyEncoding = iota
// StorageKeyPlain represents a raw (unhashed) key.
StorageKeyPlain
)
// StateUpdate represents the difference between two states resulting from state
// execution. It contains information about mutated contract codes, accounts, // execution. It contains information about mutated contract codes, accounts,
// and storage slots, along with their original values. // and storage slots, along with their original values.
type stateUpdate struct { type StateUpdate struct {
originRoot common.Hash // hash of the state before applying mutation OriginRoot common.Hash // Hash of the state before applying mutation
root common.Hash // hash of the state after applying mutation Root common.Hash // Hash of the state after applying mutation
blockNumber uint64 // Associated block number BlockNumber uint64 // Associated block number
accounts map[common.Hash][]byte // accounts stores mutated accounts in 'slim RLP' encoding // Accounts contains mutated accounts, keyed by address hash.
accountsOrigin map[common.Address][]byte // accountsOrigin stores the original values of mutated accounts in 'slim RLP' encoding Accounts map[common.Hash]*types.StateAccount
// storages stores mutated slots in 'prefix-zero-trimmed' RLP format. // Storages contains mutated storage slots, keyed by address
// The value is keyed by account hash and **storage slot key hash**. // hash and storage slot key hash.
storages map[common.Hash]map[common.Hash][]byte Storages map[common.Hash]map[common.Hash]common.Hash
// storagesOrigin stores the original values of mutated slots in // AccountsOrigin holds the original values of mutated accounts, keyed by address.
// 'prefix-zero-trimmed' RLP format. AccountsOrigin map[common.Address]*types.StateAccount
// (a) the value is keyed by account hash and **storage slot key** if rawStorageKey is true;
// (b) the value is keyed by account hash and **storage slot key hash** if rawStorageKey is false;
storagesOrigin map[common.Address]map[common.Hash][]byte
rawStorageKey bool
codes map[common.Address]*contractCode // codes contains the set of dirty codes // StoragesOrigin holds the original values of mutated storage slots.
nodes *trienode.MergedNodeSet // Aggregated dirty nodes caused by state changes // The key format depends on StorageKeyType:
// - if StorageKeyType is plain: keyed by account address and plain storage slot key.
// - if StorageKeyType is hashed: keyed by account address and storage slot key hash.
StoragesOrigin map[common.Address]map[common.Hash]common.Hash
StorageKeyType StorageKeyEncoding
Codes map[common.Address]*ContractCode // Codes contains the set of dirty codes
Nodes *trienode.MergedNodeSet // Aggregated dirty nodes caused by state changes
} }
// empty returns a flag indicating the state transition is empty or not. // Empty returns a flag indicating the state transition is empty or not.
func (sc *stateUpdate) empty() bool { func (sc *StateUpdate) Empty() bool {
return sc.originRoot == sc.root return sc.OriginRoot == sc.Root
} }
// newStateUpdate constructs a state update object by identifying the differences // NewStateUpdate constructs a state update object by identifying the differences
// between two states through state execution. It combines the specified account // between two states through state execution. It combines the specified account
// deletions and account updates to create a complete state update. // deletions and account updates to create a complete state update.
// func NewStateUpdate(typ StorageKeyEncoding, originRoot common.Hash, root common.Hash, blockNumber uint64, deletes map[common.Hash]*AccountDelete, updates map[common.Hash]*AccountUpdate, nodes *trienode.MergedNodeSet) *StateUpdate {
// rawStorageKey is a flag indicating whether to use the raw storage slot key or
// the hash of the slot key for constructing state update object.
func newStateUpdate(rawStorageKey bool, originRoot common.Hash, root common.Hash, blockNumber uint64, deletes map[common.Hash]*accountDelete, updates map[common.Hash]*accountUpdate, nodes *trienode.MergedNodeSet) *stateUpdate {
var ( var (
accounts = make(map[common.Hash][]byte) accounts = make(map[common.Hash]*types.StateAccount)
accountsOrigin = make(map[common.Address][]byte) accountsOrigin = make(map[common.Address]*types.StateAccount)
storages = make(map[common.Hash]map[common.Hash][]byte) storages = make(map[common.Hash]map[common.Hash]common.Hash)
storagesOrigin = make(map[common.Address]map[common.Hash][]byte) storagesOrigin = make(map[common.Address]map[common.Hash]common.Hash)
codes = make(map[common.Address]*contractCode) codes = make(map[common.Address]*ContractCode)
) )
// Since some accounts might be destroyed and recreated within the same // Since some accounts might be deleted and recreated within the same
// block, deletions must be aggregated first. // block, deletions must be aggregated first.
for addrHash, op := range deletes { for addrHash, op := range deletes {
addr := op.address addr := op.Address
accounts[addrHash] = nil accounts[addrHash] = nil
accountsOrigin[addr] = op.origin accountsOrigin[addr] = op.Origin
// If storage wiping exists, the hash of the storage slot key must be used // If storage wiping is present, the StorageKeyEncoding MUST be hashed.
if len(op.storages) > 0 { // Deleted storage slots are iterated from either the trie or the flat
storages[addrHash] = op.storages // state snapshot, both of which use the storage slot hash as the identifier.
// Fortunately, storage wiping is no longer practical after the Cancun fork
// and is not expected to occur.
if len(op.Storages) > 0 {
storages[addrHash] = op.Storages
} }
if len(op.storagesOrigin) > 0 { if len(op.StoragesOrigin) > 0 {
storagesOrigin[addr] = op.storagesOrigin storagesOrigin[addr] = op.StoragesOrigin
} }
} }
// Aggregate account updates then. // Aggregate account updates then.
for addrHash, op := range updates { for addrHash, op := range updates {
// Aggregate dirty contract codes if they are available. // Aggregate dirty contract codes if they are available.
addr := op.address addr := op.Address
if op.code != nil { if op.Code != nil {
codes[addr] = op.code codes[addr] = op.Code
} }
accounts[addrHash] = op.data accounts[addrHash] = op.Data
// Aggregate the account original value. If the account is already // Aggregate the account original value. If the account is already
// present in the aggregated accountsOrigin set, skip it. // present in the aggregated AccountsOrigin set, skip it.
if _, found := accountsOrigin[addr]; !found { if _, found := accountsOrigin[addr]; !found {
accountsOrigin[addr] = op.origin accountsOrigin[addr] = op.Origin
} }
// Aggregate the storage mutation list. If a slot in op.storages is // Aggregate the storage mutation list. If a slot in op.storages is
// already present in aggregated storages set, the value will be // already present in aggregated storages set, the value will be
// overwritten. // overwritten.
if len(op.storages) > 0 { if len(op.Storages) > 0 {
if _, exist := storages[addrHash]; !exist { if _, exist := storages[addrHash]; !exist {
storages[addrHash] = op.storages storages[addrHash] = op.Storages
} else { } else {
maps.Copy(storages[addrHash], op.storages) maps.Copy(storages[addrHash], op.Storages)
} }
} }
// Aggregate the storage original values. If the slot is already present // Aggregate the storage original values. If the slot is already present
// in aggregated storagesOrigin set, skip it. // in aggregated StoragesOrigin set, skip it.
storageOriginSet := op.storagesOriginByHash storageOriginSet := op.StoragesOriginByHash
if rawStorageKey { if typ == StorageKeyPlain {
storageOriginSet = op.storagesOriginByKey storageOriginSet = op.StoragesOriginByKey
} }
if len(storageOriginSet) > 0 { if len(storageOriginSet) > 0 {
origin, exist := storagesOrigin[addr] origin, exist := storagesOrigin[addr]
@ -173,32 +182,114 @@ func newStateUpdate(rawStorageKey bool, originRoot common.Hash, root common.Hash
} }
} }
} }
return &stateUpdate{ return &StateUpdate{
originRoot: originRoot, OriginRoot: originRoot,
root: root, Root: root,
blockNumber: blockNumber, BlockNumber: blockNumber,
accounts: accounts, Accounts: accounts,
accountsOrigin: accountsOrigin, AccountsOrigin: accountsOrigin,
storages: storages, Storages: storages,
storagesOrigin: storagesOrigin, StoragesOrigin: storagesOrigin,
rawStorageKey: rawStorageKey, StorageKeyType: typ,
codes: codes, Codes: codes,
nodes: nodes, Nodes: nodes,
} }
} }
// stateSet converts the current stateUpdate object into a triedb.StateSet // encodeSlot encodes the storage slot value by trimming all leading zeros
// object. This function extracts the necessary data from the stateUpdate // and then RLP-encoding the result.
// struct and formats it into the StateSet structure consumed by the triedb func encodeSlot(value common.Hash) []byte {
// package. if value == (common.Hash{}) {
func (sc *stateUpdate) stateSet() *triedb.StateSet { return nil
return &triedb.StateSet{
Accounts: sc.accounts,
AccountsOrigin: sc.accountsOrigin,
Storages: sc.storages,
StoragesOrigin: sc.storagesOrigin,
RawStorageKey: sc.rawStorageKey,
} }
blob, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(value[:]))
return blob
}
// EncodeMPTState encodes all state mutations alongside their original value
// into the Merkle-Patricia-Trie representation.
//
// It transforms account and storage updates into their corresponding MPT-encoded
// key-value mappings, using the same encoding rules as the Ethereum state trie.
func (sc *StateUpdate) EncodeMPTState() (map[common.Hash][]byte, map[common.Address][]byte, map[common.Hash]map[common.Hash][]byte, map[common.Address]map[common.Hash][]byte) {
var (
accounts = make(map[common.Hash][]byte, len(sc.Accounts))
storages = make(map[common.Hash]map[common.Hash][]byte, len(sc.Storages))
accountOrigin = make(map[common.Address][]byte, len(sc.AccountsOrigin))
storageOrigin = make(map[common.Address]map[common.Hash][]byte, len(sc.StoragesOrigin))
)
for addr, prev := range sc.AccountsOrigin {
if prev == nil {
accountOrigin[addr] = nil
} else {
accountOrigin[addr] = types.SlimAccountRLP(*prev)
}
}
for addrHash, data := range sc.Accounts {
if data == nil {
accounts[addrHash] = nil
} else {
accounts[addrHash] = types.SlimAccountRLP(*data)
}
}
for addr, slots := range sc.StoragesOrigin {
subset := make(map[common.Hash][]byte)
for key, val := range slots {
subset[key] = encodeSlot(val)
}
storageOrigin[addr] = subset
}
for addrHash, slots := range sc.Storages {
subset := make(map[common.Hash][]byte)
for key, val := range slots {
subset[key] = encodeSlot(val)
}
storages[addrHash] = subset
}
return accounts, accountOrigin, storages, storageOrigin
}
// EncodeUBTState encodes all state mutations alongside their original value
// into the Unified-Binary-Trie representation.
//
// It transforms account and storage updates into their corresponding UBT-encoded
// key-value mappings, using the same encoding rules as the Ethereum state trie.
func (sc *StateUpdate) EncodeUBTState() (map[common.Hash][]byte, map[common.Address][]byte, map[common.Hash]map[common.Hash][]byte, map[common.Address]map[common.Hash][]byte) {
var (
accounts = make(map[common.Hash][]byte, len(sc.Accounts))
storages = make(map[common.Hash]map[common.Hash][]byte, len(sc.Storages))
accountOrigin = make(map[common.Address][]byte, len(sc.AccountsOrigin))
storageOrigin = make(map[common.Address]map[common.Hash][]byte, len(sc.StoragesOrigin))
)
for addr, prev := range sc.AccountsOrigin {
if prev == nil {
accountOrigin[addr] = nil
} else {
accountOrigin[addr] = types.SlimAccountRLP(*prev)
}
}
for addrHash, data := range sc.Accounts {
if data == nil {
accounts[addrHash] = nil
} else {
accounts[addrHash] = types.SlimAccountRLP(*data)
}
}
for addr, slots := range sc.StoragesOrigin {
subset := make(map[common.Hash][]byte)
for key, val := range slots {
subset[key] = encodeSlot(val)
}
storageOrigin[addr] = subset
}
for addrHash, slots := range sc.Storages {
subset := make(map[common.Hash][]byte)
for key, val := range slots {
subset[key] = encodeSlot(val)
}
storages[addrHash] = subset
}
return accounts, accountOrigin, storages, storageOrigin
} }
// deriveCodeFields derives the missing fields of contract code changes // deriveCodeFields derives the missing fields of contract code changes
@ -207,135 +298,96 @@ func (sc *stateUpdate) stateSet() *triedb.StateSet {
// Note: This operation is expensive and not needed during normal state // Note: This operation is expensive and not needed during normal state
// transitions. It is only required when SizeTracker or StateUpdate hook // transitions. It is only required when SizeTracker or StateUpdate hook
// is enabled to produce accurate state statistics. // is enabled to produce accurate state statistics.
func (sc *stateUpdate) deriveCodeFields(reader ContractCodeReader) error { func (sc *StateUpdate) deriveCodeFields(reader ContractCodeReader) error {
cache := make(map[common.Hash]bool) cache := make(map[common.Hash]bool)
for addr, code := range sc.codes { for addr, code := range sc.Codes {
if code.originHash != types.EmptyCodeHash { if code.OriginHash != types.EmptyCodeHash {
blob := reader.Code(addr, code.originHash) blob := reader.Code(addr, code.OriginHash)
if len(blob) == 0 { if len(blob) == 0 {
return fmt.Errorf("original code of %x is empty", addr) return fmt.Errorf("original code of %x is empty", addr)
} }
code.originBlob = blob code.OriginBlob = blob
} }
if exists, ok := cache[code.hash]; ok { if exists, ok := cache[code.Hash]; ok {
code.duplicate = exists code.Duplicate = exists
continue continue
} }
res := reader.Has(addr, code.hash) res := reader.Has(addr, code.Hash)
cache[code.hash] = res cache[code.Hash] = res
code.duplicate = res code.Duplicate = res
} }
return nil return nil
} }
// ToTracingUpdate converts the internal stateUpdate to an exported tracing.StateUpdate. // ToTracingUpdate converts the internal StateUpdate to an exported tracing.StateUpdate.
func (sc *stateUpdate) ToTracingUpdate() (*tracing.StateUpdate, error) { func (sc *StateUpdate) ToTracingUpdate() (*tracing.StateUpdate, error) {
update := &tracing.StateUpdate{ update := &tracing.StateUpdate{
OriginRoot: sc.originRoot, OriginRoot: sc.OriginRoot,
Root: sc.root, Root: sc.Root,
BlockNumber: sc.blockNumber, BlockNumber: sc.BlockNumber,
AccountChanges: make(map[common.Address]*tracing.AccountChange, len(sc.accountsOrigin)), AccountChanges: make(map[common.Address]*tracing.AccountChange, len(sc.AccountsOrigin)),
StorageChanges: make(map[common.Address]map[common.Hash]*tracing.StorageChange), StorageChanges: make(map[common.Address]map[common.Hash]*tracing.StorageChange),
CodeChanges: make(map[common.Address]*tracing.CodeChange, len(sc.codes)), CodeChanges: make(map[common.Address]*tracing.CodeChange, len(sc.Codes)),
TrieChanges: make(map[common.Hash]map[string]*tracing.TrieNodeChange), TrieChanges: make(map[common.Hash]map[string]*tracing.TrieNodeChange),
} }
// Gather all account changes // Gather all account changes
for addr, oldData := range sc.accountsOrigin { for addr, oldData := range sc.AccountsOrigin {
addrHash := crypto.Keccak256Hash(addr.Bytes()) addrHash := crypto.Keccak256Hash(addr.Bytes())
newData, exists := sc.accounts[addrHash] newData, exists := sc.Accounts[addrHash]
if !exists { if !exists {
return nil, fmt.Errorf("account %x not found", addr) return nil, fmt.Errorf("account %x not found", addr)
} }
change := &tracing.AccountChange{} change := &tracing.AccountChange{
Prev: oldData,
if len(oldData) > 0 { New: newData,
acct, err := types.FullAccount(oldData)
if err != nil {
return nil, err
}
change.Prev = &types.StateAccount{
Nonce: acct.Nonce,
Balance: acct.Balance,
Root: acct.Root,
CodeHash: acct.CodeHash,
}
}
if len(newData) > 0 {
acct, err := types.FullAccount(newData)
if err != nil {
return nil, err
}
change.New = &types.StateAccount{
Nonce: acct.Nonce,
Balance: acct.Balance,
Root: acct.Root,
CodeHash: acct.CodeHash,
}
} }
update.AccountChanges[addr] = change update.AccountChanges[addr] = change
} }
// Gather all storage slot changes // Gather all storage slot changes
for addr, slots := range sc.storagesOrigin { for addr, slots := range sc.StoragesOrigin {
addrHash := crypto.Keccak256Hash(addr.Bytes()) addrHash := crypto.Keccak256Hash(addr.Bytes())
subset, exists := sc.storages[addrHash] subset, exists := sc.Storages[addrHash]
if !exists { if !exists {
return nil, fmt.Errorf("storage %x not found", addr) return nil, fmt.Errorf("storage %x not found", addr)
} }
storageChanges := make(map[common.Hash]*tracing.StorageChange, len(slots)) storageChanges := make(map[common.Hash]*tracing.StorageChange, len(slots))
for key, encPrev := range slots { for key, oldData := range slots {
// Get new value - handle both raw and hashed key formats // Get new value - handle both raw and hashed key formats
var ( var (
exists bool exists bool
encNew []byte newData common.Hash
decPrev []byte
decNew []byte
err error
) )
if sc.rawStorageKey { if sc.StorageKeyType == StorageKeyPlain {
encNew, exists = subset[crypto.Keccak256Hash(key.Bytes())] newData, exists = subset[crypto.Keccak256Hash(key.Bytes())]
} else { } else {
encNew, exists = subset[key] newData, exists = subset[key]
} }
if !exists { if !exists {
return nil, fmt.Errorf("storage slot %x-%x not found", addr, key) return nil, fmt.Errorf("storage slot %x-%x not found", addr, key)
} }
// Decode the prev and new values
if len(encPrev) > 0 {
_, decPrev, _, err = rlp.Split(encPrev)
if err != nil {
return nil, fmt.Errorf("failed to decode prevValue: %v", err)
}
}
if len(encNew) > 0 {
_, decNew, _, err = rlp.Split(encNew)
if err != nil {
return nil, fmt.Errorf("failed to decode newValue: %v", err)
}
}
storageChanges[key] = &tracing.StorageChange{ storageChanges[key] = &tracing.StorageChange{
Prev: common.BytesToHash(decPrev), Prev: oldData,
New: common.BytesToHash(decNew), New: newData,
} }
} }
update.StorageChanges[addr] = storageChanges update.StorageChanges[addr] = storageChanges
} }
// Gather all contract code changes // Gather all contract code changes
for addr, code := range sc.codes { for addr, code := range sc.Codes {
change := &tracing.CodeChange{ change := &tracing.CodeChange{
New: &tracing.ContractCode{ New: &tracing.ContractCode{
Hash: code.hash, Hash: code.Hash,
Code: code.blob, Code: code.Blob,
Exists: code.duplicate, Exists: code.Duplicate,
}, },
} }
if code.originHash != types.EmptyCodeHash { if code.OriginHash != types.EmptyCodeHash {
change.Prev = &tracing.ContractCode{ change.Prev = &tracing.ContractCode{
Hash: code.originHash, Hash: code.OriginHash,
Code: code.originBlob, Code: code.OriginBlob,
Exists: true, Exists: true,
} }
} }
@ -343,8 +395,8 @@ func (sc *stateUpdate) ToTracingUpdate() (*tracing.StateUpdate, error) {
} }
// Gather all trie node changes // Gather all trie node changes
if sc.nodes != nil { if sc.Nodes != nil {
for owner, subset := range sc.nodes.Sets { for owner, subset := range sc.Nodes.Sets {
nodeChanges := make(map[string]*tracing.TrieNodeChange, len(subset.Origins)) nodeChanges := make(map[string]*tracing.TrieNodeChange, len(subset.Origins))
for path, oldNode := range subset.Origins { for path, oldNode := range subset.Origins {
newNode, exists := subset.Nodes[path] newNode, exists := subset.Nodes[path]