core: introduce GasChangeHook v2

This commit is contained in:
Gary Rong 2026-04-20 15:42:03 +08:00
parent c16684c1ee
commit 6ab8633de0
10 changed files with 162 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.FireGasChange(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.FireGasChange(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.FireGasChange(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.FireGasChange(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.FireGasChange(st.gasRemaining.AsTracing(), after.AsTracing(), tracing.GasChangeTxLeftOverReturned)
}
}

View file

@ -164,10 +164,34 @@ 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 is invoked when the regular gas dimension changes.
//
// This hook is the single-dimensional, pre-Amsterdam gas tracing hook. It
// remains the canonical hook for all forks prior to Amsterdam (where gas
// metering is single-dimensional) and continues to fire post-Amsterdam for
// any change that affects the regular gas dimension. Tracers that do not
// need visibility into the state-access gas dimension should keep using
// this hook; they require no changes to work across the Amsterdam fork.
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 under the
// multi-dimensional gas metering introduced by EIP-8037 (Amsterdam).
//
// 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: the core does not emit state-gas changes and the
// state dimension is always zero. Tracers that register only V2 will
// still receive regular-gas changes with Gas{State: 0} and behave
// identically to the V1 hook; there is no pre-Amsterdam-only code path
// that V2 misses.
//
// V1 and V2 coexist: when both are registered on a tracer, only V2 hook 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 +274,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 +305,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 FireGasChange 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)
}
// FireGasChange 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) FireGasChange(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 +389,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.FireGasChange(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.FireGasChange(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.FireGasChange(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.FireGasChange(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.FireGasChange(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.FireGasChange(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.FireGasChange(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.FireGasChange(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.FireGasChange(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.FireGasChange(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.FireGasChange(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.FireGasChange(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.FireGasChange(
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) {
}