core/state: export StateUpdate struct (#34724)
Some checks are pending
/ Linux Build (push) Waiting to run
/ Linux Build (arm) (push) Waiting to run
/ Keeper Build (push) Waiting to run
/ Windows Build (push) Waiting to run
/ Docker Image (push) Waiting to run

In the recent refactoring, the state commit logic has been abstracted, 
making it more flexible to design state databases for various use cases.
For example, execution-only modes where state mutation is disabled.

As part of this change, the database interface was extended with a 
Commit function. However, it currently accepts an unexported struct
`stateUpdate`, which prevents downstream projects from customizing
the state commit behavior.

To address this limitation, the stateUpdate type is now exported.
This commit is contained in:
rjl493456442 2026-04-20 23:12:10 +08:00 committed by GitHub
parent 7e388fd09e
commit acbf699c33
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 445 additions and 355 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
} }
@ -190,13 +189,16 @@ func newFlatStorageIterator(it snapshot.StorageIterator, preimage PreimageReader
// is exhausted or if an error occurs. Any error encountered is retained and // is exhausted or if an error occurs. Any error encountered is retained and
// can be retrieved via Error(). // can be retrieved via Error().
func (si *flatStorageIterator) Next() bool { func (si *flatStorageIterator) Next() bool {
if si.err != nil {
return false
}
return si.it.Next() return si.it.Next()
} }
// 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 +227,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
} }
@ -254,13 +266,16 @@ func newMerkleTrieIterator(tr Trie, start common.Hash, account bool) (*merkleIte
// is exhausted or if an error occurs. Any error encountered is retained and // is exhausted or if an error occurs. Any error encountered is retained and
// can be retrieved via Error(). // can be retrieved via Error().
func (ti *merkleIterator) Next() bool { func (ti *merkleIterator) Next() bool {
if ti.err != nil {
return false
}
return ti.it.Next() return ti.it.Next()
} }
// 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 +302,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 +328,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 +458,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"
) )
@ -115,7 +113,7 @@ func (d iterativeDump) OnRoot(root common.Hash) {
// DumpToCollector iterates the state according to the given options and inserts // DumpToCollector iterates the state according to the given options and inserts
// the items into a collector for aggregation or serialization. // the items into a collector for aggregation or serialization.
func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []byte) { func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []byte, err error) {
// Sanitize the input to allow nil configs // Sanitize the input to allow nil configs
if conf == nil { if conf == nil {
conf = new(DumpConfig) conf = new(DumpConfig)
@ -131,7 +129,7 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
iteratee, err := s.db.Iteratee(s.originalRoot) iteratee, err := s.db.Iteratee(s.originalRoot)
if err != nil { if err != nil {
return nil return nil, err
} }
var startHash common.Hash var startHash common.Hash
if conf.Start != nil { if conf.Start != nil {
@ -139,14 +137,17 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
} }
acctIt, err := iteratee.NewAccountIterator(startHash) acctIt, err := iteratee.NewAccountIterator(startHash)
if err != nil { if err != nil {
return nil return nil, err
} }
defer acctIt.Release() defer acctIt.Release()
for acctIt.Next() { for acctIt.Next() {
var data types.StateAccount data := acctIt.Account()
if err := rlp.DecodeBytes(acctIt.Account(), &data); err != nil { if err := acctIt.Error(); err != nil {
panic(err) return nil, err
}
if data == nil {
return nil, fmt.Errorf("unexpected nil account value")
} }
var ( var (
account = DumpAccount{ account = DumpAccount{
@ -168,7 +169,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()
} }
@ -177,20 +178,19 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
storageIt, err := iteratee.NewStorageIterator(acctIt.Hash(), common.Hash{}) storageIt, err := iteratee.NewStorageIterator(acctIt.Hash(), common.Hash{})
if err != nil { if err != nil {
log.Error("Failed to load storage trie", "err", err) return nil, err
continue
} }
for storageIt.Next() { for storageIt.Next() {
_, content, _, err := rlp.Split(storageIt.Slot()) val := storageIt.Slot()
if err != nil { if err := storageIt.Error(); err != nil {
log.Error("Failed to decode the value returned by iterator", "error", err) storageIt.Release()
continue return nil, err
} }
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] = common.Bytes2Hex(common.TrimLeftZeroes(val[:]))
} }
storageIt.Release() storageIt.Release()
} }
@ -211,7 +211,7 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
log.Warn("Dump incomplete due to missing preimages", "missing", missingPreimages) log.Warn("Dump incomplete due to missing preimages", "missing", missingPreimages)
} }
log.Info("Trie dumping complete", "accounts", accounts, "elapsed", common.PrettyDuration(time.Since(start))) log.Info("Trie dumping complete", "accounts", accounts, "elapsed", common.PrettyDuration(time.Since(start)))
return nextKey return nextKey, nil
} }
// DumpBinTrieLeaves collects all binary trie leaf nodes into the provided map. // DumpBinTrieLeaves collects all binary trie leaf nodes into the provided map.
@ -242,7 +242,8 @@ func (s *StateDB) RawDump(opts *DumpConfig) Dump {
dump := &Dump{ dump := &Dump{
Accounts: make(map[string]DumpAccount), Accounts: make(map[string]DumpAccount),
} }
dump.Next = s.DumpToCollector(dump, opts) next, _ := s.DumpToCollector(dump, opts)
dump.Next = next
return *dump return *dump
} }

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,143 @@ 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 len(op.Storages) > 0 {
if len(op.storages) > 0 { storages[addrHash] = op.Storages
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 +177,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 +293,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 +390,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]