Skip to content

Commit

Permalink
chore(benchmark): Measure time to decrypt notes in pxe (#2714)
Browse files Browse the repository at this point in the history
Adds two new metrics, `note_successful_decrypting_time` and
`note_unsuccessful_decrypting_time` that track the time required by a
note processor to successfully or unsuccessfully (ie notes were not
meant for them, so decryption failed) process N notes.

Adds a new event `note-processor-caught-up` which logs the duration of
the catch up, along with the total number of notes seen, processed, and
failed. Also tweaks the pxe, synchroniser, and note processor so they
can be configured to start from a custom L2 block rather from the first
block.

Also adds a convenience `AccountWalletWithPrivateKey` class that lets
exporting the encryption private key, which is useful for grabbing an
account from a pxe and importing it into another.
  • Loading branch information
spalladino authored Oct 6, 2023
1 parent 7a58fe9 commit 33a230a
Show file tree
Hide file tree
Showing 21 changed files with 200 additions and 72 deletions.
15 changes: 15 additions & 0 deletions scripts/ci/aggregate_e2e_benchmark.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ const {
CIRCUIT_OUTPUT_SIZE,
CIRCUIT_INPUT_SIZE,
CIRCUIT_SIMULATED,
NOTE_SUCCESSFUL_DECRYPTING_TIME,
NOTE_TRIAL_DECRYPTING_TIME,
NOTE_PROCESSOR_CAUGHT_UP,
ROLLUP_SIZES,
BENCHMARK_FILE_JSON,
} = require("./benchmark_shared.js");
Expand Down Expand Up @@ -75,6 +78,16 @@ function processCircuitSimulation(entry, results) {
append(results, CIRCUIT_OUTPUT_SIZE, bucket, entry.outputSize);
}

// Processes an entry with event name 'note-processor-caught-up' and updates results
// Buckets are rollup sizes
function processNoteProcessorCaughtUp(entry, results) {
const { seen, decrypted } = entry;
if (ROLLUP_SIZES.includes(decrypted))
append(results, NOTE_SUCCESSFUL_DECRYPTING_TIME, decrypted, entry.duration);
if (ROLLUP_SIZES.includes(seen) && decrypted === 0)
append(results, NOTE_TRIAL_DECRYPTING_TIME, seen, entry.duration);
}

// Processes a parsed entry from a logfile and updates results
function processEntry(entry, results) {
switch (entry.eventName) {
Expand All @@ -84,6 +97,8 @@ function processEntry(entry, results) {
return processRollupBlockSynced(entry, results);
case CIRCUIT_SIMULATED:
return processCircuitSimulation(entry, results);
case NOTE_PROCESSOR_CAUGHT_UP:
return processNoteProcessorCaughtUp(entry, results);
default:
return;
}
Expand Down
3 changes: 3 additions & 0 deletions scripts/ci/benchmark_shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ module.exports = {
CIRCUIT_SIMULATION_TIME: "circuit_simulation_time_in_ms",
CIRCUIT_INPUT_SIZE: "circuit_input_size_in_bytes",
CIRCUIT_OUTPUT_SIZE: "circuit_output_size_in_bytes",
NOTE_SUCCESSFUL_DECRYPTING_TIME: "note_successful_decrypting_time",
NOTE_TRIAL_DECRYPTING_TIME: "note_unsuccessful_decrypting_time",
// Events to track
L2_BLOCK_PUBLISHED_TO_L1: "rollup-published-to-l1",
L2_BLOCK_SYNCED: "l2-block-handled",
CIRCUIT_SIMULATED: "circuit-simulation",
NOTE_PROCESSOR_CAUGHT_UP: "note-processor-caught-up",
// Other
ROLLUP_SIZES,
BENCHMARK_FILE_JSON,
Expand Down
16 changes: 11 additions & 5 deletions yarn-project/aztec.js/src/account/manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { PublicKey, getContractDeploymentInfo } from '@aztec/circuits.js';
import { Fr } from '@aztec/foundation/fields';
import { CompleteAddress, GrumpkinPrivateKey, PXE } from '@aztec/types';

import { AccountWallet, ContractDeployer, DeployMethod, WaitOpts, generatePublicKey } from '../../index.js';
import {
AccountWalletWithPrivateKey,
ContractDeployer,
DeployMethod,
WaitOpts,
generatePublicKey,
} from '../../index.js';
import { AccountContract, Salt } from '../index.js';
import { AccountInterface } from '../interface.js';
import { DeployAccountSentTx } from './deploy_account_sent_tx.js';
Expand Down Expand Up @@ -73,9 +79,9 @@ export class AccountManager {
* instances to be interacted with from this account.
* @returns A Wallet instance.
*/
public async getWallet(): Promise<AccountWallet> {
public async getWallet(): Promise<AccountWalletWithPrivateKey> {
const entrypoint = await this.getAccount();
return new AccountWallet(this.pxe, entrypoint);
return new AccountWalletWithPrivateKey(this.pxe, entrypoint, this.encryptionPrivateKey);
}

/**
Expand All @@ -84,7 +90,7 @@ export class AccountManager {
* Use the returned wallet to create Contract instances to be interacted with from this account.
* @returns A Wallet instance.
*/
public async register(): Promise<AccountWallet> {
public async register(): Promise<AccountWalletWithPrivateKey> {
const completeAddress = await this.getCompleteAddress();
await this.pxe.registerAccount(this.encryptionPrivateKey, completeAddress.partialAddress);
return this.getWallet();
Expand Down Expand Up @@ -132,7 +138,7 @@ export class AccountManager {
* @param opts - Options to wait for the tx to be mined.
* @returns A Wallet instance.
*/
public async waitDeploy(opts: WaitOpts = {}): Promise<AccountWallet> {
public async waitDeploy(opts: WaitOpts = {}): Promise<AccountWalletWithPrivateKey> {
await this.deploy().then(tx => tx.wait(opts));
return this.getWallet();
}
Expand Down
6 changes: 3 additions & 3 deletions yarn-project/aztec.js/src/account/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { CompleteAddress, GrumpkinScalar } from '@aztec/circuits.js';
import { PXE } from '@aztec/types';

import { getSchnorrAccount } from '../index.js';
import { AccountWallet } from '../wallet/account_wallet.js';
import { AccountWalletWithPrivateKey } from '../wallet/account_wallet.js';

/**
* Deploys and registers a new account using random private keys and returns the associated Schnorr account wallet. Useful for testing.
* @param pxe - PXE.
* @returns - A wallet for a fresh account.
*/
export function createAccount(pxe: PXE): Promise<AccountWallet> {
export function createAccount(pxe: PXE): Promise<AccountWalletWithPrivateKey> {
return getSchnorrAccount(pxe, GrumpkinScalar.random(), GrumpkinScalar.random()).waitDeploy();
}

Expand All @@ -30,7 +30,7 @@ export async function createRecipient(pxe: PXE): Promise<CompleteAddress> {
* @param numberOfAccounts - How many accounts to create.
* @returns The created account wallets.
*/
export async function createAccounts(pxe: PXE, numberOfAccounts = 1): Promise<AccountWallet[]> {
export async function createAccounts(pxe: PXE, numberOfAccounts = 1): Promise<AccountWalletWithPrivateKey[]> {
const accounts = [];

// Prepare deployments
Expand Down
4 changes: 2 additions & 2 deletions yarn-project/aztec.js/src/sandbox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { sleep } from '@aztec/foundation/sleep';
import zip from 'lodash.zip';

import SchnorrAccountContractAbi from '../abis/schnorr_account_contract.json' assert { type: 'json' };
import { AccountWallet, PXE, createPXEClient, getSchnorrAccount } from '../index.js';
import { AccountWalletWithPrivateKey, PXE, createPXEClient, getSchnorrAccount } from '../index.js';

export const INITIAL_SANDBOX_ENCRYPTION_KEYS = [
GrumpkinScalar.fromString('2153536ff6628eee01cf4024889ff977a18d9fa61d0e414422f7681cf085c281'),
Expand All @@ -25,7 +25,7 @@ export const { PXE_URL = 'http://localhost:8080' } = process.env;
* @param pxe - PXE instance.
* @returns A set of AccountWallet implementations for each of the initial accounts.
*/
export function getSandboxAccountsWallets(pxe: PXE): Promise<AccountWallet[]> {
export function getSandboxAccountsWallets(pxe: PXE): Promise<AccountWalletWithPrivateKey[]> {
return Promise.all(
zip(INITIAL_SANDBOX_ENCRYPTION_KEYS, INITIAL_SANDBOX_SIGNING_KEYS, INITIAL_SANDBOX_SALTS).map(
([encryptionKey, signingKey, salt]) => getSchnorrAccount(pxe, encryptionKey!, signingKey!, salt).getWallet(),
Expand Down
18 changes: 17 additions & 1 deletion yarn-project/aztec.js/src/wallet/account_wallet.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Fr } from '@aztec/circuits.js';
import { Fr, GrumpkinPrivateKey } from '@aztec/circuits.js';
import { ABIParameterVisibility, FunctionAbiHeader, FunctionType } from '@aztec/foundation/abi';
import { AuthWitness, FunctionCall, PXE, TxExecutionRequest } from '@aztec/types';

Expand Down Expand Up @@ -68,3 +68,19 @@ export class AccountWallet extends BaseWallet {
};
}
}

/**
* Extends {@link AccountWallet} with the encryption private key. Not required for
* implementing the wallet interface but useful for testing purposes or exporting
* an account to another pxe.
*/
export class AccountWalletWithPrivateKey extends AccountWallet {
constructor(pxe: PXE, account: AccountInterface, private encryptionPrivateKey: GrumpkinPrivateKey) {
super(pxe, account);
}

/** Returns the encryption private key associated with this account. */
public getEncryptionPrivateKey() {
return this.encryptionPrivateKey;
}
}
2 changes: 1 addition & 1 deletion yarn-project/aztec.js/src/wallet/base_wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export abstract class BaseWallet implements Wallet {

abstract createAuthWitness(message: Fr): Promise<AuthWitness>;

registerAccount(privKey: GrumpkinPrivateKey, partialAddress: PartialAddress): Promise<void> {
registerAccount(privKey: GrumpkinPrivateKey, partialAddress: PartialAddress): Promise<CompleteAddress> {
return this.pxe.registerAccount(privKey, partialAddress);
}
registerRecipient(account: CompleteAddress): Promise<void> {
Expand Down
49 changes: 39 additions & 10 deletions yarn-project/end-to-end/src/benchmarks/bench_publish_rollup.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
/* eslint-disable camelcase */
import { AztecNodeService } from '@aztec/aztec-node';
import { AztecAddress, BatchCall } from '@aztec/aztec.js';
import { EthAddress, Fr, GrumpkinScalar } from '@aztec/circuits.js';
import { retryUntil } from '@aztec/foundation/retry';
import { sleep } from '@aztec/foundation/sleep';
import { BenchmarkingContract } from '@aztec/noir-contracts/types';
import { createPXEService } from '@aztec/pxe';
import { SequencerClient } from '@aztec/sequencer-client';

import times from 'lodash.times';
Expand All @@ -21,44 +24,70 @@ describe('benchmarks/publish_rollup', () => {
context = await setup(2, { maxTxsPerBlock: 1024 });
[owner] = context.accounts.map(a => a.address);
contract = await BenchmarkingContract.deploy(context.wallet).send().deployed();
context.logger(`Deployed benchmarking contract at ${contract.address}`);
sequencer = (context.aztecNode as AztecNodeService).getSequencer()!;
await sequencer.stop();
}, 60_000);

// Each tx has a private execution (account entrypoint), a nested private call (create_note),
// a public call (increment_balance), and a nested public call (broadcast). These include
// emitting one private note and one unencrypted log, two storage reads and one write.
const makeBatchCall = (i: number) =>
new BatchCall(context.wallet, [
contract.methods.create_note(owner, i).request(),
contract.methods.increment_balance(owner, i).request(),
contract.methods.create_note(owner, i + 1).request(),
contract.methods.increment_balance(owner, i + 1).request(),
]);

it.each(ROLLUP_SIZES)(
`publishes a rollup with %d txs`,
async (txCount: number) => {
context.logger(`Assembling rollup with ${txCount} txs`);
// Simulate and simultaneously send %d txs. These should not yet be processed since sequencer is stopped.
// Each tx has a private execution (account entrypoint), a nested private call (create_note),
// a public call (increment_balance), and a nested public call (broadcast). These include
// emitting one private note and one unencrypted log, two storage reads and one write.
// Simulate and simultaneously send ROLLUP_SIZE txs. These should not yet be processed since sequencer is stopped.
const calls = times(txCount, makeBatchCall);
calls.forEach(call => call.simulate({ skipPublicSimulation: true }));
const sentTxs = calls.map(call => call.send());

// Awaiting txHash waits until the aztec node has received the tx into its p2p pool
await Promise.all(sentTxs.map(tx => tx.getTxHash()));
// And then wait a bit more just in case
await sleep(100);

// Restart sequencer to process all txs together
sequencer.restart();
// Wait for the last tx to be processed and finish the current node
await sentTxs[sentTxs.length - 1].wait({ timeout: 600_00 });

// Wait for the last tx to be processed and stop the current node
const { blockNumber } = await sentTxs[sentTxs.length - 1].wait({ timeout: 5 * 60_000 });
await context.teardown();

// Create a new aztec node to measure sync time of the block
// and call getTreeRoots to force a sync with world state to ensure the node has caught up
context.logger(`Starting new aztec node`);
const node = await AztecNodeService.createAndSync({ ...context.config, disableSequencer: true });
// Force a sync with world state to ensure new node has caught up before killing it
await node.getTreeRoots();

// Spin up a new pxe and sync it, we'll use it to test sync times of new accounts for the last block
context.logger(`Starting new pxe`);
const pxe = await createPXEService(node, { l2BlockPollingIntervalMS: 100, l2StartingBlock: blockNumber! - 1 });
await pxe.addContracts([{ ...contract, portalContract: EthAddress.ZERO }]);
await retryUntil(() => pxe.isGlobalStateSynchronized(), 'pxe-global-sync');
const { publicKey, partialAddress } = context.wallet.getCompleteAddress();
const privateKey = context.wallet.getEncryptionPrivateKey();
const l2Block = await node.getBlockNumber();

// Register the owner account and wait until it's synced so we measure how much time it took
context.logger(`Registering owner account on new pxe`);
await pxe.registerAccount(privateKey, partialAddress);
const isOwnerSynced = async () => (await pxe.getSyncStatus()).notes[publicKey.toString()] === l2Block;
await retryUntil(isOwnerSynced, 'pxe-owner-sync');

// Repeat for another account that didn't receive any notes for them, so we measure trial-decrypts
context.logger(`Registering fresh account on new pxe`);
const newAccount = await pxe.registerAccount(GrumpkinScalar.random(), Fr.random());
const isNewAccountSynced = async () =>
(await pxe.getSyncStatus()).notes[newAccount.publicKey.toString()] === l2Block;
await retryUntil(isNewAccountSynced, 'pxe-new-account-sync');

// Stop the external node and pxe
await pxe.stop();
await node.stop();
},
10 * 60_000,
Expand Down
8 changes: 4 additions & 4 deletions yarn-project/end-to-end/src/fixtures/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AztecNodeConfig, AztecNodeService, getConfigEnvVars } from '@aztec/aztec-node';
import {
AccountWallet,
AccountWalletWithPrivateKey,
AztecAddress,
CheatCodes,
CompleteAddress,
Expand Down Expand Up @@ -156,7 +156,7 @@ export async function setupPXEService(
/**
* The wallets to be used.
*/
wallets: AccountWallet[];
wallets: AccountWalletWithPrivateKey[];
/**
* Logger instance named as the current test.
*/
Expand Down Expand Up @@ -242,9 +242,9 @@ export type EndToEndContext = {
/** The Aztec Node configuration. */
config: AztecNodeConfig;
/** The first wallet to be used. */
wallet: AccountWallet;
wallet: AccountWalletWithPrivateKey;
/** The wallets to be used. */
wallets: AccountWallet[];
wallets: AccountWalletWithPrivateKey[];
/** Logger instance named as the current test. */
logger: DebugLogger;
/** The cheat codes. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ contract Benchmarking {

use dep::aztec::{
context::{Context},
note::note_getter_options::NoteGetterOptions,
note::{utils as note_utils, note_getter_options::NoteGetterOptions, note_header::NoteHeader},
selector::compute_selector,
log::emit_unencrypted_log,
state_vars::{map::Map, public_state::PublicState, set::Set},
Expand Down Expand Up @@ -71,4 +71,9 @@ contract Benchmarking {
fn broadcast(owner: Field) {
emit_unencrypted_log(&mut context, storage.balances.at(owner).read());
}

unconstrained fn compute_note_hash_and_nullifier(contract_address: Field, nonce: Field, storage_slot: Field, preimage: [Field; VALUE_NOTE_LEN]) -> [Field; 4] {
let note_header = NoteHeader::new(contract_address, nonce, storage_slot);
note_utils::compute_note_hash_and_nullifier(ValueNoteMethods, note_header, preimage)
}
}
11 changes: 7 additions & 4 deletions yarn-project/pxe/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { INITIAL_L2_BLOCK_NUM } from '@aztec/types';

import { readFileSync } from 'fs';
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
Expand All @@ -6,20 +8,21 @@ import { fileURLToPath } from 'url';
* Configuration settings for the PXE Service.
*/
export interface PXEServiceConfig {
/**
* The interval to wait between polling for new blocks.
*/
/** The interval to wait between polling for new blocks. */
l2BlockPollingIntervalMS: number;
/** L2 block to start scanning from */
l2StartingBlock: number;
}

/**
* Creates an instance of PXEServiceConfig out of environment variables using sensible defaults for integration testing if not set.
*/
export function getPXEServiceConfig(): PXEServiceConfig {
const { PXE_BLOCK_POLLING_INTERVAL_MS } = process.env;
const { PXE_BLOCK_POLLING_INTERVAL_MS, PXE_L2_STARTING_BLOCK } = process.env;

return {
l2BlockPollingIntervalMS: PXE_BLOCK_POLLING_INTERVAL_MS ? +PXE_BLOCK_POLLING_INTERVAL_MS : 1000,
l2StartingBlock: PXE_L2_STARTING_BLOCK ? +PXE_L2_STARTING_BLOCK : INITIAL_L2_BLOCK_NUM,
};
}

Expand Down
10 changes: 9 additions & 1 deletion yarn-project/pxe/src/note_processor/note_processor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ConstantKeyPair } from '@aztec/key-store';
import {
AztecNode,
FunctionL2Logs,
INITIAL_L2_BLOCK_NUM,
KeyPair,
KeyStore,
L2Block,
Expand Down Expand Up @@ -125,7 +126,14 @@ describe('Note Processor', () => {
keyStore = mock<KeyStore>();
simulator = mock<AcirSimulator>();
keyStore.getAccountPrivateKey.mockResolvedValue(owner.getPrivateKey());
noteProcessor = new NoteProcessor(owner.getPublicKey(), keyStore, database, aztecNode, simulator);
noteProcessor = new NoteProcessor(
owner.getPublicKey(),
keyStore,
database,
aztecNode,
INITIAL_L2_BLOCK_NUM,
simulator,
);

simulator.computeNoteHashAndNullifier.mockImplementation((...args) =>
Promise.resolve({
Expand Down
Loading

0 comments on commit 33a230a

Please sign in to comment.