From 2645eab19bac030835c959eb01f8f3af27f89adf Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Wed, 10 Jul 2024 11:03:58 -0300 Subject: [PATCH] feat: Remove proof from L1 Rollup process (#7347) Moves the proof verification from `Rollup.process` and into a new `submitProof` method, called separately from the sequencer. Eventually we'll want to move it out of the sequencer completely, but this way we don't break the current e2e flow. See #7346 --- l1-contracts/src/core/Rollup.sol | 63 +++++----- l1-contracts/src/core/interfaces/IRollup.sol | 7 +- l1-contracts/test/Rollup.t.sol | 10 +- yarn-project/accounts/package.local.json | 9 +- .../archiver/src/archiver/archiver.test.ts | 4 +- .../archiver/src/archiver/eth_log_handlers.ts | 2 +- yarn-project/circuit-types/src/stats/stats.ts | 22 +++- .../composed/integration_l1_publisher.test.ts | 22 +--- .../integration_proof_verification.test.ts | 2 +- .../package.local.json | 9 +- .../protocol-contracts/package.local.json | 9 +- .../scripts/src/benchmarks/aggregate.ts | 4 +- .../src/publisher/l1-publisher.test.ts | 31 ++--- .../src/publisher/l1-publisher.ts | 110 ++++++++++++++---- .../src/publisher/viem-tx-sender.ts | 33 +++++- .../src/sequencer/sequencer.test.ts | 10 +- .../src/sequencer/sequencer.ts | 16 ++- .../types/src/abi/contract_artifact.ts | 22 ++-- 18 files changed, 232 insertions(+), 153 deletions(-) diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index 4ef4bc84302..8f407c1bbc4 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -101,14 +101,8 @@ contract Rollup is IRollup { * @notice Process an incoming L2 block and progress the state * @param _header - The L2 block header * @param _archive - A root of the archive tree after the L2 block is applied - * @param _proof - The proof of correct execution */ - function process( - bytes calldata _header, - bytes32 _archive, - bytes calldata _aggregationObject, - bytes calldata _proof - ) external override(IRollup) { + function process(bytes calldata _header, bytes32 _archive) external override(IRollup) { // Decode and validate header HeaderLib.Header memory header = HeaderLib.decode(_header); HeaderLib.validate(header, VERSION, lastBlockTs, archive); @@ -124,6 +118,38 @@ contract Rollup is IRollup { revert Errors.Rollup__InvalidSequencer(msg.sender); } + archive = _archive; + lastBlockTs = block.timestamp; + + bytes32 inHash = INBOX.consume(); + if (header.contentCommitment.inHash != inHash) { + revert Errors.Rollup__InvalidInHash(inHash, header.contentCommitment.inHash); + } + + // TODO(#7218): Revert to fixed height tree for outbox, currently just providing min as interim + // Min size = smallest path of the rollup tree + 1 + (uint256 min,) = MerkleLib.computeMinMaxPathLength(header.contentCommitment.numTxs); + uint256 l2ToL1TreeMinHeight = min + 1; + OUTBOX.insert( + header.globalVariables.blockNumber, header.contentCommitment.outHash, l2ToL1TreeMinHeight + ); + + // pay the coinbase 1 gas token if it is not empty and header.totalFees is not zero + if (header.globalVariables.coinbase != address(0) && header.totalFees > 0) { + GAS_TOKEN.transfer(address(header.globalVariables.coinbase), header.totalFees); + } + + emit L2BlockProcessed(header.globalVariables.blockNumber); + } + + function submitProof( + bytes calldata _header, + bytes32 _archive, + bytes calldata _aggregationObject, + bytes calldata _proof + ) external override(IRollup) { + HeaderLib.Header memory header = HeaderLib.decode(_header); + bytes32[] memory publicInputs = new bytes32[](3 + Constants.HEADER_LENGTH + Constants.AGGREGATION_OBJECT_LENGTH); // the archive tree root @@ -156,28 +182,7 @@ contract Rollup is IRollup { revert Errors.Rollup__InvalidProof(); } - archive = _archive; - lastBlockTs = block.timestamp; - - bytes32 inHash = INBOX.consume(); - if (header.contentCommitment.inHash != inHash) { - revert Errors.Rollup__InvalidInHash(inHash, header.contentCommitment.inHash); - } - - // TODO(#7218): Revert to fixed height tree for outbox, currently just providing min as interim - // Min size = smallest path of the rollup tree + 1 - (uint256 min,) = MerkleLib.computeMinMaxPathLength(header.contentCommitment.numTxs); - uint256 l2ToL1TreeMinHeight = min + 1; - OUTBOX.insert( - header.globalVariables.blockNumber, header.contentCommitment.outHash, l2ToL1TreeMinHeight - ); - - // pay the coinbase 1 gas token if it is not empty and header.totalFees is not zero - if (header.globalVariables.coinbase != address(0) && header.totalFees > 0) { - GAS_TOKEN.transfer(address(header.globalVariables.coinbase), header.totalFees); - } - - emit L2BlockProcessed(header.globalVariables.blockNumber); + emit L2ProofVerified(header.globalVariables.blockNumber); } function _computePublicInputHash(bytes calldata _header, bytes32 _archive) diff --git a/l1-contracts/src/core/interfaces/IRollup.sol b/l1-contracts/src/core/interfaces/IRollup.sol index a53fb47bcb9..006d5ccb2e4 100644 --- a/l1-contracts/src/core/interfaces/IRollup.sol +++ b/l1-contracts/src/core/interfaces/IRollup.sol @@ -4,12 +4,15 @@ pragma solidity >=0.8.18; interface IRollup { event L2BlockProcessed(uint256 indexed blockNumber); + event L2ProofVerified(uint256 indexed blockNumber); - function process( + function process(bytes calldata _header, bytes32 _archive) external; + + function submitProof( bytes calldata _header, bytes32 _archive, bytes calldata _aggregationObject, - bytes memory _proof + bytes calldata _proof ) external; function setVerifier(address _verifier) external; diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index 50f7d7f66d7..a9af9338165 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -85,7 +85,7 @@ contract RollupTest is DecoderBase { availabilityOracle.publish(body); vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__InvalidChainId.selector, 0x420, 31337)); - rollup.process(header, archive, bytes(""), bytes("")); + rollup.process(header, archive); } function testRevertInvalidVersion() public { @@ -101,7 +101,7 @@ contract RollupTest is DecoderBase { availabilityOracle.publish(body); vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__InvalidVersion.selector, 0x420, 1)); - rollup.process(header, archive, bytes(""), bytes("")); + rollup.process(header, archive); } function testRevertTimestampInFuture() public { @@ -118,7 +118,7 @@ contract RollupTest is DecoderBase { availabilityOracle.publish(body); vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__TimestampInFuture.selector)); - rollup.process(header, archive, bytes(""), bytes("")); + rollup.process(header, archive); } function testRevertTimestampTooOld() public { @@ -133,7 +133,7 @@ contract RollupTest is DecoderBase { availabilityOracle.publish(body); vm.expectRevert(abi.encodeWithSelector(Errors.Rollup__TimestampTooOld.selector)); - rollup.process(header, archive, bytes(""), bytes("")); + rollup.process(header, archive); } function _testBlock(string memory name) public { @@ -153,7 +153,7 @@ contract RollupTest is DecoderBase { uint256 toConsume = inbox.toConsume(); vm.record(); - rollup.process(header, archive, bytes(""), bytes("")); + rollup.process(header, archive); assertEq(inbox.toConsume(), toConsume + 1, "Message subtree not consumed"); diff --git a/yarn-project/accounts/package.local.json b/yarn-project/accounts/package.local.json index 6e3a34a9358..d442f6c8af9 100644 --- a/yarn-project/accounts/package.local.json +++ b/yarn-project/accounts/package.local.json @@ -7,10 +7,5 @@ "build:ts": "tsc -b", "clean": "rm -rf ./dest .tsbuildinfo ./artifacts" }, - "files": [ - "dest", - "src", - "artifacts", - "!*.test.*" - ] -} \ No newline at end of file + "files": ["dest", "src", "artifacts", "!*.test.*"] +} diff --git a/yarn-project/archiver/src/archiver/archiver.test.ts b/yarn-project/archiver/src/archiver/archiver.test.ts index 1040f308fae..ea7cbba1883 100644 --- a/yarn-project/archiver/src/archiver/archiver.test.ts +++ b/yarn-project/archiver/src/archiver/archiver.test.ts @@ -248,12 +248,10 @@ function makeMessageSentEvent(l1BlockNum: bigint, l2BlockNumber: bigint, index: function makeRollupTx(l2Block: L2Block) { const header = toHex(l2Block.header.toBuffer()); const archive = toHex(l2Block.archive.root.toBuffer()); - const aggregationObject = `0x`; - const proof = `0x`; const input = encodeFunctionData({ abi: RollupAbi, functionName: 'process', - args: [header, archive, aggregationObject, proof], + args: [header, archive], }); return { input } as Transaction; } diff --git a/yarn-project/archiver/src/archiver/eth_log_handlers.ts b/yarn-project/archiver/src/archiver/eth_log_handlers.ts index 5fe3fb4d08b..00d0b6d4bef 100644 --- a/yarn-project/archiver/src/archiver/eth_log_handlers.ts +++ b/yarn-project/archiver/src/archiver/eth_log_handlers.ts @@ -91,7 +91,7 @@ async function getBlockMetadataFromRollupTx( if (functionName !== 'process') { throw new Error(`Unexpected method called ${functionName}`); } - const [headerHex, archiveRootHex] = args! as readonly [Hex, Hex, Hex, Hex]; + const [headerHex, archiveRootHex] = args! as readonly [Hex, Hex]; const header = Header.fromBuffer(Buffer.from(hexToBytes(headerHex))); diff --git a/yarn-project/circuit-types/src/stats/stats.ts b/yarn-project/circuit-types/src/stats/stats.ts index d5d6f4d6a88..8abab96dde9 100644 --- a/yarn-project/circuit-types/src/stats/stats.ts +++ b/yarn-project/circuit-types/src/stats/stats.ts @@ -26,10 +26,8 @@ export type L2BlockStats = { unencryptedLogSize?: number; }; -/** Stats logged for each L1 rollup publish tx.*/ +/** Stats logged for each L1 publish tx.*/ export type L1PublishStats = { - /** Name of the event for metrics purposes */ - eventName: 'rollup-published-to-l1'; /** Effective gas price of the tx. */ gasPrice: bigint; /** Effective gas used in the tx. */ @@ -40,7 +38,20 @@ export type L1PublishStats = { calldataGas: number; /** Size in bytes of the calldata. */ calldataSize: number; -} & L2BlockStats; +}; + +/** Stats logged for each L1 rollup publish tx.*/ +export type L1PublishBlockStats = { + /** Name of the event for metrics purposes */ + eventName: 'rollup-published-to-l1'; +} & L1PublishStats & + L2BlockStats; + +/** Stats logged for each L1 rollup publish tx.*/ +export type L1PublishProofStats = { + /** Name of the event for metrics purposes */ + eventName: 'proof-published-to-l1'; +} & L1PublishStats; /** Stats logged for synching node chain history. */ export type NodeSyncedChainHistoryStats = { @@ -271,7 +282,8 @@ export type Stats = | CircuitSimulationStats | CircuitWitnessGenerationStats | PublicDBAccessStats - | L1PublishStats + | L1PublishBlockStats + | L1PublishProofStats | L2BlockBuiltStats | L2BlockHandledStats | NodeSyncedChainHistoryStats diff --git a/yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts b/yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts index c797483e372..9c0ee2dfaa4 100644 --- a/yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts +++ b/yarn-project/end-to-end/src/composed/integration_l1_publisher.test.ts @@ -27,9 +27,7 @@ import { MAX_NULLIFIERS_PER_TX, MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, - type Proof, PublicDataUpdateRequest, - makeEmptyProof, } from '@aztec/circuits.js'; import { fr, makeProof } from '@aztec/circuits.js/testing'; import { type L1ContractAddresses, createEthereumChain } from '@aztec/ethereum'; @@ -89,7 +87,6 @@ describe('L1Publisher integration', () => { let outbox: GetContractReturnType>; let publisher: L1Publisher; - let l2Proof: Proof; let builder: TxProver; let builderDb: MerkleTrees; @@ -147,7 +144,6 @@ describe('L1Publisher integration', () => { const worldStateSynchronizer = new ServerWorldStateSynchronizer(tmpStore, builderDb, blockSource, worldStateConfig); await worldStateSynchronizer.start(); builder = await TxProver.new(config, worldStateSynchronizer, new NoopTelemetryClient()); - l2Proof = makeEmptyProof(); publisher = getL1Publisher({ rpcUrl: config.rpcUrl, @@ -411,7 +407,7 @@ describe('L1Publisher integration', () => { writeJson(`mixed_block_${i}`, block, l1ToL2Content, recipientAddress, deployerAccount.address); - await publisher.processL2Block(block, [], l2Proof); + await publisher.processL2Block(block); const logs = await publicClient.getLogs({ address: rollupAddress, @@ -431,12 +427,7 @@ describe('L1Publisher integration', () => { const expectedData = encodeFunctionData({ abi: RollupAbi, functionName: 'process', - args: [ - `0x${block.header.toBuffer().toString('hex')}`, - `0x${block.archive.root.toBuffer().toString('hex')}`, - `0x`, // empty aggregation object - `0x${l2Proof.withoutPublicInputs().toString('hex')}`, - ], + args: [`0x${block.header.toBuffer().toString('hex')}`, `0x${block.archive.root.toBuffer().toString('hex')}`], }); expect(ethTx.input).toEqual(expectedData); @@ -501,7 +492,7 @@ describe('L1Publisher integration', () => { writeJson(`empty_block_${i}`, block, [], AztecAddress.ZERO, deployerAccount.address); - await publisher.processL2Block(block, [], l2Proof); + await publisher.processL2Block(block); const logs = await publicClient.getLogs({ address: rollupAddress, @@ -521,12 +512,7 @@ describe('L1Publisher integration', () => { const expectedData = encodeFunctionData({ abi: RollupAbi, functionName: 'process', - args: [ - `0x${block.header.toBuffer().toString('hex')}`, - `0x${block.archive.root.toBuffer().toString('hex')}`, - `0x`, // empty aggregation object - `0x${l2Proof.withoutPublicInputs().toString('hex')}`, - ], + args: [`0x${block.header.toBuffer().toString('hex')}`, `0x${block.archive.root.toBuffer().toString('hex')}`], }); expect(ethTx.input).toEqual(expectedData); } 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 85fe77ace57..8dc7b8630df 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 @@ -187,7 +187,7 @@ describe('proof_verification', () => { `0x${proof.withoutPublicInputs().toString('hex')}`, ] as const; - await expect(rollupContract.write.process(args)).resolves.toBeDefined(); + await expect(rollupContract.write.submitProof(args)).resolves.toBeDefined(); }); }); }); diff --git a/yarn-project/noir-protocol-circuits-types/package.local.json b/yarn-project/noir-protocol-circuits-types/package.local.json index e3deaa112a7..d496e249715 100644 --- a/yarn-project/noir-protocol-circuits-types/package.local.json +++ b/yarn-project/noir-protocol-circuits-types/package.local.json @@ -3,10 +3,5 @@ "build": "yarn clean && yarn generate && tsc -b", "clean": "rm -rf ./dest .tsbuildinfo src/types artifacts" }, - "files": [ - "dest", - "src", - "artifacts", - "!*.test.*" - ] -} \ No newline at end of file + "files": ["dest", "src", "artifacts", "!*.test.*"] +} diff --git a/yarn-project/protocol-contracts/package.local.json b/yarn-project/protocol-contracts/package.local.json index 6e3a34a9358..d442f6c8af9 100644 --- a/yarn-project/protocol-contracts/package.local.json +++ b/yarn-project/protocol-contracts/package.local.json @@ -7,10 +7,5 @@ "build:ts": "tsc -b", "clean": "rm -rf ./dest .tsbuildinfo ./artifacts" }, - "files": [ - "dest", - "src", - "artifacts", - "!*.test.*" - ] -} \ No newline at end of file + "files": ["dest", "src", "artifacts", "!*.test.*"] +} diff --git a/yarn-project/scripts/src/benchmarks/aggregate.ts b/yarn-project/scripts/src/benchmarks/aggregate.ts index 5e6f6ab5d89..baae136653b 100644 --- a/yarn-project/scripts/src/benchmarks/aggregate.ts +++ b/yarn-project/scripts/src/benchmarks/aggregate.ts @@ -19,7 +19,7 @@ import { type CircuitProvingStats, type CircuitSimulationStats, type CircuitWitnessGenerationStats, - type L1PublishStats, + type L1PublishBlockStats, type L2BlockBuiltStats, type L2BlockHandledStats, type MetricName, @@ -87,7 +87,7 @@ function processAcirProofGenerated(entry: ProofConstructed, results: BenchmarkCo } /** Processes an entry with event name 'rollup-published-to-l1' and updates results */ -function processRollupPublished(entry: L1PublishStats, results: BenchmarkCollectedResults) { +function processRollupPublished(entry: L1PublishBlockStats, results: BenchmarkCollectedResults) { const bucket = entry.txCount; if (!BENCHMARK_BLOCK_SIZES.includes(bucket)) { return; diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts index 2e301e40e3d..6b0012fcb77 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.test.ts @@ -1,5 +1,4 @@ import { L2Block } from '@aztec/circuit-types'; -import { makeEmptyProof } from '@aztec/circuits.js'; import { sleep } from '@aztec/foundation/sleep'; import { type MockProxy, mock } from 'jest-mock-extended'; @@ -18,8 +17,6 @@ describe('L1Publisher', () => { let archive: Buffer; let txsEffectsHash: Buffer; let body: Buffer; - let proof: Buffer; - let aggregationObject: Buffer; let publisher: L1Publisher; @@ -30,8 +27,6 @@ describe('L1Publisher', () => { archive = l2Block.archive.root.toBuffer(); txsEffectsHash = l2Block.body.getTxsEffectsHash(); body = l2Block.body.toBuffer(); - aggregationObject = Buffer.alloc(0); - proof = makeEmptyProof().withoutPublicInputs(); txSender = mock(); @@ -56,22 +51,16 @@ describe('L1Publisher', () => { }); it('publishes l2 block to l1', async () => { - const result = await publisher.processL2Block(l2Block, [], makeEmptyProof()); + const result = await publisher.processL2Block(l2Block); expect(result).toEqual(true); - expect(txSender.sendProcessTx).toHaveBeenCalledWith({ - header, - archive, - body, - aggregationObject, - proof, - }); + expect(txSender.sendProcessTx).toHaveBeenCalledWith({ header, archive, body }); expect(txSender.getTransactionReceipt).toHaveBeenCalledWith(processTxHash); }); it('does not publish if last archive root is different to expected', async () => { txSender.getCurrentArchive.mockResolvedValueOnce(L2Block.random(43).archive.root.toBuffer()); - const result = await publisher.processL2Block(l2Block, [], makeEmptyProof()); + const result = await publisher.processL2Block(l2Block); expect(result).toBe(false); expect(txSender.sendPublishTx).not.toHaveBeenCalled(); expect(txSender.sendProcessTx).not.toHaveBeenCalled(); @@ -80,7 +69,7 @@ describe('L1Publisher', () => { it('does not retry if sending a publish tx fails', async () => { txSender.sendPublishTx.mockReset().mockRejectedValueOnce(new Error()).mockResolvedValueOnce(publishTxHash); - const result = await publisher.processL2Block(l2Block, [], makeEmptyProof()); + const result = await publisher.processL2Block(l2Block); expect(result).toEqual(false); expect(txSender.sendPublishTx).toHaveBeenCalledTimes(1); @@ -90,7 +79,7 @@ describe('L1Publisher', () => { it('does not retry if sending a process tx fails', async () => { txSender.sendProcessTx.mockReset().mockRejectedValueOnce(new Error()).mockResolvedValueOnce(processTxHash); - const result = await publisher.processL2Block(l2Block, [], makeEmptyProof()); + const result = await publisher.processL2Block(l2Block); expect(result).toEqual(false); expect(txSender.sendPublishTx).toHaveBeenCalledTimes(1); @@ -105,7 +94,7 @@ describe('L1Publisher', () => { .mockRejectedValueOnce(new Error()) .mockResolvedValueOnce(processTxReceipt); - const result = await publisher.processL2Block(l2Block, [], makeEmptyProof()); + const result = await publisher.processL2Block(l2Block); expect(result).toEqual(true); expect(txSender.getTransactionReceipt).toHaveBeenCalledTimes(4); @@ -114,7 +103,7 @@ describe('L1Publisher', () => { it('returns false if publish tx reverts', async () => { txSender.getTransactionReceipt.mockReset().mockResolvedValueOnce({ ...publishTxReceipt, status: false }); - const result = await publisher.processL2Block(l2Block, [], makeEmptyProof()); + const result = await publisher.processL2Block(l2Block); expect(result).toEqual(false); }); @@ -125,7 +114,7 @@ describe('L1Publisher', () => { .mockResolvedValueOnce(publishTxReceipt) .mockResolvedValueOnce({ ...publishTxReceipt, status: false }); - const result = await publisher.processL2Block(l2Block, [], makeEmptyProof()); + const result = await publisher.processL2Block(l2Block); expect(result).toEqual(false); }); @@ -133,7 +122,7 @@ describe('L1Publisher', () => { it('returns false if sending publish tx is interrupted', async () => { txSender.sendPublishTx.mockReset().mockImplementationOnce(() => sleep(10, publishTxHash)); - const resultPromise = publisher.processL2Block(l2Block, [], makeEmptyProof()); + const resultPromise = publisher.processL2Block(l2Block); publisher.interrupt(); const result = await resultPromise; @@ -144,7 +133,7 @@ describe('L1Publisher', () => { it('returns false if sending process tx is interrupted', async () => { txSender.sendProcessTx.mockReset().mockImplementationOnce(() => sleep(10, processTxHash)); - const resultPromise = publisher.processL2Block(l2Block, [], makeEmptyProof()); + const resultPromise = publisher.processL2Block(l2Block); publisher.interrupt(); const result = await resultPromise; diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts index 9cc82d3b6e2..775751f56eb 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts @@ -1,6 +1,7 @@ import { type L2Block } from '@aztec/circuit-types'; -import { type L1PublishStats } from '@aztec/circuit-types/stats'; -import { type EthAddress, type Fr, type Proof } from '@aztec/circuits.js'; +import { type L1PublishBlockStats, type L1PublishProofStats } from '@aztec/circuit-types/stats'; +import { type EthAddress, type Header, type Proof } from '@aztec/circuits.js'; +import { type Fr } from '@aztec/foundation/fields'; import { createDebugLogger } from '@aztec/foundation/log'; import { serializeToBuffer } from '@aztec/foundation/serialize'; import { InterruptibleSleep } from '@aztec/foundation/sleep'; @@ -42,8 +43,10 @@ export type MinimalTransactionReceipt = { * Pushes txs to the L1 chain and waits for their completion. */ export interface L1PublisherTxSender { + /** Returns the EOA used for sending txs to L1. */ getSenderAddress(): Promise; + /** Returns the address elected for submitting a given block number or zero if anyone can submit. */ getSubmitterAddressForBlock(blockNumber: number): Promise; /** @@ -60,6 +63,13 @@ export interface L1PublisherTxSender { */ sendProcessTx(encodedData: L1ProcessArgs): Promise; + /** + * Sends a tx to the L1 rollup contract with a proof. Returns once the tx has been mined. + * @param encodedData - Serialized data for processing the new L2 block. + * @returns The hash of the mined tx. + */ + sendSubmitProofTx(submitProofArgs: L1SubmitProofArgs): Promise; + /** * Returns a tx receipt if the tx has been mined. * @param txHash - Hash of the tx to look for. @@ -87,9 +97,7 @@ export interface L1PublisherTxSender { checkIfTxsAreAvailable(block: L2Block): Promise; } -/** - * Encoded block and proof ready to be pushed to the L1 contract. - */ +/** Arguments to the process method of the rollup contract */ export type L1ProcessArgs = { /** The L2 block header. */ header: Buffer; @@ -97,10 +105,18 @@ export type L1ProcessArgs = { archive: Buffer; /** L2 block body. */ body: Buffer; - /** Aggregation object needed to verify the proof */ - aggregationObject: Buffer; - /** Root rollup proof of the L2 block. */ +}; + +/** Arguments to the submitProof method of the rollup contract */ +export type L1SubmitProofArgs = { + /** The L2 block header. */ + header: Buffer; + /** A root of the archive tree after the L2 block is applied. */ + archive: Buffer; + /** The proof for the block. */ proof: Buffer; + /** The aggregation object for the block's proof. */ + aggregationObject: Buffer; }; /** @@ -132,11 +148,12 @@ export class L1Publisher implements L2BlockReceiver { * @param block - L2 block to publish. * @returns True once the tx has been confirmed and is successful, false on revert or interrupt, blocks otherwise. */ - public async processL2Block(block: L2Block, aggregationObject: Fr[], proof: Proof): Promise { + public async processL2Block(block: L2Block): Promise { + const ctx = { blockNumber: block.number, blockHash: block.hash().toString() }; // TODO(#4148) Remove this block number check, it's here because we don't currently have proper genesis state on the contract const lastArchive = block.header.lastArchive.root.toBuffer(); if (block.number != 1 && !(await this.checkLastArchiveHash(lastArchive))) { - this.log.info(`Detected different last archive prior to publishing a block, aborting publish...`); + this.log.info(`Detected different last archive prior to publishing a block, aborting publish...`, ctx); return false; } @@ -145,7 +162,7 @@ export class L1Publisher implements L2BlockReceiver { // Publish block transaction effects while (!this.interrupted) { if (await this.txSender.checkIfTxsAreAvailable(block)) { - this.log.verbose(`Transaction effects of a block ${block.number} already published.`); + this.log.verbose(`Transaction effects of block ${block.number} already published.`, ctx); break; } @@ -165,14 +182,14 @@ export class L1Publisher implements L2BlockReceiver { // txsEffectsHash from IAvailabilityOracle.TxsPublished event txsEffectsHash = receipt.logs[0].data; } else { - this.log.warn(`Expected 1 log, got ${receipt.logs.length}`); + this.log.warn(`Expected 1 log, got ${receipt.logs.length}`, ctx); } - this.log.info(`Block txs effects published, txsEffectsHash: ${txsEffectsHash}`); + this.log.info(`Block txs effects published`, { ...ctx, txsEffectsHash }); break; } - this.log.error(`AvailabilityOracle.publish tx status failed: ${receipt.transactionHash}`); + this.log.error(`AvailabilityOracle.publish tx status failed: ${receipt.transactionHash}`, ctx); await this.sleepOrInterrupted(); } @@ -180,8 +197,6 @@ export class L1Publisher implements L2BlockReceiver { header: block.header.toBuffer(), archive: block.archive.root.toBuffer(), body: encodedBody, - aggregationObject: serializeToBuffer(aggregationObject), - proof: proof.withoutPublicInputs(), }; // Process block @@ -199,27 +214,69 @@ export class L1Publisher implements L2BlockReceiver { // Tx was mined successfully if (receipt.status) { const tx = await this.txSender.getTransactionStats(txHash); - const stats: L1PublishStats = { + const stats: L1PublishBlockStats = { ...pick(receipt, 'gasPrice', 'gasUsed', 'transactionHash'), ...pick(tx!, 'calldataGas', 'calldataSize'), ...block.getStats(), eventName: 'rollup-published-to-l1', }; - this.log.info(`Published L2 block to L1 rollup contract`, stats); + this.log.info(`Published L2 block to L1 rollup contract`, { ...stats, ...ctx }); return true; } // Check if someone else incremented the block number if (!(await this.checkLastArchiveHash(lastArchive))) { - this.log.warn('Publish failed. Detected different last archive hash.'); + this.log.warn('Publish failed. Detected different last archive hash.', ctx); break; } - this.log.error(`Rollup.process tx status failed: ${receipt.transactionHash}`); + this.log.error(`Rollup.process tx status failed: ${receipt.transactionHash}`, ctx); await this.sleepOrInterrupted(); } - this.log.verbose('L2 block data syncing interrupted while processing blocks.'); + this.log.verbose('L2 block data syncing interrupted while processing blocks.', ctx); + return false; + } + + public async submitProof(header: Header, archiveRoot: Fr, aggregationObject: Fr[], proof: Proof): Promise { + const ctx = { blockNumber: header.globalVariables.blockNumber }; + + const txArgs: L1SubmitProofArgs = { + header: header.toBuffer(), + archive: archiveRoot.toBuffer(), + aggregationObject: serializeToBuffer(aggregationObject), + proof: proof.withoutPublicInputs(), + }; + + // Process block + while (!this.interrupted) { + const txHash = await this.sendSubmitProofTx(txArgs); + if (!txHash) { + break; + } + + const receipt = await this.getTransactionReceipt(txHash); + if (!receipt) { + break; + } + + // Tx was mined successfully + if (receipt.status) { + const tx = await this.txSender.getTransactionStats(txHash); + const stats: L1PublishProofStats = { + ...pick(receipt, 'gasPrice', 'gasUsed', 'transactionHash'), + ...pick(tx!, 'calldataGas', 'calldataSize'), + eventName: 'proof-published-to-l1', + }; + this.log.info(`Published L2 block to L1 rollup contract`, { ...stats, ...ctx }); + return true; + } + + this.log.error(`Rollup.submitProof tx status failed: ${receipt.transactionHash}`, ctx); + await this.sleepOrInterrupted(); + } + + this.log.verbose('L2 block data syncing interrupted while processing blocks.', ctx); return false; } @@ -254,6 +311,17 @@ export class L1Publisher implements L2BlockReceiver { return areSame; } + private async sendSubmitProofTx(submitProofArgs: L1SubmitProofArgs): Promise { + try { + const size = Object.values(submitProofArgs).reduce((acc, arg) => acc + arg.length, 0); + this.log.info(`SubmitProof size=${size} bytes`); + return await this.txSender.sendSubmitProofTx(submitProofArgs); + } catch (err) { + this.log.error(`Rollup submit proof failed`, err); + return undefined; + } + } + private async sendPublishTx(encodedBody: Buffer): Promise { while (!this.interrupted) { try { diff --git a/yarn-project/sequencer-client/src/publisher/viem-tx-sender.ts b/yarn-project/sequencer-client/src/publisher/viem-tx-sender.ts index c630f043638..75853b780e6 100644 --- a/yarn-project/sequencer-client/src/publisher/viem-tx-sender.ts +++ b/yarn-project/sequencer-client/src/publisher/viem-tx-sender.ts @@ -23,6 +23,7 @@ import * as chains from 'viem/chains'; import { type TxSenderConfig } from './config.js'; import { type L1PublisherTxSender, + type L1SubmitProofArgs, type MinimalTransactionReceipt, type L1ProcessArgs as ProcessTxArgs, type TransactionStats, @@ -157,12 +158,7 @@ export class ViemTxSender implements L1PublisherTxSender { * @returns The hash of the mined tx. */ async sendProcessTx(encodedData: ProcessTxArgs): Promise { - const args = [ - `0x${encodedData.header.toString('hex')}`, - `0x${encodedData.archive.toString('hex')}`, - `0x${encodedData.aggregationObject.toString('hex')}`, - `0x${encodedData.proof.toString('hex')}`, - ] as const; + const args = [`0x${encodedData.header.toString('hex')}`, `0x${encodedData.archive.toString('hex')}`] as const; const gas = await this.rollupContract.estimateGas.process(args, { account: this.account, @@ -174,6 +170,31 @@ export class ViemTxSender implements L1PublisherTxSender { return hash; } + /** + * Sends a tx to the L1 rollup contract with a proof. Returns once the tx has been mined. + * @param encodedData - Serialized data for the proof. + * @returns The hash of the mined tx. + */ + async sendSubmitProofTx(submitProofArgs: L1SubmitProofArgs): Promise { + const { header, archive, aggregationObject, proof } = submitProofArgs; + const args = [ + `0x${header.toString('hex')}`, + `0x${archive.toString('hex')}`, + `0x${aggregationObject.toString('hex')}`, + `0x${proof.toString('hex')}`, + ] as const; + + const gas = await this.rollupContract.estimateGas.submitProof(args, { + account: this.account, + }); + const hash = await this.rollupContract.write.submitProof(args, { + gas, + account: this.account, + }); + + return hash; + } + /** * Gets the chain object for the given chain id. * @param chainId - Chain id of the target EVM chain. diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index 89721ceafb9..aef46a70db7 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -148,7 +148,7 @@ describe('sequencer', () => { new GlobalVariables(chainId, version, new Fr(lastBlockNumber + 1), Fr.ZERO, coinbase, feeRecipient, gasFees), Array(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP).fill(new Fr(0n)), ); - expect(publisher.processL2Block).toHaveBeenCalledWith(block, [], proof); + expect(publisher.processL2Block).toHaveBeenCalledWith(block); expect(proverClient.cancelBlock).toHaveBeenCalledTimes(0); }); @@ -186,7 +186,7 @@ describe('sequencer', () => { new GlobalVariables(chainId, version, new Fr(lastBlockNumber + 1), Fr.ZERO, coinbase, feeRecipient, gasFees), Array(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP).fill(new Fr(0n)), ); - expect(publisher.processL2Block).toHaveBeenCalledWith(block, [], proof); + expect(publisher.processL2Block).toHaveBeenCalledWith(block); expect(proverClient.cancelBlock).toHaveBeenCalledTimes(0); }); @@ -229,7 +229,7 @@ describe('sequencer', () => { new GlobalVariables(chainId, version, new Fr(lastBlockNumber + 1), Fr.ZERO, coinbase, feeRecipient, gasFees), Array(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP).fill(new Fr(0n)), ); - expect(publisher.processL2Block).toHaveBeenCalledWith(block, [], proof); + expect(publisher.processL2Block).toHaveBeenCalledWith(block); expect(p2p.deleteTxs).toHaveBeenCalledWith([doubleSpendTx.getTxHash()]); expect(proverClient.cancelBlock).toHaveBeenCalledTimes(0); }); @@ -268,7 +268,7 @@ describe('sequencer', () => { new GlobalVariables(chainId, version, new Fr(lastBlockNumber + 1), Fr.ZERO, coinbase, feeRecipient, gasFees), Array(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP).fill(new Fr(0n)), ); - expect(publisher.processL2Block).toHaveBeenCalledWith(block, [], proof); + expect(publisher.processL2Block).toHaveBeenCalledWith(block); expect(p2p.deleteTxs).toHaveBeenCalledWith([invalidChainTx.getTxHash()]); expect(proverClient.cancelBlock).toHaveBeenCalledTimes(0); }); @@ -307,7 +307,7 @@ describe('sequencer', () => { new GlobalVariables(chainId, version, new Fr(lastBlockNumber + 1), Fr.ZERO, coinbase, feeRecipient, gasFees), Array(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP).fill(new Fr(0n)), ); - expect(publisher.processL2Block).toHaveBeenCalledWith(block, [], proof); + expect(publisher.processL2Block).toHaveBeenCalledWith(block); expect(proverClient.cancelBlock).toHaveBeenCalledTimes(0); }); diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index fb20cb3507f..31697c2a031 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -13,7 +13,7 @@ import { PROVING_STATUS, } from '@aztec/circuit-types/interfaces'; import { type L2BlockBuiltStats } from '@aztec/circuit-types/stats'; -import { AztecAddress, EthAddress, type GlobalVariables, type Header, type Proof } from '@aztec/circuits.js'; +import { AztecAddress, EthAddress, type GlobalVariables, type Header } from '@aztec/circuits.js'; import { Fr } from '@aztec/foundation/fields'; import { createDebugLogger } from '@aztec/foundation/log'; import { RunningPromise } from '@aztec/foundation/running-promise'; @@ -306,8 +306,16 @@ export class Sequencer { ...block.getStats(), } satisfies L2BlockBuiltStats); - await this.publishL2Block(block, aggregationObject, proof); + await this.publishL2Block(block); this.log.info(`Submitted rollup block ${block.number} with ${processedTxs.length} transactions`); + + // Submit the proof if we have configured this sequencer to run with a prover. + // This is temporary while we submit one proof per block, but will have to change once we + // move onto proving batches of multiple blocks at a time. + if (aggregationObject && proof) { + await this.publisher.submitProof(block.header, block.archive.root, aggregationObject, proof); + this.log.info(`Submitted proof for block ${block.number}`); + } } /** @@ -317,10 +325,10 @@ export class Sequencer { @trackSpan('Sequencer.publishL2Block', block => ({ [Attributes.BLOCK_NUMBER]: block.number, })) - protected async publishL2Block(block: L2Block, aggregationObject: Fr[], proof: Proof) { + protected async publishL2Block(block: L2Block) { // Publishes new block to the network and awaits the tx to be mined this.state = SequencerState.PUBLISHING_BLOCK; - const publishedL2Block = await this.publisher.processL2Block(block, aggregationObject, proof); + const publishedL2Block = await this.publisher.processL2Block(block); if (publishedL2Block) { this.lastPublishedBlock = block.number; } else { diff --git a/yarn-project/types/src/abi/contract_artifact.ts b/yarn-project/types/src/abi/contract_artifact.ts index 861eb227206..e8cbbb3aabf 100644 --- a/yarn-project/types/src/abi/contract_artifact.ts +++ b/yarn-project/types/src/abi/contract_artifact.ts @@ -272,13 +272,17 @@ function getNoteTypes(input: NoirCompiledContract) { * @returns Aztec contract build artifact. */ function generateContractArtifact(contract: NoirCompiledContract, aztecNrVersion?: string): ContractArtifact { - return { - name: contract.name, - functions: contract.functions.map(f => generateFunctionArtifact(f, contract)), - outputs: contract.outputs, - storageLayout: getStorageLayout(contract), - notes: getNoteTypes(contract), - fileMap: contract.file_map, - aztecNrVersion, - }; + try { + return { + name: contract.name, + functions: contract.functions.map(f => generateFunctionArtifact(f, contract)), + outputs: contract.outputs, + storageLayout: getStorageLayout(contract), + notes: getNoteTypes(contract), + fileMap: contract.file_map, + aztecNrVersion, + }; + } catch (err) { + throw new Error(`Could not generate contract artifact for ${contract.name}: ${err}`); + } }