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

fix: Complete JS call stacks across ACVM wasm boundaries #2013

Merged
merged 9 commits into from
Sep 5, 2023
16 changes: 15 additions & 1 deletion yarn-project/acir-simulator/src/acvm/acvm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
executeCircuitWithBlackBoxSolver,
} from 'acvm_js';

import { traverseCauseChain } from '../common/errors.js';

/**
* The format for fields on the ACVM.
*/
Expand Down Expand Up @@ -127,6 +129,7 @@ export async function acvm(
callback: ACIRCallback,
): Promise<ACIRExecutionResult> {
const logger = createDebugLogger('aztec:simulator:acvm');

const partialWitness = await executeCircuitWithBlackBoxSolver(
solver,
acir,
Expand All @@ -152,7 +155,18 @@ export async function acvm(
throw typedError;
}
},
);
).catch((err: Error) => {
// Wasm callbacks act as a boundary for stack traces, so we capture it here and complete the error if it happens.
const stack = new Error().stack;

traverseCauseChain(err, cause => {
if (cause.stack) {
cause.stack += stack;
}
});

throw err;
});

return { partialWitness };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
toAcvmCommitmentLoadOracleInputs,
toAcvmL1ToL2MessageLoadOracleInputs,
} from '../acvm/index.js';
import { PackedArgsCache } from '../packed_args_cache.js';
import { PackedArgsCache } from '../common/packed_args_cache.js';
import { DBOracle, PendingNoteData } from './db_oracle.js';
import { pickNotes } from './pick_notes.js';

Expand Down
18 changes: 10 additions & 8 deletions yarn-project/acir-simulator/src/client/private_execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ import { AztecAddress } from '@aztec/foundation/aztec-address';
import { Fr, Point } from '@aztec/foundation/fields';
import { createDebugLogger } from '@aztec/foundation/log';
import { to2Fields } from '@aztec/foundation/serialize';
import { FunctionL2Logs, NotePreimage, NoteSpendingInfo, SimulationError } from '@aztec/types';

import { ExecutionError } from 'acvm_js';
import { FunctionL2Logs, NotePreimage, NoteSpendingInfo } from '@aztec/types';

import { extractPrivateCircuitPublicInputs, frToAztecAddress } from '../acvm/deserialize.js';
import {
Expand All @@ -27,6 +25,7 @@ import {
toAcvmCallPrivateStackItem,
toAcvmEnqueuePublicFunctionResult,
} from '../acvm/index.js';
import { ExecutionError } from '../common/errors.js';
import {
AcirSimulator,
ExecutionResult,
Expand Down Expand Up @@ -194,12 +193,15 @@ export class PrivateFunctionExecution {
const portalContactAddress = await this.context.db.getPortalContractAddress(contractAddress);
return Promise.resolve(toACVMField(portalContactAddress));
},
}).catch((err: Error | ExecutionError) => {
throw SimulationError.fromError(
this.contractAddress,
selector,
err.cause instanceof Error ? err.cause : err,
}).catch((err: Error) => {
throw new ExecutionError(
err.message,
{
contractAddress: this.contractAddress,
functionSelector: selector,
},
extractCallStack(err, this.abi.debug),
{ cause: err },
);
});

Expand Down
15 changes: 12 additions & 3 deletions yarn-project/acir-simulator/src/client/simulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { AztecNode, FunctionCall, TxExecutionRequest } from '@aztec/types';

import { WasmBlackBoxFunctionSolver, createBlackBoxSolver } from 'acvm_js';

import { PackedArgsCache } from '../packed_args_cache.js';
import { createSimulationError } from '../common/errors.js';
import { PackedArgsCache } from '../common/packed_args_cache.js';
import { ClientTxExecutionContext } from './client_execution_context.js';
import { DBOracle, FunctionAbiWithDebugMetadata } from './db_oracle.js';
import { ExecutionResult } from './execution_result.js';
Expand Down Expand Up @@ -101,7 +102,11 @@ export class AcirSimulator {
curve,
);

return execution.run();
try {
return await execution.run();
} catch (err) {
throw createSimulationError(err instanceof Error ? err : new Error('Unknown error during private execution'));
}
}

/**
Expand Down Expand Up @@ -151,7 +156,11 @@ export class AcirSimulator {
callContext,
);

return execution.run(aztecNode);
try {
return await execution.run(aztecNode);
} catch (err) {
throw createSimulationError(err instanceof Error ? err : new Error('Unknown error during private execution'));
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { DecodedReturn, decodeReturnValues } from '@aztec/foundation/abi';
import { AztecAddress } from '@aztec/foundation/aztec-address';
import { Fr } from '@aztec/foundation/fields';
import { createDebugLogger } from '@aztec/foundation/log';
import { AztecNode, SimulationError } from '@aztec/types';
import { AztecNode } from '@aztec/types';

import { extractReturnWitness, frToAztecAddress } from '../acvm/deserialize.js';
import {
Expand All @@ -15,6 +15,7 @@ import {
toACVMField,
toACVMWitness,
} from '../acvm/index.js';
import { ExecutionError } from '../common/errors.js';
import { AcirSimulator } from '../index.js';
import { ClientTxExecutionContext } from './client_execution_context.js';
import { FunctionAbiWithDebugMetadata } from './db_oracle.js';
Expand Down Expand Up @@ -106,11 +107,14 @@ export class UnconstrainedFunctionExecution {
return Promise.resolve(toACVMField(portalContactAddress));
},
}).catch((err: Error) => {
throw SimulationError.fromError(
this.contractAddress,
this.functionData.selector,
err.cause instanceof Error ? err.cause : err,
throw new ExecutionError(
err.message,
{
contractAddress: this.contractAddress,
functionSelector: this.functionData.selector,
},
extractCallStack(err, this.abi.debug),
{ cause: err },
);
});

Expand Down
61 changes: 61 additions & 0 deletions yarn-project/acir-simulator/src/common/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { FailingFunction, NoirCallStack, SimulationError } from '@aztec/types';

/**
* An error that occurred during the execution of a function.
*/
export class ExecutionError extends Error {
constructor(
message: string,
/**
* The function that failed.
*/
public failingFunction: FailingFunction,
/**
* The noir call stack of the function that failed.
*/
public noirCallStack?: NoirCallStack,
options?: ErrorOptions,
) {
super(message, options);
}
}

/**
* Traverses the cause chain of an error.
* @param error - The error to start from.
* @param callback - A callback on every error, including the first one.
*/
export function traverseCauseChain(error: Error, callback: (error: Error) => void) {
let currentError: Error | undefined = error;
while (currentError) {
callback(currentError);
if (currentError.cause && currentError.cause instanceof Error) {
currentError = currentError.cause;
} else {
currentError = undefined;
}
}
}

/**
* Creates a simulation error from an error chain generated during the execution of a function.
* @param error - The error thrown during execution.
* @returns - A simulation error.
*/
export function createSimulationError(error: Error): SimulationError {
let rootCause = error;
let noirCallStack: NoirCallStack | undefined = undefined;
const aztecCallStack: FailingFunction[] = [];

traverseCauseChain(error, cause => {
rootCause = cause;
if (cause instanceof ExecutionError) {
aztecCallStack.push(cause.failingFunction);
if (cause.noirCallStack) {
noirCallStack = cause.noirCallStack;
}
}
});

return new SimulationError(rootCause.message, aztecCallStack, noirCallStack, { cause: rootCause });
}
2 changes: 2 additions & 0 deletions yarn-project/acir-simulator/src/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './packed_args_cache.js';
export * from './errors.js';
26 changes: 19 additions & 7 deletions yarn-project/acir-simulator/src/public/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from '@aztec/circuits.js';
import { padArrayEnd } from '@aztec/foundation/collection';
import { createDebugLogger } from '@aztec/foundation/log';
import { FunctionL2Logs, SimulationError } from '@aztec/types';
import { FunctionL2Logs } from '@aztec/types';

import {
ZERO_ACVM_FIELD,
Expand All @@ -26,8 +26,9 @@ import {
toAcvmL1ToL2MessageLoadOracleInputs,
} from '../acvm/index.js';
import { oracleDebugCallToFormattedStr } from '../client/debug.js';
import { ExecutionError, createSimulationError } from '../common/errors.js';
import { PackedArgsCache } from '../common/packed_args_cache.js';
import { AcirSimulator } from '../index.js';
import { PackedArgsCache } from '../packed_args_cache.js';
import { CommitmentsDB, PublicContractsDB, PublicStateDB } from './db.js';
import { PublicExecution, PublicExecutionResult } from './execution.js';
import { ContractStorageActionsCollector } from './state_actions.js';
Expand All @@ -54,7 +55,15 @@ export class PublicExecutor {
* @param globalVariables - The global variables to use.
* @returns The result of the run plus all nested runs.
*/
public async execute(execution: PublicExecution, globalVariables: GlobalVariables): Promise<PublicExecutionResult> {
public async simulate(execution: PublicExecution, globalVariables: GlobalVariables): Promise<PublicExecutionResult> {
try {
return await this.execute(execution, globalVariables);
} catch (err) {
throw createSimulationError(err instanceof Error ? err : new Error('Unknown error during public execution'));
}
}

private async execute(execution: PublicExecution, globalVariables: GlobalVariables): Promise<PublicExecutionResult> {
const selector = execution.functionData.selector;
this.log(`Executing public external function ${execution.contractAddress.toString()}:${selector}`);

Expand Down Expand Up @@ -141,11 +150,14 @@ export class PublicExecutor {
return Promise.resolve(toACVMField(portalContactAddress));
},
}).catch((err: Error) => {
throw SimulationError.fromError(
execution.contractAddress,
selector,
err.cause instanceof Error ? err.cause : err,
throw new ExecutionError(
err.message,
{
contractAddress: execution.contractAddress,
functionSelector: selector,
},
extractCallStack(err),
{ cause: err },
);
});

Expand Down
18 changes: 9 additions & 9 deletions yarn-project/acir-simulator/src/public/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ describe('ACIR public execution simulator', () => {
publicState.storageRead.mockResolvedValue(previousBalance);

const execution: PublicExecution = { contractAddress, functionData, args, callContext };
const result = await executor.execute(execution, GlobalVariables.empty());
const result = await executor.simulate(execution, GlobalVariables.empty());

const expectedBalance = new Fr(160n);
expect(result.returnValues[0]).toEqual(expectedBalance);
Expand Down Expand Up @@ -150,7 +150,7 @@ describe('ACIR public execution simulator', () => {
const recipientBalance = new Fr(20n);
mockStore(senderBalance, recipientBalance);

const result = await executor.execute(execution, GlobalVariables.empty());
const result = await executor.simulate(execution, GlobalVariables.empty());

const expectedRecipientBalance = new Fr(160n);
const expectedSenderBalance = new Fr(60n);
Expand Down Expand Up @@ -180,7 +180,7 @@ describe('ACIR public execution simulator', () => {
const recipientBalance = new Fr(20n);
mockStore(senderBalance, recipientBalance);

const result = await executor.execute(execution, GlobalVariables.empty());
const result = await executor.simulate(execution, GlobalVariables.empty());
expect(result.returnValues[0]).toEqual(recipientBalance);

expect(result.contractStorageReads).toEqual(
Expand Down Expand Up @@ -247,11 +247,11 @@ describe('ACIR public execution simulator', () => {
const globalVariables = new GlobalVariables(new Fr(69), new Fr(420), new Fr(1), new Fr(7));

if (isInternal === undefined) {
await expect(executor.execute(execution, globalVariables)).rejects.toThrowError(
await expect(executor.simulate(execution, globalVariables)).rejects.toThrowError(
/ContractsDb don't contain isInternal for/,
);
} else {
const result = await executor.execute(execution, globalVariables);
const result = await executor.simulate(execution, globalVariables);

expect(result.returnValues[0]).toEqual(
new Fr(
Expand Down Expand Up @@ -301,7 +301,7 @@ describe('ACIR public execution simulator', () => {
publicState.storageRead.mockResolvedValue(amount);

const execution: PublicExecution = { contractAddress, functionData, args, callContext };
const result = await executor.execute(execution, GlobalVariables.empty());
const result = await executor.simulate(execution, GlobalVariables.empty());

// Assert the commitment was created
expect(result.newCommitments.length).toEqual(1);
Expand Down Expand Up @@ -331,7 +331,7 @@ describe('ACIR public execution simulator', () => {
publicContracts.getBytecode.mockResolvedValue(Buffer.from(createL2ToL1MessagePublicAbi.bytecode, 'base64'));

const execution: PublicExecution = { contractAddress, functionData, args, callContext };
const result = await executor.execute(execution, GlobalVariables.empty());
const result = await executor.simulate(execution, GlobalVariables.empty());

// Assert the l2 to l1 message was created
expect(result.newL2ToL1Messages.length).toEqual(1);
Expand Down Expand Up @@ -393,7 +393,7 @@ describe('ACIR public execution simulator', () => {
});

const execution: PublicExecution = { contractAddress, functionData, args, callContext };
const result = await executor.execute(execution, GlobalVariables.empty());
const result = await executor.simulate(execution, GlobalVariables.empty());

expect(result.newNullifiers.length).toEqual(1);
});
Expand All @@ -415,7 +415,7 @@ describe('ACIR public execution simulator', () => {
publicContracts.getBytecode.mockResolvedValue(Buffer.from(createNullifierPublicAbi.bytecode, 'base64'));

const execution: PublicExecution = { contractAddress, functionData, args, callContext };
const result = await executor.execute(execution, GlobalVariables.empty());
const result = await executor.simulate(execution, GlobalVariables.empty());

// Assert the l2 to l1 message was created
expect(result.newNullifiers.length).toEqual(1);
Expand Down
4 changes: 3 additions & 1 deletion yarn-project/aztec-node/src/aztec-node/http-node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,9 @@ describe('HttpNode', () => {

it('should fetch a simulation error', async () => {
const tx = mockTx();
const simulationError = SimulationError.new('Fake simulation error', AztecAddress.ZERO, FunctionSelector.empty());
const simulationError = new SimulationError('test error', [
{ contractAddress: AztecAddress.ZERO, functionSelector: FunctionSelector.empty() },
]);

const response = {
simulationError: simulationError.toJSON(),
Expand Down
Loading