Skip to content

Commit

Permalink
core,eth: call frame tracing prototype
Browse files Browse the repository at this point in the history
  • Loading branch information
s1na committed Jun 22, 2021
1 parent 7a7abe3 commit fca0a4d
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 2 deletions.
5 changes: 5 additions & 0 deletions core/vm/access_list_tracer.go
Expand Up @@ -166,6 +166,11 @@ func (*AccessListTracer) CaptureFault(env *EVM, pc uint64, op OpCode, gas, cost

func (*AccessListTracer) CaptureEnd(output []byte, gasUsed uint64, t time.Duration, err error) {}

func (*AccessListTracer) CaptureEnter(env *EVM, type_ CallFrameType, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
}

func (*AccessListTracer) CaptureExit(env *EVM, output []byte, gasUsed uint64) {}

// AccessList returns the current accesslist maintained by the tracer.
func (a *AccessListTracer) AccessList() types.AccessList {
return a.list.accessList()
Expand Down
48 changes: 48 additions & 0 deletions core/vm/evm.go
Expand Up @@ -32,6 +32,18 @@ import (
// deployed contract addresses (relevant after the account abstraction).
var emptyCodeHash = crypto.Keccak256Hash(nil)

// Type of an executing call frame
type CallFrameType int

const (
CallType CallFrameType = iota
CallCodeType
DelegateCallType
StaticCallType
// Init code execution for both CREATE and CREATE2
CreateType
)

type (
// CanTransferFunc is the signature of a transfer guard function
CanTransferFunc func(StateDB, common.Address, *big.Int) bool
Expand Down Expand Up @@ -240,6 +252,12 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
defer func(startGas uint64, startTime time.Time) { // Lazy evaluation of the parameters
evm.Config.Tracer.CaptureEnd(ret, startGas-gas, time.Since(startTime), err)
}(gas, time.Now())
} else if evm.Config.Debug && evm.depth > 0 {
// Handle tracer events for entering and exiting a call frame
evm.Config.Tracer.CaptureEnter(evm, CallType, caller.Address(), addr, input, gas, value)
defer func(startGas uint64) {
evm.Config.Tracer.CaptureExit(evm, ret, startGas-gas)
}(gas)
}

if isPrecompile {
Expand Down Expand Up @@ -299,6 +317,14 @@ func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte,
}
var snapshot = evm.StateDB.Snapshot()

// Invoke tracer hooks that signal entering/exiting a call frame
if evm.Config.Debug {
evm.Config.Tracer.CaptureEnter(evm, CallCodeType, caller.Address(), addr, input, gas, value)
defer func(startGas uint64) {
evm.Config.Tracer.CaptureExit(evm, ret, startGas-gas)
}(gas)
}

// It is allowed to call precompiles, even via delegatecall
if p, isPrecompile := evm.precompile(addr); isPrecompile {
ret, gas, err = RunPrecompiledContract(p, input, gas)
Expand Down Expand Up @@ -335,6 +361,14 @@ func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []by
}
var snapshot = evm.StateDB.Snapshot()

// Invoke tracer hooks that signal entering/exiting a call frame
if evm.Config.Debug {
evm.Config.Tracer.CaptureEnter(evm, DelegateCallType, caller.Address(), addr, input, gas, nil)
defer func(startGas uint64) {
evm.Config.Tracer.CaptureExit(evm, ret, startGas-gas)
}(gas)
}

// It is allowed to call precompiles, even via delegatecall
if p, isPrecompile := evm.precompile(addr); isPrecompile {
ret, gas, err = RunPrecompiledContract(p, input, gas)
Expand Down Expand Up @@ -380,6 +414,14 @@ func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte
// future scenarios
evm.StateDB.AddBalance(addr, big0)

// Invoke tracer hooks that signal entering/exiting a call frame
if evm.Config.Debug {
evm.Config.Tracer.CaptureEnter(evm, StaticCallType, caller.Address(), addr, input, gas, nil)
defer func(startGas uint64) {
evm.Config.Tracer.CaptureExit(evm, ret, startGas-gas)
}(gas)
}

if p, isPrecompile := evm.precompile(addr); isPrecompile {
ret, gas, err = RunPrecompiledContract(p, input, gas)
} else {
Expand Down Expand Up @@ -459,7 +501,11 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64,

if evm.Config.Debug && evm.depth == 0 {
evm.Config.Tracer.CaptureStart(evm, caller.Address(), address, true, codeAndHash.code, gas, value)
} else if evm.Config.Debug && evm.depth > 0 {
// TODO: Make sure we should capture init code's call frame for the tracer
evm.Config.Tracer.CaptureEnter(evm, CreateType, caller.Address(), address, codeAndHash.code, gas, value)
}

start := time.Now()

ret, err := run(evm, contract, nil, false)
Expand Down Expand Up @@ -499,6 +545,8 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64,

if evm.Config.Debug && evm.depth == 0 {
evm.Config.Tracer.CaptureEnd(ret, gas-contract.Gas, time.Since(start), err)
} else if evm.Config.Debug && evm.depth > 0 {
evm.Config.Tracer.CaptureExit(evm, ret, gas-contract.Gas)
}
return ret, address, contract.Gas, err
}
Expand Down
18 changes: 18 additions & 0 deletions core/vm/logger.go
Expand Up @@ -106,6 +106,8 @@ func (s *StructLog) ErrorString() string {
type Tracer interface {
CaptureStart(env *EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int)
CaptureState(env *EVM, pc uint64, op OpCode, gas, cost uint64, scope *ScopeContext, rData []byte, depth int, err error)
CaptureEnter(env *EVM, type_ CallFrameType, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int)
CaptureExit(env *EVM, output []byte, gasUsed uint64)
CaptureFault(env *EVM, pc uint64, op OpCode, gas, cost uint64, scope *ScopeContext, depth int, err error)
CaptureEnd(output []byte, gasUsed uint64, t time.Duration, err error)
}
Expand Down Expand Up @@ -217,6 +219,14 @@ func (l *StructLogger) CaptureEnd(output []byte, gasUsed uint64, t time.Duration
}
}

func (l *StructLogger) CaptureEnter(env *EVM, type_ CallFrameType, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
// TODO
}

func (l *StructLogger) CaptureExit(env *EVM, output []byte, gasUsed uint64) {
// TODO
}

// StructLogs returns the captured log entries.
func (l *StructLogger) StructLogs() []StructLog { return l.logs }

Expand Down Expand Up @@ -334,3 +344,11 @@ func (t *mdLogger) CaptureEnd(output []byte, gasUsed uint64, tm time.Duration, e
fmt.Fprintf(t.out, "\nOutput: `0x%x`\nConsumed gas: `%d`\nError: `%v`\n",
output, gasUsed, err)
}

func (t *mdLogger) CaptureEnter(env *EVM, type_ CallFrameType, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
// TODO
}

func (t *mdLogger) CaptureExit(env *EVM, output []byte, gasUsed uint64) {
// TODO
}
8 changes: 8 additions & 0 deletions core/vm/logger_json.go
Expand Up @@ -93,3 +93,11 @@ func (l *JSONLogger) CaptureEnd(output []byte, gasUsed uint64, t time.Duration,
}
l.encoder.Encode(endLog{common.Bytes2Hex(output), math.HexOrDecimal64(gasUsed), t, errMsg})
}

func (l *JSONLogger) CaptureEnter(env *EVM, type_ CallFrameType, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
// TODO
}

func (l *JSONLogger) CaptureExit(env *EVM, output []byte, gasUsed uint64) {
// TODO
}
101 changes: 99 additions & 2 deletions eth/tracers/tracer.go
Expand Up @@ -308,8 +308,9 @@ type Tracer struct {
ctx map[string]interface{} // Transaction context gathered throughout execution
err error // Error, if one has occurred

interrupt uint32 // Atomic flag to signal execution interruption
reason error // Textual reason for the interruption
interrupt uint32 // Atomic flag to signal execution interruption
reason error // Textual reason for the interruption
traceCallFrames bool // When true, will invoke enter() and exit() js funcs
}

// New instantiates a new tracer instance. code specifies a Javascript snippet,
Expand Down Expand Up @@ -443,6 +444,18 @@ func New(code string, txCtx vm.TxContext) (*Tracer, error) {
}
tracer.vm.Pop()

hasEnter := tracer.vm.GetPropString(tracer.tracerObject, "enter")
tracer.vm.Pop()
hasExit := tracer.vm.GetPropString(tracer.tracerObject, "exit")
tracer.vm.Pop()
if hasEnter != hasExit {
return nil, fmt.Errorf("trace object must expose either both or none of enter() and exit()")
}
// Maintain backwards-compatibility with old tracing scripts
if hasEnter {
tracer.traceCallFrames = true
}

// Tracer is valid, inject the big int library to access large numbers
tracer.vm.EvalString(bigIntegerJS)
tracer.vm.PutGlobalString("bigInt")
Expand Down Expand Up @@ -622,6 +635,90 @@ func (jst *Tracer) CaptureEnd(output []byte, gasUsed uint64, t time.Duration, er
}
}

func (jst *Tracer) CaptureEnter(env *vm.EVM, type_ vm.CallFrameType, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) {
// TODO: Do we need env?

if !jst.traceCallFrames {
return
}
if jst.err != nil {
return
}
// If tracing was interrupted, set the error and stop
if atomic.LoadUint32(&jst.interrupt) > 0 {
jst.err = jst.reason
return
}

frame := make(map[string]interface{})
//frame["type"] = type_
frame["from"] = from
frame["to"] = to
frame["input"] = input
frame["gas"] = gas
frame["value"] = value
// Transform the context into a JavaScript object and inject into the state
obj := jst.vm.PushObject()

for key, val := range frame {
switch val := val.(type) {
case uint64:
jst.vm.PushUint(uint(val))

case string:
jst.vm.PushString(val)

case []byte:
ptr := jst.vm.PushFixedBuffer(len(val))
copy(makeSlice(ptr, uint(len(val))), val)

case common.Address:
ptr := jst.vm.PushFixedBuffer(20)
copy(makeSlice(ptr, 20), val[:])

case *big.Int:
pushBigInt(val, jst.vm)

default:
panic(fmt.Sprintf("unsupported type: %T", val))
}
jst.vm.PutPropString(obj, key)
}
jst.vm.PutPropString(jst.stateObject, "frame")

if _, err := jst.call(true, "enter", "frame"); err != nil {
jst.err = wrapError("enter", err)
}
}

func (jst *Tracer) CaptureExit(env *vm.EVM, output []byte, gasUsed uint64) {
if !jst.traceCallFrames {
return
}
if jst.err != nil {
return
}
// If tracing was interrupted, set the error and stop
if atomic.LoadUint32(&jst.interrupt) > 0 {
jst.err = jst.reason
return
}

obj := jst.vm.PushObject()

ptr := jst.vm.PushFixedBuffer(len(output))
copy(makeSlice(ptr, uint(len(output))), output)
jst.vm.PutPropString(obj, "output")

jst.vm.PushUint(uint(gasUsed))
jst.vm.PutPropString(obj, "gasUsed")

jst.vm.PutPropString(jst.stateObject, "frameResult")
if _, err := jst.call(true, "exit", "frameResult"); err != nil {
jst.err = wrapError("exit", err)
}
}

// GetResult calls the Javascript 'result' function and returns its value, or any accumulated error
func (jst *Tracer) GetResult() (json.RawMessage, error) {
// Transform the context into a JavaScript object and inject into the state
Expand Down
34 changes: 34 additions & 0 deletions eth/tracers/tracer_test.go
Expand Up @@ -207,3 +207,37 @@ func TestNoStepExec(t *testing.T) {
}
}
}

func TestEnterExit(t *testing.T) {
vmctx := testCtx()
// test that either both or none of enter() and exit() are defined
if _, err := New("{step: function() {}, fault: function() {}, result: function() { return null; }, enter: function() {}}", vmctx.txCtx); err == nil {
t.Fatal("tracer creation should've failed without exit() definition")
}
if _, err := New("{step: function() {}, fault: function() {}, result: function() { return null; }, enter: function() {}, exit: function() {}}", vmctx.txCtx); err != nil {
t.Fatal(err)
}

// test that the enter and exit method are correctly invoked and the values passed
tracer, err := New("{enters: 0, exits: 0, enterGas: 0, gasUsed: 0, step: function() {}, fault: function() {}, result: function() { return {enters: this.enters, exits: this.exits, enterGas: this.enterGas, gasUsed: this.gasUsed} }, enter: function(frame) { this.enters++; this.enterGas = frame.gas; }, exit: function(res) { this.exits++; this.gasUsed = res.gasUsed; }}", vmctx.txCtx)
if err != nil {
t.Fatal(err)
}

env := vm.NewEVM(vm.BlockContext{BlockNumber: big.NewInt(1)}, vm.TxContext{}, &dummyStatedb{}, params.TestChainConfig, vm.Config{Debug: true, Tracer: tracer})
scope := &vm.ScopeContext{
Contract: vm.NewContract(&account{}, &account{}, big.NewInt(0), 0),
}

tracer.CaptureEnter(env, vm.CallType, scope.Contract.Caller(), scope.Contract.Address(), []byte{}, 1000, new(big.Int))
tracer.CaptureExit(env, []byte{}, 400)

have, err := tracer.GetResult()
if err != nil {
t.Fatal(err)
}
want := `{"enters":1,"exits":1,"enterGas":1000,"gasUsed":400}`
if string(have) != want {
t.Errorf("Number of invocations of enter() and exit() is wrong. Have %s, want %s\n", have, want)
}
}

0 comments on commit fca0a4d

Please sign in to comment.