From fe94594526123f42abbe5af524a58a4c8cea0cf8 Mon Sep 17 00:00:00 2001 From: laizy Date: Mon, 11 Dec 2023 16:44:45 +0800 Subject: [PATCH] add call tracer function --- vm/evm/evm.go | 77 ++++++++++++---- vm/evm/instructions.go | 4 + vm/evm/logger.go | 18 +++- vm/evm/logger_json.go | 7 +- vm/evm/runtime/runtime_test.go | 7 +- vm/evm/tracers/call_tracer.go | 162 +++++++++++++++++++++++++++++++++ 6 files changed, 254 insertions(+), 21 deletions(-) create mode 100644 vm/evm/tracers/call_tracer.go diff --git a/vm/evm/evm.go b/vm/evm/evm.go index 0c7694136..d7b5f8d28 100644 --- a/vm/evm/evm.go +++ b/vm/evm/evm.go @@ -201,10 +201,15 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas if !evm.StateDB.Exist(addr) { if !isPrecompile && evm.chainRules.IsEIP158 && value.Sign() == 0 { - // Calling a non existing account, don't do anything, but ping the tracer - if evm.vmConfig.Debug && evm.depth == 0 { - evm.vmConfig.Tracer.CaptureStart(caller.Address(), addr, false, input, gas, value) - evm.vmConfig.Tracer.CaptureEnd(ret, 0, 0, nil) + // Calling a non-existing account, don't do anything, but ping the tracer + if evm.vmConfig.Debug { + if evm.depth == 0 { + evm.vmConfig.Tracer.CaptureStart(evm, caller.Address(), addr, false, input, gas, value) + evm.vmConfig.Tracer.CaptureEnd(ret, 0, 0, nil) + } else { + evm.vmConfig.Tracer.CaptureEnter(CALL, caller.Address(), addr, input, gas, value) + evm.vmConfig.Tracer.CaptureExit(ret, 0, nil) + } } return nil, gas, nil } @@ -213,11 +218,19 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas evm.Context.Transfer(evm.StateDB, caller.Address(), addr, value) // Capture the tracer start/end events in debug mode - if evm.vmConfig.Debug && evm.depth == 0 { - evm.vmConfig.Tracer.CaptureStart(caller.Address(), addr, false, input, gas, value) - defer func(startGas uint64, startTime time.Time) { // Lazy evaluation of the parameters - evm.vmConfig.Tracer.CaptureEnd(ret, startGas-gas, time.Since(startTime), err) - }(gas, time.Now()) + if evm.vmConfig.Debug { + if evm.depth == 0 { + evm.vmConfig.Tracer.CaptureStart(evm, caller.Address(), addr, false, input, gas, value) + defer func(startGas uint64, startTime time.Time) { // Lazy evaluation of the parameters + evm.vmConfig.Tracer.CaptureEnd(ret, startGas-gas, time.Since(startTime), err) + }(gas, time.Now()) + } else { + // Handle tracer events for entering and exiting a call frame + evm.vmConfig.Tracer.CaptureEnter(CALL, caller.Address(), addr, input, gas, value) + defer func(startGas uint64) { + evm.vmConfig.Tracer.CaptureExit(ret, startGas-gas, err) + }(gas) + } } if isPrecompile { @@ -276,6 +289,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.vmConfig.Debug { + evm.vmConfig.Tracer.CaptureEnter(CALLCODE, caller.Address(), addr, input, gas, value) + defer func(startGas uint64) { + evm.vmConfig.Tracer.CaptureExit(ret, startGas-gas, err) + }(gas) + } + // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { ret, gas, err = RunPrecompiledContract(p, input, gas) @@ -314,6 +335,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.vmConfig.Debug { + evm.vmConfig.Tracer.CaptureEnter(DELEGATECALL, caller.Address(), addr, input, gas, nil) + defer func(startGas uint64) { + evm.vmConfig.Tracer.CaptureExit(ret, startGas-gas, err) + }(gas) + } + // It is allowed to call precompiles, even via delegatecall if p, isPrecompile := evm.precompile(addr); isPrecompile { ret, gas, err = RunPrecompiledContract(p, input, gas) @@ -361,6 +390,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.vmConfig.Debug { + evm.vmConfig.Tracer.CaptureEnter(STATICCALL, caller.Address(), addr, input, gas, nil) + defer func(startGas uint64) { + evm.vmConfig.Tracer.CaptureExit(ret, startGas-gas, err) + }(gas) + } + if p, isPrecompile := evm.precompile(addr); isPrecompile { ret, gas, err = RunPrecompiledContract(p, input, gas) } else { @@ -402,7 +439,7 @@ func (c *codeAndHash) Hash() common.Hash { } // create creates a new contract using code as deployment code. -func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *big.Int, address common.Address) ([]byte, common.Address, uint64, error) { +func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, value *big.Int, address common.Address, typ OpCode) ([]byte, common.Address, uint64, error) { // Depth check execution. Fail if we're trying to execute above the // limit. if evm.depth > int(params.CallCreateDepth) { @@ -435,8 +472,12 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, return nil, address, gas, nil } - if evm.vmConfig.Debug && evm.depth == 0 { - evm.vmConfig.Tracer.CaptureStart(caller.Address(), address, true, codeAndHash.code, gas, value) + if evm.vmConfig.Debug { + if evm.depth == 0 { + evm.vmConfig.Tracer.CaptureStart(evm, caller.Address(), address, true, codeAndHash.code, gas, value) + } else { + evm.vmConfig.Tracer.CaptureEnter(typ, caller.Address(), address, codeAndHash.code, gas, value) + } } start := time.Now() @@ -472,8 +513,12 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, if maxCodeSizeExceeded && err == nil { err = errors.ErrMaxCodeSizeExceeded } - if evm.vmConfig.Debug && evm.depth == 0 { - evm.vmConfig.Tracer.CaptureEnd(ret, gas-contract.Gas, time.Since(start), err) + if evm.vmConfig.Debug { + if evm.depth == 0 { + evm.vmConfig.Tracer.CaptureEnd(ret, gas-contract.Gas, time.Since(start), err) + } else { + evm.vmConfig.Tracer.CaptureExit(ret, gas-contract.Gas, err) + } } return ret, address, contract.Gas, err @@ -482,7 +527,7 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, // Create creates a new contract using code as deployment code. func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *big.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) { contractAddr = crypto.CreateAddress(caller.Address(), evm.StateDB.GetNonce(caller.Address())) - return evm.create(caller, &codeAndHash{code: code}, gas, value, contractAddr) + return evm.create(caller, &codeAndHash{code: code}, gas, value, contractAddr, CREATE) } // Create2 creates a new contract using code as deployment code. @@ -492,7 +537,7 @@ func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *big.I func (evm *EVM) Create2(caller ContractRef, code []byte, gas uint64, endowment *big.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) { codeAndHash := &codeAndHash{code: code} contractAddr = crypto.CreateAddress2(caller.Address(), salt.Bytes32(), codeAndHash.Hash().Bytes()) - return evm.create(caller, codeAndHash, gas, endowment, contractAddr) + return evm.create(caller, codeAndHash, gas, endowment, contractAddr, CREATE2) } // ChainConfig returns the environment's chain configuration diff --git a/vm/evm/instructions.go b/vm/evm/instructions.go index e0bfbc4b3..a99d22241 100644 --- a/vm/evm/instructions.go +++ b/vm/evm/instructions.go @@ -826,6 +826,10 @@ func opSuicide(pc *uint64, interpreter *EVMInterpreter, callContext *callCtx) ([ interpreter.evm.StateDB.AddBalance(beneficiary.Bytes20(), balance) MakeOngTransferLog(interpreter.evm.StateDB, callContext.contract.Address(), beneficiary.Bytes20(), balance) interpreter.evm.StateDB.Suicide(callContext.contract.Address()) + if interpreter.cfg.Debug { + interpreter.cfg.Tracer.CaptureEnter(SELFDESTRUCT, callContext.contract.Address(), beneficiary.Bytes20(), []byte{}, 0, balance) + interpreter.cfg.Tracer.CaptureExit([]byte{}, 0, nil) + } return nil, nil } diff --git a/vm/evm/logger.go b/vm/evm/logger.go index 7fe6992bf..0ec38efb0 100644 --- a/vm/evm/logger.go +++ b/vm/evm/logger.go @@ -110,9 +110,11 @@ func (s *StructLog) ErrorString() string { // Note that reference types are actual VM data structures; make copies // if you need to retain them beyond the current call. type Tracer interface { - CaptureStart(from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) + 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, memory *Memory, stack *Stack, rStack *ReturnStack, rData []byte, contract *Contract, depth int, err error) + CaptureEnter(typ OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) + CaptureExit(output []byte, gasUsed uint64, err error) CaptureFault(env *EVM, pc uint64, op OpCode, gas, cost uint64, memory *Memory, stack *Stack, rStack *ReturnStack, contract *Contract, depth int, err error) CaptureEnd(output []byte, gasUsed uint64, t time.Duration, err error) @@ -144,7 +146,7 @@ func NewStructLogger(cfg *LogConfig) *StructLogger { } // CaptureStart implements the Tracer interface to initialize the tracing operation. -func (l *StructLogger) CaptureStart(from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { +func (l *StructLogger) CaptureStart(env *EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { } // CaptureState logs a new structured log message and pushes it out to the environment @@ -229,6 +231,11 @@ func (l *StructLogger) CaptureEnd(output []byte, gasUsed uint64, t time.Duration } } +func (l *StructLogger) CaptureEnter(typ OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { +} + +func (l *StructLogger) CaptureExit(output []byte, gasUsed uint64, err error) {} + // StructLogs returns the captured log entries. func (l *StructLogger) StructLogs() []StructLog { return l.logs } @@ -306,7 +313,7 @@ func NewMarkdownLogger(cfg *LogConfig, writer io.Writer) *mdLogger { return l } -func (t *mdLogger) CaptureStart(from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { +func (t *mdLogger) CaptureStart(env *EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { if !create { _, _ = fmt.Fprintf(t.out, "From: `%v`\nTo: `%v`\nData: `0x%x`\nGas: `%d`\nValue `%v` wei\n", from.String(), to.String(), @@ -361,3 +368,8 @@ 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(typ OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { +} + +func (t *mdLogger) CaptureExit(output []byte, gasUsed uint64, err error) {} diff --git a/vm/evm/logger_json.go b/vm/evm/logger_json.go index fbdc328d3..888a3f5b2 100644 --- a/vm/evm/logger_json.go +++ b/vm/evm/logger_json.go @@ -42,7 +42,7 @@ func NewJSONLogger(cfg *LogConfig, writer io.Writer) *JSONLogger { return l } -func (l *JSONLogger) CaptureStart(from common.Address, to common.Address, create bool, input []byte, +func (l *JSONLogger) CaptureStart(env *EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { } @@ -99,3 +99,8 @@ func (l *JSONLogger) CaptureEnd(output []byte, gasUsed uint64, t time.Duration, _ = l.encoder.Encode(endLog{common.Bytes2Hex(output), math.HexOrDecimal64(gasUsed), t, ""}) return } + +func (l *JSONLogger) CaptureEnter(typ OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { +} + +func (l *JSONLogger) CaptureExit(output []byte, gasUsed uint64, err error) {} diff --git a/vm/evm/runtime/runtime_test.go b/vm/evm/runtime/runtime_test.go index 044f6917a..486d27921 100644 --- a/vm/evm/runtime/runtime_test.go +++ b/vm/evm/runtime/runtime_test.go @@ -336,7 +336,7 @@ type stepCounter struct { steps int } -func (s *stepCounter) CaptureStart(from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { +func (s *stepCounter) CaptureStart(env *evm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { } func (s *stepCounter) CaptureState(env *evm.EVM, pc uint64, op evm.OpCode, gas, cost uint64, @@ -354,6 +354,11 @@ func (s *stepCounter) CaptureFault(env *evm.EVM, pc uint64, op evm.OpCode, gas, func (s *stepCounter) CaptureEnd(output []byte, gasUsed uint64, t time.Duration, err error) { } +func (s *stepCounter) CaptureEnter(typ evm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { +} + +func (s *stepCounter) CaptureExit(output []byte, gasUsed uint64, err error) {} + func TestJumpSub1024Limit(t *testing.T) { db := storage.NewCacheDB(overlaydb.NewOverlayDB(leveldbstore.NewMemLevelDBStore())) statedb := storage.NewStateDB(db, common.Hash{}, common.Hash{}, ong.OngBalanceHandle{}) diff --git a/vm/evm/tracers/call_tracer.go b/vm/evm/tracers/call_tracer.go new file mode 100644 index 000000000..a75cb22ec --- /dev/null +++ b/vm/evm/tracers/call_tracer.go @@ -0,0 +1,162 @@ +package tracers + +import ( + "encoding/hex" + "encoding/json" + "errors" + "math/big" + "strconv" + "strings" + "sync/atomic" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ontio/ontology/vm/evm" +) + +type CallFrame struct { + Type string `json:"type"` + From string `json:"from"` + To string `json:"to,omitempty"` + Value string `json:"value,omitempty"` + Gas string `json:"gas"` + GasUsed string `json:"gasUsed"` + Input string `json:"input"` + Output string `json:"output,omitempty"` + Error string `json:"error,omitempty"` + Calls []CallFrame `json:"calls,omitempty"` +} + +type CallTracer struct { + env *evm.EVM + callstack []CallFrame + interrupt uint32 // Atomic flag to signal execution interruption + reason error // Textual reason for the interruption +} + +func NewCallTracer() *CallTracer { + // First callframe contains tx context info + // and is populated on start and end. + t := &CallTracer{callstack: make([]CallFrame, 1)} + return t +} + +// CaptureStart implements the EVMLogger interface to initialize the tracing operation. +func (t *CallTracer) CaptureStart(env *evm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) { + t.env = env + t.callstack[0] = CallFrame{ + Type: "CALL", + From: addrToHex(from), + To: addrToHex(to), + Input: bytesToHex(input), + Gas: uintToHex(gas), + Value: bigToHex(value), + } + if create { + t.callstack[0].Type = "CREATE" + } +} + +// CaptureEnd is called after the call finishes to finalize the tracing. +func (t *CallTracer) CaptureEnd(output []byte, gasUsed uint64, _ time.Duration, err error) { + t.callstack[0].GasUsed = uintToHex(gasUsed) + if err != nil { + t.callstack[0].Error = err.Error() + if err.Error() == "execution reverted" && len(output) > 0 { + t.callstack[0].Output = bytesToHex(output) + } + } else { + t.callstack[0].Output = bytesToHex(output) + } +} + +// CaptureState implements the EVMLogger interface to trace a single step of VM execution. +func (t *CallTracer) CaptureState(env *evm.EVM, pc uint64, op evm.OpCode, gas, cost uint64, memory *evm.Memory, stack *evm.Stack, + rStack *evm.ReturnStack, rData []byte, contract *evm.Contract, depth int, err error) { +} + +// CaptureFault implements the EVMLogger interface to trace an execution fault. +func (t *CallTracer) CaptureFault(env *evm.EVM, pc uint64, op evm.OpCode, gas, cost uint64, memory *evm.Memory, + stack *evm.Stack, rStack *evm.ReturnStack, contract *evm.Contract, depth int, err error) { +} + +// CaptureEnter is called when EVM enters a new scope (via call, create or selfdestruct). +func (t *CallTracer) CaptureEnter(typ evm.OpCode, from common.Address, to common.Address, input []byte, gas uint64, value *big.Int) { + // Skip if tracing was interrupted + if atomic.LoadUint32(&t.interrupt) > 0 { + t.env.Cancel() + return + } + + call := CallFrame{ + Type: typ.String(), + From: addrToHex(from), + To: addrToHex(to), + Input: bytesToHex(input), + Gas: uintToHex(gas), + Value: bigToHex(value), + } + t.callstack = append(t.callstack, call) +} + +// CaptureExit is called when EVM exits a scope, even if the scope didn't +// execute any code. +func (t *CallTracer) CaptureExit(output []byte, gasUsed uint64, err error) { + size := len(t.callstack) + if size <= 1 { + return + } + // pop call + call := t.callstack[size-1] + t.callstack = t.callstack[:size-1] + size -= 1 + + call.GasUsed = uintToHex(gasUsed) + if err == nil { + call.Output = bytesToHex(output) + } else { + call.Error = err.Error() + if call.Type == "CREATE" || call.Type == "CREATE2" { + call.To = "" + } + } + t.callstack[size-1].Calls = append(t.callstack[size-1].Calls, call) +} + +// GetResult returns the json-encoded nested list of call traces, and any +// error arising from the encoding or forceful termination (via `Stop`). +func (t *CallTracer) GetResult() (json.RawMessage, error) { + if len(t.callstack) != 1 { + return nil, errors.New("incorrect number of top-level calls") + } + res, err := json.Marshal(t.callstack[0]) + if err != nil { + return nil, err + } + return json.RawMessage(res), t.reason +} + +// Stop terminates execution of the tracer at the first opportune moment. +func (t *CallTracer) Stop(err error) { + t.reason = err + atomic.StoreUint32(&t.interrupt, 1) +} + +func bytesToHex(s []byte) string { + return "0x" + hex.EncodeToString(s) +} + +func bigToHex(n *big.Int) string { + if n == nil { + return "" + } + return "0x" + n.Text(16) +} + +func uintToHex(n uint64) string { + return "0x" + strconv.FormatUint(n, 16) +} + +func addrToHex(a common.Address) string { + return strings.ToLower(a.Hex()) +}