Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions yarn-project/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ function bench_cmds {
echo "$hash BENCH_OUTPUT=bench-out/tx.bench.json yarn-project/scripts/run_test.sh stdlib/src/tx/tx_bench.test.ts"
echo "$hash:ISOLATE=1:CPUS=10:MEM=16g:LOG_LEVEL=silent BENCH_OUTPUT=bench-out/proving_broker.bench.json yarn-project/scripts/run_test.sh prover-client/src/test/proving_broker_testbench.test.ts"
echo "$hash:ISOLATE=1:CPUS=16:MEM=16g BENCH_OUTPUT=bench-out/avm_bulk_test.bench.json yarn-project/scripts/run_test.sh bb-prover/src/avm_proving_tests/avm_bulk.test.ts"
echo "$hash BENCH_OUTPUT=bench-out/lightweight_checkpoint_builder.bench.json yarn-project/scripts/run_test.sh prover-client/src/light/lightweight_checkpoint_builder.bench.test.ts"
}

function release_packages {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { CONTRACT_CLASS_LOG_SIZE_IN_FIELDS } from '@aztec/constants';
import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
import { timesAsync } from '@aztec/foundation/collection';
import { Fr } from '@aztec/foundation/curves/bn254';
import { createLogger } from '@aztec/foundation/log';
import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree';
import { ProtocolContractsList } from '@aztec/protocol-contracts';
import { computeFeePayerBalanceLeafSlot } from '@aztec/protocol-contracts/fee-juice';
import { PublicDataWrite } from '@aztec/stdlib/avm';
import { AztecAddress } from '@aztec/stdlib/aztec-address';
import { EthAddress } from '@aztec/stdlib/block';
import { GasFees } from '@aztec/stdlib/gas';
import { ContractClassLog, ContractClassLogFields } from '@aztec/stdlib/logs';
import { mockProcessedTx } from '@aztec/stdlib/testing';
import { PublicDataTreeLeaf } from '@aztec/stdlib/trees';
import type { CheckpointGlobalVariables, ProcessedTx } from '@aztec/stdlib/tx';
import { GlobalVariables } from '@aztec/stdlib/tx';
import { NativeWorldStateService } from '@aztec/world-state/native';

import { afterAll, afterEach, beforeEach, describe, it, jest } from '@jest/globals';
import { mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';

import { LightweightCheckpointBuilder } from './lightweight_checkpoint_builder.js';

jest.setTimeout(300_000);

const logger = createLogger('bench:lightweight-checkpoint-builder');

describe('LightweightCheckpointBuilder benchmarks', () => {
let worldState: NativeWorldStateService;
let feePayer: AztecAddress;
let feePayerBalance: Fr;

const results: { name: string; value: number; unit: string }[] = [];

const toPrettyString = () =>
results.map(({ name, value, unit }) => `${name}: ${value.toFixed(2)} ${unit}`).join('\n');

const toGithubActionBenchmarkJSON = (indent = 2) => JSON.stringify(results, null, indent);

beforeEach(async () => {
feePayer = AztecAddress.fromNumber(42222);
feePayerBalance = new Fr(10n ** 20n);
const feePayerSlot = await computeFeePayerBalanceLeafSlot(feePayer);
const prefilledPublicData = [new PublicDataTreeLeaf(feePayerSlot, feePayerBalance)];
worldState = await NativeWorldStateService.tmp(undefined, true, prefilledPublicData);
});

afterEach(async () => {
await worldState.close();
});

afterAll(async () => {
if (process.env.BENCH_OUTPUT) {
await mkdir(path.dirname(process.env.BENCH_OUTPUT), { recursive: true });
await writeFile(process.env.BENCH_OUTPUT, toGithubActionBenchmarkJSON());
} else {
logger.info(`\n${toPrettyString()}\n`);
}
});

const makeCheckpointConstants = (slotNumber: SlotNumber): CheckpointGlobalVariables => ({
chainId: Fr.ZERO,
version: Fr.ZERO,
slotNumber,
timestamp: BigInt(slotNumber) * 123n,
coinbase: EthAddress.ZERO,
feeRecipient: AztecAddress.ZERO,
gasFees: GasFees.empty(),
});

const makeGlobalVariables = (blockNumber: BlockNumber, slotNumber: SlotNumber): GlobalVariables =>
GlobalVariables.from({
chainId: Fr.ZERO,
version: Fr.ZERO,
blockNumber,
slotNumber,
timestamp: BigInt(blockNumber) * 123n,
coinbase: EthAddress.ZERO,
feeRecipient: AztecAddress.ZERO,
gasFees: GasFees.empty(),
});

const makeProcessedTx = async (globalVariables: GlobalVariables, seed: number): Promise<ProcessedTx> => {
const tx = await mockProcessedTx({
seed,
globalVariables,
vkTreeRoot: getVKTreeRoot(),
protocolContracts: ProtocolContractsList,
feePayer,
});

feePayerBalance = new Fr(feePayerBalance.toBigInt() - tx.txEffect.transactionFee.toBigInt());
const feePayerSlot = await computeFeePayerBalanceLeafSlot(feePayer);
const feePaymentPublicDataWrite = new PublicDataWrite(feePayerSlot, feePayerBalance);
tx.txEffect.publicDataWrites[0] = feePaymentPublicDataWrite;
if (tx.avmProvingRequest) {
tx.avmProvingRequest.inputs.publicInputs.accumulatedData.publicDataWrites[0] = feePaymentPublicDataWrite;
}

return tx;
};

/** Creates a tx with no side effects but a full contract class log. */
const makeLogsHeavyProcessedTx = async (globalVariables: GlobalVariables, seed: number): Promise<ProcessedTx> => {
const tx = await makeProcessedTx(globalVariables, seed);

// Strip side effects: keep only the tx hash nullifier and fee payment public data write.
tx.txEffect.noteHashes = [];
tx.txEffect.nullifiers = [tx.txEffect.nullifiers[0]];
tx.txEffect.l2ToL1Msgs = [];
tx.txEffect.privateLogs = [];
tx.txEffect.publicDataWrites = [tx.txEffect.publicDataWrites[0]];

// Add a full contract class log (CONTRACT_CLASS_LOG_SIZE_IN_FIELDS = 3,023 blob fields).
tx.txEffect.contractClassLogs = [
new ContractClassLog(
AztecAddress.fromNumber(seed),
ContractClassLogFields.random(CONTRACT_CLASS_LOG_SIZE_IN_FIELDS),
CONTRACT_CLASS_LOG_SIZE_IN_FIELDS,
),
];

return tx;
};

type TxFactory = (globalVariables: GlobalVariables, seed: number) => Promise<ProcessedTx>;

const testCases: { label: string; numTxs: number; makeTx: TxFactory }[] = [
{ label: 'worst-case', numTxs: 4, makeTx: makeProcessedTx },
{ label: 'worst-case', numTxs: 8, makeTx: makeProcessedTx },
{ label: 'worst-case', numTxs: 16, makeTx: makeProcessedTx },
{ label: 'class-log-heavy', numTxs: 4, makeTx: makeLogsHeavyProcessedTx },
{ label: 'class-log-heavy', numTxs: 8, makeTx: makeLogsHeavyProcessedTx },
];

describe('addBlock breakdown', () => {
it.each(testCases)('$label $numTxs txs', async ({ label, numTxs, makeTx }) => {
const slotNumber = SlotNumber(15);
const blockNumber = BlockNumber(1);
const constants = makeCheckpointConstants(slotNumber);
const fork = await worldState.fork();

const builder = await LightweightCheckpointBuilder.startNewCheckpoint(
CheckpointNumber(1),
constants,
[],
[],
fork,
);

const globalVariables = makeGlobalVariables(blockNumber, slotNumber);
const txs = await timesAsync(numTxs, i => makeTx(globalVariables, 5000 + i));

const { timings } = await builder.addBlock(globalVariables, txs, { insertTxsEffects: true });

const prefix = `addBlock/${label}/${numTxs} txs`;
for (const [step, ms] of Object.entries(timings)) {
results.push({ name: `${prefix}/${step}`, value: ms, unit: 'ms' });
}
const total = Object.values(timings).reduce((a, b) => a + b, 0);
results.push({ name: `${prefix}/total`, value: total, unit: 'ms' });

await fork.close();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ describe('LightweightCheckpointBuilder', () => {

// Build empty block
const globalVariables = makeGlobalVariables(blockNumber, slotNumber);
const block = await checkpointBuilder.addBlock(globalVariables, [], { insertTxsEffects: true });
const { block } = await checkpointBuilder.addBlock(globalVariables, [], { insertTxsEffects: true });

expect(block.header.globalVariables.blockNumber).toEqual(blockNumber);

Expand Down Expand Up @@ -155,7 +155,7 @@ describe('LightweightCheckpointBuilder', () => {
tx.txEffect.l2ToL1Msgs.push(...msgs);

// Build block with tx - insertTxsEffects will handle inserting side effects
const block = await checkpointBuilder.addBlock(globalVariables, [tx], {
const { block } = await checkpointBuilder.addBlock(globalVariables, [tx], {
insertTxsEffects: true,
});

Expand Down Expand Up @@ -202,7 +202,7 @@ describe('LightweightCheckpointBuilder', () => {
const txs = await timesAsync(3, i => makeProcessedTx(globalVariables, 1000 + i));

// Build block with txs - insertTxsEffects will handle inserting side effects
const block = await checkpointBuilder.addBlock(globalVariables, txs, {
const { block } = await checkpointBuilder.addBlock(globalVariables, txs, {
insertTxsEffects: true,
});

Expand Down Expand Up @@ -248,7 +248,7 @@ describe('LightweightCheckpointBuilder', () => {
const txs = await timesAsync(txsPerBlock, j => makeProcessedTx(globalVariables, 2000 + i * 10 + j));

// Build block - insertTxsEffects will handle inserting side effects
const block = await checkpointBuilder.addBlock(globalVariables, txs, {
const { block } = await checkpointBuilder.addBlock(globalVariables, txs, {
insertTxsEffects: true,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { type CheckpointNumber, IndexWithinCheckpoint } from '@aztec/foundation/
import { padArrayEnd } from '@aztec/foundation/collection';
import { Fr } from '@aztec/foundation/curves/bn254';
import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
import { elapsed } from '@aztec/foundation/timer';
import { L2Block } from '@aztec/stdlib/block';
import { Checkpoint } from '@aztec/stdlib/checkpoint';
import type { MerkleTreeWriteOperations } from '@aztec/stdlib/interfaces/server';
Expand Down Expand Up @@ -161,7 +162,8 @@ export class LightweightCheckpointBuilder {
globalVariables: GlobalVariables,
txs: ProcessedTx[],
opts: { insertTxsEffects?: boolean; expectedEndState?: StateReference } = {},
): Promise<L2Block> {
): Promise<{ block: L2Block; timings: Record<string, number> }> {
const timings: Record<string, number> = {};
const isFirstBlock = this.blocks.length === 0;

// Empty blocks are only allowed as the first block in a checkpoint
Expand All @@ -170,7 +172,9 @@ export class LightweightCheckpointBuilder {
}

if (isFirstBlock) {
this.lastArchives.push(await getTreeSnapshot(MerkleTreeId.ARCHIVE, this.db));
const [msGetInitialArchive, initialArchive] = await elapsed(() => getTreeSnapshot(MerkleTreeId.ARCHIVE, this.db));
this.lastArchives.push(initialArchive);
timings.getInitialArchive = msGetInitialArchive;
}

const lastArchive = this.lastArchives.at(-1)!;
Expand All @@ -180,12 +184,17 @@ export class LightweightCheckpointBuilder {
`Inserting side effects for ${txs.length} txs for block ${globalVariables.blockNumber} into db`,
{ txs: txs.map(tx => tx.hash.toString()) },
);
let msInsertSideEffects = 0;
for (const tx of txs) {
await insertSideEffects(tx, this.db);
const [ms] = await elapsed(() => insertSideEffects(tx, this.db));
msInsertSideEffects += ms;
}
timings.insertSideEffects = msInsertSideEffects;
}

const endState = await this.db.getStateReference();
const [msGetEndState, endState] = await elapsed(() => this.db.getStateReference());
timings.getEndState = msGetEndState;

if (opts.expectedEndState && !endState.equals(opts.expectedEndState)) {
this.logger.error('End state after processing txs does not match expected end state', {
globalVariables: globalVariables.toInspect(),
Expand All @@ -195,26 +204,24 @@ export class LightweightCheckpointBuilder {
throw new Error(`End state does not match expected end state when building block ${globalVariables.blockNumber}`);
}

const { header, body, blockBlobFields } = await buildHeaderAndBodyFromTxs(
txs,
lastArchive,
endState,
globalVariables,
this.spongeBlob,
isFirstBlock,
const [msBuildHeaderAndBody, { header, body, blockBlobFields }] = await elapsed(() =>
buildHeaderAndBodyFromTxs(txs, lastArchive, endState, globalVariables, this.spongeBlob, isFirstBlock),
);
timings.buildHeaderAndBody = msBuildHeaderAndBody;

header.state.validate();

await this.db.updateArchive(header);
const newArchive = await getTreeSnapshot(MerkleTreeId.ARCHIVE, this.db);
const [msUpdateArchive, newArchive] = await elapsed(() => getTreeSnapshot(MerkleTreeId.ARCHIVE, this.db));
timings.updateArchive = msUpdateArchive;
this.lastArchives.push(newArchive);

const indexWithinCheckpoint = IndexWithinCheckpoint(this.blocks.length);
const block = new L2Block(newArchive, header, body, this.checkpointNumber, indexWithinCheckpoint);
this.blocks.push(block);

await this.spongeBlob.absorb(blockBlobFields);
const [msSpongeAbsorb] = await elapsed(() => this.spongeBlob.absorb(blockBlobFields));
timings.spongeAbsorb = msSpongeAbsorb;
this.blobFields.push(...blockBlobFields);

this.logger.debug(`Built block ${header.getBlockNumber()}`, {
Expand All @@ -225,7 +232,7 @@ export class LightweightCheckpointBuilder {
txs: block.body.txEffects.map(tx => tx.txHash.toString()),
});

return block;
return { block, timings };
}

async completeCheckpoint(): Promise<Checkpoint> {
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/prover-client/src/mocks/test_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ export class TestContext {
const txs = blockTxs[i];
const state = blockEndStates[i];

const block = await builder.addBlock(blockGlobalVariables[i], txs, {
const { block } = await builder.addBlock(blockGlobalVariables[i], txs, {
expectedEndState: state,
insertTxsEffects: true,
});
Expand Down
4 changes: 2 additions & 2 deletions yarn-project/validator-client/src/checkpoint_builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ describe('CheckpointBuilder', () => {
lightweightCheckpointBuilder.getBlockCount.mockReturnValue(0);

const expectedBlock = await L2Block.random(blockNumber);
lightweightCheckpointBuilder.addBlock.mockResolvedValue(expectedBlock);
lightweightCheckpointBuilder.addBlock.mockResolvedValue({ block: expectedBlock, timings: {} });

processor.process.mockResolvedValue([
[{ hash: Fr.random(), gasUsed: { publicGas: Gas.empty() } } as unknown as ProcessedTx],
Expand All @@ -110,7 +110,7 @@ describe('CheckpointBuilder', () => {
lightweightCheckpointBuilder.getBlockCount.mockReturnValue(0);

const expectedBlock = await L2Block.random(blockNumber, { txsPerBlock: 0 });
lightweightCheckpointBuilder.addBlock.mockResolvedValue(expectedBlock);
lightweightCheckpointBuilder.addBlock.mockResolvedValue({ block: expectedBlock, timings: {} });

// No transactions processed
processor.process.mockResolvedValue([
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/validator-client/src/checkpoint_builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
}

// Add block to checkpoint
const block = await this.checkpointBuilder.addBlock(globalVariables, processedTxs, {
const { block } = await this.checkpointBuilder.addBlock(globalVariables, processedTxs, {
expectedEndState: opts.expectedEndState,
});

Expand Down
Loading