diff --git a/cspell.json b/cspell.json index f6c12ff5d82..9180d507c56 100644 --- a/cspell.json +++ b/cspell.json @@ -81,6 +81,8 @@ "fargate", "Fieldeable", "filestat", + "finalise", + "finalised", "flatmap", "foundryup", "frontend", @@ -101,6 +103,7 @@ "herskind", "ierc", "indexeddb", + "initialise", "interruptible", "isequal", "jsons", diff --git a/yarn-project/circuit-types/src/interfaces/block-prover.ts b/yarn-project/circuit-types/src/interfaces/block-prover.ts index 31560b36a5b..1c1af893e5d 100644 --- a/yarn-project/circuit-types/src/interfaces/block-prover.ts +++ b/yarn-project/circuit-types/src/interfaces/block-prover.ts @@ -10,8 +10,6 @@ export enum PROVING_STATUS { export type ProvingSuccess = { status: PROVING_STATUS.SUCCESS; - block: L2Block; - proof: Proof; }; export type ProvingFailure = { @@ -25,11 +23,23 @@ export type ProvingTicket = { provingPromise: Promise; }; +export type BlockResult = { + block: L2Block; + proof: Proof; +}; + /** * The interface to the block prover. * Provides the ability to generate proofs and build rollups. */ export interface BlockProver { + /** + * Cancels any block that is currently being built and prepares for a new one to be built + * @param numTxs - The complete size of the block, must be a power of 2 + * @param globalVariables - The global variables for this block + * @param l1ToL2Messages - The set of L1 to L2 messages to be included in this block + * @param emptyTx - An instance of an empty transaction to be used in this block + */ startNewBlock( numTxs: number, globalVariables: GlobalVariables, @@ -37,5 +47,25 @@ export interface BlockProver { emptyTx: ProcessedTx, ): Promise; + /** + * Add a processed transaction to the current block + * @param tx - The transaction to be added + */ addNewTx(tx: ProcessedTx): Promise; + + /** + * Cancels the block currently being proven. Proofs already bring built may continue but further proofs should not be started. + */ + cancelBlock(): void; + + /** + * Performs the final archive tree insertion for this block and returns the L2Block and Proof instances + */ + finaliseBlock(): Promise; + + /** + * Mark the block as having all the transactions it is going to contain. + * Will pad the block to it's complete size with empty transactions and prove all the way to the root rollup. + */ + setBlockCompleted(): Promise; } diff --git a/yarn-project/circuits.js/src/structs/global_variables.ts b/yarn-project/circuits.js/src/structs/global_variables.ts index d3f762785c4..32a9100307f 100644 --- a/yarn-project/circuits.js/src/structs/global_variables.ts +++ b/yarn-project/circuits.js/src/structs/global_variables.ts @@ -106,6 +106,10 @@ export class GlobalVariables { }; } + clone(): GlobalVariables { + return GlobalVariables.fromBuffer(this.toBuffer()); + } + isEmpty(): boolean { return ( this.chainId.isZero() && diff --git a/yarn-project/circuits.js/src/structs/header.ts b/yarn-project/circuits.js/src/structs/header.ts index 0a5413fa5a7..a6639e0f529 100644 --- a/yarn-project/circuits.js/src/structs/header.ts +++ b/yarn-project/circuits.js/src/structs/header.ts @@ -40,6 +40,10 @@ export class Header { return fields; } + clone(): Header { + return Header.fromBuffer(this.toBuffer()); + } + static fromBuffer(buffer: Buffer | BufferReader): Header { const reader = BufferReader.asReader(buffer); diff --git a/yarn-project/end-to-end/src/benchmarks/bench_tx_size_fees.test.ts b/yarn-project/end-to-end/src/benchmarks/bench_tx_size_fees.test.ts index def00fbbb70..c79c504aa45 100644 --- a/yarn-project/end-to-end/src/benchmarks/bench_tx_size_fees.test.ts +++ b/yarn-project/end-to-end/src/benchmarks/bench_tx_size_fees.test.ts @@ -56,11 +56,10 @@ describe('benchmarks/tx_size_fees', () => { beforeAll(async () => { await Promise.all([ gas.methods.mint_public(aliceWallet.getAddress(), 1000n).send().wait(), - token.methods.privately_mint_private_note(1000n).send().wait(), - token.methods.mint_public(aliceWallet.getAddress(), 1000n).send().wait(), - gas.methods.mint_public(fpc.address, 1000n).send().wait(), ]); + await token.methods.privately_mint_private_note(1000n).send().wait(); + await token.methods.mint_public(aliceWallet.getAddress(), 1000n).send().wait(); }); it.each<() => Promise>([ diff --git a/yarn-project/end-to-end/src/integration_l1_publisher.test.ts b/yarn-project/end-to-end/src/integration_l1_publisher.test.ts index 2f4baba29c3..3c179d61741 100644 --- a/yarn-project/end-to-end/src/integration_l1_publisher.test.ts +++ b/yarn-project/end-to-end/src/integration_l1_publisher.test.ts @@ -12,8 +12,8 @@ import { } from '@aztec/aztec.js'; // eslint-disable-next-line no-restricted-imports import { + PROVING_STATUS, type ProcessedTx, - type ProvingSuccess, makeEmptyProcessedTx as makeEmptyProcessedTxFromHistoricalTreeRoots, makeProcessedTx, } from '@aztec/circuit-types'; @@ -142,6 +142,7 @@ describe('L1Publisher integration', () => { l2QueueSize: 10, }; const worldStateSynchronizer = new ServerWorldStateSynchronizer(tmpStore, builderDb, blockSource, worldStateConfig); + await worldStateSynchronizer.start(); builder = await TxProver.new({}, worldStateSynchronizer, new WASMSimulator()); l2Proof = Buffer.alloc(0); @@ -390,8 +391,12 @@ describe('L1Publisher integration', () => { ); const ticket = await buildBlock(globalVariables, txs, currentL1ToL2Messages, makeEmptyProcessedTx()); const result = await ticket.provingPromise; - const block = (result as ProvingSuccess).block; + expect(result.status).toBe(PROVING_STATUS.SUCCESS); + const blockResult = await builder.finaliseBlock(); + const block = blockResult.block; prevHeader = block.header; + blockSource.getL1ToL2Messages.mockResolvedValueOnce(currentL1ToL2Messages); + blockSource.getBlocks.mockResolvedValueOnce([block]); const newL2ToL1MsgsArray = block.body.txEffects.flatMap(txEffect => txEffect.l2ToL1Msgs); @@ -480,8 +485,12 @@ describe('L1Publisher integration', () => { ); const blockTicket = await buildBlock(globalVariables, txs, l1ToL2Messages, makeEmptyProcessedTx()); const result = await blockTicket.provingPromise; - const block = (result as ProvingSuccess).block; + expect(result.status).toBe(PROVING_STATUS.SUCCESS); + const blockResult = await builder.finaliseBlock(); + const block = blockResult.block; prevHeader = block.header; + blockSource.getL1ToL2Messages.mockResolvedValueOnce(l1ToL2Messages); + blockSource.getBlocks.mockResolvedValueOnce([block]); writeJson(`empty_block_${i}`, block, [], AztecAddress.ZERO, deployerAccount.address); diff --git a/yarn-project/prover-client/src/dummy-prover.ts b/yarn-project/prover-client/src/dummy-prover.ts index f95b46fb006..de61267c738 100644 --- a/yarn-project/prover-client/src/dummy-prover.ts +++ b/yarn-project/prover-client/src/dummy-prover.ts @@ -1,4 +1,5 @@ import { + type BlockResult, L2Block, PROVING_STATUS, type ProcessedTx, @@ -30,8 +31,6 @@ export class DummyProver implements ProverClient { ): Promise { const result: ProvingSuccess = { status: PROVING_STATUS.SUCCESS, - proof: makeEmptyProof(), - block: L2Block.empty(), }; const ticket: ProvingTicket = { provingPromise: Promise.resolve(result), @@ -42,4 +41,17 @@ export class DummyProver implements ProverClient { addNewTx(_tx: ProcessedTx): Promise { return Promise.resolve(); } + + cancelBlock(): void {} + + finaliseBlock(): Promise { + return Promise.resolve({ + block: L2Block.empty(), + proof: makeEmptyProof(), + }); + } + + setBlockCompleted(): Promise { + return Promise.resolve(); + } } diff --git a/yarn-project/prover-client/src/orchestrator/block-building-helpers.ts b/yarn-project/prover-client/src/orchestrator/block-building-helpers.ts index 1fc3ed3cd7f..6cbc988145a 100644 --- a/yarn-project/prover-client/src/orchestrator/block-building-helpers.ts +++ b/yarn-project/prover-client/src/orchestrator/block-building-helpers.ts @@ -9,8 +9,7 @@ import { Fr, type GlobalVariables, KernelData, - L1_TO_L2_MSG_SUBTREE_HEIGHT, - L1_TO_L2_MSG_SUBTREE_SIBLING_PATH_LENGTH, + type L1_TO_L2_MSG_SUBTREE_SIBLING_PATH_LENGTH, MAX_NEW_NULLIFIERS_PER_TX, MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, MembershipWitness, @@ -200,29 +199,29 @@ export async function executeRootRollupCircuit( right: [BaseOrMergeRollupPublicInputs, Proof], l1ToL2Roots: RootParityInput, newL1ToL2Messages: Tuple, + messageTreeSnapshot: AppendOnlyTreeSnapshot, + messageTreeRootSiblingPath: Tuple, simulator: RollupSimulator, prover: RollupProver, db: MerkleTreeOperations, logger?: DebugLogger, ): Promise<[RootRollupPublicInputs, Proof]> { logger?.debug(`Running root rollup circuit`); - const rootInput = await getRootRollupInput(...left, ...right, l1ToL2Roots, newL1ToL2Messages, db); - - // Update the local trees to include the new l1 to l2 messages - await db.appendLeaves(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, newL1ToL2Messages); + const rootInput = await getRootRollupInput( + ...left, + ...right, + l1ToL2Roots, + newL1ToL2Messages, + messageTreeSnapshot, + messageTreeRootSiblingPath, + db, + ); // Simulate and get proof for the root circuit const rootOutput = await simulator.rootRollupCircuit(rootInput); const rootProof = await prover.getRootRollupProof(rootInput, rootOutput); - //TODO(@PhilWindle) Move this to orchestrator to ensure that we are still on the same block - // Update the archive with the latest block header - logger?.debug(`Updating and validating root trees`); - await db.updateArchive(rootOutput.header); - - await validateRootOutput(rootOutput, db); - return [rootOutput, rootProof]; } @@ -259,6 +258,8 @@ export async function getRootRollupInput( rollupProofRight: Proof, l1ToL2Roots: RootParityInput, newL1ToL2Messages: Tuple, + messageTreeSnapshot: AppendOnlyTreeSnapshot, + messageTreeRootSiblingPath: Tuple, db: MerkleTreeOperations, ) { const vks = getVerificationKeys(); @@ -274,21 +275,6 @@ export async function getRootRollupInput( return path.toFields(); }; - const newL1ToL2MessageTreeRootSiblingPathArray = await getSubtreeSiblingPath( - MerkleTreeId.L1_TO_L2_MESSAGE_TREE, - L1_TO_L2_MSG_SUBTREE_HEIGHT, - db, - ); - - const newL1ToL2MessageTreeRootSiblingPath = makeTuple( - L1_TO_L2_MSG_SUBTREE_SIBLING_PATH_LENGTH, - i => (i < newL1ToL2MessageTreeRootSiblingPathArray.length ? newL1ToL2MessageTreeRootSiblingPathArray[i] : Fr.ZERO), - 0, - ); - - // Get tree snapshots - const startL1ToL2MessageTreeSnapshot = await getTreeSnapshot(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, db); - // Get blocks tree const startArchiveSnapshot = await getTreeSnapshot(MerkleTreeId.ARCHIVE, db); const newArchiveSiblingPathArray = await getRootTreeSiblingPath(MerkleTreeId.ARCHIVE); @@ -303,8 +289,8 @@ export async function getRootRollupInput( previousRollupData, l1ToL2Roots, newL1ToL2Messages, - newL1ToL2MessageTreeRootSiblingPath, - startL1ToL2MessageTreeSnapshot, + newL1ToL2MessageTreeRootSiblingPath: messageTreeRootSiblingPath, + startL1ToL2MessageTreeSnapshot: messageTreeSnapshot, startArchiveSnapshot, newArchiveSiblingPath, }); diff --git a/yarn-project/prover-client/src/orchestrator/orchestrator.test.ts b/yarn-project/prover-client/src/orchestrator/orchestrator.test.ts index cf2257e3084..61d933576c7 100644 --- a/yarn-project/prover-client/src/orchestrator/orchestrator.test.ts +++ b/yarn-project/prover-client/src/orchestrator/orchestrator.test.ts @@ -2,7 +2,7 @@ import { MerkleTreeId, PROVING_STATUS, type ProcessedTx, - type ProvingSuccess, + type ProvingFailure, makeEmptyProcessedTx as makeEmptyProcessedTxFromHistoricalTreeRoots, makeProcessedTx, mockTx, @@ -78,9 +78,13 @@ describe('prover/tx-prover', () => { const coinbase = EthAddress.ZERO; const feeRecipient = AztecAddress.ZERO; + const makeGlobals = (blockNumber: number) => { + return new GlobalVariables(chainId, version, new Fr(blockNumber), Fr.ZERO, coinbase, feeRecipient); + }; + beforeEach(async () => { blockNumber = 3; - globalVariables = new GlobalVariables(chainId, version, new Fr(blockNumber), Fr.ZERO, coinbase, feeRecipient); + globalVariables = makeGlobals(blockNumber); builderDb = await MerkleTrees.new(openTmpStore()).then(t => t.asLatest()); expectsDb = await MerkleTrees.new(openTmpStore()).then(t => t.asLatest()); @@ -154,91 +158,6 @@ describe('prover/tx-prover', () => { } }; - // const updateL1ToL2MessageTree = async (l1ToL2Messages: Fr[]) => { - // const asBuffer = l1ToL2Messages.map(m => m.toBuffer()); - // await expectsDb.appendLeaves(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, asBuffer); - // }; - - // const updateArchive = async () => { - // const blockHash = rootRollupOutput.header.hash(); - // await expectsDb.appendLeaves(MerkleTreeId.ARCHIVE, [blockHash.toBuffer()]); - // }; - - // const getTreeSnapshot = async (tree: MerkleTreeId) => { - // const treeInfo = await expectsDb.getTreeInfo(tree); - // return new AppendOnlyTreeSnapshot(Fr.fromBuffer(treeInfo.root), Number(treeInfo.size)); - // }; - - // const getPartialStateReference = async () => { - // return new PartialStateReference( - // await getTreeSnapshot(MerkleTreeId.NOTE_HASH_TREE), - // await getTreeSnapshot(MerkleTreeId.NULLIFIER_TREE), - // await getTreeSnapshot(MerkleTreeId.PUBLIC_DATA_TREE), - // ); - // }; - - // const getStateReference = async () => { - // return new StateReference( - // await getTreeSnapshot(MerkleTreeId.L1_TO_L2_MESSAGE_TREE), - // await getPartialStateReference(), - // ); - // }; - - // const buildMockSimulatorInputs = async () => { - // const kernelOutput = makePrivateKernelTailCircuitPublicInputs(); - // kernelOutput.constants.historicalHeader = await expectsDb.buildInitialHeader(); - // kernelOutput.needsAppLogic = false; - // kernelOutput.needsSetup = false; - // kernelOutput.needsTeardown = false; - - // const tx = makeProcessedTx( - // new Tx( - // kernelOutput, - // emptyProof, - // makeEmptyLogs(), - // makeEmptyLogs(), - // times(MAX_PUBLIC_CALL_STACK_LENGTH_PER_TX, makePublicCallRequest), - // ), - // ); - - // const txs = [tx, await makeEmptyProcessedTx()]; - - // // Calculate what would be the tree roots after the first tx and update mock circuit output - // await updateExpectedTreesFromTxs([txs[0]]); - // baseRollupOutputLeft.end = await getPartialStateReference(); - // baseRollupOutputLeft.txsEffectsHash = to2Fields(toTxEffect(tx).hash()); - - // // Same for the tx on the right - // await updateExpectedTreesFromTxs([txs[1]]); - // baseRollupOutputRight.end = await getPartialStateReference(); - // baseRollupOutputRight.txsEffectsHash = to2Fields(toTxEffect(tx).hash()); - - // // Update l1 to l2 message tree - // await updateL1ToL2MessageTree(mockL1ToL2Messages); - - // // Collect all new nullifiers, commitments, and contracts from all txs in this block - // const txEffects: TxEffect[] = txs.map(tx => toTxEffect(tx)); - - // const body = new Body(padArrayEnd(mockL1ToL2Messages, Fr.ZERO, NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP), txEffects); - // // We are constructing the block here just to get body hash/calldata hash so we can pass in an empty archive and header - // const l2Block = L2Block.fromFields({ - // archive: AppendOnlyTreeSnapshot.zero(), - // header: Header.empty(), - // // Only the values below go to body hash/calldata hash - // body, - // }); - - // // Now we update can make the final header, compute the block hash and update archive - // rootRollupOutput.header.globalVariables = globalVariables; - // rootRollupOutput.header.contentCommitment.txsEffectsHash = l2Block.body.getTxsEffectsHash(); - // rootRollupOutput.header.state = await getStateReference(); - - // await updateArchive(); - // rootRollupOutput.archive = await getTreeSnapshot(MerkleTreeId.ARCHIVE); - - // return txs; - // }; - describe('error handling', () => { beforeEach(async () => { builder = await ProvingOrchestrator.new(builderDb, new WASMSimulator(), prover); @@ -374,7 +293,9 @@ describe('prover/tx-prover', () => { const result = await blockTicket.provingPromise; expect(result.status).toBe(PROVING_STATUS.SUCCESS); - expect((result as ProvingSuccess).block.number).toEqual(blockNumber); + const finalisedBlock = await builder.finaliseBlock(); + + expect(finalisedBlock.block.number).toEqual(blockNumber); await updateExpectedTreesFromTxs(txs); const noteHashTreeAfter = await builderDb.getTreeInfo(MerkleTreeId.NOTE_HASH_TREE); @@ -400,23 +321,64 @@ describe('prover/tx-prover', () => { const result = await blockTicket.provingPromise; expect(result.status).toBe(PROVING_STATUS.SUCCESS); - expect((result as ProvingSuccess).block.number).toEqual(blockNumber); + const finalisedBlock = await builder.finaliseBlock(); + + expect(finalisedBlock.block.number).toEqual(blockNumber); }, 30_000); it('builds a block with 1 transaction', async () => { const txs = await Promise.all([makeEmptyProcessedTx()]); - const blockTicket = await builder.startNewBlock(txs.length, globalVariables, [], await makeEmptyProcessedTx()); + // This will need to be a 2 tx block + const blockTicket = await builder.startNewBlock(2, globalVariables, [], await makeEmptyProcessedTx()); for (const tx of txs) { await builder.addNewTx(tx); } + // we need to complete the block as we have not added a full set of txs + await builder.setBlockCompleted(); + const result = await blockTicket.provingPromise; expect(result.status).toBe(PROVING_STATUS.SUCCESS); - expect((result as ProvingSuccess).block.number).toEqual(blockNumber); + const finalisedBlock = await builder.finaliseBlock(); + + expect(finalisedBlock.block.number).toEqual(blockNumber); }, 30_000); + it('builds multiple blocks in sequence', async () => { + const numBlocks = 5; + let header = await builderDb.buildInitialHeader(); + + for (let i = 0; i < numBlocks; i++) { + const tx = await makeBloatedProcessedTx(i + 1); + const emptyTx = await makeEmptyProcessedTx(); + tx.data.constants.historicalHeader = header; + emptyTx.data.constants.historicalHeader = header; + + const blockNum = i + 1000; + + const globals = makeGlobals(blockNum); + + // This will need to be a 2 tx block + const blockTicket = await builder.startNewBlock(2, globals, [], emptyTx); + + await builder.addNewTx(tx); + + // we need to complete the block as we have not added a full set of txs + await builder.setBlockCompleted(); + + const result = await blockTicket.provingPromise; + expect(result.status).toBe(PROVING_STATUS.SUCCESS); + const finalisedBlock = await builder.finaliseBlock(); + + expect(finalisedBlock.block.number).toEqual(blockNum); + header = finalisedBlock.block.header; + + await builderDb.commit(); + } + }, 60_000); + it('builds a mixed L2 block', async () => { const txs = await Promise.all([ makeBloatedProcessedTx(1), @@ -440,7 +402,9 @@ describe('prover/tx-prover', () => { const result = await blockTicket.provingPromise; expect(result.status).toBe(PROVING_STATUS.SUCCESS); - expect((result as ProvingSuccess).block.number).toEqual(blockNumber); + const finalisedBlock = await builder.finaliseBlock(); + + expect(finalisedBlock.block.number).toEqual(blockNumber); }, 200_000); it('builds a block concurrently with transactions', async () => { @@ -467,62 +431,102 @@ describe('prover/tx-prover', () => { const result = await blockTicket.provingPromise; expect(result.status).toBe(PROVING_STATUS.SUCCESS); - expect((result as ProvingSuccess).block.number).toEqual(blockNumber); + const finalisedBlock = await builder.finaliseBlock(); + + expect(finalisedBlock.block.number).toEqual(blockNumber); }, 200_000); - // it('cancels current blocks and switches to new ones', async () => { - // const txs = await Promise.all([ - // makeBloatedProcessedTx(1), - // makeBloatedProcessedTx(2), - // makeBloatedProcessedTx(3), - // makeBloatedProcessedTx(4), - // ]); + it('cancels current block and switches to new ones', async () => { + const txs1 = await Promise.all([makeBloatedProcessedTx(1), makeBloatedProcessedTx(2)]); + + const txs2 = await Promise.all([makeBloatedProcessedTx(3), makeBloatedProcessedTx(4)]); - // const l1ToL2Messages = range(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, 1 + 0x400).map(fr); + const globals1: GlobalVariables = makeGlobals(100); + const globals2: GlobalVariables = makeGlobals(101); - // const blockPromise1 = await builder.startNewBlock( - // txs.length, - // globalVariables, - // l1ToL2Messages, - // await makeEmptyProcessedTx(), - // ); + const l1ToL2Messages = range(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, 1 + 0x400).map(fr); + + const blockTicket1 = await builder.startNewBlock(2, globals1, l1ToL2Messages, await makeEmptyProcessedTx()); + + await builder.addNewTx(txs1[0]); + await builder.addNewTx(txs1[1]); - // builder.addNewTx(txs[0]); + // Now we cancel the block. The first block will come to a stop as and when current proofs complete + builder.cancelBlock(); - // const blockPromise2 = await builder.startNewBlock( - // txs.length, - // globalVariables, - // l1ToL2Messages, - // await makeEmptyProcessedTx(), - // ); + const result1 = await blockTicket1.provingPromise; + + // in all likelihood, the block will have a failure code as we cancelled it + // however it may have actually completed proving before we cancelled in which case it could be a succes code + if (result1.status === PROVING_STATUS.FAILURE) { + expect((result1 as ProvingFailure).reason).toBe('Proving cancelled'); + } - // builder.addNewTx(txs[0]); + await builderDb.rollback(); + + const blockTicket2 = await builder.startNewBlock(2, globals2, l1ToL2Messages, await makeEmptyProcessedTx()); + + await builder.addNewTx(txs2[0]); + await builder.addNewTx(txs2[1]); + + const result2 = await blockTicket2.provingPromise; + expect(result2.status).toBe(PROVING_STATUS.SUCCESS); + const finalisedBlock = await builder.finaliseBlock(); + + expect(finalisedBlock.block.number).toEqual(101); + }, 10000); + + it('automatically cancels an incomplete block when starting a new one', async () => { + const txs1 = await Promise.all([makeBloatedProcessedTx(1), makeBloatedProcessedTx(2)]); + + const txs2 = await Promise.all([makeBloatedProcessedTx(3), makeBloatedProcessedTx(4)]); + + const globals1: GlobalVariables = makeGlobals(100); + const globals2: GlobalVariables = makeGlobals(101); + + const l1ToL2Messages = range(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, 1 + 0x400).map(fr); - // await expect(blockPromise1).rejects.toEqual('Block cancelled'); + const blockTicket1 = await builder.startNewBlock(2, globals1, l1ToL2Messages, await makeEmptyProcessedTx()); - // const result = await blockPromise2; - // expect(result.block.number).toEqual(blockNumber); - // }, 200_000); + await builder.addNewTx(txs1[0]); + + await builderDb.rollback(); + + const blockTicket2 = await builder.startNewBlock(2, globals2, l1ToL2Messages, await makeEmptyProcessedTx()); + + await builder.addNewTx(txs2[0]); + await builder.addNewTx(txs2[1]); + + const result1 = await blockTicket1.provingPromise; + expect(result1.status).toBe(PROVING_STATUS.FAILURE); + expect((result1 as ProvingFailure).reason).toBe('Proving cancelled'); + + const result2 = await blockTicket2.provingPromise; + expect(result2.status).toBe(PROVING_STATUS.SUCCESS); + const finalisedBlock = await builder.finaliseBlock(); + + expect(finalisedBlock.block.number).toEqual(101); + }, 10000); it('builds an unbalanced L2 block', async () => { const txs = await Promise.all([makeBloatedProcessedTx(1), makeBloatedProcessedTx(2), makeBloatedProcessedTx(3)]); const l1ToL2Messages = range(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, 1 + 0x400).map(fr); - const blockTicket = await builder.startNewBlock( - txs.length, - globalVariables, - l1ToL2Messages, - await makeEmptyProcessedTx(), - ); + // this needs to be a 4 tx block that will need to be completed + const blockTicket = await builder.startNewBlock(4, globalVariables, l1ToL2Messages, await makeEmptyProcessedTx()); for (const tx of txs) { await builder.addNewTx(tx); } + await builder.setBlockCompleted(); + const result = await blockTicket.provingPromise; expect(result.status).toBe(PROVING_STATUS.SUCCESS); - expect((result as ProvingSuccess).block.number).toEqual(blockNumber); + const finalisedBlock = await builder.finaliseBlock(); + + expect(finalisedBlock.block.number).toEqual(blockNumber); }, 200_000); it('throws if adding too many transactions', async () => { @@ -540,25 +544,75 @@ describe('prover/tx-prover', () => { } await expect(async () => await builder.addNewTx(await makeEmptyProcessedTx())).rejects.toThrow( - `Rollup already contains 4 transactions`, + 'Rollup not accepting further transactions', ); const result = await blockTicket.provingPromise; expect(result.status).toBe(PROVING_STATUS.SUCCESS); - expect((result as ProvingSuccess).block.number).toEqual(blockNumber); + const finalisedBlock = await builder.finaliseBlock(); + + expect(finalisedBlock.block.number).toEqual(blockNumber); }, 30_000); it('throws if adding a transaction before start', async () => { await expect(async () => await builder.addNewTx(await makeEmptyProcessedTx())).rejects.toThrow( `Invalid proving state, call startNewBlock before adding transactions`, ); - }, 30_000); + }, 1000); + + it('throws if completing a block before start', async () => { + await expect(async () => await builder.setBlockCompleted()).rejects.toThrow( + 'Invalid proving state, call startNewBlock before adding transactions or completing the block', + ); + }, 1000); + + it('throws if finalising an incompletre block', async () => { + await expect(async () => await builder.finaliseBlock()).rejects.toThrow( + 'Invalid proving state, a block must be proven before it can be finalised', + ); + }, 1000); + + it('throws if finalising an already finalised block', async () => { + const txs = await Promise.all([makeEmptyProcessedTx(), makeEmptyProcessedTx()]); + + const blockTicket = await builder.startNewBlock(txs.length, globalVariables, [], await makeEmptyProcessedTx()); + + for (const tx of txs) { + await builder.addNewTx(tx); + } + + const result = await blockTicket.provingPromise; + expect(result.status).toBe(PROVING_STATUS.SUCCESS); + const finalisedBlock = await builder.finaliseBlock(); + expect(finalisedBlock.block.number).toEqual(blockNumber); + await expect(async () => await builder.finaliseBlock()).rejects.toThrow('Block already finalised'); + }, 20000); + + it('throws if adding to a cancelled block', async () => { + await builder.startNewBlock(2, globalVariables, [], await makeEmptyProcessedTx()); + + builder.cancelBlock(); + + await expect(async () => await builder.addNewTx(await makeEmptyProcessedTx())).rejects.toThrow( + 'Rollup not accepting further transactions', + ); + }, 10000); + + it.each([[-4], [0], [1], [3], [8.1], [7]] as const)( + 'fails to start a block with %i transaxctions', + async (blockSize: number) => { + await expect( + async () => await builder.startNewBlock(blockSize, globalVariables, [], await makeEmptyProcessedTx()), + ).rejects.toThrow(`Length of txs for the block should be a power of two and at least two (got ${blockSize})`); + }, + 10000, + ); it('rejects if too many l1 to l2 messages are provided', async () => { // Assemble a fake transaction const l1ToL2Messages = new Array(100).fill(new Fr(0n)); await expect( - async () => await builder.startNewBlock(1, globalVariables, l1ToL2Messages, await makeEmptyProcessedTx()), + async () => await builder.startNewBlock(2, globalVariables, l1ToL2Messages, await makeEmptyProcessedTx()), ).rejects.toThrow('Too many L1 to L2 messages'); }); }); diff --git a/yarn-project/prover-client/src/orchestrator/orchestrator.ts b/yarn-project/prover-client/src/orchestrator/orchestrator.ts index 7a0a15e8e97..4d3baf6e150 100644 --- a/yarn-project/prover-client/src/orchestrator/orchestrator.ts +++ b/yarn-project/prover-client/src/orchestrator/orchestrator.ts @@ -1,5 +1,10 @@ import { Body, L2Block, MerkleTreeId, type ProcessedTx, type TxEffect, toTxEffect } from '@aztec/circuit-types'; -import { PROVING_STATUS, type ProvingResult, type ProvingTicket } from '@aztec/circuit-types/interfaces'; +import { + type BlockResult, + PROVING_STATUS, + type ProvingResult, + type ProvingTicket, +} from '@aztec/circuit-types/interfaces'; import { type CircuitSimulationStats } from '@aztec/circuit-types/stats'; import { type AppendOnlyTreeSnapshot, @@ -8,12 +13,15 @@ import { type BaseRollupInputs, Fr, type GlobalVariables, + L1_TO_L2_MSG_SUBTREE_HEIGHT, + L1_TO_L2_MSG_SUBTREE_SIBLING_PATH_LENGTH, NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, NUM_BASE_PARITY_PER_ROOT_PARITY, type Proof, type RootParityInput, RootParityInputs, } from '@aztec/circuits.js'; +import { makeTuple } from '@aztec/foundation/array'; import { padArrayEnd } from '@aztec/foundation/collection'; import { MemoryFifo } from '@aztec/foundation/fifo'; import { createDebugLogger } from '@aztec/foundation/log'; @@ -36,10 +44,12 @@ import { executeMergeRollupCircuit, executeRootParityCircuit, executeRootRollupCircuit, + getSubtreeSiblingPath, getTreeSnapshot, + validateRootOutput, validateTx, } from './block-building-helpers.js'; -import { type MergeRollupInputData, PROVING_JOB_TYPE, type ProvingJob, ProvingState } from './proving-state.js'; +import { type MergeRollupInputData, ProvingState } from './proving-state.js'; const logger = createDebugLogger('aztec:prover:proving-orchestrator'); @@ -51,7 +61,7 @@ const logger = createDebugLogger('aztec:prover:proving-orchestrator'); * 4. Once a transaction is proven, it will be incorporated into a merge proof * 5. Merge proofs are produced at each level of the tree until the root proof is produced * - * The proving implementation is determined by the provided prover implementation. This could be for example a local prover or a remote prover pool. + * The proving implementation is determined by the provided prover. This could be for example a local prover or a remote prover pool. */ const SLEEP_TIME = 50; @@ -62,6 +72,23 @@ enum PROMISE_RESULT { OPERATIONS, } +/** + * Enums and structs to communicate the type of work required in each request. + */ +export enum PROVING_JOB_TYPE { + STATE_UPDATE, + BASE_ROLLUP, + MERGE_ROLLUP, + ROOT_ROLLUP, + BASE_PARITY, + ROOT_PARITY, +} + +export type ProvingJob = { + type: PROVING_JOB_TYPE; + operation: () => Promise; +}; + /** * The orchestrator, managing the flow of recursive proving operations required to build the rollup proof tree. */ @@ -81,14 +108,15 @@ export class ProvingOrchestrator { this.simulator = new RealRollupCircuitSimulator(simulationProvider); } - public static new(db: MerkleTreeOperations, simulationProvider: SimulationProvider, prover: RollupProver) { + public static async new(db: MerkleTreeOperations, simulationProvider: SimulationProvider, prover: RollupProver) { const orchestrator = new ProvingOrchestrator(db, simulationProvider, getVerificationKeys(), prover); - orchestrator.start(); + await orchestrator.start(); return Promise.resolve(orchestrator); } public start() { this.jobProcessPromise = this.processJobQueue(); + return Promise.resolve(); } public async stop() { @@ -99,7 +127,7 @@ export class ProvingOrchestrator { /** * Starts off a new block - * @param numTxs - The number of real transactions in the block + * @param numTxs - The total number of transactions in the block. Must be a power of 2 * @param globalVariables - The global variables for the block * @param l1ToL2Messages - The l1 to l2 messages for the block * @param emptyTx - The instance of an empty transaction to be used to pad this block @@ -111,9 +139,13 @@ export class ProvingOrchestrator { l1ToL2Messages: Fr[], emptyTx: ProcessedTx, ): Promise { - if (this.provingState && !this.provingState.isFinished()) { - throw new Error("Can't start a new block until the previous block is finished"); + // Check that the length of the array of txs is a power of two + // See https://graphics.stanford.edu/~seander/bithacks.html#DetermineIfPowerOf2 + if (!Number.isInteger(numTxs) || numTxs < 2 || (numTxs & (numTxs - 1)) !== 0) { + throw new Error(`Length of txs for the block should be a power of two and at least two (got ${numTxs})`); } + // Cancel any currently proving block before starting a new one + this.cancelBlock(); logger.info(`Starting new block with ${numTxs} transactions`); // we start the block by enqueueing all of the base parity circuits let baseParityInputs: BaseParityInputs[] = []; @@ -127,11 +159,28 @@ export class ProvingOrchestrator { BaseParityInputs.fromSlice(l1ToL2MessagesPadded, i), ); - //TODO:(@PhilWindle) Temporary until we figure out when to perform L1 to L2 insertions to make state consistency easier. - await Promise.resolve(); + const messageTreeSnapshot = await getTreeSnapshot(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, this.db); + + const newL1ToL2MessageTreeRootSiblingPathArray = await getSubtreeSiblingPath( + MerkleTreeId.L1_TO_L2_MESSAGE_TREE, + L1_TO_L2_MSG_SUBTREE_HEIGHT, + this.db, + ); + + const newL1ToL2MessageTreeRootSiblingPath = makeTuple( + L1_TO_L2_MSG_SUBTREE_SIBLING_PATH_LENGTH, + i => + i < newL1ToL2MessageTreeRootSiblingPathArray.length ? newL1ToL2MessageTreeRootSiblingPathArray[i] : Fr.ZERO, + 0, + ); + + // Update the local trees to include the new l1 to l2 messages + await this.db.appendLeaves(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, l1ToL2MessagesPadded); + + let provingState: ProvingState | undefined = undefined; const promise = new Promise((resolve, reject) => { - this.provingState = new ProvingState( + provingState = new ProvingState( numTxs, resolve, reject, @@ -139,15 +188,19 @@ export class ProvingOrchestrator { l1ToL2MessagesPadded, baseParityInputs.length, emptyTx, + messageTreeSnapshot, + newL1ToL2MessageTreeRootSiblingPath, ); }).catch((reason: string) => ({ status: PROVING_STATUS.FAILURE, reason } as const)); for (let i = 0; i < baseParityInputs.length; i++) { - this.enqueueJob(this.provingState!.Id, PROVING_JOB_TYPE.BASE_PARITY, () => - this.runBaseParityCircuit(baseParityInputs[i], i, this.provingState!.Id), + this.enqueueJob(provingState, PROVING_JOB_TYPE.BASE_PARITY, () => + this.runBaseParityCircuit(provingState, baseParityInputs[i], i), ); } + this.provingState = provingState; + const ticket: ProvingTicket = { provingPromise: promise, }; @@ -163,44 +216,109 @@ export class ProvingOrchestrator { throw new Error(`Invalid proving state, call startNewBlock before adding transactions`); } - if (this.provingState.numTxs === this.provingState.transactionsReceived) { - throw new Error(`Rollup already contains ${this.provingState.transactionsReceived} transactions`); + if (!this.provingState.isAcceptingTransactions()) { + throw new Error(`Rollup not accepting further transactions`); } validateTx(tx); - logger.info(`Received transaction :${tx.hash}`); + logger.info(`Received transaction: ${tx.hash}`); // We start the transaction by enqueueing the state updates - const txIndex = this.provingState!.addNewTx(tx); + const txIndex = this.provingState.addNewTx(tx); // we start this transaction off by performing it's tree insertions and - await this.prepareBaseRollupInputs(BigInt(txIndex), tx, this.provingState!.globalVariables, this.provingState!.Id); - - if (this.provingState.transactionsReceived === this.provingState.numTxs) { - // we need to pad the rollup with empty transactions - const numPaddingTxs = this.provingState.numPaddingTxs; - for (let i = 0; i < numPaddingTxs; i++) { - const paddingTxIndex = this.provingState.addNewTx(this.provingState.emptyTx); - await this.prepareBaseRollupInputs( - BigInt(paddingTxIndex), - this.provingState!.emptyTx, - this.provingState!.globalVariables, - this.provingState!.Id, - ); - } + await this.prepareBaseRollupInputs(this.provingState, BigInt(txIndex), tx); + } + + /** + * Marks the block as full and pads it to the full power of 2 block size, no more transactions will be accepted. + */ + public async setBlockCompleted() { + if (!this.provingState) { + throw new Error(`Invalid proving state, call startNewBlock before adding transactions or completing the block`); + } + + // we need to pad the rollup with empty transactions + logger.info( + `Padding rollup with ${ + this.provingState.totalNumTxs - this.provingState.transactionsReceived + } empty transactions`, + ); + for (let i = this.provingState.transactionsReceived; i < this.provingState.totalNumTxs; i++) { + const paddingTxIndex = this.provingState.addNewTx(this.provingState.emptyTx); + await this.prepareBaseRollupInputs(this.provingState, BigInt(paddingTxIndex), this.provingState!.emptyTx); } } + /** + * Cancel any further proving of the block + */ + public cancelBlock() { + this.provingState?.cancel(); + } + + /** + * Performs the final tree update for the block and returns the fully proven block. + * @returns The fully proven block and proof. + */ + public async finaliseBlock() { + if (!this.provingState || !this.provingState.rootRollupPublicInputs || !this.provingState.finalProof) { + throw new Error(`Invalid proving state, a block must be proven before it can be finalised`); + } + if (this.provingState.block) { + throw new Error('Block already finalised'); + } + + const rootRollupOutputs = this.provingState.rootRollupPublicInputs; + + logger?.debug(`Updating and validating root trees`); + await this.db.updateArchive(rootRollupOutputs.header); + + await validateRootOutput(rootRollupOutputs, this.db); + + // Collect all new nullifiers, commitments, and contracts from all txs in this block + const nonEmptyTxEffects: TxEffect[] = this.provingState!.allTxs.map(tx => toTxEffect(tx)).filter( + txEffect => !txEffect.isEmpty(), + ); + const blockBody = new Body(nonEmptyTxEffects); + + const l2Block = L2Block.fromFields({ + archive: rootRollupOutputs.archive, + header: rootRollupOutputs.header, + body: blockBody, + }); + + if (!l2Block.body.getTxsEffectsHash().equals(rootRollupOutputs.header.contentCommitment.txsEffectsHash)) { + logger(inspect(blockBody)); + throw new Error( + `Txs effects hash mismatch, ${l2Block.body + .getTxsEffectsHash() + .toString('hex')} == ${rootRollupOutputs.header.contentCommitment.txsEffectsHash.toString('hex')} `, + ); + } + + logger.info(`Successfully proven block ${l2Block.number}!`); + + this.provingState.block = l2Block; + + const blockResult: BlockResult = { + proof: this.provingState.finalProof, + block: l2Block, + }; + + return blockResult; + } + /** * Enqueue a job to be scheduled - * @param stateIdentifier - For state Id verification + * @param provingState - The proving state object being operated on * @param jobType - The type of job to be queued * @param job - The actual job, returns a promise notifying of the job's completion */ - private enqueueJob(stateIdentifier: string, jobType: PROVING_JOB_TYPE, job: () => Promise) { - if (!this.provingState!.verifyState(stateIdentifier)) { - logger(`Discarding job for state ID: ${stateIdentifier}`); + private enqueueJob(provingState: ProvingState | undefined, jobType: PROVING_JOB_TYPE, job: () => Promise) { + if (!provingState?.verifyState()) { + logger(`Not enqueueing job, proving state invalid`); return; } // We use a 'safeJob'. We don't want promise rejections in the proving pool, we want to capture the error here @@ -210,7 +328,7 @@ export class ProvingOrchestrator { await job(); } catch (err) { logger.error(`Error thrown when proving job type ${PROVING_JOB_TYPE[jobType]}: ${err}`); - this.provingState!.reject(`${err}`, stateIdentifier); + provingState!.reject(`${err}`); } }; const provingJob: ProvingJob = { @@ -221,13 +339,12 @@ export class ProvingOrchestrator { } // Updates the merkle trees for a transaction. The first enqueued job for a transaction - private async prepareBaseRollupInputs( - index: bigint, - tx: ProcessedTx, - globalVariables: GlobalVariables, - stateIdentifier: string, - ) { - const inputs = await buildBaseRollupInput(tx, globalVariables, this.db); + private async prepareBaseRollupInputs(provingState: ProvingState | undefined, index: bigint, tx: ProcessedTx) { + if (!provingState?.verifyState()) { + logger('Not preparing base rollup inputs, state invalid'); + return; + } + const inputs = await buildBaseRollupInput(tx, provingState.globalVariables, this.db); const promises = [MerkleTreeId.NOTE_HASH_TREE, MerkleTreeId.NULLIFIER_TREE, MerkleTreeId.PUBLIC_DATA_TREE].map( async (id: MerkleTreeId) => { return { key: id, value: await getTreeSnapshot(id, this.db) }; @@ -237,18 +354,18 @@ export class ProvingOrchestrator { (await Promise.all(promises)).map(obj => [obj.key, obj.value]), ); - if (!this.provingState?.verifyState(stateIdentifier)) { - logger(`Discarding job for state ID: ${stateIdentifier}`); + if (!provingState?.verifyState()) { + logger(`Discarding proving job, state no longer valid`); return; } - - this.enqueueJob(stateIdentifier, PROVING_JOB_TYPE.BASE_ROLLUP, () => - this.runBaseRollup(index, tx, inputs, treeSnapshots, stateIdentifier), + this.enqueueJob(provingState, PROVING_JOB_TYPE.BASE_ROLLUP, () => + this.runBaseRollup(provingState, index, tx, inputs, treeSnapshots), ); } // Stores the intermediate inputs prepared for a merge proof private storeMergeInputs( + provingState: ProvingState, currentLevel: bigint, currentIndex: bigint, mergeInputs: [BaseOrMergeRollupPublicInputs, Proof], @@ -258,19 +375,23 @@ export class ProvingOrchestrator { const mergeIndex = 2n ** mergeLevel - 1n + indexWithinMergeLevel; const subscript = Number(mergeIndex); const indexWithinMerge = Number(currentIndex & 1n); - const ready = this.provingState!.storeMergeInputs(mergeInputs, indexWithinMerge, subscript); - return { ready, indexWithinMergeLevel, mergeLevel, mergeInputData: this.provingState!.getMergeInputs(subscript) }; + const ready = provingState.storeMergeInputs(mergeInputs, indexWithinMerge, subscript); + return { ready, indexWithinMergeLevel, mergeLevel, mergeInputData: provingState.getMergeInputs(subscript) }; } // Executes the base rollup circuit and stored the output as intermediate state for the parent merge/root circuit // Executes the next level of merge if all inputs are available private async runBaseRollup( + provingState: ProvingState | undefined, index: bigint, tx: ProcessedTx, inputs: BaseRollupInputs, treeSnapshots: Map, - stateIdentifier: string, ) { + if (!provingState?.verifyState()) { + logger('Not running base rollup, state invalid'); + return; + } const [duration, baseRollupOutputs] = await elapsed(() => executeBaseRollupCircuit(tx, inputs, treeSnapshots, this.simulator, this.prover, logger), ); @@ -281,23 +402,27 @@ export class ProvingOrchestrator { inputSize: inputs.toBuffer().length, outputSize: baseRollupOutputs[0].toBuffer().length, } satisfies CircuitSimulationStats); - if (!this.provingState?.verifyState(stateIdentifier)) { - logger(`Discarding job for state ID: ${stateIdentifier}`); + if (!provingState?.verifyState()) { + logger(`Discarding job as state no longer valid`); return; } - const currentLevel = this.provingState!.numMergeLevels + 1n; + const currentLevel = provingState.numMergeLevels + 1n; logger.info(`Completed base rollup at index ${index}, current level ${currentLevel}`); - this.storeAndExecuteNextMergeLevel(currentLevel, index, baseRollupOutputs, stateIdentifier); + this.storeAndExecuteNextMergeLevel(provingState, currentLevel, index, baseRollupOutputs); } // Executes the merge rollup circuit and stored the output as intermediate state for the parent merge/root circuit // Executes the next level of merge if all inputs are available private async runMergeRollup( + provingState: ProvingState | undefined, level: bigint, index: bigint, mergeInputData: MergeRollupInputData, - stateIdentifier: string, ) { + if (!provingState?.verifyState()) { + logger('Not running merge rollup, state invalid'); + return; + } const circuitInputs = createMergeRollupInputs( [mergeInputData.inputs[0]!, mergeInputData.proofs[0]!], [mergeInputData.inputs[1]!, mergeInputData.proofs[1]!], @@ -312,64 +437,52 @@ export class ProvingOrchestrator { inputSize: circuitInputs.toBuffer().length, outputSize: circuitOutputs[0].toBuffer().length, } satisfies CircuitSimulationStats); - if (!this.provingState?.verifyState(stateIdentifier)) { - logger(`Discarding job for state ID: ${stateIdentifier}`); + if (!provingState?.verifyState()) { + logger(`Discarding job as state no longer valid`); return; } logger.info(`Completed merge rollup at level ${level}, index ${index}`); - this.storeAndExecuteNextMergeLevel(level, index, circuitOutputs, stateIdentifier); + this.storeAndExecuteNextMergeLevel(provingState, level, index, circuitOutputs); } // Executes the root rollup circuit - private async runRootRollup( - mergeInputData: MergeRollupInputData, - rootParityInput: RootParityInput, - stateIdentifier: string, - ) { + private async runRootRollup(provingState: ProvingState | undefined) { + if (!provingState?.verifyState()) { + logger('Not running root rollup, state no longer valid'); + return; + } + const mergeInputData = provingState.getMergeInputs(0); + const rootParityInput = provingState.finalRootParityInput!; const [circuitsOutput, proof] = await executeRootRollupCircuit( [mergeInputData.inputs[0]!, mergeInputData.proofs[0]!], [mergeInputData.inputs[1]!, mergeInputData.proofs[1]!], rootParityInput, - this.provingState!.newL1ToL2Messages, + provingState.newL1ToL2Messages, + provingState.messageTreeSnapshot, + provingState.messageTreeRootSiblingPath, this.simulator, this.prover, this.db, logger, ); logger.info(`Completed root rollup`); - // Collect all new nullifiers, commitments, and contracts from all txs in this block - const nonEmptyTxEffects: TxEffect[] = this.provingState!.allTxs.map(tx => toTxEffect(tx)).filter( - txEffect => !txEffect.isEmpty(), - ); - const blockBody = new Body(nonEmptyTxEffects); - - const l2Block = L2Block.fromFields({ - archive: circuitsOutput.archive, - header: circuitsOutput.header, - body: blockBody, - }); - if (!l2Block.body.getTxsEffectsHash().equals(circuitsOutput.header.contentCommitment.txsEffectsHash)) { - logger(inspect(blockBody)); - throw new Error( - `Txs effects hash mismatch, ${l2Block.body - .getTxsEffectsHash() - .toString('hex')} == ${circuitsOutput.header.contentCommitment.txsEffectsHash.toString('hex')} `, - ); - } + provingState.rootRollupPublicInputs = circuitsOutput; + provingState.finalProof = proof; const provingResult: ProvingResult = { status: PROVING_STATUS.SUCCESS, - block: l2Block, - proof, }; - logger.info(`Successfully proven block ${l2Block.number}!`); - this.provingState!.resolve(provingResult, stateIdentifier); + provingState.resolve(provingResult); } // Executes the base parity circuit and stores the intermediate state for the root parity circuit // Enqueues the root parity circuit if all inputs are available - private async runBaseParityCircuit(inputs: BaseParityInputs, index: number, stateIdentifier: string) { + private async runBaseParityCircuit(provingState: ProvingState | undefined, inputs: BaseParityInputs, index: number) { + if (!provingState?.verifyState()) { + logger('Not running base parity, state no longer valid'); + return; + } const [duration, circuitOutputs] = await elapsed(() => executeBaseParityCircuit(inputs, this.simulator, this.prover, logger), ); @@ -380,27 +493,32 @@ export class ProvingOrchestrator { inputSize: inputs.toBuffer().length, outputSize: circuitOutputs.toBuffer().length, } satisfies CircuitSimulationStats); - if (!this.provingState?.verifyState(stateIdentifier)) { - logger(`Discarding job for state ID: ${stateIdentifier}`); + + if (!provingState?.verifyState()) { + logger(`Discarding job as state no longer valid`); return; } - this.provingState!.setRootParityInputs(circuitOutputs, index); + provingState.setRootParityInputs(circuitOutputs, index); - if (!this.provingState!.areRootParityInputsReady()) { + if (!provingState.areRootParityInputsReady()) { // not ready to run the root parity circuit yet return; } const rootParityInputs = new RootParityInputs( - this.provingState!.rootParityInput as Tuple, + provingState.rootParityInput as Tuple, ); - this.enqueueJob(stateIdentifier, PROVING_JOB_TYPE.ROOT_PARITY, () => - this.runRootParityCircuit(rootParityInputs, stateIdentifier), + this.enqueueJob(provingState, PROVING_JOB_TYPE.ROOT_PARITY, () => + this.runRootParityCircuit(provingState, rootParityInputs), ); } // Runs the root parity circuit ans stored the outputs // Enqueues the root rollup proof if all inputs are available - private async runRootParityCircuit(inputs: RootParityInputs, stateIdentifier: string) { + private async runRootParityCircuit(provingState: ProvingState | undefined, inputs: RootParityInputs) { + if (!provingState?.verifyState()) { + logger(`Not running root parity circuit as state is no longer valid`); + return; + } const [duration, circuitOutputs] = await elapsed(() => executeRootParityCircuit(inputs, this.simulator, this.prover, logger), ); @@ -411,35 +529,30 @@ export class ProvingOrchestrator { inputSize: inputs.toBuffer().length, outputSize: circuitOutputs.toBuffer().length, } satisfies CircuitSimulationStats); - if (!this.provingState?.verifyState(stateIdentifier)) { - logger(`Discarding job for state ID: ${stateIdentifier}`); + + if (!provingState?.verifyState()) { + logger(`Discarding job as state no longer valid`); return; } - this.provingState!.finalRootParityInput = circuitOutputs; - this.checkAndExecuteRootRollup(stateIdentifier); + provingState!.finalRootParityInput = circuitOutputs; + this.checkAndExecuteRootRollup(provingState); } - private checkAndExecuteRootRollup(stateIdentifier: string) { - if (!this.provingState!.isReadyForRootRollup()) { - logger('Not ready for root'); + private checkAndExecuteRootRollup(provingState: ProvingState | undefined) { + if (!provingState?.isReadyForRootRollup()) { + logger('Not ready for root rollup'); return; } - this.enqueueJob(stateIdentifier, PROVING_JOB_TYPE.ROOT_ROLLUP, () => - this.runRootRollup( - this.provingState!.getMergeInputs(0)!, - this.provingState!.finalRootParityInput!, - stateIdentifier, - ), - ); + this.enqueueJob(provingState, PROVING_JOB_TYPE.ROOT_ROLLUP, () => this.runRootRollup(provingState)); } private storeAndExecuteNextMergeLevel( + provingState: ProvingState, currentLevel: bigint, currentIndex: bigint, mergeInputData: [BaseOrMergeRollupPublicInputs, Proof], - stateIdentifier: string, ) { - const result = this.storeMergeInputs(currentLevel, currentIndex, mergeInputData); + const result = this.storeMergeInputs(provingState, currentLevel, currentIndex, mergeInputData); // Are we ready to execute the next circuit? if (!result.ready) { @@ -447,11 +560,11 @@ export class ProvingOrchestrator { } if (result.mergeLevel === 0n) { - this.checkAndExecuteRootRollup(stateIdentifier); + this.checkAndExecuteRootRollup(provingState); } else { // onto the next merge level - this.enqueueJob(stateIdentifier, PROVING_JOB_TYPE.MERGE_ROLLUP, () => - this.runMergeRollup(result.mergeLevel, result.indexWithinMergeLevel, result.mergeInputData, stateIdentifier), + this.enqueueJob(provingState, PROVING_JOB_TYPE.MERGE_ROLLUP, () => + this.runMergeRollup(provingState, result.mergeLevel, result.indexWithinMergeLevel, result.mergeInputData), ); } } diff --git a/yarn-project/prover-client/src/orchestrator/proving-state.ts b/yarn-project/prover-client/src/orchestrator/proving-state.ts index c21f7e4e385..4201de80a35 100644 --- a/yarn-project/prover-client/src/orchestrator/proving-state.ts +++ b/yarn-project/prover-client/src/orchestrator/proving-state.ts @@ -1,59 +1,54 @@ -import { type ProcessedTx, type ProvingResult } from '@aztec/circuit-types'; +import { type L2Block, type ProcessedTx, type ProvingResult } from '@aztec/circuit-types'; import { + type AppendOnlyTreeSnapshot, type BaseOrMergeRollupPublicInputs, type Fr, type GlobalVariables, + type L1_TO_L2_MSG_SUBTREE_SIBLING_PATH_LENGTH, type NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, type Proof, type RootParityInput, + type RootRollupPublicInputs, } from '@aztec/circuits.js'; -import { randomBytes } from '@aztec/foundation/crypto'; import { type Tuple } from '@aztec/foundation/serialize'; -/** - * Enums and structs to communicate the type of work required in each request. - */ -export enum PROVING_JOB_TYPE { - STATE_UPDATE, - BASE_ROLLUP, - MERGE_ROLLUP, - ROOT_ROLLUP, - BASE_PARITY, - ROOT_PARITY, -} - -export type ProvingJob = { - type: PROVING_JOB_TYPE; - operation: () => Promise; -}; - export type MergeRollupInputData = { inputs: [BaseOrMergeRollupPublicInputs | undefined, BaseOrMergeRollupPublicInputs | undefined]; proofs: [Proof | undefined, Proof | undefined]; }; +enum PROVING_STATE_LIFECYCLE { + PROVING_STATE_CREATED, + PROVING_STATE_FULL, + PROVING_STATE_RESOLVED, + PROVING_STATE_REJECTED, +} + /** * The current state of the proving schedule. Contains the raw inputs (txs) and intermediate state to generate every constituent proof in the tree. * Carries an identifier so we can identify if the proving state is discarded and a new one started. * Captures resolve and reject callbacks to provide a promise base interface to the consumer of our proving. */ export class ProvingState { - private stateIdentifier: string; + private provingStateLifecyle = PROVING_STATE_LIFECYCLE.PROVING_STATE_CREATED; private mergeRollupInputs: MergeRollupInputData[] = []; private rootParityInputs: Array = []; private finalRootParityInputs: RootParityInput | undefined; - private finished = false; + public rootRollupPublicInputs: RootRollupPublicInputs | undefined; + public finalProof: Proof | undefined; + public block: L2Block | undefined; private txs: ProcessedTx[] = []; constructor( - public readonly numTxs: number, + public readonly totalNumTxs: number, private completionCallback: (result: ProvingResult) => void, private rejectionCallback: (reason: string) => void, public readonly globalVariables: GlobalVariables, public readonly newL1ToL2Messages: Tuple, numRootParityInputs: number, public readonly emptyTx: ProcessedTx, + public readonly messageTreeSnapshot: AppendOnlyTreeSnapshot, + public readonly messageTreeRootSiblingPath: Tuple, ) { - this.stateIdentifier = randomBytes(32).toString('hex'); this.rootParityInputs = Array.from({ length: numRootParityInputs }).map(_ => undefined); } @@ -65,22 +60,11 @@ export class ProvingState { return this.baseMergeLevel; } - public get Id() { - return this.stateIdentifier; - } - - public get numPaddingTxs() { - return this.totalNumTxs - this.numTxs; - } - - public get totalNumTxs() { - const realTxs = Math.max(2, this.numTxs); - const pow2Txs = Math.ceil(Math.log2(realTxs)); - return 2 ** pow2Txs; - } - public addNewTx(tx: ProcessedTx) { this.txs.push(tx); + if (this.txs.length === this.totalNumTxs) { + this.provingStateLifecyle = PROVING_STATE_LIFECYCLE.PROVING_STATE_FULL; + } return this.txs.length - 1; } @@ -100,8 +84,15 @@ export class ProvingState { return this.rootParityInputs; } - public verifyState(stateId: string) { - return stateId === this.stateIdentifier && !this.finished; + public verifyState() { + return ( + this.provingStateLifecyle === PROVING_STATE_LIFECYCLE.PROVING_STATE_CREATED || + this.provingStateLifecyle === PROVING_STATE_LIFECYCLE.PROVING_STATE_FULL + ); + } + + public isAcceptingTransactions() { + return this.provingStateLifecyle === PROVING_STATE_LIFECYCLE.PROVING_STATE_CREATED; } public get allTxs() { @@ -134,16 +125,11 @@ export class ProvingState { } public isReadyForRootRollup() { - if (this.mergeRollupInputs[0] === undefined) { - return false; - } - if (this.mergeRollupInputs[0].inputs.findIndex(p => !p) !== -1) { - return false; - } - if (this.finalRootParityInput === undefined) { - return false; - } - return true; + return !( + this.mergeRollupInputs[0] === undefined || + this.finalRootParityInput === undefined || + this.mergeRollupInputs[0].inputs.findIndex(p => !p) !== -1 + ); } public setRootParityInputs(inputs: RootParityInput, index: number) { @@ -154,29 +140,23 @@ export class ProvingState { return this.rootParityInputs.findIndex(p => !p) === -1; } - public reject(reason: string, stateIdentifier: string) { - if (!this.verifyState(stateIdentifier)) { - return; - } - if (this.finished) { + public cancel() { + this.reject('Proving cancelled'); + } + + public reject(reason: string) { + if (!this.verifyState()) { return; } - this.finished = true; + this.provingStateLifecyle = PROVING_STATE_LIFECYCLE.PROVING_STATE_REJECTED; this.rejectionCallback(reason); } - public resolve(result: ProvingResult, stateIdentifier: string) { - if (!this.verifyState(stateIdentifier)) { + public resolve(result: ProvingResult) { + if (!this.verifyState()) { return; } - if (this.finished) { - return; - } - this.finished = true; + this.provingStateLifecyle = PROVING_STATE_LIFECYCLE.PROVING_STATE_RESOLVED; this.completionCallback(result); } - - public isFinished() { - return this.finished; - } } 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 a9cb35d5f7e..61b05ed416f 100644 --- a/yarn-project/prover-client/src/tx-prover/tx-prover.ts +++ b/yarn-project/prover-client/src/tx-prover/tx-prover.ts @@ -1,5 +1,5 @@ import { type ProcessedTx } from '@aztec/circuit-types'; -import { type ProverClient, type ProvingTicket } from '@aztec/circuit-types/interfaces'; +import { type BlockResult, type ProverClient, type ProvingTicket } from '@aztec/circuit-types/interfaces'; import { type Fr, type GlobalVariables } from '@aztec/circuits.js'; import { type SimulationProvider } from '@aztec/simulator'; import { type WorldStateSynchronizer } from '@aztec/world-state'; @@ -15,7 +15,7 @@ import { EmptyRollupProver } from '../prover/empty.js'; export class TxProver implements ProverClient { private orchestrator: ProvingOrchestrator; constructor( - worldStateSynchronizer: WorldStateSynchronizer, + private worldStateSynchronizer: WorldStateSynchronizer, simulationProvider: SimulationProvider, protected vks: VerificationKeys, ) { @@ -31,8 +31,7 @@ export class TxProver implements ProverClient { * Starts the prover instance */ public start() { - this.orchestrator.start(); - return Promise.resolve(); + return this.orchestrator.start(); } /** @@ -58,16 +57,51 @@ export class TxProver implements ProverClient { return prover; } - public startNewBlock( + /** + * Cancels any block that is currently being built and prepares for a new one to be built + * @param numTxs - The complete size of the block, must be a power of 2 + * @param globalVariables - The global variables for this block + * @param l1ToL2Messages - The set of L1 to L2 messages to be included in this block + * @param emptyTx - An instance of an empty transaction to be used in this block + */ + public async startNewBlock( numTxs: number, globalVariables: GlobalVariables, newL1ToL2Messages: Fr[], emptyTx: ProcessedTx, ): Promise { + const previousBlockNumber = globalVariables.blockNumber.toNumber() - 1; + await this.worldStateSynchronizer.syncImmediate(previousBlockNumber); return this.orchestrator.startNewBlock(numTxs, globalVariables, newL1ToL2Messages, emptyTx); } + /** + * Add a processed transaction to the current block + * @param tx - The transaction to be added + */ public addNewTx(tx: ProcessedTx): Promise { return this.orchestrator.addNewTx(tx); } + + /** + * Cancels the block currently being proven. Proofs already bring built may continue but further proofs should not be started. + */ + public cancelBlock(): void { + this.orchestrator.cancelBlock(); + } + + /** + * Performs the final archive tree insertion for this block and returns the L2Block and Proof instances + */ + public finaliseBlock(): Promise { + return this.orchestrator.finaliseBlock(); + } + + /** + * Mark the block as having all the transactions it is going to contain. + * Will pad the block to it's complete size with empty transactions and prove all the way to the root rollup. + */ + public setBlockCompleted(): Promise { + return this.orchestrator.setBlockCompleted(); + } } diff --git a/yarn-project/sequencer-client/src/sequencer/public_processor.test.ts b/yarn-project/sequencer-client/src/sequencer/public_processor.test.ts index b1794ec5999..0e572d0a1f9 100644 --- a/yarn-project/sequencer-client/src/sequencer/public_processor.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/public_processor.test.ts @@ -1,4 +1,5 @@ import { + type BlockProver, EncryptedTxL2Logs, type FunctionCall, type ProcessedTx, @@ -51,12 +52,14 @@ import { type PublicKernelCircuitSimulator } from '../simulator/index.js'; import { type ContractsDataSourcePublicDB, type WorldStatePublicDB } from '../simulator/public_executor.js'; import { RealPublicKernelCircuitSimulator } from '../simulator/public_kernel.js'; import { PublicProcessor } from './public_processor.js'; +import { type TxValidator } from './tx_validator.js'; describe('public_processor', () => { let db: MockProxy; let publicExecutor: MockProxy; let publicContractsDB: MockProxy; let publicWorldStateDB: MockProxy; + let prover: MockProxy; let proof: Proof; let root: Buffer; @@ -68,6 +71,7 @@ describe('public_processor', () => { publicExecutor = mock(); publicContractsDB = mock(); publicWorldStateDB = mock(); + prover = mock(); proof = makeEmptyProof(); root = Buffer.alloc(32, 5); @@ -98,7 +102,7 @@ describe('public_processor', () => { }); const hash = tx.getTxHash(); - const [processed, failed] = await processor.process([tx]); + const [processed, failed] = await processor.process([tx], 1, prover); expect(processed.length).toBe(1); @@ -126,19 +130,22 @@ describe('public_processor', () => { } expect(failed).toEqual([]); + + expect(prover.addNewTx).toHaveBeenCalledWith(p); }); it('returns failed txs without aborting entire operation', async function () { publicExecutor.simulate.mockRejectedValue(new SimulationError(`Failed`, [])); const tx = mockTx(1, { numberOfNonRevertiblePublicCallRequests: 0, numberOfRevertiblePublicCallRequests: 1 }); - const [processed, failed] = await processor.process([tx]); + const [processed, failed] = await processor.process([tx], 1, prover); expect(processed).toEqual([]); expect(failed[0].tx).toEqual(tx); expect(failed[0].error).toEqual(new SimulationError(`Failed`, [])); expect(publicWorldStateDB.commit).toHaveBeenCalledTimes(0); expect(publicWorldStateDB.rollbackToCommit).toHaveBeenCalledTimes(1); + expect(prover.addNewTx).toHaveBeenCalledTimes(0); }); }); @@ -180,7 +187,7 @@ describe('public_processor', () => { throw new Error(`Unexpected execution request: ${execution}`); }); - const [processed, failed] = await processor.process([tx]); + const [processed, failed] = await processor.process([tx], 1, prover); expect(processed).toHaveLength(1); expect(processed).toEqual([expectedTxByHash(tx)]); @@ -188,6 +195,8 @@ describe('public_processor', () => { expect(publicExecutor.simulate).toHaveBeenCalledTimes(2); expect(publicWorldStateDB.commit).toHaveBeenCalledTimes(1); expect(publicWorldStateDB.rollbackToCommit).toHaveBeenCalledTimes(0); + + expect(prover.addNewTx).toHaveBeenCalledWith(processed[0]); }); it('runs a tx with an enqueued public call with nested execution', async function () { @@ -206,7 +215,7 @@ describe('public_processor', () => { publicExecutor.simulate.mockResolvedValue(publicExecutionResult); - const [processed, failed] = await processor.process([tx]); + const [processed, failed] = await processor.process([tx], 1, prover); expect(processed).toHaveLength(1); expect(processed).toEqual([expectedTxByHash(tx)]); @@ -216,6 +225,67 @@ describe('public_processor', () => { expect(publicWorldStateDB.rollbackToCheckpoint).toHaveBeenCalledTimes(0); expect(publicWorldStateDB.commit).toHaveBeenCalledTimes(1); expect(publicWorldStateDB.rollbackToCommit).toHaveBeenCalledTimes(0); + + expect(prover.addNewTx).toHaveBeenCalledWith(processed[0]); + }); + + it('does not attempt to overfill a block', async function () { + const txs = Array.from([1, 2, 3], index => + mockTx(index, { numberOfNonRevertiblePublicCallRequests: 0, numberOfRevertiblePublicCallRequests: 1 }), + ); + + let txCount = 0; + + publicExecutor.simulate.mockImplementation(execution => { + const tx = txs[txCount++]; + for (const request of tx.enqueuedPublicFunctionCalls) { + if (execution.contractAddress.equals(request.contractAddress)) { + const result = PublicExecutionResultBuilder.fromPublicCallRequest({ request }).build(); + // result.unencryptedLogs = tx.unencryptedLogs.functionLogs[0]; + return Promise.resolve(result); + } + } + throw new Error(`Unexpected execution request: ${execution}`); + }); + + // We are passing 3 txs but only 2 can fit in the block + const [processed, failed] = await processor.process(txs, 2, prover); + + expect(processed).toHaveLength(2); + expect(processed).toEqual([expectedTxByHash(txs[0]), expectedTxByHash(txs[1])]); + expect(failed).toHaveLength(0); + expect(publicExecutor.simulate).toHaveBeenCalledTimes(2); + expect(publicWorldStateDB.commit).toHaveBeenCalledTimes(2); + expect(publicWorldStateDB.rollbackToCommit).toHaveBeenCalledTimes(0); + + expect(prover.addNewTx).toHaveBeenCalledWith(processed[0]); + expect(prover.addNewTx).toHaveBeenCalledWith(processed[1]); + }); + + it('does not send a transaction to the prover if validation fails', async function () { + const tx = mockTx(1, { numberOfNonRevertiblePublicCallRequests: 0, numberOfRevertiblePublicCallRequests: 1 }); + + publicExecutor.simulate.mockImplementation(execution => { + for (const request of tx.enqueuedPublicFunctionCalls) { + if (execution.contractAddress.equals(request.contractAddress)) { + const result = PublicExecutionResultBuilder.fromPublicCallRequest({ request }).build(); + // result.unencryptedLogs = tx.unencryptedLogs.functionLogs[0]; + return Promise.resolve(result); + } + } + throw new Error(`Unexpected execution request: ${execution}`); + }); + + const txValidator: MockProxy = mock(); + txValidator.validateTxs.mockRejectedValue([[], [tx]]); + + const [processed, failed] = await processor.process([tx], 1, prover, txValidator); + + expect(processed).toHaveLength(0); + expect(failed).toHaveLength(1); + expect(publicExecutor.simulate).toHaveBeenCalledTimes(1); + + expect(prover.addNewTx).toHaveBeenCalledTimes(0); }); it('rolls back app logic db updates on failed public execution, but persists setup/teardown', async function () { @@ -312,7 +382,7 @@ describe('public_processor', () => { const appLogicSpy = jest.spyOn(publicKernel, 'publicKernelCircuitAppLogic'); const teardownSpy = jest.spyOn(publicKernel, 'publicKernelCircuitTeardown'); - const [processed, failed] = await processor.process([tx]); + const [processed, failed] = await processor.process([tx], 1, prover); expect(processed).toHaveLength(1); expect(processed).toEqual([expectedTxByHash(tx)]); @@ -337,6 +407,8 @@ describe('public_processor', () => { ); expect(txEffect.encryptedLogs.getTotalLogCount()).toBe(0); expect(txEffect.unencryptedLogs.getTotalLogCount()).toBe(0); + + expect(prover.addNewTx).toHaveBeenCalledWith(processed[0]); }); it('fails a transaction that reverts in setup', async function () { @@ -425,7 +497,7 @@ describe('public_processor', () => { const appLogicSpy = jest.spyOn(publicKernel, 'publicKernelCircuitAppLogic'); const teardownSpy = jest.spyOn(publicKernel, 'publicKernelCircuitTeardown'); - const [processed, failed] = await processor.process([tx]); + const [processed, failed] = await processor.process([tx], 1, prover); expect(processed).toHaveLength(0); expect(failed).toHaveLength(1); @@ -440,6 +512,8 @@ describe('public_processor', () => { expect(publicWorldStateDB.rollbackToCheckpoint).toHaveBeenCalledTimes(0); expect(publicWorldStateDB.commit).toHaveBeenCalledTimes(0); expect(publicWorldStateDB.rollbackToCommit).toHaveBeenCalledTimes(1); + + expect(prover.addNewTx).toHaveBeenCalledTimes(0); }); it('fails a transaction that reverts in teardown', async function () { @@ -528,7 +602,7 @@ describe('public_processor', () => { const appLogicSpy = jest.spyOn(publicKernel, 'publicKernelCircuitAppLogic'); const teardownSpy = jest.spyOn(publicKernel, 'publicKernelCircuitTeardown'); - const [processed, failed] = await processor.process([tx]); + const [processed, failed] = await processor.process([tx], 1, prover); expect(processed).toHaveLength(0); expect(failed).toHaveLength(1); @@ -542,6 +616,8 @@ describe('public_processor', () => { expect(publicWorldStateDB.rollbackToCheckpoint).toHaveBeenCalledTimes(0); expect(publicWorldStateDB.commit).toHaveBeenCalledTimes(0); expect(publicWorldStateDB.rollbackToCommit).toHaveBeenCalledTimes(1); + + expect(prover.addNewTx).toHaveBeenCalledTimes(0); }); it('runs a tx with setup and teardown phases', async function () { @@ -635,7 +711,7 @@ describe('public_processor', () => { const appLogicSpy = jest.spyOn(publicKernel, 'publicKernelCircuitAppLogic'); const teardownSpy = jest.spyOn(publicKernel, 'publicKernelCircuitTeardown'); - const [processed, failed] = await processor.process([tx]); + const [processed, failed] = await processor.process([tx], 1, prover); expect(processed).toHaveLength(1); expect(processed).toEqual([expectedTxByHash(tx)]); @@ -663,6 +739,8 @@ describe('public_processor', () => { ); expect(txEffect.encryptedLogs.getTotalLogCount()).toBe(0); expect(txEffect.unencryptedLogs.getTotalLogCount()).toBe(0); + + expect(prover.addNewTx).toHaveBeenCalledWith(processed[0]); }); }); }); diff --git a/yarn-project/sequencer-client/src/sequencer/public_processor.ts b/yarn-project/sequencer-client/src/sequencer/public_processor.ts index 0b451883d7e..0d03d43d6d9 100644 --- a/yarn-project/sequencer-client/src/sequencer/public_processor.ts +++ b/yarn-project/sequencer-client/src/sequencer/public_processor.ts @@ -1,4 +1,5 @@ import { + type BlockProver, type FailedTx, type ProcessedTx, type SimulationError, @@ -22,6 +23,7 @@ import { ContractsDataSourcePublicDB, WorldStateDB, WorldStatePublicDB } from '. import { RealPublicKernelCircuitSimulator } from '../simulator/public_kernel.js'; import { type AbstractPhaseManager, PublicKernelPhase } from './abstract_phase_manager.js'; import { PhaseManagerFactory } from './phase_manager_factory.js'; +import { type TxValidator } from './tx_validator.js'; /** * Creates new instances of PublicProcessor given the provided merkle tree db and contract data source. @@ -84,7 +86,12 @@ export class PublicProcessor { * @param txs - Txs to process. * @returns The list of processed txs with their circuit simulation outputs. */ - public async process(txs: Tx[]): Promise<[ProcessedTx[], FailedTx[], ProcessReturnValues[]]> { + public async process( + txs: Tx[], + maxTransactions = txs.length, + blockProver?: BlockProver, + txValidator?: TxValidator, + ): Promise<[ProcessedTx[], FailedTx[], ProcessReturnValues[]]> { // The processor modifies the tx objects in place, so we need to clone them. txs = txs.map(tx => Tx.clone(tx)); const result: ProcessedTx[] = []; @@ -92,11 +99,30 @@ export class PublicProcessor { const returns: ProcessReturnValues[] = []; for (const tx of txs) { + // only process up to the limit of the block + if (result.length >= maxTransactions) { + break; + } try { const [processedTx, returnValues] = !tx.hasPublicCalls() ? [makeProcessedTx(tx, tx.data.toKernelCircuitPublicInputs(), tx.proof)] : await this.processTxWithPublicCalls(tx); validateProcessedTx(processedTx); + // Re-validate the transaction + if (txValidator) { + // Only accept processed transactions that are not double-spends, + // public functions emitting nullifiers would pass earlier check but fail here. + // Note that we're checking all nullifiers generated in the private execution twice, + // we could store the ones already checked and skip them here as an optimization. + const [_, invalid] = await txValidator.validateTxs([processedTx]); + if (invalid.length) { + throw new Error(`Transaction ${invalid[0].hash} invalid after processing public functions`); + } + } + // if we were given a prover then send the transaction to it for proving + if (blockProver) { + await blockProver.addNewTx(processedTx); + } result.push(processedTx); returns.push(returnValues); } catch (err: any) { @@ -120,7 +146,7 @@ export class PublicProcessor { */ public makeEmptyProcessedTx(): ProcessedTx { const { chainId, version } = this.globalVariables; - return makeEmptyProcessedTx(this.historicalHeader, chainId, version); + return makeEmptyProcessedTx(this.historicalHeader.clone(), chainId, version); } private async processTxWithPublicCalls(tx: Tx): Promise<[ProcessedTx, ProcessReturnValues | undefined]> { diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index 1b875fbbcf2..fafbf634235 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -122,8 +122,6 @@ describe('sequencer', () => { const proof = makeEmptyProof(); const result: ProvingSuccess = { status: PROVING_STATUS.SUCCESS, - proof, - block, }; const ticket: ProvingTicket = { provingPromise: Promise.resolve(result), @@ -131,6 +129,7 @@ describe('sequencer', () => { p2p.getTxs.mockResolvedValueOnce([tx]); proverClient.startNewBlock.mockResolvedValueOnce(ticket); + proverClient.finaliseBlock.mockResolvedValue({ block, proof }); publisher.processL2Block.mockResolvedValueOnce(true); globalVariableBuilder.buildGlobalVariables.mockResolvedValueOnce( new GlobalVariables(chainId, version, new Fr(lastBlockNumber + 1), Fr.ZERO, coinbase, feeRecipient), @@ -140,13 +139,13 @@ describe('sequencer', () => { await sequencer.work(); expect(proverClient.startNewBlock).toHaveBeenCalledWith( - 1, + 2, new GlobalVariables(chainId, version, new Fr(lastBlockNumber + 1), Fr.ZERO, coinbase, feeRecipient), Array(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP).fill(new Fr(0n)), publicProcessor.makeEmptyProcessedTx(), ); - expect(proverClient.addNewTx).toHaveBeenCalledWith(expect.objectContaining({ hash: tx.getTxHash() })); expect(publisher.processL2Block).toHaveBeenCalledWith(block); + expect(proverClient.cancelBlock).toHaveBeenCalledTimes(0); }); it('builds a block out of several txs rejecting double spends', async () => { @@ -159,8 +158,6 @@ describe('sequencer', () => { const proof = makeEmptyProof(); const result: ProvingSuccess = { status: PROVING_STATUS.SUCCESS, - proof, - block, }; const ticket: ProvingTicket = { provingPromise: Promise.resolve(result), @@ -168,6 +165,7 @@ describe('sequencer', () => { p2p.getTxs.mockResolvedValueOnce(txs); proverClient.startNewBlock.mockResolvedValueOnce(ticket); + proverClient.finaliseBlock.mockResolvedValue({ block, proof }); publisher.processL2Block.mockResolvedValueOnce(true); globalVariableBuilder.buildGlobalVariables.mockResolvedValueOnce( new GlobalVariables(chainId, version, new Fr(lastBlockNumber + 1), Fr.ZERO, coinbase, feeRecipient), @@ -190,10 +188,9 @@ describe('sequencer', () => { Array(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP).fill(new Fr(0n)), publicProcessor.makeEmptyProcessedTx(), ); - expect(proverClient.addNewTx).toHaveBeenCalledWith(expect.objectContaining({ hash: txs[0].getTxHash() })); - expect(proverClient.addNewTx).toHaveBeenCalledWith(expect.objectContaining({ hash: txs[2].getTxHash() })); expect(publisher.processL2Block).toHaveBeenCalledWith(block); expect(p2p.deleteTxs).toHaveBeenCalledWith([doubleSpendTx.getTxHash()]); + expect(proverClient.cancelBlock).toHaveBeenCalledTimes(0); }); it('builds a block out of several txs rejecting incorrect chain ids', async () => { @@ -206,8 +203,6 @@ describe('sequencer', () => { const proof = makeEmptyProof(); const result: ProvingSuccess = { status: PROVING_STATUS.SUCCESS, - proof, - block, }; const ticket: ProvingTicket = { provingPromise: Promise.resolve(result), @@ -215,6 +210,7 @@ describe('sequencer', () => { p2p.getTxs.mockResolvedValueOnce(txs); proverClient.startNewBlock.mockResolvedValueOnce(ticket); + proverClient.finaliseBlock.mockResolvedValue({ block, proof }); publisher.processL2Block.mockResolvedValueOnce(true); globalVariableBuilder.buildGlobalVariables.mockResolvedValueOnce( new GlobalVariables(chainId, version, new Fr(lastBlockNumber + 1), Fr.ZERO, coinbase, feeRecipient), @@ -232,10 +228,9 @@ describe('sequencer', () => { Array(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP).fill(new Fr(0n)), publicProcessor.makeEmptyProcessedTx(), ); - expect(proverClient.addNewTx).toHaveBeenCalledWith(expect.objectContaining({ hash: txs[0].getTxHash() })); - expect(proverClient.addNewTx).toHaveBeenCalledWith(expect.objectContaining({ hash: txs[2].getTxHash() })); expect(publisher.processL2Block).toHaveBeenCalledWith(block); expect(p2p.deleteTxs).toHaveBeenCalledWith([invalidChainTx.getTxHash()]); + expect(proverClient.cancelBlock).toHaveBeenCalledTimes(0); }); it('aborts building a block if the chain moves underneath it', async () => { @@ -245,8 +240,6 @@ describe('sequencer', () => { const proof = makeEmptyProof(); const result: ProvingSuccess = { status: PROVING_STATUS.SUCCESS, - proof, - block, }; const ticket: ProvingTicket = { provingPromise: Promise.resolve(result), @@ -254,6 +247,7 @@ describe('sequencer', () => { p2p.getTxs.mockResolvedValueOnce([tx]); proverClient.startNewBlock.mockResolvedValueOnce(ticket); + proverClient.finaliseBlock.mockResolvedValue({ block, proof }); publisher.processL2Block.mockResolvedValueOnce(true); globalVariableBuilder.buildGlobalVariables.mockResolvedValueOnce( new GlobalVariables(chainId, version, new Fr(lastBlockNumber + 1), Fr.ZERO, coinbase, feeRecipient), @@ -272,6 +266,7 @@ describe('sequencer', () => { await sequencer.work(); expect(publisher.processL2Block).not.toHaveBeenCalled(); + expect(proverClient.cancelBlock).toHaveBeenCalledTimes(1); }); }); diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index 7976b9ae6fb..e400e3ba935 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -1,7 +1,7 @@ import { type L1ToL2MessageSource, type L2Block, type L2BlockSource, type ProcessedTx, Tx } from '@aztec/circuit-types'; import { type BlockProver, PROVING_STATUS } from '@aztec/circuit-types/interfaces'; import { type L2BlockBuiltStats } from '@aztec/circuit-types/stats'; -import { AztecAddress, EthAddress, type GlobalVariables } from '@aztec/circuits.js'; +import { AztecAddress, EthAddress } from '@aztec/circuits.js'; import { Fr } from '@aztec/foundation/fields'; import { createDebugLogger } from '@aztec/foundation/log'; import { RunningPromise } from '@aztec/foundation/running-promise'; @@ -193,57 +193,75 @@ export class Sequencer { this.log.info(`Building block ${newBlockNumber} with ${validTxs.length} transactions`); this.state = SequencerState.CREATING_BLOCK; + // Get l1 to l2 messages from the contract + this.log('Requesting L1 to L2 messages from contract'); + const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(BigInt(newBlockNumber)); + this.log(`Retrieved ${l1ToL2Messages.length} L1 to L2 messages for block ${newBlockNumber}`); + // We create a fresh processor each time to reset any cached state (eg storage writes) const processor = await this.publicProcessorFactory.create(historicalHeader, newGlobalVariables); - const [publicProcessorDuration, [processedTxs, failedTxs]] = await elapsed(() => processor.process(validTxs)); + + const emptyTx = processor.makeEmptyProcessedTx(); + + const blockBuildingTimer = new Timer(); + + // We must initialise the block to be a power of 2 in size + const numRealTxs = validTxs.length; + const pow2 = Math.log2(numRealTxs); + const totalTxs = 2 ** Math.ceil(pow2); + const blockSize = Math.max(2, totalTxs); + const blockTicket = await this.prover.startNewBlock(blockSize, newGlobalVariables, l1ToL2Messages, emptyTx); + + const [publicProcessorDuration, [processedTxs, failedTxs]] = await elapsed(() => + processor.process(validTxs, blockSize, this.prover, txValidator), + ); if (failedTxs.length > 0) { const failedTxData = failedTxs.map(fail => fail.tx); this.log(`Dropping failed txs ${Tx.getHashes(failedTxData).join(', ')}`); await this.p2pClient.deleteTxs(Tx.getHashes(failedTxData)); } - // Only accept processed transactions that are not double-spends, - // public functions emitting nullifiers would pass earlier check but fail here. - // Note that we're checking all nullifiers generated in the private execution twice, - // we could store the ones already checked and skip them here as an optimization. - const processedValidTxs = await this.takeValidTxs(processedTxs, txValidator); - - if (processedValidTxs.length === 0) { + if (processedTxs.length === 0) { this.log('No txs processed correctly to build block. Exiting'); + this.prover.cancelBlock(); return; } await assertBlockHeight(); - // Get l1 to l2 messages from the contract - this.log('Requesting L1 to L2 messages from contract'); - const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(BigInt(newBlockNumber)); - this.log(`Retrieved ${l1ToL2Messages.length} L1 to L2 messages for block ${newBlockNumber}`); + // All real transactions have been added, set the block as full and complete the proving. + await this.prover.setBlockCompleted(); - // Build the new block by running the rollup circuits - this.log(`Assembling block with txs ${processedValidTxs.map(tx => tx.hash).join(', ')}`); + // Here we are now waiting for the block to be proven. + // TODO(@PhilWindle) We should probably periodically check for things like another + // block being published before ours instead of just waiting on our block + const result = await blockTicket.provingPromise; + if (result.status === PROVING_STATUS.FAILURE) { + throw new Error(`Block proving failed, reason: ${result.reason}`); + } await assertBlockHeight(); - const emptyTx = processor.makeEmptyProcessedTx(); - const [rollupCircuitsDuration, block] = await elapsed(() => - this.buildBlock(processedValidTxs, l1ToL2Messages, emptyTx, newGlobalVariables), - ); + // Block is proven, now finalise and publish! + const blockResult = await this.prover.finaliseBlock(); + const block = blockResult.block; + + await assertBlockHeight(); this.log(`Assembled block ${block.number}`, { eventName: 'l2-block-built', duration: workTimer.ms(), publicProcessDuration: publicProcessorDuration, - rollupCircuitsDuration: rollupCircuitsDuration, + rollupCircuitsDuration: blockBuildingTimer.ms(), ...block.getStats(), } satisfies L2BlockBuiltStats); - await assertBlockHeight(); - await this.publishL2Block(block); - this.log.info(`Submitted rollup block ${block.number} with ${processedValidTxs.length} transactions`); + this.log.info(`Submitted rollup block ${block.number} with ${processedTxs.length} transactions`); } catch (err) { this.log.error(`Rolling back world state DB due to error assembling block`, (err as any).stack); + // Cancel any further proving on the block + this.prover?.cancelBlock(); await this.worldState.getLatest().rollback(); } } @@ -289,33 +307,6 @@ export class Sequencer { return min >= this.lastPublishedBlock; } - /** - * Pads the set of txs to a power of two and assembles a block by calling the block builder. - * @param txs - Processed txs to include in the next block. - * @param l1ToL2Messages - L1 to L2 messages to be part of the block. - * @param emptyTx - Empty tx to repeat at the end of the block to pad to a power of two. - * @param globalVariables - Global variables to use in the block. - * @returns The new block. - */ - protected async buildBlock( - txs: ProcessedTx[], - l1ToL2Messages: Fr[], - emptyTx: ProcessedTx, - globalVariables: GlobalVariables, - ) { - const blockTicket = await this.prover.startNewBlock(txs.length, globalVariables, l1ToL2Messages, emptyTx); - - for (const tx of txs) { - await this.prover.addNewTx(tx); - } - - const result = await blockTicket.provingPromise; - if (result.status === PROVING_STATUS.FAILURE) { - throw new Error(`Block proving failed, reason: ${result.reason}`); - } - return result.block; - } - get coinbase(): EthAddress { return this._coinbase; }