Skip to content

Commit

Permalink
feat: Simulate enqueued public functions and locate failing constrain…
Browse files Browse the repository at this point in the history
…ts on them (#1853)

Resolves #1813 and
#1377
- Simulates the public part of transactions before submission to the
node.
- Introduces a SimulationError to show complete call stacks (aztec
function calls and noir function calls)
- Reconstructs public function errors to show noir call stacks on public
functions after simulation
- Fixes a bug where the public tx processor was mutating Tx objects in
place.
  • Loading branch information
sirasistant authored Aug 30, 2023
1 parent 8b43fdd commit a065fd5
Show file tree
Hide file tree
Showing 27 changed files with 600 additions and 103 deletions.
87 changes: 44 additions & 43 deletions yarn-project/acir-simulator/src/acvm/acvm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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 {
ForeignCallInput,
Expand Down Expand Up @@ -77,32 +78,10 @@ function extractOpcodeLocationFromError(err: string): string | undefined {
return match?.groups?.opcodeLocation;
}

/**
* The data for a call in the call stack.
*/
interface SourceCodeLocation {
/**
* The path to the source file.
*/
filePath: string;
/**
* The line number of the call.
*/
line: number;
/**
* The source code of the file.
*/
fileSource: string;
/**
* The source code text of the failed constraint.
*/
assertionText: string;
}

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

const callStack = debugSymbols.locations[opcodeLocation] || [];
Expand All @@ -111,30 +90,47 @@ function getCallStackFromOpcodeLocation(opcodeLocation: string, debug: FunctionD

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

const assertionText = source.substring(span.start, span.end + 1);
const locationText = source.substring(span.start, span.end + 1);
const precedingText = source.substring(0, span.start);
const line = precedingText.split('\n').length;

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

/**
* Creates a formatted string for an error stack
* @param callStack - The error stack
* @returns - The formatted string
* Extracts source code locations from an ACVM error if possible.
* @param errMessage - The ACVM 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.
*/
function printErrorStack(callStack: SourceCodeLocation[]): string {
// TODO experiment with formats of reporting this for better error reporting
return [
'Error: Assertion failed',
callStack.map(call => ` at ${call.filePath}:${call.line} '${call.assertionText}'`),
].join('\n');
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);
}
}

/**
Expand Down Expand Up @@ -174,22 +170,27 @@ export async function acvm(
typedError = new Error(`Error in oracle callback ${err}`);
}
oracleError = typedError;
logger.error(`Error in oracle callback ${name}: ${typedError.message}`);
logger.error(`Error in oracle callback ${name}:`, typedError.message, typedError.stack);
throw typedError;
}
},
).catch((acvmError: string) => {
).catch((acvmErrorString: string) => {
if (oracleError) {
throw oracleError;
}
const opcodeLocation = extractOpcodeLocationFromError(acvmError);
if (!opcodeLocation || !debug) {
throw new Error(acvmError);
}

const callStack = getCallStackFromOpcodeLocation(opcodeLocation, debug);
logger(printErrorStack(callStack));
throw new Error(`Assertion failed: '${callStack.pop()?.assertionText ?? 'Unknown'}'`);
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 Promise.resolve({ partialWitness });
Expand Down
10 changes: 6 additions & 4 deletions yarn-project/acir-simulator/src/client/private_execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +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 } from '@aztec/types';
import { FunctionL2Logs, NotePreimage, NoteSpendingInfo, SimulationError } from '@aztec/types';

import { extractPrivateCircuitPublicInputs, frToAztecAddress } from '../acvm/deserialize.js';
import {
Expand Down Expand Up @@ -55,8 +55,8 @@ export class PrivateFunctionExecution {
* @returns The execution result.
*/
public async run(): Promise<ExecutionResult> {
const selector = this.functionData.selector.toString();
this.log(`Executing external function ${this.contractAddress.toString()}:${selector}`);
const selector = this.functionData.selector;
this.log(`Executing external function ${this.contractAddress}:${selector}`);

const acir = Buffer.from(this.abi.bytecode, 'base64');
const initialWitness = this.getInitialWitness();
Expand Down Expand Up @@ -197,7 +197,9 @@ export class PrivateFunctionExecution {
},
},
this.abi.debug,
);
).catch((err: Error) => {
throw SimulationError.fromError(this.contractAddress, selector, err);
});

const publicInputs = extractPrivateCircuitPublicInputs(partialWitness, acir);

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 } from '@aztec/types';
import { AztecNode, SimulationError } from '@aztec/types';

import { extractReturnWitness, frToAztecAddress } from '../acvm/deserialize.js';
import { ACVMField, ZERO_ACVM_FIELD, acvm, fromACVMField, toACVMField, toACVMWitness } from '../acvm/index.js';
Expand Down Expand Up @@ -33,9 +33,7 @@ export class UnconstrainedFunctionExecution {
* @returns The return values of the executed function.
*/
public async run(aztecNode?: AztecNode): Promise<DecodedReturn> {
this.log(
`Executing unconstrained function ${this.contractAddress.toShortString()}:${this.functionData.selector.toString()}`,
);
this.log(`Executing unconstrained function ${this.contractAddress.toShortString()}:${this.functionData.selector}`);

const acir = Buffer.from(this.abi.bytecode, 'base64');
const initialWitness = toACVMWitness(1, this.args);
Expand Down Expand Up @@ -105,7 +103,9 @@ export class UnconstrainedFunctionExecution {
},
},
this.abi.debug,
);
).catch((err: Error) => {
throw SimulationError.fromError(this.contractAddress, this.functionData.selector, err);
});

const returnValues: ACVMField[] = extractReturnWitness(acir, partialWitness);

Expand Down
5 changes: 3 additions & 2 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 } from '@aztec/types';
import { FunctionL2Logs, SimulationError } from '@aztec/types';

import {
ZERO_ACVM_FIELD,
Expand Down Expand Up @@ -67,7 +67,6 @@ export class PublicExecutor {
// Functions can request to pack arguments before calling other functions.
// We use this cache to hold the packed arguments.
const packedArgs = await PackedArgsCache.create([]);

const { partialWitness } = await acvm(await AcirSimulator.getSolver(), acir, initialWitness, {
packArguments: async args => {
return toACVMField(await packedArgs.pack(args.map(fromACVMField)));
Expand Down Expand Up @@ -140,6 +139,8 @@ export class PublicExecutor {
(await this.contractsDb.getPortalContractAddress(contractAddress)) ?? EthAddress.ZERO;
return Promise.resolve(toACVMField(portalContactAddress));
},
}).catch((err: Error) => {
throw SimulationError.fromError(execution.contractAddress, selector, err);
});

const {
Expand Down
35 changes: 34 additions & 1 deletion yarn-project/aztec-node/src/aztec-node/http-node.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AztecAddress, CircuitsWasm, EthAddress, Fr, HistoricBlockData } from '@aztec/circuits.js';
import { AztecAddress, CircuitsWasm, EthAddress, Fr, FunctionSelector, HistoricBlockData } from '@aztec/circuits.js';
import { randomBytes } from '@aztec/foundation/crypto';
import { Pedersen } from '@aztec/merkle-tree';
import {
Expand All @@ -10,6 +10,7 @@ import {
LogType,
MerkleTreeId,
SiblingPath,
SimulationError,
TxHash,
mockTx,
} from '@aztec/types';
Expand Down Expand Up @@ -482,4 +483,36 @@ describe('HttpNode', () => {
expect(result).toEqual(blockData);
});
});

describe('simulatePublicCalls', () => {
it('should fetch a successful simulation response', async () => {
const tx = mockTx();
const response = {};
setFetchMock(response);

await httpNode.simulatePublicCalls(tx);

const init: RequestInit = {
method: 'POST',
body: tx.toBuffer(),
};
const call = (fetch as jest.Mock).mock.calls[0] as any[];
expect(call[0].href).toBe(`${TEST_URL}simulate-tx`);
expect(call[1]).toStrictEqual(init);
});

it('should fetch a simulation error', async () => {
const tx = mockTx();
const simulationError = new SimulationError('Failing function', {
contractAddress: AztecAddress.ZERO,
functionSelector: FunctionSelector.empty(),
});
const response = {
simulationError: simulationError.toJSON(),
};
setFetchMock(response);

await expect(httpNode.simulatePublicCalls(tx)).rejects.toThrow(simulationError);
});
});
});
16 changes: 16 additions & 0 deletions yarn-project/aztec-node/src/aztec-node/http-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
LogType,
MerkleTreeId,
SiblingPath,
SimulationError,
Tx,
TxHash,
} from '@aztec/types';
Expand Down Expand Up @@ -369,4 +370,19 @@ export class HttpNode implements AztecNode {
const response = await (await fetch(url.toString())).json();
return response.blockData;
}

/**
* Simulates the public part of a transaction with the current state.
* @param tx - The transaction to simulate.
**/
public async simulatePublicCalls(tx: Tx) {
const url = new URL(`${this.baseUrl}/simulate-tx`);
const init: RequestInit = {};
init['method'] = 'POST';
init['body'] = tx.toBuffer();
const response = await (await fetch(url, init)).json();
if (response.simulationError) {
throw SimulationError.fromJSON(response.simulationError);
}
}
}
48 changes: 45 additions & 3 deletions yarn-project/aztec-node/src/aztec-node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@ import {
CircuitsWasm,
EthAddress,
Fr,
GlobalVariables,
HistoricBlockData,
L1_TO_L2_MSG_TREE_HEIGHT,
PRIVATE_DATA_TREE_HEIGHT,
} from '@aztec/circuits.js';
import { AztecAddress } from '@aztec/foundation/aztec-address';
import { createDebugLogger } from '@aztec/foundation/log';
import { InMemoryTxPool, P2P, createP2PClient } from '@aztec/p2p';
import { SequencerClient } from '@aztec/sequencer-client';
import {
GlobalVariableBuilder,
PublicProcessorFactory,
SequencerClient,
getGlobalVariableBuilder,
} from '@aztec/sequencer-client';
import {
AztecNode,
ContractData,
Expand Down Expand Up @@ -61,6 +67,8 @@ export class AztecNodeService implements AztecNode {
protected sequencer: SequencerClient,
protected chainId: number,
protected version: number,
protected globalVariableBuilder: GlobalVariableBuilder,
protected merkleTreesDb: levelup.LevelUp,
private log = createDebugLogger('aztec:node'),
) {}

Expand All @@ -81,9 +89,10 @@ export class AztecNodeService implements AztecNode {
const p2pClient = await createP2PClient(config, new InMemoryTxPool(), archiver);

// now create the merkle trees and the world state syncher
const merkleTreeDB = await MerkleTrees.new(levelup(createMemDown()), await CircuitsWasm.get());
const merkleTreesDb = levelup(createMemDown());
const merkleTrees = await MerkleTrees.new(merkleTreesDb, await CircuitsWasm.get());
const worldStateConfig: WorldStateConfig = getWorldStateConfig();
const worldStateSynchroniser = new ServerWorldStateSynchroniser(merkleTreeDB, archiver, worldStateConfig);
const worldStateSynchroniser = new ServerWorldStateSynchroniser(merkleTrees, archiver, worldStateConfig);

// start both and wait for them to sync from the block source
await Promise.all([p2pClient.start(), worldStateSynchroniser.start()]);
Expand All @@ -108,6 +117,8 @@ export class AztecNodeService implements AztecNode {
sequencer,
config.chainId,
config.version,
getGlobalVariableBuilder(config),
merkleTreesDb,
);
}

Expand Down Expand Up @@ -367,6 +378,37 @@ export class AztecNodeService implements AztecNode {
);
}

/**
* Simulates the public part of a transaction with the current state.
* @param tx - The transaction to simulate.
**/
public async simulatePublicCalls(tx: Tx) {
this.log.info(`Simulating tx ${await tx.getTxHash()}`);
const blockNumber = (await this.blockSource.getBlockNumber()) + 1;
const newGlobalVariables = await this.globalVariableBuilder.buildGlobalVariables(new Fr(blockNumber));
const prevGlobalVariables = (await this.blockSource.getL2Block(-1))?.globalVariables ?? GlobalVariables.empty();

// Instantiate merkle trees so uncommited updates by this simulation are local to it.
// TODO we should be able to remove this after https://github.com/AztecProtocol/aztec-packages/issues/1869
// So simulation of public functions doesn't affect the merkle trees.
const merkleTrees = new MerkleTrees(this.merkleTreesDb, this.log);
await merkleTrees.init(await CircuitsWasm.get(), {
globalVariables: prevGlobalVariables,
});

const publicProcessorFactory = new PublicProcessorFactory(
merkleTrees.asLatest(),
this.contractDataSource,
this.l1ToL2MessageSource,
);
const processor = await publicProcessorFactory.create(prevGlobalVariables, newGlobalVariables);
const [, failedTxs] = await processor.process([tx]);
if (failedTxs.length) {
throw failedTxs[0].error;
}
this.log.info(`Simulated tx ${await tx.getTxHash()} succeeds`);
}

/**
* Returns an instance of MerkleTreeOperations having first ensured the world state is fully synched
* @returns An instance of a committed MerkleTreeOperations
Expand Down
Loading

0 comments on commit a065fd5

Please sign in to comment.