( template: WalletTemplate, artifact: Artifact | undefined, unlockingScriptId: string, scenarioId: string, )
| 64 | }; |
| 65 | |
| 66 | const debugSingleScenario = ( |
| 67 | template: WalletTemplate, artifact: Artifact | undefined, unlockingScriptId: string, scenarioId: string, |
| 68 | ): DebugResult => { |
| 69 | const { vm, program } = createProgram(template, unlockingScriptId, scenarioId); |
| 70 | |
| 71 | const fullDebugSteps = vm.debug(program); |
| 72 | |
| 73 | // P2SH executions have 3 phases, we only want the last one (locking script execution) |
| 74 | // https://libauth.org/types/AuthenticationVirtualMachine.html#__type.debug |
| 75 | const lockingScriptDebugResult = fullDebugSteps.slice(findLastIndex(fullDebugSteps, (state) => state.ip === 0)); |
| 76 | |
| 77 | // The controlStack determines whether the current debug step is in the executed branch |
| 78 | // It also tracks loop / function usage, but for the purpose of determining whether a step was executed, |
| 79 | // we only need to check that there are no 'false' items in the control stack. |
| 80 | // https://libauth.org/types/AuthenticationProgramStateControlStack.html |
| 81 | // https://github.com/bitjson/bch-loops#control-stack |
| 82 | const executedDebugSteps = lockingScriptDebugResult |
| 83 | .filter((debugStep) => debugStep.controlStack.every(item => item !== false)); |
| 84 | |
| 85 | // P2PKH inputs do not have an artifact, so we skip the console.log handling |
| 86 | if (artifact) { |
| 87 | // Try to match each executed debug step to a log entry if it exists. Note that inside loops, |
| 88 | // the same log statement may be executed multiple times in different debug steps |
| 89 | // Also note that multiple log statements may exist for the same ip, so we need to handle all of them |
| 90 | const executedLogs = executedDebugSteps |
| 91 | .flatMap((debugStep, index) => { |
| 92 | const logEntries = artifact.debug?.logs?.filter((log) => log.ip === debugStep.ip); |
| 93 | if (!logEntries || logEntries.length === 0) return []; |
| 94 | |
| 95 | const reversedPriorDebugSteps = executedDebugSteps.slice(0, index + 1).reverse(); |
| 96 | |
| 97 | return logEntries.map((logEntry) => { |
| 98 | const decodedLogData = logEntry.data |
| 99 | .map((dataEntry) => decodeLogDataEntry(dataEntry, reversedPriorDebugSteps, vm)); |
| 100 | return { logEntry, decodedLogData }; |
| 101 | }); |
| 102 | }); |
| 103 | |
| 104 | for (const { logEntry, decodedLogData } of executedLogs) { |
| 105 | const inputIndex = extractInputIndexFromScenario(scenarioId); |
| 106 | logConsoleLogStatement(logEntry, decodedLogData, artifact.contractName, inputIndex); |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | const lastExecutedDebugStep = executedDebugSteps[executedDebugSteps.length - 1]; |
| 111 | |
| 112 | // If an error is present in the last step, that means a require statement in the middle of the function failed |
| 113 | if (lastExecutedDebugStep.error) { |
| 114 | // In Libauth any thrown error gets registered in the instruction that happens right *after* the instruction that |
| 115 | // caused the error (in other words the OP_VERIFY). We need to decrement the instruction pointer to get the correct |
| 116 | // failing instruction. |
| 117 | const failingIp = lastExecutedDebugStep.ip - 1; |
| 118 | const failingInstruction = lastExecutedDebugStep.instructions[failingIp]; |
| 119 | |
| 120 | // With optimisations, the OP_CHECKSIG and OP_VERIFY instructions are merged into a single opcode (OP_CHECKSIGVERIFY). |
| 121 | // However, for the final verify, the OP_VERIFY is not present. In most cases, the implicit final VERIFY is checked |
| 122 | // later in the code. However, for NULLFAIL, the error is thrown in the OP_CHECKSIG opcode, rather than in the |
| 123 | // implicit final VERIFY. The error message is registered in the next instruction, so we need to increment the |
no test coverage detected