From 6cc0c680f6d2300d71784588e7df8c77e1d25758 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Wed, 11 Mar 2026 10:58:14 -0300 Subject: [PATCH 1/2] fix(archiver): do not error if proposed block matches checkpointed The archiver block store checks that blocks added as proposed are not already checkpointed, and fails if so. But this can still happen if the processing of a block proposal is too slow, and the checkpointed data from L1 comes in first. Still, if the proposed block matches the checkpointed one, we should not err. --- yarn-project/archiver/src/archiver.ts | 7 ++++++- yarn-project/archiver/src/errors.ts | 9 +++++++++ yarn-project/archiver/src/store/block_store.ts | 6 ++++++ .../archiver/src/store/kv_archiver_store.test.ts | 13 +++++++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/yarn-project/archiver/src/archiver.ts b/yarn-project/archiver/src/archiver.ts index f27e8cbab3c7..694bfe8847ef 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'; @@ -246,6 +246,11 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra 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..51ffec09df24 100644 --- a/yarn-project/archiver/src/errors.ts +++ b/yarn-project/archiver/src/errors.ts @@ -89,6 +89,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/store/block_store.ts b/yarn-project/archiver/src/store/block_store.ts index a9ec9a501c85..0a2e45e8e2a5 100644 --- a/yarn-project/archiver/src/store/block_store.ts +++ b/yarn-project/archiver/src/store/block_store.ts @@ -35,6 +35,7 @@ import { } from '@aztec/stdlib/tx'; import { + BlockAlreadyCheckpointedError, BlockArchiveNotConsistentError, BlockIndexNotSequentialError, BlockNotFoundError, @@ -166,6 +167,11 @@ export class BlockStore { // Verify we're not overwriting checkpointed blocks const lastCheckpointedBlockNumber = await this.getCheckpointedL2BlockNumber(); if (!opts.force && firstBlockNumber <= lastCheckpointedBlockNumber) { + // Check if the proposed block matches the already-checkpointed one + const existingBlock = await this.getBlock(BlockNumber(firstBlockNumber)); + if (existingBlock && existingBlock.archive.root.equals(blocks[0].archive.root)) { + throw new BlockAlreadyCheckpointedError(firstBlockNumber); + } throw new CannotOverwriteCheckpointedBlockError(firstBlockNumber, lastCheckpointedBlockNumber); } 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..0ae41a555865 100644 --- a/yarn-project/archiver/src/store/kv_archiver_store.test.ts +++ b/yarn-project/archiver/src/store/kv_archiver_store.test.ts @@ -42,6 +42,7 @@ import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees'; import { type IndexedTxEffect, TxHash } from '@aztec/stdlib/tx'; import { + BlockAlreadyCheckpointedError, BlockArchiveNotConsistentError, BlockIndexNotSequentialError, BlockNumberNotSequentialError, @@ -1372,6 +1373,18 @@ describe('KVArchiverDataStore', () => { }); await expect(store.addProposedBlocks([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.addProposedBlocks([checkpointedBlock])).rejects.toThrow(BlockAlreadyCheckpointedError); + }); }); describe('getBlocksForCheckpoint', () => { From 7e49977497449ff1b157ae98a7c326419ad662d0 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Wed, 11 Mar 2026 11:14:00 -0300 Subject: [PATCH 2/2] refactor(archiver): addProposedBlock now takes one block at a time Simplifies the implementation since we never cared for pushing more than one block at a time, so this removes the plural version of the method. --- .../archiver/src/archiver-store.test.ts | 6 +- .../archiver/src/archiver-sync.test.ts | 10 +- yarn-project/archiver/src/archiver.ts | 2 +- yarn-project/archiver/src/errors.ts | 25 +-- .../src/modules/data_store_updater.test.ts | 12 +- .../src/modules/data_store_updater.ts | 22 +-- .../archiver/src/store/block_store.ts | 87 ++++------- .../src/store/kv_archiver_store.test.ts | 147 +++++++----------- .../archiver/src/store/kv_archiver_store.ts | 10 +- 9 files changed, 122 insertions(+), 199 deletions(-) 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 694bfe8847ef..3955cfc83c22 100644 --- a/yarn-project/archiver/src/archiver.ts +++ b/yarn-project/archiver/src/archiver.ts @@ -242,7 +242,7 @@ 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) { diff --git a/yarn-project/archiver/src/errors.ts b/yarn-project/archiver/src/errors.ts index 51ffec09df24..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( 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 0a2e45e8e2a5..d6e28a72ff01 100644 --- a/yarn-project/archiver/src/store/block_store.ts +++ b/yarn-project/archiver/src/store/block_store.ts @@ -42,9 +42,7 @@ import { BlockNumberNotSequentialError, CannotOverwriteCheckpointedBlockError, CheckpointNotFoundError, - CheckpointNumberNotConsistentError, CheckpointNumberNotSequentialError, - InitialBlockNumberNotSequentialError, InitialCheckpointNumberNotSequentialError, } from '../errors.js'; @@ -142,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(); @@ -166,76 +159,52 @@ export class BlockStore { // Verify we're not overwriting checkpointed blocks const lastCheckpointedBlockNumber = await this.getCheckpointedL2BlockNumber(); - if (!opts.force && firstBlockNumber <= lastCheckpointedBlockNumber) { + if (!opts.force && blockNumber <= lastCheckpointedBlockNumber) { // Check if the proposed block matches the already-checkpointed one - const existingBlock = await this.getBlock(BlockNumber(firstBlockNumber)); - if (existingBlock && existingBlock.archive.root.equals(blocks[0].archive.root)) { - throw new BlockAlreadyCheckpointedError(firstBlockNumber); + const existingBlock = await this.getBlock(BlockNumber(blockNumber)); + if (existingBlock && existingBlock.archive.root.equals(block.archive.root)) { + throw new BlockAlreadyCheckpointedError(blockNumber); } - throw new CannotOverwriteCheckpointedBlockError(firstBlockNumber, lastCheckpointedBlockNumber); + 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 0ae41a555865..a77015844573 100644 --- a/yarn-project/archiver/src/store/kv_archiver_store.test.ts +++ b/yarn-project/archiver/src/store/kv_archiver_store.test.ts @@ -47,9 +47,7 @@ import { BlockIndexNotSequentialError, BlockNumberNotSequentialError, CannotOverwriteCheckpointedBlockError, - CheckpointNumberNotConsistentError, CheckpointNumberNotSequentialError, - InitialBlockNumberNotSequentialError, InitialCheckpointNumberNotSequentialError, } from '../errors.js'; import { MessageStoreError } from '../store/message_store.js'; @@ -68,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[]; @@ -389,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); @@ -432,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); @@ -728,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); @@ -756,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); @@ -770,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)); @@ -795,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(); @@ -819,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; @@ -852,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); @@ -873,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(); @@ -890,7 +900,7 @@ describe('KVArchiverDataStore', () => { checkpointNumber: CheckpointNumber(1), indexWithinCheckpoint: IndexWithinCheckpoint(0), }); - await store.addProposedBlocks([block1]); + await store.addProposedBlock(block1); const archive = block1.archive.root; @@ -917,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); @@ -977,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); @@ -1036,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); @@ -1045,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( @@ -1079,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 () => { @@ -1115,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); @@ -1134,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); @@ -1153,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 () => { @@ -1188,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), { @@ -1196,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); }); @@ -1216,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), { @@ -1224,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); }); @@ -1244,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), { @@ -1252,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 () => { @@ -1276,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 () => { @@ -1300,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 () => { @@ -1316,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); }); @@ -1336,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), { @@ -1344,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); }); @@ -1364,14 +1337,14 @@ 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 () => { @@ -1383,7 +1356,7 @@ describe('KVArchiverDataStore', () => { // Re-propose the same block that was already checkpointed const checkpointedBlock = checkpoint1.checkpoint.blocks[1]; - await expect(store.addProposedBlocks([checkpointedBlock])).rejects.toThrow(BlockAlreadyCheckpointedError); + await expect(store.addProposedBlock(checkpointedBlock)).rejects.toThrow(BlockAlreadyCheckpointedError); }); }); @@ -1814,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( @@ -3065,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 }), @@ -3073,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( @@ -3185,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); @@ -3205,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([]); @@ -3236,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); @@ -3269,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 @@ -3298,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)); @@ -3319,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)); @@ -3347,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(); @@ -3399,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); } /**