Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions acvm-repo/acvm_js/src/execute.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{future::Future, pin::Pin};

use acvm::acir::circuit::brillig::BrilligBytecode;
use acvm::acir::circuit::opcodes::AcirFunctionId;
use acvm::{BlackBoxFunctionSolver, FieldElement};
use acvm::{
acir::circuit::{Circuit, Program},
Expand Down Expand Up @@ -88,7 +89,7 @@ async fn execute_circuit_with_return_witness_pedantic(
console_error_panic_hook::set_once();

let program: Program<FieldElement> = Program::deserialize_program(&program)
.map_err(|_| JsExecutionError::new("Failed to deserialize circuit. This is likely due to differing serialization formats between ACVM_JS and your compiler".to_string(), None, None, None))?;
.map_err(|_| JsExecutionError::new("Failed to deserialize circuit. This is likely due to differing serialization formats between ACVM_JS and your compiler".to_string(), None, None, None, None))?;

let mut witness_stack = execute_program_with_native_program_and_return(
&program,
Expand All @@ -103,7 +104,7 @@ async fn execute_circuit_with_return_witness_pedantic(
let main_circuit = &program.functions[0];
let return_witness =
extract_indices(&solved_witness, main_circuit.return_values.0.iter().copied().collect())
.map_err(|err| JsExecutionError::new(err, None, None, None))?;
.map_err(|err| JsExecutionError::new(err, None, None, None, None))?;

Ok((solved_witness, return_witness).into())
}
Expand Down Expand Up @@ -155,7 +156,7 @@ async fn execute_program_with_native_type_return(
"Failed to deserialize circuit. This is likely due to differing serialization formats between ACVM_JS and your compiler".to_string(),
None,
None,
None))?;
None, None))?;

execute_program_with_native_program_and_return(
&program,
Expand Down Expand Up @@ -216,18 +217,21 @@ impl<'a, B: BlackBoxFunctionSolver<FieldElement>> ProgramExecutor<'a, B> {
let main = &self.functions[0];

let mut witness_stack = WitnessStack::default();
let main_witness = self.execute_circuit(main, initial_witness, &mut witness_stack).await?;
let main_witness = self
.execute_circuit(main, AcirFunctionId(0), initial_witness, &mut witness_stack)
.await?;
witness_stack.push(0, main_witness);
Ok(witness_stack)
}

fn execute_circuit(
&'a self,
circuit: &'a Circuit<FieldElement>,
acir_function_id: AcirFunctionId,
initial_witness: WitnessMap<FieldElement>,
witness_stack: &'a mut WitnessStack<FieldElement>,
) -> Pin<Box<dyn Future<Output = Result<WitnessMap<FieldElement>, Error>> + 'a>> {
Box::pin(async {
Box::pin(async move {
let mut acvm = ACVM::new(
self.blackbox_solver,
&circuit.opcodes,
Expand Down Expand Up @@ -297,6 +301,7 @@ impl<'a, B: BlackBoxFunctionSolver<FieldElement>> ProgramExecutor<'a, B> {
message,
call_stack,
raw_assertion_payload,
Some(acir_function_id),
brillig_function_id,
)
.into());
Expand All @@ -311,7 +316,12 @@ impl<'a, B: BlackBoxFunctionSolver<FieldElement>> ProgramExecutor<'a, B> {
let acir_to_call = &self.functions[call_info.id.as_usize()];
let initial_witness = call_info.initial_witness;
let call_solved_witness = self
.execute_circuit(acir_to_call, initial_witness, witness_stack)
.execute_circuit(
acir_to_call,
call_info.id,
initial_witness,
witness_stack,
)
.await?;
let mut call_resolved_outputs = Vec::new();
for return_witness_index in acir_to_call.return_values.indices() {
Expand All @@ -321,7 +331,7 @@ impl<'a, B: BlackBoxFunctionSolver<FieldElement>> ProgramExecutor<'a, B> {
call_resolved_outputs.push(*return_value);
} else {
// TODO: look at changing this call stack from None
return Err(JsExecutionError::new(format!("Failed to read from solved witness of ACIR call at witness {}", return_witness_index), None, None, None).into());
return Err(JsExecutionError::new(format!("Failed to read from solved witness of ACIR call at witness {}", return_witness_index), None, None, None, None).into());
}
}
acvm.resolve_pending_acir_call(call_resolved_outputs);
Expand Down
11 changes: 10 additions & 1 deletion acvm-repo/acvm_js/src/js_execution_error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use acvm::{
FieldElement,
acir::circuit::{OpcodeLocation, brillig::BrilligFunctionId},
acir::circuit::{OpcodeLocation, brillig::BrilligFunctionId, opcodes::AcirFunctionId},
pwg::RawAssertionPayload,
};
use gloo_utils::format::JsValueSerdeExt;
Expand All @@ -17,6 +17,7 @@ export type RawAssertionPayload = {
export type ExecutionError = Error & {
callStack?: string[];
rawAssertionPayload?: RawAssertionPayload;
acirFunctionId?: number;
brilligFunctionId?: number;
};
"#;
Expand All @@ -41,6 +42,7 @@ impl JsExecutionError {
message: String,
call_stack: Option<Vec<OpcodeLocation>>,
assertion_payload: Option<RawAssertionPayload<FieldElement>>,
acir_function_id: Option<AcirFunctionId>,
brillig_function_id: Option<BrilligFunctionId>,
) -> Self {
let mut error = JsExecutionError::constructor(JsString::from(message));
Expand All @@ -60,6 +62,12 @@ impl JsExecutionError {
None => JsValue::UNDEFINED,
};

let acir_function_id = match acir_function_id {
Some(function_id) => <JsValue as JsValueSerdeExt>::from_serde(&function_id)
.expect("Cannot serialize ACIR function id"),
None => JsValue::UNDEFINED,
};

let brillig_function_id = match brillig_function_id {
Some(function_id) => <JsValue as JsValueSerdeExt>::from_serde(&function_id)
.expect("Cannot serialize Brillig function id"),
Expand All @@ -68,6 +76,7 @@ impl JsExecutionError {

error.set_property("callStack", js_call_stack);
error.set_property("rawAssertionPayload", assertion_payload);
error.set_property("acirFunctionId", acir_function_id);
error.set_property("brilligFunctionId", brillig_function_id);

error
Expand Down
4 changes: 2 additions & 2 deletions tooling/noir_codegen/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ function main() {
const programs = files.map((file_path): [string, CompiledCircuit] => {
const program_name = path.parse(file_path).name;
const file_contents = fs.readFileSync(file_path).toString();
const { abi, bytecode } = JSON.parse(file_contents);
const { abi, bytecode, debug_symbols, file_map } = JSON.parse(file_contents);

return [program_name, { abi, bytecode }];
return [program_name, { abi, bytecode, debug_symbols, file_map }];
});

const result = codegen(programs, !cliConfig.externalArtifact, cliConfig.useFixedLengthArrays);
Expand Down
4 changes: 3 additions & 1 deletion tooling/noir_js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"dependencies": {
"@noir-lang/acvm_js": "workspace:*",
"@noir-lang/noirc_abi": "workspace:*",
"@noir-lang/types": "workspace:*"
"@noir-lang/types": "workspace:*",
"pako": "^2.1.0"
},
"files": [
"lib",
Expand Down Expand Up @@ -52,6 +53,7 @@
"@types/chai": "^4",
"@types/mocha": "^10.0.10",
"@types/node": "^22.13.10",
"@types/pako": "^2",
"@types/prettier": "^3.0.0",
"chai": "^4.4.1",
"eslint": "^9.24.0",
Expand Down
121 changes: 121 additions & 0 deletions tooling/noir_js/src/debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { BrilligFunctionId, DebugFileMap, DebugInfo, OpcodeLocation } from '@noir-lang/types';
import { inflate } from 'pako';
import { base64Decode } from './base64_decode';
import { ExecutionError } from '@noir-lang/acvm_js';

/**
* A stack of calls, resolved or not
*/
type CallStack = SourceCodeLocation[] | OpcodeLocation[];

/**
* A resolved pointer to a failing section of the noir source code.
*/
interface SourceCodeLocation {
/**
* The path to the source file.
*/
filePath: string;
/**
* The line number of the location.
*/
line: number;
/**
* The column number of the location.
*/
column: number;
/**
* The source code text of the location.
*/
locationText: string;
}

export function parseDebugSymbols(debugSymbols: string): DebugInfo[] {
return JSON.parse(inflate(base64Decode(debugSymbols), { to: 'string', raw: true })).debug_infos;
}

/**
* Extracts the call stack from an thrown by the acvm.
* @param error - The error to extract from.
* @param debug - The debug metadata of the program called.
* @param files - The files used for compilation of the program.
* @returns The call stack, if available.
*/
export function extractCallStack(error: ExecutionError, debug: DebugInfo, files: DebugFileMap): CallStack | undefined {
if (!('callStack' in error) || !error.callStack) {
return undefined;
}
const { callStack, brilligFunctionId } = error;
if (!debug) {
return callStack;
}

try {
return resolveOpcodeLocations(callStack, debug, files, brilligFunctionId);
} catch (_err) {
return callStack;
}
}

/**
* Resolves the source code locations from an array of opcode locations
*/
function resolveOpcodeLocations(
opcodeLocations: OpcodeLocation[],
debug: DebugInfo,
files: DebugFileMap,
brilligFunctionId?: BrilligFunctionId,
): SourceCodeLocation[] {
return opcodeLocations.flatMap((opcodeLocation) =>
getSourceCodeLocationsFromOpcodeLocation(opcodeLocation, debug, files, brilligFunctionId),
);
}

/**
* 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 getSourceCodeLocationsFromOpcodeLocation(
opcodeLocation: string,
debug: DebugInfo,
files: DebugFileMap,
brilligFunctionId?: BrilligFunctionId,
): SourceCodeLocation[] {
let callStack = debug.locations[opcodeLocation] || [];
if (callStack.length === 0) {
const brilligLocation = extractBrilligLocation(opcodeLocation);
if (brilligFunctionId !== undefined && brilligLocation !== undefined) {
callStack = debug.brillig_locations[brilligFunctionId][brilligLocation] || [];
}
}
return callStack.map((call) => {
const { file: fileId, span } = call;

const { path, source } = files[fileId];

const locationText = source.substring(span.start, span.end);
const precedingText = source.substring(0, span.start);
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,
locationText,
};
});
}

/**
* Extracts a brillig location from an opcode location.
*/
function extractBrilligLocation(opcodeLocation: string): string | undefined {
const splitted = opcodeLocation.split('.');
if (splitted.length === 2) {
return splitted[1];
}
return undefined;
}
54 changes: 37 additions & 17 deletions tooling/noir_js/src/witness_generation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { abiDecodeError, abiEncode, InputMap } from '@noir-lang/noirc_abi';
import { base64Decode } from './base64_decode.js';
import { WitnessStack, ForeignCallHandler, ForeignCallInput, ExecutionError, executeProgram } from '@noir-lang/acvm_js';
import { Abi, CompiledCircuit } from '@noir-lang/types';
import { CompiledCircuit } from '@noir-lang/types';
import { extractCallStack, parseDebugSymbols } from './debug.js';

const defaultForeignCallHandler: ForeignCallHandler = async (name: string, args: ForeignCallInput[]) => {
if (name == 'print') {
Expand All @@ -16,26 +17,45 @@ const defaultForeignCallHandler: ForeignCallHandler = async (name: string, args:

// Payload is any since it can be of any type defined by the circuit dev.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ErrorWithPayload = ExecutionError & { decodedAssertionPayload?: any };
export type ErrorWithPayload = ExecutionError & { decodedAssertionPayload?: any; noirCallStack?: string[] };

function parseErrorPayload(abi: Abi, originalError: ExecutionError): Error {
const payload = originalError.rawAssertionPayload;
if (!payload) return originalError;
function enrichExecutionError(artifact: CompiledCircuit, originalError: ExecutionError): Error {
const enrichedError = originalError as ErrorWithPayload;

try {
// Decode the payload
const decodedPayload = abiDecodeError(abi, payload);
if (originalError.rawAssertionPayload) {
try {
// Decode the payload
const decodedPayload = abiDecodeError(artifact.abi, originalError.rawAssertionPayload);

if (typeof decodedPayload === 'string') {
// If it's a string, just add it to the error message
enrichedError.message = `Circuit execution failed: ${decodedPayload}`;
} else {
// If not, attach the payload to the original error
enrichedError.decodedAssertionPayload = decodedPayload;
if (typeof decodedPayload === 'string') {
// If it's a string, just add it to the error message
enrichedError.message = `Circuit execution failed: ${decodedPayload}`;
} else {
// If not, attach the payload to the original error
enrichedError.decodedAssertionPayload = decodedPayload;
}
} catch (_errorDecoding) {
// Ignore errors decoding the payload
}
} catch (_errorDecoding) {
// Ignore errors decoding the payload
}

try {
// Decode the callstack
const callStack = extractCallStack(
originalError,
parseDebugSymbols(artifact.debug_symbols)[originalError.acirFunctionId!],
artifact.file_map,
);

enrichedError.noirCallStack = callStack?.map((errorLocation) => {
if (typeof errorLocation === 'string') {
return `at opcode ${errorLocation}`;
} else {
return `at ${errorLocation.locationText} (${errorLocation.filePath}:${errorLocation.line}:${errorLocation.column})`;
}
});
} catch (_errorResolving) {
// Ignore errors resolving the callstack
}

return enrichedError;
Expand All @@ -58,7 +78,7 @@ export async function generateWitness(
} catch (err) {
// Typescript types catched errors as unknown or any, so we need to narrow its type to check if it has raw assertion payload.
if (typeof err === 'object' && err !== null && 'rawAssertionPayload' in err) {
throw parseErrorPayload(compiledProgram.abi, err as ExecutionError);
throw enrichExecutionError(compiledProgram, err as ExecutionError);
}
throw new Error(`Circuit execution failed: ${err}`);
}
Expand Down
6 changes: 4 additions & 2 deletions tooling/noir_js/test/node/execute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,18 @@ it('successfully executes a program with multiple acir circuits', async () => {
expect(() => new Noir(fold_fibonacci_program).execute(inputs)).to.not.throw();
});

it('circuit with a fmt string assert message should fail with the resolved assertion message', async () => {
it('circuit with a fmt string assert message should fail with the resolved assertion information', async () => {
const inputs = {
x: '10',
y: '5',
};
try {
await new Noir(assert_msg_runtime).execute(inputs);
} catch (error) {
const knownError = error as Error;
const knownError = error as ErrorWithPayload;
expect(knownError.message).to.equal('Circuit execution failed: Expected x < y but got 10 < 5');
expect(knownError.noirCallStack).to.have.lengthOf(1);
expect(knownError.noirCallStack![0]).to.match(/^at x < y \(.*assert_msg_runtime\/src\/main.nr:3:12\)$/);
}
});

Expand Down
Loading
Loading