Skip to content

Commit

Permalink
core/state: simplify journal api, remove externally visible snapshot ids
Browse files Browse the repository at this point in the history
  • Loading branch information
holiman committed Nov 6, 2024
1 parent 83b5d93 commit e896dac
Show file tree
Hide file tree
Showing 16 changed files with 186 additions and 216 deletions.
2 changes: 1 addition & 1 deletion cmd/evm/internal/t8ntool/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ func (pre *Prestate) Apply(vmConfig vm.Config, chainConfig *params.ChainConfig,
// (ret []byte, usedGas uint64, failed bool, err error)
msgResult, err := core.ApplyMessage(evm, msg, gaspool)
if err != nil {
statedb.RevertToSnapshot(snapshot)
statedb.RevertSnapshot(snapshot)
log.Info("rejected tx", "index", i, "hash", tx.Hash(), "from", msg.From, "error", err)
rejectedTxs = append(rejectedTxs, &rejectedTx{i, err.Error()})
gaspool.SetGas(prevGas)
Expand Down
12 changes: 6 additions & 6 deletions core/state/journal.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,23 @@ import (
)

type journal interface {
// snapshot returns an identifier for the current revision of the state.
// snapshot starts a new journal scope which can be reverted or discarded.
// The lifeycle of journalling is as follows:
// - snapshot() starts a 'scope'.
// - The method snapshot() may be called any number of times.
// - For each call to snapshot, there should be a corresponding call to end
// the scope via either of:
// - revertToSnapshot, which undoes the changes in the scope, or
// - discardSnapshot, which discards the ability to revert the changes in the scope.
snapshot() int
snapshot()

// revertToSnapshot reverts all state changes made since the given revision.
revertToSnapshot(revid int, s *StateDB)
// revertSnapshot reverts all state changes made since the last call to snapshot().
revertSnapshot(s *StateDB)

// discardSnapshot removes the snapshot with the given id; after calling this
// discardSnapshot removes the latest snapshot; after calling this
// method, it is no longer possible to revert to that particular snapshot, the
// changes are considered part of the parent scope.
discardSnapshot(revid int)
discardSnapshot()

// reset clears the journal so it can be reused.
reset()
Expand Down
62 changes: 26 additions & 36 deletions core/state/journal_linear.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,14 @@
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 linear journal that can be
// reverted on demand.
type journalEntry interface {
Expand All @@ -52,8 +45,7 @@ type linearJournal struct {
entries []journalEntry // Current changes tracked by the linearJournal
dirties map[common.Address]int // Dirty accounts and the number of changes

validRevisions []revision
nextRevisionId int
revisions []int // sequence of indexes to points in time designating snapshots
}

// compile-time interface check
Expand All @@ -71,9 +63,8 @@ func newLinearJournal() *linearJournal {
// can be reused.
func (j *linearJournal) reset() {
j.entries = j.entries[:0]
j.validRevisions = j.validRevisions[:0]
j.revisions = j.revisions[:0]
clear(j.dirties)
j.nextRevisionId = 0
}

func (j linearJournal) dirtyAccounts() []common.Address {
Expand All @@ -85,33 +76,33 @@ func (j linearJournal) dirtyAccounts() []common.Address {
return dirty
}

// snapshot returns an identifier for the current revision of the state.
func (j *linearJournal) snapshot() int {
id := j.nextRevisionId
j.nextRevisionId++
j.validRevisions = append(j.validRevisions, revision{id, j.length()})
return id
// snapshot starts a new journal scope which can be reverted or discarded.
func (j *linearJournal) snapshot() {
j.revisions = append(j.revisions, len(j.entries))
}

func (j *linearJournal) 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 (valid revisions: %d)", revid, len(j.validRevisions)))
}
snapshot := j.validRevisions[idx].journalIndex

// revertSnapshot reverts all state changes made since the last call to snapshot().
func (j *linearJournal) revertSnapshot(s *StateDB) {
id := len(j.revisions) - 1
revision := j.revisions[id]
// Replay the linearJournal to undo changes and remove invalidated snapshots
j.revert(s, snapshot)
j.validRevisions = j.validRevisions[:idx]
j.revertTo(s, revision)
j.revisions = j.revisions[:id]
}

// discardSnapshot removes the snapshot with the given id; after calling this
// discardSnapshot removes the latest snapshot; after calling this
// method, it is no longer possible to revert to that particular snapshot, the
// changes are considered part of the parent scope.
func (j *linearJournal) discardSnapshot(id int) {
func (j *linearJournal) discardSnapshot() {
id := len(j.revisions) - 1
if id == 0 {
// If a transaction is applied successfully, the statedb.Finalize will
// end by clearing and resetting the journal. Invoking a discardSnapshot
// afterwards will land here: calling discard on an empty journal.
// This is fine
return
}
j.revisions = j.revisions[:id]
}

// append inserts a new modification entry to the end of the change linearJournal.
Expand All @@ -124,7 +115,7 @@ func (j *linearJournal) append(entry journalEntry) {

// revert undoes a batch of journalled modifications along with any reverted
// dirty handling too.
func (j *linearJournal) revert(statedb *StateDB, snapshot int) {
func (j *linearJournal) revertTo(statedb *StateDB, snapshot int) {
for i := len(j.entries) - 1; i >= snapshot; i-- {
// Undo the changes made by the operation
j.entries[i].revert(statedb)
Expand Down Expand Up @@ -158,10 +149,9 @@ func (j *linearJournal) copy() journal {
entries = append(entries, j.entries[i].copy())
}
return &linearJournal{
entries: entries,
dirties: maps.Clone(j.dirties),
validRevisions: slices.Clone(j.validRevisions),
nextRevisionId: j.nextRevisionId,
entries: entries,
dirties: maps.Clone(j.dirties),
revisions: slices.Clone(j.revisions),
}
}

Expand Down
47 changes: 14 additions & 33 deletions core/state/journal_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,11 @@ package state

import (
"bytes"
"fmt"
"maps"
"slices"

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

Expand Down Expand Up @@ -349,51 +347,34 @@ func (j *sparseJournal) copy() journal {
return cp
}

// snapshot returns an identifier for the current revision of the state.
// snapshot starts a new journal scope which can be reverted or discarded.
// OBS: A call to Snapshot is _required_ in order to initialize the journalling,
// invoking the journal-methods without having invoked Snapshot will lead to
// panic.
func (j *sparseJournal) snapshot() int {
id := len(j.entries)
func (j *sparseJournal) snapshot() {
j.entries = append(j.entries, newScopedJournal())
return id
}

// revertToSnapshot reverts all state changes made since the given revision.
func (j *sparseJournal) revertToSnapshot(id int, s *StateDB) {
if id >= len(j.entries) {
panic(fmt.Errorf("revision id %v cannot be reverted", id))
}
// Revert the entries sequentially
for i := len(j.entries) - 1; i >= id; i-- {
entry := j.entries[i]
entry.revert(s)
}
// revertSnapshot reverts all state changes made since the last call to snapshot().
func (j *sparseJournal) revertSnapshot(s *StateDB) {
id := len(j.entries) - 1
j.entries[id].revert(s)
j.entries = j.entries[:id]
}

// discardSnapshot removes the snapshot with the given id; after calling this
// discardSnapshot removes the latest snapshot; after calling this
// method, it is no longer possible to revert to that particular snapshot, the
// changes are considered part of the parent scope.
func (j *sparseJournal) discardSnapshot(id int) {
func (j *sparseJournal) discardSnapshot() {
id := len(j.entries) - 1
// here we must merge the 'id' with it's parent.
if id == 0 {
// If a transaction is applied successfully, the statedb.Finalize will
// end by clearing and resetting the journal. Invoking a discardSnapshot
// afterwards will land here: calling discard on an empty journal.
// This is fine
return
}
// here we must merge the 'id' with it's parent.
want := len(j.entries) - 1
have := id
if want != have {
if want == 0 && id == 1 {
// If a transcation is applied successfully, the statedb.Finalize will
// end by clearing and resetting the journal. Invoking a discardSnapshot
// afterwards will lead us here.
// Let's not panic, but it's ok to complain a bit
log.Error("Extraneous invocation to discard snapshot")
return
} else {
panic(fmt.Sprintf("journalling error, want discard(%d), have discard(%d)", want, have))
}
}
entry := j.entries[id]
parent := j.entries[id-1]
entry.merge(parent)
Expand Down
44 changes: 23 additions & 21 deletions core/state/journal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,23 +75,23 @@ func testJournalAccessList(t *testing.T, j journal) {
statedb.accessList = newAccessList()
statedb.journal = j

j.snapshot()
{
// If the journal performs the rollback in the wrong order, this
// will cause a panic.
id := j.snapshot()
statedb.AddSlotToAccessList(common.Address{0x1}, common.Hash{0x4})
statedb.AddSlotToAccessList(common.Address{0x3}, common.Hash{0x4})
statedb.RevertToSnapshot(id)
}
statedb.RevertSnapshot()
j.snapshot()
{
id := j.snapshot()
statedb.AddAddressToAccessList(common.Address{0x2})
statedb.AddAddressToAccessList(common.Address{0x3})
statedb.AddAddressToAccessList(common.Address{0x4})
statedb.RevertToSnapshot(id)
if statedb.accessList.ContainsAddress(common.Address{0x2}) {
t.Fatal("should be missing")
}
}
statedb.RevertSnapshot()
if statedb.accessList.ContainsAddress(common.Address{0x2}) {
t.Fatal("should be missing")
}
}

Expand All @@ -107,25 +107,27 @@ func testJournalRefunds(t *testing.T, j journal) {
var statedb = &StateDB{}
statedb.accessList = newAccessList()
statedb.journal = j
zero := j.snapshot()
j.refundChange(0)
j.refundChange(1)
j.snapshot()
{
id := j.snapshot()
j.refundChange(2)
j.refundChange(3)
j.revertToSnapshot(id, statedb)
j.refundChange(0)
j.refundChange(1)
j.snapshot()
{
j.refundChange(2)
j.refundChange(3)
}
j.revertSnapshot(statedb)
if have, want := statedb.refund, uint64(2); have != want {
t.Fatalf("have %d want %d", have, want)
}
j.snapshot()
{
j.refundChange(2)
j.refundChange(3)
}
j.discardSnapshot()
}
{
id := j.snapshot()
j.refundChange(2)
j.refundChange(3)
j.discardSnapshot(id)
}
j.revertToSnapshot(zero, statedb)
j.revertSnapshot(statedb)
if have, want := statedb.refund, uint64(0); have != want {
t.Fatalf("have %d want %d", have, want)
}
Expand Down
59 changes: 31 additions & 28 deletions core/state/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,25 +160,27 @@ func TestSnapshot(t *testing.T) {
s := newStateEnv()

// snapshot the genesis state
genesis := s.state.Snapshot()

// set initial state object value
s.state.SetState(stateobjaddr, storageaddr, data1)
snapshot := s.state.Snapshot()

// set a new state object value, revert it and ensure correct content
s.state.SetState(stateobjaddr, storageaddr, data2)
s.state.RevertToSnapshot(snapshot)

if v := s.state.GetState(stateobjaddr, storageaddr); v != data1 {
t.Errorf("wrong storage value %v, want %v", v, data1)
}
if v := s.state.GetCommittedState(stateobjaddr, storageaddr); v != (common.Hash{}) {
t.Errorf("wrong committed storage value %v, want %v", v, common.Hash{})
s.state.Snapshot()
{
// set initial state object value
s.state.SetState(stateobjaddr, storageaddr, data1)
s.state.Snapshot()
{
// set a new state object value, revert it and ensure correct content
s.state.SetState(stateobjaddr, storageaddr, data2)

}
s.state.RevertSnapshot()

if v := s.state.GetState(stateobjaddr, storageaddr); v != data1 {
t.Errorf("wrong storage value %v, want %v", v, data1)
}
if v := s.state.GetCommittedState(stateobjaddr, storageaddr); v != (common.Hash{}) {
t.Errorf("wrong committed storage value %v, want %v", v, common.Hash{})
}
}

// revert up to the genesis state and ensure correct content
s.state.RevertToSnapshot(genesis)
s.state.RevertSnapshot()
if v := s.state.GetState(stateobjaddr, storageaddr); v != (common.Hash{}) {
t.Errorf("wrong storage value %v, want %v", v, common.Hash{})
}
Expand All @@ -189,22 +191,23 @@ func TestSnapshot(t *testing.T) {

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

func TestCreateObjectRevert(t *testing.T) {
state, _ := New(types.EmptyRootHash, NewDatabaseForTesting())
addr := common.BytesToAddress([]byte("so0"))
snap := state.Snapshot()

state.CreateAccount(addr)
so0 := state.getStateObject(addr)
so0.SetBalance(uint256.NewInt(42))
so0.SetNonce(43)
so0.SetCode(crypto.Keccak256Hash([]byte{'c', 'a', 'f', 'e'}), []byte{'c', 'a', 'f', 'e'})
state.setStateObject(so0)

state.RevertToSnapshot(snap)
state.Snapshot()
{
state.CreateAccount(addr)
so0 := state.getStateObject(addr)
so0.SetBalance(uint256.NewInt(42))
so0.SetNonce(43)
so0.SetCode(crypto.Keccak256Hash([]byte{'c', 'a', 'f', 'e'}), []byte{'c', 'a', 'f', 'e'})
state.setStateObject(so0)
}
state.RevertSnapshot()
if state.Exist(addr) {
t.Error("Unexpected account after revert")
}
Expand Down
Loading

0 comments on commit e896dac

Please sign in to comment.