Skip to content

Commit 2ab33e7

Browse files
authored
feat(avm): remove rethrowable reverts hack (#9752)
This PR removes the RethrowableError hack in the AVM simulator, and relies on the PublicContext's [rethrow](https://github.com/AztecProtocol/aztec-packages/blob/master/noir-projects/aztec-nr/aztec/src/context/public_context.nr#L88) to propagate the errors. There are two caveats. First, because currently Aztec-nr does not keep track of the cause chain, it would be impossible to have the call stack and original contract address available, so that the PXE can interpret the error and show debug information. Solidity has the same problem. I'm introducing a heuristic to keep track of the call stack for the simple case where the caller always rethrows: the simulator keeps a running trace in the machineState, and the caller uses this trace IF the revertData coincides. That is, if you are (re)throwing the same as what we were tracking. Second, while this all works well for errors in user code (e.g., `assert` in Noir), because they generate a revertData with an error selector and data, the "intrinsic" errors from the simulator (aka exceptional halts) do not work as well. E.g., "division by zero", "duplicated nullifier", "l1 to l2 blah blah". These errors are exceptions in typescript and do not have an associated error selector, and do not add to the revertdata. This _could_ be done with enshrined error selectors. That's easy in the simulator, but it's not easy in the circuit for several reasons that are beyond the scope of this description. The current solution is to propagate the error message (the user will see the right error) but you cannot "catch and process" the error in aztec.nr because there is no selector. This is not a limitation right now because there's no interface in the PublicContext that would let you catch errors. To be continued. Part of #9061.
1 parent 5c5be5c commit 2ab33e7

File tree

14 files changed

+250
-229
lines changed

14 files changed

+250
-229
lines changed

noir-projects/noir-contracts/contracts/avm_test_contract/src/main.nr

+15
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,21 @@ contract AvmTest {
229229
helper_with_failed_assertion()
230230
}
231231

232+
#[public]
233+
fn external_call_to_assertion_failure() {
234+
AvmTest::at(context.this_address()).assertion_failure().call(&mut context);
235+
}
236+
237+
#[public]
238+
fn divide_by_zero() -> u8 {
239+
1 / 0
240+
}
241+
242+
#[public]
243+
fn external_call_to_divide_by_zero() {
244+
AvmTest::at(context.this_address()).divide_by_zero().call(&mut context);
245+
}
246+
232247
#[public]
233248
fn debug_logging() {
234249
dep::aztec::oracle::debug_log::debug_log("just text");

yarn-project/end-to-end/src/e2e_avm_simulator.test.ts

+26-9
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,32 @@ describe('e2e_avm_simulator', () => {
3333
});
3434

3535
describe('Assertions', () => {
36-
it('PXE processes failed assertions and fills in the error message with the expression', async () => {
37-
await expect(avmContract.methods.assertion_failure().simulate()).rejects.toThrow(
38-
"Assertion failed: This assertion should fail! 'not_true == true'",
39-
);
40-
});
41-
it('PXE processes failed assertions and fills in the error message with the expression (even complex ones)', async () => {
42-
await expect(avmContract.methods.assert_nullifier_exists(123).simulate()).rejects.toThrow(
43-
"Assertion failed: Nullifier doesn't exist! 'context.nullifier_exists(nullifier, context.this_address())'",
44-
);
36+
describe('Not nested', () => {
37+
it('PXE processes user code assertions and recovers message', async () => {
38+
await expect(avmContract.methods.assertion_failure().simulate()).rejects.toThrow(
39+
"Assertion failed: This assertion should fail! 'not_true == true'",
40+
);
41+
});
42+
it('PXE processes user code assertions and recovers message (complex)', async () => {
43+
await expect(avmContract.methods.assert_nullifier_exists(123).simulate()).rejects.toThrow(
44+
"Assertion failed: Nullifier doesn't exist! 'context.nullifier_exists(nullifier, context.this_address())'",
45+
);
46+
});
47+
it('PXE processes intrinsic assertions and recovers message', async () => {
48+
await expect(avmContract.methods.divide_by_zero().simulate()).rejects.toThrow('Division by zero');
49+
});
50+
});
51+
describe('Nested', () => {
52+
it('PXE processes user code assertions and recovers message', async () => {
53+
await expect(avmContract.methods.external_call_to_assertion_failure().simulate()).rejects.toThrow(
54+
"Assertion failed: This assertion should fail! 'not_true == true'",
55+
);
56+
});
57+
it('PXE processes intrinsic assertions and recovers message', async () => {
58+
await expect(avmContract.methods.external_call_to_divide_by_zero().simulate()).rejects.toThrow(
59+
'Division by zero',
60+
);
61+
});
4562
});
4663
});
4764

yarn-project/simulator/src/acvm/acvm.ts

+3-112
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
1-
import { type NoirCallStack, type SourceCodeLocation } from '@aztec/circuit-types';
2-
import { type Fr } from '@aztec/circuits.js';
3-
import type { BrilligFunctionId, FunctionAbi, FunctionDebugMetadata, OpcodeLocation } from '@aztec/foundation/abi';
1+
import { type NoirCallStack } from '@aztec/circuit-types';
2+
import type { FunctionDebugMetadata } from '@aztec/foundation/abi';
43
import { createDebugLogger } from '@aztec/foundation/log';
54

65
import {
76
type ExecutionError,
87
type ForeignCallInput,
98
type ForeignCallOutput,
10-
type RawAssertionPayload,
119
executeCircuitWithReturnWitness,
1210
} from '@noir-lang/acvm_js';
13-
import { abiDecodeError } from '@noir-lang/noirc_abi';
1411

15-
import { traverseCauseChain } from '../common/errors.js';
12+
import { resolveOpcodeLocations, traverseCauseChain } from '../common/errors.js';
1613
import { type ACVMWitness } from './acvm_types.js';
1714
import { type ORACLE_NAMES } from './oracle/index.js';
1815

@@ -37,112 +34,6 @@ export interface ACIRExecutionResult {
3734
returnWitness: ACVMWitness;
3835
}
3936

40-
/**
41-
* Extracts a brillig location from an opcode location.
42-
* @param opcodeLocation - The opcode location to extract from. It should be in the format `acirLocation.brilligLocation` or `acirLocation`.
43-
* @returns The brillig location if the opcode location contains one.
44-
*/
45-
function extractBrilligLocation(opcodeLocation: string): string | undefined {
46-
const splitted = opcodeLocation.split('.');
47-
if (splitted.length === 2) {
48-
return splitted[1];
49-
}
50-
return undefined;
51-
}
52-
53-
/**
54-
* Extracts the call stack from the location of a failing opcode and the debug metadata.
55-
* One opcode can point to multiple calls due to inlining.
56-
*/
57-
function getSourceCodeLocationsFromOpcodeLocation(
58-
opcodeLocation: string,
59-
debug: FunctionDebugMetadata,
60-
brilligFunctionId?: BrilligFunctionId,
61-
): SourceCodeLocation[] {
62-
const { debugSymbols, files } = debug;
63-
64-
let callStack = debugSymbols.locations[opcodeLocation] || [];
65-
if (callStack.length === 0) {
66-
const brilligLocation = extractBrilligLocation(opcodeLocation);
67-
if (brilligFunctionId !== undefined && brilligLocation !== undefined) {
68-
callStack = debugSymbols.brillig_locations[brilligFunctionId][brilligLocation] || [];
69-
}
70-
}
71-
return callStack.map(call => {
72-
const { file: fileId, span } = call;
73-
74-
const { path, source } = files[fileId];
75-
76-
const locationText = source.substring(span.start, span.end);
77-
const precedingText = source.substring(0, span.start);
78-
const previousLines = precedingText.split('\n');
79-
// Lines and columns in stacks are one indexed.
80-
const line = previousLines.length;
81-
const column = previousLines[previousLines.length - 1].length + 1;
82-
83-
return {
84-
filePath: path,
85-
line,
86-
column,
87-
fileSource: source,
88-
locationText,
89-
};
90-
});
91-
}
92-
93-
/**
94-
* Extracts the source code locations for an array of opcode locations
95-
* @param opcodeLocations - The opcode locations that caused the error.
96-
* @param debug - The debug metadata of the function.
97-
* @returns The source code locations.
98-
*/
99-
export function resolveOpcodeLocations(
100-
opcodeLocations: OpcodeLocation[],
101-
debug: FunctionDebugMetadata,
102-
brilligFunctionId?: BrilligFunctionId,
103-
): SourceCodeLocation[] {
104-
return opcodeLocations.flatMap(opcodeLocation =>
105-
getSourceCodeLocationsFromOpcodeLocation(opcodeLocation, debug, brilligFunctionId),
106-
);
107-
}
108-
109-
export function resolveAssertionMessage(errorPayload: RawAssertionPayload, abi: FunctionAbi): string | undefined {
110-
const decoded = abiDecodeError(
111-
{ parameters: [], error_types: abi.errorTypes, return_type: null }, // eslint-disable-line camelcase
112-
errorPayload,
113-
);
114-
115-
if (typeof decoded === 'string') {
116-
return decoded;
117-
} else {
118-
return JSON.stringify(decoded);
119-
}
120-
}
121-
122-
export function resolveAssertionMessageFromRevertData(revertData: Fr[], abi: FunctionAbi): string | undefined {
123-
if (revertData.length == 0) {
124-
return undefined;
125-
}
126-
127-
const [errorSelector, ...errorData] = revertData;
128-
129-
return resolveAssertionMessage(
130-
{
131-
selector: errorSelector.toBigInt().toString(),
132-
data: errorData.map(f => f.toString()),
133-
},
134-
abi,
135-
);
136-
}
137-
138-
export function resolveAssertionMessageFromError(err: Error, abi: FunctionAbi): string {
139-
if (typeof err === 'object' && err !== null && 'rawAssertionPayload' in err && err.rawAssertionPayload) {
140-
return `Assertion failed: ${resolveAssertionMessage(err.rawAssertionPayload as RawAssertionPayload, abi)}`;
141-
} else {
142-
return err.message;
143-
}
144-
}
145-
14637
/**
14738
* The function call that executes an ACIR.
14839
*/

yarn-project/simulator/src/avm/avm_machine_state.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { type Fr } from '@aztec/circuits.js';
22

33
import { GAS_DIMENSIONS, type Gas } from './avm_gas.js';
44
import { TaggedMemory } from './avm_memory_types.js';
5-
import { OutOfGasError } from './errors.js';
5+
import { type AvmRevertReason, OutOfGasError } from './errors.js';
66

77
/**
88
* A few fields of machine state are initialized from AVM session inputs or call instruction arguments
@@ -12,6 +12,16 @@ export type InitialAvmMachineState = {
1212
daGasLeft: number;
1313
};
1414

15+
/**
16+
* Used to track the call stack and revert data of nested calls.
17+
* This is used to provide a more detailed revert reason when a contract call reverts.
18+
* It is only a heuristic and may not always provide the correct revert reason.
19+
*/
20+
type TrackedRevertInfo = {
21+
revertDataRepresentative: Fr[];
22+
recursiveRevertReason: AvmRevertReason;
23+
};
24+
1525
type CallStackEntry = {
1626
callPc: number;
1727
returnPc: number;
@@ -30,6 +40,12 @@ export class AvmMachineState {
3040
public nextPc: number = 0;
3141
/** return/revertdata of the last nested call. */
3242
public nestedReturndata: Fr[] = [];
43+
/**
44+
* Used to track the call stack and revert data of nested calls.
45+
* This is used to provide a more detailed revert reason when a contract call reverts.
46+
* It is only a heuristic and may not always provide the correct revert reason.
47+
*/
48+
public collectedRevertInfo: TrackedRevertInfo | undefined;
3349

3450
/**
3551
* On INTERNALCALL, internal call stack is pushed to with the current pc and the return pc.

yarn-project/simulator/src/avm/avm_simulator.test.ts

+16-18
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { type MemoryValue, TypeTag, type Uint8, type Uint64 } from './avm_memory
2020
import { AvmSimulator } from './avm_simulator.js';
2121
import { isAvmBytecode, markBytecodeAsAvm } from './bytecode_utils.js';
2222
import {
23+
getAvmTestContractArtifact,
2324
getAvmTestContractBytecode,
2425
initContext,
2526
initExecutionEnvironment,
@@ -321,10 +322,10 @@ describe('AVM simulator: transpiled Noir contracts', () => {
321322

322323
expect(results.reverted).toBe(true);
323324
expect(results.revertReason).toBeDefined();
325+
expect(results.output).toHaveLength(1); // Error selector for static string error
324326
expect(
325327
resolveAvmTestContractAssertionMessage('assert_nullifier_exists', results.revertReason!, results.output),
326328
).toMatch("Nullifier doesn't exist!");
327-
expect(results.output).toHaveLength(1); // Error selector for static string error
328329
});
329330

330331
describe.each([
@@ -929,14 +930,16 @@ describe('AVM simulator: transpiled Noir contracts', () => {
929930

930931
it(`Nested call with not enough gas (expect failure)`, async () => {
931932
const gas = [/*l2=*/ 5, /*da=*/ 10000].map(g => new Fr(g));
932-
const calldata: Fr[] = [value0, value1, ...gas];
933+
const targetFunctionSelector = FunctionSelector.fromSignature(
934+
'nested_call_to_add_with_gas(Field,Field,Field,Field)',
935+
);
936+
const calldata: Fr[] = [targetFunctionSelector.toField(), value0, value1, ...gas];
933937
const context = createContext(calldata);
934-
const callBytecode = getAvmTestContractBytecode('nested_call_to_add_with_gas');
935-
const nestedBytecode = getAvmTestContractBytecode('public_dispatch');
936-
mockGetBytecode(worldStateDB, nestedBytecode);
938+
const artifact = getAvmTestContractArtifact('public_dispatch');
939+
mockGetBytecode(worldStateDB, artifact.bytecode);
937940

938941
const contractClass = makeContractClassPublic(0, {
939-
bytecode: nestedBytecode,
942+
bytecode: artifact.bytecode,
940943
selector: FunctionSelector.random(),
941944
});
942945
mockGetContractClass(worldStateDB, contractClass);
@@ -945,16 +948,12 @@ describe('AVM simulator: transpiled Noir contracts', () => {
945948

946949
mockTraceFork(trace);
947950

948-
const results = await new AvmSimulator(context).executeBytecode(callBytecode);
949-
// TODO(7141): change this once we don't force rethrowing of exceptions.
950-
// Outer frame should not revert, but inner should, so the forwarded return value is 0
951-
// expect(results.revertReason).toBeUndefined();
952-
// expect(results.reverted).toBe(false);
951+
const results = await new AvmSimulator(context).executeBytecode(artifact.bytecode);
953952
expect(results.reverted).toBe(true);
954-
expect(results.revertReason?.message).toEqual('Not enough L2GAS gas left');
953+
expect(results.revertReason?.message).toMatch('Not enough L2GAS gas left');
955954

956-
// Nested call should NOT have been made and therefore should not be traced
957-
expect(trace.traceNestedCall).toHaveBeenCalledTimes(0);
955+
// Nested call should have been made (and failed).
956+
expect(trace.traceNestedCall).toHaveBeenCalledTimes(1);
958957
});
959958

960959
it(`Nested static call which modifies storage (expect failure)`, async () => {
@@ -971,7 +970,8 @@ describe('AVM simulator: transpiled Noir contracts', () => {
971970
const contractInstance = makeContractInstanceFromClassId(contractClass.id);
972971
mockGetContractInstance(worldStateDB, contractInstance);
973972

974-
mockTraceFork(trace);
973+
const nestedTrace = mock<PublicSideEffectTraceInterface>();
974+
mockTraceFork(trace, nestedTrace);
975975

976976
const results = await new AvmSimulator(context).executeBytecode(callBytecode);
977977

@@ -980,9 +980,7 @@ describe('AVM simulator: transpiled Noir contracts', () => {
980980
'Static call cannot update the state, emit L2->L1 messages or generate logs',
981981
);
982982

983-
// TODO(7141): external call doesn't recover from nested exception until
984-
// we support recoverability of reverts (here and in kernel)
985-
//expectTracedNestedCall(context.environment, results, nestedTrace, /*isStaticCall=*/true);
983+
expectTracedNestedCall(context.environment, nestedTrace, /*isStaticCall=*/ true);
986984

987985
// Nested call should NOT have been able to write storage
988986
expect(trace.tracePublicStorageWrite).toHaveBeenCalledTimes(0);

yarn-project/simulator/src/avm/avm_simulator.ts

+2-7
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
AvmExecutionError,
1313
InvalidProgramCounterError,
1414
NoBytecodeForContractError,
15-
revertDataFromExceptionalHalt,
1615
revertReasonFromExceptionalHalt,
1716
revertReasonFromExplicitRevert,
1817
} from './errors.js';
@@ -134,12 +133,8 @@ export class AvmSimulator {
134133
}
135134

136135
const revertReason = revertReasonFromExceptionalHalt(err, this.context);
137-
// Note: "exceptional halts" cannot return data, hence []
138-
const results = new AvmContractCallResult(
139-
/*reverted=*/ true,
140-
/*output=*/ revertDataFromExceptionalHalt(err),
141-
revertReason,
142-
);
136+
// Note: "exceptional halts" cannot return data, hence [].
137+
const results = new AvmContractCallResult(/*reverted=*/ true, /*output=*/ [], revertReason);
143138
this.log.debug(`Context execution results: ${results.toString()}`);
144139

145140
this.printOpcodeTallies();

0 commit comments

Comments
 (0)