core/tracing: simplify journal implementation

This commit is contained in:
Felix Lange 2025-02-05 00:32:55 +01:00
parent 4ba05e9aa5
commit 2795c0e23b

View file

@ -19,7 +19,6 @@ package tracing
import ( import (
"fmt" "fmt"
"math/big" "math/big"
"sort"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
@ -30,20 +29,12 @@ const (
CREATE2 = 0xf5 CREATE2 = 0xf5
) )
type revision struct {
id int
journalIndex int
}
// journal is a state change journal to be wrapped around a tracer. // journal is a state change journal to be wrapped around a tracer.
// It will emit the state change hooks with reverse values when a call reverts. // It will emit the state change hooks with reverse values when a call reverts.
type journal struct { type journal struct {
entries []entry hooks *Hooks
hooks *Hooks entries []entry
revisions []int
validRevisions []revision
nextRevisionId int
revIds []int
} }
type entry interface { type entry interface {
@ -64,9 +55,10 @@ func WrapWithJournal(hooks *Hooks) (*Hooks, error) {
} }
// Create a new Hooks instance and copy all hooks // Create a new Hooks instance and copy all hooks
wrapped := hooks.copy() wrapped := *hooks
// Create journal // Create journal
j := &journal{entries: make([]entry, 0), hooks: hooks} j := &journal{hooks: hooks}
// Scope hooks need to be re-implemented. // Scope hooks need to be re-implemented.
wrapped.OnTxEnd = j.OnTxEnd wrapped.OnTxEnd = j.OnTxEnd
wrapped.OnEnter = j.OnEnter wrapped.OnEnter = j.OnEnter
@ -89,7 +81,7 @@ func WrapWithJournal(hooks *Hooks) (*Hooks, error) {
wrapped.OnStorageChange = j.OnStorageChange wrapped.OnStorageChange = j.OnStorageChange
} }
return wrapped, nil return &wrapped, nil
} }
// reset clears the journal, after this operation the journal can be used anew. // reset clears the journal, after this operation the journal can be used anew.
@ -97,48 +89,34 @@ func WrapWithJournal(hooks *Hooks) (*Hooks, error) {
// can be reused. // can be reused.
func (j *journal) reset() { func (j *journal) reset() {
j.entries = j.entries[:0] j.entries = j.entries[:0]
j.validRevisions = j.validRevisions[:0] j.revisions = j.revisions[:0]
j.nextRevisionId = 0
} }
// snapshot returns an identifier for the current revision of the state. // snapshot records a revision and stores it to the revision stack.
func (j *journal) snapshot() int { func (j *journal) snapshot() {
id := j.nextRevisionId rev := len(j.entries)
j.nextRevisionId++ j.revisions = append(j.revisions, rev)
j.validRevisions = append(j.validRevisions, revision{id, j.length()})
return id
} }
// revertToSnapshot reverts all state changes made since the given revision. // revert reverts all state changes up to the last tracked revision.
func (j *journal) revertToSnapshot(revid int, hooks *Hooks) { func (j *journal) revert(hooks *Hooks) {
// Find the snapshot in the stack of valid snapshots. // Replay the journal entries above the last revision to undo changes,
idx := sort.Search(len(j.validRevisions), func(i int) bool { // then remove the reverted changes from the journal.
return j.validRevisions[i].id >= revid rev := j.revisions[len(j.revisions)-1]
}) for i := len(j.entries) - 1; i >= rev; i-- {
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(hooks, snapshot)
j.validRevisions = j.validRevisions[:idx]
}
// revert undoes a batch of journaled modifications.
func (j *journal) revert(hooks *Hooks, snapshot int) {
for i := len(j.entries) - 1; i >= snapshot; i-- {
// Undo the changes made by the operation
j.entries[i].revert(hooks) j.entries[i].revert(hooks)
} }
j.entries = j.entries[:snapshot] j.entries = j.entries[:rev]
j.popRevision()
} }
// length returns the current number of entries in the journal. // popRevision removes an item from the revision stack. This basically forgets about
func (j *journal) length() int { // the last call to snapshot() and moves to the one prior.
return len(j.entries) func (j *journal) popRevision() {
j.revisions = j.revisions[:len(j.revisions)-1]
} }
// OnTxEnd resets the journal since each transaction has its own EVM call stack.
func (j *journal) OnTxEnd(receipt *types.Receipt, err error) { func (j *journal) OnTxEnd(receipt *types.Receipt, err error) {
j.reset() j.reset()
if j.hooks.OnTxEnd != nil { if j.hooks.OnTxEnd != nil {
@ -146,18 +124,22 @@ func (j *journal) OnTxEnd(receipt *types.Receipt, err error) {
} }
} }
// OnEnter is invoked for each EVM call frame and records a journal revision.
func (j *journal) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { func (j *journal) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
j.revIds = append(j.revIds, j.snapshot()) j.snapshot()
if j.hooks.OnEnter != nil { if j.hooks.OnEnter != nil {
j.hooks.OnEnter(depth, typ, from, to, input, gas, value) j.hooks.OnEnter(depth, typ, from, to, input, gas, value)
} }
} }
// OnExit is invoked when an EVM call frame ends.
// If the call has reverted, all state changes made by that frame are undone.
// If the call did not revert, we forget about changes in that revision.
func (j *journal) OnExit(depth int, output []byte, gasUsed uint64, err error, reverted bool) { func (j *journal) OnExit(depth int, output []byte, gasUsed uint64, err error, reverted bool) {
revId := j.revIds[len(j.revIds)-1]
j.revIds = j.revIds[:len(j.revIds)-1]
if reverted { if reverted {
j.revertToSnapshot(revId, j.hooks) j.revert(j.hooks)
} else {
j.popRevision()
} }
if j.hooks.OnExit != nil { if j.hooks.OnExit != nil {
j.hooks.OnExit(depth, output, gasUsed, err, reverted) j.hooks.OnExit(depth, output, gasUsed, err, reverted)