core/tracing: state journal wrapper (#30441)

Here we add some more changes for live tracing API v1.1:

- Hook `OnSystemCallStartV2` was introduced with `VMContext` as parameter.
- Hook `OnBlockHashRead` was introduced.
- `GetCodeHash` was added to the state interface
- The new `WrapWithJournal` construction helps with tracking EVM reverts in the tracer.

---------

Co-authored-by: Felix Lange <fjl@twurst.com>
This commit is contained in:
Sina M 2025-02-05 13:58:25 +01:00 committed by GitHub
parent ed1d46b3d3
commit aaaf01d712
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 709 additions and 32 deletions

View File

@ -419,7 +419,7 @@ func MakePreState(db ethdb.Database, accounts types.GenesisAlloc) *state.StateDB
statedb, _ := state.New(types.EmptyRootHash, sdb)
for addr, a := range accounts {
statedb.SetCode(addr, a.Code)
statedb.SetNonce(addr, a.Nonce)
statedb.SetNonce(addr, a.Nonce, tracing.NonceChangeGenesis)
statedb.SetBalance(addr, uint256.MustFromBig(a.Balance), tracing.BalanceIncreaseGenesisBalance)
for k, v := range a.Storage {
statedb.SetState(addr, k, v)

View File

@ -154,7 +154,7 @@ func hashAlloc(ga *types.GenesisAlloc, isVerkle bool) (common.Hash, error) {
statedb.AddBalance(addr, uint256.MustFromBig(account.Balance), tracing.BalanceIncreaseGenesisBalance)
}
statedb.SetCode(addr, account.Code)
statedb.SetNonce(addr, account.Nonce)
statedb.SetNonce(addr, account.Nonce, tracing.NonceChangeGenesis)
for key, value := range account.Storage {
statedb.SetState(addr, key, value)
}
@ -180,7 +180,7 @@ func flushAlloc(ga *types.GenesisAlloc, triedb *triedb.Database) (common.Hash, e
statedb.AddBalance(addr, uint256.MustFromBig(account.Balance), tracing.BalanceIncreaseGenesisBalance)
}
statedb.SetCode(addr, account.Code)
statedb.SetNonce(addr, account.Nonce)
statedb.SetNonce(addr, account.Nonce, tracing.NonceChangeGenesis)
for key, value := range account.Storage {
statedb.SetState(addr, key, value)
}

View File

@ -432,7 +432,7 @@ func (s *StateDB) SetBalance(addr common.Address, amount *uint256.Int, reason tr
}
}
func (s *StateDB) SetNonce(addr common.Address, nonce uint64) {
func (s *StateDB) SetNonce(addr common.Address, nonce uint64, reason tracing.NonceChangeReason) {
stateObject := s.getOrNewStateObject(addr)
if stateObject != nil {
stateObject.SetNonce(nonce)

View File

@ -69,7 +69,7 @@ func newStateTestAction(addr common.Address, r *rand.Rand, index int) testAction
{
name: "SetNonce",
fn: func(a testAction, s *StateDB) {
s.SetNonce(addr, uint64(a.args[0]))
s.SetNonce(addr, uint64(a.args[0]), tracing.NonceChangeUnspecified)
},
args: make([]int64, 1),
},

View File

@ -179,10 +179,13 @@ func (s *hookedStateDB) AddBalance(addr common.Address, amount *uint256.Int, rea
return prev
}
func (s *hookedStateDB) SetNonce(address common.Address, nonce uint64) {
s.inner.SetNonce(address, nonce)
if s.hooks.OnNonceChange != nil {
s.hooks.OnNonceChange(address, nonce-1, nonce)
func (s *hookedStateDB) SetNonce(address common.Address, nonce uint64, reason tracing.NonceChangeReason) {
prev := s.inner.GetNonce(address)
s.inner.SetNonce(address, nonce, reason)
if s.hooks.OnNonceChangeV2 != nil {
s.hooks.OnNonceChangeV2(address, prev, nonce, reason)
} else if s.hooks.OnNonceChange != nil {
s.hooks.OnNonceChange(address, prev, nonce)
}
}

View File

@ -85,7 +85,7 @@ func TestHooks(t *testing.T) {
var wants = []string{
"0xaa00000000000000000000000000000000000000.balance: 0->100 (BalanceChangeUnspecified)",
"0xaa00000000000000000000000000000000000000.balance: 100->50 (BalanceChangeTransfer)",
"0xaa00000000000000000000000000000000000000.nonce: 1336->1337",
"0xaa00000000000000000000000000000000000000.nonce: 0->1337",
"0xaa00000000000000000000000000000000000000.code: (0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470) ->0x1325 (0xa12ae05590de0c93a00bc7ac773c2fdb621e44f814985e72194f921c0050f728)",
"0xaa00000000000000000000000000000000000000.storage slot 0x0000000000000000000000000000000000000000000000000000000000000001: 0x0000000000000000000000000000000000000000000000000000000000000000 ->0x0000000000000000000000000000000000000000000000000000000000000011",
"0xaa00000000000000000000000000000000000000.storage slot 0x0000000000000000000000000000000000000000000000000000000000000001: 0x0000000000000000000000000000000000000000000000000000000000000011 ->0x0000000000000000000000000000000000000000000000000000000000000022",
@ -113,7 +113,7 @@ func TestHooks(t *testing.T) {
})
sdb.AddBalance(common.Address{0xaa}, uint256.NewInt(100), tracing.BalanceChangeUnspecified)
sdb.SubBalance(common.Address{0xaa}, uint256.NewInt(50), tracing.BalanceChangeTransfer)
sdb.SetNonce(common.Address{0xaa}, 1337)
sdb.SetNonce(common.Address{0xaa}, 1337, tracing.NonceChangeGenesis)
sdb.SetCode(common.Address{0xaa}, []byte{0x13, 37})
sdb.SetState(common.Address{0xaa}, common.HexToHash("0x01"), common.HexToHash("0x11"))
sdb.SetState(common.Address{0xaa}, common.HexToHash("0x01"), common.HexToHash("0x22"))

View File

@ -60,7 +60,7 @@ func TestUpdateLeaks(t *testing.T) {
for i := byte(0); i < 255; i++ {
addr := common.BytesToAddress([]byte{i})
state.AddBalance(addr, uint256.NewInt(uint64(11*i)), tracing.BalanceChangeUnspecified)
state.SetNonce(addr, uint64(42*i))
state.SetNonce(addr, uint64(42*i), tracing.NonceChangeUnspecified)
if i%2 == 0 {
state.SetState(addr, common.BytesToHash([]byte{i, i, i}), common.BytesToHash([]byte{i, i, i, i}))
}
@ -95,7 +95,7 @@ func TestIntermediateLeaks(t *testing.T) {
modify := func(state *StateDB, addr common.Address, i, tweak byte) {
state.SetBalance(addr, uint256.NewInt(uint64(11*i)+uint64(tweak)), tracing.BalanceChangeUnspecified)
state.SetNonce(addr, uint64(42*i+tweak))
state.SetNonce(addr, uint64(42*i+tweak), tracing.NonceChangeUnspecified)
if i%2 == 0 {
state.SetState(addr, common.Hash{i, i, i, 0}, common.Hash{})
state.SetState(addr, common.Hash{i, i, i, tweak}, common.Hash{i, i, i, i, tweak})
@ -357,7 +357,7 @@ func newTestAction(addr common.Address, r *rand.Rand) testAction {
{
name: "SetNonce",
fn: func(a testAction, s *StateDB) {
s.SetNonce(addr, uint64(a.args[0]))
s.SetNonce(addr, uint64(a.args[0]), tracing.NonceChangeUnspecified)
},
args: make([]int64, 1),
},

View File

@ -487,7 +487,7 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
ret, _, st.gasRemaining, vmerr = st.evm.Create(sender, msg.Data, st.gasRemaining, value)
} else {
// Increment the nonce for the next transaction.
st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1)
st.state.SetNonce(msg.From, st.state.GetNonce(msg.From)+1, tracing.NonceChangeEoACall)
// Apply EIP-7702 authorizations.
if msg.SetCodeAuthorizations != nil {
@ -602,7 +602,7 @@ func (st *stateTransition) applyAuthorization(auth *types.SetCodeAuthorization)
}
// Update nonce and account code.
st.state.SetNonce(authority, auth.Nonce+1)
st.state.SetNonce(authority, auth.Nonce+1, tracing.NonceChangeAuthorization)
if auth.Address == (common.Address{}) {
// Delegation to zero address means clear.
st.state.SetCode(authority, nil)

View File

@ -4,6 +4,53 @@ All notable changes to the tracing interface will be documented in this file.
## [Unreleased]
The tracing interface has been extended with backwards-compatible changes to support more use-cases and simplify tracer code. The most notable change is a state journaling library which emits reverse events when a call is reverted.
### Deprecated methods
- `OnSystemCallStart()`: This hook is deprecated in favor of `OnSystemCallStartV2(vm *VMContext)`.
- `OnNonceChange(addr common.Address, prev, new uint64)`: This hook is deprecated in favor of `OnNonceChangeV2(addr common.Address, prev, new uint64, reason NonceChangeReason)`.
### New methods
- `OnBlockHashRead(blockNum uint64, hash common.Hash)`: This hook is called when a block hash is read by EVM.
- `OnSystemCallStartV2(vm *VMContext)`. This allows access to EVM context during system calls. It is a successor to `OnSystemCallStart`.
- `OnNonceChangeV2(addr common.Address, prev, new uint64, reason NonceChangeReason)`: This hook is called when a nonce change occurs. It is a successor to `OnNonceChange`.
### New types
- `NonceChangeReason` is a new type used to provide a reason for nonce changes. Notably it includes `NonceChangeRevert` which will be emitted by the state journaling library when a nonce change is due to a revert.
### Modified types
- `VMContext.StateDB` has been extended with `GetCodeHash(addr common.Address) common.Hash` method used to retrieve the code hash an account.
- `BalanceChangeReason` has been extended with the `BalanceChangeRevert` reason. More on that below.
### State journaling
Tracers receive state changes events from the node. The tracer was so far expected to keep track of modified accounts and slots and revert those changes when a call frame failed. Now a utility tracer wrapper is provided which will emit "reverse change" events when a call frame fails. To use this feature the hooks have to be wrapped prior to registering the tracer. The following example demonstrates how to use the state journaling library:
```go
func init() {
tracers.LiveDirectory.Register("test", func (cfg json.RawMessage) (*tracing.Hooks, error) {
hooks, err := newTestTracer(cfg)
if err != nil {
return nil, err
}
return tracing.WrapWithJournal(hooks)
})
}
```
The state changes that are covered by the journaling library are:
- `OnBalanceChange`. Note that `OnBalanceChange` will carry the `BalanceChangeRevert` reason.
- `OnNonceChange`, `OnNonceChangeV2`
- `OnCodeChange`
- `OnStorageChange`
## [v1.14.9](https://github.com/ethereum/go-ethereum/releases/tag/v1.14.9)
### Modified types
- `GasChangeReason` has been extended with the following reasons which will be enabled only post-Verkle. There shouldn't be any gas changes with those reasons prior to the fork.

View File

@ -23,11 +23,12 @@ func _() {
_ = x[BalanceIncreaseSelfdestruct-12]
_ = x[BalanceDecreaseSelfdestruct-13]
_ = x[BalanceDecreaseSelfdestructBurn-14]
_ = x[BalanceChangeRevert-15]
}
const _BalanceChangeReason_name = "BalanceChangeUnspecifiedBalanceIncreaseRewardMineUncleBalanceIncreaseRewardMineBlockBalanceIncreaseWithdrawalBalanceIncreaseGenesisBalanceBalanceIncreaseRewardTransactionFeeBalanceDecreaseGasBuyBalanceIncreaseGasReturnBalanceIncreaseDaoContractBalanceDecreaseDaoAccountBalanceChangeTransferBalanceChangeTouchAccountBalanceIncreaseSelfdestructBalanceDecreaseSelfdestructBalanceDecreaseSelfdestructBurn"
const _BalanceChangeReason_name = "BalanceChangeUnspecifiedBalanceIncreaseRewardMineUncleBalanceIncreaseRewardMineBlockBalanceIncreaseWithdrawalBalanceIncreaseGenesisBalanceBalanceIncreaseRewardTransactionFeeBalanceDecreaseGasBuyBalanceIncreaseGasReturnBalanceIncreaseDaoContractBalanceDecreaseDaoAccountBalanceChangeTransferBalanceChangeTouchAccountBalanceIncreaseSelfdestructBalanceDecreaseSelfdestructBalanceDecreaseSelfdestructBurnBalanceChangeRevert"
var _BalanceChangeReason_index = [...]uint16{0, 24, 54, 84, 109, 138, 173, 194, 218, 244, 269, 290, 315, 342, 369, 400}
var _BalanceChangeReason_index = [...]uint16{0, 24, 54, 84, 109, 138, 173, 194, 218, 244, 269, 290, 315, 342, 369, 400, 419}
func (i BalanceChangeReason) String() string {
if i >= BalanceChangeReason(len(_BalanceChangeReason_index)-1) {

View File

@ -14,6 +14,14 @@
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
// Package tracing defines hooks for 'live tracing' of block processing and transaction
// execution. Here we define the low-level [Hooks] object that carries hooks which are
// invoked by the go-ethereum core at various points in the state transition.
//
// To create a tracer that can be invoked with Geth, you need to register it using
// [github.com/ethereum/go-ethereum/eth/tracers.LiveDirectory.Register].
//
// See https://geth.ethereum.org/docs/developers/evm-tracing/live-tracing for a tutorial.
package tracing
import (
@ -163,6 +171,9 @@ type (
// NonceChangeHook is called when the nonce of an account changes.
NonceChangeHook = func(addr common.Address, prev, new uint64)
// NonceChangeHookV2 is called when the nonce of an account changes.
NonceChangeHookV2 = func(addr common.Address, prev, new uint64, reason NonceChangeReason)
// CodeChangeHook is called when the code of an account changes.
CodeChangeHook = func(addr common.Address, prevCodeHash common.Hash, prevCode []byte, codeHash common.Hash, code []byte)
@ -171,6 +182,9 @@ type (
// LogHook is called when a log is emitted.
LogHook = func(log *types.Log)
// BlockHashReadHook is called when EVM reads the blockhash of a block.
BlockHashReadHook = func(blockNumber uint64, hash common.Hash)
)
type Hooks struct {
@ -195,9 +209,12 @@ type Hooks struct {
// State events
OnBalanceChange BalanceChangeHook
OnNonceChange NonceChangeHook
OnNonceChangeV2 NonceChangeHookV2
OnCodeChange CodeChangeHook
OnStorageChange StorageChangeHook
OnLog LogHook
// Block hash read
OnBlockHashRead BlockHashReadHook
}
// BalanceChangeReason is used to indicate the reason for a balance change, useful
@ -249,6 +266,10 @@ const (
// account within the same tx (captured at end of tx).
// Note it doesn't account for a self-destruct which appoints itself as recipient.
BalanceDecreaseSelfdestructBurn BalanceChangeReason = 14
// BalanceChangeRevert is emitted when the balance is reverted back to a previous value due to call failure.
// It is only emitted when the tracer has opted in to use the journaling wrapper (WrapWithJournal).
BalanceChangeRevert BalanceChangeReason = 15
)
// GasChangeReason is used to indicate the reason for a gas change, useful
@ -321,3 +342,29 @@ const (
// it will be "manually" tracked by a direct emit of the gas change event.
GasChangeIgnored GasChangeReason = 0xFF
)
// NonceChangeReason is used to indicate the reason for a nonce change.
type NonceChangeReason byte
const (
NonceChangeUnspecified NonceChangeReason = 0
// NonceChangeGenesis is the nonce allocated to accounts at genesis.
NonceChangeGenesis NonceChangeReason = 1
// NonceChangeEoACall is the nonce change due to an EoA call.
NonceChangeEoACall NonceChangeReason = 2
// NonceChangeContractCreator is the nonce change of an account creating a contract.
NonceChangeContractCreator NonceChangeReason = 3
// NonceChangeNewContract is the nonce change of a newly created contract.
NonceChangeNewContract NonceChangeReason = 4
// NonceChangeTransaction is the nonce change due to a EIP-7702 authorization.
NonceChangeAuthorization NonceChangeReason = 5
// NonceChangeRevert is emitted when the nonce is reverted back to a previous value due to call failure.
// It is only emitted when the tracer has opted in to use the journaling wrapper (WrapWithJournal).
NonceChangeRevert NonceChangeReason = 6
)

237
core/tracing/journal.go Normal file
View File

@ -0,0 +1,237 @@
// Copyright 2025 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package tracing
import (
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
// 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.
type journal struct {
hooks *Hooks
entries []entry
revisions []int
}
type entry interface {
revert(tracer *Hooks)
}
// WrapWithJournal wraps the given tracer with a journaling layer.
func WrapWithJournal(hooks *Hooks) (*Hooks, error) {
if hooks == nil {
return nil, fmt.Errorf("wrapping nil tracer")
}
// No state change to journal, return the wrapped hooks as is
if hooks.OnBalanceChange == nil && hooks.OnNonceChange == nil && hooks.OnNonceChangeV2 == nil && hooks.OnCodeChange == nil && hooks.OnStorageChange == nil {
return hooks, nil
}
if hooks.OnNonceChange != nil && hooks.OnNonceChangeV2 != nil {
return nil, fmt.Errorf("cannot have both OnNonceChange and OnNonceChangeV2")
}
// Create a new Hooks instance and copy all hooks
wrapped := *hooks
// Create journal
j := &journal{hooks: hooks}
// Scope hooks need to be re-implemented.
wrapped.OnTxEnd = j.OnTxEnd
wrapped.OnEnter = j.OnEnter
wrapped.OnExit = j.OnExit
// Wrap state change hooks.
if hooks.OnBalanceChange != nil {
wrapped.OnBalanceChange = j.OnBalanceChange
}
if hooks.OnNonceChange != nil || hooks.OnNonceChangeV2 != nil {
// Regardless of which hook version is used in the tracer,
// the journal will want to capture the nonce change reason.
wrapped.OnNonceChangeV2 = j.OnNonceChangeV2
// A precaution to ensure EVM doesn't call both hooks.
wrapped.OnNonceChange = nil
}
if hooks.OnCodeChange != nil {
wrapped.OnCodeChange = j.OnCodeChange
}
if hooks.OnStorageChange != nil {
wrapped.OnStorageChange = j.OnStorageChange
}
return &wrapped, nil
}
// 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.revisions = j.revisions[:0]
}
// snapshot records a revision and stores it to the revision stack.
func (j *journal) snapshot() {
rev := len(j.entries)
j.revisions = append(j.revisions, rev)
}
// revert reverts all state changes up to the last tracked revision.
func (j *journal) revert(hooks *Hooks) {
// Replay the journal entries above the last revision to undo changes,
// then remove the reverted changes from the journal.
rev := j.revisions[len(j.revisions)-1]
for i := len(j.entries) - 1; i >= rev; i-- {
j.entries[i].revert(hooks)
}
j.entries = j.entries[:rev]
j.popRevision()
}
// popRevision removes an item from the revision stack. This basically forgets about
// the last call to snapshot() and moves to the one prior.
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) {
j.reset()
if j.hooks.OnTxEnd != nil {
j.hooks.OnTxEnd(receipt, err)
}
}
// 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) {
j.snapshot()
if j.hooks.OnEnter != nil {
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) {
if reverted {
j.revert(j.hooks)
} else {
j.popRevision()
}
if j.hooks.OnExit != nil {
j.hooks.OnExit(depth, output, gasUsed, err, reverted)
}
}
func (j *journal) OnBalanceChange(addr common.Address, prev, new *big.Int, reason BalanceChangeReason) {
j.entries = append(j.entries, balanceChange{addr: addr, prev: prev, new: new})
if j.hooks.OnBalanceChange != nil {
j.hooks.OnBalanceChange(addr, prev, new, reason)
}
}
func (j *journal) OnNonceChangeV2(addr common.Address, prev, new uint64, reason NonceChangeReason) {
// When a contract is created, the nonce of the creator is incremented.
// This change is not reverted when the creation fails.
if reason != NonceChangeContractCreator {
j.entries = append(j.entries, nonceChange{addr: addr, prev: prev, new: new})
}
if j.hooks.OnNonceChangeV2 != nil {
j.hooks.OnNonceChangeV2(addr, prev, new, reason)
} else if j.hooks.OnNonceChange != nil {
j.hooks.OnNonceChange(addr, prev, new)
}
}
func (j *journal) OnCodeChange(addr common.Address, prevCodeHash common.Hash, prevCode []byte, codeHash common.Hash, code []byte) {
j.entries = append(j.entries, codeChange{
addr: addr,
prevCodeHash: prevCodeHash,
prevCode: prevCode,
newCodeHash: codeHash,
newCode: code,
})
if j.hooks.OnCodeChange != nil {
j.hooks.OnCodeChange(addr, prevCodeHash, prevCode, codeHash, code)
}
}
func (j *journal) OnStorageChange(addr common.Address, slot common.Hash, prev, new common.Hash) {
j.entries = append(j.entries, storageChange{addr: addr, slot: slot, prev: prev, new: new})
if j.hooks.OnStorageChange != nil {
j.hooks.OnStorageChange(addr, slot, prev, new)
}
}
type (
balanceChange struct {
addr common.Address
prev *big.Int
new *big.Int
}
nonceChange struct {
addr common.Address
prev uint64
new uint64
}
codeChange struct {
addr common.Address
prevCodeHash common.Hash
prevCode []byte
newCodeHash common.Hash
newCode []byte
}
storageChange struct {
addr common.Address
slot common.Hash
prev common.Hash
new common.Hash
}
)
func (b balanceChange) revert(hooks *Hooks) {
if hooks.OnBalanceChange != nil {
hooks.OnBalanceChange(b.addr, b.new, b.prev, BalanceChangeRevert)
}
}
func (n nonceChange) revert(hooks *Hooks) {
if hooks.OnNonceChangeV2 != nil {
hooks.OnNonceChangeV2(n.addr, n.new, n.prev, NonceChangeRevert)
} else if hooks.OnNonceChange != nil {
hooks.OnNonceChange(n.addr, n.new, n.prev)
}
}
func (c codeChange) revert(hooks *Hooks) {
if hooks.OnCodeChange != nil {
hooks.OnCodeChange(c.addr, c.newCodeHash, c.newCode, c.prevCodeHash, c.prevCode)
}
}
func (s storageChange) revert(hooks *Hooks) {
if hooks.OnStorageChange != nil {
hooks.OnStorageChange(s.addr, s.slot, s.new, s.prev)
}
}

View File

@ -0,0 +1,335 @@
// Copyright 2025 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package tracing
import (
"errors"
"math/big"
"reflect"
"testing"
"github.com/ethereum/go-ethereum/common"
)
type testTracer struct {
t *testing.T
bal *big.Int
nonce uint64
code []byte
storage map[common.Hash]common.Hash
}
func (t *testTracer) OnBalanceChange(addr common.Address, prev *big.Int, new *big.Int, reason BalanceChangeReason) {
t.t.Logf("OnBalanceChange(%v, %v -> %v, %v)", addr, prev, new, reason)
if t.bal != nil && t.bal.Cmp(prev) != 0 {
t.t.Errorf(" !! wrong prev balance (expected %v)", t.bal)
}
t.bal = new
}
func (t *testTracer) OnNonceChange(addr common.Address, prev uint64, new uint64) {
t.t.Logf("OnNonceChange(%v, %v -> %v)", addr, prev, new)
t.nonce = new
}
func (t *testTracer) OnNonceChangeV2(addr common.Address, prev uint64, new uint64, reason NonceChangeReason) {
t.t.Logf("OnNonceChangeV2(%v, %v -> %v, %v)", addr, prev, new, reason)
t.nonce = new
}
func (t *testTracer) OnCodeChange(addr common.Address, prevCodeHash common.Hash, prevCode []byte, codeHash common.Hash, code []byte) {
t.t.Logf("OnCodeChange(%v, %v -> %v)", addr, prevCodeHash, codeHash)
t.code = code
}
func (t *testTracer) OnStorageChange(addr common.Address, slot common.Hash, prev common.Hash, new common.Hash) {
t.t.Logf("OnStorageCodeChange(%v, %v, %v -> %v)", addr, slot, prev, new)
if t.storage == nil {
t.storage = make(map[common.Hash]common.Hash)
}
if new == (common.Hash{}) {
delete(t.storage, slot)
} else {
t.storage[slot] = new
}
}
func TestJournalIntegration(t *testing.T) {
tr := &testTracer{t: t}
wr, err := WrapWithJournal(&Hooks{OnBalanceChange: tr.OnBalanceChange, OnNonceChange: tr.OnNonceChange, OnCodeChange: tr.OnCodeChange, OnStorageChange: tr.OnStorageChange})
if err != nil {
t.Fatalf("failed to wrap test tracer: %v", err)
}
addr := common.HexToAddress("0x1234")
{
wr.OnEnter(0, 0, addr, addr, nil, 1000, big.NewInt(0))
wr.OnBalanceChange(addr, nil, big.NewInt(100), BalanceChangeUnspecified)
wr.OnCodeChange(addr, common.Hash{}, nil, common.Hash{}, []byte{1, 2, 3})
wr.OnStorageChange(addr, common.Hash{1}, common.Hash{}, common.Hash{2})
{
wr.OnEnter(1, 0, addr, addr, nil, 1000, big.NewInt(0))
wr.OnNonceChangeV2(addr, 0, 1, NonceChangeUnspecified)
wr.OnBalanceChange(addr, big.NewInt(100), big.NewInt(200), BalanceChangeUnspecified)
wr.OnBalanceChange(addr, big.NewInt(200), big.NewInt(250), BalanceChangeUnspecified)
wr.OnStorageChange(addr, common.Hash{1}, common.Hash{2}, common.Hash{3})
wr.OnStorageChange(addr, common.Hash{2}, common.Hash{}, common.Hash{4})
wr.OnExit(1, nil, 100, errors.New("revert"), true)
}
wr.OnExit(0, nil, 150, nil, false)
}
if tr.bal.Cmp(big.NewInt(100)) != 0 {
t.Fatalf("unexpected balance: %v", tr.bal)
}
if tr.nonce != 0 {
t.Fatalf("unexpected nonce: %v", tr.nonce)
}
if len(tr.code) != 3 {
t.Fatalf("unexpected code: %v", tr.code)
}
if len(tr.storage) != 1 {
t.Fatalf("unexpected storage len. want %d, have %d", 1, len(tr.storage))
}
if tr.storage[common.Hash{1}] != (common.Hash{2}) {
t.Fatalf("unexpected storage. want %v, have %v", common.Hash{2}, tr.storage[common.Hash{1}])
}
}
func TestJournalTopRevert(t *testing.T) {
tr := &testTracer{t: t}
wr, err := WrapWithJournal(&Hooks{OnBalanceChange: tr.OnBalanceChange, OnNonceChange: tr.OnNonceChange})
if err != nil {
t.Fatalf("failed to wrap test tracer: %v", err)
}
addr := common.HexToAddress("0x1234")
{
wr.OnEnter(0, 0, addr, addr, nil, 1000, big.NewInt(0))
wr.OnBalanceChange(addr, big.NewInt(0), big.NewInt(100), BalanceChangeUnspecified)
{
wr.OnEnter(1, 0, addr, addr, nil, 1000, big.NewInt(0))
wr.OnNonceChangeV2(addr, 0, 1, NonceChangeUnspecified)
wr.OnBalanceChange(addr, big.NewInt(100), big.NewInt(200), BalanceChangeUnspecified)
wr.OnBalanceChange(addr, big.NewInt(200), big.NewInt(250), BalanceChangeUnspecified)
wr.OnExit(1, nil, 100, errors.New("revert"), true)
}
wr.OnExit(0, nil, 150, errors.New("revert"), true)
}
if tr.bal.Cmp(big.NewInt(0)) != 0 {
t.Fatalf("unexpected balance: %v", tr.bal)
}
if tr.nonce != 0 {
t.Fatalf("unexpected nonce: %v", tr.nonce)
}
}
// This test checks that changes in nested calls are reverted properly.
func TestJournalNestedCalls(t *testing.T) {
tr := &testTracer{t: t}
wr, err := WrapWithJournal(&Hooks{OnBalanceChange: tr.OnBalanceChange, OnNonceChange: tr.OnNonceChange})
if err != nil {
t.Fatalf("failed to wrap test tracer: %v", err)
}
addr := common.HexToAddress("0x1234")
{
wr.OnEnter(0, 0, addr, addr, nil, 1000, big.NewInt(0))
wr.OnBalanceChange(addr, big.NewInt(0), big.NewInt(100), BalanceChangeUnspecified)
{
wr.OnEnter(1, 0, addr, addr, nil, 1000, big.NewInt(0))
wr.OnBalanceChange(addr, big.NewInt(100), big.NewInt(200), BalanceChangeUnspecified)
{
wr.OnEnter(2, 0, addr, addr, nil, 1000, big.NewInt(0))
wr.OnExit(2, nil, 100, nil, false)
}
{
wr.OnEnter(2, 0, addr, addr, nil, 1000, big.NewInt(0))
wr.OnBalanceChange(addr, big.NewInt(200), big.NewInt(300), BalanceChangeUnspecified)
wr.OnExit(2, nil, 100, nil, false)
}
{
wr.OnEnter(2, 0, addr, addr, nil, 1000, big.NewInt(0))
wr.OnExit(2, nil, 100, nil, false)
}
wr.OnBalanceChange(addr, big.NewInt(300), big.NewInt(400), BalanceChangeUnspecified)
{
wr.OnEnter(2, 0, addr, addr, nil, 1000, big.NewInt(0))
wr.OnBalanceChange(addr, big.NewInt(400), big.NewInt(500), BalanceChangeUnspecified)
wr.OnExit(2, nil, 100, errors.New("revert"), true)
}
{
wr.OnEnter(2, 0, addr, addr, nil, 1000, big.NewInt(0))
wr.OnExit(2, nil, 100, errors.New("revert"), true)
}
{
wr.OnEnter(2, 0, addr, addr, nil, 1000, big.NewInt(0))
wr.OnBalanceChange(addr, big.NewInt(400), big.NewInt(600), BalanceChangeUnspecified)
wr.OnExit(2, nil, 100, nil, false)
}
wr.OnExit(1, nil, 100, errors.New("revert"), true)
}
wr.OnExit(0, nil, 150, nil, false)
}
if tr.bal.Uint64() != 100 {
t.Fatalf("unexpected balance: %v", tr.bal)
}
}
func TestNonceIncOnCreate(t *testing.T) {
const opCREATE = 0xf0
tr := &testTracer{t: t}
wr, err := WrapWithJournal(&Hooks{OnNonceChange: tr.OnNonceChange})
if err != nil {
t.Fatalf("failed to wrap test tracer: %v", err)
}
addr := common.HexToAddress("0x1234")
{
wr.OnEnter(0, opCREATE, addr, addr, nil, 1000, big.NewInt(0))
wr.OnNonceChangeV2(addr, 0, 1, NonceChangeContractCreator)
wr.OnExit(0, nil, 100, errors.New("revert"), true)
}
if tr.nonce != 1 {
t.Fatalf("unexpected nonce: %v", tr.nonce)
}
}
func TestOnNonceChangeV2(t *testing.T) {
tr := &testTracer{t: t}
wr, err := WrapWithJournal(&Hooks{OnNonceChangeV2: tr.OnNonceChangeV2})
if err != nil {
t.Fatalf("failed to wrap test tracer: %v", err)
}
addr := common.HexToAddress("0x1234")
{
wr.OnEnter(2, 0, addr, addr, nil, 1000, big.NewInt(0))
wr.OnNonceChangeV2(addr, 0, 1, NonceChangeEoACall)
wr.OnExit(2, nil, 100, nil, true)
}
if tr.nonce != 0 {
t.Fatalf("unexpected nonce: %v", tr.nonce)
}
}
func TestAllHooksCalled(t *testing.T) {
tracer := newTracerAllHooks()
hooks := tracer.hooks()
wrapped, err := WrapWithJournal(hooks)
if err != nil {
t.Fatalf("failed to wrap hooks with journal: %v", err)
}
// Get the underlying value of the wrapped hooks
wrappedValue := reflect.ValueOf(wrapped).Elem()
wrappedType := wrappedValue.Type()
// Iterate over all fields of the wrapped hooks
for i := 0; i < wrappedType.NumField(); i++ {
field := wrappedType.Field(i)
// Skip fields that are not function types
if field.Type.Kind() != reflect.Func {
continue
}
// Skip non-hooks, i.e. Copy
if field.Name == "copy" {
continue
}
// Skip if field is not set
if wrappedValue.Field(i).IsNil() {
continue
}
// Get the method
method := wrappedValue.Field(i)
// Call the method with zero values
params := make([]reflect.Value, method.Type().NumIn())
for j := 0; j < method.Type().NumIn(); j++ {
params[j] = reflect.Zero(method.Type().In(j))
}
method.Call(params)
}
// Check if all hooks were called
if tracer.numCalled() != tracer.hooksCount() {
t.Errorf("Not all hooks were called. Expected %d, got %d", tracer.hooksCount(), tracer.numCalled())
}
for hookName, called := range tracer.hooksCalled {
if !called {
t.Errorf("Hook %s was not called", hookName)
}
}
}
type tracerAllHooks struct {
hooksCalled map[string]bool
}
func newTracerAllHooks() *tracerAllHooks {
t := &tracerAllHooks{hooksCalled: make(map[string]bool)}
// Initialize all hooks to false. We will use this to
// get total count of hooks.
hooksType := reflect.TypeOf((*Hooks)(nil)).Elem()
for i := 0; i < hooksType.NumField(); i++ {
t.hooksCalled[hooksType.Field(i).Name] = false
}
delete(t.hooksCalled, "OnNonceChange")
return t
}
func (t *tracerAllHooks) hooksCount() int {
return len(t.hooksCalled)
}
func (t *tracerAllHooks) numCalled() int {
count := 0
for _, called := range t.hooksCalled {
if called {
count++
}
}
return count
}
func (t *tracerAllHooks) hooks() *Hooks {
h := &Hooks{}
// Create a function for each hook that sets the
// corresponding hooksCalled field to true.
hooksValue := reflect.ValueOf(h).Elem()
for i := 0; i < hooksValue.NumField(); i++ {
field := hooksValue.Type().Field(i)
if field.Name == "OnNonceChange" {
continue
}
hookMethod := reflect.MakeFunc(field.Type, func(args []reflect.Value) []reflect.Value {
t.hooksCalled[field.Name] = true
return nil
})
hooksValue.Field(i).Set(hookMethod)
}
return h
}

View File

@ -680,9 +680,9 @@ func TestOpenDrops(t *testing.T) {
statedb.AddBalance(crypto.PubkeyToAddress(gapper.PublicKey), uint256.NewInt(1000000), tracing.BalanceChangeUnspecified)
statedb.AddBalance(crypto.PubkeyToAddress(dangler.PublicKey), uint256.NewInt(1000000), tracing.BalanceChangeUnspecified)
statedb.AddBalance(crypto.PubkeyToAddress(filler.PublicKey), uint256.NewInt(1000000), tracing.BalanceChangeUnspecified)
statedb.SetNonce(crypto.PubkeyToAddress(filler.PublicKey), 3)
statedb.SetNonce(crypto.PubkeyToAddress(filler.PublicKey), 3, tracing.NonceChangeUnspecified)
statedb.AddBalance(crypto.PubkeyToAddress(overlapper.PublicKey), uint256.NewInt(1000000), tracing.BalanceChangeUnspecified)
statedb.SetNonce(crypto.PubkeyToAddress(overlapper.PublicKey), 2)
statedb.SetNonce(crypto.PubkeyToAddress(overlapper.PublicKey), 2, tracing.NonceChangeUnspecified)
statedb.AddBalance(crypto.PubkeyToAddress(underpayer.PublicKey), uint256.NewInt(1000000), tracing.BalanceChangeUnspecified)
statedb.AddBalance(crypto.PubkeyToAddress(outpricer.PublicKey), uint256.NewInt(1000000), tracing.BalanceChangeUnspecified)
statedb.AddBalance(crypto.PubkeyToAddress(exceeder.PublicKey), uint256.NewInt(1000000), tracing.BalanceChangeUnspecified)
@ -1526,7 +1526,7 @@ func TestAdd(t *testing.T) {
// Seed the state database with this account
statedb.AddBalance(addrs[acc], new(uint256.Int).SetUint64(seed.balance), tracing.BalanceChangeUnspecified)
statedb.SetNonce(addrs[acc], seed.nonce)
statedb.SetNonce(addrs[acc], seed.nonce, tracing.NonceChangeUnspecified)
// Sign the seed transactions and store them in the data store
for _, tx := range seed.txs {
@ -1581,7 +1581,7 @@ func TestAdd(t *testing.T) {
// Apply the nonce updates to the state db
for _, tx := range txs {
sender, _ := types.Sender(types.LatestSigner(params.MainnetChainConfig), tx)
chain.statedb.SetNonce(sender, tx.Nonce()+1)
chain.statedb.SetNonce(sender, tx.Nonce()+1, tracing.NonceChangeUnspecified)
}
pool.Reset(chain.CurrentBlock(), header)
verifyPoolInternals(t, pool)

View File

@ -251,7 +251,7 @@ func (c *testChain) State() (*state.StateDB, error) {
if *c.trigger {
c.statedb, _ = state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
// simulate that the new head block included tx0 and tx1
c.statedb.SetNonce(c.address, 2)
c.statedb.SetNonce(c.address, 2, tracing.NonceChangeUnspecified)
c.statedb.SetBalance(c.address, new(uint256.Int).SetUint64(params.Ether), tracing.BalanceChangeUnspecified)
*c.trigger = false
}
@ -312,7 +312,7 @@ func testAddBalance(pool *LegacyPool, addr common.Address, amount *big.Int) {
func testSetNonce(pool *LegacyPool, addr common.Address, nonce uint64) {
pool.mu.Lock()
pool.currentState.SetNonce(addr, nonce)
pool.currentState.SetNonce(addr, nonce, tracing.NonceChangeUnspecified)
pool.mu.Unlock()
}
@ -1011,7 +1011,7 @@ func TestQueueTimeLimiting(t *testing.T) {
}
// remove current transactions and increase nonce to prepare for a reset and cleanup
statedb.SetNonce(crypto.PubkeyToAddress(remote.PublicKey), 2)
statedb.SetNonce(crypto.PubkeyToAddress(remote.PublicKey), 2, tracing.NonceChangeUnspecified)
<-pool.requestReset(nil, nil)
// make sure queue, pending are cleared

View File

@ -29,6 +29,7 @@ import (
"github.com/ethereum/go-ethereum/consensus/ethash"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
@ -226,7 +227,7 @@ func TestProcessParentBlockHash(t *testing.T) {
// block 2 parent hash is 0x0200....
// etc
checkBlockHashes := func(statedb *state.StateDB, isVerkle bool) {
statedb.SetNonce(params.HistoryStorageAddress, 1)
statedb.SetNonce(params.HistoryStorageAddress, 1, tracing.NonceChangeUnspecified)
statedb.SetCode(params.HistoryStorageAddress, params.HistoryStorageCode)
// Process n blocks, from 1 .. num
var num = 2

View File

@ -439,7 +439,7 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64,
if nonce+1 < nonce {
return nil, common.Address{}, gas, ErrNonceUintOverflow
}
evm.StateDB.SetNonce(caller.Address(), nonce+1)
evm.StateDB.SetNonce(caller.Address(), nonce+1, tracing.NonceChangeContractCreator)
// Charge the contract creation init gas in verkle mode
if evm.chainRules.IsEIP4762 {
@ -487,7 +487,7 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64,
evm.StateDB.CreateContract(address)
if evm.chainRules.IsEIP158 {
evm.StateDB.SetNonce(address, 1)
evm.StateDB.SetNonce(address, 1, tracing.NonceChangeNewContract)
}
// Charge the contract creation init gas in verkle mode
if evm.chainRules.IsEIP4762 {

View File

@ -448,6 +448,9 @@ func opBlockhash(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) (
if witness := interpreter.evm.StateDB.Witness(); witness != nil {
witness.AddBlockHash(num64)
}
if tracer := interpreter.evm.Config.Tracer; tracer != nil && tracer.OnBlockHashRead != nil {
tracer.OnBlockHashRead(num64, res)
}
num.SetBytes(res[:])
} else {
num.Clear()

View File

@ -39,7 +39,7 @@ type StateDB interface {
GetBalance(common.Address) *uint256.Int
GetNonce(common.Address) uint64
SetNonce(common.Address, uint64)
SetNonce(common.Address, uint64, tracing.NonceChangeReason)
GetCodeHash(common.Address) common.Hash
GetCode(common.Address) []byte

View File

@ -414,7 +414,7 @@ func benchmarkNonModifyingCode(gas uint64, code []byte, name string, tracerCode
eoa := common.HexToAddress("E0")
{
cfg.State.CreateAccount(eoa)
cfg.State.SetNonce(eoa, 100)
cfg.State.SetNonce(eoa, 100, tracing.NonceChangeUnspecified)
}
reverting := common.HexToAddress("EE")
{

View File

@ -57,6 +57,7 @@ func newNoopTracer(_ json.RawMessage) (*tracing.Hooks, error) {
OnCodeChange: t.OnCodeChange,
OnStorageChange: t.OnStorageChange,
OnLog: t.OnLog,
OnBlockHashRead: t.OnBlockHashRead,
}, nil
}
@ -108,5 +109,7 @@ func (t *noop) OnLog(l *types.Log) {
}
func (t *noop) OnBlockHashRead(number uint64, hash common.Hash) {}
func (t *noop) OnGasChange(old, new uint64, reason tracing.GasChangeReason) {
}

View File

@ -86,7 +86,7 @@ func (diff *StateOverride) Apply(statedb *state.StateDB, precompiles vm.Precompi
}
// Override account nonce.
if account.Nonce != nil {
statedb.SetNonce(addr, uint64(*account.Nonce))
statedb.SetNonce(addr, uint64(*account.Nonce), tracing.NonceChangeUnspecified)
}
// Override account(contract) code.
if account.Code != nil {

View File

@ -512,7 +512,7 @@ func MakePreState(db ethdb.Database, accounts types.GenesisAlloc, snapshotter bo
statedb, _ := state.New(types.EmptyRootHash, sdb)
for addr, a := range accounts {
statedb.SetCode(addr, a.Code)
statedb.SetNonce(addr, a.Nonce)
statedb.SetNonce(addr, a.Nonce, tracing.NonceChangeUnspecified)
statedb.SetBalance(addr, uint256.MustFromBig(a.Balance), tracing.BalanceChangeUnspecified)
for k, v := range a.Storage {
statedb.SetState(addr, k, v)