diff --git a/yarn-project/bootstrap.sh b/yarn-project/bootstrap.sh index a7992576d9e5..fdf24a891720 100755 --- a/yarn-project/bootstrap.sh +++ b/yarn-project/bootstrap.sh @@ -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 { diff --git a/yarn-project/prover-client/src/light/lightweight_checkpoint_builder.bench.test.ts b/yarn-project/prover-client/src/light/lightweight_checkpoint_builder.bench.test.ts new file mode 100644 index 000000000000..3c4694db53cf --- /dev/null +++ b/yarn-project/prover-client/src/light/lightweight_checkpoint_builder.bench.test.ts @@ -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 => { + 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 => { + 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; + + 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(); + }); + }); +}); diff --git a/yarn-project/prover-client/src/light/lightweight_checkpoint_builder.test.ts b/yarn-project/prover-client/src/light/lightweight_checkpoint_builder.test.ts index 8c791e955acc..0777f473ab69 100644 --- a/yarn-project/prover-client/src/light/lightweight_checkpoint_builder.test.ts +++ b/yarn-project/prover-client/src/light/lightweight_checkpoint_builder.test.ts @@ -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); @@ -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, }); @@ -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, }); @@ -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, }); diff --git a/yarn-project/prover-client/src/light/lightweight_checkpoint_builder.ts b/yarn-project/prover-client/src/light/lightweight_checkpoint_builder.ts index cb789075c3f8..ab32be72936d 100644 --- a/yarn-project/prover-client/src/light/lightweight_checkpoint_builder.ts +++ b/yarn-project/prover-client/src/light/lightweight_checkpoint_builder.ts @@ -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'; @@ -161,7 +162,8 @@ export class LightweightCheckpointBuilder { globalVariables: GlobalVariables, txs: ProcessedTx[], opts: { insertTxsEffects?: boolean; expectedEndState?: StateReference } = {}, - ): Promise { + ): Promise<{ block: L2Block; timings: Record }> { + const timings: Record = {}; const isFirstBlock = this.blocks.length === 0; // Empty blocks are only allowed as the first block in a checkpoint @@ -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)!; @@ -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(), @@ -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()}`, { @@ -225,7 +232,7 @@ export class LightweightCheckpointBuilder { txs: block.body.txEffects.map(tx => tx.txHash.toString()), }); - return block; + return { block, timings }; } async completeCheckpoint(): Promise { diff --git a/yarn-project/prover-client/src/mocks/test_context.ts b/yarn-project/prover-client/src/mocks/test_context.ts index b27adcec0d97..8efb5d8b7e1c 100644 --- a/yarn-project/prover-client/src/mocks/test_context.ts +++ b/yarn-project/prover-client/src/mocks/test_context.ts @@ -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, }); diff --git a/yarn-project/validator-client/src/checkpoint_builder.test.ts b/yarn-project/validator-client/src/checkpoint_builder.test.ts index 38945d92aa4e..abf782d6b8ea 100644 --- a/yarn-project/validator-client/src/checkpoint_builder.test.ts +++ b/yarn-project/validator-client/src/checkpoint_builder.test.ts @@ -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], @@ -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([ diff --git a/yarn-project/validator-client/src/checkpoint_builder.ts b/yarn-project/validator-client/src/checkpoint_builder.ts index 7805f431610b..70c053db6d7a 100644 --- a/yarn-project/validator-client/src/checkpoint_builder.ts +++ b/yarn-project/validator-client/src/checkpoint_builder.ts @@ -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, });