Skip to content
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

feat: Add support for assert messages & runtime call stacks #1997

Merged
merged 10 commits into from
Sep 5, 2023
2 changes: 1 addition & 1 deletion yarn-project/acir-simulator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"@aztec/circuits.js": "workspace:^",
"@aztec/foundation": "workspace:^",
"@aztec/types": "workspace:^",
"acvm_js": "github:noir-lang/acvm-js-wasm#arv/0.24.1",
"acvm_js": "github:noir-lang/acvm-js-wasm#arv/0.25.0",
"levelup": "^5.1.1",
"memdown": "^6.1.1",
"tslib": "^2.4.0"
Expand Down
108 changes: 45 additions & 63 deletions yarn-project/acir-simulator/src/acvm/acvm.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { FunctionDebugMetadata } from '@aztec/foundation/abi';
import { FunctionDebugMetadata, OpcodeLocation } from '@aztec/foundation/abi';
import { AztecAddress } from '@aztec/foundation/aztec-address';
import { EthAddress } from '@aztec/foundation/eth-address';
import { Fr } from '@aztec/foundation/fields';
import { createDebugLogger } from '@aztec/foundation/log';
import { NoirCallStack } from '@aztec/types';
import { NoirCallStack, SourceCodeLocation } from '@aztec/types';

import {
ExecutionError,
ForeignCallInput,
ForeignCallOutput,
WasmBlackBoxFunctionSolver,
Expand Down Expand Up @@ -70,18 +71,14 @@ export interface ACIRExecutionResult {
partialWitness: ACVMWitness;
}

/**
* Extracts the opcode location from an ACVM error string.
*/
function extractOpcodeLocationFromError(err: string): string | undefined {
const match = err.match(/^Cannot satisfy constraint (?<opcodeLocation>[0-9]+(?:\.[0-9]+)?)/);
return match?.groups?.opcodeLocation;
}

/**
* Extracts the call stack from the location of a failing opcode and the debug metadata.
* One opcode can point to multiple calls due to inlining.
*/
function getCallStackFromOpcodeLocation(opcodeLocation: string, debug: FunctionDebugMetadata): NoirCallStack {
function getSourceCodeLocationsFromOpcodeLocation(
opcodeLocation: string,
debug: FunctionDebugMetadata,
): SourceCodeLocation[] {
const { debugSymbols, files } = debug;

const callStack = debugSymbols.locations[opcodeLocation] || [];
Expand All @@ -92,45 +89,32 @@ function getCallStackFromOpcodeLocation(opcodeLocation: string, debug: FunctionD

const locationText = source.substring(span.start, span.end + 1);
const precedingText = source.substring(0, span.start);
const line = precedingText.split('\n').length;
const previousLines = precedingText.split('\n');
// Lines and columns in stacks are one indexed.
const line = previousLines.length;
const column = previousLines[previousLines.length - 1].length + 1;

return {
filePath: path,
line,
column,
fileSource: source,
locationText,
};
});
}

/**
* Extracts source code locations from an ACVM error if possible.
* @param errMessage - The ACVM error.
* Extracts the source code locations for an array of opcode locations
* @param opcodeLocations - The opcode locations that caused the error.
* @param debug - The debug metadata of the function.
* @returns The source code locations or undefined if they couldn't be extracted from the error.
* @returns The source code locations.
*/
export function processAcvmError(errMessage: string, debug: FunctionDebugMetadata): NoirCallStack | undefined {
const opcodeLocation = extractOpcodeLocationFromError(errMessage);
if (!opcodeLocation) {
return undefined;
}

return getCallStackFromOpcodeLocation(opcodeLocation, debug);
}

/**
* An error thrown by the ACVM during simulation. Optionally contains a noir call stack.
*/
export class ACVMError extends Error {
constructor(
message: string,
/**
* The noir call stack of the error, if it could be extracted.
*/
public callStack?: NoirCallStack,
) {
super(message);
}
export function resolveOpcodeLocations(
opcodeLocations: OpcodeLocation[],
debug: FunctionDebugMetadata,
): SourceCodeLocation[] {
return opcodeLocations.flatMap(opcodeLocation => getSourceCodeLocationsFromOpcodeLocation(opcodeLocation, debug));
}

/**
Expand All @@ -141,13 +125,8 @@ export async function acvm(
acir: Buffer,
initialWitness: ACVMWitness,
callback: ACIRCallback,
debug?: FunctionDebugMetadata,
): Promise<ACIRExecutionResult> {
const logger = createDebugLogger('aztec:simulator:acvm');
// This is a workaround to avoid the ACVM removing the information about the underlying error.
// We should probably update the ACVM to let proper errors through.
let oracleError: Error | undefined = undefined;

const partialWitness = await executeCircuitWithBlackBoxSolver(
solver,
acir,
Expand All @@ -169,31 +148,34 @@ export async function acvm(
} else {
typedError = new Error(`Error in oracle callback ${err}`);
}
oracleError = typedError;
logger.error(`Error in oracle callback ${name}:`, typedError.message, typedError.stack);
logger.error(`Error in oracle callback ${name}`);
throw typedError;
}
},
).catch((acvmErrorString: string) => {
if (oracleError) {
throw oracleError;
}

if (debug) {
const callStack = processAcvmError(acvmErrorString, debug);

if (callStack) {
throw new ACVMError(
`Assertion failed: '${callStack[callStack.length - 1]?.locationText ?? 'Unknown'}'`,
callStack,
);
}
}
// If we cannot find a callstack, throw the original error.
throw new ACVMError(acvmErrorString);
});
);

return { partialWitness };
}

/**
* Extracts the call stack from an thrown by the acvm.
* @param error - The error to extract from.
* @param debug - The debug metadata of the function called.
* @returns The call stack, if available.
*/
export function extractCallStack(
error: Error | ExecutionError,
debug?: FunctionDebugMetadata,
): NoirCallStack | undefined {
if (!('callStack' in error) || !error.callStack) {
return undefined;
}
const { callStack } = error;
if (!debug) {
return callStack;
}

return Promise.resolve({ partialWitness });
return resolveOpcodeLocations(callStack, debug);
}

/**
Expand Down
Loading