diff --git a/yarn-project/aztec-node/src/aztec-node/server.test.ts b/yarn-project/aztec-node/src/aztec-node/server.test.ts index 37f3adca164f..2419072d7bce 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.test.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.test.ts @@ -1,4 +1,3 @@ -import { TestCircuitVerifier } from '@aztec/bb-prover'; import { EpochCache } from '@aztec/epoch-cache'; import type { RollupContract } from '@aztec/ethereum/contracts'; import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types'; @@ -198,7 +197,8 @@ describe('aztec node', () => { globalVariablesBuilder, epochCache, getPackageVersion() ?? '', - new TestCircuitVerifier(), + undefined, + undefined, ); }); @@ -715,7 +715,8 @@ describe('aztec node', () => { globalVariablesBuilder, epochCache, getPackageVersion() ?? '', - new TestCircuitVerifier(), + undefined, + undefined, undefined, undefined, undefined, @@ -903,7 +904,8 @@ describe('aztec node', () => { globalVariablesBuilder, epochCache, getPackageVersion() ?? '', - new TestCircuitVerifier(), + undefined, + undefined, undefined, undefined, undefined, diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 92dfc102a387..b268b5b5dc9b 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -1,5 +1,6 @@ import { Archiver, createArchiver } from '@aztec/archiver'; -import { BBCircuitVerifier, QueuedIVCVerifier, TestCircuitVerifier } from '@aztec/bb-prover'; +import { BBCircuitVerifier, BatchChonkVerifier, QueuedIVCVerifier } from '@aztec/bb-prover'; +import { TestCircuitVerifier } from '@aztec/bb-prover/test'; import { type BlobClientInterface, createBlobClientWithFileStores } from '@aztec/blob-client/client'; import { Blob } from '@aztec/blob-lib'; import { ARCHIVE_HEIGHT, type L1_TO_L2_MSG_TREE_HEIGHT, type NOTE_HASH_TREE_HEIGHT } from '@aztec/constants'; @@ -150,7 +151,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { protected readonly globalVariableBuilder: GlobalVariableBuilderInterface, protected readonly epochCache: EpochCacheInterface, protected readonly packageVersion: string, - private proofVerifier: ClientProtocolCircuitVerifier, + private peerChonkVerifier: ClientProtocolCircuitVerifier | undefined, + private rpcChonkVerifier: ClientProtocolCircuitVerifier | undefined, private telemetry: TelemetryClient = getTelemetryClient(), private log = createLogger('node'), private blobClient?: BlobClientInterface, @@ -172,6 +174,11 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { } } + /** @internal Exposed for testing — returns the RPC proof verifier, if any. */ + public getProofVerifier(): ClientProtocolCircuitVerifier | undefined { + return this.rpcChonkVerifier; + } + public async getWorldStateSyncStatus(): Promise { const status = await this.worldStateSynchronizer.status(); return status.syncSummary; @@ -306,10 +313,17 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { options.prefilledPublicData, telemetry, ); - const circuitVerifier = - config.realProofs || config.debugForceTxProofVerification - ? await BBCircuitVerifier.new(config) - : new TestCircuitVerifier(config.proverTestVerificationDelayMs); + const useRealVerifiers = config.realProofs || config.debugForceTxProofVerification; + let peerChonkVerifier: ClientProtocolCircuitVerifier | undefined; + let rpcChonkVerifier: ClientProtocolCircuitVerifier | undefined; + if (useRealVerifiers) { + peerChonkVerifier = await BatchChonkVerifier.new(config, config.bbPeerVerifyBatchSize, 'peer'); + const rpcVerifier = await BBCircuitVerifier.new(config); + rpcChonkVerifier = new QueuedIVCVerifier(rpcVerifier, config.bbRpcVerifyConcurrency); + } else { + peerChonkVerifier = new TestCircuitVerifier(config.proverTestVerificationDelayMs); + rpcChonkVerifier = new TestCircuitVerifier(config.proverTestVerificationDelayMs); + } let debugLogStore: DebugLogStore; if (!config.realProofs) { @@ -323,8 +337,6 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { debugLogStore = new NullDebugLogStore(); } - const proofVerifier = new QueuedIVCVerifier(config, circuitVerifier); - const proverOnly = config.enableProverNode && config.disableValidator; if (proverOnly) { log.info('Starting in prover-only mode: skipping validator, sequencer, sentinel, and slasher subsystems'); @@ -334,7 +346,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { const p2pClient = await createP2PClient( config, archiver, - proofVerifier, + peerChonkVerifier, worldStateSynchronizer, epochCache, packageVersion, @@ -575,7 +587,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { globalVariableBuilder, epochCache, packageVersion, - proofVerifier, + peerChonkVerifier, + rpcChonkVerifier, telemetry, log, blobClient, @@ -922,7 +935,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { await tryStop(this.validatorsSentinel); await tryStop(this.epochPruneWatcher); await tryStop(this.slasherClient); - await tryStop(this.proofVerifier); + await Promise.all([tryStop(this.peerChonkVerifier), tryStop(this.rpcChonkVerifier)]); await tryStop(this.sequencer); await tryStop(this.proverNode); await tryStop(this.p2pClient); @@ -1046,11 +1059,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { referenceBlock: BlockParameter, blockHash: BlockHash, ): Promise | undefined> { - // The Noir circuit checks the archive membership proof against `anchor_block_header.last_archive.root`, - // which is the archive tree root BEFORE the anchor block was added (i.e. the state after block N-1). - // So we need the world state at block N-1, not block N, to produce a sibling path matching that root. - const referenceBlockNumber = await this.resolveBlockNumber(referenceBlock); - const committedDb = await this.getWorldState(BlockNumber(referenceBlockNumber - 1)); + const committedDb = await this.getWorldState(referenceBlock); const [pathAndIndex] = await committedDb.findSiblingPaths(MerkleTreeId.ARCHIVE, [blockHash]); return pathAndIndex === undefined ? undefined @@ -1312,7 +1321,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { { isSimulation, skipFeeEnforcement }: { isSimulation?: boolean; skipFeeEnforcement?: boolean } = {}, ): Promise { const db = this.worldStateSynchronizer.getCommitted(); - const verifier = isSimulation ? undefined : this.proofVerifier; + const verifier = isSimulation ? undefined : this.rpcChonkVerifier; // We accept transactions if they are not expired by the next slot (checked based on the ExpirationTimestamp field) const { ts: nextSlotTimestamp } = this.epochCache.getEpochAndSlotInNextL1Slot(); @@ -1361,7 +1370,15 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { archiver.updateConfig(config); } if (newConfig.realProofs !== this.config.realProofs) { - this.proofVerifier = config.realProofs ? await BBCircuitVerifier.new(newConfig) : new TestCircuitVerifier(); + await Promise.all([tryStop(this.peerChonkVerifier), tryStop(this.rpcChonkVerifier)]); + if (newConfig.realProofs) { + this.peerChonkVerifier = await BatchChonkVerifier.new(newConfig, newConfig.bbPeerVerifyBatchSize, 'peer'); + const rpcVerifier = await BBCircuitVerifier.new(newConfig); + this.rpcChonkVerifier = new QueuedIVCVerifier(rpcVerifier, newConfig.bbRpcVerifyConcurrency); + } else { + this.peerChonkVerifier = new TestCircuitVerifier(); + this.rpcChonkVerifier = new TestCircuitVerifier(); + } } this.config = newConfig; @@ -1659,25 +1676,6 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { return snapshot; } - /** Resolves a block parameter to a block number. */ - protected async resolveBlockNumber(block: BlockParameter): Promise { - if (block === 'latest') { - return BlockNumber(await this.blockSource.getBlockNumber()); - } - if (BlockHash.isBlockHash(block)) { - const initialBlockHash = await this.#getInitialHeaderHash(); - if (block.equals(initialBlockHash)) { - return BlockNumber.ZERO; - } - const header = await this.blockSource.getBlockHeaderByHash(block); - if (!header) { - throw new Error(`Block hash ${block.toString()} not found.`); - } - return header.getBlockNumber(); - } - return block as BlockNumber; - } - /** * Ensure we fully sync the world state * @returns A promise that fulfils once the world state is synced diff --git a/yarn-project/bb-prover/package.json b/yarn-project/bb-prover/package.json index 04774fdf6ea8..5e08beeba10c 100644 --- a/yarn-project/bb-prover/package.json +++ b/yarn-project/bb-prover/package.json @@ -80,6 +80,7 @@ "@aztec/telemetry-client": "workspace:^", "@aztec/world-state": "workspace:^", "commander": "^12.1.0", + "msgpackr": "^1.11.2", "pako": "^2.1.0", "source-map-support": "^0.5.21", "tslib": "^2.4.0" diff --git a/yarn-project/bb-prover/src/config.ts b/yarn-project/bb-prover/src/config.ts index 60a33c9a67b6..96fc89e6e7bb 100644 --- a/yarn-project/bb-prover/src/config.ts +++ b/yarn-project/bb-prover/src/config.ts @@ -3,8 +3,11 @@ export interface BBConfig { bbWorkingDirectory: string; /** Whether to skip tmp dir cleanup for debugging purposes */ bbSkipCleanup: boolean; - numConcurrentIVCVerifiers: number; bbIVCConcurrency: number; + /** Max concurrent verifications for the RPC verifier (QueuedIVCVerifier concurrency). Defaults to BB_NUM_IVC_VERIFIERS or 8. */ + bbRpcVerifyConcurrency: number; + /** Max batch size for the peer chonk verifier (BatchChonkVerifier batch size). Defaults to BB_NUM_IVC_VERIFIERS or 8. */ + bbPeerVerifyBatchSize: number; } export interface ACVMConfig { diff --git a/yarn-project/bb-prover/src/verifier/batch_chonk_verifier.ts b/yarn-project/bb-prover/src/verifier/batch_chonk_verifier.ts new file mode 100644 index 000000000000..06ae6c8735d3 --- /dev/null +++ b/yarn-project/bb-prover/src/verifier/batch_chonk_verifier.ts @@ -0,0 +1,271 @@ +import { BackendType, Barretenberg } from '@aztec/bb.js'; +import { FifoFrameReader } from '@aztec/foundation/fifo'; +import { createLogger } from '@aztec/foundation/log'; +import { SerialQueue } from '@aztec/foundation/queue'; +import { Timer } from '@aztec/foundation/timer'; +import { ProtocolCircuitVks } from '@aztec/noir-protocol-circuits-types/server/vks'; +import type { ClientProtocolCircuitVerifier, IVCProofVerificationResult } from '@aztec/stdlib/interfaces/server'; +import type { Tx } from '@aztec/stdlib/tx'; + +import { Unpackr } from 'msgpackr'; +import { execFile } from 'node:child_process'; +import { unlinkSync } from 'node:fs'; +import { unlink } from 'node:fs/promises'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { promisify } from 'node:util'; + +import type { BBConfig } from '../config.js'; + +const execFileAsync = promisify(execFile); + +/** Result from the FIFO, matching the C++ VerifyResult struct. */ +interface FifoVerifyResult { + request_id: number; + status: number; + error_message: string; + time_in_verify_ms: number; +} + +/** Maps client protocol artifacts used for chonk verification to VK indices. */ +const CHONK_VK_ARTIFACTS = ['HidingKernelToRollup', 'HidingKernelToPublic'] as const; + +interface PendingRequest { + resolve: (result: IVCProofVerificationResult) => void; + reject: (error: Error) => void; + totalTimer: Timer; +} + +/** + * Batch verifier for Chonk IVC proofs. Uses the bb batch verifier service + * which batches IPA verification into a single SRS MSM for better throughput. + * + * Architecture: + * - Spawns a persistent `bb msgpack run` process via Barretenberg (native backend) + * - Sends proofs via the msgpack RPC protocol (ChonkBatchVerifierQueue) + * - Receives results via a named FIFO pipe (async, out-of-order) + * - Bisects batch failures to isolate individual bad proofs + */ +export class BatchChonkVerifier implements ClientProtocolCircuitVerifier { + private bb!: Barretenberg; + private fifoPath: string; + private nextRequestId = 0; + private pendingRequests = new Map(); + private sendQueue: SerialQueue; + private fifoReader: FifoFrameReader; + private logger = createLogger('bb-prover:batch_chonk_verifier'); + /** Maps artifact name to VK index in the batch verifier. */ + private vkIndexMap = new Map(); + /** Bound cleanup handler for process exit signals. */ + private exitCleanup: (() => void) | null = null; + + private constructor( + private config: BBConfig, + private batchSize: number, + private label: string, + ) { + this.fifoPath = path.join(os.tmpdir(), `bb-batch-${label}-${process.pid}-${Date.now()}.fifo`); + this.fifoReader = new FifoFrameReader(); + this.sendQueue = new SerialQueue(); + this.sendQueue.start(1); + } + + /** + * Create and start a new BatchChonkVerifier. + * @param config - BB binary configuration. + * @param batchSize - Max proofs per batch. + * @param label - Descriptive label for FIFO path and logging (e.g. 'peer', 'rpc'). + */ + static async new(config: BBConfig, batchSize = 8, label = 'verifier'): Promise { + const verifier = new BatchChonkVerifier(config, batchSize, label); + await verifier.start(); + return verifier; + } + + private async start(): Promise { + this.logger.info('Starting BatchChonkVerifier'); + + // Force native backend — batch verification is not supported in WASM + this.bb = await Barretenberg.new({ + bbPath: this.config.bbBinaryPath, + backend: BackendType.NativeUnixSocket, + }); + await this.bb.initSRSChonk(); + + // Collect VKs for all chonk-verifiable circuits + const vkBuffers: Uint8Array[] = []; + for (const artifact of CHONK_VK_ARTIFACTS) { + const vk = ProtocolCircuitVks[artifact]; + if (!vk) { + throw new Error(`Missing VK for ${artifact}`); + } + this.vkIndexMap.set(artifact, vkBuffers.length); + vkBuffers.push(vk.keyAsBytes); + } + + // Create FIFO pipe for async result delivery + await execFileAsync('mkfifo', [this.fifoPath]); + this.registerExitCleanup(); + + // Start the batch verifier service in bb + await this.bb.chonkBatchVerifierStart({ + vks: vkBuffers, + numCores: this.config.bbIVCConcurrency || 0, + batchSize: this.batchSize, + fifoPath: this.fifoPath, + }); + + // Start FIFO reader (must happen after service start, since bb opens FIFO for writing) + this.startFifoReader(); + + this.logger.info('BatchChonkVerifier started', { fifoPath: this.fifoPath }); + } + + public verifyProof(tx: Tx): Promise { + const totalTimer = new Timer(); + const requestId = this.nextRequestId++; + const circuit = tx.data.forPublic ? 'HidingKernelToPublic' : 'HidingKernelToRollup'; + const vkIndex = this.vkIndexMap.get(circuit); + if (vkIndex === undefined) { + throw new Error(`No VK index for circuit ${circuit}`); + } + + // Attach public inputs to get the flat proof fields array (C++ splits into ChonkProof segments) + const proofWithPubInputs = tx.chonkProof.attachPublicInputs(tx.data.publicInputs().toFields()); + const proofFields = proofWithPubInputs.fieldsWithPublicInputs.map(f => f.toBuffer()); + + // Create pending promise + const resultPromise = new Promise((resolve, reject) => { + this.pendingRequests.set(requestId, { resolve, reject, totalTimer }); + }); + + // Enqueue via the serial send queue (bb pipe is single-request). + // Catch send errors to avoid dangling promises if the bb process dies. + void this.sendQueue + .put(async () => { + await this.bb.chonkBatchVerifierQueue({ + requestId, + vkIndex, + proofFields, + }); + }) + .catch(err => { + const pending = this.pendingRequests.get(requestId); + if (pending) { + this.pendingRequests.delete(requestId); + pending.reject(err instanceof Error ? err : new Error(String(err))); + } + }); + + return resultPromise; + } + + public async stop(): Promise { + this.logger.info('Stopping BatchChonkVerifier'); + + // Stop accepting new proofs + await this.sendQueue.end(); + + // Stop the bb service (flushes remaining proofs) + try { + await this.bb.chonkBatchVerifierStop({}); + } catch (err) { + this.logger.warn(`Error stopping batch verifier service: ${err}`); + } + + // Stop FIFO reader + this.fifoReader.stop(); + + // Clean up FIFO file and deregister exit handler + await unlink(this.fifoPath).catch(() => {}); + this.deregisterExitCleanup(); + + // Reject any remaining pending requests + for (const [id, pending] of this.pendingRequests) { + pending.reject(new Error('BatchChonkVerifier stopped')); + this.pendingRequests.delete(id); + } + + // Destroy bb process + await this.bb.destroy(); + + this.logger.info('BatchChonkVerifier stopped'); + } + + private startFifoReader(): void { + const unpackr = new Unpackr({ useRecords: false }); + + this.fifoReader.on('frame', (payload: Buffer) => { + try { + const result = unpackr.unpack(payload) as FifoVerifyResult; + this.handleResult(result); + } catch (err) { + this.logger.error(`FIFO: failed to decode msgpack result: ${err}`); + } + }); + + this.fifoReader.on('error', (err: Error) => { + this.logger.error(`FIFO reader error: ${err}`); + }); + + this.fifoReader.on('end', () => { + this.logger.debug('FIFO reader: stream ended'); + for (const [id, pending] of this.pendingRequests) { + pending.reject(new Error('FIFO stream ended unexpectedly')); + this.pendingRequests.delete(id); + } + }); + + this.fifoReader.start(this.fifoPath); + } + + private handleResult(result: FifoVerifyResult): void { + const pending = this.pendingRequests.get(result.request_id); + if (!pending) { + this.logger.warn(`Received result for unknown request_id=${result.request_id}`); + return; + } + this.pendingRequests.delete(result.request_id); + + const valid = result.status === 0; // VerifyStatus::OK + const durationMs = result.time_in_verify_ms; + const totalDurationMs = pending.totalTimer.ms(); + + const ivcResult: IVCProofVerificationResult = { valid, durationMs, totalDurationMs }; + + if (!valid) { + this.logger.warn(`Proof verification failed for request_id=${result.request_id}: ${result.error_message}`); + } else { + this.logger.debug(`Proof verified`, { + requestId: result.request_id, + durationMs: Math.ceil(durationMs), + totalDurationMs: Math.ceil(totalDurationMs), + }); + } + + pending.resolve(ivcResult); + } + + private registerExitCleanup(): void { + // Signal handlers must be synchronous — unlinkSync is intentional here + this.exitCleanup = () => { + try { + unlinkSync(this.fifoPath); + } catch { + /* ignore */ + } + }; + process.on('exit', this.exitCleanup); + process.on('SIGINT', this.exitCleanup); + process.on('SIGTERM', this.exitCleanup); + } + + private deregisterExitCleanup(): void { + if (this.exitCleanup) { + process.removeListener('exit', this.exitCleanup); + process.removeListener('SIGINT', this.exitCleanup); + process.removeListener('SIGTERM', this.exitCleanup); + this.exitCleanup = null; + } + } +} diff --git a/yarn-project/bb-prover/src/verifier/index.ts b/yarn-project/bb-prover/src/verifier/index.ts index 1ce2f3a7e41a..229e41274d6e 100644 --- a/yarn-project/bb-prover/src/verifier/index.ts +++ b/yarn-project/bb-prover/src/verifier/index.ts @@ -1,2 +1,3 @@ +export * from './batch_chonk_verifier.js'; export * from './bb_verifier.js'; export * from './queued_chonk_verifier.js'; diff --git a/yarn-project/bb-prover/src/verifier/queued_chonk_verifier.ts b/yarn-project/bb-prover/src/verifier/queued_chonk_verifier.ts index 4eeca0d042e5..ecee1df8ab1b 100644 --- a/yarn-project/bb-prover/src/verifier/queued_chonk_verifier.ts +++ b/yarn-project/bb-prover/src/verifier/queued_chonk_verifier.ts @@ -16,8 +16,6 @@ import { import { createHistogram } from 'node:perf_hooks'; -import type { BBConfig } from '../config.js'; - class IVCVerifierMetrics { private ivcVerificationHistogram: Histogram; private ivcTotalVerificationHistogram: Histogram; @@ -86,15 +84,15 @@ export class QueuedIVCVerifier implements ClientProtocolCircuitVerifier { private metrics: IVCVerifierMetrics; public constructor( - config: BBConfig, private verifier: ClientProtocolCircuitVerifier, + concurrency: number, private telemetry: TelemetryClient = getTelemetryClient(), private logger = createLogger('bb-prover:queued_chonk_verifier'), ) { this.metrics = new IVCVerifierMetrics(this.telemetry, 'QueuedIVCVerifier'); this.queue = new SerialQueue(); - this.logger.info(`Starting QueuedIVCVerifier with ${config.numConcurrentIVCVerifiers} concurrent verifiers`); - this.queue.start(config.numConcurrentIVCVerifiers); + this.logger.info(`Starting QueuedIVCVerifier with ${concurrency} concurrent verifiers`); + this.queue.start(concurrency); } public async verifyProof(tx: Tx): Promise { @@ -103,7 +101,10 @@ export class QueuedIVCVerifier implements ClientProtocolCircuitVerifier { return result; } - stop(): Promise { - return this.queue.end(); + async stop(): Promise { + await this.queue.end(); + if ('stop' in this.verifier && typeof this.verifier.stop === 'function') { + await this.verifier.stop(); + } } } diff --git a/yarn-project/bootstrap.sh b/yarn-project/bootstrap.sh index fdf24a891720..d48d94fdce96 100755 --- a/yarn-project/bootstrap.sh +++ b/yarn-project/bootstrap.sh @@ -248,6 +248,7 @@ function bench_cmds { 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" + echo "$hash:ISOLATE=1:CPUS=16:MEM=16g:TIMEOUT=1200 BENCH_OUTPUT=bench-out/batch_verifier.bench.json yarn-project/scripts/run_test.sh ivc-integration/src/batch_verifier.bench.test.ts" } function release_packages { diff --git a/yarn-project/end-to-end/src/fixtures/e2e_prover_test.ts b/yarn-project/end-to-end/src/fixtures/e2e_prover_test.ts index 59f38bfeedea..9e66d2844c49 100644 --- a/yarn-project/end-to-end/src/fixtures/e2e_prover_test.ts +++ b/yarn-project/end-to-end/src/fixtures/e2e_prover_test.ts @@ -4,12 +4,6 @@ import { AztecAddress, EthAddress } from '@aztec/aztec.js/addresses'; import { type Logger, createLogger } from '@aztec/aztec.js/log'; import type { AztecNode } from '@aztec/aztec.js/node'; import { CheatCodes } from '@aztec/aztec/testing'; -import { - BBCircuitVerifier, - type ClientProtocolCircuitVerifier, - QueuedIVCVerifier, - TestCircuitVerifier, -} from '@aztec/bb-prover'; import { BackendType, Barretenberg } from '@aztec/bb.js'; import type { DeployAztecL1ContractsReturnType } from '@aztec/ethereum/deploy-aztec-l1-contracts'; import { Buffer32 } from '@aztec/foundation/buffer'; @@ -68,7 +62,6 @@ export class FullProverTest { private provenComponents: ProvenSetup[] = []; private bbConfigCleanup?: () => Promise; private acvmConfigCleanup?: () => Promise; - circuitProofVerifier?: ClientProtocolCircuitVerifier; provenAsset!: TokenContract; context!: EndToEndContext; private proverAztecNode!: AztecNodeService; @@ -79,6 +72,11 @@ export class FullProverTest { private coinbase: EthAddress; private realProofs: boolean; + /** Returns the proof verifier from the prover node (for test assertions). */ + get circuitProofVerifier() { + return this.proverAztecNode?.getProofVerifier(); + } + constructor(testName: string, minNumberOfTxsPerBlock: number, coinbase: EthAddress, realProofs = true) { this.logger = createLogger(`e2e:full_prover_test:${testName}`); this.minNumberOfTxsPerBlock = minNumberOfTxsPerBlock; @@ -170,9 +168,6 @@ export class FullProverTest { await Barretenberg.initSingleton({ backend: BackendType.NativeUnixSocket }); - const verifier = await BBCircuitVerifier.new(bbConfig); - this.circuitProofVerifier = new QueuedIVCVerifier(bbConfig, verifier); - this.logger.debug(`Configuring the node for real proofs...`); await this.aztecNodeAdmin.setConfig({ realProofs: true, @@ -180,7 +175,6 @@ export class FullProverTest { }); } else { this.logger.debug(`Configuring the node min txs per block ${this.minNumberOfTxsPerBlock}...`); - this.circuitProofVerifier = new TestCircuitVerifier(); await this.aztecNodeAdmin.setConfig({ minTxsPerBlock: this.minNumberOfTxsPerBlock, }); diff --git a/yarn-project/end-to-end/src/fixtures/get_bb_config.ts b/yarn-project/end-to-end/src/fixtures/get_bb_config.ts index 9ede6ef6a1e7..3bac5e7ad096 100644 --- a/yarn-project/end-to-end/src/fixtures/get_bb_config.ts +++ b/yarn-project/end-to-end/src/fixtures/get_bb_config.ts @@ -13,8 +13,10 @@ const { BB_SKIP_CLEANUP = '', TEMP_DIR = tmpdir(), BB_WORKING_DIRECTORY = '', - BB_NUM_IVC_VERIFIERS = '1', BB_IVC_CONCURRENCY = '1', + BB_NUM_IVC_VERIFIERS = '8', + BB_RPC_VERIFY_CONCURRENCY, + BB_PEER_VERIFY_BATCH_SIZE, } = process.env; export const getBBConfig = async ( @@ -41,16 +43,18 @@ export const getBBConfig = async ( const bbSkipCleanup = ['1', 'true'].includes(BB_SKIP_CLEANUP); const cleanup = bbSkipCleanup ? () => Promise.resolve() : () => tryRmDir(directoryToCleanup); - const numIvcVerifiers = Number(BB_NUM_IVC_VERIFIERS); const ivcConcurrency = Number(BB_IVC_CONCURRENCY); + const numIvcVerifiers = Number(BB_NUM_IVC_VERIFIERS); + return { bbSkipCleanup, bbBinaryPath, bbWorkingDirectory, cleanup, - numConcurrentIVCVerifiers: numIvcVerifiers, bbIVCConcurrency: ivcConcurrency, + bbRpcVerifyConcurrency: BB_RPC_VERIFY_CONCURRENCY ? Number(BB_RPC_VERIFY_CONCURRENCY) : numIvcVerifiers, + bbPeerVerifyBatchSize: BB_PEER_VERIFY_BATCH_SIZE ? Number(BB_PEER_VERIFY_BATCH_SIZE) : numIvcVerifiers, }; } catch (err) { logger.error(`Native BB not available, error: ${err}`); diff --git a/yarn-project/foundation/package.json b/yarn-project/foundation/package.json index 294288bc0cc4..2311a1552799 100644 --- a/yarn-project/foundation/package.json +++ b/yarn-project/foundation/package.json @@ -94,7 +94,8 @@ "./promise": "./dest/promise/index.js", "./string": "./dest/string/index.js", "./message": "./dest/message/index.js", - "./number": "./dest/number/index.js" + "./number": "./dest/number/index.js", + "./fifo": "./dest/fifo/index.js" }, "scripts": { "build": "yarn clean && ../scripts/tsc.sh", diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index 6cd085cb9e16..503530d47023 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -21,8 +21,10 @@ export type EnvVar = | 'BB_BINARY_PATH' | 'BB_SKIP_CLEANUP' | 'BB_WORKING_DIRECTORY' - | 'BB_NUM_IVC_VERIFIERS' | 'BB_IVC_CONCURRENCY' + | 'BB_NUM_IVC_VERIFIERS' + | 'BB_RPC_VERIFY_CONCURRENCY' + | 'BB_PEER_VERIFY_BATCH_SIZE' | 'BOOTSTRAP_NODES' | 'BLOB_ARCHIVE_API_URL' | 'BLOB_FILE_STORE_URLS' diff --git a/yarn-project/foundation/src/fifo/fifo_frame_reader.test.ts b/yarn-project/foundation/src/fifo/fifo_frame_reader.test.ts new file mode 100644 index 000000000000..a8c710a9ed44 --- /dev/null +++ b/yarn-project/foundation/src/fifo/fifo_frame_reader.test.ts @@ -0,0 +1,209 @@ +import { PassThrough } from 'node:stream'; + +import { FifoFrameReader } from './fifo_frame_reader.js'; + +/** Build a length-delimited frame buffer for the given payload. */ +function buildFrame(payload: Buffer): Buffer { + const header = Buffer.alloc(4); + header.writeUInt32BE(payload.length, 0); + return Buffer.concat([header, payload]); +} + +/** Collect N frames from a reader, returning them as a promise. */ +function collectFrames(reader: FifoFrameReader, count: number): Promise { + return new Promise((resolve, reject) => { + const frames: Buffer[] = []; + reader.on('frame', frame => { + frames.push(frame); + if (frames.length >= count) { + resolve(frames); + } + }); + reader.on('error', reject); + }); +} + +describe('FifoFrameReader', () => { + it('reads a single frame', async () => { + const stream = new PassThrough(); + const reader = new FifoFrameReader(); + reader.startFromStream(stream); + + const framesPromise = collectFrames(reader, 1); + stream.write(buildFrame(Buffer.from('hello world'))); + stream.end(); + + const frames = await framesPromise; + expect(frames).toHaveLength(1); + expect(frames[0].toString()).toBe('hello world'); + reader.stop(); + }); + + it('reads multiple frames from a single push', async () => { + const stream = new PassThrough(); + const reader = new FifoFrameReader(); + reader.startFromStream(stream); + + const framesPromise = collectFrames(reader, 3); + + const combined = Buffer.concat([ + buildFrame(Buffer.from('aaa')), + buildFrame(Buffer.from('bbb')), + buildFrame(Buffer.from('ccc')), + ]); + stream.write(combined); + stream.end(); + + const frames = await framesPromise; + expect(frames).toHaveLength(3); + expect(frames.map(f => f.toString())).toEqual(['aaa', 'bbb', 'ccc']); + reader.stop(); + }); + + it('handles frames split across multiple chunks', async () => { + const stream = new PassThrough(); + const reader = new FifoFrameReader(); + reader.startFromStream(stream); + + const framesPromise = collectFrames(reader, 1); + + const payload = Buffer.from('this is a longer payload that will be split'); + const frame = buildFrame(payload); + + // Split the frame into small chunks + for (let i = 0; i < frame.length; i += 5) { + stream.write(frame.subarray(i, Math.min(i + 5, frame.length))); + } + stream.end(); + + const frames = await framesPromise; + expect(frames).toHaveLength(1); + expect(frames[0].toString()).toBe(payload.toString()); + reader.stop(); + }); + + it('handles header split across chunks', async () => { + const stream = new PassThrough(); + const reader = new FifoFrameReader(); + reader.startFromStream(stream); + + const framesPromise = collectFrames(reader, 1); + + const payload = Buffer.from('data'); + const frame = buildFrame(payload); + + // Split in the middle of the 4-byte header + stream.write(frame.subarray(0, 2)); + stream.write(frame.subarray(2)); + stream.end(); + + const frames = await framesPromise; + expect(frames).toHaveLength(1); + expect(frames[0].toString()).toBe('data'); + reader.stop(); + }); + + it('emits error on zero-length payload', async () => { + const stream = new PassThrough(); + const reader = new FifoFrameReader(); + reader.startFromStream(stream); + + const errorPromise = new Promise(resolve => { + reader.on('error', resolve); + }); + + const header = Buffer.alloc(4); + header.writeUInt32BE(0, 0); + stream.write(header); + + const error = await errorPromise; + expect(error.message).toContain('Invalid payload length: 0'); + }); + + it('emits error on oversized payload', async () => { + const maxSize = 100; + const reader = new FifoFrameReader(maxSize); + const stream = new PassThrough(); + reader.startFromStream(stream); + + const errorPromise = new Promise(resolve => { + reader.on('error', resolve); + }); + + const header = Buffer.alloc(4); + header.writeUInt32BE(maxSize + 1, 0); + stream.write(header); + + const error = await errorPromise; + expect(error.message).toContain(`Invalid payload length: ${maxSize + 1}`); + }); + + it('emits end when stream ends', async () => { + const stream = new PassThrough(); + const reader = new FifoFrameReader(); + reader.startFromStream(stream); + + const endPromise = new Promise(resolve => { + reader.on('end', resolve); + }); + + stream.end(); + await endPromise; + reader.stop(); + }); + + it('reads valid frames before an invalid frame', async () => { + const stream = new PassThrough(); + const reader = new FifoFrameReader(); + reader.startFromStream(stream); + + const frames: Buffer[] = []; + reader.on('frame', frame => frames.push(frame)); + + const errorPromise = new Promise(resolve => { + reader.on('error', resolve); + }); + + // Write 2 valid frames then an invalid one + stream.write(buildFrame(Buffer.from('good1'))); + stream.write(buildFrame(Buffer.from('good2'))); + + // Invalid: zero length header + const badHeader = Buffer.alloc(4); + badHeader.writeUInt32BE(0, 0); + stream.write(badHeader); + + await errorPromise; + expect(frames).toHaveLength(2); + expect(frames[0].toString()).toBe('good1'); + expect(frames[1].toString()).toBe('good2'); + }); + + it('throws if started twice', () => { + const stream = new PassThrough(); + const reader = new FifoFrameReader(); + reader.startFromStream(stream); + + expect(() => reader.startFromStream(new PassThrough())).toThrow('already running'); + reader.stop(); + }); + + it('handles large payloads', async () => { + const stream = new PassThrough(); + const reader = new FifoFrameReader(); + reader.startFromStream(stream); + + const framesPromise = collectFrames(reader, 1); + + // 1MB payload + const payload = Buffer.alloc(1024 * 1024, 0x42); + stream.write(buildFrame(payload)); + stream.end(); + + const frames = await framesPromise; + expect(frames).toHaveLength(1); + expect(frames[0].length).toBe(1024 * 1024); + expect(frames[0][0]).toBe(0x42); + reader.stop(); + }); +}); diff --git a/yarn-project/foundation/src/fifo/fifo_frame_reader.ts b/yarn-project/foundation/src/fifo/fifo_frame_reader.ts new file mode 100644 index 000000000000..3b3bd322d33b --- /dev/null +++ b/yarn-project/foundation/src/fifo/fifo_frame_reader.ts @@ -0,0 +1,98 @@ +import EventEmitter from 'node:events'; +import * as fs from 'node:fs'; +import type { Readable } from 'node:stream'; + +/** + * Events emitted by FifoFrameReader. + * + * - `frame`: A complete frame payload (without the 4-byte length header). + * - `error`: An unrecoverable error (invalid frame length, stream error). + * - `end`: The underlying stream has ended. + */ +export interface FifoFrameReaderEvents { + frame: [payload: Buffer]; + error: [error: Error]; + end: []; +} + +/** + * Reads length-delimited frames from a readable stream (typically a named FIFO pipe). + * + * Wire format: `[4-byte big-endian payload length][payload bytes]` + * + * Emits a `frame` event for each complete frame with the raw payload buffer. + * Callers are responsible for deserializing the payload (e.g., via msgpack). + * + * On encountering an invalid payload length (0 or >maxPayloadSize), emits `error` + * and destroys the stream. + */ +export class FifoFrameReader extends EventEmitter { + private stream: Readable | null = null; + private pendingBuf: Buffer = Buffer.alloc(0); + private running = false; + + constructor(private readonly maxPayloadSize = 10 * 1024 * 1024) { + super(); + } + + /** Open a FIFO at the given path and start reading frames. */ + start(fifoPath: string, highWaterMark = 64 * 1024): void { + this.startFromStream(fs.createReadStream(fifoPath, { highWaterMark })); + } + + /** Start reading frames from an existing readable stream. */ + startFromStream(stream: Readable): void { + if (this.running) { + throw new Error('FifoFrameReader is already running'); + } + this.running = true; + this.pendingBuf = Buffer.alloc(0); + this.stream = stream; + + stream.on('data', (chunk: Buffer | string) => { + const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + this.pendingBuf = this.pendingBuf.length > 0 ? Buffer.concat([this.pendingBuf, buf]) : buf; + this.drainFrames(); + }); + + stream.on('error', (err: Error) => { + if (this.running) { + this.emit('error', err); + } + }); + + stream.on('end', () => { + this.emit('end'); + }); + } + + /** Stop reading and destroy the underlying stream. */ + stop(): void { + this.running = false; + if (this.stream) { + this.stream.destroy(); + this.stream = null; + } + } + + /** Parse complete frames out of the pending buffer. */ + private drainFrames(): void { + while (this.pendingBuf.length >= 4) { + const payloadLen = this.pendingBuf.readUInt32BE(0); + if (payloadLen === 0 || payloadLen > this.maxPayloadSize) { + this.emit('error', new Error(`Invalid payload length: ${payloadLen}`)); + this.stop(); + return; + } + + const frameLen = 4 + payloadLen; + if (this.pendingBuf.length < frameLen) { + break; // Wait for more data + } + + const payload = this.pendingBuf.subarray(4, frameLen); + this.pendingBuf = this.pendingBuf.subarray(frameLen); + this.emit('frame', Buffer.from(payload)); + } + } +} diff --git a/yarn-project/foundation/src/fifo/index.ts b/yarn-project/foundation/src/fifo/index.ts new file mode 100644 index 000000000000..d53ee629f835 --- /dev/null +++ b/yarn-project/foundation/src/fifo/index.ts @@ -0,0 +1 @@ +export { FifoFrameReader } from './fifo_frame_reader.js'; diff --git a/yarn-project/ivc-integration/src/batch_verifier.bench.test.ts b/yarn-project/ivc-integration/src/batch_verifier.bench.test.ts new file mode 100644 index 000000000000..57c00d18a329 --- /dev/null +++ b/yarn-project/ivc-integration/src/batch_verifier.bench.test.ts @@ -0,0 +1,141 @@ +/** + * Batch chonk verifier benchmarks. + * + * Measures throughput of the batch verifier service at various batch sizes and core counts, + * using proofs generated from the testing IVC stack (no external inputs needed). + */ +import { AztecClientBackend, BackendType, Barretenberg } from '@aztec/bb.js'; +import { createLogger } from '@aztec/foundation/log'; + +import { jest } from '@jest/globals'; +import { mkdir, writeFile } from 'node:fs/promises'; +import { dirname } from 'node:path'; + +import { corruptProofFields, runBatchVerifier } from './batch_verifier_test_helpers.js'; +import { generateTestingIVCStack } from './witgen.js'; + +const logger = createLogger('ivc-integration:bench:batch-verifier'); + +jest.setTimeout(600_000); + +type BenchEntry = { name: string; value: number; unit: string }; + +describe('Batch Chonk Verifier Benchmarks', () => { + let bb: Barretenberg; + let validProofFields: Uint8Array[]; + let invalidProofFields: Uint8Array[]; + let vk: Uint8Array; + const benchResults: BenchEntry[] = []; + + beforeAll(async () => { + bb = await Barretenberg.new({ backend: BackendType.NativeUnixSocket }); + await bb.initSRSChonk(); + + logger.info('Generating proof for benchmarks...'); + const [bytecodes, witnesses, , vks] = await generateTestingIVCStack(1, 0); + const backend = new AztecClientBackend(bytecodes, bb); + const [proofFields, , generatedVk] = await backend.prove(witnesses, vks); + validProofFields = proofFields; + invalidProofFields = corruptProofFields(validProofFields); + vk = generatedVk; + logger.info('Proof generated'); + }); + + afterAll(async () => { + if (process.env.BENCH_OUTPUT) { + await mkdir(dirname(process.env.BENCH_OUTPUT), { recursive: true }); + await writeFile(process.env.BENCH_OUTPUT, JSON.stringify(benchResults, null, 2)); + } else { + logger.info('Benchmark results:'); + for (const r of benchResults) { + logger.info(` ${r.name}: ${r.value.toFixed(1)} ${r.unit}`); + } + } + await bb.destroy(); + }); + + // -- Core count sweep -- + + for (const numCores of [4, 8, 16]) { + it(`throughput: 16 proofs, batch_size=8, ${numCores} cores`, async () => { + const numProofs = 16; + const batchSize = 8; + const proofs = Array.from({ length: numProofs }, () => ({ vkIndex: 0, proofFields: validProofFields })); + + const wallStart = performance.now(); + const results = await runBatchVerifier(bb, { vks: [vk], numCores, batchSize, proofs }); + const wallMs = performance.now() - wallStart; + + expect(results).toHaveLength(numProofs); + expect(results.every(r => r.status === 0)).toBe(true); + + const avgVerifyMs = results.reduce((sum, r) => sum + r.time_in_verify_ms, 0) / results.length; + const throughput = (numProofs / wallMs) * 1000; + + benchResults.push( + { name: `BatchVerify/16_proofs/${numCores}_cores/wall_time`, value: wallMs, unit: 'ms' }, + { name: `BatchVerify/16_proofs/${numCores}_cores/avg_verify`, value: avgVerifyMs, unit: 'ms' }, + { name: `BatchVerify/16_proofs/${numCores}_cores/throughput`, value: throughput, unit: 'proofs/sec' }, + ); + + logger.info(`16 proofs, ${numCores} cores`, { + wallMs: Math.ceil(wallMs), + avgVerifyMs: Math.ceil(avgVerifyMs), + throughput: throughput.toFixed(2), + }); + }); + } + + // -- Batch size sweep -- + + for (const batchSize of [2, 4, 8]) { + it(`batch_size sweep: 8 proofs, batch_size=${batchSize}, 8 cores`, async () => { + const numProofs = 8; + const numCores = 8; + const proofs = Array.from({ length: numProofs }, () => ({ vkIndex: 0, proofFields: validProofFields })); + + const wallStart = performance.now(); + const results = await runBatchVerifier(bb, { vks: [vk], numCores, batchSize, proofs }); + const wallMs = performance.now() - wallStart; + + expect(results).toHaveLength(numProofs); + expect(results.every(r => r.status === 0)).toBe(true); + + const throughput = (numProofs / wallMs) * 1000; + + benchResults.push( + { name: `BatchVerify/batch_size_${batchSize}/wall_time`, value: wallMs, unit: 'ms' }, + { name: `BatchVerify/batch_size_${batchSize}/throughput`, value: throughput, unit: 'proofs/sec' }, + ); + + logger.info(`batch_size=${batchSize}`, { wallMs: Math.ceil(wallMs), throughput: throughput.toFixed(2) }); + }); + } + + // -- Bisection overhead -- + + it('bisection overhead: 8 proofs with 2 bad, batch_size=8, 8 cores', async () => { + const numProofs = 8; + const numBad = 2; + const proofs = Array.from({ length: numProofs }, (_, i) => ({ + vkIndex: 0, + proofFields: i < numBad ? invalidProofFields : validProofFields, + })); + + const wallStart = performance.now(); + const results = await runBatchVerifier(bb, { vks: [vk], numCores: 8, batchSize: 8, proofs }); + const wallMs = performance.now() - wallStart; + + expect(results).toHaveLength(numProofs); + const byId = results.sort((a, b) => a.request_id - b.request_id); + expect(byId.filter(r => r.status === 0)).toHaveLength(numProofs - numBad); + expect(byId.filter(r => r.status === 1)).toHaveLength(numBad); + + benchResults.push({ + name: `BatchVerify/mixed_${numBad}_bad_of_${numProofs}/wall_time`, + value: wallMs, + unit: 'ms', + }); + logger.info(`mixed ${numBad} bad of ${numProofs}`, { wallMs: Math.ceil(wallMs) }); + }); +}); diff --git a/yarn-project/ivc-integration/src/batch_verifier.test.ts b/yarn-project/ivc-integration/src/batch_verifier.test.ts new file mode 100644 index 000000000000..5e200dd4b175 --- /dev/null +++ b/yarn-project/ivc-integration/src/batch_verifier.test.ts @@ -0,0 +1,216 @@ +import { AztecClientBackend, BackendType, Barretenberg } from '@aztec/bb.js'; +import { createLogger } from '@aztec/foundation/log'; + +import { jest } from '@jest/globals'; + +import { corruptProofFields, createFifo, readFifoResults } from './batch_verifier_test_helpers.js'; +import { generateTestingIVCStack } from './witgen.js'; + +const logger = createLogger('ivc-integration:test:batch-verifier'); + +jest.setTimeout(300_000); + +describe('Batch Chonk Verifier workloads', () => { + let bb: Barretenberg; + // Cache a proof + VK so we don't re-prove for every test + let validProofFields: Uint8Array[]; + let invalidProofFields: Uint8Array[]; + let vk: Uint8Array; + // Second proof from a different circuit stack (complex tx with reader app) + let validProofFields2: Uint8Array[]; + let vk2: Uint8Array; + + beforeAll(async () => { + bb = await Barretenberg.new({ backend: BackendType.NativeUnixSocket }); + await bb.initSRSChonk(); + + // Generate proof from simple tx (1 creator, 0 readers) + logger.info('Generating simple proof...'); + const [bytecodes1, witnesses1, , vks1] = await generateTestingIVCStack(1, 0); + const backend1 = new AztecClientBackend(bytecodes1, bb); + const [proofFields1, , generatedVk1] = await backend1.prove(witnesses1, vks1); + validProofFields = proofFields1; + invalidProofFields = corruptProofFields(validProofFields); + vk = generatedVk1; + + // Generate proof from complex tx (1 creator, 1 reader) — different circuit stack, same VK type + logger.info('Generating complex proof...'); + const [bytecodes2, witnesses2, , vks2] = await generateTestingIVCStack(1, 1); + const backend2 = new AztecClientBackend(bytecodes2, bb); + const [proofFields2, , generatedVk2] = await backend2.prove(witnesses2, vks2); + validProofFields2 = proofFields2; + vk2 = generatedVk2; + + logger.info('Proofs generated, ready for batch tests'); + }); + + afterAll(async () => { + await bb.destroy(); + }); + + it('should flush a single proof without waiting for a full batch', async () => { + const { fifoPath, cleanup } = await createFifo('single'); + + try { + // batch_size=4 but we only queue 1 proof — must not hang + await bb.chonkBatchVerifierStart({ + vks: [vk], + numCores: 0, + batchSize: 4, + fifoPath, + }); + + const resultPromise = readFifoResults(fifoPath, 1); + + await bb.chonkBatchVerifierQueue({ + requestId: 7, + vkIndex: 0, + proofFields: validProofFields, + }); + + // Don't call stop — the coordinator processes immediately when idle + const results = await resultPromise; + + expect(results).toHaveLength(1); + expect(results[0].request_id).toBe(7); + expect(results[0].status).toBe(0); + + logger.info('Single-proof flush test passed', { + verifyMs: Math.ceil(results[0].time_in_verify_ms), + }); + + await bb.chonkBatchVerifierStop({}); + } finally { + await cleanup(); + } + }); + + it('should verify multiple proofs in parallel', async () => { + const numProofs = 4; + const { fifoPath, cleanup } = await createFifo('parallel'); + + try { + await bb.chonkBatchVerifierStart({ + vks: [vk], + numCores: 0, + batchSize: 8, + fifoPath, + }); + + const resultPromise = readFifoResults(fifoPath, numProofs); + + // Queue all proofs + for (let i = 0; i < numProofs; i++) { + await bb.chonkBatchVerifierQueue({ + requestId: i, + vkIndex: 0, + proofFields: validProofFields, + }); + } + + await bb.chonkBatchVerifierStop({}); + const results = await resultPromise; + + expect(results).toHaveLength(numProofs); + const resultsByRequestId = new Map(results.map(r => [r.request_id, r])); + for (let i = 0; i < numProofs; i++) { + const r = resultsByRequestId.get(i); + expect(r).toBeDefined(); + expect(r!.status).toBe(0); + } + + logger.info(`Parallel test: ${numProofs} proofs verified`, { + verifyTimes: results.map(r => Math.ceil(r.time_in_verify_ms)), + }); + } finally { + await cleanup(); + } + }); + + it('should handle mixed valid and invalid proofs in one batch', async () => { + const { fifoPath, cleanup } = await createFifo('mixed'); + + // Interleave valid and invalid proofs + const proofs: { id: number; proofFields: Uint8Array[]; expectedStatus: number }[] = [ + { id: 0, proofFields: validProofFields, expectedStatus: 0 }, + { id: 1, proofFields: invalidProofFields, expectedStatus: 1 }, + { id: 2, proofFields: validProofFields, expectedStatus: 0 }, + { id: 3, proofFields: invalidProofFields, expectedStatus: 1 }, + { id: 4, proofFields: validProofFields, expectedStatus: 0 }, + ]; + + try { + await bb.chonkBatchVerifierStart({ + vks: [vk], + numCores: 0, + batchSize: 8, + fifoPath, + }); + + const resultPromise = readFifoResults(fifoPath, proofs.length); + + for (const p of proofs) { + await bb.chonkBatchVerifierQueue({ + requestId: p.id, + vkIndex: 0, + proofFields: p.proofFields, + }); + } + + await bb.chonkBatchVerifierStop({}); + const results = await resultPromise; + + expect(results).toHaveLength(proofs.length); + const resultsByRequestId = new Map(results.map(r => [r.request_id, r])); + + for (const p of proofs) { + const r = resultsByRequestId.get(p.id); + expect(r).toBeDefined(); + expect(r!.status).toBe(p.expectedStatus); + } + + const numValid = results.filter(r => r.status === 0).length; + const numInvalid = results.filter(r => r.status === 1).length; + logger.info(`Mixed test: ${numValid} valid, ${numInvalid} invalid`); + } finally { + await cleanup(); + } + }); + + it('should verify proofs with multiple VKs', async () => { + const { fifoPath, cleanup } = await createFifo('multi-vk'); + + try { + // Register both VKs + await bb.chonkBatchVerifierStart({ + vks: [vk, vk2], + numCores: 0, + batchSize: 8, + fifoPath, + }); + + const resultPromise = readFifoResults(fifoPath, 4); + + // Queue proofs against their respective VKs + await bb.chonkBatchVerifierQueue({ requestId: 0, vkIndex: 0, proofFields: validProofFields }); + await bb.chonkBatchVerifierQueue({ requestId: 1, vkIndex: 1, proofFields: validProofFields2 }); + await bb.chonkBatchVerifierQueue({ requestId: 2, vkIndex: 0, proofFields: validProofFields }); + await bb.chonkBatchVerifierQueue({ requestId: 3, vkIndex: 1, proofFields: validProofFields2 }); + + await bb.chonkBatchVerifierStop({}); + const results = await resultPromise; + + expect(results).toHaveLength(4); + const resultsByRequestId = new Map(results.map(r => [r.request_id, r])); + for (let i = 0; i < 4; i++) { + const r = resultsByRequestId.get(i); + expect(r).toBeDefined(); + expect(r!.status).toBe(0); + } + + logger.info('Multi-VK test: all 4 proofs verified with correct VKs'); + } finally { + await cleanup(); + } + }); +}); diff --git a/yarn-project/ivc-integration/src/batch_verifier_queue.test.ts b/yarn-project/ivc-integration/src/batch_verifier_queue.test.ts new file mode 100644 index 000000000000..51d740edd877 --- /dev/null +++ b/yarn-project/ivc-integration/src/batch_verifier_queue.test.ts @@ -0,0 +1,274 @@ +/** + * Batch chonk verifier queue robustness tests. + * + * Exercises edge cases: sub-batch flush, single proof, degenerate batch sizes, + * all-invalid, mixed valid/invalid with bisection, sequential start/stop cycles, + * and core count extremes. + * + * Detailed performance benchmarks live in C++ (chonk.bench.cpp) alongside + * pinned IVC inputs. + */ +import { AztecClientBackend, BackendType, Barretenberg } from '@aztec/bb.js'; +import { createLogger } from '@aztec/foundation/log'; + +import { jest } from '@jest/globals'; + +import { corruptProofFields, runBatchVerifier } from './batch_verifier_test_helpers.js'; +import { generateTestingIVCStack } from './witgen.js'; + +const logger = createLogger('ivc-integration:test:batch-verifier-queue'); + +jest.setTimeout(600_000); + +describe('Batch Chonk Verifier Queue', () => { + let bb: Barretenberg; + let validProofFields: Uint8Array[]; + let invalidProofFields: Uint8Array[]; + let vk: Uint8Array; + + beforeAll(async () => { + bb = await Barretenberg.new({ backend: BackendType.NativeUnixSocket }); + await bb.initSRSChonk(); + + logger.info('Generating proof for tests...'); + const [bytecodes, witnesses, , vks] = await generateTestingIVCStack(1, 0); + const backend = new AztecClientBackend(bytecodes, bb); + const [proofFields, , generatedVk] = await backend.prove(witnesses, vks); + validProofFields = proofFields; + invalidProofFields = corruptProofFields(validProofFields); + vk = generatedVk; + logger.info('Proof generated'); + }); + + afterAll(async () => { + await bb.destroy(); + }); + + // -- Basic cases -- + + it('single valid proof', async () => { + const results = await runBatchVerifier(bb, { + vks: [vk], + numCores: 4, + batchSize: 8, + proofs: [{ vkIndex: 0, proofFields: validProofFields }], + }); + expect(results).toHaveLength(1); + expect(results[0].status).toBe(0); + expect(results[0].request_id).toBe(0); + }); + + it('single invalid proof', async () => { + const results = await runBatchVerifier(bb, { + vks: [vk], + numCores: 4, + batchSize: 8, + proofs: [{ vkIndex: 0, proofFields: invalidProofFields }], + }); + expect(results).toHaveLength(1); + expect(results[0].status).toBe(1); + }); + + // -- Sub-batch flush: N < batch_size -- + + for (const n of [1, 2, 3, 5, 7]) { + it(`flushes ${n} proof(s) with batch_size=8`, async () => { + const proofs = Array.from({ length: n }, () => ({ vkIndex: 0, proofFields: validProofFields })); + const results = await runBatchVerifier(bb, { vks: [vk], numCores: 4, batchSize: 8, proofs }); + expect(results).toHaveLength(n); + expect(results.every(r => r.status === 0)).toBe(true); + }); + } + + // -- Exact batch boundary -- + + it('N exactly equals batch_size', async () => { + const results = await runBatchVerifier(bb, { + vks: [vk], + numCores: 4, + batchSize: 4, + proofs: Array.from({ length: 4 }, () => ({ vkIndex: 0, proofFields: validProofFields })), + }); + expect(results).toHaveLength(4); + expect(results.every(r => r.status === 0)).toBe(true); + }); + + it('N is one more than batch_size', async () => { + const results = await runBatchVerifier(bb, { + vks: [vk], + numCores: 4, + batchSize: 4, + proofs: Array.from({ length: 5 }, () => ({ vkIndex: 0, proofFields: validProofFields })), + }); + expect(results).toHaveLength(5); + expect(results.every(r => r.status === 0)).toBe(true); + }); + + // -- Degenerate batch_size=1 (every proof is its own batch) -- + + it('batch_size=1 verifies each proof individually', async () => { + const proofs = [ + { vkIndex: 0, proofFields: validProofFields }, + { vkIndex: 0, proofFields: invalidProofFields }, + { vkIndex: 0, proofFields: validProofFields }, + { vkIndex: 0, proofFields: invalidProofFields }, + ]; + const results = await runBatchVerifier(bb, { vks: [vk], numCores: 4, batchSize: 1, proofs }); + expect(results).toHaveLength(4); + const byId = results.sort((a, b) => a.request_id - b.request_id); + expect(byId[0].status).toBe(0); + expect(byId[1].status).toBe(1); + expect(byId[2].status).toBe(0); + expect(byId[3].status).toBe(1); + }); + + // -- All invalid -- + + it('all proofs invalid', async () => { + const proofs = Array.from({ length: 8 }, () => ({ vkIndex: 0, proofFields: invalidProofFields })); + const results = await runBatchVerifier(bb, { vks: [vk], numCores: 4, batchSize: 4, proofs }); + expect(results).toHaveLength(8); + expect(results.every(r => r.status === 1)).toBe(true); + }); + + // -- Mixed valid/invalid with bisection -- + + it('1 bad out of 8 (bisection identifies it)', async () => { + const proofs = Array.from({ length: 8 }, (_, i) => ({ + vkIndex: 0, + proofFields: i === 3 ? invalidProofFields : validProofFields, + })); + const results = await runBatchVerifier(bb, { vks: [vk], numCores: 4, batchSize: 8, proofs }); + expect(results).toHaveLength(8); + const byId = results.sort((a, b) => a.request_id - b.request_id); + expect(byId[3].status).toBe(1); + expect(byId.filter(r => r.status === 0)).toHaveLength(7); + }); + + it('bad proofs at batch boundaries', async () => { + const proofs = Array.from({ length: 8 }, (_, i) => ({ + vkIndex: 0, + proofFields: i === 0 || i === 4 ? invalidProofFields : validProofFields, + })); + const results = await runBatchVerifier(bb, { vks: [vk], numCores: 4, batchSize: 4, proofs }); + expect(results).toHaveLength(8); + const byId = results.sort((a, b) => a.request_id - b.request_id); + expect(byId[0].status).toBe(1); + expect(byId[4].status).toBe(1); + expect(byId.filter(r => r.status === 0)).toHaveLength(6); + }); + + it('half bad proofs', async () => { + const proofs = Array.from({ length: 16 }, (_, i) => ({ + vkIndex: 0, + proofFields: i % 2 === 0 ? invalidProofFields : validProofFields, + })); + const results = await runBatchVerifier(bb, { vks: [vk], numCores: 8, batchSize: 8, proofs }); + expect(results).toHaveLength(16); + expect(results.filter(r => r.status === 0)).toHaveLength(8); + expect(results.filter(r => r.status === 1)).toHaveLength(8); + }); + + // -- Core count extremes -- + + it('works with numCores=1', async () => { + const proofs = Array.from({ length: 4 }, () => ({ vkIndex: 0, proofFields: validProofFields })); + const results = await runBatchVerifier(bb, { vks: [vk], numCores: 1, batchSize: 4, proofs }); + expect(results).toHaveLength(4); + expect(results.every(r => r.status === 0)).toBe(true); + }); + + it('16 cores, batch_size=16, 32 proofs', async () => { + const proofs = Array.from({ length: 32 }, () => ({ vkIndex: 0, proofFields: validProofFields })); + const results = await runBatchVerifier(bb, { vks: [vk], numCores: 16, batchSize: 16, proofs }); + expect(results).toHaveLength(32); + expect(results.every(r => r.status === 0)).toBe(true); + }); + + // -- Request ID tracking -- + + it('returns correct request_ids for all proofs', async () => { + const n = 12; + const proofs = Array.from({ length: n }, () => ({ vkIndex: 0, proofFields: validProofFields })); + const results = await runBatchVerifier(bb, { vks: [vk], numCores: 4, batchSize: 4, proofs }); + const ids = results.map(r => r.request_id).sort((a, b) => a - b); + expect(ids).toEqual(Array.from({ length: n }, (_, i) => i)); + }); + + // -- Sequential start/stop cycles -- + + it('can start, verify, stop, then start again', async () => { + const results1 = await runBatchVerifier(bb, { + vks: [vk], + numCores: 4, + batchSize: 4, + proofs: Array.from({ length: 4 }, () => ({ vkIndex: 0, proofFields: validProofFields })), + }); + expect(results1).toHaveLength(4); + expect(results1.every(r => r.status === 0)).toBe(true); + + const results2 = await runBatchVerifier(bb, { + vks: [vk], + numCores: 8, + batchSize: 2, + proofs: [ + { vkIndex: 0, proofFields: validProofFields }, + { vkIndex: 0, proofFields: invalidProofFields }, + { vkIndex: 0, proofFields: validProofFields }, + ], + }); + expect(results2).toHaveLength(3); + const byId = results2.sort((a, b) => a.request_id - b.request_id); + expect(byId[0].status).toBe(0); + expect(byId[1].status).toBe(1); + expect(byId[2].status).toBe(0); + }); + + // -- Random bisection patterns -- + + /** Simple seeded PRNG for deterministic randomness in tests. */ + function seededRandom(seed: number): () => number { + let s = seed; + return () => { + s = (s * 1664525 + 1013904223) & 0xffffffff; + return (s >>> 0) / 0xffffffff; + }; + } + + for (const { totalProofs, batchSize, numBad, seed } of [ + { totalProofs: 8, batchSize: 8, numBad: 1, seed: 42 }, + { totalProofs: 8, batchSize: 8, numBad: 3, seed: 123 }, + { totalProofs: 8, batchSize: 4, numBad: 2, seed: 999 }, + { totalProofs: 12, batchSize: 4, numBad: 4, seed: 7 }, + { totalProofs: 16, batchSize: 8, numBad: 5, seed: 314 }, + { totalProofs: 6, batchSize: 3, numBad: 3, seed: 271 }, + { totalProofs: 10, batchSize: 10, numBad: 1, seed: 555 }, + { totalProofs: 4, batchSize: 2, numBad: 4, seed: 0 }, + ]) { + it(`random pattern: ${numBad} bad in ${totalProofs}, batchSize=${batchSize}, seed=${seed}`, async () => { + // Deterministically pick which indices are bad + const rng = seededRandom(seed); + const indices = Array.from({ length: totalProofs }, (_, i) => i); + // Fisher-Yates shuffle + for (let i = indices.length - 1; i > 0; i--) { + const j = Math.floor(rng() * (i + 1)); + [indices[i], indices[j]] = [indices[j], indices[i]]; + } + const badIndices = new Set(indices.slice(0, numBad)); + + const proofs = Array.from({ length: totalProofs }, (_, i) => ({ + vkIndex: 0, + proofFields: badIndices.has(i) ? invalidProofFields : validProofFields, + })); + + const results = await runBatchVerifier(bb, { vks: [vk], numCores: 4, batchSize, proofs }); + expect(results).toHaveLength(totalProofs); + + const byId = results.sort((a, b) => a.request_id - b.request_id); + for (let i = 0; i < totalProofs; i++) { + const expectedStatus = badIndices.has(i) ? 1 : 0; + expect(byId[i].status).toBe(expectedStatus); + } + }); + } +}); diff --git a/yarn-project/ivc-integration/src/batch_verifier_test_helpers.ts b/yarn-project/ivc-integration/src/batch_verifier_test_helpers.ts new file mode 100644 index 000000000000..513b647db88d --- /dev/null +++ b/yarn-project/ivc-integration/src/batch_verifier_test_helpers.ts @@ -0,0 +1,107 @@ +import type { Barretenberg } from '@aztec/bb.js'; +import { FifoFrameReader } from '@aztec/foundation/fifo'; + +import { Unpackr } from 'msgpackr'; +import { execFile } from 'node:child_process'; +import { unlink } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { promisify } from 'node:util'; + +const execFileAsync = promisify(execFile); + +/** Result from the FIFO, matching the C++ VerifyResult struct. */ +export interface FifoVerifyResult { + request_id: number; + status: number; + error_message: string; + time_in_verify_ms: number; +} + +/** Read N length-delimited msgpack frames from a FIFO. */ +export function readFifoResults(fifoPath: string, count: number): Promise { + return new Promise((resolve, reject) => { + const unpackr = new Unpackr({ useRecords: false }); + const reader = new FifoFrameReader(); + const results: FifoVerifyResult[] = []; + + reader.on('frame', (payload: Buffer) => { + results.push(unpackr.unpack(payload) as FifoVerifyResult); + if (results.length >= count) { + reader.stop(); + resolve(results); + } + }); + + reader.on('error', err => { + if (results.length < count) { + reject(err); + } + }); + + reader.on('end', () => { + if (results.length >= count) { + resolve(results); + } else { + reject(new Error(`FIFO ended after ${results.length}/${count} results`)); + } + }); + + reader.start(fifoPath); + }); +} + +/** Create a FIFO and return its path + cleanup function. */ +export async function createFifo(label: string): Promise<{ fifoPath: string; cleanup: () => Promise }> { + const fifoPath = join(tmpdir(), `bb-test-${label}-${process.pid}-${Date.now()}.fifo`); + await execFileAsync('mkfifo', [fifoPath]); + return { + fifoPath, + cleanup: () => unlink(fifoPath).catch(() => {}), + }; +} + +/** Corrupt flat proof fields by flipping bytes in an early field element. */ +export function corruptProofFields(fields: Uint8Array[]): Uint8Array[] { + const corrupted = fields.map(f => Uint8Array.from(f)); + corrupted[2] = Uint8Array.from(corrupted[2]); + corrupted[2][0] ^= 0xff; + corrupted[2][1] ^= 0xff; + return corrupted; +} + +/** Run the batch verifier with the given workload and collect all results. */ +export async function runBatchVerifier( + bb: Barretenberg, + opts: { + vks: Uint8Array[]; + numCores: number; + batchSize: number; + proofs: { vkIndex: number; proofFields: Uint8Array[] }[]; + }, +): Promise { + const { fifoPath, cleanup } = await createFifo(`${opts.numCores}c-${opts.proofs.length}p-bs${opts.batchSize}`); + try { + await bb.chonkBatchVerifierStart({ + vks: opts.vks, + numCores: opts.numCores, + batchSize: opts.batchSize, + fifoPath, + }); + + const resultPromise = readFifoResults(fifoPath, opts.proofs.length); + + for (let i = 0; i < opts.proofs.length; i++) { + await bb.chonkBatchVerifierQueue({ + requestId: i, + vkIndex: opts.proofs[i].vkIndex, + proofFields: opts.proofs[i].proofFields, + }); + } + + await bb.chonkBatchVerifierStop({}); + return await resultPromise; + } finally { + await cleanup(); + } +} diff --git a/yarn-project/p2p/src/client/factory.ts b/yarn-project/p2p/src/client/factory.ts index aa154872dada..ddc0eef4c666 100644 --- a/yarn-project/p2p/src/client/factory.ts +++ b/yarn-project/p2p/src/client/factory.ts @@ -48,7 +48,7 @@ export const P2P_ATTESTATION_STORE_NAME = 'p2p-attestation'; export async function createP2PClient( inputConfig: P2PConfig & DataStoreConfig & ChainConfig, archiver: L2BlockSource & ContractDataSource, - proofVerifier: ClientProtocolCircuitVerifier, + proofVerifier: ClientProtocolCircuitVerifier | undefined, worldStateSynchronizer: WorldStateSynchronizer, epochCache: EpochCacheInterface, packageVersion: string, @@ -208,7 +208,7 @@ export async function createP2PClient( async function createP2PService( config: P2PConfig & DataStoreConfig, archiver: L2BlockSource & ContractDataSource, - proofVerifier: ClientProtocolCircuitVerifier, + proofVerifier: ClientProtocolCircuitVerifier | undefined, worldStateSynchronizer: WorldStateSynchronizer, epochCache: EpochCacheInterface, store: AztecAsyncKVStore, diff --git a/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts b/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts index 849f105b46be..7565b73edf94 100644 --- a/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts +++ b/yarn-project/p2p/src/msg_validators/tx_validator/factory.ts @@ -178,9 +178,12 @@ export function createFirstStageTxValidationsForGossipedTransactions( * (e.g., duplicates, insufficient balance, pool full). */ export function createSecondStageTxValidationsForGossipedTransactions( - proofVerifier: ClientProtocolCircuitVerifier, + proofVerifier: ClientProtocolCircuitVerifier | undefined, bindings?: LoggerBindings, ): Record { + if (!proofVerifier) { + return {}; + } return { proofValidator: { validator: new TxProofValidator(proofVerifier, bindings), @@ -196,7 +199,7 @@ export function createSecondStageTxValidationsForGossipedTransactions( * caught later by the block building validator. */ function createTxValidatorForMinimumTxIntegrityChecks( - verifier: ClientProtocolCircuitVerifier, + verifier: ClientProtocolCircuitVerifier | undefined, { l1ChainId, rollupVersion, @@ -206,7 +209,7 @@ function createTxValidatorForMinimumTxIntegrityChecks( }, bindings?: LoggerBindings, ): TxValidator { - return new AggregateTxValidator( + const validators: TxValidator[] = [ new MetadataTxValidator( { l1ChainId: new Fr(l1ChainId), @@ -218,8 +221,11 @@ function createTxValidatorForMinimumTxIntegrityChecks( ), new SizeTxValidator(bindings), new DataTxValidator(bindings), - new TxProofValidator(verifier, bindings), - ); + ]; + if (verifier) { + validators.push(new TxProofValidator(verifier, bindings)); + } + return new AggregateTxValidator(...validators); } /** @@ -229,7 +235,7 @@ function createTxValidatorForMinimumTxIntegrityChecks( * enters the pending pool or during block building. */ export function createTxValidatorForReqResponseReceivedTxs( - verifier: ClientProtocolCircuitVerifier, + verifier: ClientProtocolCircuitVerifier | undefined, { l1ChainId, rollupVersion, @@ -248,7 +254,7 @@ export function createTxValidatorForReqResponseReceivedTxs( * re-execution; their validity against state is checked during block building. */ export function createTxValidatorForBlockProposalReceivedTxs( - verifier: ClientProtocolCircuitVerifier, + verifier: ClientProtocolCircuitVerifier | undefined, { l1ChainId, rollupVersion, diff --git a/yarn-project/p2p/src/services/libp2p/libp2p_service.ts b/yarn-project/p2p/src/services/libp2p/libp2p_service.ts index e59fbaa99b4c..4dc9f1349fa9 100644 --- a/yarn-project/p2p/src/services/libp2p/libp2p_service.ts +++ b/yarn-project/p2p/src/services/libp2p/libp2p_service.ts @@ -191,7 +191,7 @@ export class LibP2PService extends WithTracer implements P2PService { protected mempools: MemPools, protected archiver: L2BlockSource & ContractDataSource, private epochCache: EpochCacheInterface, - private proofVerifier: ClientProtocolCircuitVerifier, + private proofVerifier: ClientProtocolCircuitVerifier | undefined, private worldStateSynchronizer: WorldStateSynchronizer, telemetry: TelemetryClient, logger: Logger = createLogger('p2p:libp2p_service'), @@ -271,7 +271,7 @@ export class LibP2PService extends WithTracer implements P2PService { mempools: MemPools; l2BlockSource: L2BlockSource & ContractDataSource; epochCache: EpochCacheInterface; - proofVerifier: ClientProtocolCircuitVerifier; + proofVerifier: ClientProtocolCircuitVerifier | undefined; worldStateSynchronizer: WorldStateSynchronizer; peerStore: AztecAsyncKVStore; telemetry: TelemetryClient; diff --git a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/tx_validator.ts b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/tx_validator.ts index 3f79866a31c9..ae2aa93dd0c8 100644 --- a/yarn-project/p2p/src/services/reqresp/batch-tx-requester/tx_validator.ts +++ b/yarn-project/p2p/src/services/reqresp/batch-tx-requester/tx_validator.ts @@ -6,7 +6,7 @@ import { createTxValidatorForReqResponseReceivedTxs } from '../../../msg_validat export interface BatchRequestTxValidatorConfig { l1ChainId: number; rollupVersion: number; - proofVerifier: ClientProtocolCircuitVerifier; + proofVerifier: ClientProtocolCircuitVerifier | undefined; } export interface IBatchRequestTxValidator { diff --git a/yarn-project/p2p/src/test-helpers/mock-pubsub.ts b/yarn-project/p2p/src/test-helpers/mock-pubsub.ts index cf48654e0aff..b4bd767b0199 100644 --- a/yarn-project/p2p/src/test-helpers/mock-pubsub.ts +++ b/yarn-project/p2p/src/test-helpers/mock-pubsub.ts @@ -52,7 +52,7 @@ export function getMockPubSubP2PServiceFactory( mempools: MemPools; l2BlockSource: L2BlockSource & ContractDataSource; epochCache: EpochCacheInterface; - proofVerifier: ClientProtocolCircuitVerifier; + proofVerifier: ClientProtocolCircuitVerifier | undefined; worldStateSynchronizer: WorldStateSynchronizer; peerStore: AztecAsyncKVStore; telemetry: TelemetryClient; diff --git a/yarn-project/prover-client/src/config.ts b/yarn-project/prover-client/src/config.ts index 658558f9eb44..6fd9e828d8fd 100644 --- a/yarn-project/prover-client/src/config.ts +++ b/yarn-project/prover-client/src/config.ts @@ -42,16 +42,24 @@ export const bbConfigMappings: ConfigMappingsType = { description: 'Whether to skip cleanup of bb temporary files', ...booleanConfigHelper(false), }, - numConcurrentIVCVerifiers: { - env: 'BB_NUM_IVC_VERIFIERS', - description: 'Max number of chonk verifiers to run concurrently', - ...numberConfigHelper(8), - }, bbIVCConcurrency: { env: 'BB_IVC_CONCURRENCY', description: 'Number of threads to use for IVC verification', ...numberConfigHelper(1), }, + bbRpcVerifyConcurrency: { + env: 'BB_RPC_VERIFY_CONCURRENCY', + description: + 'Max concurrent verifications for the RPC verifier (QueuedIVCVerifier). Falls back to BB_NUM_IVC_VERIFIERS.', + parseEnv: (val: string) => (val ? Number(val) : Number(process.env.BB_NUM_IVC_VERIFIERS ?? '8')), + defaultValue: Number(process.env.BB_NUM_IVC_VERIFIERS ?? '8'), + }, + bbPeerVerifyBatchSize: { + env: 'BB_PEER_VERIFY_BATCH_SIZE', + description: 'Max batch size for the peer chonk verifier (BatchChonkVerifier). Falls back to BB_NUM_IVC_VERIFIERS.', + parseEnv: (val: string) => (val ? Number(val) : Number(process.env.BB_NUM_IVC_VERIFIERS ?? '8')), + defaultValue: Number(process.env.BB_NUM_IVC_VERIFIERS ?? '8'), + }, }; export const proverClientConfigMappings: ConfigMappingsType = { diff --git a/yarn-project/prover-client/src/mocks/test_context.ts b/yarn-project/prover-client/src/mocks/test_context.ts index 73c71c03e3e9..737b43992436 100644 --- a/yarn-project/prover-client/src/mocks/test_context.ts +++ b/yarn-project/prover-client/src/mocks/test_context.ts @@ -104,8 +104,9 @@ export class TestContext { bbBinaryPath: config.expectedBBPath, bbWorkingDirectory: config.bbWorkingDirectory, bbSkipCleanup: config.bbSkipCleanup, - numConcurrentIVCVerifiers: 2, bbIVCConcurrency: 1, + bbRpcVerifyConcurrency: 8, + bbPeerVerifyBatchSize: 8, }; localProver = await createProver(bbConfig); } diff --git a/yarn-project/txe/src/state_machine/index.ts b/yarn-project/txe/src/state_machine/index.ts index 5976e9f346a6..ca44362beee2 100644 --- a/yarn-project/txe/src/state_machine/index.ts +++ b/yarn-project/txe/src/state_machine/index.ts @@ -59,6 +59,7 @@ export class TXEStateMachine { new MockEpochCache(), getPackageVersion() ?? '', new TestCircuitVerifier(), + new TestCircuitVerifier(), undefined, log, ); diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index a62105bd4c69..5e3e539b3f0f 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -931,6 +931,7 @@ __metadata: commander: "npm:^12.1.0" jest: "npm:^30.0.0" jest-mock-extended: "npm:^4.0.0" + msgpackr: "npm:^1.11.2" pako: "npm:^2.1.0" source-map-support: "npm:^0.5.21" ts-node: "npm:^10.9.1"