diff --git a/yarn-project/archiver/src/archiver/archiver.test.ts b/yarn-project/archiver/src/archiver/archiver.test.ts index 2adcd5cb681..37bd20e82b5 100644 --- a/yarn-project/archiver/src/archiver/archiver.test.ts +++ b/yarn-project/archiver/src/archiver/archiver.test.ts @@ -1,11 +1,9 @@ -import { ExtendedContractData, L2Block, L2BlockL2Logs, LogType } from '@aztec/circuit-types'; -import { NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP } from '@aztec/circuits.js'; +import { Body, ExtendedContractData, L2Block, L2BlockL2Logs, LogType } from '@aztec/circuit-types'; import { AztecAddress } from '@aztec/foundation/aztec-address'; -import { times } from '@aztec/foundation/collection'; import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr } from '@aztec/foundation/fields'; import { sleep } from '@aztec/foundation/sleep'; -import { ContractDeploymentEmitterAbi, InboxAbi, RollupAbi } from '@aztec/l1-artifacts'; +import { AvailabilityOracleAbi, ContractDeploymentEmitterAbi, InboxAbi, RollupAbi } from '@aztec/l1-artifacts'; import { MockProxy, mock } from 'jest-mock-extended'; import { Chain, HttpTransport, Log, PublicClient, Transaction, encodeFunctionData, toHex } from 'viem'; @@ -18,6 +16,7 @@ describe('Archiver', () => { const rollupAddress = EthAddress.ZERO.toString(); const inboxAddress = EthAddress.ZERO.toString(); const registryAddress = EthAddress.ZERO.toString(); + const availabilityOracleAddress = EthAddress.ZERO.toString(); const contractDeploymentEmitterAddress = '0x0000000000000000000000000000000000000001'; const blockNumbers = [1, 2, 3]; let publicClient: MockProxy>; @@ -32,6 +31,7 @@ describe('Archiver', () => { const archiver = new Archiver( publicClient, EthAddress.fromString(rollupAddress), + EthAddress.fromString(availabilityOracleAddress), EthAddress.fromString(inboxAddress), EthAddress.fromString(registryAddress), EthAddress.fromString(contractDeploymentEmitterAddress), @@ -43,7 +43,9 @@ describe('Archiver', () => { expect(latestBlockNum).toEqual(0); const blocks = blockNumbers.map(x => L2Block.random(x, 4, x, x + 1, x * 2, x * 3)); + const publishTxs = blocks.map(block => block.body).map(makePublishTx); const rollupTxs = blocks.map(makeRollupTx); + // `L2Block.random(x)` creates some l1 to l2 messages. We add those, // since it is expected by the test that these would be consumed. // Archiver removes such messages from pending store. @@ -58,15 +60,15 @@ describe('Archiver', () => { const l1ToL2MessageAddedEvents = [ makeL1ToL2MessageAddedEvents( 100n, - blocks[0].body.l1ToL2Messages.map(key => key.toString()), + blocks[0].body.l1ToL2Messages.flatMap(key => (key.isZero() ? [] : key.toString())), ), makeL1ToL2MessageAddedEvents( 100n, - blocks[1].body.l1ToL2Messages.map(key => key.toString()), + blocks[1].body.l1ToL2Messages.flatMap(key => (key.isZero() ? [] : key.toString())), ), makeL1ToL2MessageAddedEvents( 2501n, - blocks[2].body.l1ToL2Messages.map(key => key.toString()), + blocks[2].body.l1ToL2Messages.flatMap(key => (key.isZero() ? [] : key.toString())), ), makeL1ToL2MessageAddedEvents(2502n, [ messageToCancel1, @@ -80,14 +82,23 @@ describe('Archiver', () => { publicClient.getLogs .mockResolvedValueOnce(l1ToL2MessageAddedEvents.slice(0, 2).flat()) .mockResolvedValueOnce([]) // no messages to cancel + .mockResolvedValueOnce([makeTxsPublishedEvent(101n, blocks[0].body.getCalldataHash())]) .mockResolvedValueOnce([makeL2BlockProcessedEvent(101n, 1n)]) .mockResolvedValueOnce([makeContractDeploymentEvent(103n, blocks[0])]) // the first loop of the archiver ends here at block 2500 .mockResolvedValueOnce(l1ToL2MessageAddedEvents.slice(2, 4).flat()) .mockResolvedValueOnce(makeL1ToL2MessageCancelledEvents(2503n, l1ToL2MessagesToCancel)) + .mockResolvedValueOnce([ + makeTxsPublishedEvent(2510n, blocks[1].body.getCalldataHash()), + makeTxsPublishedEvent(2520n, blocks[2].body.getCalldataHash()), + ]) .mockResolvedValueOnce([makeL2BlockProcessedEvent(2510n, 2n), makeL2BlockProcessedEvent(2520n, 3n)]) .mockResolvedValueOnce([makeContractDeploymentEvent(2540n, blocks[1])]) .mockResolvedValue([]); - rollupTxs.forEach(tx => publicClient.getTransaction.mockResolvedValueOnce(tx)); + publicClient.getTransaction.mockResolvedValueOnce(publishTxs[0]); + publicClient.getTransaction.mockResolvedValueOnce(rollupTxs[0]); + + publishTxs.slice(1).forEach(tx => publicClient.getTransaction.mockResolvedValueOnce(tx)); + rollupTxs.slice(1).forEach(tx => publicClient.getTransaction.mockResolvedValueOnce(tx)); await archiver.start(false); @@ -135,6 +146,7 @@ describe('Archiver', () => { const archiver = new Archiver( publicClient, EthAddress.fromString(rollupAddress), + EthAddress.fromString(availabilityOracleAddress), EthAddress.fromString(inboxAddress), EthAddress.fromString(registryAddress), EthAddress.fromString(contractDeploymentEmitterAddress), @@ -150,7 +162,10 @@ describe('Archiver', () => { }; const blocks = blockNumbers.map(x => L2Block.random(x, 4, x, x + 1, x * 2, x * 3)); + + const publishTxs = blocks.map(block => block.body).map(makePublishTx); const rollupTxs = blocks.map(makeRollupTx); + // `L2Block.random(x)` creates some l1 to l2 messages. We add those, // since it is expected by the test that these would be consumed. // Archiver removes such messages from pending store. @@ -162,11 +177,11 @@ describe('Archiver', () => { const l1ToL2MessageAddedEvents = [ makeL1ToL2MessageAddedEvents( 100n, - blocks[0].body.l1ToL2Messages.map(key => key.toString()), + blocks[0].body.l1ToL2Messages.flatMap(key => (key.isZero() ? [] : key.toString())), ), makeL1ToL2MessageAddedEvents( 101n, - blocks[1].body.l1ToL2Messages.map(key => key.toString()), + blocks[1].body.l1ToL2Messages.flatMap(key => (key.isZero() ? [] : key.toString())), ), makeL1ToL2MessageAddedEvents(102n, additionalL1ToL2MessagesBlock102), makeL1ToL2MessageAddedEvents(103n, additionalL1ToL2MessagesBlock103), @@ -184,8 +199,13 @@ describe('Archiver', () => { ); }) .mockResolvedValueOnce([]) + .mockResolvedValueOnce([ + makeTxsPublishedEvent(70n, blocks[0].body.getCalldataHash()), + makeTxsPublishedEvent(80n, blocks[1].body.getCalldataHash()), + ]) .mockResolvedValueOnce([makeL2BlockProcessedEvent(70n, 1n), makeL2BlockProcessedEvent(80n, 2n)]) .mockResolvedValue([]); + publishTxs.slice(0, numL2BlocksInTest).forEach(tx => publicClient.getTransaction.mockResolvedValueOnce(tx)); rollupTxs.slice(0, numL2BlocksInTest).forEach(tx => publicClient.getTransaction.mockResolvedValueOnce(tx)); await archiver.start(false); @@ -207,11 +227,10 @@ describe('Archiver', () => { }, 10_000); it('pads L1 to L2 messages', async () => { - const NUM_RECEIVED_L1_MESSAGES = 2; - const archiver = new Archiver( publicClient, EthAddress.fromString(rollupAddress), + EthAddress.fromString(availabilityOracleAddress), EthAddress.fromString(inboxAddress), EthAddress.fromString(registryAddress), EthAddress.fromString(contractDeploymentEmitterAddress), @@ -223,8 +242,8 @@ describe('Archiver', () => { expect(latestBlockNum).toEqual(0); const block = L2Block.random(1, 4, 1, 2, 4, 6); - block.body.l1ToL2Messages = times(2, Fr.random); const rollupTx = makeRollupTx(block); + const publishTx = makePublishTx(block.body); publicClient.getBlockNumber.mockResolvedValueOnce(2500n); // logs should be created in order of how archiver syncs. @@ -236,8 +255,10 @@ describe('Archiver', () => { ), ) .mockResolvedValueOnce([]) + .mockResolvedValueOnce([makeTxsPublishedEvent(101n, block.body.getCalldataHash())]) .mockResolvedValueOnce([makeL2BlockProcessedEvent(101n, 1n)]) .mockResolvedValue([]); + publicClient.getTransaction.mockResolvedValueOnce(publishTx); publicClient.getTransaction.mockResolvedValueOnce(rollupTx); await archiver.start(false); @@ -250,10 +271,9 @@ describe('Archiver', () => { latestBlockNum = await archiver.getBlockNumber(); expect(latestBlockNum).toEqual(1); - const expectedL1Messages = block.body.l1ToL2Messages - .concat(times(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP - NUM_RECEIVED_L1_MESSAGES, () => Fr.ZERO)) - .map(x => x.value); + const expectedL1Messages = block.body.l1ToL2Messages.map(x => x.value); const receivedBlock = await archiver.getBlock(1); + expect(receivedBlock?.body.l1ToL2Messages.map(x => x.value)).toEqual(expectedL1Messages); await archiver.stop(); @@ -263,7 +283,7 @@ describe('Archiver', () => { /** * Makes a fake L2BlockProcessed event for testing purposes. * @param l1BlockNum - L1 block number. - * @param l2BlockNum - L2Block number. + * @param l2BlockNum - L2 Block number. * @returns An L2BlockProcessed event log. */ function makeL2BlockProcessedEvent(l1BlockNum: bigint, l2BlockNum: bigint) { @@ -274,6 +294,21 @@ function makeL2BlockProcessedEvent(l1BlockNum: bigint, l2BlockNum: bigint) { } as Log; } +/** + * Makes a fake TxsPublished event for testing purposes. + * @param l1BlockNum - L1 block number. + * @param txsHash - txsHash for the body. + * @returns A TxsPublished event log. + */ +function makeTxsPublishedEvent(l1BlockNum: bigint, txsHash: Buffer) { + return { + blockNumber: l1BlockNum, + args: { + txsHash: txsHash.toString('hex'), + }, + } as Log; +} + /** * Makes a fake ContractDeployment event for testing purposes. * @param l1BlockNum - L1 block number. @@ -360,3 +395,18 @@ function makeRollupTx(l2Block: L2Block) { }); return { input } as Transaction; } + +/** + * Makes a fake availability oracle tx for testing purposes. + * @param blockBody - The block body posted by the simulated tx. + * @returns A fake tx with calldata that corresponds to calling publish in the Availability Oracle contract. + */ +function makePublishTx(blockBody: Body) { + const body = toHex(blockBody.toBuffer()); + const input = encodeFunctionData({ + abi: AvailabilityOracleAbi, + functionName: 'publish', + args: [body], + }); + return { input } as Transaction; +} diff --git a/yarn-project/archiver/src/archiver/archiver.ts b/yarn-project/archiver/src/archiver/archiver.ts index 5b7ffc3c591..f6908c1f3bd 100644 --- a/yarn-project/archiver/src/archiver/archiver.ts +++ b/yarn-project/archiver/src/archiver/archiver.ts @@ -16,15 +16,10 @@ import { TxHash, UnencryptedL2Log, } from '@aztec/circuit-types'; -import { - ContractClassRegisteredEvent, - FunctionSelector, - NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, -} from '@aztec/circuits.js'; +import { ContractClassRegisteredEvent, FunctionSelector } from '@aztec/circuits.js'; import { ContractInstanceDeployedEvent } from '@aztec/circuits.js/contract'; import { createEthereumChain } from '@aztec/ethereum'; import { AztecAddress } from '@aztec/foundation/aztec-address'; -import { padArrayEnd } from '@aztec/foundation/collection'; import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr } from '@aztec/foundation/fields'; import { DebugLogger, createDebugLogger } from '@aztec/foundation/log'; @@ -43,7 +38,8 @@ import { Chain, HttpTransport, PublicClient, createPublicClient, http } from 'vi import { ArchiverDataStore } from './archiver_store.js'; import { ArchiverConfig } from './config.js'; import { - retrieveBlocks, + retrieveBlockBodiesFromAvailabilityOracle, + retrieveBlockMetadataFromRollup, retrieveNewCancelledL1ToL2Messages, retrieveNewContractData, retrieveNewPendingL1ToL2Messages, @@ -84,6 +80,7 @@ export class Archiver implements ArchiveSource { constructor( private readonly publicClient: PublicClient, private readonly rollupAddress: EthAddress, + private readonly availabilityOracleAddress: EthAddress, private readonly inboxAddress: EthAddress, private readonly registryAddress: EthAddress, private readonly contractDeploymentEmitterAddress: EthAddress, @@ -114,6 +111,7 @@ export class Archiver implements ArchiveSource { const archiver = new Archiver( publicClient, config.l1Contracts.rollupAddress, + config.l1Contracts.availabilityOracleAddress, config.l1Contracts.inboxAddress, config.l1Contracts.registryAddress, config.l1Contracts.contractDeploymentEmitterAddress, @@ -238,7 +236,20 @@ export class Archiver implements ArchiveSource { // Read all data from chain and then write to our stores at the end const nextExpectedL2BlockNum = BigInt((await this.store.getBlockNumber()) + 1); - const retrievedBlocks = await retrieveBlocks( + + const retrievedBlockBodies = await retrieveBlockBodiesFromAvailabilityOracle( + this.publicClient, + this.availabilityOracleAddress, + blockUntilSynced, + lastL1Blocks.addedBlock + 1n, + currentL1BlockNumber, + ); + + const blockBodies = retrievedBlockBodies.retrievedData.map(([blockBody]) => blockBody); + + await this.store.addBlockBodies(blockBodies); + + const retrievedBlockMetadata = await retrieveBlockMetadataFromRollup( this.publicClient, this.rollupAddress, blockUntilSynced, @@ -247,6 +258,23 @@ export class Archiver implements ArchiveSource { nextExpectedL2BlockNum, ); + const retrievedBodyHashes = retrievedBlockMetadata.retrievedData.map( + ([header]) => header.contentCommitment.txsHash, + ); + + const blockBodiesFromStore = await this.store.getBlockBodies(retrievedBodyHashes); + + if (retrievedBlockMetadata.retrievedData.length !== blockBodiesFromStore.length) { + throw new Error('Block headers length does not equal block bodies length'); + } + + const retrievedBlocks = { + retrievedData: retrievedBlockMetadata.retrievedData.map( + (blockMetadata, i) => + new L2Block(blockMetadata[1], blockMetadata[0], blockBodiesFromStore[i], blockMetadata[2]), + ), + }; + if (retrievedBlocks.retrievedData.length === 0) { return; } else { @@ -260,7 +288,7 @@ export class Archiver implements ArchiveSource { // create the block number -> block hash mapping to ensure we retrieve the appropriate events const blockNumberToBodyHash: { [key: number]: Buffer | undefined } = {}; retrievedBlocks.retrievedData.forEach((block: L2Block) => { - blockNumberToBodyHash[block.number] = block.body.getCalldataHash(); + blockNumberToBodyHash[block.number] = block.header.contentCommitment.txsHash; }); const retrievedContracts = await retrieveNewContractData( this.publicClient, @@ -313,20 +341,7 @@ export class Archiver implements ArchiveSource { await this.store.confirmL1ToL2EntryKeys(block.body.l1ToL2Messages); } - // store retrieved L2 blocks after removing new logs information. - // remove logs to serve "lightweight" block information. Logs can be fetched separately if needed. - await this.store.addBlocks( - retrievedBlocks.retrievedData.map(block => { - // Ensure we pad the L1 to L2 message array to the full size before storing. - block.body.l1ToL2Messages = padArrayEnd( - block.body.l1ToL2Messages, - Fr.ZERO, - NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, - ); - - return block; - }), - ); + await this.store.addBlocks(retrievedBlocks.retrievedData); } /** diff --git a/yarn-project/archiver/src/archiver/archiver_store.ts b/yarn-project/archiver/src/archiver/archiver_store.ts index f5449bcc26f..3e46a584d6c 100644 --- a/yarn-project/archiver/src/archiver/archiver_store.ts +++ b/yarn-project/archiver/src/archiver/archiver_store.ts @@ -1,4 +1,5 @@ import { + Body, ContractData, ExtendedContractData, GetUnencryptedLogsResponse, @@ -38,6 +39,21 @@ export interface ArchiverDataStore { */ addBlocks(blocks: L2Block[]): Promise; + /** + * Append new block bodies to the store's list. + * @param blockBodies - The L2 block bodies to be added to the store. + * @returns True if the operation is successful. + */ + addBlockBodies(blockBodies: Body[]): Promise; + + /** + * Gets block bodies that have the same txsHashes as we supply. + * + * @param txsHashes - A list of txsHashes (body hashes). + * @returns The requested L2 block bodies + */ + getBlockBodies(txsHashes: Buffer[]): Promise; + /** * Gets up to `limit` amount of L2 blocks starting from `from`. * @param from - Number of the first block to return (inclusive). diff --git a/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts b/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts index be90e7da57c..66ed2937cd8 100644 --- a/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts +++ b/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts @@ -43,6 +43,10 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch }); describe('addBlocks', () => { + it('returns success when adding block bodies', async () => { + await expect(store.addBlockBodies(blocks.map(block => block.body))).resolves.toBe(true); + }); + it('returns success when adding blocks', async () => { await expect(store.addBlocks(blocks)).resolves.toBe(true); }); @@ -56,6 +60,7 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch describe('getBlocks', () => { beforeEach(async () => { await store.addBlocks(blocks); + await store.addBlockBodies(blocks.map(block => block.body)); }); it.each(blockTests)('retrieves previously stored blocks', async (start, limit, getExpectedBlocks) => { @@ -157,6 +162,7 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch blocks.map(block => store.addLogs(block.body.encryptedLogs, block.body.unencryptedLogs, block.number)), ); await store.addBlocks(blocks); + await store.addBlockBodies(blocks.map(block => block.body)); }); it.each([ @@ -363,6 +369,7 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch beforeEach(async () => { block = L2Block.random(1); await store.addBlocks([block]); + await store.addBlockBodies([block.body]); }); it('returns previously stored contract data', async () => { @@ -381,6 +388,7 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch beforeEach(async () => { block = L2Block.random(1); await store.addBlocks([block]); + await store.addBlockBodies([block.body]); }); it('returns the contract data for a known block', async () => { @@ -398,6 +406,7 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch it('stores extended contract data', async () => { const block = L2Block.random(1); await store.addBlocks([block]); + await store.addBlockBodies([block.body]); await expect(store.addExtendedContractData([ExtendedContractData.random()], block.number)).resolves.toEqual( true, ); @@ -410,6 +419,7 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch it('"pushes" extended contract data and does not overwrite', async () => { const block = L2Block.random(1); await store.addBlocks([block]); + await store.addBlockBodies([block.body]); // Assuming one contract per tx, and the first two txs const firstContract = ExtendedContractData.random(block.body.txEffects[0].contractData[0]); @@ -432,6 +442,7 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch block = L2Block.random(1); extendedContractData = ExtendedContractData.random(block.body.txEffects[0].contractData[0]); await store.addBlocks([block]); + await store.addBlockBodies([block.body]); await store.addExtendedContractData([extendedContractData], block.number); }); @@ -453,6 +464,7 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch block = L2Block.random(1); extendedContractData = ExtendedContractData.random(block.body.txEffects[0].contractData[0]); await store.addBlocks([block]); + await store.addBlockBodies([block.body]); await store.addExtendedContractData([extendedContractData], block.number); }); @@ -480,6 +492,8 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch ); await store.addBlocks(blocks); + await store.addBlockBodies(blocks.map(block => block.body)); + await Promise.all( blocks.map(block => store.addLogs(block.body.encryptedLogs, block.body.unencryptedLogs, block.number)), ); diff --git a/yarn-project/archiver/src/archiver/data_retrieval.ts b/yarn-project/archiver/src/archiver/data_retrieval.ts index ad648de6ccf..9ac40aaac34 100644 --- a/yarn-project/archiver/src/archiver/data_retrieval.ts +++ b/yarn-project/archiver/src/archiver/data_retrieval.ts @@ -1,5 +1,5 @@ -import { ExtendedContractData, L1ToL2Message, L2Block } from '@aztec/circuit-types'; -import { Fr } from '@aztec/circuits.js'; +import { Body, ExtendedContractData, L1ToL2Message } from '@aztec/circuit-types'; +import { AppendOnlyTreeSnapshot, Fr, Header } from '@aztec/circuits.js'; import { EthAddress } from '@aztec/foundation/eth-address'; import { PublicClient } from 'viem'; @@ -9,10 +9,12 @@ import { getL1ToL2MessageCancelledLogs, getL2BlockProcessedLogs, getPendingL1ToL2MessageLogs, - processBlockLogs, + getTxsPublishedLogs, processCancelledL1ToL2MessagesLogs, processContractDeploymentLogs, + processL2BlockProcessedLogs, processPendingL1ToL2MessageAddedLogs, + processTxsPublishedLogs, } from './eth_log_handlers.js'; /** @@ -30,24 +32,24 @@ type DataRetrieval = { }; /** - * Fetches new L2 Blocks. + * Fetches new L2 block metadata (header, archive snapshot). * @param publicClient - The viem public client to use for transaction retrieval. * @param rollupAddress - The address of the rollup contract. * @param blockUntilSynced - If true, blocks until the archiver has fully synced. * @param searchStartBlock - The block number to use for starting the search. * @param searchEndBlock - The highest block number that we should search up to. * @param expectedNextL2BlockNum - The next L2 block number that we expect to find. - * @returns An array of L2 Blocks and the next eth block to search from + * @returns An array of tuples representing block metadata including the header, archive tree snapshot, and associated l1 block number; as well as the next eth block to search from. */ -export async function retrieveBlocks( +export async function retrieveBlockMetadataFromRollup( publicClient: PublicClient, rollupAddress: EthAddress, blockUntilSynced: boolean, searchStartBlock: bigint, searchEndBlock: bigint, expectedNextL2BlockNum: bigint, -): Promise> { - const retrievedBlocks: L2Block[] = []; +): Promise> { + const retrievedBlockMetadata: [Header, AppendOnlyTreeSnapshot, bigint][] = []; do { if (searchStartBlock > searchEndBlock) { break; @@ -62,12 +64,55 @@ export async function retrieveBlocks( break; } - const newBlocks = await processBlockLogs(publicClient, expectedNextL2BlockNum, l2BlockProcessedLogs); - retrievedBlocks.push(...newBlocks); + const newBlockMetadata = await processL2BlockProcessedLogs( + publicClient, + expectedNextL2BlockNum, + l2BlockProcessedLogs, + ); + retrievedBlockMetadata.push(...newBlockMetadata); searchStartBlock = l2BlockProcessedLogs[l2BlockProcessedLogs.length - 1].blockNumber! + 1n; - expectedNextL2BlockNum += BigInt(newBlocks.length); + expectedNextL2BlockNum += BigInt(newBlockMetadata.length); + } while (blockUntilSynced && searchStartBlock <= searchEndBlock); + return { nextEthBlockNumber: searchStartBlock, retrievedData: retrievedBlockMetadata }; +} + +/** + * Fetches new L2 block bodies and their hashes. + * @param publicClient - The viem public client to use for transaction retrieval. + * @param availabilityOracleAddress - The address of the availability oracle contract. + * @param blockUntilSynced - If true, blocks until the archiver has fully synced. + * @param searchStartBlock - The block number to use for starting the search. + * @param searchEndBlock - The highest block number that we should search up to. + * @returns A array of tuples of L2 block bodies and their associated hash as well as the next eth block to search from + */ +export async function retrieveBlockBodiesFromAvailabilityOracle( + publicClient: PublicClient, + availabilityOracleAddress: EthAddress, + blockUntilSynced: boolean, + searchStartBlock: bigint, + searchEndBlock: bigint, +): Promise> { + const retrievedBlockBodies: [Body, Buffer][] = []; + + do { + if (searchStartBlock > searchEndBlock) { + break; + } + const l2TxsPublishedLogs = await getTxsPublishedLogs( + publicClient, + availabilityOracleAddress, + searchStartBlock, + searchEndBlock, + ); + if (l2TxsPublishedLogs.length === 0) { + break; + } + + const newBlockBodies = await processTxsPublishedLogs(publicClient, l2TxsPublishedLogs); + retrievedBlockBodies.push(...newBlockBodies); + searchStartBlock = l2TxsPublishedLogs[l2TxsPublishedLogs.length - 1].blockNumber! + 1n; } while (blockUntilSynced && searchStartBlock <= searchEndBlock); - return { nextEthBlockNumber: searchStartBlock, retrievedData: retrievedBlocks }; + return { nextEthBlockNumber: searchStartBlock, retrievedData: retrievedBlockBodies }; } /** diff --git a/yarn-project/archiver/src/archiver/eth_log_handlers.ts b/yarn-project/archiver/src/archiver/eth_log_handlers.ts index bcaf305488e..40d73f75168 100644 --- a/yarn-project/archiver/src/archiver/eth_log_handlers.ts +++ b/yarn-project/archiver/src/archiver/eth_log_handlers.ts @@ -1,17 +1,18 @@ import { + Body, ContractData, EncodedContractFunction, ExtendedContractData, L1Actor, L1ToL2Message, L2Actor, - L2Block, } from '@aztec/circuit-types'; +import { AppendOnlyTreeSnapshot, Header } from '@aztec/circuits.js'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr } from '@aztec/foundation/fields'; import { BufferReader, numToUInt32BE } from '@aztec/foundation/serialize'; -import { ContractDeploymentEmitterAbi, InboxAbi, RollupAbi } from '@aztec/l1-artifacts'; +import { AvailabilityOracleAbi, ContractDeploymentEmitterAbi, InboxAbi, RollupAbi } from '@aztec/l1-artifacts'; import { Hex, Log, PublicClient, decodeFunctionData, getAbiItem, getAddress, hexToBytes } from 'viem'; @@ -63,61 +64,116 @@ export function processCancelledL1ToL2MessagesLogs( * @param publicClient - The viem public client to use for transaction retrieval. * @param expectedL2BlockNumber - The next expected L2 block number. * @param logs - L2BlockProcessed logs. + * @returns - An array of tuples representing block metadata including the header, archive tree snapshot, and associated l1 block number. */ -export async function processBlockLogs( +export async function processL2BlockProcessedLogs( publicClient: PublicClient, expectedL2BlockNumber: bigint, logs: Log[], -): Promise { - const retrievedBlocks: L2Block[] = []; +): Promise<[Header, AppendOnlyTreeSnapshot, bigint][]> { + const retrievedBlockMetadata: [Header, AppendOnlyTreeSnapshot, bigint][] = []; for (const log of logs) { const blockNum = log.args.blockNumber; if (blockNum !== expectedL2BlockNumber) { throw new Error('Block number mismatch. Expected: ' + expectedL2BlockNumber + ' but got: ' + blockNum + '.'); } // TODO: Fetch blocks from calldata in parallel - const newBlock = await getBlockFromCallData(publicClient, log.transactionHash!, log.args.blockNumber); - newBlock.setL1BlockNumber(log.blockNumber!); - retrievedBlocks.push(newBlock); + const [header, archive] = await getBlockMetadataFromRollupTx( + publicClient, + log.transactionHash!, + log.args.blockNumber, + ); + + retrievedBlockMetadata.push([header, archive, log.blockNumber!]); expectedL2BlockNumber++; } - return retrievedBlocks; + + return retrievedBlockMetadata; +} + +export async function processTxsPublishedLogs( + publicClient: PublicClient, + logs: Log[], +): Promise<[Body, Buffer][]> { + const retrievedBlockBodies: [Body, Buffer][] = []; + for (const log of logs) { + const newBlockBody = await getBlockBodiesFromAvailabilityOracleTx(publicClient, log.transactionHash!); + retrievedBlockBodies.push([newBlockBody, Buffer.from(hexToBytes(log.args.txsHash))]); + } + + return retrievedBlockBodies; } /** - * Builds an L2 block out of calldata from the tx that published it. + * Gets block metadata (header and archive snapshot) from the calldata of an L1 transaction. * Assumes that the block was published from an EOA. * TODO: Add retries and error management. * @param publicClient - The viem public client to use for transaction retrieval. * @param txHash - Hash of the tx that published it. * @param l2BlockNum - L2 block number. - * @returns An L2 block deserialized from the calldata. + * @returns L2 block metadata (header and archive) from the calldata, deserialized */ -async function getBlockFromCallData( +async function getBlockMetadataFromRollupTx( publicClient: PublicClient, txHash: `0x${string}`, l2BlockNum: bigint, -): Promise { +): Promise<[Header, AppendOnlyTreeSnapshot]> { const { input: data } = await publicClient.getTransaction({ hash: txHash }); const { functionName, args } = decodeFunctionData({ abi: RollupAbi, data, }); + if (functionName !== 'process') { throw new Error(`Unexpected method called ${functionName}`); } - const [headerHex, archiveRootHex, bodyHex] = args! as [Hex, Hex, Hex, Hex]; - const blockBuffer = Buffer.concat([ - Buffer.from(hexToBytes(headerHex)), - Buffer.from(hexToBytes(archiveRootHex)), // L2Block.archive.root - numToUInt32BE(Number(l2BlockNum)), // L2Block.archive.nextAvailableLeafIndex - Buffer.from(hexToBytes(bodyHex)), - ]); - const block = L2Block.fromBuffer(blockBuffer); - if (BigInt(block.number) !== l2BlockNum) { - throw new Error(`Block number mismatch: expected ${l2BlockNum} but got ${block.number}`); + const [headerHex, archiveRootHex] = args! as readonly [Hex, Hex, Hex, Hex]; + + const header = Header.fromBuffer(Buffer.from(hexToBytes(headerHex))); + + const blockNumberFromHeader = header.globalVariables.blockNumber.toBigInt(); + + if (blockNumberFromHeader !== l2BlockNum) { + throw new Error(`Block number mismatch: expected ${l2BlockNum} but got ${blockNumberFromHeader}`); } - return block; + + const archive = AppendOnlyTreeSnapshot.fromBuffer( + Buffer.concat([ + Buffer.from(hexToBytes(archiveRootHex)), // L2Block.archive.root + numToUInt32BE(Number(l2BlockNum)), // L2Block.archive.nextAvailableLeafIndex + ]), + ); + + return [header, archive]; +} + +/** + * Gets block bodies from calldata of an L1 transaction, and deserializes them into Body objects. + * Assumes that the block was published from an EOA. + * TODO: Add retries and error management. + * @param publicClient - The viem public client to use for transaction retrieval. + * @param txHash - Hash of the tx that published it. + * @returns An L2 block body from the calldata, deserialized + */ +async function getBlockBodiesFromAvailabilityOracleTx( + publicClient: PublicClient, + txHash: `0x${string}`, +): Promise { + const { input: data } = await publicClient.getTransaction({ hash: txHash }); + const { functionName, args } = decodeFunctionData({ + abi: AvailabilityOracleAbi, + data, + }); + + if (functionName !== 'publish') { + throw new Error(`Unexpected method called ${functionName}`); + } + + const [bodyHex] = args! as [Hex]; + + const blockBody = Body.fromBuffer(Buffer.from(hexToBytes(bodyHex))); + + return blockBody; } /** @@ -145,6 +201,31 @@ export function getL2BlockProcessedLogs( }); } +/** + * Gets relevant `TxsPublished` logs from chain. + * @param publicClient - The viem public client to use for transaction retrieval. + * @param dataAvailabilityOracleAddress - The address of the availability oracle contract. + * @param fromBlock - First block to get logs from (inclusive). + * @param toBlock - Last block to get logs from (inclusive). + * @returns An array of `TxsPublished` logs. + */ +export function getTxsPublishedLogs( + publicClient: PublicClient, + dataAvailabilityOracleAddress: EthAddress, + fromBlock: bigint, + toBlock: bigint, +): Promise[]> { + return publicClient.getLogs({ + address: getAddress(dataAvailabilityOracleAddress.toString()), + event: getAbiItem({ + abi: AvailabilityOracleAbi, + name: 'TxsPublished', + }), + fromBlock, + toBlock: toBlock + 1n, // the toBlock argument in getLogs is exclusive + }); +} + /** * Gets relevant `ContractDeployment` logs from chain. * @param publicClient - The viem public client to use for transaction retrieval. diff --git a/yarn-project/archiver/src/archiver/kv_archiver_store/block_body_store.test.ts b/yarn-project/archiver/src/archiver/kv_archiver_store/block_body_store.test.ts new file mode 100644 index 00000000000..d86682fd7c3 --- /dev/null +++ b/yarn-project/archiver/src/archiver/kv_archiver_store/block_body_store.test.ts @@ -0,0 +1,24 @@ +import { Body } from '@aztec/circuit-types'; +import { openTmpStore } from '@aztec/kv-store/utils'; + +import { KVArchiverDataStore } from './kv_archiver_store.js'; + +describe('Block Body Store', () => { + let archiverStore: KVArchiverDataStore; + + beforeEach(() => { + archiverStore = new KVArchiverDataStore(openTmpStore()); + }); + + it('Should add and return block bodies', async () => { + const body = Body.random(1); + + await archiverStore.addBlockBodies([body]); + + const txsHash = body.getCalldataHash(); + + const [returnedBody] = await archiverStore.getBlockBodies([txsHash]); + + expect(body).toStrictEqual(returnedBody); + }); +}); diff --git a/yarn-project/archiver/src/archiver/kv_archiver_store/block_body_store.ts b/yarn-project/archiver/src/archiver/kv_archiver_store/block_body_store.ts new file mode 100644 index 00000000000..12d7580ae83 --- /dev/null +++ b/yarn-project/archiver/src/archiver/kv_archiver_store/block_body_store.ts @@ -0,0 +1,54 @@ +import { Body } from '@aztec/circuit-types'; +import { AztecKVStore, AztecMap } from '@aztec/kv-store'; + +export class BlockBodyStore { + /** Map block body hash to block body */ + #blockBodies: AztecMap; + + constructor(private db: AztecKVStore) { + this.#blockBodies = db.openMap('archiver_block_bodies'); + } + + /** + * Append new block bodies to the store's map. + * @param blockBodies - The L2 block bodies to be added to the store. + * @returns True if the operation is successful. + */ + addBlockBodies(blockBodies: Body[]): Promise { + return this.db.transaction(() => { + for (const body of blockBodies) { + void this.#blockBodies.set(body.getCalldataHash().toString('hex'), body.toBuffer()); + } + + return true; + }); + } + + /** + * Gets a list of L2 block bodies with its associated txsHashes + * @param txsHashes - The txsHashes list that corresponds to the blockBodies we want to retrieve + * @returns The requested L2 block bodies + */ + async getBlockBodies(txsHashes: Buffer[]): Promise { + const blockBodiesBuffer = await this.db.transaction(() => + txsHashes.map(txsHash => this.#blockBodies.get(txsHash.toString('hex'))), + ); + + if (blockBodiesBuffer.some(bodyBuffer => bodyBuffer === undefined)) { + throw new Error('Block body buffer is undefined'); + } + + return blockBodiesBuffer.map(blockBodyBuffer => Body.fromBuffer(blockBodyBuffer!)); + } + + /** + * Gets an L2 block body. + * @param txsHash - The txHash of the the block body to return + * @returns The requested L2 block body + */ + getBlockBody(txsHash: Buffer): Body | undefined { + const blockBody = this.#blockBodies.get(txsHash.toString('hex')); + + return blockBody && Body.fromBuffer(blockBody); + } +} diff --git a/yarn-project/archiver/src/archiver/kv_archiver_store/block_store.ts b/yarn-project/archiver/src/archiver/kv_archiver_store/block_store.ts index 49bac68d66b..b875dab6376 100644 --- a/yarn-project/archiver/src/archiver/kv_archiver_store/block_store.ts +++ b/yarn-project/archiver/src/archiver/kv_archiver_store/block_store.ts @@ -1,14 +1,16 @@ import { INITIAL_L2_BLOCK_NUM, L2Block, L2Tx, TxHash } from '@aztec/circuit-types'; -import { AztecAddress } from '@aztec/circuits.js'; +import { AppendOnlyTreeSnapshot, AztecAddress, Header } from '@aztec/circuits.js'; import { createDebugLogger } from '@aztec/foundation/log'; import { AztecKVStore, AztecMap, Range } from '@aztec/kv-store'; +import { BlockBodyStore } from './block_body_store.js'; + type BlockIndexValue = [blockNumber: number, index: number]; -type BlockContext = { - blockNumber: number; +type BlockStorage = { l1BlockNumber: bigint; - block: Buffer; + header: Buffer; + archive: Buffer; }; /** @@ -16,7 +18,7 @@ type BlockContext = { */ export class BlockStore { /** Map block number to block data */ - #blocks: AztecMap; + #blocks: AztecMap; /** Index mapping transaction hash (as a string) to its location in a block */ #txIndex: AztecMap; @@ -26,9 +28,12 @@ export class BlockStore { #log = createDebugLogger('aztec:archiver:block_store'); - constructor(private db: AztecKVStore) { - this.#blocks = db.openMap('archiver_blocks'); + #blockBodyStore: BlockBodyStore; + + constructor(private db: AztecKVStore, blockBodyStore: BlockBodyStore) { + this.#blockBodyStore = blockBodyStore; + this.#blocks = db.openMap('archiver_blocks'); this.#txIndex = db.openMap('archiver_tx_index'); this.#contractIndex = db.openMap('archiver_contract_index'); } @@ -42,25 +47,27 @@ export class BlockStore { return this.db.transaction(() => { for (const block of blocks) { void this.#blocks.set(block.number, { - blockNumber: block.number, - block: block.toBuffer(), + header: block.header.toBuffer(), + archive: block.archive.toBuffer(), l1BlockNumber: block.getL1BlockNumber(), }); - for (const [i, tx] of block.getTxs().entries()) { + block.getTxs().forEach((tx, i) => { if (tx.txHash.isZero()) { - continue; + return; } void this.#txIndex.set(tx.txHash.toString(), [block.number, i]); - } + }); - for (const [i, contractData] of block.body.txEffects.flatMap(txEffect => txEffect.contractData).entries()) { - if (contractData.contractAddress.isZero()) { - continue; - } + block.body.txEffects + .flatMap(txEffect => txEffect.contractData) + .forEach((contractData, i) => { + if (contractData.contractAddress.isZero()) { + return; + } - void this.#contractIndex.set(contractData.contractAddress.toString(), [block.number, i]); - } + void this.#contractIndex.set(contractData.contractAddress.toString(), [block.number, i]); + }); } return true; @@ -71,26 +78,42 @@ export class BlockStore { * Gets up to `limit` amount of L2 blocks starting from `from`. * @param start - Number of the first block to return (inclusive). * @param limit - The number of blocks to return. - * @returns The requested L2 blocks, without logs attached + * @returns The requested L2 blocks */ *getBlocks(start: number, limit: number): IterableIterator { - for (const blockCtx of this.#blocks.values(this.#computeBlockRange(start, limit))) { - yield L2Block.fromBuffer(blockCtx.block); + for (const blockStorage of this.#blocks.values(this.#computeBlockRange(start, limit))) { + yield this.getBlockFromBlockStorage(blockStorage); } } /** * Gets an L2 block. * @param blockNumber - The number of the block to return. - * @returns The requested L2 block, without logs attached + * @returns The requested L2 block. */ getBlock(blockNumber: number): L2Block | undefined { - const blockCtx = this.#blocks.get(blockNumber); - if (!blockCtx || !blockCtx.block) { + const blockStorage = this.#blocks.get(blockNumber); + if (!blockStorage || !blockStorage.header) { return undefined; } - return L2Block.fromBuffer(blockCtx.block); + return this.getBlockFromBlockStorage(blockStorage); + } + + private getBlockFromBlockStorage(blockStorage: BlockStorage) { + const header = Header.fromBuffer(blockStorage.header); + const archive = AppendOnlyTreeSnapshot.fromBuffer(blockStorage.archive); + const body = this.#blockBodyStore.getBlockBody(header.contentCommitment.txsHash); + + if (body === undefined) { + throw new Error('Body is not able to be retrieved from BodyStore'); + } + + return L2Block.fromFields({ + header, + archive, + body, + }); } /** diff --git a/yarn-project/archiver/src/archiver/kv_archiver_store/kv_archiver_store.ts b/yarn-project/archiver/src/archiver/kv_archiver_store/kv_archiver_store.ts index 27f7073109a..473cd3be67c 100644 --- a/yarn-project/archiver/src/archiver/kv_archiver_store/kv_archiver_store.ts +++ b/yarn-project/archiver/src/archiver/kv_archiver_store/kv_archiver_store.ts @@ -1,4 +1,5 @@ import { + Body, ContractData, ExtendedContractData, GetUnencryptedLogsResponse, @@ -17,6 +18,7 @@ import { AztecKVStore } from '@aztec/kv-store'; import { ContractClassPublic, ContractInstanceWithAddress } from '@aztec/types/contracts'; import { ArchiverDataStore, ArchiverL1SynchPoint } from '../archiver_store.js'; +import { BlockBodyStore } from './block_body_store.js'; import { BlockStore } from './block_store.js'; import { ContractClassStore } from './contract_class_store.js'; import { ContractInstanceStore } from './contract_instance_store.js'; @@ -29,6 +31,7 @@ import { MessageStore } from './message_store.js'; */ export class KVArchiverDataStore implements ArchiverDataStore { #blockStore: BlockStore; + #blockBodyStore: BlockBodyStore; #logStore: LogStore; #contractStore: ContractStore; #messageStore: MessageStore; @@ -38,7 +41,8 @@ export class KVArchiverDataStore implements ArchiverDataStore { #log = createDebugLogger('aztec:archiver:data-store'); constructor(db: AztecKVStore, logsMaxPageSize: number = 1000) { - this.#blockStore = new BlockStore(db); + this.#blockBodyStore = new BlockBodyStore(db); + this.#blockStore = new BlockStore(db, this.#blockBodyStore); this.#logStore = new LogStore(db, this.#blockStore, logsMaxPageSize); this.#contractStore = new ContractStore(db, this.#blockStore); this.#messageStore = new MessageStore(db); @@ -66,6 +70,25 @@ export class KVArchiverDataStore implements ArchiverDataStore { return (await Promise.all(data.map(c => this.#contractInstanceStore.addContractInstance(c)))).every(Boolean); } + /** + * Append new block bodies to the store's list. + * @param blockBodies - The L2 block bodies to be added to the store. + * @returns True if the operation is successful. + */ + addBlockBodies(blockBodies: Body[]): Promise { + return this.#blockBodyStore.addBlockBodies(blockBodies); + } + + /** + * Gets block bodies that have the same txHashes as we supply. + * + * @param txsHashes - A list of txsHashes (body hashes). + * @returns The requested L2 block bodies + */ + getBlockBodies(txsHashes: Buffer[]): Promise { + return this.#blockBodyStore.getBlockBodies(txsHashes); + } + /** * Append new blocks to the store's list. * @param blocks - The L2 blocks to be added to the store. @@ -77,11 +100,10 @@ export class KVArchiverDataStore implements ArchiverDataStore { /** * Gets up to `limit` amount of L2 blocks starting from `from`. - * The blocks returned do not contain any logs. * * @param start - Number of the first block to return (inclusive). * @param limit - The number of blocks to return. - * @returns The requested L2 blocks, without any logs attached + * @returns The requested L2 blocks */ getBlocks(start: number, limit: number): Promise { try { diff --git a/yarn-project/archiver/src/archiver/kv_archiver_store/message_store.ts b/yarn-project/archiver/src/archiver/kv_archiver_store/message_store.ts index e9e13392991..641a8421cb5 100644 --- a/yarn-project/archiver/src/archiver/kv_archiver_store/message_store.ts +++ b/yarn-project/archiver/src/archiver/kv_archiver_store/message_store.ts @@ -115,6 +115,10 @@ export class MessageStore { confirmPendingMessages(entryKeys: Fr[]): Promise { return this.db.transaction(() => { for (const entryKey of entryKeys) { + if (entryKey.equals(Fr.ZERO)) { + continue; + } + const messageCtx = this.#messages.get(entryKey.toString()); if (!messageCtx) { throw new Error(`Message ${entryKey.toString()} not found`); diff --git a/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts b/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts index 9d27331589e..84708ec56cd 100644 --- a/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts +++ b/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts @@ -1,4 +1,5 @@ import { + Body, ContractData, ExtendedContractData, ExtendedUnencryptedL2Log, @@ -31,6 +32,11 @@ export class MemoryArchiverStore implements ArchiverDataStore { */ private l2BlockContexts: L2BlockContext[] = []; + /** + * A mapping of body hash to body + */ + private l2BlockBodies: Map = new Map(); + /** * An array containing all the L2 Txs in the L2 blocks that have been fetched so far. */ @@ -118,6 +124,35 @@ export class MemoryArchiverStore implements ArchiverDataStore { return Promise.resolve(true); } + /** + * Append new block bodies to the store's list. + * @param blockBodies - The L2 block bodies to be added to the store. + * @returns True if the operation is successful. + */ + addBlockBodies(blockBodies: Body[]): Promise { + for (const body of blockBodies) { + void this.l2BlockBodies.set(body.getCalldataHash().toString('hex'), body); + } + + return Promise.resolve(true); + } + + /** + * Gets block bodies that have the same txHashes as we supply. + * + * @param txsHashes - A list of txsHashes (body hashes). + * @returns The requested L2 block bodies + */ + getBlockBodies(txsHashes: Buffer[]): Promise { + const blockBodies = txsHashes.map(txsHash => this.l2BlockBodies.get(txsHash.toString('hex'))); + + if (blockBodies.some(bodyBuffer => bodyBuffer === undefined)) { + throw new Error('Block body is undefined'); + } + + return Promise.resolve(blockBodies as Body[]); + } + /** * Append new logs to the store's list. * @param encryptedLogs - The encrypted logs to be added to the store. @@ -181,6 +216,10 @@ export class MemoryArchiverStore implements ArchiverDataStore { */ public confirmL1ToL2EntryKeys(entryKeys: Fr[]): Promise { entryKeys.forEach(entryKey => { + if (entryKey.equals(Fr.ZERO)) { + return; + } + this.confirmedL1ToL2Messages.addMessage(entryKey, this.pendingL1ToL2Messages.getMessage(entryKey)!); this.pendingL1ToL2Messages.removeMessage(entryKey); }); diff --git a/yarn-project/archiver/src/index.ts b/yarn-project/archiver/src/index.ts index e47ee3fcc37..7f306cdf634 100644 --- a/yarn-project/archiver/src/index.ts +++ b/yarn-project/archiver/src/index.ts @@ -30,6 +30,7 @@ async function main() { const archiver = new Archiver( publicClient, l1Contracts.rollupAddress, + l1Contracts.availabilityOracleAddress, l1Contracts.inboxAddress, l1Contracts.registryAddress, l1Contracts.contractDeploymentEmitterAddress, diff --git a/yarn-project/circuit-types/src/body.ts b/yarn-project/circuit-types/src/body.ts index 2314a723fff..d6f6495e036 100644 --- a/yarn-project/circuit-types/src/body.ts +++ b/yarn-project/circuit-types/src/body.ts @@ -1,13 +1,16 @@ import { L2BlockL2Logs, TxEffect } from '@aztec/circuit-types'; import { NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP } from '@aztec/circuits.js'; +import { makeTuple } from '@aztec/foundation/array'; +import { padArrayEnd } from '@aztec/foundation/collection'; import { sha256 } from '@aztec/foundation/crypto'; import { Fr } from '@aztec/foundation/fields'; -import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; - -import times from 'lodash.times'; +import { BufferReader, Tuple, serializeToBuffer } from '@aztec/foundation/serialize'; export class Body { - constructor(public l1ToL2Messages: Fr[], public txEffects: TxEffect[]) {} + constructor( + public l1ToL2Messages: Tuple, + public txEffects: TxEffect[], + ) {} /** * Serializes a block body @@ -23,7 +26,12 @@ export class Body { */ static fromBuffer(buf: Buffer | BufferReader) { const reader = BufferReader.asReader(buf); - return new this(reader.readVector(Fr), reader.readVector(TxEffect)); + const l1ToL2Messages = reader.readVector(Fr); + + return new this( + padArrayEnd(l1ToL2Messages, Fr.ZERO, NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP), + reader.readVector(TxEffect), + ); } /** @@ -83,12 +91,13 @@ export class Body { numPublicCallsPerTx = 3, numEncryptedLogsPerCall = 2, numUnencryptedLogsPerCall = 1, + numL1ToL2MessagesPerCall = 2, ) { - const newL1ToL2Messages = times(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, Fr.random); + const newL1ToL2Messages = makeTuple(numL1ToL2MessagesPerCall, Fr.random); const txEffects = [...new Array(txsPerBlock)].map(_ => TxEffect.random(numPrivateCallsPerTx, numPublicCallsPerTx, numEncryptedLogsPerCall, numUnencryptedLogsPerCall), ); - return new Body(newL1ToL2Messages, txEffects); + return new Body(padArrayEnd(newL1ToL2Messages, Fr.ZERO, NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP), txEffects); } } diff --git a/yarn-project/circuit-types/src/l2_block.ts b/yarn-project/circuit-types/src/l2_block.ts index 57e4738461a..732221b776e 100644 --- a/yarn-project/circuit-types/src/l2_block.ts +++ b/yarn-project/circuit-types/src/l2_block.ts @@ -63,16 +63,14 @@ export class L2Block { /** * Serializes a block - * @remarks This can be used specifying no logs, which is used when the block is being served via JSON-RPC because the logs are expected to be served - * separately. - * @returns A serialized L2 block logs. + * @returns A serialized L2 block as a Buffer. */ toBuffer() { return serializeToBuffer(this.header, this.archive, this.body); } /** - * Deserializes L2 block without logs from a buffer. + * Deserializes L2 block from a buffer. * @param str - A serialized L2 block. * @returns Deserialized L2 block. */ @@ -81,10 +79,8 @@ export class L2Block { } /** - * Serializes a block without logs to a string. - * @remarks This is used when the block is being served via JSON-RPC because the logs are expected to be served - * separately. - * @returns A serialized L2 block without logs. + * Serializes a block to a string. + * @returns A serialized L2 block as a string. */ toString(): string { return this.toBuffer().toString(STRING_ENCODING); @@ -107,18 +103,24 @@ export class L2Block { numPublicCallsPerTx = 3, numEncryptedLogsPerCall = 2, numUnencryptedLogsPerCall = 1, + numL1ToL2MessagesPerCall = 2, ): L2Block { + const body = Body.random( + txsPerBlock, + numPrivateCallsPerTx, + numPublicCallsPerTx, + numEncryptedLogsPerCall, + numUnencryptedLogsPerCall, + numL1ToL2MessagesPerCall, + ); + + const txsHash = body.getCalldataHash(); + return L2Block.fromFields( { archive: makeAppendOnlyTreeSnapshot(1), - header: makeHeader(0, l2BlockNum), - body: Body.random( - txsPerBlock, - numPrivateCallsPerTx, - numPublicCallsPerTx, - numEncryptedLogsPerCall, - numUnencryptedLogsPerCall, - ), + header: makeHeader(0, l2BlockNum, txsHash), + body, }, // just for testing purposes, each random L2 block got emitted in the equivalent L1 block BigInt(l2BlockNum), diff --git a/yarn-project/circuits.js/src/tests/factories.ts b/yarn-project/circuits.js/src/tests/factories.ts index ddde7859053..fddeacf2aaf 100644 --- a/yarn-project/circuits.js/src/tests/factories.ts +++ b/yarn-project/circuits.js/src/tests/factories.ts @@ -1032,10 +1032,10 @@ export function makeRootRollupPublicInputs( /** * Makes content commitment */ -export function makeContentCommitment(seed = 0): ContentCommitment { +export function makeContentCommitment(seed = 0, txsHash: Buffer | undefined = undefined): ContentCommitment { return new ContentCommitment( new Fr(seed), - toBufferBE(BigInt(seed + 0x100), NUM_BYTES_PER_SHA256), + txsHash ?? toBufferBE(BigInt(seed + 0x100), NUM_BYTES_PER_SHA256), toBufferBE(BigInt(seed + 0x200), NUM_BYTES_PER_SHA256), toBufferBE(BigInt(seed + 0x300), NUM_BYTES_PER_SHA256), ); @@ -1044,10 +1044,14 @@ export function makeContentCommitment(seed = 0): ContentCommitment { /** * Makes header. */ -export function makeHeader(seed = 0, blockNumber: number | undefined = undefined): Header { +export function makeHeader( + seed = 0, + blockNumber: number | undefined = undefined, + txsHash: Buffer | undefined = undefined, +): Header { return new Header( makeAppendOnlyTreeSnapshot(seed + 0x100), - makeContentCommitment(seed + 0x200), + makeContentCommitment(seed + 0x200, txsHash), makeStateReference(seed + 0x600), makeGlobalVariables((seed += 0x700), blockNumber), ); diff --git a/yarn-project/sequencer-client/src/block_builder/solo_block_builder.test.ts b/yarn-project/sequencer-client/src/block_builder/solo_block_builder.test.ts index 89e95faa8e4..0830546e859 100644 --- a/yarn-project/sequencer-client/src/block_builder/solo_block_builder.test.ts +++ b/yarn-project/sequencer-client/src/block_builder/solo_block_builder.test.ts @@ -57,7 +57,7 @@ import { } from '@aztec/circuits.js/testing'; import { makeTuple, range } from '@aztec/foundation/array'; import { toBufferBE } from '@aztec/foundation/bigint-buffer'; -import { times } from '@aztec/foundation/collection'; +import { padArrayEnd, times } from '@aztec/foundation/collection'; import { Tuple, to2Fields } from '@aztec/foundation/serialize'; import { openTmpStore } from '@aztec/kv-store/utils'; import { MerkleTreeOperations, MerkleTrees } from '@aztec/world-state'; @@ -256,7 +256,7 @@ describe('sequencer/solo_block_builder', () => { ), ); - const body = new Body(mockL1ToL2Messages, txEffects); + 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(), diff --git a/yarn-project/sequencer-client/src/block_builder/solo_block_builder.ts b/yarn-project/sequencer-client/src/block_builder/solo_block_builder.ts index 1d38330567d..211c5856c40 100644 --- a/yarn-project/sequencer-client/src/block_builder/solo_block_builder.ts +++ b/yarn-project/sequencer-client/src/block_builder/solo_block_builder.ts @@ -129,7 +129,7 @@ export class SoloBlockBuilder implements BlockBuilder { ), ); - const blockBody = new Body(newL1ToL2Messages, txEffects); + const blockBody = new Body(padArrayEnd(newL1ToL2Messages, Fr.ZERO, NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP), txEffects); const l2Block = L2Block.fromFields({ archive: circuitsOutput.archive,