From 99b8186dc4156628ccfc515f753b213a29c04a59 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Wed, 25 Sep 2024 14:28:34 -0300 Subject: [PATCH 1/6] feat: Prover node submits epoch proofs --- l1-contracts/src/core/Rollup.sol | 11 +- l1-contracts/test/Rollup.t.sol | 9 +- .../src/interfaces/block-prover.ts | 6 +- .../src/interfaces/prover-client.ts | 7 +- yarn-project/circuits.js/src/structs/proof.ts | 7 + .../src/e2e_prover/e2e_prover_test.ts | 6 +- .../end-to-end/src/e2e_prover/full.test.ts | 14 +- .../end-to-end/src/e2e_prover_node.test.ts | 67 ++++---- .../src/fixtures/snapshot_manager.ts | 6 +- .../e2e_public_testnet_transfer.test.ts | 15 +- .../foundation/src/collection/array.ts | 15 ++ yarn-project/foundation/src/config/env_var.ts | 1 + yarn-project/foundation/src/types/index.ts | 5 + yarn-project/prover-client/src/index.ts | 2 +- .../prover-client/src/mocks/test_context.ts | 4 +- .../src/orchestrator/orchestrator.ts | 13 +- .../src/test/bb_prover_full_rollup.test.ts | 57 ++++--- .../prover-client/src/tx-prover/tx-prover.ts | 8 +- yarn-project/prover-node/src/config.ts | 8 +- yarn-project/prover-node/src/factory.ts | 1 + ...ck-proving-job.ts => epoch-proving-job.ts} | 79 ++++----- .../prover-node/src/prover-node.test.ts | 49 +++--- yarn-project/prover-node/src/prover-node.ts | 41 +++-- .../sequencer-client/src/publisher/index.ts | 2 +- .../src/publisher/l1-publisher.ts | 150 ++++++++++++++---- 25 files changed, 359 insertions(+), 224 deletions(-) rename yarn-project/prover-node/src/job/{block-proving-job.ts => epoch-proving-job.ts} (68%) diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index 92e618fe69e5..ee9b23d30eb2 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -100,7 +100,7 @@ contract Rollup is Leonidas, IRollup, ITestRollup { // Genesis block blocks[0] = BlockLog({ archive: bytes32(Constants.GENESIS_ARCHIVE_ROOT), - blockHash: bytes32(0), + blockHash: bytes32(0), // TODO(palla/prover): The first block does not have hash zero slotNumber: Slot.wrap(0) }); for (uint256 i = 0; i < _validators.length; i++) { @@ -550,7 +550,8 @@ contract Rollup is Leonidas, IRollup, ITestRollup { } bytes32 expectedPreviousBlockHash = blocks[previousBlockNumber].blockHash; - if (expectedPreviousBlockHash != _args[2]) { + // TODO: Remove 0 check once we inject the proper genesis block hash + if (expectedPreviousBlockHash != 0 && expectedPreviousBlockHash != _args[2]) { revert Errors.Rollup__InvalidPreviousBlockHash(expectedPreviousBlockHash, _args[2]); } @@ -608,16 +609,16 @@ contract Rollup is Leonidas, IRollup, ITestRollup { // out_hash: root of this epoch's l2 to l1 message tree publicInputs[8] = _args[5]; - // fees[9-40]: array of recipient-value pairs + // fees[9-72]: array of recipient-value pairs for (uint256 i = 0; i < 64; i++) { publicInputs[9 + i] = _fees[i]; } // vk_tree_root - publicInputs[41] = vkTreeRoot; + publicInputs[73] = vkTreeRoot; // prover_id: id of current epoch's prover - publicInputs[42] = _args[6]; + publicInputs[74] = _args[6]; // the block proof is recursive, which means it comes with an aggregation object // this snippet copies it into the public inputs needed for verification diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index 90798ca24f7b..f1ea19b3daf4 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -671,10 +671,11 @@ contract RollupTest is DecoderBase { ); _submitEpochProof(rollup, 1, wrong, data.archive, preBlockHash, data.blockHash, bytes32(0)); - vm.expectRevert( - abi.encodeWithSelector(Errors.Rollup__InvalidPreviousBlockHash.selector, preBlockHash, wrong) - ); - _submitEpochProof(rollup, 1, preArchive, data.archive, wrong, data.blockHash, bytes32(0)); + // TODO: Reenable when we setup proper initial block hash + // vm.expectRevert( + // abi.encodeWithSelector(Errors.Rollup__InvalidPreviousBlockHash.selector, preBlockHash, wrong) + // ); + // _submitEpochProof(rollup, 1, preArchive, data.archive, wrong, data.blockHash, bytes32(0)); } function testSubmitProofInvalidArchive() public setUpFor("empty_block_1") { diff --git a/yarn-project/circuit-types/src/interfaces/block-prover.ts b/yarn-project/circuit-types/src/interfaces/block-prover.ts index 8ef43b177335..1443621761f7 100644 --- a/yarn-project/circuit-types/src/interfaces/block-prover.ts +++ b/yarn-project/circuit-types/src/interfaces/block-prover.ts @@ -1,4 +1,4 @@ -import { type Fr, type GlobalVariables, type Proof } from '@aztec/circuits.js'; +import { type Fr, type GlobalVariables, type Proof, type RootRollupPublicInputs } from '@aztec/circuits.js'; import { type L2Block } from '../l2_block.js'; import { type ProcessedTx } from '../tx/processed_tx.js'; @@ -32,6 +32,8 @@ export type ProvingBlockResult = SimulationBlockResult & { aggregationObject: Fr[]; }; +export type ProvingEpochResult = { publicInputs: RootRollupPublicInputs; proof: Proof }; + /** Receives processed txs as part of block simulation or proving. */ export interface ProcessedTxHandler { /** @@ -75,4 +77,6 @@ export interface BlockProver extends BlockSimulator { export interface EpochProver extends BlockProver { startNewEpoch(epochNumber: number, totalNumBlocks: number): ProvingTicket; + + finaliseEpoch(): ProvingEpochResult; } diff --git a/yarn-project/circuit-types/src/interfaces/prover-client.ts b/yarn-project/circuit-types/src/interfaces/prover-client.ts index 7e8aa848f4fd..e7648c0ada83 100644 --- a/yarn-project/circuit-types/src/interfaces/prover-client.ts +++ b/yarn-project/circuit-types/src/interfaces/prover-client.ts @@ -2,7 +2,7 @@ import { type TxHash } from '@aztec/circuit-types'; import { Fr } from '@aztec/circuits.js'; import { type ConfigMappingsType, booleanConfigHelper, numberConfigHelper } from '@aztec/foundation/config'; -import { type BlockProver } from './block-prover.js'; +import { type EpochProver } from './block-prover.js'; import { type MerkleTreeOperations } from './merkle_tree_operations.js'; import { type ProvingJobSource } from './proving-job.js'; @@ -84,10 +84,9 @@ function parseProverId(str: string) { /** * The interface to the prover client. * Provides the ability to generate proofs and build rollups. - * TODO(palla/prover-node): Rename this interface */ -export interface ProverClient { - createBlockProver(db: MerkleTreeOperations): BlockProver; +export interface EpochProverManager { + createEpochProver(db: MerkleTreeOperations): EpochProver; start(): Promise; diff --git a/yarn-project/circuits.js/src/structs/proof.ts b/yarn-project/circuits.js/src/structs/proof.ts index 298210cc5517..57b57606df4b 100644 --- a/yarn-project/circuits.js/src/structs/proof.ts +++ b/yarn-project/circuits.js/src/structs/proof.ts @@ -91,6 +91,13 @@ export class Proof { static fromString(str: string) { return Proof.fromBuffer(Buffer.from(str, 'hex')); } + + /** Returns whether this proof is actually empty. */ + public isEmpty() { + return ( + this.buffer.length === EMPTY_PROOF_SIZE && this.buffer.every(byte => byte === 0) && this.numPublicInputs === 0 + ); + } } /** diff --git a/yarn-project/end-to-end/src/e2e_prover/e2e_prover_test.ts b/yarn-project/end-to-end/src/e2e_prover/e2e_prover_test.ts index 89a833ff216c..182eb4dc1cb3 100644 --- a/yarn-project/end-to-end/src/e2e_prover/e2e_prover_test.ts +++ b/yarn-project/end-to-end/src/e2e_prover/e2e_prover_test.ts @@ -116,7 +116,7 @@ export class FullProverTest { FullProverTest.TOKEN_DECIMALS, ) .send() - .deployed({ proven: true }); + .deployed(); this.logger.verbose(`Token deployed to ${asset.address}`); return { tokenContractAddress: asset.address }; @@ -302,7 +302,7 @@ export class FullProverTest { const { fakeProofsAsset: asset, accounts } = this; const amount = 10000n; - const waitOpts = { proven: true }; + const waitOpts = { proven: false }; this.logger.verbose(`Minting ${amount} publicly...`); await asset.methods.mint_public(accounts[0].address, amount).send().wait(waitOpts); @@ -314,7 +314,7 @@ export class FullProverTest { await this.addPendingShieldNoteToPXE(0, amount, secretHash, receipt.txHash); const txClaim = asset.methods.redeem_shield(accounts[0].address, amount, secret).send(); - await txClaim.wait({ debug: true, proven: true }); + await txClaim.wait({ ...waitOpts, debug: true }); this.logger.verbose(`Minting complete.`); return { amount }; diff --git a/yarn-project/end-to-end/src/e2e_prover/full.test.ts b/yarn-project/end-to-end/src/e2e_prover/full.test.ts index f6d52aabb76f..01bd0436de7b 100644 --- a/yarn-project/end-to-end/src/e2e_prover/full.test.ts +++ b/yarn-project/end-to-end/src/e2e_prover/full.test.ts @@ -58,12 +58,14 @@ describe('full_prover', () => { logger.info(`Verifying private kernel tail proof`); await expect(t.circuitProofVerifier?.verifyProof(privateTx)).resolves.not.toThrow(); - const sentPrivateTx = privateInteraction.send({ skipPublicSimulation: true }); - const sentPublicTx = publicInteraction.send({ skipPublicSimulation: true }); - await Promise.all([ - sentPrivateTx.wait({ timeout: 60, interval: 10, proven: true, provenTimeout: 1200 }), - sentPublicTx.wait({ timeout: 60, interval: 10, proven: true, provenTimeout: 1200 }), - ]); + // WARN: The following depends on the epoch boundaries to work + logger.info(`Sending first tx and awaiting it to be mined`); + await privateInteraction.send({ skipPublicSimulation: true }).wait({ timeout: 60, interval: 10 }); + logger.info(`Sending second tx and awaiting it to be proven`); + await publicInteraction + .send({ skipPublicSimulation: true }) + .wait({ timeout: 60, interval: 10, proven: true, provenTimeout: 1200 }); + tokenSim.transferPrivate(accounts[0].address, accounts[1].address, privateSendAmount); tokenSim.transferPublic(accounts[0].address, accounts[1].address, publicSendAmount); diff --git a/yarn-project/end-to-end/src/e2e_prover_node.test.ts b/yarn-project/end-to-end/src/e2e_prover_node.test.ts index efab32e13fda..fdda08b7c2f5 100644 --- a/yarn-project/end-to-end/src/e2e_prover_node.test.ts +++ b/yarn-project/end-to-end/src/e2e_prover_node.test.ts @@ -60,7 +60,7 @@ describe('e2e_prover_node', () => { }, ); - await snapshotManager.snapshot('setup', addAccounts(2, logger), async ({ accountKeys }, ctx) => { + await snapshotManager.snapshot('setup', addAccounts(2, logger, false), async ({ accountKeys }, ctx) => { const accountManagers = accountKeys.map(ak => getSchnorrAccount(ctx.pxe, ak[0], ak[1], 1)); await Promise.all(accountManagers.map(a => a.register())); const wallets = await Promise.all(accountManagers.map(a => a.getWallet())); @@ -72,6 +72,7 @@ describe('e2e_prover_node', () => { await snapshotManager.snapshot( 'deploy-test-contract', async () => { + logger.info(`Deploying test contract`); const owner = wallet.getAddress(); const contract = await StatefulTestContract.deploy(wallet, owner, owner, 42).send().deployed(); return { contractAddress: contract.address }; @@ -84,33 +85,43 @@ describe('e2e_prover_node', () => { ctx = await snapshotManager.setup(); }); - it('submits three blocks, then prover proves the first two', async () => { + it('submits five blocks, then prover proves the first two epochs', async () => { // wait for the proven chain to catch up with the pending chain before we shut off the prover node + logger.info(`Waiting for proven chain to catch up with pending chain`); await waitForProvenChain(ctx.aztecNode); // Stop the current prover node await ctx.proverNode.stop(); + logger.info(`Sending txs`); const msgSender = ctx.deployL1ContractsValues.walletClient.account.address; const txReceipt1 = await msgTestContract.methods .consume_message_from_arbitrary_sender_private(msgContent, msgSecret, EthAddress.fromString(msgSender)) .send() .wait(); + logger.info(`Tx #1 ${txReceipt1.txHash} mined in ${txReceipt1.blockNumber}`); const txReceipt2 = await contract.methods.create_note(recipient, recipient, 10).send().wait(); + logger.info(`Tx #2 ${txReceipt2.txHash} mined in ${txReceipt2.blockNumber}`); const txReceipt3 = await contract.methods.increment_public_value(recipient, 20).send().wait(); - - // Check everything went well during setup and txs were mined in two different blocks - const firstBlock = txReceipt1.blockNumber!; - const secondBlock = firstBlock + 1; - expect(txReceipt2.blockNumber).toEqual(secondBlock); - expect(txReceipt3.blockNumber).toEqual(firstBlock + 2); - expect(await contract.methods.get_public_value(recipient).simulate()).toEqual(20n); - expect(await contract.methods.summed_values(recipient).simulate()).toEqual(10n); + logger.info(`Tx #3 ${txReceipt3.txHash} mined in ${txReceipt3.blockNumber}`); + const txReceipt4 = await contract.methods.create_note(recipient, recipient, 30).send().wait(); + logger.info(`Tx #4 ${txReceipt4.txHash} mined in ${txReceipt4.blockNumber}`); + const txReceipt5 = await contract.methods.increment_public_value(recipient, 40).send().wait(); + logger.info(`Tx #5 ${txReceipt5.txHash} mined in ${txReceipt5.blockNumber}`); + + // Check everything went well during setup and txs were mined in different blocks + const startBlock = txReceipt1.blockNumber!; + expect(txReceipt2.blockNumber).toEqual(startBlock + 1); + expect(txReceipt3.blockNumber).toEqual(startBlock + 2); + expect(txReceipt4.blockNumber).toEqual(startBlock + 3); + expect(txReceipt5.blockNumber).toEqual(startBlock + 4); + expect(await contract.methods.get_public_value(recipient).simulate()).toEqual(60n); + expect(await contract.methods.summed_values(recipient).simulate()).toEqual(40n); // Kick off a prover node await sleep(1000); const proverId = Fr.fromString(Buffer.from('awesome-prover', 'utf-8').toString('hex')); - logger.info(`Creating prover node ${proverId.toString()}`); + logger.info(`Creating prover node with prover id ${proverId.toString()}`); // HACK: We have to use the existing archiver to fetch L2 data, since anvil's chain dump/load used by the // snapshot manager does not include events nor txs, so a new archiver would not "see" old blocks. const proverConfig: ProverNodeConfig = { @@ -119,34 +130,34 @@ describe('e2e_prover_node', () => { dataDirectory: undefined, proverId, proverNodeMaxPendingJobs: 100, + proverNodeEpochSize: 2, }; const archiver = ctx.aztecNode.getBlockSource() as Archiver; const proverNode = await createProverNode(proverConfig, { aztecNodeTxProvider: ctx.aztecNode, archiver }); - // Prove the first two blocks simultaneously - logger.info(`Starting proof for first block #${firstBlock}`); - await proverNode.startProof(firstBlock, firstBlock); - logger.info(`Starting proof for second block #${secondBlock}`); - await proverNode.startProof(secondBlock, secondBlock); + // Prove the first two epochs simultaneously + logger.info(`Starting proof for first epoch ${startBlock}-${startBlock + 1}`); + await proverNode.startProof(startBlock, startBlock + 1); + logger.info(`Starting proof for second epoch ${startBlock + 2}-${startBlock + 3}`); + await proverNode.startProof(startBlock + 2, startBlock + 3); // Confirm that we cannot go back to prove an old one - await expect(proverNode.startProof(firstBlock, firstBlock)).rejects.toThrow(/behind the current world state/i); + await expect(proverNode.startProof(startBlock, startBlock + 1)).rejects.toThrow(/behind the current world state/i); // Await until proofs get submitted - await waitForProvenChain(ctx.aztecNode, secondBlock); - expect(await ctx.aztecNode.getProvenBlockNumber()).toEqual(secondBlock); + await waitForProvenChain(ctx.aztecNode, startBlock + 3); + expect(await ctx.aztecNode.getProvenBlockNumber()).toEqual(startBlock + 3); // Check that the prover id made it to the emitted event const { publicClient, l1ContractAddresses } = ctx.deployL1ContractsValues; const logs = await retrieveL2ProofVerifiedEvents(publicClient, l1ContractAddresses.rollupAddress, 1n); - expect(logs.length).toEqual(secondBlock); - - const expectedBlockNumbers = [firstBlock, secondBlock]; - const logsSlice = logs.slice(firstBlock - 1); - for (let i = 0; i < 2; i++) { - const log = logsSlice[i]; - expect(log.l2BlockNumber).toEqual(BigInt(expectedBlockNumbers[i])); - expect(log.proverId.toString()).toEqual(proverId.toString()); - } + + // Logs for first epoch + expect(logs[logs.length - 2].l2BlockNumber).toEqual(BigInt(startBlock + 1)); + expect(logs[logs.length - 2].proverId.toString()).toEqual(proverId.toString()); + + // Logs for 2nd epoch + expect(logs[logs.length - 1].l2BlockNumber).toEqual(BigInt(startBlock + 3)); + expect(logs[logs.length - 1].proverId.toString()).toEqual(proverId.toString()); }); }); diff --git a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts index bb1af6a9da5f..f8c4ef4eb1b5 100644 --- a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts +++ b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts @@ -33,6 +33,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; import { copySync, removeSync } from 'fs-extra/esm'; import getPort from 'get-port'; import { join } from 'path'; +import { type Hex } from 'viem'; import { mnemonicToAccount } from 'viem/accounts'; import { MNEMONIC } from './fixtures.js'; @@ -420,9 +421,10 @@ async function setupFromState(statePath: string, logger: Logger): Promise + (numberOfAccounts: number, logger: DebugLogger, waitUntilProven = false) => async ({ pxe }: { pxe: PXE }) => { // Generate account keys. const accountKeys: [Fr, GrumpkinScalar][] = Array.from({ length: numberOfAccounts }).map(_ => [ diff --git a/yarn-project/end-to-end/src/public-testnet/e2e_public_testnet_transfer.test.ts b/yarn-project/end-to-end/src/public-testnet/e2e_public_testnet_transfer.test.ts index 27a86df27873..08636379f646 100644 --- a/yarn-project/end-to-end/src/public-testnet/e2e_public_testnet_transfer.test.ts +++ b/yarn-project/end-to-end/src/public-testnet/e2e_public_testnet_transfer.test.ts @@ -60,12 +60,7 @@ describe(`deploys and transfers a private only token`, () => { logger.info(`Deploying accounts.`); - const accounts = await createAccounts(pxe, 2, [secretKey1, secretKey2], { - interval: 0.1, - proven: true, - provenTimeout: 600, - timeout: 300, - }); + const accounts = await createAccounts(pxe, 2, [secretKey1, secretKey2], { interval: 0.1, timeout: 300 }); logger.info(`Accounts deployed, deploying token.`); @@ -84,18 +79,14 @@ describe(`deploys and transfers a private only token`, () => { skipInitialization: false, skipPublicSimulation: true, }) - .deployed({ - proven: true, - provenTimeout: 600, - timeout: 300, - }); + .deployed({ timeout: 300 }); logger.info(`Performing transfer.`); await token.methods .transfer(transferValue, deployerWallet.getAddress(), recipientWallet.getAddress(), deployerWallet.getAddress()) .send() - .wait({ proven: true, provenTimeout: 600, timeout: 300 }); + .wait({ timeout: 300 }); logger.info(`Transfer completed`); diff --git a/yarn-project/foundation/src/collection/array.ts b/yarn-project/foundation/src/collection/array.ts index 6f2262a1af3c..b703e66a1196 100644 --- a/yarn-project/foundation/src/collection/array.ts +++ b/yarn-project/foundation/src/collection/array.ts @@ -100,3 +100,18 @@ export function unique(arr: T[]): T[] { export function compactArray(arr: (T | undefined)[]): T[] { return arr.filter((x: T | undefined): x is T => x !== undefined); } + +/** + * Returns whether two arrays are equal. The arrays are equal if they have the same length and all elements are equal. + */ +export function areArraysEqual(a: T[], b: T[], eq: (a: T, b: T) => boolean = (a: T, b: T) => a === b): boolean { + if (a.length !== b.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (!eq(a[i], b[i])) { + return false; + } + } + return true; +} diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index 5183e2847ed6..a526c1b9eb44 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -126,6 +126,7 @@ export type EnvVar = | 'VALIDATOR_ATTESTATIONS_POOLING_INTERVAL_MS' | 'PROVER_NODE_DISABLE_AUTOMATIC_PROVING' | 'PROVER_NODE_MAX_PENDING_JOBS' + | 'PROVER_NODE_EPOCH_SIZE' | 'PROOF_VERIFIER_POLL_INTERVAL_MS' | 'PROOF_VERIFIER_L1_START_BLOCK' | 'LOG_LEVEL' diff --git a/yarn-project/foundation/src/types/index.ts b/yarn-project/foundation/src/types/index.ts index 71872c1d99f0..05b4a76a785e 100644 --- a/yarn-project/foundation/src/types/index.ts +++ b/yarn-project/foundation/src/types/index.ts @@ -15,3 +15,8 @@ export type PartialBy = Omit & Partial>; /** Removes readonly modifiers for a type. */ export type Writeable = { -readonly [P in keyof T]: T[P] }; + +/** Removes readonly modifiers for an object. */ +export function unfreeze(obj: T): Writeable { + return obj as Writeable; +} diff --git a/yarn-project/prover-client/src/index.ts b/yarn-project/prover-client/src/index.ts index 1945a792047c..36affdfba2ae 100644 --- a/yarn-project/prover-client/src/index.ts +++ b/yarn-project/prover-client/src/index.ts @@ -1,4 +1,4 @@ -export { ProverClient } from '@aztec/circuit-types'; +export { EpochProverManager } from '@aztec/circuit-types'; export * from './tx-prover/tx-prover.js'; export * from './config.js'; diff --git a/yarn-project/prover-client/src/mocks/test_context.ts b/yarn-project/prover-client/src/mocks/test_context.ts index c3e709fb6d38..8ccd8bba506d 100644 --- a/yarn-project/prover-client/src/mocks/test_context.ts +++ b/yarn-project/prover-client/src/mocks/test_context.ts @@ -8,7 +8,7 @@ import { type Tx, type TxValidator, } from '@aztec/circuit-types'; -import { type Gas, GlobalVariables, Header, type Nullifier, type TxContext } from '@aztec/circuits.js'; +import { type Gas, type GlobalVariables, Header, type Nullifier, type TxContext } from '@aztec/circuits.js'; import { type Fr } from '@aztec/foundation/fields'; import { type DebugLogger } from '@aztec/foundation/log'; import { openTmpStore } from '@aztec/kv-store/utils'; @@ -89,7 +89,7 @@ export class TestContext { actualDb, publicExecutor, publicKernel, - GlobalVariables.empty(), + globalVariables, Header.empty(), worldStateDB, telemetry, diff --git a/yarn-project/prover-client/src/orchestrator/orchestrator.ts b/yarn-project/prover-client/src/orchestrator/orchestrator.ts index 63cf099f51ae..d6406a493329 100644 --- a/yarn-project/prover-client/src/orchestrator/orchestrator.ts +++ b/yarn-project/prover-client/src/orchestrator/orchestrator.ts @@ -134,19 +134,11 @@ export class ProvingOrchestrator implements EpochProver { this.paddingTx = undefined; } - @trackSpan('ProvingOrchestrator.startNewEpoch', (epochNumber, totalNumBlocks) => ({ - [Attributes.EPOCH_SIZE]: totalNumBlocks, - [Attributes.EPOCH_NUMBER]: epochNumber, - })) public startNewEpoch(epochNumber: number, totalNumBlocks: number): ProvingTicket { const { promise: _promise, resolve, reject } = promiseWithResolvers(); - const promise = _promise.catch( - (reason): ProvingResult => ({ - status: PROVING_STATUS.FAILURE, - reason, - }), - ); + const promise = _promise.catch((reason): ProvingResult => ({ status: PROVING_STATUS.FAILURE, reason })); + logger.info(`Starting epoch ${epochNumber} with ${totalNumBlocks} blocks`); this.provingState = new EpochProvingState(epochNumber, totalNumBlocks, resolve, reject); return { provingPromise: promise }; } @@ -1032,6 +1024,7 @@ export class ProvingOrchestrator implements EpochProver { signal => this.prover.getRootRollupProof(inputs, signal, provingState.epochNumber), ), result => { + logger.verbose(`Orchestrator completed root rollup for epoch ${provingState.epochNumber}`); provingState.rootRollupPublicInputs = result.inputs; provingState.finalProof = result.proof.binaryProof; provingState.resolve({ status: PROVING_STATUS.SUCCESS }); diff --git a/yarn-project/prover-client/src/test/bb_prover_full_rollup.test.ts b/yarn-project/prover-client/src/test/bb_prover_full_rollup.test.ts index fdecf4270cab..205027a729e7 100644 --- a/yarn-project/prover-client/src/test/bb_prover_full_rollup.test.ts +++ b/yarn-project/prover-client/src/test/bb_prover_full_rollup.test.ts @@ -7,6 +7,7 @@ import { type DebugLogger, createDebugLogger } from '@aztec/foundation/log'; import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types'; import { NoopTelemetryClient } from '@aztec/telemetry-client/noop'; +import { makeGlobals } from '../mocks/fixtures.js'; import { TestContext } from '../mocks/test_context.js'; describe('prover/bb_prover/full-rollup', () => { @@ -27,46 +28,44 @@ describe('prover/bb_prover/full-rollup', () => { await context.cleanup(); }); - it('proves a private-only rollup full of empty txs', async () => { + it('proves a private-only epoch full of empty txs', async () => { + const totalBlocks = 2; const totalTxs = 2; const nonEmptyTxs = 0; - logger.info(`Proving a private-only full rollup with ${nonEmptyTxs}/${totalTxs} non-empty transactions`); + logger.info(`Proving a full epoch with ${totalBlocks} blocks with ${nonEmptyTxs}/${totalTxs} non-empty txs each`); + const initialHeader = context.actualDb.getInitialHeader(); - const txs = times(nonEmptyTxs, (i: number) => { - const tx = mockTx(1000 * (i + 1), { - numberOfNonRevertiblePublicCallRequests: 0, - numberOfRevertiblePublicCallRequests: 0, + const provingTicket = context.orchestrator.startNewEpoch(1, totalBlocks); + + for (let blockNum = 1; blockNum <= totalBlocks; blockNum++) { + const globals = makeGlobals(blockNum); + const l1ToL2Messages = makeTuple(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, Fr.random); + const txs = times(nonEmptyTxs, (i: number) => { + const txOpts = { numberOfNonRevertiblePublicCallRequests: 0, numberOfRevertiblePublicCallRequests: 0 }; + const tx = mockTx(blockNum * 100_000 + 1000 * (i + 1), txOpts); + tx.data.constants.historicalHeader = initialHeader; + tx.data.constants.vkTreeRoot = getVKTreeRoot(); + return tx; }); - tx.data.constants.historicalHeader = initialHeader; - tx.data.constants.vkTreeRoot = getVKTreeRoot(); - return tx; - }); - const l1ToL2Messages = makeTuple( - NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, - Fr.random, - ); - - logger.info(`Starting new block`); - const provingTicket = await context.orchestrator.startNewBlock(totalTxs, context.globalVariables, l1ToL2Messages); + logger.info(`Starting new block #${blockNum}`); + await context.orchestrator.startNewBlock(totalTxs, globals, l1ToL2Messages); + logger.info(`Processing public functions`); + const [processed, failed] = await context.processPublicFunctions(txs, nonEmptyTxs, context.blockProver); + expect(processed.length).toBe(nonEmptyTxs); + expect(failed.length).toBe(0); - logger.info(`Processing public functions`); - const [processed, failed] = await context.processPublicFunctions(txs, nonEmptyTxs, context.blockProver); - expect(processed.length).toBe(nonEmptyTxs); - expect(failed.length).toBe(0); - - logger.info(`Setting block as completed`); - await context.orchestrator.setBlockCompleted(); + logger.info(`Setting block as completed`); + await context.orchestrator.setBlockCompleted(); + } + logger.info(`Awaiting proofs`); const provingResult = await provingTicket.provingPromise; - expect(provingResult.status).toBe(PROVING_STATUS.SUCCESS); + const epochResult = context.orchestrator.finaliseEpoch(); - logger.info(`Finalising block`); - const blockResult = await context.orchestrator.finaliseBlock(); - - await expect(prover.verifyProof('BlockRootRollupFinalArtifact', blockResult.proof)).resolves.not.toThrow(); + await expect(prover.verifyProof('RootRollupArtifact', epochResult.proof)).resolves.not.toThrow(); }); // TODO(@PhilWindle): Remove public functions and re-enable once we can handle empty tx slots diff --git a/yarn-project/prover-client/src/tx-prover/tx-prover.ts b/yarn-project/prover-client/src/tx-prover/tx-prover.ts index a6dc55feb8b9..23a10f54f83e 100644 --- a/yarn-project/prover-client/src/tx-prover/tx-prover.ts +++ b/yarn-project/prover-client/src/tx-prover/tx-prover.ts @@ -1,7 +1,7 @@ import { BBNativeRollupProver, TestCircuitProver } from '@aztec/bb-prover'; import { - type BlockProver, - type ProverClient, + type EpochProver, + type EpochProverManager, type ProvingJobSource, type ServerCircuitProver, } from '@aztec/circuit-types/interfaces'; @@ -19,7 +19,7 @@ import { ProverAgent } from '../prover-agent/prover-agent.js'; * A prover factory. * TODO(palla/prover-node): Rename this class */ -export class TxProver implements ProverClient { +export class TxProver implements EpochProverManager { private queue: MemoryProvingQueue; private running = false; @@ -33,7 +33,7 @@ export class TxProver implements ProverClient { this.queue = new MemoryProvingQueue(telemetry, config.proverJobTimeoutMs, config.proverJobPollIntervalMs); } - public createBlockProver(db: MerkleTreeOperations): BlockProver { + public createEpochProver(db: MerkleTreeOperations): EpochProver { return new ProvingOrchestrator(db, this.queue, this.telemetry, this.config.proverId); } diff --git a/yarn-project/prover-node/src/config.ts b/yarn-project/prover-node/src/config.ts index d7229c80afe5..9ac8ecb7b0d6 100644 --- a/yarn-project/prover-node/src/config.ts +++ b/yarn-project/prover-node/src/config.ts @@ -26,10 +26,11 @@ export type ProverNodeConfig = ArchiverConfig & TxProviderConfig & { proverNodeDisableAutomaticProving?: boolean; proverNodeMaxPendingJobs?: number; + proverNodeEpochSize?: number; }; const specificProverNodeConfigMappings: ConfigMappingsType< - Pick + Pick > = { proverNodeDisableAutomaticProving: { env: 'PROVER_NODE_DISABLE_AUTOMATIC_PROVING', @@ -41,6 +42,11 @@ const specificProverNodeConfigMappings: ConfigMappingsType< description: 'The maximum number of pending jobs for the prover node', ...numberConfigHelper(100), }, + proverNodeEpochSize: { + env: 'PROVER_NODE_EPOCH_SIZE', + description: 'The number of blocks to prove in a single epoch', + ...numberConfigHelper(2), + }, }; export const proverNodeConfigMappings: ConfigMappingsType = { diff --git a/yarn-project/prover-node/src/factory.ts b/yarn-project/prover-node/src/factory.ts index 7e9c31e8cbfd..e6dd1e3524b0 100644 --- a/yarn-project/prover-node/src/factory.ts +++ b/yarn-project/prover-node/src/factory.ts @@ -56,6 +56,7 @@ export async function createProverNode( { disableAutomaticProving: config.proverNodeDisableAutomaticProving, maxPendingJobs: config.proverNodeMaxPendingJobs, + epochSize: config.proverNodeEpochSize, }, ); } diff --git a/yarn-project/prover-node/src/job/block-proving-job.ts b/yarn-project/prover-node/src/job/epoch-proving-job.ts similarity index 68% rename from yarn-project/prover-node/src/job/block-proving-job.ts rename to yarn-project/prover-node/src/job/epoch-proving-job.ts index 4c11f152210d..424801b6d960 100644 --- a/yarn-project/prover-node/src/job/block-proving-job.ts +++ b/yarn-project/prover-node/src/job/epoch-proving-job.ts @@ -1,6 +1,6 @@ import { - type BlockProver, EmptyTxValidator, + type EpochProver, type L1ToL2MessageSource, type L2Block, type L2BlockSource, @@ -24,20 +24,20 @@ import { type ProverNodeMetrics } from '../metrics.js'; * re-executes their public calls, generates a rollup proof, and submits it to L1. This job will update the * world state as part of public call execution via the public processor. */ -export class BlockProvingJob { - private state: BlockProvingJobState = 'initialized'; - private log = createDebugLogger('aztec:block-proving-job'); +export class EpochProvingJob { + private state: EpochProvingJobState = 'initialized'; + private log = createDebugLogger('aztec:epoch-proving-job'); private uuid: string; constructor( - private prover: BlockProver, + private prover: EpochProver, private publicProcessorFactory: PublicProcessorFactory, private publisher: L1Publisher, private l2BlockSource: L2BlockSource, private l1ToL2MessageSource: L1ToL2MessageSource, private txProvider: TxProvider, private metrics: ProverNodeMetrics, - private cleanUp: (job: BlockProvingJob) => Promise = () => Promise.resolve(), + private cleanUp: (job: EpochProvingJob) => Promise = () => Promise.resolve(), ) { this.uuid = crypto.randomUUID(); } @@ -46,26 +46,38 @@ export class BlockProvingJob { return this.uuid; } - public getState(): BlockProvingJobState { + public getState(): EpochProvingJobState { return this.state; } + /** + * Proves the given block range and submits the proof to L1. + * @param fromBlock - Start block. + * @param toBlock - Last block (inclusive). + */ public async run(fromBlock: number, toBlock: number) { - if (fromBlock !== toBlock) { - throw new Error(`Block ranges are not yet supported`); + if (fromBlock > toBlock) { + throw new Error(`Invalid block range: ${fromBlock} to ${toBlock}`); } - this.log.info(`Starting block proving job`, { fromBlock, toBlock, uuid: this.uuid }); + const epochNumber = fromBlock; // Use starting block number as epoch number + const epochSize = toBlock - fromBlock + 1; + this.log.info(`Starting epoch proving job`, { fromBlock, toBlock, epochNumber, uuid: this.uuid }); this.state = 'processing'; const timer = new Timer(); + try { - let historicalHeader = (await this.l2BlockSource.getBlock(fromBlock - 1))?.header; + const provingTicket = this.prover.startNewEpoch(epochNumber, epochSize); + let previousHeader = (await this.l2BlockSource.getBlock(fromBlock - 1))?.header; + for (let blockNumber = fromBlock; blockNumber <= toBlock; blockNumber++) { + // Gather all data to prove this block const block = await this.getBlock(blockNumber); const globalVariables = block.header.globalVariables; const txHashes = block.body.txEffects.map(tx => tx.txHash); const txCount = block.body.numberOfTxsIncludingPadded; const l1ToL2Messages = await this.getL1ToL2Messages(block); + const txs = await this.getTxs(txHashes); this.log.verbose(`Starting block processing`, { number: block.number, @@ -74,54 +86,45 @@ export class BlockProvingJob { noteHashTreeRoot: block.header.state.partial.noteHashTree.root, nullifierTreeRoot: block.header.state.partial.nullifierTree.root, publicDataTreeRoot: block.header.state.partial.publicDataTree.root, - historicalHeader: historicalHeader?.hash(), + previousHeader: previousHeader?.hash(), uuid: this.uuid, ...globalVariables, }); - // When we move to proving epochs, this should change into a startNewEpoch and be lifted outside the loop. - const provingTicket = await this.prover.startNewBlock(txCount, globalVariables, l1ToL2Messages); + // Start block proving + await this.prover.startNewBlock(txCount, globalVariables, l1ToL2Messages); - const publicProcessor = this.publicProcessorFactory.create(historicalHeader, globalVariables); - - const txs = await this.getTxs(txHashes); + // Process public fns + const publicProcessor = this.publicProcessorFactory.create(previousHeader, globalVariables); await this.processTxs(publicProcessor, txs, txCount); - this.log.verbose(`Processed all txs for block`, { blockNumber: block.number, blockHash: block.hash().toString(), uuid: this.uuid, }); + // Mark block as completed and update archive tree await this.prover.setBlockCompleted(); + previousHeader = block.header; + } - // This should be moved outside the loop to match the creation of the proving ticket when we move to epochs. - this.state = 'awaiting-prover'; - const result = await provingTicket.provingPromise; - if (result.status === PROVING_STATUS.FAILURE) { - throw new Error(`Block proving failed: ${result.reason}`); - } - - historicalHeader = block.header; + this.state = 'awaiting-prover'; + const result = await provingTicket.provingPromise; + if (result.status === PROVING_STATUS.FAILURE) { + throw new Error(`Epoch proving failed: ${result.reason}`); } - const { block, aggregationObject, proof } = await this.prover.finaliseBlock(); - this.log.info(`Finalised proof for block range`, { fromBlock, toBlock, uuid: this.uuid }); + const { publicInputs, proof } = this.prover.finaliseEpoch(); + this.log.info(`Finalised proof for epoch`, { epochNumber, fromBlock, toBlock, uuid: this.uuid }); this.state = 'publishing-proof'; - await this.publisher.submitBlockProof( - block.header, - block.archive.root, - this.prover.getProverId(), - aggregationObject, - proof, - ); - this.log.info(`Submitted proof for block range`, { fromBlock, toBlock, uuid: this.uuid }); + await this.publisher.submitEpochProof({ epochNumber, fromBlock, toBlock, publicInputs, proof }); + this.log.info(`Submitted proof for epoch`, { epochNumber, fromBlock, toBlock, uuid: this.uuid }); this.state = 'completed'; this.metrics.recordProvingJob(timer); } catch (err) { - this.log.error(`Error running block prover job`, err, { uuid: this.uuid }); + this.log.error(`Error running epoch prover job`, err, { uuid: this.uuid }); this.state = 'failed'; } finally { await this.cleanUp(this); @@ -177,7 +180,7 @@ export class BlockProvingJob { } } -export type BlockProvingJobState = +export type EpochProvingJobState = | 'initialized' | 'processing' | 'awaiting-prover' diff --git a/yarn-project/prover-node/src/prover-node.test.ts b/yarn-project/prover-node/src/prover-node.test.ts index e5cd74cfa32b..6e52f77a0d4f 100644 --- a/yarn-project/prover-node/src/prover-node.test.ts +++ b/yarn-project/prover-node/src/prover-node.test.ts @@ -1,8 +1,8 @@ import { + type EpochProverManager, type L1ToL2MessageSource, type L2BlockSource, type MerkleTreeAdminOperations, - type ProverClient, type TxProvider, WorldStateRunningState, type WorldStateSynchronizer, @@ -14,11 +14,11 @@ import { type ContractDataSource } from '@aztec/types/contracts'; import { type MockProxy, mock } from 'jest-mock-extended'; -import { type BlockProvingJob } from './job/block-proving-job.js'; +import { type EpochProvingJob } from './job/epoch-proving-job.js'; import { ProverNode } from './prover-node.js'; describe('prover-node', () => { - let prover: MockProxy; + let prover: MockProxy; let publisher: MockProxy; let l2BlockSource: MockProxy; let l1ToL2MessageSource: MockProxy; @@ -31,13 +31,13 @@ describe('prover-node', () => { // List of all jobs ever created by the test prover node and their dependencies let jobs: { - job: MockProxy; - cleanUp: (job: BlockProvingJob) => Promise; + job: MockProxy; + cleanUp: (job: EpochProvingJob) => Promise; db: MerkleTreeAdminOperations; }[]; beforeEach(() => { - prover = mock(); + prover = mock(); publisher = mock(); l2BlockSource = mock(); l1ToL2MessageSource = mock(); @@ -61,7 +61,7 @@ describe('prover-node', () => { txProvider, simulator, telemetryClient, - { maxPendingJobs: 3, pollingIntervalMs: 10 }, + { maxPendingJobs: 3, pollingIntervalMs: 10, epochSize: 2 }, ); }); @@ -82,13 +82,12 @@ describe('prover-node', () => { await proverNode.work(); await proverNode.work(); - expect(jobs.length).toEqual(2); - expect(jobs[0].job.run).toHaveBeenCalledWith(4, 4); - expect(jobs[1].job.run).toHaveBeenCalledWith(5, 5); + expect(jobs.length).toEqual(1); + expect(jobs[0].job.run).toHaveBeenCalledWith(4, 5); }); it('stops proving when maximum jobs are reached', async () => { - setBlockNumbers(10, 3); + setBlockNumbers(20, 3); await proverNode.work(); await proverNode.work(); @@ -96,13 +95,13 @@ describe('prover-node', () => { await proverNode.work(); expect(jobs.length).toEqual(3); - expect(jobs[0].job.run).toHaveBeenCalledWith(4, 4); - expect(jobs[1].job.run).toHaveBeenCalledWith(5, 5); - expect(jobs[2].job.run).toHaveBeenCalledWith(6, 6); + expect(jobs[0].job.run).toHaveBeenCalledWith(4, 5); + expect(jobs[1].job.run).toHaveBeenCalledWith(6, 7); + expect(jobs[2].job.run).toHaveBeenCalledWith(8, 9); }); it('reports on pending jobs', async () => { - setBlockNumbers(5, 3); + setBlockNumbers(8, 3); await proverNode.work(); await proverNode.work(); @@ -116,7 +115,7 @@ describe('prover-node', () => { }); it('cleans up jobs when completed', async () => { - setBlockNumbers(10, 3); + setBlockNumbers(20, 3); await proverNode.work(); await proverNode.work(); @@ -124,10 +123,6 @@ describe('prover-node', () => { await proverNode.work(); expect(jobs.length).toEqual(3); - expect(jobs[0].job.run).toHaveBeenCalledWith(4, 4); - expect(jobs[1].job.run).toHaveBeenCalledWith(5, 5); - expect(jobs[2].job.run).toHaveBeenCalledWith(6, 6); - expect(proverNode.getJobs().length).toEqual(3); // Clean up the first job @@ -138,7 +133,7 @@ describe('prover-node', () => { // Request another job to run and ensure it gets pushed await proverNode.work(); expect(jobs.length).toEqual(4); - expect(jobs[3].job.run).toHaveBeenCalledWith(7, 7); + expect(jobs[3].job.run).toHaveBeenCalledWith(10, 11); expect(proverNode.getJobs().length).toEqual(3); expect(proverNode.getJobs().map(({ uuid }) => uuid)).toEqual(['1', '2', '3']); }); @@ -147,7 +142,7 @@ describe('prover-node', () => { setBlockNumbers(10, 3); // We trigger an error by setting world state past the block that the prover node will try proving - worldState.status.mockResolvedValue({ syncedToL2Block: 5, state: WorldStateRunningState.RUNNING }); + worldState.status.mockResolvedValue({ syncedToL2Block: 7, state: WorldStateRunningState.RUNNING }); // These two calls should return in failures await proverNode.work(); @@ -157,16 +152,16 @@ describe('prover-node', () => { // But now the prover node should move forward await proverNode.work(); expect(jobs.length).toEqual(1); - expect(jobs[0].job.run).toHaveBeenCalledWith(6, 6); + expect(jobs[0].job.run).toHaveBeenCalledWith(8, 9); }); class TestProverNode extends ProverNode { - protected override doCreateBlockProvingJob( + protected override doCreateEpochProvingJob( db: MerkleTreeAdminOperations, _publicProcessorFactory: PublicProcessorFactory, - cleanUp: (job: BlockProvingJob) => Promise, - ): BlockProvingJob { - const job = mock({ getState: () => 'processing' }); + cleanUp: (job: EpochProvingJob) => Promise, + ): EpochProvingJob { + const job = mock({ getState: () => 'processing' }); job.getId.mockReturnValue(jobs.length.toString()); jobs.push({ job, cleanUp, db }); return job; diff --git a/yarn-project/prover-node/src/prover-node.ts b/yarn-project/prover-node/src/prover-node.ts index 02b3a1b44482..d31e05b215a0 100644 --- a/yarn-project/prover-node/src/prover-node.ts +++ b/yarn-project/prover-node/src/prover-node.ts @@ -1,11 +1,12 @@ import { + type EpochProverManager, type L1ToL2MessageSource, type L2BlockSource, type MerkleTreeOperations, - type ProverClient, type TxProvider, type WorldStateSynchronizer, } from '@aztec/circuit-types'; +import { compact } from '@aztec/foundation/collection'; import { createDebugLogger } from '@aztec/foundation/log'; import { RunningPromise } from '@aztec/foundation/running-promise'; import { type L1Publisher } from '@aztec/sequencer-client'; @@ -13,9 +14,16 @@ import { PublicProcessorFactory, type SimulationProvider } from '@aztec/simulato import { type TelemetryClient } from '@aztec/telemetry-client'; import { type ContractDataSource } from '@aztec/types/contracts'; -import { BlockProvingJob, type BlockProvingJobState } from './job/block-proving-job.js'; +import { EpochProvingJob, type EpochProvingJobState } from './job/epoch-proving-job.js'; import { ProverNodeMetrics } from './metrics.js'; +type ProverNodeOptions = { + pollingIntervalMs: number; + disableAutomaticProving: boolean; + maxPendingJobs: number; + epochSize: number; +}; + /** * An Aztec Prover Node is a standalone process that monitors the unfinalised chain on L1 for unproven blocks, * fetches their txs from a tx source in the p2p network or an external node, re-executes their public functions, @@ -25,12 +33,12 @@ export class ProverNode { private log = createDebugLogger('aztec:prover-node'); private runningPromise: RunningPromise | undefined; private latestBlockWeAreProving: number | undefined; - private jobs: Map = new Map(); - private options: { pollingIntervalMs: number; disableAutomaticProving: boolean; maxPendingJobs: number }; + private jobs: Map = new Map(); + private options: ProverNodeOptions; private metrics: ProverNodeMetrics; constructor( - private prover: ProverClient, + private prover: EpochProverManager, private publisher: L1Publisher, private l2BlockSource: L2BlockSource, private l1ToL2MessageSource: L1ToL2MessageSource, @@ -39,13 +47,14 @@ export class ProverNode { private txProvider: TxProvider, private simulator: SimulationProvider, private telemetryClient: TelemetryClient, - options: { pollingIntervalMs?: number; disableAutomaticProving?: boolean; maxPendingJobs?: number } = {}, + options: Partial = {}, ) { this.options = { pollingIntervalMs: 1_000, disableAutomaticProving: false, maxPendingJobs: 100, - ...options, + epochSize: 2, + ...compact(options), }; this.metrics = new ProverNodeMetrics(telemetryClient, 'ProverNode'); @@ -58,7 +67,7 @@ export class ProverNode { start() { this.runningPromise = new RunningPromise(this.work.bind(this), this.options.pollingIntervalMs); this.runningPromise.start(); - this.log.info('Started ProverNode'); + this.log.info('Started ProverNode', this.options); } /** @@ -101,8 +110,8 @@ export class ProverNode { // Consider both the latest block we are proving and the last block proven on the chain const latestBlockBeingProven = this.latestBlockWeAreProving ?? 0; const latestProven = Math.max(latestBlockBeingProven, latestProvenBlockNumber); - if (latestProven >= latestBlockNumber) { - this.log.debug(`No new blocks to prove`, { + if (latestBlockNumber - latestProven < this.options.epochSize) { + this.log.debug(`No epoch to prove`, { latestBlockNumber, latestProvenBlockNumber, latestBlockBeingProven, @@ -111,7 +120,7 @@ export class ProverNode { } const fromBlock = latestProven + 1; - const toBlock = fromBlock; // We only prove one block at a time for now + const toBlock = fromBlock + this.options.epochSize - 1; try { await this.startProof(fromBlock, toBlock); @@ -151,7 +160,7 @@ export class ProverNode { /** * Returns an array of jobs being processed. */ - public getJobs(): { uuid: string; status: BlockProvingJobState }[] { + public getJobs(): { uuid: string; status: EpochProvingJobState }[] { return Array.from(this.jobs.entries()).map(([uuid, job]) => ({ uuid, status: job.getState() })); } @@ -186,19 +195,19 @@ export class ProverNode { this.jobs.delete(job.getId()); }; - const job = this.doCreateBlockProvingJob(db, publicProcessorFactory, cleanUp); + const job = this.doCreateEpochProvingJob(db, publicProcessorFactory, cleanUp); this.jobs.set(job.getId(), job); return job; } /** Extracted for testing purposes. */ - protected doCreateBlockProvingJob( + protected doCreateEpochProvingJob( db: MerkleTreeOperations, publicProcessorFactory: PublicProcessorFactory, cleanUp: () => Promise, ) { - return new BlockProvingJob( - this.prover.createBlockProver(db), + return new EpochProvingJob( + this.prover.createEpochProver(db), publicProcessorFactory, this.publisher, this.l2BlockSource, diff --git a/yarn-project/sequencer-client/src/publisher/index.ts b/yarn-project/sequencer-client/src/publisher/index.ts index e51b4d3cdeae..97e14e962627 100644 --- a/yarn-project/sequencer-client/src/publisher/index.ts +++ b/yarn-project/sequencer-client/src/publisher/index.ts @@ -1,2 +1,2 @@ -export { L1Publisher } from './l1-publisher.js'; +export { L1Publisher, L1SubmitEpochProofArgs } from './l1-publisher.js'; export * from './config.js'; diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts index 447e42b3dcf7..d2abdef39276 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts @@ -1,10 +1,19 @@ import { ConsensusPayload, type L2Block, type TxHash, getHashedSignaturePayload } from '@aztec/circuit-types'; import { type L1PublishBlockStats, type L1PublishProofStats } from '@aztec/circuit-types/stats'; -import { ETHEREUM_SLOT_DURATION, EthAddress, type FeeRecipient, type Header, type Proof } from '@aztec/circuits.js'; +import { + AGGREGATION_OBJECT_LENGTH, + ETHEREUM_SLOT_DURATION, + EthAddress, + type FeeRecipient, + type Header, + type Proof, + type RootRollupPublicInputs, +} from '@aztec/circuits.js'; import { createEthereumChain } from '@aztec/ethereum'; import { makeTuple } from '@aztec/foundation/array'; +import { areArraysEqual, times } from '@aztec/foundation/collection'; import { type Signature } from '@aztec/foundation/eth-signature'; -import { type Fr } from '@aztec/foundation/fields'; +import { Fr } from '@aztec/foundation/fields'; import { createDebugLogger } from '@aztec/foundation/log'; import { type Tuple, serializeToBuffer } from '@aztec/foundation/serialize'; import { InterruptibleSleep } from '@aztec/foundation/sleep'; @@ -95,7 +104,7 @@ type L1SubmitBlockProofArgs = { }; /** Arguments to the submitEpochProof method of the rollup contract */ -type L1SubmitEpochProofArgs = { +export type L1SubmitEpochProofArgs = { epochSize: number; previousArchive: Fr; endArchive: Fr; @@ -106,7 +115,6 @@ type L1SubmitEpochProofArgs = { proverId: Fr; fees: Tuple; proof: Proof; - aggregationObject: Fr[]; }; /** @@ -306,8 +314,10 @@ export class L1Publisher { } this.metrics.recordFailedTx('process'); - - this.log.error(`Rollup.process tx status failed: ${receipt.transactionHash}`, ctx); + this.log.error(`Rollup.process tx status failed ${receipt.transactionHash}`, { + ...ctx, + ...receipt, + }); await this.sleepOrInterrupted(); } @@ -367,13 +377,21 @@ export class L1Publisher { return false; } - public async submitEpochProof( - args: L1SubmitEpochProofArgs, - ctx: { blockNumber: number; slotNumber: number }, - ): Promise { - // Process block + public async submitEpochProof(args: { + epochNumber: number; + fromBlock: number; + toBlock: number; + publicInputs: RootRollupPublicInputs; + proof: Proof; + }): Promise { + const { epochNumber, fromBlock, toBlock } = args; + const ctx = { epochNumber, fromBlock, toBlock }; if (!this.interrupted) { const timer = new Timer(); + + // Validate epoch proof range and hashes are correct before submitting + await this.validateEpochProofSubmission(args); + const txHash = await this.sendSubmitEpochProofTx(args); if (!txHash) { return false; @@ -406,6 +424,63 @@ export class L1Publisher { return false; } + private async validateEpochProofSubmission(args: { + fromBlock: number; + toBlock: number; + publicInputs: RootRollupPublicInputs; + proof: Proof; + }) { + const { fromBlock, toBlock, publicInputs, proof } = args; + + // Check that the block numbers match the expected epoch to be proven + const [pending, proven] = await this.rollupContract.read.tips(); + if (proven !== BigInt(fromBlock) - 1n) { + throw new Error(`Cannot submit epoch proof for ${fromBlock}-${toBlock} as proven block is ${proven}`); + } + if (toBlock > pending) { + throw new Error(`Cannot submit epoch proof for ${fromBlock}-${toBlock} as pending block is ${pending}`); + } + + // Check the block hash and archive for the immediate block before the epoch + const [previousArchive, previousBlockHash] = await this.rollupContract.read.blocks([proven]); + if (publicInputs.previousArchive.root.toString() !== previousArchive) { + throw new Error( + `Previous archive root mismatch: ${publicInputs.previousArchive.root.toString()} !== ${previousArchive}`, + ); + } + // TODO: Remove zero check once we inject the proper zero blockhash + if (previousBlockHash !== Fr.ZERO.toString() && publicInputs.previousBlockHash.toString() !== previousBlockHash) { + throw new Error( + `Previous block hash mismatch: ${publicInputs.previousBlockHash.toString()} !== ${previousBlockHash}`, + ); + } + + // Check the block hash and archive for the last block in the epoch + const [endArchive, endBlockHash] = await this.rollupContract.read.blocks([BigInt(toBlock)]); + if (publicInputs.endArchive.root.toString() !== endArchive) { + throw new Error(`End archive root mismatch: ${publicInputs.endArchive.root.toString()} !== ${endArchive}`); + } + if (publicInputs.endBlockHash.toString() !== endBlockHash) { + throw new Error(`End block hash mismatch: ${publicInputs.endBlockHash.toString()} !== ${endBlockHash}`); + } + + // Compare the public inputs computed by the contract with the ones injected + const rollupPublicInputs = await this.rollupContract.read.getEpochProofPublicInputs( + this.getSubmitEpochProofArgs(args), + ); + const aggregationObject = proof.isEmpty() + ? times(AGGREGATION_OBJECT_LENGTH, Fr.zero) + : proof.extractAggregationObject(); + const argsPublicInputs = [...publicInputs.toFields(), ...aggregationObject]; + + if (!areArraysEqual(rollupPublicInputs.map(Fr.fromString), argsPublicInputs, (a, b) => a.equals(b))) { + const fmt = (inputs: Fr[] | readonly string[]) => inputs.map(x => x.toString()).join(', '); + throw new Error( + `Root rollup public inputs mismatch:\nRollup: ${fmt(rollupPublicInputs)}\nComputed:${fmt(argsPublicInputs)}`, + ); + } + } + /** * Calling `interrupt` will cause any in progress call to `publishRollup` to return `false` asap. * Be warned, the call may return false even if the tx subsequently gets successfully mined. @@ -449,26 +524,15 @@ export class L1Publisher { } } - private async sendSubmitEpochProofTx(args: L1SubmitEpochProofArgs): Promise { + private async sendSubmitEpochProofTx(args: { + fromBlock: number; + toBlock: number; + publicInputs: RootRollupPublicInputs; + proof: Proof; + }): Promise { try { - const txArgs = [ - BigInt(args.epochSize), - [ - args.previousArchive.toString(), - args.endArchive.toString(), - args.previousBlockHash.toString(), - args.endBlockHash.toString(), - args.endTimestamp.toString(), - args.outHash.toString(), - args.proverId.toString(), - ], - makeTuple(64, i => - i % 2 === 0 ? args.fees[i / 2].recipient.toString() : args.fees[(i - 1) / 2].value.toString(), - ), - `0x${serializeToBuffer(args.aggregationObject).toString('hex')}`, - `0x${args.proof.withoutPublicInputs().toString('hex')}`, - ] as const; - + const proofHex: Hex = `0x${args.proof.withoutPublicInputs().toString('hex')}`; + const txArgs = [...this.getSubmitEpochProofArgs(args), proofHex] as const; this.log.info(`SubmitEpochProof proofSize=${args.proof.withoutPublicInputs().length} bytes`); await this.rollupContract.simulate.submitEpochRootProof(txArgs, { account: this.account }); return await this.rollupContract.write.submitEpochRootProof(txArgs, { account: this.account }); @@ -478,6 +542,32 @@ export class L1Publisher { } } + private getSubmitEpochProofArgs(args: { + fromBlock: number; + toBlock: number; + publicInputs: RootRollupPublicInputs; + proof: Proof; + }) { + return [ + BigInt(args.toBlock - args.fromBlock + 1), + [ + args.publicInputs.previousArchive.root.toString(), + args.publicInputs.endArchive.root.toString(), + args.publicInputs.previousBlockHash.toString(), + args.publicInputs.endBlockHash.toString(), + args.publicInputs.endTimestamp.toString(), + args.publicInputs.outHash.toString(), + args.publicInputs.proverId.toString(), + ], + makeTuple(64, i => + i % 2 === 0 + ? args.publicInputs.fees[i / 2].recipient.toField().toString() + : args.publicInputs.fees[(i - 1) / 2].value.toString(), + ), + `0x${serializeToBuffer(args.proof.extractAggregationObject()).toString('hex')}`, + ] as const; + } + private async sendProposeTx(encodedData: L1ProcessArgs): Promise { if (!this.interrupted) { try { From bfca69afc33679a620f82a390f09593c5fb7b86f Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Wed, 25 Sep 2024 19:57:54 -0300 Subject: [PATCH 2/6] Bump timeouts --- yarn-project/end-to-end/src/e2e_prover/full.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn-project/end-to-end/src/e2e_prover/full.test.ts b/yarn-project/end-to-end/src/e2e_prover/full.test.ts index 01bd0436de7b..0bc8650b8bb0 100644 --- a/yarn-project/end-to-end/src/e2e_prover/full.test.ts +++ b/yarn-project/end-to-end/src/e2e_prover/full.test.ts @@ -60,11 +60,11 @@ describe('full_prover', () => { // WARN: The following depends on the epoch boundaries to work logger.info(`Sending first tx and awaiting it to be mined`); - await privateInteraction.send({ skipPublicSimulation: true }).wait({ timeout: 60, interval: 10 }); + await privateInteraction.send({ skipPublicSimulation: true }).wait({ timeout: 300, interval: 10 }); logger.info(`Sending second tx and awaiting it to be proven`); await publicInteraction .send({ skipPublicSimulation: true }) - .wait({ timeout: 60, interval: 10, proven: true, provenTimeout: 1200 }); + .wait({ timeout: 300, interval: 10, proven: true, provenTimeout: 1500 }); tokenSim.transferPrivate(accounts[0].address, accounts[1].address, privateSendAmount); tokenSim.transferPublic(accounts[0].address, accounts[1].address, publicSendAmount); From 69f2f4830c04ea89834a2afcac91e639fe254bd5 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Wed, 25 Sep 2024 22:23:08 -0300 Subject: [PATCH 3/6] Try fixing e2e prover full --- yarn-project/end-to-end/src/e2e_prover/e2e_prover_test.ts | 4 ++++ yarn-project/end-to-end/src/e2e_prover/full.test.ts | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/yarn-project/end-to-end/src/e2e_prover/e2e_prover_test.ts b/yarn-project/end-to-end/src/e2e_prover/e2e_prover_test.ts index 182eb4dc1cb3..394de73b78eb 100644 --- a/yarn-project/end-to-end/src/e2e_prover/e2e_prover_test.ts +++ b/yarn-project/end-to-end/src/e2e_prover/e2e_prover_test.ts @@ -172,7 +172,11 @@ export class FullProverTest { minTxsPerBlock: this.minNumberOfTxsPerBlock, }); } else { + this.logger.debug(`Configuring the node min txs per block ${this.minNumberOfTxsPerBlock}...`); this.circuitProofVerifier = new TestCircuitVerifier(); + await this.aztecNode.setConfig({ + minTxsPerBlock: this.minNumberOfTxsPerBlock, + }); } this.logger.debug(`Main setup completed, initializing full prover PXE, Node, and Prover Node...`); diff --git a/yarn-project/end-to-end/src/e2e_prover/full.test.ts b/yarn-project/end-to-end/src/e2e_prover/full.test.ts index 0bc8650b8bb0..94b0299e3dc1 100644 --- a/yarn-project/end-to-end/src/e2e_prover/full.test.ts +++ b/yarn-project/end-to-end/src/e2e_prover/full.test.ts @@ -9,7 +9,7 @@ process.env.AVM_PROVING_STRICT = '1'; describe('full_prover', () => { const realProofs = !['true', '1'].includes(process.env.FAKE_PROOFS ?? ''); - const t = new FullProverTest('full_prover', 2, realProofs); + const t = new FullProverTest('full_prover', 1, realProofs); let { provenAssets, accounts, tokenSim, logger } = t; beforeAll(async () => { @@ -58,7 +58,11 @@ describe('full_prover', () => { logger.info(`Verifying private kernel tail proof`); await expect(t.circuitProofVerifier?.verifyProof(privateTx)).resolves.not.toThrow(); - // WARN: The following depends on the epoch boundaries to work + // TODO(palla/prover): The following depends on the epoch boundaries to work. It assumes that we're proving + // 2-block epochs, and a new epoch is starting now, so the 2nd tx will land on the last block of the epoch and + // get proven. That relies on how many blocks we mined before getting here. + // We can make this more robust when we add padding, set 1-block epochs, and rollback the test config to + // have a min of 2 txs per block, so these both land on the same block. logger.info(`Sending first tx and awaiting it to be mined`); await privateInteraction.send({ skipPublicSimulation: true }).wait({ timeout: 300, interval: 10 }); logger.info(`Sending second tx and awaiting it to be proven`); From 3c0cf02b976607ccdda5c69e05f455ede3745ee0 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 26 Sep 2024 10:08:44 -0300 Subject: [PATCH 4/6] Fix aggregation object --- l1-contracts/src/core/Rollup.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index ee9b23d30eb2..c4710d6a01fd 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -629,7 +629,7 @@ contract Rollup is Leonidas, IRollup, ITestRollup { assembly { part := calldataload(add(_aggregationObject.offset, mul(i, 32))) } - publicInputs[i + 43] = part; + publicInputs[i + 75] = part; } return publicInputs; From 7a11c7b1d667de519c9b916793a2f6b7e4b6902c Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 26 Sep 2024 14:38:13 -0300 Subject: [PATCH 5/6] Add separate epoch verifier to L1 contract --- l1-contracts/src/core/Rollup.sol | 28 ++++++-- l1-contracts/src/core/interfaces/IRollup.sol | 3 +- yarn-project/bb-prover/src/honk.ts | 12 ++-- yarn-project/bb-prover/src/index.ts | 1 + .../bb-prover/src/verifier/bb_verifier.ts | 4 +- .../cli/src/cmds/l1/deploy_l1_verifier.ts | 64 +++++++------------ .../integration_proof_verification.test.ts | 1 + .../src/e2e_prover/e2e_prover_test.ts | 64 ++++++++----------- .../ethereum/src/deploy_l1_contracts.ts | 44 +++++++++++++ 9 files changed, 126 insertions(+), 95 deletions(-) diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index c4710d6a01fd..43f2d75ac6b3 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -62,7 +62,7 @@ contract Rollup is Leonidas, IRollup, ITestRollup { IProofCommitmentEscrow public immutable PROOF_COMMITMENT_ESCROW; uint256 public immutable VERSION; IFeeJuicePortal public immutable FEE_JUICE_PORTAL; - IVerifier public verifier; + IVerifier public blockProofVerifier; ChainTips public tips; DataStructures.EpochProofClaim public proofClaim; @@ -80,6 +80,10 @@ contract Rollup is Leonidas, IRollup, ITestRollup { // Testing only. This should be removed eventually. uint256 private assumeProvenThroughBlockNumber; + // Listed at the end of the contract to avoid changing storage slots + // TODO(palla/prover) Drop blockProofVerifier and move this verifier to that slot + IVerifier public epochProofVerifier; + constructor( IRegistry _registry, IFeeJuicePortal _fpcJuicePortal, @@ -87,7 +91,8 @@ contract Rollup is Leonidas, IRollup, ITestRollup { address _ares, address[] memory _validators ) Leonidas(_ares) { - verifier = new MockVerifier(); + blockProofVerifier = new MockVerifier(); + epochProofVerifier = new MockVerifier(); REGISTRY = _registry; FEE_JUICE_PORTAL = _fpcJuicePortal; PROOF_COMMITMENT_ESCROW = new MockProofCommitmentEscrow(); @@ -144,8 +149,19 @@ contract Rollup is Leonidas, IRollup, ITestRollup { * * @param _verifier - The new verifier contract */ - function setVerifier(address _verifier) external override(ITestRollup) onlyOwner { - verifier = IVerifier(_verifier); + function setBlockVerifier(address _verifier) external override(ITestRollup) onlyOwner { + blockProofVerifier = IVerifier(_verifier); + } + + /** + * @notice Set the verifier contract + * + * @dev This is only needed for testing, and should be removed + * + * @param _verifier - The new verifier contract + */ + function setEpochVerifier(address _verifier) external override(ITestRollup) onlyOwner { + epochProofVerifier = IVerifier(_verifier); } /** @@ -410,7 +426,7 @@ contract Rollup is Leonidas, IRollup, ITestRollup { publicInputs[i + 91] = part; } - if (!verifier.verify(_proof, publicInputs)) { + if (!blockProofVerifier.verify(_proof, publicInputs)) { revert Errors.Rollup__InvalidProof(); } @@ -484,7 +500,7 @@ contract Rollup is Leonidas, IRollup, ITestRollup { bytes32[] memory publicInputs = getEpochProofPublicInputs(_epochSize, _args, _fees, _aggregationObject); - if (!verifier.verify(_proof, publicInputs)) { + if (!epochProofVerifier.verify(_proof, publicInputs)) { revert Errors.Rollup__InvalidProof(); } diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index 3ed428cfab86..62aad69681b2 100644 --- a/l1-contracts/src/core/interfaces/IRollup.sol +++ b/l1-contracts/src/core/interfaces/IRollup.sol @@ -11,7 +11,8 @@ import {DataStructures} from "@aztec/core/libraries/DataStructures.sol"; import {Timestamp, Slot, Epoch} from "@aztec/core/libraries/TimeMath.sol"; interface ITestRollup { - function setVerifier(address _verifier) external; + function setBlockVerifier(address _verifier) external; + function setEpochVerifier(address _verifier) external; function setVkTreeRoot(bytes32 _vkTreeRoot) external; function setAssumeProvenThroughBlockNumber(uint256 blockNumber) external; } diff --git a/yarn-project/bb-prover/src/honk.ts b/yarn-project/bb-prover/src/honk.ts index 93c72cae6e82..a01776ff46a7 100644 --- a/yarn-project/bb-prover/src/honk.ts +++ b/yarn-project/bb-prover/src/honk.ts @@ -6,16 +6,16 @@ const UltraKeccakHonkCircuits = [ 'BlockRootRollupFinalArtifact', 'RootRollupArtifact', ] as const satisfies ProtocolArtifact[]; -type UltraKeccakHonkCircuits = (typeof UltraKeccakHonkCircuits)[number]; -type UltraHonkCircuits = Exclude; +export type UltraKeccakHonkProtocolArtifact = (typeof UltraKeccakHonkCircuits)[number]; +export type UltraHonkProtocolArtifact = Exclude; -export function getUltraHonkFlavorForCircuit(artifact: UltraKeccakHonkCircuits): 'ultra_keccak_honk'; -export function getUltraHonkFlavorForCircuit(artifact: UltraHonkCircuits): 'ultra_honk'; +export function getUltraHonkFlavorForCircuit(artifact: UltraKeccakHonkProtocolArtifact): 'ultra_keccak_honk'; +export function getUltraHonkFlavorForCircuit(artifact: UltraHonkProtocolArtifact): 'ultra_honk'; export function getUltraHonkFlavorForCircuit(artifact: ProtocolArtifact): UltraHonkFlavor; export function getUltraHonkFlavorForCircuit(artifact: ProtocolArtifact): UltraHonkFlavor { return isUltraKeccakHonkCircuit(artifact) ? 'ultra_keccak_honk' : 'ultra_honk'; } -function isUltraKeccakHonkCircuit(artifact: ProtocolArtifact): artifact is UltraKeccakHonkCircuits { - return UltraKeccakHonkCircuits.includes(artifact as UltraKeccakHonkCircuits); +function isUltraKeccakHonkCircuit(artifact: ProtocolArtifact): artifact is UltraKeccakHonkProtocolArtifact { + return UltraKeccakHonkCircuits.includes(artifact as UltraKeccakHonkProtocolArtifact); } diff --git a/yarn-project/bb-prover/src/index.ts b/yarn-project/bb-prover/src/index.ts index e8914146199c..0ea93e762630 100644 --- a/yarn-project/bb-prover/src/index.ts +++ b/yarn-project/bb-prover/src/index.ts @@ -3,5 +3,6 @@ export * from './test/index.js'; export * from './verifier/index.js'; export * from './config.js'; export * from './bb/execute.js'; +export * from './honk.js'; export { type ClientProtocolCircuitVerifier } from '@aztec/circuit-types'; diff --git a/yarn-project/bb-prover/src/verifier/bb_verifier.ts b/yarn-project/bb-prover/src/verifier/bb_verifier.ts index 55b16f1b8462..394b582c17f0 100644 --- a/yarn-project/bb-prover/src/verifier/bb_verifier.ts +++ b/yarn-project/bb-prover/src/verifier/bb_verifier.ts @@ -22,7 +22,7 @@ import { verifyProof, } from '../bb/execute.js'; import { type BBConfig } from '../config.js'; -import { getUltraHonkFlavorForCircuit } from '../honk.js'; +import { type UltraKeccakHonkProtocolArtifact, getUltraHonkFlavorForCircuit } from '../honk.js'; import { mapProtocolArtifactNameToCircuitName } from '../stats.js'; import { extractVkData } from '../verification_key/verification_key_data.js'; @@ -127,7 +127,7 @@ export class BBCircuitVerifier implements ClientProtocolCircuitVerifier { await runInDirectory(this.config.bbWorkingDirectory, operation, this.config.bbSkipCleanup); } - public async generateSolidityContract(circuit: ProtocolArtifact, contractName: string) { + public async generateSolidityContract(circuit: UltraKeccakHonkProtocolArtifact, contractName: string) { const result = await generateContractForCircuit( this.config.bbBinaryPath, this.config.bbWorkingDirectory, diff --git a/yarn-project/cli/src/cmds/l1/deploy_l1_verifier.ts b/yarn-project/cli/src/cmds/l1/deploy_l1_verifier.ts index 656986f5b580..675905d1b8ad 100644 --- a/yarn-project/cli/src/cmds/l1/deploy_l1_verifier.ts +++ b/yarn-project/cli/src/cmds/l1/deploy_l1_verifier.ts @@ -1,5 +1,5 @@ import { createCompatibleClient } from '@aztec/aztec.js'; -import { createEthereumChain, createL1Clients, deployL1Contract } from '@aztec/ethereum'; +import { compileContract, createEthereumChain, createL1Clients, deployL1Contract } from '@aztec/ethereum'; import { type DebugLogger, type LogFn } from '@aztec/foundation/log'; import { InvalidOptionArgumentError } from 'commander'; @@ -24,41 +24,7 @@ export async function deployUltraHonkVerifier( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - Importing bb-prover even in devDeps results in a circular dependency error through @aztec/simulator. Need to ignore because this line doesn't cause an error in a dev environment const { BBCircuitVerifier } = await import('@aztec/bb-prover'); - - const circuitVerifier = await BBCircuitVerifier.new({ bbBinaryPath, bbWorkingDirectory }); - const contractSrc = await circuitVerifier.generateSolidityContract( - 'BlockRootRollupFinalArtifact', - 'UltraHonkVerifier.sol', - ); - log('Generated UltraHonkVerifier contract'); - - const input = { - language: 'Solidity', - sources: { - 'UltraHonkVerifier.sol': { - content: contractSrc, - }, - }, - settings: { - // we require the optimizer - optimizer: { - enabled: true, - runs: 200, - }, - evmVersion: 'paris', - outputSelection: { - '*': { - '*': ['evm.bytecode.object', 'abi'], - }, - }, - }, - }; - - const output = JSON.parse(solc.compile(JSON.stringify(input))); - log('Compiled UltraHonkVerifier'); - - const abi = output.contracts['UltraHonkVerifier.sol']['HonkVerifier'].abi; - const bytecode: string = output.contracts['UltraHonkVerifier.sol']['HonkVerifier'].evm.bytecode.object; + const verifier = await BBCircuitVerifier.new({ bbBinaryPath, bbWorkingDirectory }); const { publicClient, walletClient } = createL1Clients( ethRpcUrl, @@ -66,9 +32,6 @@ export async function deployUltraHonkVerifier( createEthereumChain(ethRpcUrl, l1ChainId).chainInfo, ); - const { address: verifierAddress } = await deployL1Contract(walletClient, publicClient, abi, `0x${bytecode}`); - log(`Deployed HonkVerifier at ${verifierAddress.toString()}`); - const pxe = await createCompatibleClient(pxeRpcUrl, debugLogger); const { l1ContractAddresses } = await pxe.getNodeInfo(); @@ -80,7 +43,25 @@ export async function deployUltraHonkVerifier( client: walletClient, }); - await rollup.write.setVerifier([verifierAddress.toString()]); + // REFACTOR: Extract this method to a common package. We need a package that deals with L1 + // but also has a reference to L1 artifacts and bb-prover. + const setupVerifier = async ( + artifact: Parameters<(typeof verifier)['generateSolidityContract']>[0], // Cannot properly import the type here due to the hack above + method: 'setBlockVerifier' | 'setEpochVerifier', + ) => { + const contract = await verifier.generateSolidityContract(artifact, 'UltraHonkVerifier.sol'); + log(`Generated UltraHonkVerifier contract for ${artifact}`); + const { abi, bytecode } = compileContract('UltraHonkVerifier.sol', 'HonkVerifier', contract, solc); + log(`Compiled UltraHonkVerifier contract for ${artifact}`); + const { address: verifierAddress } = await deployL1Contract(walletClient, publicClient, abi, bytecode); + log(`Deployed real ${artifact} verifier at ${verifierAddress}`); + await rollup.write[method]([verifierAddress.toString()]); + log(`Set ${artifact} verifier in ${rollup.address} rollup contract to ${verifierAddress}`); + }; + + await setupVerifier('BlockRootRollupFinalArtifact', 'setBlockVerifier'); + await setupVerifier('RootRollupArtifact', 'setEpochVerifier'); + log(`Rollup accepts only real proofs now`); } @@ -117,6 +98,7 @@ export async function deployMockVerifier( client: walletClient, }); - await rollup.write.setVerifier([mockVerifierAddress.toString()]); + await rollup.write.setBlockVerifier([mockVerifierAddress.toString()]); + await rollup.write.setEpochVerifier([mockVerifierAddress.toString()]); log(`Rollup accepts only fake proofs now`); } diff --git a/yarn-project/end-to-end/src/composed/integration_proof_verification.test.ts b/yarn-project/end-to-end/src/composed/integration_proof_verification.test.ts index 402cfe3fe819..797f42bfc1fd 100644 --- a/yarn-project/end-to-end/src/composed/integration_proof_verification.test.ts +++ b/yarn-project/end-to-end/src/composed/integration_proof_verification.test.ts @@ -29,6 +29,7 @@ import { getLogger, setupL1Contracts, startAnvil } from '../fixtures/utils.js'; /** * Regenerate this test's fixture with * AZTEC_GENERATE_TEST_DATA=1 yarn workspace @aztec/end-to-end test e2e_prover + * TODO(palla/prover): Migrate to root rollup */ describe('proof_verification', () => { let proof: Proof; diff --git a/yarn-project/end-to-end/src/e2e_prover/e2e_prover_test.ts b/yarn-project/end-to-end/src/e2e_prover/e2e_prover_test.ts index 394de73b78eb..7dc346215f68 100644 --- a/yarn-project/end-to-end/src/e2e_prover/e2e_prover_test.ts +++ b/yarn-project/end-to-end/src/e2e_prover/e2e_prover_test.ts @@ -16,7 +16,13 @@ import { createDebugLogger, deployL1Contract, } from '@aztec/aztec.js'; -import { BBCircuitVerifier, type ClientProtocolCircuitVerifier, TestCircuitVerifier } from '@aztec/bb-prover'; +import { + BBCircuitVerifier, + type ClientProtocolCircuitVerifier, + TestCircuitVerifier, + type UltraKeccakHonkProtocolArtifact, +} from '@aztec/bb-prover'; +import { compileContract } from '@aztec/ethereum'; import { RollupAbi } from '@aztec/l1-artifacts'; import { TokenContract } from '@aztec/noir-contracts.js'; import { type ProverNode, type ProverNodeConfig, createProverNode } from '@aztec/prover-node'; @@ -359,51 +365,31 @@ export class FullProverTest { throw new Error('No verifier'); } + const verifier = this.circuitProofVerifier as BBCircuitVerifier; const { walletClient, publicClient, l1ContractAddresses } = this.context.deployL1ContractsValues; - - const contract = await (this.circuitProofVerifier as BBCircuitVerifier).generateSolidityContract( - 'BlockRootRollupFinalArtifact', - 'UltraHonkVerifier.sol', - ); - - const input = { - language: 'Solidity', - sources: { - 'UltraHonkVerifier.sol': { - content: contract, - }, - }, - settings: { - // we require the optimizer - optimizer: { - enabled: true, - runs: 200, - }, - evmVersion: 'paris', - outputSelection: { - '*': { - '*': ['evm.bytecode.object', 'abi'], - }, - }, - }, - }; - - const output = JSON.parse(solc.compile(JSON.stringify(input))); - - const abi = output.contracts['UltraHonkVerifier.sol']['HonkVerifier'].abi; - const bytecode: string = output.contracts['UltraHonkVerifier.sol']['HonkVerifier'].evm.bytecode.object; - - const { address: verifierAddress } = await deployL1Contract(walletClient, publicClient, abi, `0x${bytecode}`); - - this.logger.info(`Deployed Real verifier at ${verifierAddress}`); - const rollup = getContract({ abi: RollupAbi, address: l1ContractAddresses.rollupAddress.toString(), client: walletClient, }); - await rollup.write.setVerifier([verifierAddress.toString()]); + // REFACTOR: Extract this method to a common package. We need a package that deals with L1 + // but also has a reference to L1 artifacts and bb-prover. + const setupVerifier = async ( + artifact: UltraKeccakHonkProtocolArtifact, + method: 'setBlockVerifier' | 'setEpochVerifier', + ) => { + const contract = await verifier.generateSolidityContract(artifact, 'UltraHonkVerifier.sol'); + const { abi, bytecode } = compileContract('UltraHonkVerifier.sol', 'HonkVerifier', contract, solc); + const { address: verifierAddress } = await deployL1Contract(walletClient, publicClient, abi, bytecode); + this.logger.info(`Deployed real ${artifact} verifier at ${verifierAddress}`); + + await rollup.write[method]([verifierAddress.toString()]); + }; + + await setupVerifier('BlockRootRollupFinalArtifact', 'setBlockVerifier'); + await setupVerifier('RootRollupArtifact', 'setEpochVerifier'); + this.logger.info('Rollup only accepts valid proofs now'); } } diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index 966bb00224f9..848ba98d5afc 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -361,6 +361,50 @@ class L1Deployer { } } +/** + * Compiles a contract source code using the provided solc compiler. + * @param fileName - Contract file name (eg UltraHonkVerifier.sol) + * @param contractName - Contract name within the file (eg HonkVerifier) + * @param source - Source code to compile + * @param solc - Solc instance + * @returns ABI and bytecode of the compiled contract + */ +export function compileContract( + fileName: string, + contractName: string, + source: string, + solc: { compile: (source: string) => string }, +): { abi: Narrow; bytecode: Hex } { + const input = { + language: 'Solidity', + sources: { + [fileName]: { + content: source, + }, + }, + settings: { + // we require the optimizer + optimizer: { + enabled: true, + runs: 200, + }, + evmVersion: 'paris', + outputSelection: { + '*': { + '*': ['evm.bytecode.object', 'abi'], + }, + }, + }, + }; + + const output = JSON.parse(solc.compile(JSON.stringify(input))); + + const abi = output.contracts[fileName][contractName].abi; + const bytecode: `0x${string}` = `0x${output.contracts[fileName][contractName].evm.bytecode.object}`; + + return { abi, bytecode }; +} + // docs:start:deployL1Contract /** * Helper function to deploy ETH contracts. From d304f635f1ec4e4fbec91dd85176d0a38dea7c05 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 26 Sep 2024 18:50:14 -0300 Subject: [PATCH 6/6] feat: Pad epochs with empty blocks Root rollup cannot handle a single child, so this PR uses the empty block root circuit to pad the epoch in the orhcestrator. --- .../empty_block_root_rollup_inputs.nr | 3 +- .../bb-prover/src/prover/bb_prover.ts | 7 +- .../bb-prover/src/test/test_circuit_prover.ts | 3 +- .../src/interfaces/block-prover.ts | 2 + .../rollup/empty_block_root_rollup_inputs.ts | 11 +-- .../circuits.js/src/tests/factories.ts | 1 - .../noir-protocol-circuits-types/src/index.ts | 18 ++++ .../src/type_conversion.ts | 1 - .../src/orchestrator/epoch-proving-state.ts | 11 ++- .../src/orchestrator/orchestrator.ts | 77 ++++++++++++++-- .../orchestrator_multiple_blocks.test.ts | 5 +- .../src/test/bb_prover_full_rollup.test.ts | 90 ++++++++++--------- .../prover-node/src/job/epoch-proving-job.ts | 3 + 13 files changed, 162 insertions(+), 70 deletions(-) diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/empty_block_root_rollup_inputs.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/empty_block_root_rollup_inputs.nr index 3ca5cb691a06..ecff942e975a 100644 --- a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/empty_block_root_rollup_inputs.nr +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/block_root/empty_block_root_rollup_inputs.nr @@ -6,7 +6,6 @@ pub struct EmptyBlockRootRollupInputs { archive: AppendOnlyTreeSnapshot, block_hash: Field, global_variables: GlobalVariables, - out_hash: Field, vk_tree_root: Field, // TODO(#7346): Temporarily added prover_id while we verify block-root proofs on L1 prover_id: Field, @@ -21,7 +20,7 @@ impl EmptyBlockRootRollupInputs { end_block_hash: self.block_hash, start_global_variables: self.global_variables, end_global_variables: self.global_variables, - out_hash: self.out_hash, + out_hash: 0, // out_hash is ignored when merging if the block proof is padding fees: [FeeRecipient::empty(); 32], vk_tree_root: self.vk_tree_root, prover_id: self.prover_id diff --git a/yarn-project/bb-prover/src/prover/bb_prover.ts b/yarn-project/bb-prover/src/prover/bb_prover.ts index 40c75b28912f..1e945b3c58d1 100644 --- a/yarn-project/bb-prover/src/prover/bb_prover.ts +++ b/yarn-project/bb-prover/src/prover/bb_prover.ts @@ -59,6 +59,7 @@ import { convertBlockRootRollupInputsToWitnessMap, convertBlockRootRollupOutputsFromWitnessMap, convertEmptyBlockRootRollupInputsToWitnessMap, + convertEmptyBlockRootRollupOutputsFromWitnessMap, convertMergeRollupInputsToWitnessMap, convertMergeRollupOutputsFromWitnessMap, convertPrivateKernelEmptyInputsToWitnessMap, @@ -395,12 +396,12 @@ export class BBNativeRollupProver implements ServerCircuitProver { 'EmptyBlockRootRollupArtifact', RECURSIVE_PROOF_LENGTH, convertEmptyBlockRootRollupInputsToWitnessMap, - convertBlockRootRollupOutputsFromWitnessMap, + convertEmptyBlockRootRollupOutputsFromWitnessMap, ); - const verificationKey = await this.getVerificationKeyDataForCircuit('BlockRootRollupArtifact'); + const verificationKey = await this.getVerificationKeyDataForCircuit('EmptyBlockRootRollupArtifact'); - await this.verifyProof('BlockRootRollupArtifact', proof.binaryProof); + await this.verifyProof('EmptyBlockRootRollupArtifact', proof.binaryProof); return makePublicInputsAndRecursiveProof(circuitOutput, proof, verificationKey); } diff --git a/yarn-project/bb-prover/src/test/test_circuit_prover.ts b/yarn-project/bb-prover/src/test/test_circuit_prover.ts index 8a56a11119ed..89df65b7fa55 100644 --- a/yarn-project/bb-prover/src/test/test_circuit_prover.ts +++ b/yarn-project/bb-prover/src/test/test_circuit_prover.ts @@ -54,6 +54,7 @@ import { convertBlockRootRollupInputsToWitnessMap, convertBlockRootRollupOutputsFromWitnessMap, convertEmptyBlockRootRollupInputsToWitnessMap, + convertEmptyBlockRootRollupOutputsFromWitnessMap, convertMergeRollupInputsToWitnessMap, convertMergeRollupOutputsFromWitnessMap, convertPrivateKernelEmptyInputsToWitnessMap, @@ -367,7 +368,7 @@ export class TestCircuitProver implements ServerCircuitProver { SimulatedServerCircuitArtifacts.EmptyBlockRootRollupArtifact, ); - const result = convertBlockRootRollupOutputsFromWitnessMap(witness); + const result = convertEmptyBlockRootRollupOutputsFromWitnessMap(witness); this.instrumentation.recordDuration('simulationDuration', 'empty-block-root-rollup', timer); emitCircuitSimulationStats( diff --git a/yarn-project/circuit-types/src/interfaces/block-prover.ts b/yarn-project/circuit-types/src/interfaces/block-prover.ts index 1443621761f7..a2a053d8a373 100644 --- a/yarn-project/circuit-types/src/interfaces/block-prover.ts +++ b/yarn-project/circuit-types/src/interfaces/block-prover.ts @@ -78,5 +78,7 @@ export interface BlockProver extends BlockSimulator { export interface EpochProver extends BlockProver { startNewEpoch(epochNumber: number, totalNumBlocks: number): ProvingTicket; + setEpochCompleted(): void; + finaliseEpoch(): ProvingEpochResult; } diff --git a/yarn-project/circuits.js/src/structs/rollup/empty_block_root_rollup_inputs.ts b/yarn-project/circuits.js/src/structs/rollup/empty_block_root_rollup_inputs.ts index 52de60ff2232..87506ad0a7c7 100644 --- a/yarn-project/circuits.js/src/structs/rollup/empty_block_root_rollup_inputs.ts +++ b/yarn-project/circuits.js/src/structs/rollup/empty_block_root_rollup_inputs.ts @@ -13,7 +13,6 @@ export class EmptyBlockRootRollupInputs { public readonly archive: AppendOnlyTreeSnapshot, public readonly blockHash: Fr, public readonly globalVariables: GlobalVariables, - public readonly outHash: Fr, public readonly vkTreeRoot: Fr, // // TODO(#7346): Temporarily added prover_id while we verify block-root proofs on L1 public readonly proverId: Fr, @@ -50,14 +49,7 @@ export class EmptyBlockRootRollupInputs { * @returns An array of fields. */ static getFields(fields: FieldsOf) { - return [ - fields.archive, - fields.blockHash, - fields.globalVariables, - fields.outHash, - fields.vkTreeRoot, - fields.proverId, - ] as const; + return [fields.archive, fields.blockHash, fields.globalVariables, fields.vkTreeRoot, fields.proverId] as const; } /** @@ -73,7 +65,6 @@ export class EmptyBlockRootRollupInputs { GlobalVariables.fromBuffer(reader), Fr.fromBuffer(reader), Fr.fromBuffer(reader), - Fr.fromBuffer(reader), ); } diff --git a/yarn-project/circuits.js/src/tests/factories.ts b/yarn-project/circuits.js/src/tests/factories.ts index 490108f49203..50f332c96d96 100644 --- a/yarn-project/circuits.js/src/tests/factories.ts +++ b/yarn-project/circuits.js/src/tests/factories.ts @@ -1077,7 +1077,6 @@ export function makeEmptyBlockRootRollupInputs( globalVariables ?? makeGlobalVariables(seed + 0x200), fr(seed + 0x300), fr(seed + 0x400), - fr(seed + 0x500), ); } diff --git a/yarn-project/noir-protocol-circuits-types/src/index.ts b/yarn-project/noir-protocol-circuits-types/src/index.ts index d6ef3108fa5e..042b1b92b13d 100644 --- a/yarn-project/noir-protocol-circuits-types/src/index.ts +++ b/yarn-project/noir-protocol-circuits-types/src/index.ts @@ -84,6 +84,7 @@ import { type PublicKernelInnerReturnType, type PublicKernelMergeReturnType, type PrivateKernelResetReturnType as ResetReturnType, + type RollupBlockRootEmptyReturnType, type ParityRootReturnType as RootParityReturnType, type RollupRootReturnType as RootRollupReturnType, type PrivateKernelTailReturnType as TailReturnType, @@ -602,6 +603,23 @@ export function convertMergeRollupOutputsFromWitnessMap(outputs: WitnessMap): Ba return mapBaseOrMergeRollupPublicInputsFromNoir(returnType); } +/** + * Converts the outputs of the empty block root rollup circuit from a witness map. + * @param outputs - The block root rollup outputs as a witness map. + * @returns The public inputs. + */ +export function convertEmptyBlockRootRollupOutputsFromWitnessMap( + outputs: WitnessMap, +): BlockRootOrBlockMergePublicInputs { + // Decode the witness map into two fields, the return values and the inputs + const decodedInputs: DecodedInputs = abiDecode(ServerCircuitArtifacts.EmptyBlockRootRollupArtifact.abi, outputs); + + // Cast the inputs as the return type + const returnType = decodedInputs.return_value as RollupBlockRootEmptyReturnType; + + return mapBlockRootOrBlockMergePublicInputsFromNoir(returnType); +} + /** * Converts the outputs of the block root rollup circuit from a witness map. * @param outputs - The block root rollup outputs as a witness map. diff --git a/yarn-project/noir-protocol-circuits-types/src/type_conversion.ts b/yarn-project/noir-protocol-circuits-types/src/type_conversion.ts index 616b3228b5a2..f03a7af62b22 100644 --- a/yarn-project/noir-protocol-circuits-types/src/type_conversion.ts +++ b/yarn-project/noir-protocol-circuits-types/src/type_conversion.ts @@ -2334,7 +2334,6 @@ export function mapEmptyBlockRootRollupInputsToNoir( archive: mapAppendOnlyTreeSnapshotToNoir(rootRollupInputs.archive), block_hash: mapFieldToNoir(rootRollupInputs.blockHash), global_variables: mapGlobalVariablesToNoir(rootRollupInputs.globalVariables), - out_hash: mapFieldToNoir(rootRollupInputs.outHash), vk_tree_root: mapFieldToNoir(rootRollupInputs.vkTreeRoot), prover_id: mapFieldToNoir(rootRollupInputs.proverId), }; diff --git a/yarn-project/prover-client/src/orchestrator/epoch-proving-state.ts b/yarn-project/prover-client/src/orchestrator/epoch-proving-state.ts index 350240242b8a..03d32a5e3929 100644 --- a/yarn-project/prover-client/src/orchestrator/epoch-proving-state.ts +++ b/yarn-project/prover-client/src/orchestrator/epoch-proving-state.ts @@ -55,22 +55,26 @@ export class EpochProvingState { public readonly totalNumBlocks: number, private completionCallback: (result: ProvingResult) => void, private rejectionCallback: (reason: string) => void, + /** Whether to prove the epoch. Temporary while we still care about proving blocks. */ + public readonly proveEpoch: boolean, ) {} /** Returns the current block proving state */ public get currentBlock(): BlockProvingState | undefined { - return this.blocks[this.blocks.length - 1]; + return this.blocks.at(-1); } // Returns the number of levels of merge rollups public get numMergeLevels() { - return BigInt(Math.ceil(Math.log2(this.totalNumBlocks)) - 1); + const totalLeaves = Math.max(2, this.totalNumBlocks); + return BigInt(Math.ceil(Math.log2(totalLeaves)) - 1); } // Calculates the index and level of the parent rollup circuit // Based on tree implementation in unbalanced_tree.ts -> batchInsert() // REFACTOR: This is repeated from the block orchestrator public findMergeLevel(currentLevel: bigint, currentIndex: bigint) { + const totalLeaves = Math.max(2, this.totalNumBlocks); const moveUpMergeLevel = (levelSize: number, index: bigint, nodeToShift: boolean) => { levelSize /= 2; if (levelSize & 1) { @@ -79,8 +83,7 @@ export class EpochProvingState { index >>= 1n; return { thisLevelSize: levelSize, thisIndex: index, shiftUp: nodeToShift }; }; - let [thisLevelSize, shiftUp] = - this.totalNumBlocks & 1 ? [this.totalNumBlocks - 1, true] : [this.totalNumBlocks, false]; + let [thisLevelSize, shiftUp] = totalLeaves & 1 ? [totalLeaves - 1, true] : [totalLeaves, false]; const maxLevel = this.numMergeLevels + 1n; let placeholder = currentIndex; for (let i = 0; i < maxLevel - currentLevel; i++) { diff --git a/yarn-project/prover-client/src/orchestrator/orchestrator.ts b/yarn-project/prover-client/src/orchestrator/orchestrator.ts index d6406a493329..44c3a3bd19eb 100644 --- a/yarn-project/prover-client/src/orchestrator/orchestrator.ts +++ b/yarn-project/prover-client/src/orchestrator/orchestrator.ts @@ -31,6 +31,7 @@ import { type BaseRollupInputs, type BlockRootOrBlockMergePublicInputs, BlockRootRollupInputs, + EmptyBlockRootRollupInputs, Fr, type GlobalVariables, type KernelCircuitPublicInputs, @@ -134,12 +135,14 @@ export class ProvingOrchestrator implements EpochProver { this.paddingTx = undefined; } - public startNewEpoch(epochNumber: number, totalNumBlocks: number): ProvingTicket { + public startNewEpoch(epochNumber: number, totalNumBlocks: number, proveEpoch = true): ProvingTicket { const { promise: _promise, resolve, reject } = promiseWithResolvers(); const promise = _promise.catch((reason): ProvingResult => ({ status: PROVING_STATUS.FAILURE, reason })); - + if (totalNumBlocks <= 0) { + throw new Error(`Invalid number of blocks for epoch: ${totalNumBlocks}`); + } logger.info(`Starting epoch ${epochNumber} with ${totalNumBlocks} blocks`); - this.provingState = new EpochProvingState(epochNumber, totalNumBlocks, resolve, reject); + this.provingState = new EpochProvingState(epochNumber, totalNumBlocks, resolve, reject, proveEpoch); return { provingPromise: promise }; } @@ -161,8 +164,9 @@ export class ProvingOrchestrator implements EpochProver { l1ToL2Messages: Fr[], ): Promise { // If no proving state, assume we only care about proving this block and initialize a 1-block epoch + // TODO(palla/prover): Remove this flow once we drop block-only proving if (!this.provingState) { - this.startNewEpoch(globalVariables.blockNumber.toNumber(), 1); + this.startNewEpoch(globalVariables.blockNumber.toNumber(), 1, false); } if (!this.provingState?.isAcceptingBlocks()) { @@ -349,6 +353,69 @@ export class ProvingOrchestrator implements EpochProver { await this.buildBlockHeader(provingState); } + @trackSpan('ProvingOrchestrator.setEpochCompleted', function () { + if (!this.provingState) { + return {}; + } + return { + [Attributes.EPOCH_NUMBER]: this.provingState.epochNumber, + [Attributes.EPOCH_SIZE]: this.provingState.totalNumBlocks, + }; + }) + public setEpochCompleted() { + const provingState = this.provingState; + if (!provingState) { + throw new Error(`Invalid proving state, call startNewEpoch first`); + } + + const lastBlock = provingState.currentBlock?.block; + if (!lastBlock) { + throw new Error(`Epoch needs at least one completed block in order to be marked as completed`); + } + + const paddingBlockCount = Math.max(2, provingState.totalNumBlocks) - provingState.blocks.length; + if (paddingBlockCount === 0) { + return; + } + + logger.debug(`Padding epoch proof with ${paddingBlockCount} empty block proofs`); + + const inputs = EmptyBlockRootRollupInputs.from({ + archive: lastBlock.archive, + blockHash: lastBlock.header.hash(), + globalVariables: lastBlock.header.globalVariables, + vkTreeRoot: getVKTreeRoot(), + proverId: this.proverId, + }); + + logger.debug(`Enqueuing deferred proving for padding block to enqueue ${paddingBlockCount} paddings`); + this.deferredProving( + provingState, + wrapCallbackInSpan( + this.tracer, + 'ProvingOrchestrator.prover.getEmptyBlockRootRollupProof', + { + [Attributes.PROTOCOL_CIRCUIT_TYPE]: 'server', + [Attributes.PROTOCOL_CIRCUIT_NAME]: 'empty-block-root-rollup' satisfies CircuitName, + }, + signal => this.prover.getEmptyBlockRootRollupProof(inputs, signal, provingState.epochNumber), + ), + result => { + logger.debug(`Completed proof for padding block`); + const currentLevel = provingState.numMergeLevels + 1n; + for (let i = 0; i < paddingBlockCount; i++) { + logger.debug(`Enqueuing padding block with index ${provingState.blocks.length + i}`); + const index = BigInt(provingState.blocks.length + i); + this.storeAndExecuteNextBlockMergeLevel(provingState, currentLevel, index, [ + result.inputs, + result.proof, + result.verificationKey.keyAsFields, + ]); + } + }, + ); + } + private async buildBlockHeader(provingState: BlockProvingState) { // Collect all new nullifiers, commitments, and contracts from all txs in this block to build body const gasFees = provingState.globalVariables.gasFees; @@ -860,7 +927,7 @@ export class ProvingOrchestrator implements EpochProver { proverId: this.proverId, }); - const shouldProveEpoch = this.provingState!.totalNumBlocks > 1; + const shouldProveEpoch = this.provingState!.proveEpoch; this.deferredProving( provingState, diff --git a/yarn-project/prover-client/src/orchestrator/orchestrator_multiple_blocks.test.ts b/yarn-project/prover-client/src/orchestrator/orchestrator_multiple_blocks.test.ts index 7795c423e77c..533b8ecb62d0 100644 --- a/yarn-project/prover-client/src/orchestrator/orchestrator_multiple_blocks.test.ts +++ b/yarn-project/prover-client/src/orchestrator/orchestrator_multiple_blocks.test.ts @@ -19,7 +19,7 @@ describe('prover/orchestrator/multi-block', () => { }); describe('multiple blocks', () => { - it.each([4, 5])('builds an epoch with %s blocks in sequence', async (numBlocks: number) => { + it.each([1, 4, 5])('builds an epoch with %s blocks in sequence', async (numBlocks: number) => { const provingTicket = context.orchestrator.startNewEpoch(1, numBlocks); let header = context.actualDb.getInitialHeader(); @@ -48,6 +48,9 @@ describe('prover/orchestrator/multi-block', () => { header = finalisedBlock.block.header; } + logger.info('Setting epoch as completed'); + context.orchestrator.setEpochCompleted(); + logger.info('Awaiting epoch ticket'); const result = await provingTicket.provingPromise; expect(result).toEqual({ status: PROVING_STATUS.SUCCESS }); diff --git a/yarn-project/prover-client/src/test/bb_prover_full_rollup.test.ts b/yarn-project/prover-client/src/test/bb_prover_full_rollup.test.ts index 205027a729e7..1c0a94d4a2f5 100644 --- a/yarn-project/prover-client/src/test/bb_prover_full_rollup.test.ts +++ b/yarn-project/prover-client/src/test/bb_prover_full_rollup.test.ts @@ -13,60 +13,66 @@ import { TestContext } from '../mocks/test_context.js'; describe('prover/bb_prover/full-rollup', () => { let context: TestContext; let prover: BBNativeRollupProver; - let logger: DebugLogger; + let log: DebugLogger; beforeAll(async () => { const buildProver = async (bbConfig: BBProverConfig) => { prover = await BBNativeRollupProver.new(bbConfig, new NoopTelemetryClient()); return prover; }; - logger = createDebugLogger('aztec:bb-prover-full-rollup'); - context = await TestContext.new(logger, 'legacy', 1, buildProver); + log = createDebugLogger('aztec:bb-prover-full-rollup'); + context = await TestContext.new(log, 'legacy', 1, buildProver); }); afterAll(async () => { await context.cleanup(); }); - it('proves a private-only epoch full of empty txs', async () => { - const totalBlocks = 2; - const totalTxs = 2; - const nonEmptyTxs = 0; - - logger.info(`Proving a full epoch with ${totalBlocks} blocks with ${nonEmptyTxs}/${totalTxs} non-empty txs each`); - - const initialHeader = context.actualDb.getInitialHeader(); - const provingTicket = context.orchestrator.startNewEpoch(1, totalBlocks); - - for (let blockNum = 1; blockNum <= totalBlocks; blockNum++) { - const globals = makeGlobals(blockNum); - const l1ToL2Messages = makeTuple(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, Fr.random); - const txs = times(nonEmptyTxs, (i: number) => { - const txOpts = { numberOfNonRevertiblePublicCallRequests: 0, numberOfRevertiblePublicCallRequests: 0 }; - const tx = mockTx(blockNum * 100_000 + 1000 * (i + 1), txOpts); - tx.data.constants.historicalHeader = initialHeader; - tx.data.constants.vkTreeRoot = getVKTreeRoot(); - return tx; - }); - - logger.info(`Starting new block #${blockNum}`); - await context.orchestrator.startNewBlock(totalTxs, globals, l1ToL2Messages); - logger.info(`Processing public functions`); - const [processed, failed] = await context.processPublicFunctions(txs, nonEmptyTxs, context.blockProver); - expect(processed.length).toBe(nonEmptyTxs); - expect(failed.length).toBe(0); - - logger.info(`Setting block as completed`); - await context.orchestrator.setBlockCompleted(); - } - - logger.info(`Awaiting proofs`); - const provingResult = await provingTicket.provingPromise; - expect(provingResult.status).toBe(PROVING_STATUS.SUCCESS); - const epochResult = context.orchestrator.finaliseEpoch(); - - await expect(prover.verifyProof('RootRollupArtifact', epochResult.proof)).resolves.not.toThrow(); - }); + it.each([ + [1, 1, 0, 2], // Epoch with a single block, requires one padding block proof + [2, 2, 0, 2], // Full epoch with two blocks + [2, 3, 0, 2], // Epoch with two blocks but the block merge tree was assembled as with 3 leaves, requires one padding block proof + ])( + 'proves a private-only epoch with %i/%i blocks with %i/%i non-empty txs each', + async (blockCount, totalBlocks, nonEmptyTxs, totalTxs) => { + log.info(`Proving epoch with ${blockCount}/${totalBlocks} blocks with ${nonEmptyTxs}/${totalTxs} non-empty txs`); + + const initialHeader = context.actualDb.getInitialHeader(); + const provingTicket = context.orchestrator.startNewEpoch(1, totalBlocks); + + for (let blockNum = 1; blockNum <= blockCount; blockNum++) { + const globals = makeGlobals(blockNum); + const l1ToL2Messages = makeTuple(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, Fr.random); + const txs = times(nonEmptyTxs, (i: number) => { + const txOpts = { numberOfNonRevertiblePublicCallRequests: 0, numberOfRevertiblePublicCallRequests: 0 }; + const tx = mockTx(blockNum * 100_000 + 1000 * (i + 1), txOpts); + tx.data.constants.historicalHeader = initialHeader; + tx.data.constants.vkTreeRoot = getVKTreeRoot(); + return tx; + }); + + log.info(`Starting new block #${blockNum}`); + await context.orchestrator.startNewBlock(totalTxs, globals, l1ToL2Messages); + log.info(`Processing public functions`); + const [processed, failed] = await context.processPublicFunctions(txs, nonEmptyTxs, context.blockProver); + expect(processed.length).toBe(nonEmptyTxs); + expect(failed.length).toBe(0); + + log.info(`Setting block as completed`); + await context.orchestrator.setBlockCompleted(); + } + + log.info(`Setting epoch as completed`); + context.orchestrator.setEpochCompleted(); + + log.info(`Awaiting proofs`); + const provingResult = await provingTicket.provingPromise; + expect(provingResult.status).toBe(PROVING_STATUS.SUCCESS); + const epochResult = context.orchestrator.finaliseEpoch(); + + await expect(prover.verifyProof('RootRollupArtifact', epochResult.proof)).resolves.not.toThrow(); + }, + ); // TODO(@PhilWindle): Remove public functions and re-enable once we can handle empty tx slots it.skip('proves all circuits', async () => { diff --git a/yarn-project/prover-node/src/job/epoch-proving-job.ts b/yarn-project/prover-node/src/job/epoch-proving-job.ts index 424801b6d960..d104bfc52dc0 100644 --- a/yarn-project/prover-node/src/job/epoch-proving-job.ts +++ b/yarn-project/prover-node/src/job/epoch-proving-job.ts @@ -108,6 +108,9 @@ export class EpochProvingJob { previousHeader = block.header; } + // Pad epoch with empty block proofs if needed + this.prover.setEpochCompleted(); + this.state = 'awaiting-prover'; const result = await provingTicket.provingPromise; if (result.status === PROVING_STATUS.FAILURE) {