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

feat: add gate count profiling for transactions #9632

Merged
merged 13 commits into from
Nov 4, 2024
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { FunctionCall, TxExecutionRequest } from '@aztec/circuit-types';
import type { FunctionCall, PrivateKernelProverProfileResult, TxExecutionRequest } from '@aztec/circuit-types';
import { type AztecAddress, type GasSettings } from '@aztec/circuits.js';
import {
type FunctionAbi,
Expand Down Expand Up @@ -27,6 +27,14 @@ export type SimulateMethodOptions = {
skipTxValidation?: boolean;
};

/**
* The result of a profile() call.
*/
export type ProfileResult = PrivateKernelProverProfileResult & {
/** The result of the transaction as returned by the contract function. */
returnValues: any;
};

/**
* This is the class that is returned when calling e.g. `contract.methods.myMethod(arg0, arg1)`.
* It contains available interactions one can call on a method, including view.
Expand Down Expand Up @@ -110,4 +118,30 @@ export class ContractFunctionInteraction extends BaseContractInteraction {

return rawReturnValues ? decodeFromAbi(this.functionDao.returnTypes, rawReturnValues) : [];
}

/**
* Simulate a transaction and profile the gate count for each function in the transaction.
* @param options - Same options as `simulate`.
*
* @returns An object containing the function return value and profile result.
*/
public async simulateWithProfile(options: SimulateMethodOptions = {}): Promise<ProfileResult> {
if (this.functionDao.functionType == FunctionType.UNCONSTRAINED) {
throw new Error("Can't profile an unconstrained function.");
}

const txRequest = await this.create();
const simulatedTx = await this.wallet.simulateTx(txRequest, true, options?.from, options?.skipTxValidation, true);

const rawReturnValues =
this.functionDao.functionType == FunctionType.PRIVATE
? simulatedTx.getPrivateReturnValues().nested?.[0].values
: simulatedTx.getPublicReturnValues()?.[0].values;
const rawReturnValuesDecoded = rawReturnValues ? decodeFromAbi(this.functionDao.returnTypes, rawReturnValues) : [];

return {
returnValues: rawReturnValuesDecoded,
gateCounts: simulatedTx.profileResult!.gateCounts,
};
}
}
1 change: 1 addition & 0 deletions yarn-project/aztec.js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export {
type DeployOptions,
type SendMethodOptions,
type WaitOpts,
type ProfileResult,
} from './contract/index.js';

export { ContractDeployer } from './deployment/index.js';
Expand Down
3 changes: 2 additions & 1 deletion yarn-project/aztec.js/src/wallet/base_wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,9 @@ export abstract class BaseWallet implements Wallet {
simulatePublic: boolean,
msgSender?: AztecAddress,
skipTxValidation?: boolean,
profile?: boolean,
): Promise<TxSimulationResult> {
return this.pxe.simulateTx(txRequest, simulatePublic, msgSender, skipTxValidation, this.scopes);
return this.pxe.simulateTx(txRequest, simulatePublic, msgSender, skipTxValidation, profile, this.scopes);
}
sendTx(tx: Tx): Promise<TxHash> {
return this.pxe.sendTx(tx);
Expand Down
76 changes: 76 additions & 0 deletions yarn-project/bb-prover/src/bb/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export type BBSuccess = {
proofPath?: string;
/** Full path of the contract. */
contractPath?: string;
/** The number of gates in the circuit. */
circuitSize?: number;
};

export type BBFailure = {
Expand Down Expand Up @@ -872,6 +874,80 @@ export async function generateContractForCircuit(
);
}

/**
* Compute bb gate count for a given circuit
* @param pathToBB - The full path to the bb binary
* @param workingDirectory - A temporary directory for writing the bytecode
* @param circuitName - The name of the circuit
* @param bytecode - The bytecode of the circuit
* @param flavor - The flavor of the backend - mega_honk or ultra_honk variants
* @returns An object containing the status, gate count, and time taken
*/
export async function computeGateCountForCircuit(
pathToBB: string,
workingDirectory: string,
circuitName: string,
bytecode: Buffer,
flavor: UltraHonkFlavor | 'mega_honk',
): Promise<BBFailure | BBSuccess> {
// Check that the working directory exists
try {
await fs.access(workingDirectory);
} catch (error) {
return { status: BB_RESULT.FAILURE, reason: `Working directory ${workingDirectory} does not exist` };
}

// The bytecode is written to e.g. /workingDirectory/BaseParityArtifact-bytecode
const bytecodePath = `${workingDirectory}/${circuitName}-bytecode`;

const binaryPresent = await fs
.access(pathToBB, fs.constants.R_OK)
.then(_ => true)
.catch(_ => false);
if (!binaryPresent) {
return { status: BB_RESULT.FAILURE, reason: `Failed to find bb binary at ${pathToBB}` };
}

// Accumulate the stdout from bb
let stdout = '';
const logHandler = (message: string) => {
stdout += message;
};

try {
// Write the bytecode to the working directory
await fs.writeFile(bytecodePath, bytecode);
const timer = new Timer();

const result = await executeBB(
pathToBB,
flavor === 'mega_honk' ? `gates_mega_honk` : `gates`,
['-b', bytecodePath, '-v'],
logHandler,
);
const duration = timer.ms();

if (result.status == BB_RESULT.SUCCESS) {
// Look for "circuit_size" in the stdout and parse the number
const circuitSizeMatch = stdout.match(/circuit_size": (\d+)/);
if (!circuitSizeMatch) {
return { status: BB_RESULT.FAILURE, reason: 'Failed to parse circuit_size from bb gates stdout.' };
}
const circuitSize = parseInt(circuitSizeMatch[1]);

return {
status: BB_RESULT.SUCCESS,
durationMs: duration,
circuitSize: circuitSize,
};
}

return { status: BB_RESULT.FAILURE, reason: 'Failed getting the gate count.' };
} catch (error) {
return { status: BB_RESULT.FAILURE, reason: `${error}` };
}
}

const CACHE_FILENAME = '.cache';
async function fsCache<T>(
dir: string,
Expand Down
16 changes: 16 additions & 0 deletions yarn-project/bb-prover/src/prover/bb_private_kernel_prover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
BB_RESULT,
PROOF_FIELDS_FILENAME,
PROOF_FILENAME,
computeGateCountForCircuit,
computeVerificationKey,
executeBbClientIvcProof,
verifyProof,
Expand Down Expand Up @@ -228,6 +229,21 @@ export class BBNativePrivateKernelProver implements PrivateKernelProver {
this.log.info(`Successfully verified ${circuitType} proof in ${Math.ceil(result.durationMs)} ms`);
}

public async computeGateCountForCircuit(bytecode: Buffer, circuitName: string): Promise<number> {
const result = await computeGateCountForCircuit(
this.bbBinaryPath,
this.bbWorkingDirectory,
circuitName,
bytecode,
'mega_honk',
);
if (result.status === BB_RESULT.FAILURE) {
throw new Error(result.reason);
}

return result.circuitSize as number;
}

private async verifyProofFromKey(
flavor: UltraHonkFlavor,
verificationKey: Buffer,
Expand Down
14 changes: 14 additions & 0 deletions yarn-project/circuit-types/src/interfaces/private_kernel_prover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import {

import { type WitnessMap } from '@noir-lang/acvm_js';

export type PrivateKernelProverProfileResult = {
gateCounts: { circuitName: string; gateCount: number }[];
};

/**
* Represents the output of the proof creation process for init and inner private kernel circuit.
* Contains the public inputs required for the init and inner private kernel circuit and the generated proof.
Expand All @@ -28,6 +32,8 @@ export type PrivateKernelSimulateOutput<PublicInputsType> = {
outputWitness: WitnessMap;

bytecode: Buffer;

profileResult?: PrivateKernelProverProfileResult;
};

/**
Expand Down Expand Up @@ -97,4 +103,12 @@ export interface PrivateKernelProver {
* @returns A Promise resolving to a Proof object
*/
computeAppCircuitVerificationKey(bytecode: Buffer, appCircuitName?: string): Promise<AppCircuitSimulateOutput>;

/**
* Compute the gate count for a given circuit.
* @param bytecode - The circuit bytecode in gzipped bincode format
* @param circuitName - The name of the circuit
* @returns A Promise resolving to the gate count
*/
computeGateCountForCircuit(bytecode: Buffer, circuitName: string): Promise<number>;
}
2 changes: 2 additions & 0 deletions yarn-project/circuit-types/src/interfaces/pxe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export interface PXE {
* @param simulatePublic - Whether to simulate the public part of the transaction.
* @param msgSender - (Optional) The message sender to use for the simulation.
* @param skipTxValidation - (Optional) If false, this function throws if the transaction is unable to be included in a block at the current state.
* @param profile - (Optional) If true, will run the private kernel prover with profiling enabled and include the result (gate count) in TxSimulationResult.
* @param scopes - (Optional) The accounts whose notes we can access in this call. Currently optional and will default to all.
* @returns A simulated transaction result object that includes public and private return values.
* @throws If the code for the functions executed in this transaction has not been made available via `addContracts`.
Expand All @@ -171,6 +172,7 @@ export interface PXE {
simulatePublic: boolean,
msgSender?: AztecAddress,
skipTxValidation?: boolean,
profile?: boolean,
scopes?: AztecAddress[],
): Promise<TxSimulationResult>;

Expand Down
14 changes: 12 additions & 2 deletions yarn-project/circuit-types/src/tx/simulated_tx.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { ClientIvcProof, PrivateKernelTailCircuitPublicInputs } from '@aztec/circuits.js';

import { EncryptedNoteTxL2Logs, EncryptedTxL2Logs, UnencryptedTxL2Logs } from '../index.js';
import {
EncryptedNoteTxL2Logs,
EncryptedTxL2Logs,
type PrivateKernelProverProfileResult,
UnencryptedTxL2Logs,
} from '../index.js';
import {
PrivateExecutionResult,
collectEnqueuedPublicFunctionCalls,
Expand Down Expand Up @@ -60,6 +65,7 @@ export class TxSimulationResult extends PrivateSimulationResult {
privateExecutionResult: PrivateExecutionResult,
publicInputs: PrivateKernelTailCircuitPublicInputs,
public publicOutput?: PublicSimulationOutput,
public profileResult?: PrivateKernelProverProfileResult,
) {
super(privateExecutionResult, publicInputs);
}
Expand All @@ -71,11 +77,13 @@ export class TxSimulationResult extends PrivateSimulationResult {
static fromPrivateSimulationResultAndPublicOutput(
privateSimulationResult: PrivateSimulationResult,
publicOutput?: PublicSimulationOutput,
profileResult?: PrivateKernelProverProfileResult,
) {
return new TxSimulationResult(
privateSimulationResult.privateExecutionResult,
privateSimulationResult.publicInputs,
publicOutput,
profileResult,
);
}

Expand All @@ -84,14 +92,16 @@ export class TxSimulationResult extends PrivateSimulationResult {
privateExecutionResult: this.privateExecutionResult.toJSON(),
publicInputs: this.publicInputs.toBuffer().toString('hex'),
publicOutput: this.publicOutput ? this.publicOutput.toJSON() : undefined,
profileResult: this.profileResult,
};
}

public static override fromJSON(obj: any) {
const privateExecutionResult = PrivateExecutionResult.fromJSON(obj.privateExecutionResult);
const publicInputs = PrivateKernelTailCircuitPublicInputs.fromBuffer(Buffer.from(obj.publicInputs, 'hex'));
const publicOuput = obj.publicOutput ? PublicSimulationOutput.fromJSON(obj.publicOutput) : undefined;
return new TxSimulationResult(privateExecutionResult, publicInputs, publicOuput);
const profileResult = obj.profileResult;
return new TxSimulationResult(privateExecutionResult, publicInputs, publicOuput, profileResult);
}
}

Expand Down
5 changes: 4 additions & 1 deletion yarn-project/cli-wallet/src/cmds/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
createArgsOption,
createArtifactOption,
createContractAddressOption,
createProfileOption,
createTypeOption,
integerArgParser,
parsePaymentMethod,
Expand Down Expand Up @@ -287,6 +288,7 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL
)
.addOption(createAccountOption('Alias or address of the account to simulate from', !db, db))
.addOption(createTypeOption(false))
.addOption(createProfileOption())
.action(async (functionName, _options, command) => {
const { simulate } = await import('./simulate.js');
const options = command.optsWithGlobals();
Expand All @@ -299,13 +301,14 @@ export function injectCommands(program: Command, log: LogFn, debugLogger: DebugL
type,
secretKey,
publicKey,
profile,
} = options;

const client = await createCompatibleClient(rpcUrl, debugLogger);
const account = await createOrRetrieveAccount(client, parsedFromAddress, db, type, secretKey, Fr.ZERO, publicKey);
const wallet = await getWalletWithScopes(account, db);
const artifactPath = await artifactPathFromPromiseOrAlias(artifactPathPromise, contractAddress, db);
await simulate(wallet, functionName, args, artifactPath, contractAddress, log);
await simulate(wallet, functionName, args, artifactPath, contractAddress, profile, log);
});

program
Expand Down
24 changes: 21 additions & 3 deletions yarn-project/cli-wallet/src/cmds/simulate.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,40 @@
import { type AccountWalletWithSecretKey, type AztecAddress, Contract } from '@aztec/aztec.js';
import { type AccountWalletWithSecretKey, type AztecAddress, Contract, type ProfileResult } from '@aztec/aztec.js';
import { prepTx } from '@aztec/cli/utils';
import { type LogFn } from '@aztec/foundation/log';

import { format } from 'util';

function printProfileResult(result: ProfileResult, log: LogFn) {
log(format('Simulation result:'));
log(format('Return value: ', JSON.stringify(result.returnValues, null, 2)));

log(format('Gate count: '));
let acc = 0;
result.gateCounts.forEach(r => {
acc += r.gateCount;
log(format(' ', r.circuitName.padEnd(30), 'Gates:', r.gateCount, '\tAcc:', acc));
});
}

export async function simulate(
wallet: AccountWalletWithSecretKey,
functionName: string,
functionArgsIn: any[],
contractArtifactPath: string,
contractAddress: AztecAddress,
profile: boolean,
log: LogFn,
) {
const { functionArgs, contractArtifact } = await prepTx(contractArtifactPath, functionName, functionArgsIn, log);

const contract = await Contract.at(contractAddress, contractArtifact, wallet);
const call = contract.methods[functionName](...functionArgs);

const result = await call.simulate();
log(format('\nSimulation result: ', result, '\n'));
if (profile) {
const result = await call.simulateWithProfile();
printProfileResult(result, log);
} else {
const result = await call.simulate();
log(format('\nSimulation result: ', result, '\n'));
}
}
7 changes: 7 additions & 0 deletions yarn-project/cli-wallet/src/utils/options/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ export function createArtifactOption(db?: WalletDB) {
.makeOptionMandatory(false);
}

export function createProfileOption() {
return new Option(
'-p, --profile',
'Run the real prover and get the gate count for each function in the transaction.',
).default(false);
}

async function contractArtifactFromWorkspace(pkg?: string, contractName?: string) {
const cwd = process.cwd();
try {
Expand Down
Loading
Loading