Skip to content

Commit

Permalink
core/state: semantic journalling (part 1) (ethereum#28880)
Browse files Browse the repository at this point in the history
This is a follow-up to ethereum#29520, and a preparatory PR to a more thorough
change in the journalling system.

### API methods instead of `append` operations

This PR hides the journal-implementation details away, so that the
statedb invokes methods like `JournalCreate`, instead of explicitly
appending journal-events in a list. This means that it's up to the
journal whether to implement it as a sequence of events or
aggregate/merge events.

### Snapshot-management inside the journal 

This PR also makes it so that management of valid snapshots is moved
inside the journal, exposed via the methods `Snapshot() int` and
`RevertToSnapshot(revid int, s *StateDB)`.


### SetCode

JournalSetCode journals the setting of code: it is implicit that the
previous values were "no code" and emptyCodeHash. Therefore, we can
simplify the setCode journal.

### Selfdestruct

The self-destruct journalling is a bit strange: we allow the
selfdestruct operation to be journalled several times. This makes it so
that we also are forced to store whether the account was already
destructed.

What we can do instead, is to only journal the first destruction, and
after that only journal balance-changes, but not journal the
selfdestruct itself.

This simplifies the journalling, so that internals about state
management does not leak into the journal-API.

### Preimages

Preimages were, for some reason, integrated into the journal management,
despite not being a consensus-critical data structure. This PR undoes
that.

---------

Co-authored-by: Gary Rong <garyrong0905@gmail.com>
  • Loading branch information
holiman and rjl493456442 authored Aug 28, 2024
1 parent 9eb9154 commit 0e5546f
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 129 deletions.
168 changes: 133 additions & 35 deletions core/state/journal.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,21 @@
package state

import (
"fmt"
"maps"
"slices"
"sort"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/holiman/uint256"
)

type revision struct {
id int
journalIndex int
}

// journalEntry is a modification entry in the state change journal that can be
// reverted on demand.
type journalEntry interface {
Expand All @@ -42,6 +51,9 @@ type journalEntry interface {
type journal struct {
entries []journalEntry // Current changes tracked by the journal
dirties map[common.Address]int // Dirty accounts and the number of changes

validRevisions []revision
nextRevisionId int
}

// newJournal creates a new initialized journal.
Expand All @@ -51,6 +63,40 @@ func newJournal() *journal {
}
}

// reset clears the journal, after this operation the journal can be used anew.
// It is semantically similar to calling 'newJournal', but the underlying slices
// can be reused.
func (j *journal) reset() {
j.entries = j.entries[:0]
j.validRevisions = j.validRevisions[:0]
clear(j.dirties)
j.nextRevisionId = 0
}

// snapshot returns an identifier for the current revision of the state.
func (j *journal) snapshot() int {
id := j.nextRevisionId
j.nextRevisionId++
j.validRevisions = append(j.validRevisions, revision{id, j.length()})
return id
}

// revertToSnapshot reverts all state changes made since the given revision.
func (j *journal) revertToSnapshot(revid int, s *StateDB) {
// Find the snapshot in the stack of valid snapshots.
idx := sort.Search(len(j.validRevisions), func(i int) bool {
return j.validRevisions[i].id >= revid
})
if idx == len(j.validRevisions) || j.validRevisions[idx].id != revid {
panic(fmt.Errorf("revision id %v cannot be reverted", revid))
}
snapshot := j.validRevisions[idx].journalIndex

// Replay the journal to undo changes and remove invalidated snapshots
j.revert(s, snapshot)
j.validRevisions = j.validRevisions[:idx]
}

// append inserts a new modification entry to the end of the change journal.
func (j *journal) append(entry journalEntry) {
j.entries = append(j.entries, entry)
Expand Down Expand Up @@ -95,11 +141,90 @@ func (j *journal) copy() *journal {
entries = append(entries, j.entries[i].copy())
}
return &journal{
entries: entries,
dirties: maps.Clone(j.dirties),
entries: entries,
dirties: maps.Clone(j.dirties),
validRevisions: slices.Clone(j.validRevisions),
nextRevisionId: j.nextRevisionId,
}
}

func (j *journal) logChange(txHash common.Hash) {
j.append(addLogChange{txhash: txHash})
}

func (j *journal) createObject(addr common.Address) {
j.append(createObjectChange{account: &addr})
}

func (j *journal) createContract(addr common.Address) {
j.append(createContractChange{account: addr})
}

func (j *journal) destruct(addr common.Address) {
j.append(selfDestructChange{account: &addr})
}

func (j *journal) storageChange(addr common.Address, key, prev, origin common.Hash) {
j.append(storageChange{
account: &addr,
key: key,
prevvalue: prev,
origvalue: origin,
})
}

func (j *journal) transientStateChange(addr common.Address, key, prev common.Hash) {
j.append(transientStorageChange{
account: &addr,
key: key,
prevalue: prev,
})
}

func (j *journal) refundChange(previous uint64) {
j.append(refundChange{prev: previous})
}

func (j *journal) balanceChange(addr common.Address, previous *uint256.Int) {
j.append(balanceChange{
account: &addr,
prev: previous.Clone(),
})
}

func (j *journal) setCode(address common.Address) {
j.append(codeChange{account: &address})
}

func (j *journal) nonceChange(address common.Address, prev uint64) {
j.append(nonceChange{
account: &address,
prev: prev,
})
}

func (j *journal) touchChange(address common.Address) {
j.append(touchChange{
account: &address,
})
if address == ripemd {
// Explicitly put it in the dirty-cache, which is otherwise generated from
// flattened journals.
j.dirty(address)
}
}

func (j *journal) accessListAddAccount(addr common.Address) {
j.append(accessListAddAccountChange{&addr})
}

func (j *journal) accessListAddSlot(addr common.Address, slot common.Hash) {
j.append(accessListAddSlotChange{
address: &addr,
slot: &slot,
})
}

type (
// Changes to the account trie.
createObjectChange struct {
Expand All @@ -114,9 +239,7 @@ type (
}

selfDestructChange struct {
account *common.Address
prev bool // whether account had already self-destructed
prevbalance *uint256.Int
account *common.Address
}

// Changes to individual accounts.
Expand All @@ -135,8 +258,7 @@ type (
origvalue common.Hash
}
codeChange struct {
account *common.Address
prevcode, prevhash []byte
account *common.Address
}

// Changes to other state values.
Expand All @@ -146,9 +268,6 @@ type (
addLogChange struct {
txhash common.Hash
}
addPreimageChange struct {
hash common.Hash
}
touchChange struct {
account *common.Address
}
Expand Down Expand Up @@ -200,8 +319,7 @@ func (ch createContractChange) copy() journalEntry {
func (ch selfDestructChange) revert(s *StateDB) {
obj := s.getStateObject(*ch.account)
if obj != nil {
obj.selfDestructed = ch.prev
obj.setBalance(ch.prevbalance)
obj.selfDestructed = false
}
}

Expand All @@ -211,9 +329,7 @@ func (ch selfDestructChange) dirtied() *common.Address {

func (ch selfDestructChange) copy() journalEntry {
return selfDestructChange{
account: ch.account,
prev: ch.prev,
prevbalance: new(uint256.Int).Set(ch.prevbalance),
account: ch.account,
}
}

Expand Down Expand Up @@ -263,19 +379,15 @@ func (ch nonceChange) copy() journalEntry {
}

func (ch codeChange) revert(s *StateDB) {
s.getStateObject(*ch.account).setCode(common.BytesToHash(ch.prevhash), ch.prevcode)
s.getStateObject(*ch.account).setCode(types.EmptyCodeHash, nil)
}

func (ch codeChange) dirtied() *common.Address {
return ch.account
}

func (ch codeChange) copy() journalEntry {
return codeChange{
account: ch.account,
prevhash: common.CopyBytes(ch.prevhash),
prevcode: common.CopyBytes(ch.prevcode),
}
return codeChange{account: ch.account}
}

func (ch storageChange) revert(s *StateDB) {
Expand Down Expand Up @@ -344,20 +456,6 @@ func (ch addLogChange) copy() journalEntry {
}
}

func (ch addPreimageChange) revert(s *StateDB) {
delete(s.preimages, ch.hash)
}

func (ch addPreimageChange) dirtied() *common.Address {
return nil
}

func (ch addPreimageChange) copy() journalEntry {
return addPreimageChange{
hash: ch.hash,
}
}

func (ch accessListAddAccountChange) revert(s *StateDB) {
/*
One important invariant here, is that whenever a (addr, slot) is added, if the
Expand Down
38 changes: 8 additions & 30 deletions core/state/state_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,7 @@ func (s *stateObject) markSelfdestructed() {
}

func (s *stateObject) touch() {
s.db.journal.append(touchChange{
account: &s.address,
})
if s.address == ripemd {
// Explicitly put it in the dirty-cache, which is otherwise generated from
// flattened journals.
s.db.journal.dirty(s.address)
}
s.db.journal.touchChange(s.address)
}

// getTrie returns the associated storage trie. The trie will be opened if it's
Expand Down Expand Up @@ -252,16 +245,11 @@ func (s *stateObject) SetState(key, value common.Hash) {
return
}
// New value is different, update and journal the change
s.db.journal.append(storageChange{
account: &s.address,
key: key,
prevvalue: prev,
origvalue: origin,
})
s.db.journal.storageChange(s.address, key, prev, origin)
s.setState(key, value, origin)
if s.db.logger != nil && s.db.logger.OnStorageChange != nil {
s.db.logger.OnStorageChange(s.address, key, prev, value)
}
s.setState(key, value, origin)
}

// setState updates a value in account dirty storage. The dirtiness will be
Expand Down Expand Up @@ -511,10 +499,7 @@ func (s *stateObject) SubBalance(amount *uint256.Int, reason tracing.BalanceChan
}

func (s *stateObject) SetBalance(amount *uint256.Int, reason tracing.BalanceChangeReason) {
s.db.journal.append(balanceChange{
account: &s.address,
prev: new(uint256.Int).Set(s.data.Balance),
})
s.db.journal.balanceChange(s.address, s.data.Balance)
if s.db.logger != nil && s.db.logger.OnBalanceChange != nil {
s.db.logger.OnBalanceChange(s.address, s.Balance().ToBig(), amount.ToBig(), reason)
}
Expand Down Expand Up @@ -590,14 +575,10 @@ func (s *stateObject) CodeSize() int {
}

func (s *stateObject) SetCode(codeHash common.Hash, code []byte) {
prevcode := s.Code()
s.db.journal.append(codeChange{
account: &s.address,
prevhash: s.CodeHash(),
prevcode: prevcode,
})
s.db.journal.setCode(s.address)
if s.db.logger != nil && s.db.logger.OnCodeChange != nil {
s.db.logger.OnCodeChange(s.address, common.BytesToHash(s.CodeHash()), prevcode, codeHash, code)
// TODO remove prevcode from this callback
s.db.logger.OnCodeChange(s.address, common.BytesToHash(s.CodeHash()), nil, codeHash, code)
}
s.setCode(codeHash, code)
}
Expand All @@ -609,10 +590,7 @@ func (s *stateObject) setCode(codeHash common.Hash, code []byte) {
}

func (s *stateObject) SetNonce(nonce uint64) {
s.db.journal.append(nonceChange{
account: &s.address,
prev: s.data.Nonce,
})
s.db.journal.nonceChange(s.address, s.data.Nonce)
if s.db.logger != nil && s.db.logger.OnNonceChange != nil {
s.db.logger.OnNonceChange(s.address, s.data.Nonce, nonce)
}
Expand Down
Loading

0 comments on commit 0e5546f

Please sign in to comment.