diff --git a/yarn-project/archiver/src/archiver-store.test.ts b/yarn-project/archiver/src/archiver-store.test.ts index 084c4dd89573..ef0b7f5d0789 100644 --- a/yarn-project/archiver/src/archiver-store.test.ts +++ b/yarn-project/archiver/src/archiver-store.test.ts @@ -24,7 +24,7 @@ import { EventEmitter } from 'events'; import { type MockProxy, mock } from 'jest-mock-extended'; import { Archiver, type ArchiverEmitter } from './archiver.js'; -import { InitialBlockNumberNotSequentialError } from './errors.js'; +import { BlockNumberNotSequentialError } from './errors.js'; import type { ArchiverInstrumentation } from './modules/instrumentation.js'; import { ArchiverL1Synchronizer } from './modules/l1_synchronizer.js'; import { KVArchiverDataStore } from './store/kv_archiver_store.js'; @@ -265,7 +265,7 @@ describe('Archiver Store', () => { await archiver.addBlock(block1); // Block 3 should be rejected because block 2 is missing - await expect(archiver.addBlock(block3)).rejects.toThrow(InitialBlockNumberNotSequentialError); + await expect(archiver.addBlock(block3)).rejects.toThrow(BlockNumberNotSequentialError); }); it('rejects blocks with duplicate block numbers', async () => { @@ -276,7 +276,7 @@ describe('Archiver Store', () => { await archiver.addBlock(block2); // Adding block 2 again shoud be rejected - await expect(archiver.addBlock(block2)).rejects.toThrow(InitialBlockNumberNotSequentialError); + await expect(archiver.addBlock(block2)).rejects.toThrow(BlockNumberNotSequentialError); }); it('rejects first block if not starting from block 1', async () => { diff --git a/yarn-project/archiver/src/archiver-sync.test.ts b/yarn-project/archiver/src/archiver-sync.test.ts index c95735262b64..e0050f28e8a0 100644 --- a/yarn-project/archiver/src/archiver-sync.test.ts +++ b/yarn-project/archiver/src/archiver-sync.test.ts @@ -1327,7 +1327,7 @@ describe('Archiver Sync', () => { expect(await archiver.getSynchedCheckpointNumber()).toEqual(CheckpointNumber(1)); const blockAlreadySyncedFromCheckpoint = cp1.blocks[cp1.blocks.length - 1]; - // Now try and add one of the blocks via the addProposedBlocks method. It should throw + // Now try and add one of the blocks via the addProposedBlock method. It should throw await expect(archiver.addBlock(blockAlreadySyncedFromCheckpoint)).rejects.toThrow(); }, 10_000); @@ -1428,8 +1428,12 @@ describe('Archiver Sync', () => { const { checkpoint: cp3 } = await fake.addCheckpoint(CheckpointNumber(3), { l1BlockNumber: 5010n }); // Add blocks from BOTH checkpoints locally (matching the L1 checkpoints) - await archiverStore.addProposedBlocks(cp2.blocks, { force: true }); - await archiverStore.addProposedBlocks(cp3.blocks, { force: true }); + for (const block of cp2.blocks) { + await archiverStore.addProposedBlock(block, { force: true }); + } + for (const block of cp3.blocks) { + await archiverStore.addProposedBlock(block, { force: true }); + } // Verify all blocks are visible locally const lastBlockInCheckpoint3 = cp3.blocks[cp3.blocks.length - 1].number; diff --git a/yarn-project/archiver/src/archiver.ts b/yarn-project/archiver/src/archiver.ts index f27e8cbab3c7..3955cfc83c22 100644 --- a/yarn-project/archiver/src/archiver.ts +++ b/yarn-project/archiver/src/archiver.ts @@ -30,7 +30,7 @@ import { import { type TelemetryClient, type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client'; import { type ArchiverConfig, mapArchiverConfig } from './config.js'; -import { NoBlobBodiesFoundError } from './errors.js'; +import { BlockAlreadyCheckpointedError, NoBlobBodiesFoundError } from './errors.js'; import { validateAndLogTraceAvailability } from './l1/validate_trace.js'; import { ArchiverDataSourceBase } from './modules/data_source_base.js'; import { ArchiverDataStoreUpdater } from './modules/data_store_updater.js'; @@ -242,10 +242,15 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra } try { - await this.updater.addProposedBlocks([block]); + await this.updater.addProposedBlock(block); this.log.debug(`Added block ${block.number} to store`); resolve(); } catch (err: any) { + if (err instanceof BlockAlreadyCheckpointedError) { + this.log.debug(`Proposed block ${block.number} matches already checkpointed block, ignoring late proposal`); + resolve(); + continue; + } this.log.error(`Failed to add block ${block.number} to store: ${err.message}`); reject(err); } diff --git a/yarn-project/archiver/src/errors.ts b/yarn-project/archiver/src/errors.ts index 9ef345cf89e8..f47122bc4514 100644 --- a/yarn-project/archiver/src/errors.ts +++ b/yarn-project/archiver/src/errors.ts @@ -6,24 +6,9 @@ export class NoBlobBodiesFoundError extends Error { } } -export class InitialBlockNumberNotSequentialError extends Error { - constructor( - public readonly newBlockNumber: number, - public readonly previousBlockNumber: number | undefined, - ) { - super( - `Cannot insert new block ${newBlockNumber} given previous block number in store is ${ - previousBlockNumber ?? 'undefined' - }`, - ); - } -} - export class BlockNumberNotSequentialError extends Error { constructor(newBlockNumber: number, previous: number | undefined) { - super( - `Cannot insert new block ${newBlockNumber} given previous block number in batch is ${previous ?? 'undefined'}`, - ); + super(`Cannot insert new block ${newBlockNumber} given previous block number is ${previous ?? 'undefined'}`); } } @@ -48,14 +33,6 @@ export class CheckpointNumberNotSequentialError extends Error { } } -export class CheckpointNumberNotConsistentError extends Error { - constructor(newCheckpointNumber: number, previous: number | undefined) { - super( - `Cannot insert block for new checkpoint ${newCheckpointNumber} given previous block was checkpoint ${previous ?? 'undefined'}`, - ); - } -} - export class BlockIndexNotSequentialError extends Error { constructor(newBlockIndex: number, previousBlockIndex: number | undefined) { super( @@ -89,6 +66,15 @@ export class BlockNotFoundError extends Error { } } +/** Thrown when a proposed block matches a block that was already checkpointed. This is expected for late proposals. */ +export class BlockAlreadyCheckpointedError extends Error { + constructor(public readonly blockNumber: number) { + super(`Block ${blockNumber} has already been checkpointed with the same content`); + this.name = 'BlockAlreadyCheckpointedError'; + } +} + +/** Thrown when a proposed block conflicts with an already checkpointed block (different content). */ export class CannotOverwriteCheckpointedBlockError extends Error { constructor( public readonly blockNumber: number, diff --git a/yarn-project/archiver/src/modules/data_store_updater.test.ts b/yarn-project/archiver/src/modules/data_store_updater.test.ts index 94721e4c22ea..d93e2361efa4 100644 --- a/yarn-project/archiver/src/modules/data_store_updater.test.ts +++ b/yarn-project/archiver/src/modules/data_store_updater.test.ts @@ -57,7 +57,7 @@ describe('ArchiverDataStoreUpdater', () => { }); describe('contract data', () => { - it('stores contract class and instance data when blocks are added via addProposedBlocks', async () => { + it('stores contract class and instance data when blocks are added via addProposedBlock', async () => { // Create block with contract class and instance logs const block = await L2Block.random(BlockNumber(1), { checkpointNumber: CheckpointNumber(1), @@ -66,7 +66,7 @@ describe('ArchiverDataStoreUpdater', () => { block.body.txEffects[0].contractClassLogs = [contractClassLog]; block.body.txEffects[0].privateLogs = [PrivateLog.fromBuffer(getSampleContractInstancePublishedEventPayload())]; - await updater.addProposedBlocks([block]); + await updater.addProposedBlock(block); // Verify contract class was stored const retrievedClass = await store.getContractClass(contractClassId); @@ -92,7 +92,7 @@ describe('ArchiverDataStoreUpdater', () => { PrivateLog.fromBuffer(getSampleContractInstancePublishedEventPayload()), ]; - await updater.addProposedBlocks([localBlock]); + await updater.addProposedBlock(localBlock); // Verify contract data was stored const timestamp = localBlock.header.globalVariables.timestamp + 1n; @@ -155,7 +155,7 @@ describe('ArchiverDataStoreUpdater', () => { slotNumber: SlotNumber(100), }); - await updater.addProposedBlocks([block]); + await updater.addProposedBlock(block); // Create checkpoint with the SAME block (same archive root) const publishedCheckpoint = makePublishedCheckpoint(makeCheckpoint([block]), 10); @@ -175,7 +175,7 @@ describe('ArchiverDataStoreUpdater', () => { indexWithinCheckpoint: IndexWithinCheckpoint(0), slotNumber: SlotNumber(100), }); - await updater.addProposedBlocks([localBlock]); + await updater.addProposedBlock(localBlock); const publicLogsBefore = await store.getPublicLogs({}); expect(publicLogsBefore.logs.map(l => l.log)).toEqual(localBlock.body.txEffects.flatMap(tx => tx.publicLogs)); @@ -203,7 +203,7 @@ describe('ArchiverDataStoreUpdater', () => { indexWithinCheckpoint: IndexWithinCheckpoint(0), slotNumber: SlotNumber(100), }); - await updater.addProposedBlocks([localBlock]); + await updater.addProposedBlock(localBlock); const publicLogsBefore = await store.getPublicLogs({}); expect(publicLogsBefore.logs.map(l => l.log)).toEqual(localBlock.body.txEffects.flatMap(tx => tx.publicLogs)); diff --git a/yarn-project/archiver/src/modules/data_store_updater.ts b/yarn-project/archiver/src/modules/data_store_updater.ts index 83864240f01d..d5e1081dbdeb 100644 --- a/yarn-project/archiver/src/modules/data_store_updater.ts +++ b/yarn-project/archiver/src/modules/data_store_updater.ts @@ -52,29 +52,29 @@ export class ArchiverDataStoreUpdater { ) {} /** - * Adds proposed blocks to the store with contract class/instance extraction from logs. - * These are uncheckpointed blocks that have been proposed by the sequencer but not yet included in a checkpoint on L1. + * Adds a proposed block to the store with contract class/instance extraction from logs. + * This is an uncheckpointed block that has been proposed by the sequencer but not yet included in a checkpoint on L1. * Extracts ContractClassPublished, ContractInstancePublished, ContractInstanceUpdated events, * and individually broadcasted functions from the block logs. * - * @param blocks - The proposed L2 blocks to add. + * @param block - The proposed L2 block to add. * @param pendingChainValidationStatus - Optional validation status to set. * @returns True if the operation is successful. */ - public async addProposedBlocks( - blocks: L2Block[], + public async addProposedBlock( + block: L2Block, pendingChainValidationStatus?: ValidateCheckpointResult, ): Promise { const result = await this.store.transactionAsync(async () => { - await this.store.addProposedBlocks(blocks); + await this.store.addProposedBlock(block); const opResults = await Promise.all([ // Update the pending chain validation status if provided pendingChainValidationStatus && this.store.setPendingChainValidationStatus(pendingChainValidationStatus), - // Add any logs emitted during the retrieved blocks - this.store.addLogs(blocks), - // Unroll all logs emitted during the retrieved blocks and extract any contract classes and instances from them - ...blocks.map(block => this.addContractDataToDb(block)), + // Add any logs emitted during the retrieved block + this.store.addLogs([block]), + // Unroll all logs emitted during the retrieved block and extract any contract classes and instances from it + this.addContractDataToDb(block), ]); await this.l2TipsCache?.refresh(); @@ -108,7 +108,7 @@ export class ArchiverDataStoreUpdater { await this.store.addCheckpoints(checkpoints); - // Filter out blocks that were already inserted via addProposedBlocks() to avoid duplicating logs/contract data + // Filter out blocks that were already inserted via addProposedBlock() to avoid duplicating logs/contract data const newBlocks = checkpoints .flatMap((ch: PublishedCheckpoint) => ch.checkpoint.blocks) .filter(b => lastAlreadyInsertedBlockNumber === undefined || b.number > lastAlreadyInsertedBlockNumber); diff --git a/yarn-project/archiver/src/store/block_store.ts b/yarn-project/archiver/src/store/block_store.ts index a9ec9a501c85..d6e28a72ff01 100644 --- a/yarn-project/archiver/src/store/block_store.ts +++ b/yarn-project/archiver/src/store/block_store.ts @@ -35,15 +35,14 @@ import { } from '@aztec/stdlib/tx'; import { + BlockAlreadyCheckpointedError, BlockArchiveNotConsistentError, BlockIndexNotSequentialError, BlockNotFoundError, BlockNumberNotSequentialError, CannotOverwriteCheckpointedBlockError, CheckpointNotFoundError, - CheckpointNumberNotConsistentError, CheckpointNumberNotSequentialError, - InitialBlockNumberNotSequentialError, InitialCheckpointNumberNotSequentialError, } from '../errors.js'; @@ -141,23 +140,18 @@ export class BlockStore { } /** - * Append new proposed blocks to the store's list. All blocks must be for the 'current' checkpoint. - * These are uncheckpointed blocks that have been proposed by the sequencer but not yet included in a checkpoint on L1. + * Append a new proposed block to the store. + * This is an uncheckpointed block that has been proposed by the sequencer but not yet included in a checkpoint on L1. * For checkpointed blocks (already published to L1), use addCheckpoints() instead. - * @param blocks - The proposed L2 blocks to be added to the store. + * @param block - The proposed L2 block to be added to the store. * @returns True if the operation is successful. */ - async addProposedBlocks(blocks: L2Block[], opts: { force?: boolean } = {}): Promise { - if (blocks.length === 0) { - return true; - } - + async addProposedBlock(block: L2Block, opts: { force?: boolean } = {}): Promise { return await this.db.transactionAsync(async () => { - // Check that the block immediately before the first block to be added is present in the store. - const firstBlockNumber = blocks[0].number; - const firstBlockCheckpointNumber = blocks[0].checkpointNumber; - const firstBlockIndex = blocks[0].indexWithinCheckpoint; - const firstBlockLastArchive = blocks[0].header.lastArchive.root; + const blockNumber = block.number; + const blockCheckpointNumber = block.checkpointNumber; + const blockIndex = block.indexWithinCheckpoint; + const blockLastArchive = block.header.lastArchive.root; // Extract the latest block and checkpoint numbers const previousBlockNumber = await this.getLatestBlockNumber(); @@ -165,71 +159,52 @@ export class BlockStore { // Verify we're not overwriting checkpointed blocks const lastCheckpointedBlockNumber = await this.getCheckpointedL2BlockNumber(); - if (!opts.force && firstBlockNumber <= lastCheckpointedBlockNumber) { - throw new CannotOverwriteCheckpointedBlockError(firstBlockNumber, lastCheckpointedBlockNumber); + if (!opts.force && blockNumber <= lastCheckpointedBlockNumber) { + // Check if the proposed block matches the already-checkpointed one + const existingBlock = await this.getBlock(BlockNumber(blockNumber)); + if (existingBlock && existingBlock.archive.root.equals(block.archive.root)) { + throw new BlockAlreadyCheckpointedError(blockNumber); + } + throw new CannotOverwriteCheckpointedBlockError(blockNumber, lastCheckpointedBlockNumber); } - // Check that the first block number is the expected one - if (!opts.force && previousBlockNumber !== firstBlockNumber - 1) { - throw new InitialBlockNumberNotSequentialError(firstBlockNumber, previousBlockNumber); + // Check that the block number is the expected one + if (!opts.force && previousBlockNumber !== blockNumber - 1) { + throw new BlockNumberNotSequentialError(blockNumber, previousBlockNumber); } // The same check as above but for checkpoints - if (!opts.force && previousCheckpointNumber !== firstBlockCheckpointNumber - 1) { - throw new InitialCheckpointNumberNotSequentialError(firstBlockCheckpointNumber, previousCheckpointNumber); + if (!opts.force && previousCheckpointNumber !== blockCheckpointNumber - 1) { + throw new CheckpointNumberNotSequentialError(blockCheckpointNumber, previousCheckpointNumber); } // Extract the previous block if there is one and see if it is for the same checkpoint or not const previousBlockResult = await this.getBlock(previousBlockNumber); - let expectedFirstblockIndex = 0; + let expectedBlockIndex = 0; let previousBlockIndex: number | undefined = undefined; if (previousBlockResult !== undefined) { - if (previousBlockResult.checkpointNumber === firstBlockCheckpointNumber) { + if (previousBlockResult.checkpointNumber === blockCheckpointNumber) { // The previous block is for the same checkpoint, therefore our index should follow it previousBlockIndex = previousBlockResult.indexWithinCheckpoint; - expectedFirstblockIndex = previousBlockIndex + 1; + expectedBlockIndex = previousBlockIndex + 1; } - if (!previousBlockResult.archive.root.equals(firstBlockLastArchive)) { + if (!previousBlockResult.archive.root.equals(blockLastArchive)) { throw new BlockArchiveNotConsistentError( - firstBlockNumber, + blockNumber, previousBlockResult.number, - firstBlockLastArchive, + blockLastArchive, previousBlockResult.archive.root, ); } } - // Now check that the first block has the expected index value - if (!opts.force && expectedFirstblockIndex !== firstBlockIndex) { - throw new BlockIndexNotSequentialError(firstBlockIndex, previousBlockIndex); + // Now check that the block has the expected index value + if (!opts.force && expectedBlockIndex !== blockIndex) { + throw new BlockIndexNotSequentialError(blockIndex, previousBlockIndex); } - // Iterate over blocks array and insert them, checking that the block numbers and indexes are sequential. Also check they are for the correct checkpoint. - let previousBlock: L2Block | undefined = undefined; - for (const block of blocks) { - if (!opts.force && previousBlock) { - if (previousBlock.number + 1 !== block.number) { - throw new BlockNumberNotSequentialError(block.number, previousBlock.number); - } - if (previousBlock.indexWithinCheckpoint + 1 !== block.indexWithinCheckpoint) { - throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint); - } - if (!previousBlock.archive.root.equals(block.header.lastArchive.root)) { - throw new BlockArchiveNotConsistentError( - block.number, - previousBlock.number, - block.header.lastArchive.root, - previousBlock.archive.root, - ); - } - } - if (!opts.force && firstBlockCheckpointNumber !== block.checkpointNumber) { - throw new CheckpointNumberNotConsistentError(block.checkpointNumber, firstBlockCheckpointNumber); - } - previousBlock = block; - await this.addBlockToDatabase(block, block.checkpointNumber, block.indexWithinCheckpoint); - } + await this.addBlockToDatabase(block, block.checkpointNumber, block.indexWithinCheckpoint); return true; }); diff --git a/yarn-project/archiver/src/store/kv_archiver_store.test.ts b/yarn-project/archiver/src/store/kv_archiver_store.test.ts index d05044ded8d2..a77015844573 100644 --- a/yarn-project/archiver/src/store/kv_archiver_store.test.ts +++ b/yarn-project/archiver/src/store/kv_archiver_store.test.ts @@ -42,13 +42,12 @@ import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees'; import { type IndexedTxEffect, TxHash } from '@aztec/stdlib/tx'; import { + BlockAlreadyCheckpointedError, BlockArchiveNotConsistentError, BlockIndexNotSequentialError, BlockNumberNotSequentialError, CannotOverwriteCheckpointedBlockError, - CheckpointNumberNotConsistentError, CheckpointNumberNotSequentialError, - InitialBlockNumberNotSequentialError, InitialCheckpointNumberNotSequentialError, } from '../errors.js'; import { MessageStoreError } from '../store/message_store.js'; @@ -67,6 +66,18 @@ import { } from '../test/mock_structs.js'; import { type ArchiverL1SynchPoint, KVArchiverDataStore } from './kv_archiver_store.js'; +async function addProposedBlocks( + store: KVArchiverDataStore, + blocks: L2Block[], + opts?: { force?: boolean }, +): Promise { + let result = true; + for (const block of blocks) { + result = (await store.addProposedBlock(block, opts)) && result; + } + return result; +} + describe('KVArchiverDataStore', () => { let store: KVArchiverDataStore; let publishedCheckpoints: PublishedCheckpoint[]; @@ -388,7 +399,7 @@ describe('KVArchiverDataStore', () => { indexWithinCheckpoint: IndexWithinCheckpoint(0), lastArchive: lastBlockArchive, }); - await store.addProposedBlocks([block2]); + await store.addProposedBlock(block2); // Verify state: checkpoint 1 exists, block 2 exists but is orphaned (no checkpoint 2) expect(await store.getSynchedCheckpointNumber()).toBe(1); @@ -431,7 +442,7 @@ describe('KVArchiverDataStore', () => { indexWithinCheckpoint: IndexWithinCheckpoint(2), lastArchive: block3.archive, }); - await store.addProposedBlocks([block2, block3, block4]); + await addProposedBlocks(store, [block2, block3, block4]); expect(await store.getSynchedCheckpointNumber()).toBe(1); expect(await store.getLatestBlockNumber()).toBe(4); @@ -727,7 +738,7 @@ describe('KVArchiverDataStore', () => { lastArchive: block5.archive, }); - await store.addProposedBlocks([block4, block5, block6]); + await addProposedBlocks(store, [block4, block5, block6]); // Checkpoint number should still be 1 (no new checkpoint added) expect(await store.getSynchedCheckpointNumber()).toBe(1); @@ -755,7 +766,7 @@ describe('KVArchiverDataStore', () => { indexWithinCheckpoint: IndexWithinCheckpoint(1), lastArchive: block3.archive, }); - await store.addProposedBlocks([block3, block4]); + await addProposedBlocks(store, [block3, block4]); // getBlock should work for both checkpointed and uncheckpointed blocks expect((await store.getBlock(BlockNumber(1)))?.number).toBe(1); @@ -769,7 +780,7 @@ describe('KVArchiverDataStore', () => { indexWithinCheckpoint: IndexWithinCheckpoint(2), lastArchive: block4.archive, }); - await store.addProposedBlocks([block5]); + await store.addProposedBlock(block5); // Verify the uncheckpointed blocks have correct data const retrieved3 = await store.getBlock(BlockNumber(3)); @@ -794,7 +805,7 @@ describe('KVArchiverDataStore', () => { indexWithinCheckpoint: IndexWithinCheckpoint(1), lastArchive: block1.archive, }); - await store.addProposedBlocks([block1, block2]); + await addProposedBlocks(store, [block1, block2]); // getBlockByHash should work for uncheckpointed blocks const hash1 = await block1.header.hash(); @@ -818,7 +829,7 @@ describe('KVArchiverDataStore', () => { indexWithinCheckpoint: IndexWithinCheckpoint(1), lastArchive: block1.archive, }); - await store.addProposedBlocks([block1, block2]); + await addProposedBlocks(store, [block1, block2]); // getBlockByArchive should work for uncheckpointed blocks const archive1 = block1.archive.root; @@ -851,7 +862,7 @@ describe('KVArchiverDataStore', () => { indexWithinCheckpoint: IndexWithinCheckpoint(1), lastArchive: block3.archive, }); - await store.addProposedBlocks([block3, block4]); + await addProposedBlocks(store, [block3, block4]); // getCheckpointedBlock should work for checkpointed blocks expect((await store.getCheckpointedBlock(BlockNumber(1)))?.block.number).toBe(1); @@ -872,7 +883,7 @@ describe('KVArchiverDataStore', () => { checkpointNumber: CheckpointNumber(1), indexWithinCheckpoint: IndexWithinCheckpoint(0), }); - await store.addProposedBlocks([block1]); + await store.addProposedBlock(block1); const hash = await block1.header.hash(); @@ -889,7 +900,7 @@ describe('KVArchiverDataStore', () => { checkpointNumber: CheckpointNumber(1), indexWithinCheckpoint: IndexWithinCheckpoint(0), }); - await store.addProposedBlocks([block1]); + await store.addProposedBlock(block1); const archive = block1.archive.root; @@ -916,7 +927,7 @@ describe('KVArchiverDataStore', () => { indexWithinCheckpoint: IndexWithinCheckpoint(2), lastArchive: block2.archive, }); - await store.addProposedBlocks([block1, block2, block3]); + await addProposedBlocks(store, [block1, block2, block3]); expect(await store.getSynchedCheckpointNumber()).toBe(0); expect(await store.getLatestBlockNumber()).toBe(3); @@ -976,7 +987,7 @@ describe('KVArchiverDataStore', () => { indexWithinCheckpoint: IndexWithinCheckpoint(2), lastArchive: block4.archive, }); - await store.addProposedBlocks([block3, block4, block5]); + await addProposedBlocks(store, [block3, block4, block5]); expect(await store.getSynchedCheckpointNumber()).toBe(1); expect(await store.getLatestBlockNumber()).toBe(5); @@ -1035,7 +1046,7 @@ describe('KVArchiverDataStore', () => { indexWithinCheckpoint: IndexWithinCheckpoint(1), lastArchive: block3.archive, }); - await store.addProposedBlocks([block3, block4]); + await addProposedBlocks(store, [block3, block4]); // getBlocks should retrieve all blocks const allBlocks = await store.getBlocks(BlockNumber(1), 10); @@ -1044,32 +1055,7 @@ describe('KVArchiverDataStore', () => { }); }); - describe('addProposedBlocks validation', () => { - it('throws if blocks have different checkpoint numbers', async () => { - // First, establish checkpoint 1 with blocks 1-2 - const checkpoint1 = makePublishedCheckpoint( - await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 }), - 10, - ); - await store.addCheckpoints([checkpoint1]); - - // Try to add blocks 3 and 4 with different checkpoint numbers - // Chain archives correctly to test the checkpoint number validation - const lastBlockArchive = checkpoint1.checkpoint.blocks.at(-1)!.archive; - const block3 = await L2Block.random(BlockNumber(3), { - checkpointNumber: CheckpointNumber(2), - indexWithinCheckpoint: IndexWithinCheckpoint(0), - lastArchive: lastBlockArchive, - }); - const block4 = await L2Block.random(BlockNumber(4), { - checkpointNumber: CheckpointNumber(3), - indexWithinCheckpoint: IndexWithinCheckpoint(1), - lastArchive: block3.archive, - }); - - await expect(store.addProposedBlocks([block3, block4])).rejects.toThrow(CheckpointNumberNotConsistentError); - }); - + describe('addProposedBlock validation', () => { it('throws if checkpoint number is not the current checkpoint', async () => { // First, establish checkpoint 1 with blocks 1-2 const checkpoint1 = makePublishedCheckpoint( @@ -1078,19 +1064,13 @@ describe('KVArchiverDataStore', () => { ); await store.addCheckpoints([checkpoint1]); - // Try to add blocks for checkpoint 3 (skipping checkpoint 2) + // Try to add a block for checkpoint 3 (skipping checkpoint 2) const block3 = await L2Block.random(BlockNumber(3), { checkpointNumber: CheckpointNumber(3), indexWithinCheckpoint: IndexWithinCheckpoint(0), }); - const block4 = await L2Block.random(BlockNumber(4), { - checkpointNumber: CheckpointNumber(3), - indexWithinCheckpoint: IndexWithinCheckpoint(1), - }); - await expect(store.addProposedBlocks([block3, block4])).rejects.toThrow( - InitialCheckpointNumberNotSequentialError, - ); + await expect(store.addProposedBlock(block3)).rejects.toThrow(CheckpointNumberNotSequentialError); }); it('allows blocks with the same checkpoint number for the current checkpoint', async () => { @@ -1114,7 +1094,7 @@ describe('KVArchiverDataStore', () => { lastArchive: block3.archive, }); - await expect(store.addProposedBlocks([block3, block4])).resolves.toBe(true); + await expect(addProposedBlocks(store, [block3, block4])).resolves.toBe(true); // Verify blocks were added expect((await store.getBlock(BlockNumber(3)))?.equals(block3)).toBe(true); @@ -1133,7 +1113,7 @@ describe('KVArchiverDataStore', () => { lastArchive: block1.archive, }); - await expect(store.addProposedBlocks([block1, block2])).resolves.toBe(true); + await expect(addProposedBlocks(store, [block1, block2])).resolves.toBe(true); // Verify blocks were added expect((await store.getBlock(BlockNumber(1)))?.equals(block1)).toBe(true); @@ -1152,24 +1132,18 @@ describe('KVArchiverDataStore', () => { indexWithinCheckpoint: IndexWithinCheckpoint(0), }); - await expect(store.addProposedBlocks([block1])).resolves.toBe(true); - await expect(store.addProposedBlocks([block2])).rejects.toThrow(InitialBlockNumberNotSequentialError); + await expect(store.addProposedBlock(block1)).resolves.toBe(true); + await expect(store.addProposedBlock(block2)).rejects.toThrow(BlockNumberNotSequentialError); }); it('throws if first block has wrong checkpoint number when store is empty', async () => { - // Try to add blocks for checkpoint 2 when store is empty (should start at 1) + // Try to add a block for checkpoint 2 when store is empty (should start at 1) const block1 = await L2Block.random(BlockNumber(1), { checkpointNumber: CheckpointNumber(2), indexWithinCheckpoint: IndexWithinCheckpoint(0), }); - const block2 = await L2Block.random(BlockNumber(2), { - checkpointNumber: CheckpointNumber(2), - indexWithinCheckpoint: IndexWithinCheckpoint(1), - }); - await expect(store.addProposedBlocks([block1, block2])).rejects.toThrow( - InitialCheckpointNumberNotSequentialError, - ); + await expect(store.addProposedBlock(block1)).rejects.toThrow(CheckpointNumberNotSequentialError); }); it('allows adding more blocks to the same checkpoint in separate calls', async () => { @@ -1187,7 +1161,7 @@ describe('KVArchiverDataStore', () => { indexWithinCheckpoint: IndexWithinCheckpoint(0), lastArchive: lastBlockArchive, }); - await expect(store.addProposedBlocks([block3])).resolves.toBe(true); + await expect(store.addProposedBlock(block3)).resolves.toBe(true); // Add block 4 for the same checkpoint 2 in a separate call const block4 = await L2Block.random(BlockNumber(4), { @@ -1195,7 +1169,7 @@ describe('KVArchiverDataStore', () => { indexWithinCheckpoint: IndexWithinCheckpoint(1), lastArchive: block3.archive, }); - await expect(store.addProposedBlocks([block4])).resolves.toBe(true); + await expect(store.addProposedBlock(block4)).resolves.toBe(true); expect(await store.getLatestBlockNumber()).toBe(4); }); @@ -1215,7 +1189,7 @@ describe('KVArchiverDataStore', () => { indexWithinCheckpoint: IndexWithinCheckpoint(0), lastArchive: lastBlockArchive, }); - await expect(store.addProposedBlocks([block3])).resolves.toBe(true); + await expect(store.addProposedBlock(block3)).resolves.toBe(true); // Add block 4 for the same checkpoint 2 in a separate call but with a missing index const block4 = await L2Block.random(BlockNumber(4), { @@ -1223,7 +1197,7 @@ describe('KVArchiverDataStore', () => { indexWithinCheckpoint: IndexWithinCheckpoint(2), lastArchive: block3.archive, }); - await expect(store.addProposedBlocks([block4])).rejects.toThrow(BlockIndexNotSequentialError); + await expect(store.addProposedBlock(block4)).rejects.toThrow(BlockIndexNotSequentialError); expect(await store.getLatestBlockNumber()).toBe(3); }); @@ -1243,7 +1217,7 @@ describe('KVArchiverDataStore', () => { indexWithinCheckpoint: IndexWithinCheckpoint(0), lastArchive: lastBlockArchive, }); - await store.addProposedBlocks([block3]); + await store.addProposedBlock(block3); // Try to add block 4 for checkpoint 3 (should fail because current checkpoint is still 2) const block4 = await L2Block.random(BlockNumber(4), { @@ -1251,7 +1225,7 @@ describe('KVArchiverDataStore', () => { indexWithinCheckpoint: IndexWithinCheckpoint(0), lastArchive: block3.archive, }); - await expect(store.addProposedBlocks([block4])).rejects.toThrow(InitialCheckpointNumberNotSequentialError); + await expect(store.addProposedBlock(block4)).rejects.toThrow(CheckpointNumberNotSequentialError); }); it('force option bypasses checkpoint number validation', async () => { @@ -1275,7 +1249,7 @@ describe('KVArchiverDataStore', () => { lastArchive: block3.archive, }); - await expect(store.addProposedBlocks([block3, block4], { force: true })).resolves.toBe(true); + await expect(addProposedBlocks(store, [block3, block4], { force: true })).resolves.toBe(true); }); it('force option bypasses blockindex number validation', async () => { @@ -1299,7 +1273,7 @@ describe('KVArchiverDataStore', () => { lastArchive: block3.archive, }); - await expect(store.addProposedBlocks([block3, block4], { force: true })).resolves.toBe(true); + await expect(addProposedBlocks(store, [block3, block4], { force: true })).resolves.toBe(true); }); it('throws if adding blocks with non-consecutive archives', async () => { @@ -1315,7 +1289,7 @@ describe('KVArchiverDataStore', () => { checkpointNumber: CheckpointNumber(2), indexWithinCheckpoint: IndexWithinCheckpoint(0), }); - await expect(store.addProposedBlocks([block3])).rejects.toThrow(BlockArchiveNotConsistentError); + await expect(store.addProposedBlock(block3)).rejects.toThrow(BlockArchiveNotConsistentError); expect(await store.getLatestBlockNumber()).toBe(2); }); @@ -1335,7 +1309,7 @@ describe('KVArchiverDataStore', () => { indexWithinCheckpoint: IndexWithinCheckpoint(0), lastArchive: lastBlockArchive, }); - await expect(store.addProposedBlocks([block3])).resolves.toBe(true); + await expect(store.addProposedBlock(block3)).resolves.toBe(true); // Add block 4 with incorrect archive (should fail) const block4 = await L2Block.random(BlockNumber(4), { @@ -1343,7 +1317,7 @@ describe('KVArchiverDataStore', () => { indexWithinCheckpoint: IndexWithinCheckpoint(1), lastArchive: AppendOnlyTreeSnapshot.random(), }); - await expect(store.addProposedBlocks([block4])).rejects.toThrow(BlockArchiveNotConsistentError); + await expect(store.addProposedBlock(block4)).rejects.toThrow(BlockArchiveNotConsistentError); expect(await store.getLatestBlockNumber()).toBe(3); }); @@ -1363,14 +1337,26 @@ describe('KVArchiverDataStore', () => { checkpointNumber: CheckpointNumber(1), indexWithinCheckpoint: IndexWithinCheckpoint(1), }); - await expect(store.addProposedBlocks([block2])).rejects.toThrow(CannotOverwriteCheckpointedBlockError); + await expect(store.addProposedBlock(block2)).rejects.toThrow(CannotOverwriteCheckpointedBlockError); // Try to add a block that would overwrite checkpointed block 1 const block1 = await L2Block.random(BlockNumber(1), { checkpointNumber: CheckpointNumber(1), indexWithinCheckpoint: IndexWithinCheckpoint(0), }); - await expect(store.addProposedBlocks([block1])).rejects.toThrow(CannotOverwriteCheckpointedBlockError); + await expect(store.addProposedBlock(block1)).rejects.toThrow(CannotOverwriteCheckpointedBlockError); + }); + + it('throws BlockAlreadyCheckpointedError if proposed block matches the checkpointed one', async () => { + const checkpoint1 = makePublishedCheckpoint( + await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 }), + 10, + ); + await store.addCheckpoints([checkpoint1]); + + // Re-propose the same block that was already checkpointed + const checkpointedBlock = checkpoint1.checkpoint.blocks[1]; + await expect(store.addProposedBlock(checkpointedBlock)).rejects.toThrow(BlockAlreadyCheckpointedError); }); }); @@ -1801,7 +1787,7 @@ describe('KVArchiverDataStore', () => { it('deleteLogs', async () => { const block = publishedCheckpoints[0].checkpoint.blocks[0]; - await store.addProposedBlocks([block]); + await store.addProposedBlock(block); await expect(store.addLogs([block])).resolves.toEqual(true); expect((await store.getPublicLogs({ fromBlock: BlockNumber(1) })).logs.length).toEqual( @@ -3052,7 +3038,7 @@ describe('KVArchiverDataStore', () => { }); describe('idempotency', () => { - it('handles adding blocks via addProposedBlocks then same blocks via addCheckpoints', async () => { + it('handles adding blocks via addProposedBlock then same blocks via addCheckpoints', async () => { // First add checkpoint 1 to establish a base const checkpoint1 = makePublishedCheckpoint( await Checkpoint.random(CheckpointNumber(1), { numBlocks: 1, startBlockNumber: 1 }), @@ -3060,13 +3046,13 @@ describe('KVArchiverDataStore', () => { ); await store.addCheckpoints([checkpoint1]); - // Add provisional block 2 via addProposedBlocks + // Add provisional block 2 via addProposedBlock const provisionalBlock = await L2Block.random(BlockNumber(2), { checkpointNumber: CheckpointNumber(2), indexWithinCheckpoint: IndexWithinCheckpoint(0), lastArchive: checkpoint1.checkpoint.blocks[0].archive, }); - await store.addProposedBlocks([provisionalBlock]); + await store.addProposedBlock(provisionalBlock); // Now add checkpoint 2 containing the same block via addCheckpoints const checkpoint2 = new Checkpoint( @@ -3172,7 +3158,7 @@ describe('KVArchiverDataStore', () => { slotNumber: SlotNumber(101), // Different slot number }); - await store.addProposedBlocks([block1, block2, block3]); + await addProposedBlocks(store, [block1, block2, block3]); const blocksForSlot100 = await store.getBlocksForSlot(SlotNumber(100)); expect(blocksForSlot100.length).toBe(2); @@ -3192,7 +3178,7 @@ describe('KVArchiverDataStore', () => { slotNumber: SlotNumber(100), }); - await store.addProposedBlocks([block1]); + await store.addProposedBlock(block1); const blocksForSlot999 = await store.getBlocksForSlot(SlotNumber(999)); expect(blocksForSlot999).toEqual([]); @@ -3223,7 +3209,7 @@ describe('KVArchiverDataStore', () => { slotNumber: SlotNumber(50), }); - await store.addProposedBlocks([block1, block2, block3]); + await addProposedBlocks(store, [block1, block2, block3]); const blocksForSlot = await store.getBlocksForSlot(SlotNumber(50)); expect(blocksForSlot.length).toBe(3); @@ -3256,7 +3242,7 @@ describe('KVArchiverDataStore', () => { lastArchive: block3.archive, }); - await store.addProposedBlocks([block1, block2, block3, block4]); + await addProposedBlocks(store, [block1, block2, block3, block4]); expect(await store.getLatestBlockNumber()).toBe(4); // Remove blocks after block 2 @@ -3285,7 +3271,7 @@ describe('KVArchiverDataStore', () => { lastArchive: block2.archive, }); - await store.addProposedBlocks([block1, block2, block3]); + await addProposedBlocks(store, [block1, block2, block3]); // Remove blocks after block 1 const removedBlocks = await store.removeBlocksAfter(BlockNumber(1)); @@ -3306,7 +3292,7 @@ describe('KVArchiverDataStore', () => { lastArchive: block1.archive, }); - await store.addProposedBlocks([block1, block2]); + await addProposedBlocks(store, [block1, block2]); // Remove blocks after block 2 (none to remove) const removedBlocks = await store.removeBlocksAfter(BlockNumber(2)); @@ -3334,7 +3320,7 @@ describe('KVArchiverDataStore', () => { txsPerBlock: 2, }); - await store.addProposedBlocks([block1, block2]); + await addProposedBlocks(store, [block1, block2]); // Verify block2 is retrievable by hash and archive before removal const block2Hash = await block2.header.hash(); @@ -3386,7 +3372,7 @@ describe('KVArchiverDataStore', () => { lastArchive: block1.archive, }); - await store.addProposedBlocks([block1, block2]); + await addProposedBlocks(store, [block1, block2]); const removedBlocks = await store.removeBlocksAfter(BlockNumber(0)); diff --git a/yarn-project/archiver/src/store/kv_archiver_store.ts b/yarn-project/archiver/src/store/kv_archiver_store.ts index d46075e2a588..25efd120f66f 100644 --- a/yarn-project/archiver/src/store/kv_archiver_store.ts +++ b/yarn-project/archiver/src/store/kv_archiver_store.ts @@ -246,14 +246,14 @@ export class KVArchiverDataStore implements ContractDataSource { } /** - * Append new proposed blocks to the store's list. - * These are uncheckpointed blocks that have been proposed by the sequencer but not yet included in a checkpoint on L1. + * Append a new proposed block to the store. + * This is an uncheckpointed block that has been proposed by the sequencer but not yet included in a checkpoint on L1. * For checkpointed blocks (already published to L1), use addCheckpoints() instead. - * @param blocks - The proposed L2 blocks to be added to the store. + * @param block - The proposed L2 block to be added to the store. * @returns True if the operation is successful. */ - addProposedBlocks(blocks: L2Block[], opts: { force?: boolean; checkpointNumber?: number } = {}): Promise { - return this.#blockStore.addProposedBlocks(blocks, opts); + addProposedBlock(block: L2Block, opts: { force?: boolean } = {}): Promise { + return this.#blockStore.addProposedBlock(block, opts); } /**