-
Notifications
You must be signed in to change notification settings - Fork 233
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(avm-simulator): error stack tracking in AVM to match ACVM/ACIR-SIM #6289
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -62,7 +62,7 @@ describe('AVM simulator: injected bytecode', () => { | |
const results = await new AvmSimulator(context).executeBytecode(bytecode); | ||
expect(results.reverted).toBe(true); | ||
expect(results.output).toEqual([]); | ||
expect(results.revertReason?.name).toEqual('OutOfGasError'); | ||
expect(results.revertReason?.message).toEqual('Not enough L2GAS gas left'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
expect(context.machineState.l2GasLeft).toEqual(0); | ||
expect(context.machineState.daGasLeft).toEqual(0); | ||
}); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,16 @@ | ||
import { type AztecAddress } from '@aztec/circuits.js'; | ||
import { type FailingFunction, type NoirCallStack } from '@aztec/circuit-types'; | ||
import { type AztecAddress, type Fr } from '@aztec/circuits.js'; | ||
|
||
import { ExecutionError } from '../common/errors.js'; | ||
import { type AvmContext } from './avm_context.js'; | ||
|
||
/** | ||
* Avm-specific errors should derive from this | ||
*/ | ||
export abstract class AvmExecutionError extends Error { | ||
constructor(message: string, ...rest: any[]) { | ||
super(message, ...rest); | ||
this.name = 'AvmInterpreterError'; | ||
constructor(message: string) { | ||
super(message); | ||
this.name = 'AvmExecutionError'; | ||
} | ||
Comment on lines
-7
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Weirdly typescript started yelling me for not telling it what types rest should have.... |
||
} | ||
|
||
|
@@ -63,3 +67,89 @@ export class OutOfGasError extends AvmExecutionError { | |
this.name = 'OutOfGasError'; | ||
} | ||
} | ||
|
||
/** | ||
* Error thrown to propagate a nested call's revert. | ||
* @param message - the error's message | ||
* @param nestedError - the revert reason of the nested call | ||
*/ | ||
export class RethrownError extends AvmExecutionError { | ||
constructor(message: string, public nestedError: AvmRevertReason) { | ||
super(message); | ||
this.name = 'RethrownError'; | ||
} | ||
} | ||
|
||
/** | ||
* Meaningfully named alias for ExecutionError when used in the context of the AVM. | ||
* Maintains a recursive structure reflecting the AVM's external callstack/errorstack, where | ||
* options.cause is the error that caused this error (if this is not the root-cause itself). | ||
*/ | ||
export class AvmRevertReason extends ExecutionError { | ||
constructor(message: string, failingFunction: FailingFunction, noirCallStack: NoirCallStack, options?: ErrorOptions) { | ||
super(message, failingFunction, noirCallStack, options); | ||
} | ||
} | ||
|
||
/** | ||
* Helper to create a "revert reason" error optionally with a nested error cause. | ||
* | ||
* @param message - the error message | ||
* @param context - the context of the AVM execution used to extract the failingFunction and noirCallStack | ||
* @param nestedError - the error that caused this one (if this is not the root-cause itself) | ||
*/ | ||
function createRevertReason(message: string, context: AvmContext, nestedError?: AvmRevertReason): AvmRevertReason { | ||
return new AvmRevertReason( | ||
message, | ||
/*failingFunction=*/ { | ||
contractAddress: context.environment.address, | ||
functionSelector: context.environment.temporaryFunctionSelector, | ||
}, | ||
/*noirCallStack=*/ [...context.machineState.internalCallStack, context.machineState.pc].map(pc => `0.${pc}`), | ||
/*options=*/ { cause: nestedError }, | ||
); | ||
} | ||
|
||
/** | ||
* Create a "revert reason" error for an exceptional halt, | ||
* creating the recursive structure if the halt was a RethrownError. | ||
* | ||
* @param haltingError - the lower-level error causing the exceptional halt | ||
* @param context - the context of the AVM execution used to extract the failingFunction and noirCallStack | ||
*/ | ||
export function revertReasonFromExceptionalHalt(haltingError: AvmExecutionError, context: AvmContext): AvmRevertReason { | ||
// A RethrownError has a nested/child AvmRevertReason | ||
const nestedError = haltingError instanceof RethrownError ? haltingError.nestedError : undefined; | ||
return createRevertReason(haltingError.message, context, nestedError); | ||
} | ||
|
||
/** | ||
* Create a "revert reason" error for an explicit revert (a root cause). | ||
* | ||
* @param revertData - output data of the explicit REVERT instruction | ||
* @param context - the context of the AVM execution used to extract the failingFunction and noirCallStack | ||
*/ | ||
export function revertReasonFromExplicitRevert(revertData: Fr[], context: AvmContext): AvmRevertReason { | ||
const revertMessage = decodeRevertDataAsMessage(revertData); | ||
return createRevertReason(revertMessage, context); | ||
} | ||
|
||
/** | ||
* Interpret revert data as a message string. | ||
* | ||
* @param revertData - output data of an explicit REVERT instruction | ||
*/ | ||
export function decodeRevertDataAsMessage(revertData: Fr[]): string { | ||
if (revertData.length === 0) { | ||
return 'Assertion failed.'; | ||
} else { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add an e2e test to see how an intrinsic failure shows up? (e.g., the assert bit size error we saw). I expect `Assertion failed. 'expression here'", which is not true, but it's good enough. I think ideally "assertion failed" should only appear for the REVERT opcode (or rethrows?). The intrinsic failures and the truly exceptional failures (unknown error type) are not assertions in noir. However, this is almost a nit. I'd love to see a test but I wouldn't spend much time trying to reconcile this! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
try { | ||
// We remove the first element which is the 'error selector'. | ||
const revertOutput = revertData.slice(1); | ||
// Try to interpret the output as a text string. | ||
return 'Assertion failed: ' + String.fromCharCode(...revertOutput.map(fr => fr.toNumber())); | ||
} catch (e) { | ||
return 'Assertion failed: <cannot interpret as string>'; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider
public readonly
instead, but I'm ok with getters.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that
readonly
means it is essentially immutable (can only be initialized in the constructor), but we want it to be mutable by functions inside theAvmMachineState