core: introduce GasChangeHook v2 (#34946)
Some checks are pending
/ Linux Build (push) Waiting to run
/ Linux Build (arm) (push) Waiting to run
/ Keeper Build (push) Waiting to run
/ Windows Build (push) Waiting to run
/ Docker Image (push) Waiting to run

This PR introduces OnGasChangeV2 tracing hook, as the pre-requisite for landing
EIP-8037.

---------

Co-authored-by: Sina M <1591639+s1na@users.noreply.github.com>
This commit is contained in:
rjl493456442 2026-05-13 16:53:47 +08:00 committed by GitHub
parent 21c5a287f9
commit 0494cdce23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 164 additions and 63 deletions

View file

@ -420,8 +420,10 @@ func (st *stateTransition) buyGas() error {
return err
}
if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil {
st.evm.Config.Tracer.OnGasChange(0, st.msg.GasLimit, tracing.GasChangeTxInitialBalance)
if st.evm.Config.Tracer.HasGasHook() {
empty := vm.GasBudget{}
initial := vm.NewGasBudget(st.msg.GasLimit)
st.evm.Config.Tracer.EmitGasChange(empty.AsTracing(), initial.AsTracing(), tracing.GasChangeTxInitialBalance)
}
st.gasRemaining = vm.NewGasBudget(st.msg.GasLimit)
st.initialBudget = st.gasRemaining.Copy()
@ -566,8 +568,8 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
if !sufficient {
return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gasRemaining.RegularGas, cost.RegularGas)
}
if t := st.evm.Config.Tracer; t != nil && t.OnGasChange != nil {
t.OnGasChange(prior, st.gasRemaining.RegularGas, tracing.GasChangeTxIntrinsicGas)
if st.evm.Config.Tracer.HasGasHook() {
st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxIntrinsicGas)
}
// Gas limit suffices for the floor data cost (EIP-7623)
if rules.IsPrague {
@ -651,8 +653,8 @@ func (st *stateTransition) execute() (*ExecutionResult, error) {
// After EIP-7623: Data-heavy transactions pay the floor gas.
if used := st.gasUsed(); used < floorDataGas {
prior, _ := st.gasRemaining.Charge(vm.GasCosts{RegularGas: floorDataGas - used})
if t := st.evm.Config.Tracer; t != nil && t.OnGasChange != nil {
t.OnGasChange(prior, st.gasRemaining.RegularGas, tracing.GasChangeTxDataFloor)
if st.evm.Config.Tracer.HasGasHook() {
st.evm.Config.Tracer.EmitGasChange(prior.AsTracing(), st.gasRemaining.AsTracing(), tracing.GasChangeTxDataFloor)
}
}
if peakGasUsed < floorDataGas {
@ -780,8 +782,11 @@ func (st *stateTransition) calcRefund() vm.GasBudget {
if refund > st.state.GetRefund() {
refund = st.state.GetRefund()
}
if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil && refund > 0 {
st.evm.Config.Tracer.OnGasChange(st.gasRemaining.RegularGas, st.gasRemaining.RegularGas+refund, tracing.GasChangeTxRefunds)
if refund > 0 && st.evm.Config.Tracer.HasGasHook() {
after := st.gasRemaining
after.RegularGas += refund
st.evm.Config.Tracer.EmitGasChange(st.gasRemaining.AsTracing(), after.AsTracing(), tracing.GasChangeTxRefunds)
}
return vm.NewGasBudget(refund)
}
@ -793,8 +798,10 @@ func (st *stateTransition) returnGas() {
remaining.Mul(remaining, st.msg.GasPrice)
st.state.AddBalance(st.msg.From, remaining, tracing.BalanceIncreaseGasReturn)
if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil && st.gasRemaining.RegularGas > 0 {
st.evm.Config.Tracer.OnGasChange(st.gasRemaining.RegularGas, 0, tracing.GasChangeTxLeftOverReturned)
if st.gasRemaining.RegularGas > 0 && st.evm.Config.Tracer.HasGasHook() {
after := st.gasRemaining
after.RegularGas = 0
st.evm.Config.Tracer.EmitGasChange(st.gasRemaining.AsTracing(), after.AsTracing(), tracing.GasChangeTxLeftOverReturned)
}
}

View file

@ -164,10 +164,36 @@ type (
// FaultHook is invoked when an error occurs during the execution of an opcode.
FaultHook = func(pc uint64, op byte, gas, cost uint64, scope OpContext, depth int, err error)
// GasChangeHook is invoked when the gas changes.
// GasChangeHook reports changes to the regular execution gas. Tracers
// that don't need visibility into the state-access gas dimension
// introduced by EIP-8037 (Amsterdam) can implement only this hook; it
// will continue to fire across the Amsterdam fork unchanged.
//
// If both this hook and GasChangeHookV2 are implemented on the same
// tracer, only V2 will be invoked. Implement exactly one to avoid
// double-counting.
GasChangeHook = func(old, new uint64, reason GasChangeReason)
// TODO(sina, rjl), please add GasChangeV2Hook by landing the multi-dimensional gas
// GasChangeHookV2 is invoked when any gas dimension changes. It is the
// multi-dimensional successor to GasChangeHook, exposing the state-access
// gas dimension introduced by EIP-8037 (Amsterdam) alongside the regular
// dimension.
//
// Compatibility:
// - Post-Amsterdam: fires for changes to either the regular or the
// state-access dimension. The non-changing dimension is passed through
// unchanged in both `old` and `new` so consumers always observe the
// complete gas vector.
// - Pre-Amsterdam: no state-access gas events occur, so the State field
// of both `old` and `new` is always zero. Tracers that register only
// V2 still receive every regular-gas change as Gas{State: 0} and
// behave identically to a V1 tracer; there is no pre-Amsterdam event
// a V2-only tracer misses.
//
// V1 and V2 coexist: when both are registered on a tracer, only V2 is
// invoked. Tracers SHOULD register at most one of the two to avoid
// double-counting.
GasChangeHookV2 = func(old, new Gas, reason GasChangeReason)
/*
- Chain events -
@ -250,13 +276,14 @@ type (
type Hooks struct {
// VM events
OnTxStart TxStartHook
OnTxEnd TxEndHook
OnEnter EnterHook
OnExit ExitHook
OnOpcode OpcodeHook
OnFault FaultHook
OnGasChange GasChangeHook
OnTxStart TxStartHook
OnTxEnd TxEndHook
OnEnter EnterHook
OnExit ExitHook
OnOpcode OpcodeHook
OnFault FaultHook
OnGasChange GasChangeHook
OnGasChangeV2 GasChangeHookV2
// Chain events
OnBlockchainInit BlockchainInitHook
OnClose CloseHook
@ -280,6 +307,35 @@ type Hooks struct {
OnBlockHashRead BlockHashReadHook
}
// HasGasHook reports whether any gas-change hook is registered. Call sites
// should use this to short-circuit before constructing the Gas / GasBudget
// arguments to EmitGasChange when tracing is off — the dispatch is otherwise
// always paid the cost of evaluating those args.
func (h *Hooks) HasGasHook() bool {
return h != nil && (h.OnGasChangeV2 != nil || h.OnGasChange != nil)
}
// EmitGasChange dispatches a gas change event to the registered hooks. If the
// multi-dimensional OnGasChangeV2 hook is set it is invoked with the full Gas
// vectors; otherwise the single-dimensional OnGasChange hook is invoked with
// the regular-gas dimension only. The call is a no-op when the receiver is
// nil, when neither hook is registered, or when the reason is GasChangeIgnored.
//
// Call sites SHOULD use this helper instead of invoking the hooks directly so
// that both variants stay consistent across the Amsterdam fork boundary.
func (h *Hooks) EmitGasChange(old, new Gas, reason GasChangeReason) {
if h == nil || reason == GasChangeIgnored {
return
}
if h.OnGasChangeV2 != nil {
h.OnGasChangeV2(old, new, reason)
return
}
if h.OnGasChange != nil {
h.OnGasChange(old.Regular, new.Regular, reason)
}
}
// BalanceChangeReason is used to indicate the reason for a balance change, useful
// for tracing and reporting.
type BalanceChangeReason byte
@ -335,6 +391,19 @@ const (
BalanceChangeRevert BalanceChangeReason = 15
)
// Gas represents a multi-dimensional gas budget introduced by EIP-8037.
// It carries the regular execution gas and the state-access gas, which are
// metered independently from the Amsterdam fork onwards.
//
// Before Amsterdam, gas metering is single-dimensional and only the Regular
// field is meaningful; State is always zero. The struct is shaped so that
// pre-Amsterdam call sites can populate it as Gas{Regular: g} without loss
// of fidelity relative to the legacy single-uint64 hook.
type Gas struct {
Regular uint64 // Regular is the budget for ordinary execution gas.
State uint64 // State is the budget dedicated to state-access gas (zero pre-Amsterdam).
}
// GasChangeReason is used to indicate the reason for a gas change, useful
// for tracing and reporting.
//

View file

@ -131,8 +131,8 @@ func (c *Contract) UseGas(cost GasCosts, logger *tracing.Hooks, reason tracing.G
if !ok {
return false
}
if logger != nil && logger.OnGasChange != nil && reason != tracing.GasChangeIgnored {
logger.OnGasChange(prior, c.Gas.RegularGas, reason)
if logger.HasGasHook() && reason != tracing.GasChangeIgnored {
logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason)
}
return true
}
@ -143,8 +143,8 @@ func (c *Contract) RefundGas(refund GasBudget, logger *tracing.Hooks, reason tra
if !changed {
return
}
if logger != nil && logger.OnGasChange != nil && reason != tracing.GasChangeIgnored {
logger.OnGasChange(prior, c.Gas.RegularGas, reason)
if logger.HasGasHook() && reason != tracing.GasChangeIgnored {
logger.EmitGasChange(prior.AsTracing(), c.Gas.AsTracing(), reason)
}
}

View file

@ -269,8 +269,8 @@ func RunPrecompiledContract(stateDB StateDB, p PrecompiledContract, address comm
gas.Exhaust()
return nil, gas, ErrOutOfGas
}
if logger != nil && logger.OnGasChange != nil {
logger.OnGasChange(prior, gas.RegularGas, tracing.GasChangeCallPrecompiledContract)
if logger.HasGasHook() {
logger.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeCallPrecompiledContract)
}
// Touch the precompile for block-level accessList recording once Amsterdam
// fork is activated.

View file

@ -317,8 +317,8 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
if err != ErrExecutionReverted {
if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil {
evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution)
if evm.Config.Tracer.HasGasHook() {
evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
}
gas.Exhaust()
}
@ -371,8 +371,8 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
if err != ErrExecutionReverted {
if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil {
evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution)
if evm.Config.Tracer.HasGasHook() {
evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
}
gas.Exhaust()
}
@ -415,8 +415,8 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address,
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
if err != ErrExecutionReverted {
if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil {
evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution)
if evm.Config.Tracer.HasGasHook() {
evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
}
gas.Exhaust()
}
@ -470,8 +470,8 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b
if err != nil {
evm.StateDB.RevertToSnapshot(snapshot)
if err != ErrExecutionReverted {
if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil {
evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution)
if evm.Config.Tracer.HasGasHook() {
evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
}
gas.Exhaust()
}
@ -509,8 +509,8 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
gas.Exhaust()
return nil, common.Address{}, gas, ErrOutOfGas
}
if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil {
evm.Config.Tracer.OnGasChange(prior, gas.RegularGas, tracing.GasChangeWitnessContractCollisionCheck)
if evm.Config.Tracer.HasGasHook() {
evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractCollisionCheck)
}
}
@ -528,8 +528,8 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
if evm.StateDB.GetNonce(address) != 0 ||
(contractHash != (common.Hash{}) && contractHash != types.EmptyCodeHash) || // non-empty code
isEIP7610RejectedAccount(evm.ChainConfig().ChainID, address, evm.chainRules.IsEIP158) {
if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil {
evm.Config.Tracer.OnGasChange(gas.RegularGas, 0, tracing.GasChangeCallFailedExecution)
if evm.Config.Tracer.HasGasHook() {
evm.Config.Tracer.EmitGasChange(gas.AsTracing(), tracing.Gas{}, tracing.GasChangeCallFailedExecution)
}
gas.Exhaust()
return nil, common.Address{}, gas, ErrContractAddressCollision
@ -558,8 +558,8 @@ func (evm *EVM) create(caller common.Address, code []byte, gas GasBudget, value
return nil, common.Address{}, gas, ErrOutOfGas
}
prior, _ := gas.Charge(GasCosts{RegularGas: consumed})
if evm.Config.Tracer != nil && evm.Config.Tracer.OnGasChange != nil {
evm.Config.Tracer.OnGasChange(prior, gas.RegularGas, tracing.GasChangeWitnessContractInit)
if evm.Config.Tracer.HasGasHook() {
evm.Config.Tracer.EmitGasChange(prior.AsTracing(), gas.AsTracing(), tracing.GasChangeWitnessContractInit)
}
}
evm.Context.Transfer(evm.StateDB, caller, address, value, &evm.chainRules)
@ -673,15 +673,17 @@ func (evm *EVM) captureBegin(depth int, typ OpCode, from common.Address, to comm
if tracer.OnEnter != nil {
tracer.OnEnter(depth, byte(typ), from, to, input, startGas, value)
}
if tracer.OnGasChange != nil {
tracer.OnGasChange(0, startGas, tracing.GasChangeCallInitialBalance)
if tracer.HasGasHook() {
initial := NewGasBudget(startGas)
tracer.EmitGasChange(tracing.Gas{}, initial.AsTracing(), tracing.GasChangeCallInitialBalance)
}
}
func (evm *EVM) captureEnd(depth int, startGas uint64, leftOverGas uint64, ret []byte, err error) {
tracer := evm.Config.Tracer
if leftOverGas != 0 && tracer.OnGasChange != nil {
tracer.OnGasChange(leftOverGas, 0, tracing.GasChangeCallLeftOverReturned)
if leftOverGas != 0 && tracer.HasGasHook() {
leftover := NewGasBudget(leftOverGas)
tracer.EmitGasChange(leftover.AsTracing(), tracing.Gas{}, tracing.GasChangeCallLeftOverReturned)
}
var reverted bool
if err != nil {

View file

@ -16,7 +16,11 @@
package vm
import "fmt"
import (
"fmt"
"github.com/ethereum/go-ethereum/core/tracing"
)
// GasCosts denotes a vector of gas costs in the
// multidimensional metering paradigm. It represents the cost
@ -77,21 +81,26 @@ func (g GasBudget) CanAfford(cost GasCosts) bool {
}
// Charge deducts the given gas cost from the budget. It returns the
// pre-charge gas value and false if the budget does not have sufficient
// pre-charge budget and false if the budget does not have sufficient
// gas to cover the cost.
func (g *GasBudget) Charge(cost GasCosts) (uint64, bool) {
prior := g.RegularGas
if prior < cost.RegularGas {
func (g *GasBudget) Charge(cost GasCosts) (GasBudget, bool) {
prior := *g
if g.RegularGas < cost.RegularGas {
return prior, false
}
g.RegularGas -= cost.RegularGas
return prior, true
}
// Refund adds the given gas budget back. It returns the pre-refund gas
// value and whether the budget was actually changed.
func (g *GasBudget) Refund(other GasBudget) (uint64, bool) {
prior := g.RegularGas
// Refund adds the given gas budget back. It returns the pre-refund budget
// and whether the budget was actually changed.
func (g *GasBudget) Refund(other GasBudget) (GasBudget, bool) {
prior := *g
g.RegularGas += other.RegularGas
return prior, g.RegularGas != prior
return prior, g.RegularGas != prior.RegularGas
}
// AsTracing converts the GasBudget into the tracing-facing Gas vector.
func (g GasBudget) AsTracing() tracing.Gas {
return tracing.Gas{Regular: g.RegularGas, State: g.StateGas}
}

View file

@ -234,8 +234,12 @@ func (evm *EVM) Run(contract *Contract, input []byte, readOnly bool) (ret []byte
// Do tracing before potential memory expansion
if debug {
if evm.Config.Tracer.OnGasChange != nil {
evm.Config.Tracer.OnGasChange(gasCopy, gasCopy-cost, tracing.GasChangeCallOpCode)
if evm.Config.Tracer.HasGasHook() {
evm.Config.Tracer.EmitGasChange(
tracing.Gas{Regular: gasCopy, State: contract.Gas.StateGas},
tracing.Gas{Regular: gasCopy - cost, State: contract.Gas.StateGas},
tracing.GasChangeCallOpCode,
)
}
if evm.Config.Tracer.OnOpcode != nil {
evm.Config.Tracer.OnOpcode(pc, byte(op), gasCopy, cost, callContext, evm.returnData, evm.depth, VMErrorFromErr(err))

View file

@ -47,6 +47,7 @@ func newNoopTracer(_ json.RawMessage) (*tracing.Hooks, error) {
OnOpcode: t.OnOpcode,
OnFault: t.OnFault,
OnGasChange: t.OnGasChange,
OnGasChangeV2: t.OnGasChangeV2,
OnBlockchainInit: t.OnBlockchainInit,
OnBlockStart: t.OnBlockStart,
OnBlockEnd: t.OnBlockEnd,
@ -113,3 +114,6 @@ func (t *noop) OnBlockHashRead(number uint64, hash common.Hash) {}
func (t *noop) OnGasChange(old, new uint64, reason tracing.GasChangeReason) {
}
func (t *noop) OnGasChangeV2(old, new tracing.Gas, reason tracing.GasChangeReason) {
}

View file

@ -65,10 +65,11 @@ func newMuxTracerFromConfig(ctx *tracers.Context, cfg json.RawMessage, chainConf
// the aggregated JSON result returned by GetResult.
//
// For hooks that have both a V1 and V2 form (OnCodeChange / OnCodeChangeV2,
// OnNonceChange / OnNonceChangeV2, OnSystemCallStart / OnSystemCallStartV2),
// the mux exposes only the V2 variant upward. The fanout then prefers each
// child's V2 hook and falls back to V1 if only V1 is set, mirroring the
// precedence already used in core/state_processor.go.
// OnNonceChange / OnNonceChangeV2, OnGasChange / OnGasChangeV2,
// OnSystemCallStart / OnSystemCallStartV2), the mux exposes only the V2
// variant upward. The fanout then prefers each child's V2 hook and falls
// back to V1 if only V1 is set, mirroring the precedence already used in
// core/state_processor.go.
func NewMuxTracer(names []string, objects []*tracers.Tracer) (*tracers.Tracer, error) {
t := &muxTracer{names: names, tracers: objects}
return &tracers.Tracer{
@ -79,7 +80,7 @@ func NewMuxTracer(names []string, objects []*tracers.Tracer) (*tracers.Tracer, e
OnExit: t.OnExit,
OnOpcode: t.OnOpcode,
OnFault: t.OnFault,
OnGasChange: t.OnGasChange,
OnGasChangeV2: t.OnGasChangeV2,
OnBalanceChange: t.OnBalanceChange,
OnNonceChangeV2: t.OnNonceChangeV2,
OnCodeChangeV2: t.OnCodeChangeV2,
@ -109,10 +110,12 @@ func (t *muxTracer) OnFault(pc uint64, op byte, gas, cost uint64, scope tracing.
}
}
func (t *muxTracer) OnGasChange(old, new uint64, reason tracing.GasChangeReason) {
func (t *muxTracer) OnGasChangeV2(old, new tracing.Gas, reason tracing.GasChangeReason) {
for _, t := range t.tracers {
if t.OnGasChange != nil {
t.OnGasChange(old, new, reason)
if t.OnGasChangeV2 != nil {
t.OnGasChangeV2(old, new, reason)
} else if t.OnGasChange != nil {
t.OnGasChange(old.Regular, new.Regular, reason)
}
}
}

View file

@ -47,6 +47,7 @@ func newNoopTracer(ctx *tracers.Context, cfg json.RawMessage, chainConfig *param
OnOpcode: t.OnOpcode,
OnFault: t.OnFault,
OnGasChange: t.OnGasChange,
OnGasChangeV2: t.OnGasChangeV2,
OnBalanceChange: t.OnBalanceChange,
OnNonceChange: t.OnNonceChange,
OnCodeChange: t.OnCodeChange,
@ -66,6 +67,8 @@ func (t *noopTracer) OnFault(pc uint64, op byte, gas, cost uint64, _ tracing.OpC
func (t *noopTracer) OnGasChange(old, new uint64, reason tracing.GasChangeReason) {}
func (t *noopTracer) OnGasChangeV2(old, new tracing.Gas, reason tracing.GasChangeReason) {}
func (t *noopTracer) OnEnter(depth int, typ byte, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
}