import { SentioProvider } from "./sentio-provider"; import { Breakpoint, InitArguments, Variables, Location, Stackframe, Call, ExternalCall, CallTrace, InternalCall } from "./types"; import * as Codec from "@truffle/codec"; import { safeNativizeVariables } from "./helper"; const Interpreter = require("js-interpreter"); const DebugUtils = require("@truffle/debug-utils"); const truffle_debugger = require("@sentio/truffle-debugger"); const selectors = require("@sentio/truffle-debugger").selectors; let { data, evm, trace, session, sourcemapping, stacktrace, controller, txlog } = selectors; // skip these AST nodes in stepOver, stepInto and stepOut const SKIP_NODE_TYPES = [ "Literal", "Identifier", "MemberAccess", "IndexAccess", // "FunctionDefinition", "ElementaryTypeName" ]; export class Debugger { private static _instance: any; private truffle_debugger: any; private readonly sectionPrintouts = new Set(["bui", "glo", "con", "loc"]); private provider: SentioProvider; private constructor() { } public initialized: boolean = false; public static get Instance(): Debugger { return this._instance || (this._instance = new this()) } public async initialize(init_arguments: InitArguments) { this.provider = new SentioProvider(init_arguments.providerURL, { keepAlive: false }); await this.provider.init( init_arguments.txnHash, init_arguments.chainId, init_arguments.disableOptimizer, init_arguments.codeOverrides || {}, ); this.truffle_debugger = await truffle_debugger.forTx( init_arguments.txnHash, { provider: this.provider, compilations: init_arguments.shimmedCompilations, lightMode: true, storageLookup: init_arguments.storageLookup, }); await this.truffle_debugger.startFullMode(); this.initialized = true; } public async reset() { if (this.truffle_debugger.view(session.status.loaded)) { await this.truffle_debugger.reset(); } } public async stepOver() { await this.truffle_debugger.stepOver(SKIP_NODE_TYPES); } public async stepInto() { // // for debug // while (true) { // await this.truffle_debugger.advance(); // let node = this.truffle_debugger.view(data.current.node) // if (node?.nodeType === "FunctionDefinition") { // break // } // } // console.log("jump dir:", this.truffle_debugger.view(txlog.current.jumpDirection)) // console.log("node:", this.truffle_debugger.view(data.current.node)) // console.log("inst:", this.truffle_debugger.view(sourcemapping.current.instruction)) // console.log("loc:", this.currentLocation()) await this.truffle_debugger.stepInto(SKIP_NODE_TYPES); console.log("jump dir:", this.truffle_debugger.view(txlog.current.jumpDirection)) console.log("node:", this.truffle_debugger.view(data.current.node)) console.log("inst:", this.truffle_debugger.view(sourcemapping.current.instruction)) } public async stepOut() { // await this.truffle_debugger.advance(); // for debug // console.log("jump dir:", this.truffle_debugger.view(txlog.current.jumpDirection)) // console.log("node:", this.truffle_debugger.view(data.current.node)) // console.log("inst:", this.truffle_debugger.view(sourcemapping.current.instruction)) await this.truffle_debugger.stepOut(SKIP_NODE_TYPES); } public async stepNext() { await this.truffle_debugger.stepNext(); } public async advance(steps?: number) { if (steps) { await this.truffle_debugger.advance(steps); } else { await this.truffle_debugger.advance(); } } public async continueUntilBreakpoint() { await this.truffle_debugger.continueUntilBreakpoint(); } public async addBreakpoint(breakpoint: Breakpoint) { await this.truffle_debugger.addBreakpoint(this.determinBreakPoint(breakpoint)); } public async removeBreakpoint(breakpoint: Breakpoint) { await this.truffle_debugger.removeBreakpoint(this.determinBreakPoint(breakpoint)); } public async removeAllBreakpoints() { await this.truffle_debugger.removeAllBreakpoints(); } public async runToEnd() { await this.truffle_debugger.runToEnd(); } public async variables(): Promise { const values = await this.truffle_debugger.variables({ indicateUnknown: true }); const sections = this.truffle_debugger.view(data.current.identifiers.sections); const sectionVars: Variables = { builtin: [], global: [], contract: [], local: [] }; for (const [section, variables] of Object.entries(sections)) { // only check the first 3 characters of each name given in the input sectionPrintouts // since each section name defined in the constructor contains 3 characters const includeThisSection = this.sectionPrintouts.has(section.slice(0, 3)); //@ts-ignore if (includeThisSection && variables.length > 0) { //@ts-ignore for (const variable of variables) { const value = values[variable]; //@ts-ignore sectionVars[section].push({ name: variable, raw: value }); } } } return sectionVars; } public stackTrace(): Stackframe[] { let final: boolean = false; if (this.truffle_debugger.view(session.status.loaded)) { //print final report if finished & failed, intermediate if not if ( this.truffle_debugger.view(trace.finished) && !this.truffle_debugger.view(evm.transaction.status) ) { final = true; //print final stack trace } else { final = false; //intermediate call stack } } let report = final ? this.truffle_debugger.view(stacktrace.current.finalReport) : this.truffle_debugger.view(stacktrace.current.report); return report; } public currentLocation(session?: any): Location | undefined { session = session || this.truffle_debugger; if (session.view(trace.finishedOrUnloaded)) { return undefined; } const currentLocation = session.view(controller.current.location); const currentLines = currentLocation.sourceRange ? currentLocation.sourceRange.lines : null; const currentLength = currentLocation.sourceRange ? currentLocation.sourceRange.length : null; const currentSourceId = currentLocation.source ? currentLocation.source.id : null; const index = session.view(trace.index); // if there's no source if (currentSourceId === undefined) { return { lines: currentLines, length: currentLength, instructionIndex: index, } } //search sources for given string let sources = Object.values( this.truffle_debugger.view(sourcemapping.views.sources) ); //we will indeed need the sources here, not just IDs let matchingSources = sources.filter(source => // @ts-ignore source.id === currentSourceId ); return { // @ts-ignore compilationId: matchingSources[0]?.compilationId, lines: currentLines, length: currentLength, // @ts-ignore sourcePath: matchingSources[0]?.sourcePath, instructionIndex: index, } } public currentStep(): any { return this.truffle_debugger.view(trace.step); } public async terminate() { this.truffle_debugger = null; this.initialized = false; } public async getCallTrace(): Promise { const bugger = this.truffle_debugger; await bugger.resetTrace(); let finished: boolean; let allCallStack: Call[] = []; let funcDef: any = undefined; // Initial call const { address, sender, data: calldata } = bugger.view(txlog.current.call); if (address) { const rootCall: Call = { pc: 0, op: "CALL", location: this.currentLocation(bugger), from: sender, to: address, inputs: [calldata], calls: [], depth: 0 } allCallStack.push(rootCall) } else { // TODO: handle create } const filter = (op: string) => { return op.includes("JUMP") || ["CALL", "STATICCALL", "DELEGATECALL", "CALLCODE", "CREATE", "CREATE2", "RETURN", "STOP", "REVERT"].includes(op) } do { await bugger.advance(1, filter); const location = this.currentLocation(bugger); const step = bugger.view(trace.step); const op = step.op; const pc = step.pc; const depth = step.depth; const from = bugger.view(evm.current.call).sender; finished = bugger.view(controller.current.trace.finished); const astNode = bugger.view(data.current.node); if (allCallStack[allCallStack.length - 1].depth > depth) { let topCall = allCallStack.pop(); let sameLevelCalls = []; while (topCall && topCall.depth > depth) { sameLevelCalls.push(topCall); topCall = allCallStack.pop() } if (topCall) { topCall.calls = topCall.calls.concat(sameLevelCalls.reverse()); allCallStack.push(topCall); } } const nextDepth = bugger.view(evm.next.state).depth; if ((nextDepth !== -1 && nextDepth < depth || bugger.view(trace.stepsRemaining) === 0)) { // if (bugger.view(txlog.current.isHalting)) { const decodings = await this.decodeReturnValue(bugger); // TODO(pcxu): handle selfdestruct abd revert case //functions get returnValues //@ts-ignore const decoding = decodings.find? decodings.find( //@ts-ignore decoding => decoding.kind === "return" ): undefined; if (decoding) { //we'll trust this method over the method resulting from an internal return, //*if* it produces a valid return-value decoding. if it doesn't, we ignore it. // externalCallStack[externalCallStack.length - 1].returnValue = decoding.arguments; if (allCallStack.length > 1) { let topCall = allCallStack.pop(); topCall!.returnValue = decoding.arguments; var left = allCallStack.length; if (allCallStack[left-1].calls === undefined) { allCallStack[left-1].calls = []; } allCallStack[left-1].calls.push(topCall!); } } } // If a new contract is being created, add to the call stack if ((op == 'CREATE' || op == "CREATE2")) { // Assemble the internal call report and store for completion var call: ExternalCall = { from: "", to: "", op: op, pc: pc, inputs: [], calls: [], location, depth }; allCallStack.push(call); continue; } // If a contract is being self destructed, gather that as a subcall too if (op === 'SELFDESTRUCT') { var left = allCallStack.length; if (allCallStack[left-1].calls === undefined) { allCallStack[left-1].calls = []; } allCallStack[left-1].calls.push({ op: op, from: "", to: "", pc: pc, inputs: [], calls: [], location, depth }); continue; } // Handle the external call and delegate call if (op === "CALL" || op === 'CALLCODE' || op === 'DELEGATECALL' || op === 'STATICCALL') { let to = bugger.view(txlog.current.callAddress); const context = bugger.view(txlog.current.callContext); const decoding = this.decodeCall(bugger); const contractName = context ? context.contractName : undefined; let functionName, variables; // @ts-ignore if (decoding.kind === "function" || decoding.kind === "constructor") { // @ts-ignore functionName = decoding.abi?.name; // @ts-ignore variables = decoding.arguments; } // Assemble the internal call report and store for completion let thisCall: ExternalCall = { op: op, from: from, to: to, contractName: contractName, inputs: variables, functionName: functionName, pc: pc, calls: [], location, depth }; allCallStack.push(thisCall); continue; } else if (bugger.view(txlog.current.isJump)) { const jumpDirection = bugger.view(txlog.current.jumpDirection); if (jumpDirection === "i" && astNode && astNode.nodeType === "FunctionCall") { if (!(bugger.view(txlog.current.waitingForInternalCallToAbsorb))) { allCallStack.push({ op, from, pc, inputs: [], calls: [], location, depth }) } } else if (jumpDirection === "o") { const astMatchesTxLog = bugger.view( txlog.current.currentFunctionIsAsExpected ); //don't log returns from the wrong function...? //(I've added this second check due to a strange case Amal found, hopefully this doesn't screw anything up) if (astMatchesTxLog || funcDef?.name === allCallStack[allCallStack.length - 1].functionName) { //in this case, we have to do decoding & fn identification let outputAllocations = bugger.view( txlog.current.outputParameterAllocations ); if (!outputAllocations && astNode && astNode.nodeType === 'YulBlock') { outputAllocations = this.locateParameters(funcDef.returnParameters.parameters, step.stack.length - 2) } if (outputAllocations) { let topCall = allCallStack.pop(); if (topCall) { topCall.returnValue = this.getInputsOrOutputs( bugger, outputAllocations, ); var left = allCallStack.length; if (left === 0) { allCallStack.push(topCall) } else { allCallStack[left-1].calls.push(topCall); } } } } } } else if (astNode && astNode.nodeType === "FunctionDefinition") { const txlogNext = bugger.view(txlog.next); if (txlogNext.inInternalSourceOrYul && (txlogNext.astNode != null || txlogNext.source.internal != null)) { continue; } if (bugger.view(txlog.current.waitingForFunctionDefinition)) { const internalFunctionsTable = bugger.view( data.current.functionsByProgramCounter ); if (internalFunctionsTable[pc] != null) { funcDef = internalFunctionsTable[pc].node; } const inputAllocations = bugger.view( txlog.current.inputParameterAllocations ); if (inputAllocations) { const functionNode = bugger.view(txlog.current.astNode); const contractNode = bugger.view(txlog.current.contract); allCallStack[allCallStack.length - 1].inputs = this.getInputsOrOutputs(bugger, inputAllocations); allCallStack[allCallStack.length - 1].functionName = functionNode.name || undefined; // @ts-ignore allCallStack[allCallStack.length - 1].contractName = contractNode && contractNode.nodeType === "ContractDefinition" ? contractNode.name : null; } } } // If an existing call is returning, pop off the call stack if (op === 'REVERT') { (allCallStack[allCallStack.length - 1] as ExternalCall).error = "execution reverted"; continue; } } while (!finished); for (const call of allCallStack) { if (call.depth === 1) { allCallStack[0].calls.push(call); } } await bugger.reset(); return [allCallStack[0]]; } public async evalExpression(expression: string): Promise { let variables = await this.truffle_debugger.variables({ indicateUnknown: true }); //if we're just dealing with a single variable, handle that case //separately (so that we can do things in a better way for that //case) let variable = expression.trim(); if (variable in variables) { return variables[variable]; } // converts all !<...> expressions to JS-valid selector requests const preprocessSelectors = (expr: string) => { const regex = /!<([^>]+)>/g; const select = "$"; // expect repl context to have this func const replacer = (_: any, selector: any) => `${select}("${selector}")`; return expr.replace(regex, replacer); }; //HACK //if we're not in the single-variable case, we'll need to do some //things to Javascriptify our variables so that the JS syntax for //using them is closer to the Solidity syntax let context = safeNativizeVariables(variables); //HACK -- we can't use "this" as a variable name, so we're going to //find an available replacement name, and then modify the context //and expression appropriately let pseudoThis = "_this"; while (pseudoThis in context) { pseudoThis = "_" + pseudoThis; } //in addition to pseudoThis, which replaces this, we also have //pseudoPseudoThis, which replaces pseudoThis in order to ensure //that any uses of pseudoThis yield an error instead of showing this let pseudoPseudoThis = "thereisnovariableofthatname"; while (pseudoPseudoThis in context) { pseudoPseudoThis = "_" + pseudoPseudoThis; } context = DebugUtils.cleanThis(context, pseudoThis); let expr = expression.replace( //those characters in [] are the legal JS variable name characters //note that pseudoThis contains no special characters new RegExp("(?) { const select = (expr: string) => { let selector, result; try { selector = expr .split(".") .reduce((sel, next) => (next.length ? sel[next] : sel), selectors); } catch (_) { throw new Error(`Unknown selector: ${expr}`); } // throws its own exception result = this.truffle_debugger.view(selector); return result; }; let interpreter; interpreter = new Interpreter(expression, function ( interpreter: any, globalObject: any ) { //first let's set up our select function (which will be called $) interpreter.setProperty( globalObject, "$", interpreter.createNativeFunction((selectorName: any) => { return interpreter.nativeToPseudo(select(selectorName)); }) ); //now let's set up the variables for (const [variable, value] of Object.entries(context)) { try { //note: circular objects wll raise an exception here and get excluded. interpreter.setProperty( globalObject, variable, interpreter.nativeToPseudo(value) ); } catch (_) { //just omit things that don't work } } }); interpreter.run(); return interpreter.pseudoToNative(interpreter.value); } private determinBreakPoint(breakPoint: Breakpoint) { //search sources for given string let sources = Object.values( this.truffle_debugger.view(sourcemapping.views.sources) ); //we will indeed need the sources here, not just IDs let matchingSources = sources.filter(source => // @ts-ignore source.sourcePath === breakPoint.sourcePath && source.compilationId === breakPoint.compilationId ); return { // @ts-ignore sourceId: matchingSources[0].id, line: breakPoint.line } } private getInputsOrOutputs(session: any, inputOrOutputAllocations: any) { const userDefinedTypes = session.view(data.views.userDefinedTypes); const state = session.view(data.current.state); const mappingKeys = session.view(data.views.mappingKeys); const allocations = session.view(data.info.allocations); const contexts = session.view(data.views.contexts); const currentContext = session.view(data.current.context); const internalFunctionsTable = session.view( data.current.functionsByProgramCounter ); let variables: any[] = []; if (inputOrOutputAllocations) { const compilationId = session.view(txlog.current.compilationId); //can't do a yield* inside a map, have to do this loop manually for (let { name, definition, pointer } of inputOrOutputAllocations) { const decoder = Codec.decodeVariable( definition, pointer, { userDefinedTypes: userDefinedTypes, state, mappingKeys: mappingKeys, allocations: allocations, contexts: contexts, currentContext, internalFunctionsTable }, compilationId ); variables.push({ name, value: decoder.next().value }); } } return variables; } private async decodeReturnValue(session: any) { const userDefinedTypes = session.view(data.views.userDefinedTypes); const state = session.view(data.next.state); //next state has the return data const allocations = session.view(data.info.allocations); const contexts = session.view(data.views.contexts); const currentContext = session.view(data.current.context); const status = session.view(data.current.returnStatus); //may be undefined const returnAllocation = session.view(data.current.returnAllocation); //may be null const errorId = session.view(data.current.errorId); const internalFunctionsTable = session.view( data.current.internalFunctionsTable ); const decoder = Codec.decodeReturndata( { userDefinedTypes, state, allocations, contexts, currentContext, internalFunctionsTable }, returnAllocation, status, errorId ); let result = decoder.next(); // while (!result.done) { // let request = result.value; // let response; // switch (request.type) { // //skip storage case, it won't happen here // case "code": // response = yield* evm.requestCode(request.address); // break; // default: // debug("unrecognized request type!"); // } // debug("sending response"); // result = decoder.next(response); // } //at this point, result.value holds the final value return result.value; } private decodeCall(session: any, decodeCurrent = false) { const isCall = session.view(data.current.isCall); const isCreate = session.view(data.current.isCreate); if (!isCall && !isCreate && !decodeCurrent) { return null; } const currentCallIsCreate = session.view(data.current.currentCallIsCreate); const userDefinedTypes = session.view(data.views.userDefinedTypes); let state = decodeCurrent ? session.view(data.current.state) : session.view(data.next.state); if (decodeCurrent && currentCallIsCreate) { //if we want to decode the *current* call, but the current call //is a creation, we had better pass in the code, not the calldata state = { ...state, calldata: state.code }; } const allocations = session.view(data.info.allocations); const contexts = session.view(data.views.contexts); const context = decodeCurrent ? session.view(data.current.context) : session.view(data.current.callContext); const isConstructor = decodeCurrent ? session.view(data.current.currentCallIsCreate) : isCreate; const decoder = Codec.decodeCalldata( { state, userDefinedTypes, allocations, contexts, currentContext: context }, isConstructor ); let result = decoder.next(); // while (!result.done) { // let request = result.value; // let response; // switch (request.type) { // //skip storage case, it won't happen here // case "code": // response = yield* evm.requestCode(request.address); // break; // default: // debug("unrecognized request type!"); // } // debug("sending response"); // result = decoder.next(response); // } //at this point, result.value holds the final value return result.value; } private locateParameters(parameters: any[], top: number) { const reverseParameters = parameters.slice().reverse(); //note we clone before reversing because reverse() is in place let results = []; let currentPosition = top; for (let parameter of reverseParameters) { const words = Codec.Ast.Utils.stackSize(parameter); const pointer = { location: "stack", from: currentPosition - words + 1, to: currentPosition }; results.unshift({ name: parameter.name ? parameter.name : undefined, //replace "" with undefined definition: parameter, pointer }); currentPosition -= words; } return results; } }