Skip to content

core, eth/tracers: separate dynamic gas reporting #32191

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cmd/evm/internal/t8ntool/file_tracer.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,5 +148,10 @@ func (l *fileWritingTracer) hooks() *tracing.Hooks {
l.inner.OnSystemCallEnd()
}
},
OnGasChange: func(old, new uint64, reason tracing.GasChangeReason) {
if l.inner.OnGasChange != nil {
l.inner.OnGasChange(old, new, reason)
}
},
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@
{"pc":38,"op":82,"gas":"0x65ee4","gasCost":"0x6","memSize":0,"stack":["0x10","0x0"],"depth":2,"refund":0,"opName":"MSTORE"}
{"pc":39,"op":96,"gas":"0x65ede","gasCost":"0x3","memSize":32,"stack":[],"depth":2,"refund":0,"opName":"PUSH1"}
{"pc":41,"op":96,"gas":"0x65edb","gasCost":"0x3","memSize":32,"stack":["0x20"],"depth":2,"refund":0,"opName":"PUSH1"}
{"pc":43,"op":253,"gas":"0x65ed8","gasCost":"0x0","memSize":32,"stack":["0x20","0x0"],"depth":2,"refund":0,"opName":"REVERT"}
{"pc":43,"op":253,"gas":"0x65ed8","gasCost":"0x0","memSize":32,"stack":[],"depth":2,"refund":0,"opName":"REVERT","error":"execution reverted"}
{"pc":43,"op":253,"gas":"0x65ed8","gasCost":"0x0","memSize":32,"stack":["0x20","0x0"],"depth":2,"refund":0,"opName":"REVERT","error":"execution reverted"}
{"output":"0000000000000000000000000000000000000000000000000000000000000010","gasUsed":"0x2e44","error":"execution reverted"}
{"pc":69,"op":96,"gas":"0x67976","gasCost":"0x3","memSize":0,"stack":["0x0"],"depth":1,"refund":0,"opName":"PUSH1"}
{"pc":71,"op":85,"gas":"0x67973","gasCost":"0x1388","memSize":0,"stack":["0x0","0x2"],"depth":1,"refund":4800,"opName":"SSTORE"}
Expand Down
7 changes: 4 additions & 3 deletions core/tracing/gen_gas_change_reason_stringer.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions core/tracing/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,9 @@ const (
// GasChangeTxDataFloor is the amount of extra gas the transaction has to pay to reach the minimum gas requirement for the
// transaction data. This change will always be a negative change.
GasChangeTxDataFloor GasChangeReason = 19
// GasChangeCallOpCodeDynamic is the amount of dynamic gas that will be charged for an opcode executed by the EVM.
// It will be emitted after the `OnOpcode` callback. So the cost should be attributed to the last instance of `OnOpcode`.
GasChangeCallOpCodeDynamic GasChangeReason = 20

// GasChangeIgnored is a special value that can be used to indicate that the gas change should be ignored as
// it will be "manually" tracked by a direct emit of the gas change event.
Expand Down
16 changes: 10 additions & 6 deletions core/vm/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,8 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (
// For optimisation reason we're using uint64 as the program counter.
// It's theoretically possible to go above 2^64. The YP defines the PC
// to be uint256. Practically much less so feasible.
pc = uint64(0) // program counter
cost uint64
pc = uint64(0) // program counter
cost, dynamicCost uint64
// copies used by tracer
pcCopy uint64 // needed for the deferred EVMLogger
gasCopy uint64 // for EVMLogger to log gas remaining before execution
Expand Down Expand Up @@ -234,7 +234,7 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (
for {
if debug {
// Capture pre-execution values for tracing.
logged, pcCopy, gasCopy = false, pc, contract.Gas
logged, pcCopy, gasCopy, dynamicCost = false, pc, contract.Gas, 0
}

if in.evm.chainRules.IsEIP4762 && !contract.IsDeployment && !contract.IsSystemCall {
Expand Down Expand Up @@ -286,7 +286,6 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (
}
// Consume the gas and return an error if not enough gas is available.
// cost is explicitly set so that the capture state defer method can get the proper cost
var dynamicCost uint64
dynamicCost, err = operation.dynamicGas(in.evm, contract, stack, mem, memorySize)
cost += dynamicCost // for tracing
if err != nil {
Expand All @@ -303,10 +302,15 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (
// Do tracing before potential memory expansion
if debug {
if in.evm.Config.Tracer.OnGasChange != nil {
in.evm.Config.Tracer.OnGasChange(gasCopy, gasCopy-cost, tracing.GasChangeCallOpCode)
// Trace the constant cost only
in.evm.Config.Tracer.OnGasChange(gasCopy, gasCopy-operation.constantGas, tracing.GasChangeCallOpCode)
}
if in.evm.Config.Tracer.OnOpcode != nil {
in.evm.Config.Tracer.OnOpcode(pc, byte(op), gasCopy, cost, callContext, in.returnData, in.evm.depth, VMErrorFromErr(err))
in.evm.Config.Tracer.OnOpcode(pc, byte(op), gasCopy, operation.constantGas, callContext, in.returnData, in.evm.depth, VMErrorFromErr(err))
// If any, trace the dynamic cost as well
if in.evm.Config.Tracer.OnGasChange != nil && dynamicCost > 0 {
in.evm.Config.Tracer.OnGasChange(gasCopy-operation.constantGas, gasCopy-cost, tracing.GasChangeCallOpCodeDynamic)
}
logged = true
}
}
Expand Down
33 changes: 24 additions & 9 deletions core/vm/runtime/runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,11 @@ func TestColdAccountAccessCost(t *testing.T) {
Execute(tc.code, nil, &Config{
EVMConfig: vm.Config{
Tracer: &tracing.Hooks{
OnGasChange: func(old, new uint64, reason tracing.GasChangeReason) {
if step-1 == tc.step && reason == tracing.GasChangeCallOpCodeDynamic {
have += old - new
}
},
OnOpcode: func(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) {
// Uncomment to investigate failures:
//t.Logf("%d: %v %d", step, vm.OpCode(op).String(), cost)
Expand Down Expand Up @@ -700,10 +705,15 @@ func TestRuntimeJSTracer(t *testing.T) {
this.exits++;
this.gasUsed = res.getGasUsed();
}}`,
`{enters: 0, exits: 0, enterGas: 0, gasUsed: 0, steps:0,
`{enters: 0, exits: 0, enterGas: 0, gasUsed: 0, steps:0, dynamicGas:0,
fault: function() {},
result: function() {
return [this.enters, this.exits,this.enterGas,this.gasUsed, this.steps].join(",")
return [this.enters, this.exits,this.enterGas,this.gasUsed, this.steps, this.dynamicGas].join(",")
},
gas: function(change) {
if (change.getReason() == "CallOpCodeDynamic") {
this.dynamicGas += change.getOld() - change.getNew();
}
},
enter: function(frame) {
this.enters++;
Expand All @@ -726,7 +736,7 @@ func TestRuntimeJSTracer(t *testing.T) {
Push(0). // value
Op(vm.CREATE).
Op(vm.POP).Bytes(),
results: []string{`"1,1,952853,6,12"`, `"1,1,952853,6,0"`},
results: []string{`"1,1,952853,6,12"`, `"1,1,952853,6,0,5"`},
},
{ // CREATE2
code: program.New().MstoreSmall(initcode, 0).
Expand All @@ -736,27 +746,27 @@ func TestRuntimeJSTracer(t *testing.T) {
Push(0). // value
Op(vm.CREATE2).
Op(vm.POP).Bytes(),
results: []string{`"1,1,952844,6,13"`, `"1,1,952844,6,0"`},
results: []string{`"1,1,952844,6,13"`, `"1,1,952844,6,0,11"`},
},
{ // CALL
code: program.New().Call(nil, 0xbb, 0, 0, 0, 0, 0).Op(vm.POP).Bytes(),
results: []string{`"1,1,981796,6,13"`, `"1,1,981796,6,0"`},
results: []string{`"1,1,981796,6,13"`, `"1,1,981796,6,0,984296"`},
},
{ // CALLCODE
code: program.New().CallCode(nil, 0xcc, 0, 0, 0, 0, 0).Op(vm.POP).Bytes(),
results: []string{`"1,1,981796,6,13"`, `"1,1,981796,6,0"`},
results: []string{`"1,1,981796,6,13"`, `"1,1,981796,6,0,984296"`},
},
{ // STATICCALL
code: program.New().StaticCall(nil, 0xdd, 0, 0, 0, 0).Op(vm.POP).Bytes(),
results: []string{`"1,1,981799,6,12"`, `"1,1,981799,6,0"`},
results: []string{`"1,1,981799,6,12"`, `"1,1,981799,6,0,984299"`},
},
{ // DELEGATECALL
code: program.New().DelegateCall(nil, 0xee, 0, 0, 0, 0).Op(vm.POP).Bytes(),
results: []string{`"1,1,981799,6,12"`, `"1,1,981799,6,0"`},
results: []string{`"1,1,981799,6,12"`, `"1,1,981799,6,0,984299"`},
},
{ // CALL self-destructing contract
code: program.New().Call(nil, 0xff, 0, 0, 0, 0, 0).Op(vm.POP).Bytes(),
results: []string{`"2,2,0,5003,12"`, `"2,2,0,5003,0"`},
results: []string{`"2,2,0,5003,12"`, `"2,2,0,5003,0,984296"`},
},
}
calleeCode := []byte{
Expand Down Expand Up @@ -921,6 +931,11 @@ func TestDelegatedAccountAccessCost(t *testing.T) {
State: statedb,
EVMConfig: vm.Config{
Tracer: &tracing.Hooks{
OnGasChange: func(old, new uint64, reason tracing.GasChangeReason) {
if step-1 == tc.step && reason == tracing.GasChangeCallOpCodeDynamic {
have += old - new
}
},
OnOpcode: func(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) {
// Uncomment to investigate failures:
t.Logf("%d: %v %d", step, vm.OpCode(op).String(), cost)
Expand Down
53 changes: 47 additions & 6 deletions eth/tracers/js/goja.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,17 +129,20 @@ type jsTracer struct {
step goja.Callable
enter goja.Callable
exit goja.Callable
gas goja.Callable

// Underlying structs being passed into JS
log *steplog
frame *callframe
frameResult *callframeResult
gasChange *gasChange

// Goja-wrapping of types prepared for JS consumption
logValue goja.Value
dbValue goja.Value
frameValue goja.Value
frameResultValue goja.Value
gasChangeValue goja.Value
}

// newJsTracer instantiates a new JS tracer instance. code is a
Expand Down Expand Up @@ -202,6 +205,7 @@ func newJsTracer(code string, ctx *tracers.Context, cfg json.RawMessage, chainCo
if hasEnter != hasExit {
return nil, errors.New("trace object must expose either both or none of enter() and exit()")
}
t.gas, _ = goja.AssertFunction(obj.Get("gas"))
t.traceFrame = hasEnter
t.obj = obj
t.step = step
Expand Down Expand Up @@ -230,18 +234,21 @@ func newJsTracer(code string, ctx *tracers.Context, cfg json.RawMessage, chainCo
}
t.frame = &callframe{vm: vm, toBig: t.toBig, toBuf: t.toBuf}
t.frameResult = &callframeResult{vm: vm, toBuf: t.toBuf}
t.gasChange = &gasChange{vm: vm}
t.frameValue = t.frame.setupObject()
t.frameResultValue = t.frameResult.setupObject()
t.logValue = t.log.setupObject()
t.gasChangeValue = t.gasChange.setupObject()

return &tracers.Tracer{
Hooks: &tracing.Hooks{
OnTxStart: t.OnTxStart,
OnTxEnd: t.OnTxEnd,
OnEnter: t.OnEnter,
OnExit: t.OnExit,
OnOpcode: t.OnOpcode,
OnFault: t.OnFault,
OnTxStart: t.OnTxStart,
OnTxEnd: t.OnTxEnd,
OnEnter: t.OnEnter,
OnExit: t.OnExit,
OnOpcode: t.OnOpcode,
OnFault: t.OnFault,
OnGasChange: t.OnGasChange,
},
GetResult: t.GetResult,
Stop: t.Stop,
Expand Down Expand Up @@ -329,6 +336,20 @@ func (t *jsTracer) onStart(from common.Address, to common.Address, create bool,
t.ctx["value"] = valueBig
}

// OnGasChange keeps track of the changes in available gas
func (t *jsTracer) OnGasChange(old, new uint64, reason tracing.GasChangeReason) {
if t.gas == nil {
return
}

t.gasChange.old = old
t.gasChange.new = new
t.gasChange.reason = reason
if _, err := t.gas(t.obj, t.gasChangeValue); err != nil {
t.onError("gas", err)
}
}

// OnOpcode implements the Tracer interface to trace a single step of VM execution.
func (t *jsTracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpContext, rData []byte, depth int, err error) {
if !t.traceStep {
Expand Down Expand Up @@ -1043,3 +1064,23 @@ func (l *steplog) setupObject() *goja.Object {
o.Set("contract", l.contract.setupObject())
return o
}

type gasChange struct {
vm *goja.Runtime

old, new uint64
reason tracing.GasChangeReason
}

func (g *gasChange) GetOld() uint64 { return g.old }
func (g *gasChange) GetNew() uint64 { return g.new }
func (g *gasChange) GetReason() string { return g.reason.String() }

func (g *gasChange) setupObject() *goja.Object {
o := g.vm.NewObject()
// Setup basic fields.
o.Set("getOld", g.vm.ToValue(g.GetOld))
o.Set("getNew", g.vm.ToValue(g.GetNew))
o.Set("getReason", g.vm.ToValue(g.GetReason))
return o
}
29 changes: 22 additions & 7 deletions eth/tracers/js/internal/tracers/call_tracer_legacy.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@
// an inner call.
descended: false,

// keeps track of last pushed call to attribute the next dynamic cost with
lastPushedCall: undefined,

// step is invoked for every opcode that the VM executes.
step: function(log, db) {
this.lastPushedCall = undefined
// Capture any errors immediately
var error = log.getError();
if (error !== undefined) {
Expand All @@ -52,6 +56,7 @@
value: '0x' + log.stack.peek(0).toString(16)
};
this.callstack.push(call);
this.lastPushedCall = call
this.descended = true
return;
}
Expand All @@ -61,14 +66,16 @@
if (this.callstack[left-1].calls === undefined) {
this.callstack[left-1].calls = [];
}
this.callstack[left-1].calls.push({
type: op,
from: toHex(log.contract.getAddress()),
to: toHex(toAddress(log.stack.peek(0).toString(16))),
gasIn: log.getGas(),
var call = {
type: op,
from: toHex(log.contract.getAddress()),
to: toHex(toAddress(log.stack.peek(0).toString(16))),
gasIn: log.getGas(),
gasCost: log.getCost(),
value: '0x' + db.getBalance(log.contract.getAddress()).toString(16)
});
value: '0x' + db.getBalance(log.contract.getAddress()).toString(16)
}
this.callstack[left - 1].calls.push(call);
this.lastPushedCall = call
return
}
// If a new method invocation is being done, add to the call stack
Expand Down Expand Up @@ -98,6 +105,7 @@
call.value = '0x' + log.stack.peek(2).toString(16);
}
this.callstack.push(call);
this.lastPushedCall = call
this.descended = true
return;
}
Expand Down Expand Up @@ -161,6 +169,13 @@
}
},

gas: function(change) {
if (this.lastPushedCall !== undefined && change.getReason() == "CallOpCodeDynamic") {
this.lastPushedCall.gasCost += change.getOld() - change.getNew();
this.lastPushedCall = undefined
}
},

// fault is invoked when the actual execution of an opcode fails.
fault: function(log, db) {
// If the topmost call already reverted, don't handle the additional fault again
Expand Down
Loading
Loading