Skip to content

Commit

Permalink
core/state: value diff tracking in StateDB (#27349)
Browse files Browse the repository at this point in the history
This change makes the StateDB track the state key value diff of a block transition.
We already tracked current account and storage values for the purpose of updating
the state snapshot. With this PR, we now also track the original (pre-transition) values
of accounts and storage slots.
  • Loading branch information
rjl493456442 committed Jul 11, 2023
1 parent aecf3f9 commit 4b06e4f
Show file tree
Hide file tree
Showing 24 changed files with 908 additions and 228 deletions.
2 changes: 1 addition & 1 deletion core/state/dump.go
Expand Up @@ -171,7 +171,7 @@ func (s *StateDB) DumpToCollector(c DumpCollector, conf *DumpConfig) (nextKey []
} else {
address = &addr
}
obj := newObject(s, addr, data)
obj := newObject(s, addr, &data)
if !conf.SkipCode {
account.Code = obj.Code(s.db)
}
Expand Down
14 changes: 12 additions & 2 deletions core/state/journal.go
Expand Up @@ -95,6 +95,10 @@ type (
prevdestruct bool
prevAccount []byte
prevStorage map[common.Hash][]byte

prevAccountOriginExist bool
prevAccountOrigin []byte
prevStorageOrigin map[common.Hash][]byte
}
suicideChange struct {
account *common.Address
Expand Down Expand Up @@ -163,10 +167,16 @@ func (ch resetObjectChange) revert(s *StateDB) {
delete(s.stateObjectsDestruct, ch.prev.address)
}
if ch.prevAccount != nil {
s.snapAccounts[ch.prev.addrHash] = ch.prevAccount
s.accounts[ch.prev.addrHash] = ch.prevAccount
}
if ch.prevStorage != nil {
s.snapStorage[ch.prev.addrHash] = ch.prevStorage
s.storages[ch.prev.addrHash] = ch.prevStorage
}
if ch.prevAccountOriginExist {
s.accountsOrigin[ch.prev.addrHash] = ch.prevAccountOrigin
}
if ch.prevStorageOrigin != nil {
s.storagesOrigin[ch.prev.addrHash] = ch.prevStorageOrigin
}
}

Expand Down
7 changes: 7 additions & 0 deletions core/state/metrics.go
Expand Up @@ -27,4 +27,11 @@ var (
storageTriesUpdatedMeter = metrics.NewRegisteredMeter("state/update/storagenodes", nil)
accountTrieDeletedMeter = metrics.NewRegisteredMeter("state/delete/accountnodes", nil)
storageTriesDeletedMeter = metrics.NewRegisteredMeter("state/delete/storagenodes", nil)

slotDeletionMaxCount = metrics.NewRegisteredGauge("state/delete/storage/max/slot", nil)
slotDeletionMaxSize = metrics.NewRegisteredGauge("state/delete/storage/max/size", nil)
slotDeletionTimer = metrics.NewRegisteredResettingTimer("state/delete/storage/timer", nil)
slotDeletionCount = metrics.NewRegisteredMeter("state/delete/storage/slot", nil)
slotDeletionSize = metrics.NewRegisteredMeter("state/delete/storage/size", nil)
slotDeletionSkip = metrics.NewRegisteredGauge("state/delete/storage/skip", nil)
)
2 changes: 1 addition & 1 deletion core/state/snapshot/generate.go
Expand Up @@ -366,7 +366,7 @@ func (dl *diskLayer) generateRange(ctx *generatorContext, trieId *trie.ID, prefi
return false, nil, err
}
if nodes != nil {
tdb.Update(root, types.EmptyRootHash, trienode.NewWithNodeSet(nodes))
tdb.Update(root, types.EmptyRootHash, trienode.NewWithNodeSet(nodes), nil)
tdb.Commit(root, false)
}
resolver = func(owner common.Hash, path []byte, hash common.Hash) []byte {
Expand Down
2 changes: 1 addition & 1 deletion core/state/snapshot/generate_test.go
Expand Up @@ -203,7 +203,7 @@ func (t *testHelper) Commit() common.Hash {
if nodes != nil {
t.nodes.Merge(nodes)
}
t.triedb.Update(root, types.EmptyRootHash, t.nodes)
t.triedb.Update(root, types.EmptyRootHash, t.nodes, nil)
t.triedb.Commit(root, false)
return root
}
Expand Down
124 changes: 76 additions & 48 deletions core/state/state_object.go
Expand Up @@ -57,29 +57,35 @@ func (s Storage) Copy() Storage {
// stateObject represents an Ethereum account which is being modified.
//
// The usage pattern is as follows:
// First you need to obtain a state object.
// Account values can be accessed and modified through the object.
// Finally, call commitTrie to write the modified storage trie into a database.
// - First you need to obtain a state object.
// - Account values as well as storages can be accessed and modified through the object.
// - Finally, call commit to return the changes of storage trie and update account data.
type stateObject struct {
address common.Address
addrHash common.Hash // hash of ethereum address of the account
data types.StateAccount
db *StateDB
address common.Address // address of ethereum account
addrHash common.Hash // hash of ethereum address of the account
origin *types.StateAccount // Account original data without any change applied, nil means it was not existent
data types.StateAccount // Account data with all mutations applied in the scope of block

// Write caches.
trie Trie // storage trie, which becomes non-nil on first access
code Code // contract bytecode, which gets set when code is loaded

originStorage Storage // Storage cache of original entries to dedup rewrites, reset for every transaction
originStorage Storage // Storage cache of original entries to dedup rewrites
pendingStorage Storage // Storage entries that need to be flushed to disk, at the end of an entire block
dirtyStorage Storage // Storage entries that have been modified in the current transaction execution
dirtyStorage Storage // Storage entries that have been modified in the current transaction execution, reset for every transaction

// Cache flags.
// When an object is marked suicided it will be deleted from the trie
// during the "update" phase of the state transition.
dirtyCode bool // true if the code was updated
suicided bool
deleted bool

// Flag whether the account was marked as suicided. The suicided account
// is still accessible in the scope of same transaction.
suicided bool

// Flag whether the account was marked as deleted. The suicided account
// or the account is considered as empty will be marked as deleted at
// the end of transaction and no longer accessible anymore.
deleted bool
}

// empty returns whether the account is considered empty.
Expand All @@ -88,21 +94,17 @@ func (s *stateObject) empty() bool {
}

// newObject creates a state object.
func newObject(db *StateDB, address common.Address, data types.StateAccount) *stateObject {
if data.Balance == nil {
data.Balance = new(big.Int)
}
if data.CodeHash == nil {
data.CodeHash = types.EmptyCodeHash.Bytes()
}
if data.Root == (common.Hash{}) {
data.Root = types.EmptyRootHash
func newObject(db *StateDB, address common.Address, acct *types.StateAccount) *stateObject {
origin := acct
if acct == nil {
acct = types.NewEmptyStateAccount()
}
return &stateObject{
db: db,
address: address,
addrHash: crypto.Keccak256Hash(address[:]),
data: data,
origin: origin,
data: *acct,
originStorage: make(Storage),
pendingStorage: make(Storage),
dirtyStorage: make(Storage),
Expand Down Expand Up @@ -135,10 +137,8 @@ func (s *stateObject) touch() {
func (s *stateObject) getTrie(db Database) (Trie, error) {
if s.trie == nil {
// Try fetching from prefetcher first
// We don't prefetch empty tries
if s.data.Root != types.EmptyRootHash && s.db.prefetcher != nil {
// When the miner is creating the pending state, there is no
// prefetcher
// When the miner is creating the pending state, there is no prefetcher
s.trie = s.db.prefetcher.trie(s.addrHash, s.data.Root)
}
if s.trie == nil {
Expand Down Expand Up @@ -277,6 +277,7 @@ func (s *stateObject) updateTrie(db Database) (Trie, error) {
// The snapshot storage map for the object
var (
storage map[common.Hash][]byte
origin map[common.Hash][]byte
hasher = s.db.hasher
)
tr, err := s.getTrie(db)
Expand All @@ -291,6 +292,7 @@ func (s *stateObject) updateTrie(db Database) (Trie, error) {
if value == s.originStorage[key] {
continue
}
prev := s.originStorage[key]
s.originStorage[key] = value

// rlp-encoded value to be used by the snapshot
Expand All @@ -311,17 +313,34 @@ func (s *stateObject) updateTrie(db Database) (Trie, error) {
}
s.db.StorageUpdated += 1
}
// If state snapshotting is active, cache the data til commit
if s.db.snap != nil {
if storage == nil {
// Retrieve the old storage map, if available, create a new one otherwise
if storage = s.db.snapStorage[s.addrHash]; storage == nil {
storage = make(map[common.Hash][]byte)
s.db.snapStorage[s.addrHash] = storage
}
// Cache the mutated storage slots until commit
if storage == nil {
if storage = s.db.storages[s.addrHash]; storage == nil {
storage = make(map[common.Hash][]byte)
s.db.storages[s.addrHash] = storage
}
}
khash := crypto.HashData(hasher, key[:])
storage[khash] = snapshotVal // snapshotVal will be nil if it's deleted

// Cache the original value of mutated storage slots
if origin == nil {
if origin = s.db.storagesOrigin[s.addrHash]; origin == nil {
origin = make(map[common.Hash][]byte)
s.db.storagesOrigin[s.addrHash] = origin
}
}
// Track the original value of slot only if it's mutated first time
if _, ok := origin[khash]; !ok {
if prev == (common.Hash{}) {
origin[khash] = nil // nil if it was not present previously
} else {
// Encoding []byte cannot fail, ok to ignore the error.
b, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(prev[:]))
origin[khash] = b
}
storage[crypto.HashData(hasher, key[:])] = snapshotVal // will be nil if it's deleted
}
// Cache the items for preloading
usedStorage = append(usedStorage, common.CopyBytes(key[:])) // Copy needed for closure
}
if s.db.prefetcher != nil {
Expand Down Expand Up @@ -351,15 +370,15 @@ func (s *stateObject) updateRoot(db Database) {
s.data.Root = tr.Hash()
}

// commitTrie submits the storage changes into the storage trie and re-computes
// the root. Besides, all trie changes will be collected in a nodeset and returned.
func (s *stateObject) commitTrie(db Database) (*trienode.NodeSet, error) {
// commit returns the changes made in storage trie and updates the account data.
func (s *stateObject) commit(db Database) (*trienode.NodeSet, error) {
tr, err := s.updateTrie(db)
if err != nil {
return nil, err
}
// If nothing changed, don't bother with committing anything
if tr == nil {
s.origin = s.data.Copy()
return nil, nil
}
// Track the amount of time wasted on committing the storage trie
Expand All @@ -371,6 +390,9 @@ func (s *stateObject) commitTrie(db Database) (*trienode.NodeSet, error) {
return nil, err
}
s.data.Root = root

// Update original account data after commit
s.origin = s.data.Copy()
return nodes, nil
}

Expand Down Expand Up @@ -410,18 +432,24 @@ func (s *stateObject) setBalance(amount *big.Int) {
}

func (s *stateObject) deepCopy(db *StateDB) *stateObject {
stateObject := newObject(db, s.address, s.data)
obj := &stateObject{
db: db,
address: s.address,
addrHash: s.addrHash,
origin: s.origin,
data: s.data,
}
if s.trie != nil {
stateObject.trie = db.db.CopyTrie(s.trie)
}
stateObject.code = s.code
stateObject.dirtyStorage = s.dirtyStorage.Copy()
stateObject.originStorage = s.originStorage.Copy()
stateObject.pendingStorage = s.pendingStorage.Copy()
stateObject.suicided = s.suicided
stateObject.dirtyCode = s.dirtyCode
stateObject.deleted = s.deleted
return stateObject
obj.trie = db.db.CopyTrie(s.trie)
}
obj.code = s.code
obj.dirtyStorage = s.dirtyStorage.Copy()
obj.originStorage = s.originStorage.Copy()
obj.pendingStorage = s.pendingStorage.Copy()
obj.suicided = s.suicided
obj.dirtyCode = s.dirtyCode
obj.deleted = s.deleted
return obj
}

//
Expand Down
16 changes: 8 additions & 8 deletions core/state/state_test.go
Expand Up @@ -30,22 +30,22 @@ import (
"github.com/ethereum/go-ethereum/trie"
)

type stateTest struct {
type stateEnv struct {
db ethdb.Database
state *StateDB
}

func newStateTest() *stateTest {
func newStateEnv() *stateEnv {
db := rawdb.NewMemoryDatabase()
sdb, _ := New(types.EmptyRootHash, NewDatabase(db), nil)
return &stateTest{db: db, state: sdb}
return &stateEnv{db: db, state: sdb}
}

func TestDump(t *testing.T) {
db := rawdb.NewMemoryDatabase()
tdb := NewDatabaseWithConfig(db, &trie.Config{Preimages: true})
sdb, _ := New(types.EmptyRootHash, tdb, nil)
s := &stateTest{db: db, state: sdb}
s := &stateEnv{db: db, state: sdb}

// generate a few entries
obj1 := s.state.GetOrNewStateObject(common.BytesToAddress([]byte{0x01}))
Expand Down Expand Up @@ -99,7 +99,7 @@ func TestIterativeDump(t *testing.T) {
db := rawdb.NewMemoryDatabase()
tdb := NewDatabaseWithConfig(db, &trie.Config{Preimages: true})
sdb, _ := New(types.EmptyRootHash, tdb, nil)
s := &stateTest{db: db, state: sdb}
s := &stateEnv{db: db, state: sdb}

// generate a few entries
obj1 := s.state.GetOrNewStateObject(common.BytesToAddress([]byte{0x01}))
Expand Down Expand Up @@ -133,7 +133,7 @@ func TestIterativeDump(t *testing.T) {
}

func TestNull(t *testing.T) {
s := newStateTest()
s := newStateEnv()
address := common.HexToAddress("0x823140710bf13990e4500136726d8b55")
s.state.CreateAccount(address)
//value := common.FromHex("0x823140710bf13990e4500136726d8b55")
Expand All @@ -155,7 +155,7 @@ func TestSnapshot(t *testing.T) {
var storageaddr common.Hash
data1 := common.BytesToHash([]byte{42})
data2 := common.BytesToHash([]byte{43})
s := newStateTest()
s := newStateEnv()

// snapshot the genesis state
genesis := s.state.Snapshot()
Expand Down Expand Up @@ -186,7 +186,7 @@ func TestSnapshot(t *testing.T) {
}

func TestSnapshotEmpty(t *testing.T) {
s := newStateTest()
s := newStateEnv()
s.state.RevertToSnapshot(s.state.Snapshot())
}

Expand Down

0 comments on commit 4b06e4f

Please sign in to comment.