From d7446323293ae5daf484eac5c4f5ec9a242846ce Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Tue, 16 Dec 2025 23:04:28 +0000 Subject: [PATCH 01/36] Initial changes --- .../archiver/src/archiver/archiver.ts | 37 ++++++++-- .../src/test/mock_l1_to_l2_message_source.ts | 8 ++- .../archiver/src/test/mock_l2_block_source.ts | 34 ++++++--- .../aztec-node/src/aztec-node/server.ts | 9 ++- .../aztec-node/src/sentinel/sentinel.test.ts | 7 +- .../aztec-node/src/sentinel/sentinel.ts | 72 +++++++++++-------- .../end-to-end/src/e2e_epochs/epochs_test.ts | 2 +- .../e2e_l1_publisher/e2e_l1_publisher.test.ts | 2 +- .../end-to-end/src/e2e_snapshot_sync.test.ts | 2 +- .../kv-store/src/stores/l2_tips_store.ts | 11 +-- 10 files changed, 122 insertions(+), 62 deletions(-) diff --git a/yarn-project/archiver/src/archiver/archiver.ts b/yarn-project/archiver/src/archiver/archiver.ts index ec2f9a5dff7e..8916e3158727 100644 --- a/yarn-project/archiver/src/archiver/archiver.ts +++ b/yarn-project/archiver/src/archiver/archiver.ts @@ -33,6 +33,7 @@ import type { FunctionSelector } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import { type ArchiverEmitter, + type CheckpointId, CheckpointedL2Block, CommitteeAttestation, L2Block, @@ -583,13 +584,13 @@ export class Archiver const newBlocks = blockPromises.filter(isDefined).flat(); // TODO(pw/mbps): Don't convert to legacy blocks here - const blocks: L2Block[] = (await Promise.all(newBlocks.map(x => this.getBlock(x.number)))).filter(isDefined); + //const blocks: L2Block[] = (await Promise.all(newBlocks.map(x => this.getBlock(x.number)))).filter(isDefined); // Emit an event for listening services to react to the chain prune this.emit(L2BlockSourceEvents.L2PruneDetected, { type: L2BlockSourceEvents.L2PruneDetected, epochNumber: pruneFromEpochNumber, - blocks, + blocks: newBlocks, }); this.log.debug( @@ -1373,6 +1374,16 @@ export class Archiver return publishedBlock; } + public async getL2BlocksNew(from: BlockNumber, limit: number, proven?: boolean): Promise { + const blocks = await this.store.store.getBlocks(from, limit); + + if (proven === true) { + const provenBlockNumber = await this.store.getProvenBlockNumber(); + return blocks.filter(b => b.number <= provenBlockNumber); + } + return blocks; + } + public async getBlockHeader(number: BlockNumber | 'latest'): Promise { if (number === 'latest') { number = await this.store.getSynchedL2BlockNumber(); @@ -1543,18 +1554,30 @@ export class Archiver const provenBlockHeaderHash = (await provenBlockHeader?.hash()) ?? GENESIS_BLOCK_HEADER_HASH; const finalizedBlockHeaderHash = (await finalizedBlockHeader?.hash()) ?? GENESIS_BLOCK_HEADER_HASH; + const latestCheckpoint = await this.store.getCheckpointData(await this.store.getSynchedCheckpointNumber()); + const checkpointId: CheckpointId | undefined = + latestCheckpoint === undefined + ? undefined + : { + number: latestCheckpoint.checkpointNumber, + blockHeadersHash: latestCheckpoint.header.blockHeadersHash.toString(), + }; + return { - latest: { number: latestBlockNumber, hash: latestBlockHeaderHash.toString() }, - proven: { number: provenBlockNumber, hash: provenBlockHeaderHash.toString() }, - finalized: { number: finalizedBlockNumber, hash: finalizedBlockHeaderHash.toString() }, + blocks: { + latest: { number: latestBlockNumber, hash: latestBlockHeaderHash.toString() }, + proven: { number: provenBlockNumber, hash: provenBlockHeaderHash.toString() }, + finalized: { number: finalizedBlockNumber, hash: finalizedBlockHeaderHash.toString() }, + }, + checkpoint: checkpointId, }; } public async rollbackTo(targetL2BlockNumber: BlockNumber): Promise { // TODO(pw/mbps): This still assumes 1 block per checkpoint const currentBlocks = await this.getL2Tips(); - const currentL2Block = currentBlocks.latest.number; - const currentProvenBlock = currentBlocks.proven.number; + const currentL2Block = currentBlocks.blocks.latest.number; + const currentProvenBlock = currentBlocks.blocks.proven.number; if (targetL2BlockNumber >= currentL2Block) { throw new Error(`Target L2 block ${targetL2BlockNumber} must be less than current L2 block ${currentL2Block}`); diff --git a/yarn-project/archiver/src/test/mock_l1_to_l2_message_source.ts b/yarn-project/archiver/src/test/mock_l1_to_l2_message_source.ts index 83499dfa6763..04ff1c16e8e7 100644 --- a/yarn-project/archiver/src/test/mock_l1_to_l2_message_source.ts +++ b/yarn-project/archiver/src/test/mock_l1_to_l2_message_source.ts @@ -35,9 +35,11 @@ export class MockL1ToL2MessageSource implements L1ToL2MessageSource { const number = this.blockNumber; const tip = { number: BlockNumber(number), hash: new Fr(number).toString() }; return Promise.resolve({ - latest: tip, - proven: tip, - finalized: tip, + blocks: { + latest: tip, + proven: tip, + finalized: tip, + }, }); } } diff --git a/yarn-project/archiver/src/test/mock_l2_block_source.ts b/yarn-project/archiver/src/test/mock_l2_block_source.ts index 873d799c74cd..8cbda4d564fc 100644 --- a/yarn-project/archiver/src/test/mock_l2_block_source.ts +++ b/yarn-project/archiver/src/test/mock_l2_block_source.ts @@ -10,6 +10,7 @@ import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import { L2Block, L2BlockHash, + L2BlockNew, type L2BlockSource, type L2Tips, PublishedL2Block, @@ -140,6 +141,15 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { ); } + async getL2BlockNew(number: BlockNumber): Promise { + const block = await this.getBlock(number); + return block.toL2Block(); + } + async getL2BlocksNew(from: BlockNumber, limit: number, proven?: boolean): Promise { + const blocks = await this.getBlocks(from, limit, proven); + return blocks.map(x => x.toL2Block()); + } + public async getPublishedBlockByHash(blockHash: Fr): Promise { for (const block of this.l2Blocks) { const hash = await block.hash(); @@ -263,17 +273,19 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { const finalizedBlock = this.l2Blocks[finalized - 1]; return { - latest: { - number: BlockNumber(latest), - hash: (await latestBlock?.hash())?.toString(), - }, - proven: { - number: BlockNumber(proven), - hash: (await provenBlock?.hash())?.toString(), - }, - finalized: { - number: BlockNumber(finalized), - hash: (await finalizedBlock?.hash())?.toString(), + blocks: { + latest: { + number: BlockNumber(latest), + hash: (await latestBlock?.hash())?.toString(), + }, + proven: { + number: BlockNumber(proven), + hash: (await provenBlock?.hash())?.toString(), + }, + finalized: { + number: BlockNumber(finalized), + hash: (await finalizedBlock?.hash())?.toString(), + }, }, }; } diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index b9cf0d0788f2..7d9982a19665 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -58,6 +58,7 @@ import { type DataInBlock, type L2Block, L2BlockHash, + L2BlockNew, type L2BlockSource, type PublishedL2Block, } from '@aztec/stdlib/block'; @@ -620,6 +621,10 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { return (await this.blockSource.getPublishedBlocks(from, limit)) ?? []; } + public async getL2BlocksNew(from: BlockNumber, limit: number): Promise { + return (await this.blockSource.getL2BlocksNew(from, limit)) ?? []; + } + /** * Method to fetch the current base fees. * @returns The current base fees. @@ -1303,7 +1308,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { } // And it has an L2 block hash - const l2BlockHash = await archiver.getL2Tips().then(tips => tips.latest.hash); + const l2BlockHash = await archiver.getL2Tips().then(tips => tips.blocks.latest.hash); if (!l2BlockHash) { this.metrics.recordSnapshotError(); throw new Error(`Archiver has no latest L2 block hash downloaded. Cannot start snapshot.`); @@ -1337,7 +1342,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { throw new Error('Archiver implementation does not support rollbacks.'); } - const finalizedBlock = await archiver.getL2Tips().then(tips => tips.finalized.number); + const finalizedBlock = await archiver.getL2Tips().then(tips => tips.blocks.finalized.number); if (targetBlock < finalizedBlock) { if (force) { this.log.warn(`Clearing world state database to allow rolling back behind finalized block ${finalizedBlock}`); diff --git a/yarn-project/aztec-node/src/sentinel/sentinel.test.ts b/yarn-project/aztec-node/src/sentinel/sentinel.test.ts index cccca2afbb57..68a722b6421e 100644 --- a/yarn-project/aztec-node/src/sentinel/sentinel.test.ts +++ b/yarn-project/aztec-node/src/sentinel/sentinel.test.ts @@ -9,6 +9,7 @@ import { OffenseType, WANT_TO_SLASH_EVENT, type WantToSlashArgs } from '@aztec/s import type { SlasherConfig } from '@aztec/slasher/config'; import { CommitteeAttestation, + L2BlockNew, type L2BlockSource, type L2BlockStream, type L2BlockStreamEvent, @@ -89,7 +90,7 @@ describe('sentinel', () => { describe('getSlotActivity', () => { let signers: Secp256k1Signer[]; let validators: EthAddress[]; - let block: PublishedL2Block; + let block: L2BlockNew; let attestations: BlockAttestation[]; let proposer: EthAddress; let committee: EthAddress[]; @@ -97,8 +98,8 @@ describe('sentinel', () => { beforeEach(async () => { signers = times(4, Secp256k1Signer.random); validators = signers.map(signer => signer.address); - block = await randomPublishedL2Block(Number(slot)); - attestations = signers.map(signer => makeBlockAttestation({ signer, archive: block.block.archive.root })); + block = await L2BlockNew.random(BlockNumber(1), { slotNumber: SlotNumber(0) }); + attestations = signers.map(signer => makeBlockAttestation({ signer, archive: block.archive.root })); proposer = validators[0]; committee = [...validators]; diff --git a/yarn-project/aztec-node/src/sentinel/sentinel.ts b/yarn-project/aztec-node/src/sentinel/sentinel.ts index 9a3cb4e8fde4..1e29458f41e0 100644 --- a/yarn-project/aztec-node/src/sentinel/sentinel.ts +++ b/yarn-project/aztec-node/src/sentinel/sentinel.ts @@ -1,5 +1,5 @@ import type { EpochCache } from '@aztec/epoch-cache'; -import { BlockNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types'; import { countWhile, filterAsync, fromEntries, getEntries, mapValues } from '@aztec/foundation/collection'; import { EthAddress } from '@aztec/foundation/eth-address'; import { createLogger } from '@aztec/foundation/log'; @@ -19,7 +19,7 @@ import { L2BlockStream, type L2BlockStreamEvent, type L2BlockStreamEventHandler, - getAttestationInfoFromPublishedL2Block, + getAttestationInfoFromPublishedCheckpoint, } from '@aztec/stdlib/block'; import { getEpochAtSlot, getSlotRangeForEpoch, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers'; import type { @@ -44,8 +44,10 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme protected initialSlot: SlotNumber | undefined; protected lastProcessedSlot: SlotNumber | undefined; // eslint-disable-next-line aztec-custom/no-non-primitive-in-collections - protected slotNumberToBlock: Map = - new Map(); + protected slotNumberToCheckpoint: Map< + SlotNumber, + { checkpointNumber: CheckpointNumber; archive: string; attestors: EthAddress[] } + > = new Map(); constructor( protected epochCache: EpochCache, @@ -87,33 +89,45 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise { await this.l2TipsStore.handleBlockStreamEvent(event); - if (event.type === 'blocks-added') { - // Store mapping from slot to archive, block number, and attestors - for (const block of event.blocks) { - this.slotNumberToBlock.set(block.block.header.getSlot(), { - blockNumber: BlockNumber(block.block.number), - archive: block.block.archive.root.toString(), - attestors: getAttestationInfoFromPublishedL2Block(block) - .filter(a => a.status === 'recovered-from-signature') - .map(a => a.address!), - }); - } - - // Prune the archive map to only keep at most N entries - const historyLength = this.store.getHistoryLength(); - if (this.slotNumberToBlock.size > historyLength) { - const toDelete = Array.from(this.slotNumberToBlock.keys()) - .sort((a, b) => Number(a - b)) - .slice(0, this.slotNumberToBlock.size - historyLength); - for (const key of toDelete) { - this.slotNumberToBlock.delete(key); - } - } + if (event.type === 'checkpoint-added') { + await this.handleCheckpoint(event); } else if (event.type === 'chain-proven') { await this.handleChainProven(event); } } + protected async handleCheckpoint(event: L2BlockStreamEvent) { + if (event.type !== 'checkpoint-added') { + return; + } + const checkpointNumber = CheckpointNumber(event.checkpoint.number); + const [checkpoint] = await this.archiver.getPublishedCheckpoints(checkpointNumber, 1); + if (!checkpoint) { + this.logger.error(`Failed to get checkpoint ${checkpointNumber}`, { checkpoint }); + return; + } + + // Store mapping from slot to archive, block number, and attestors + this.slotNumberToCheckpoint.set(checkpoint.checkpoint.header.slotNumber, { + checkpointNumber: CheckpointNumber(checkpointNumber), + archive: checkpoint.checkpoint.archive.root.toString(), + attestors: getAttestationInfoFromPublishedCheckpoint(checkpoint) + .filter(a => a.status === 'recovered-from-signature') + .map(a => a.address!), + }); + + // Prune the archive map to only keep at most N entries + const historyLength = this.store.getHistoryLength(); + if (this.slotNumberToCheckpoint.size > historyLength) { + const toDelete = Array.from(this.slotNumberToCheckpoint.keys()) + .sort((a, b) => Number(a - b)) + .slice(0, this.slotNumberToCheckpoint.size - historyLength); + for (const key of toDelete) { + this.slotNumberToCheckpoint.delete(key); + } + } + } + protected async handleChainProven(event: L2BlockStreamEvent) { if (event.type !== 'chain-proven') { return; @@ -291,8 +305,8 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme return false; } - const archiverLastBlockHash = await this.l2TipsStore.getL2Tips().then(tip => tip.latest.hash); - const p2pLastBlockHash = await this.p2p.getL2Tips().then(tips => tips.latest.hash); + const archiverLastBlockHash = await this.l2TipsStore.getL2Tips().then(tip => tip.blocks.latest.hash); + const p2pLastBlockHash = await this.p2p.getL2Tips().then(tips => tips.blocks.latest.hash); const isP2pSynced = archiverLastBlockHash === p2pLastBlockHash; if (!isP2pSynced) { this.logger.debug(`Waiting for P2P client to sync with archiver`, { archiverLastBlockHash, p2pLastBlockHash }); @@ -331,7 +345,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme // or all attestations for all proposals in the slot if no block was mined. // We gather from both p2p (contains the ones seen on the p2p layer) and archiver // (contains the ones synced from mined blocks, which we may have missed from p2p). - const block = this.slotNumberToBlock.get(slot); + const block = this.slotNumberToCheckpoint.get(slot); const p2pAttested = await this.p2p.getAttestationsForSlot(slot, block?.archive); // Filter out attestations with invalid signatures const p2pAttestors = p2pAttested.map(a => a.getSender()).filter((s): s is EthAddress => s !== undefined); diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_test.ts index 94a81a78ef5b..ef9608c7df4c 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_test.ts @@ -335,7 +335,7 @@ export class EpochsTestContext { ]); this.logger.info(`Wait for node synch ${blockNumber} ${type}`, { blockNumber, type, syncState, tips }); if (type === 'proven') { - synched = tips.proven.number >= blockNumber && syncState.latestBlockNumber >= blockNumber; + synched = tips.blocks.proven.number >= blockNumber && syncState.latestBlockNumber >= blockNumber; } else if (type === 'finalized') { synched = syncState.finalizedBlockNumber >= blockNumber; } else { diff --git a/yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts b/yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts index 68fb87bcdeb7..c88ed173558f 100644 --- a/yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts +++ b/yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts @@ -222,7 +222,7 @@ describe('L1Publisher integration', () => { ? { number: latestBlock.number, hash: latestBlock.hash.toString() } : { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }; - return Promise.resolve({ latest: res, proven: res, finalized: res }); + return Promise.resolve({ blocks: { latest: res, proven: res, finalized: res } }); }, getBlockNumber(): Promise { return Promise.resolve(BlockNumber(blocks.at(-1)?.number ?? BlockNumber.ZERO)); diff --git a/yarn-project/end-to-end/src/e2e_snapshot_sync.test.ts b/yarn-project/end-to-end/src/e2e_snapshot_sync.test.ts index 3673642a8bfa..4116a06b0109 100644 --- a/yarn-project/end-to-end/src/e2e_snapshot_sync.test.ts +++ b/yarn-project/end-to-end/src/e2e_snapshot_sync.test.ts @@ -80,7 +80,7 @@ describe('e2e_snapshot_sync', () => { const expectNodeSyncedToL2Block = async (node: AztecNode | ProverNode, blockNumber: number) => { const tips = await node.getL2Tips(); - expect(tips.latest.number).toBeGreaterThanOrEqual(blockNumber); + expect(tips.blocks.latest.number).toBeGreaterThanOrEqual(blockNumber); const worldState = await node.getWorldStateSyncStatus(); expect(worldState.latestBlockNumber).toBeGreaterThanOrEqual(blockNumber); }; diff --git a/yarn-project/kv-store/src/stores/l2_tips_store.ts b/yarn-project/kv-store/src/stores/l2_tips_store.ts index 7d03a56f3430..5ffc71c22541 100644 --- a/yarn-project/kv-store/src/stores/l2_tips_store.ts +++ b/yarn-project/kv-store/src/stores/l2_tips_store.ts @@ -16,6 +16,7 @@ import type { AztecAsyncKVStore } from '../interfaces/store.js'; export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider { private readonly l2TipsStore: AztecAsyncMap; private readonly l2BlockHashesStore: AztecAsyncMap; + // TODO(pw/mbps): Store and serve checkpoint constructor(store: AztecAsyncKVStore, namespace: string) { this.l2TipsStore = store.openMap([namespace, 'l2_tips'].join('_')); @@ -28,9 +29,11 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo public async getL2Tips(): Promise { return { - latest: await this.getL2Tip('latest'), - finalized: await this.getL2Tip('finalized'), - proven: await this.getL2Tip('proven'), + blocks: { + latest: await this.getL2Tip('latest'), + finalized: await this.getL2Tip('finalized'), + proven: await this.getL2Tip('proven'), + }, }; } @@ -50,7 +53,7 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise { switch (event.type) { case 'blocks-added': { - const blocks = event.blocks.map(b => b.block); + const blocks = event.blocks; for (const block of blocks) { await this.l2BlockHashesStore.set(block.number, (await block.hash()).toString()); } From 10f281133a1267b422d2139b9e280a45e773a48d Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Tue, 16 Dec 2025 23:04:44 +0000 Subject: [PATCH 02/36] WIP --- .../src/actions/build-snapshot-metadata.ts | 4 +-- yarn-project/p2p/src/client/p2p_client.ts | 18 ++++++++----- .../mem_pools/tx_pool/tx_pool_bench.test.ts | 8 +++--- .../prover-node/src/prover-node.test.ts | 14 +++++----- .../block_synchronizer.test.ts | 12 ++++----- .../block_synchronizer/block_synchronizer.ts | 4 +-- .../src/sequencer/sequencer.test.ts | 16 +++++++----- .../src/sequencer/sequencer.ts | 4 +-- .../src/watchers/epoch_prune_watcher.test.ts | 26 ++++++++++++------- .../src/watchers/epoch_prune_watcher.ts | 9 ++++--- 10 files changed, 68 insertions(+), 47 deletions(-) diff --git a/yarn-project/node-lib/src/actions/build-snapshot-metadata.ts b/yarn-project/node-lib/src/actions/build-snapshot-metadata.ts index d7c8e0d2a236..9fb91f34722f 100644 --- a/yarn-project/node-lib/src/actions/build-snapshot-metadata.ts +++ b/yarn-project/node-lib/src/actions/build-snapshot-metadata.ts @@ -7,13 +7,13 @@ export async function buildSnapshotMetadata( archiver: Archiver, config: UploadSnapshotConfig, ): Promise { - const [rollupAddress, l1BlockNumber, { latest }] = await Promise.all([ + const [rollupAddress, l1BlockNumber, { blocks }] = await Promise.all([ archiver.getRollupAddress(), archiver.getL1BlockNumber(), archiver.getL2Tips(), ] as const); - const { number: l2BlockNumber, hash: l2BlockHash } = latest; + const { number: l2BlockNumber, hash: l2BlockHash } = blocks.latest; if (!l2BlockHash) { throw new Error(`Failed to get L2 block hash from archiver.`); } diff --git a/yarn-project/p2p/src/client/p2p_client.ts b/yarn-project/p2p/src/client/p2p_client.ts index 220e2651a397..34bc598dc9de 100644 --- a/yarn-project/p2p/src/client/p2p_client.ts +++ b/yarn-project/p2p/src/client/p2p_client.ts @@ -206,9 +206,11 @@ export class P2PClient const genesisHash = GENESIS_BLOCK_HEADER_HASH.toString(); return { - latest: { hash: latestBlockHash ?? genesisHash, number: latestBlockNumber }, - proven: { hash: provenBlockHash ?? genesisHash, number: provenBlockNumber }, - finalized: { hash: finalizedBlockHash ?? genesisHash, number: finalizedBlockNumber }, + blocks: { + latest: { hash: latestBlockHash ?? genesisHash, number: latestBlockNumber }, + proven: { hash: provenBlockHash ?? genesisHash, number: provenBlockNumber }, + finalized: { hash: finalizedBlockHash ?? genesisHash, number: finalizedBlockNumber }, + }, }; } @@ -216,7 +218,7 @@ export class P2PClient this.log.debug(`Handling block stream event ${event.type}`); switch (event.type) { case 'blocks-added': - await this.handleLatestL2Blocks(event.blocks.map(b => b.block.toL2Block())); + await this.handleLatestL2Blocks(event.blocks); break; case 'chain-finalized': { // TODO (alexg): I think we can prune the block hashes map here @@ -240,6 +242,8 @@ export class P2PClient this.txCollection.stopCollectingForBlocksAfter(event.block.number); await this.handlePruneL2Blocks(event.block.number); break; + case 'checkpoint-added': + break; default: { const _: never = event; break; @@ -274,9 +278,9 @@ export class P2PClient // get the current latest block numbers const latestBlockNumbers = await this.l2BlockSource.getL2Tips(); - this.latestBlockNumberAtStart = latestBlockNumbers.latest.number; - this.provenBlockNumberAtStart = latestBlockNumbers.proven.number; - this.finalizedBlockNumberAtStart = latestBlockNumbers.finalized.number; + this.latestBlockNumberAtStart = latestBlockNumbers.blocks.latest.number; + this.provenBlockNumberAtStart = latestBlockNumbers.blocks.proven.number; + this.finalizedBlockNumberAtStart = latestBlockNumbers.blocks.finalized.number; const syncedLatestBlock = (await this.getSyncedLatestBlockNum()) + 1; const syncedProvenBlock = (await this.getSyncedProvenBlockNum()) + 1; diff --git a/yarn-project/p2p/src/mem_pools/tx_pool/tx_pool_bench.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool/tx_pool_bench.test.ts index 7bcc872a4eb8..8989ec0566e7 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool/tx_pool_bench.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool/tx_pool_bench.test.ts @@ -158,9 +158,11 @@ describe('TxPool: Benchmarks', () => { getBlockNumber: () => Promise.resolve(BlockNumber.ZERO), getL2Tips: () => Promise.resolve({ - latest: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, - proven: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, - finalized: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + blocks: { + latest: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + proven: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + finalized: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + }, }), }); wsSync = new ServerWorldStateSynchronizer(ws, l2, getDefaultConfig(worldStateConfigMappings)); diff --git a/yarn-project/prover-node/src/prover-node.test.ts b/yarn-project/prover-node/src/prover-node.test.ts index 1d9e83bfd70c..45142a49d84b 100644 --- a/yarn-project/prover-node/src/prover-node.test.ts +++ b/yarn-project/prover-node/src/prover-node.test.ts @@ -150,13 +150,15 @@ describe('prover-node', () => { l2BlockSource.getCheckpointsForEpoch.mockResolvedValue(checkpoints); l2BlockSource.getPublishedCheckpoints.mockResolvedValue([lastPublishedCheckpoint]); l2BlockSource.getL2Tips.mockResolvedValue({ - latest: { - number: BlockNumber.fromCheckpointNumber(checkpoints.at(-1)!.number), - // TODO: This should be the actual block hash - hash: checkpoints.at(-1)!.hash().toString(), + blocks: { + latest: { + number: BlockNumber.fromCheckpointNumber(checkpoints.at(-1)!.number), + // TODO: This should be the actual block hash + hash: checkpoints.at(-1)!.hash().toString(), + }, + proven: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + finalized: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, }, - proven: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, - finalized: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, }); l2BlockSource.getBlockHeader.mockImplementation(number => Promise.resolve(number === checkpoints[0].blocks[0].number - 1 ? previousBlockHeader : undefined), diff --git a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts index 762839fb5f92..a53a21b8f12f 100644 --- a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts +++ b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts @@ -2,7 +2,7 @@ import { BlockNumber } from '@aztec/foundation/branded-types'; import { timesParallel } from '@aztec/foundation/collection'; import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import { L2TipsKVStore } from '@aztec/kv-store/stores'; -import { L2Block, type L2BlockStream } from '@aztec/stdlib/block'; +import { L2Block, L2BlockNew, type L2BlockStream } from '@aztec/stdlib/block'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import { randomPublishedL2Block } from '@aztec/stdlib/testing'; @@ -47,11 +47,11 @@ describe('BlockSynchronizer', () => { }); it('sets header from latest block', async () => { - const block = await randomPublishedL2Block(1); + const block = await L2BlockNew.random(BlockNumber(1)); await synchronizer.handleBlockStreamEvent({ type: 'blocks-added', blocks: [block] }); const obtainedHeader = await anchorBlockDataProvider.getBlockHeader(); - expect(obtainedHeader).toEqual(block.block.getBlockHeader()); + expect(obtainedHeader).toEqual(block.header); }); it('removes notes from db on a reorg', async () => { @@ -61,13 +61,13 @@ describe('BlockSynchronizer', () => { const resetNoteSyncData = jest .spyOn(taggingDataProvider, 'resetNoteSyncData') .mockImplementation(() => Promise.resolve()); - aztecNode.getBlockHeader.mockImplementation(async blockNumber => - (await L2Block.random(BlockNumber(blockNumber as number))).getBlockHeader(), + aztecNode.getBlockHeader.mockImplementation( + async blockNumber => (await L2BlockNew.random(BlockNumber(blockNumber as number))).header, ); await synchronizer.handleBlockStreamEvent({ type: 'blocks-added', - blocks: await timesParallel(5, randomPublishedL2Block), + blocks: await timesParallel(5, i => L2BlockNew.random(BlockNumber(i))), }); await synchronizer.handleBlockStreamEvent({ type: 'chain-pruned', block: { number: BlockNumber(3), hash: '0x3' } }); diff --git a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.ts b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.ts index 59e0e66344b6..abbe7c1fab1a 100644 --- a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.ts +++ b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.ts @@ -50,13 +50,13 @@ export class BlockSynchronizer implements L2BlockStreamEventHandler { switch (event.type) { case 'blocks-added': { - const lastBlock = event.blocks.at(-1)!.block; + const lastBlock = event.blocks.at(-1)!; this.log.verbose(`Updated pxe last block to ${lastBlock.number}`, { blockHash: lastBlock.hash(), archive: lastBlock.archive.root.toString(), header: lastBlock.header.toInspect(), }); - await this.anchorBlockDataProvider.setHeader(lastBlock.getBlockHeader()); + await this.anchorBlockDataProvider.setHeader(lastBlock.header); break; } case 'chain-pruned': { diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index a661bd3fd17c..d21fb5bb50a8 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -453,16 +453,20 @@ describe('sequencer', () => { p2p.getStatus.mockImplementation(() => Promise.resolve({ state: P2PClientState.IDLE, syncedToL2Block })); l2BlockSource.getL2Tips.mockImplementation(() => Promise.resolve({ - latest: syncedToL2Block, - proven: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, - finalized: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + blocks: { + latest: syncedToL2Block, + proven: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + finalized: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + }, }), ); l1ToL2MessageSource.getL2Tips.mockImplementation(() => Promise.resolve({ - latest: syncedToL2Block, - proven: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, - finalized: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + blocks: { + latest: syncedToL2Block, + proven: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + finalized: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + }, }), ); diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index 90cb6c0115f9..99453b2d2428 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -955,9 +955,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter t.latest), + this.l2BlockSource.getL2Tips().then(t => t.blocks.latest), this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block), - this.l1ToL2MessageSource.getL2Tips().then(t => t.latest), + this.l1ToL2MessageSource.getL2Tips().then(t => t.blocks.latest), this.l2BlockSource.getPendingChainValidationStatus(), ] as const); diff --git a/yarn-project/slasher/src/watchers/epoch_prune_watcher.test.ts b/yarn-project/slasher/src/watchers/epoch_prune_watcher.test.ts index 11f2045c5429..4e2c1221fb00 100644 --- a/yarn-project/slasher/src/watchers/epoch_prune_watcher.test.ts +++ b/yarn-project/slasher/src/watchers/epoch_prune_watcher.test.ts @@ -2,7 +2,7 @@ import type { EpochCache } from '@aztec/epoch-cache'; import { BlockNumber, EpochNumber } from '@aztec/foundation/branded-types'; import { EthAddress } from '@aztec/foundation/eth-address'; import { sleep } from '@aztec/foundation/sleep'; -import { L2Block, type L2BlockSourceEventEmitter, L2BlockSourceEvents } from '@aztec/stdlib/block'; +import { L2Block, L2BlockNew, type L2BlockSourceEventEmitter, L2BlockSourceEvents } from '@aztec/stdlib/block'; import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers'; import type { BuildBlockResult, @@ -74,9 +74,11 @@ describe('EpochPruneWatcher', () => { const emitSpy = jest.spyOn(watcher, 'emit'); const epochNumber = EpochNumber(1); - const block = await L2Block.random( + const block = await L2BlockNew.random( BlockNumber(12), // block number - 4, // txs per block + { + txsPerBlock: 4, + }, ); txProvider.getAvailableTxs.mockResolvedValue({ txs: [], missingTxs: [block.body.txEffects[0].txHash] }); @@ -118,9 +120,11 @@ describe('EpochPruneWatcher', () => { it('should slash if the data is available and the epoch could have been proven', async () => { const emitSpy = jest.spyOn(watcher, 'emit'); - const block = await L2Block.random( + const block = await L2BlockNew.random( BlockNumber(12), // block number - 4, // txs per block + { + txsPerBlock: 4, + }, ); const tx = Tx.random(); txProvider.getAvailableTxs.mockResolvedValue({ txs: [tx], missingTxs: [] }); @@ -170,13 +174,17 @@ describe('EpochPruneWatcher', () => { it('should not slash if the data is available but the epoch could not have been proven', async () => { const emitSpy = jest.spyOn(watcher, 'emit'); - const blockFromL1 = await L2Block.random( + const blockFromL1 = await L2BlockNew.random( BlockNumber(12), // block number - 1, // txs per block + { + txsPerBlock: 1, + }, ); - const blockFromBuilder = await L2Block.random( + const blockFromBuilder = await L2BlockNew.random( BlockNumber(13), // block number - 1, // txs per block + { + txsPerBlock: 1, + }, ); const tx = Tx.random(); txProvider.getAvailableTxs.mockResolvedValue({ txs: [tx], missingTxs: [] }); diff --git a/yarn-project/slasher/src/watchers/epoch_prune_watcher.ts b/yarn-project/slasher/src/watchers/epoch_prune_watcher.ts index cd05872eb1e4..cd87846d803e 100644 --- a/yarn-project/slasher/src/watchers/epoch_prune_watcher.ts +++ b/yarn-project/slasher/src/watchers/epoch_prune_watcher.ts @@ -5,6 +5,7 @@ import { type Logger, createLogger } from '@aztec/foundation/log'; import { EthAddress, L2Block, + L2BlockNew, type L2BlockPruneEvent, type L2BlockSourceEventEmitter, L2BlockSourceEvents, @@ -95,10 +96,10 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter this.emit(WANT_TO_SLASH_EVENT, args); } - private async processPruneL2Blocks(blocks: L2Block[], epochNumber: EpochNumber): Promise { + private async processPruneL2Blocks(blocks: L2BlockNew[], epochNumber: EpochNumber): Promise { try { const l1Constants = this.epochCache.getL1Constants(); - const epochBlocks = blocks.filter(b => getEpochAtSlot(b.slot, l1Constants) === epochNumber); + const epochBlocks = blocks.filter(b => getEpochAtSlot(b.header.getSlot(), l1Constants) === epochNumber); this.log.info( `Detected chain prune. Validating epoch ${epochNumber} with blocks ${epochBlocks[0]?.number} to ${epochBlocks[epochBlocks.length - 1]?.number}.`, { blocks: epochBlocks.map(b => b.toBlockInfo()) }, @@ -119,7 +120,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter } } - public async validateBlocks(blocks: L2Block[]): Promise { + public async validateBlocks(blocks: L2BlockNew[]): Promise { if (blocks.length === 0) { return; } @@ -133,7 +134,7 @@ export class EpochPruneWatcher extends (EventEmitter as new () => WatcherEmitter } } - public async validateBlock(blockFromL1: L2Block, fork: MerkleTreeWriteOperations): Promise { + public async validateBlock(blockFromL1: L2BlockNew, fork: MerkleTreeWriteOperations): Promise { this.log.debug(`Validating pruned block ${blockFromL1.header.globalVariables.blockNumber}`); const txHashes = blockFromL1.body.txEffects.map(txEffect => txEffect.txHash); // We load txs from the mempool directly, since the TxCollector running in the background has already been From 538e024ca6fa49b75788c3635a1ca16414ecfb99 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Tue, 16 Dec 2025 23:05:01 +0000 Subject: [PATCH 03/36] WIP --- .../stdlib/src/block/attestation_info.ts | 10 ++- .../stdlib/src/block/l2_block_source.ts | 32 ++++++-- .../src/block/l2_block_stream/interfaces.ts | 10 ++- .../l2_block_stream/l2_block_stream.test.ts | 4 +- .../block/l2_block_stream/l2_block_stream.ts | 78 +++++++++++-------- .../l2_block_stream/l2_tips_memory_store.ts | 21 +++-- .../block/test/l2_tips_store_test_suite.ts | 29 ++++--- .../stdlib/src/interfaces/archiver.test.ts | 17 +++- .../stdlib/src/interfaces/archiver.ts | 6 ++ .../stdlib/src/interfaces/aztec-node.test.ts | 15 +++- .../stdlib/src/interfaces/aztec-node.ts | 8 +- .../stdlib/src/interfaces/prover-node.test.ts | 8 +- .../src/wrappers/l2_block_stream.ts | 2 +- .../txe/src/state_machine/archiver.ts | 15 ++++ .../server_world_state_synchronizer.test.ts | 12 +-- .../server_world_state_synchronizer.ts | 12 +-- .../world-state/src/test/integration.test.ts | 8 +- 17 files changed, 193 insertions(+), 94 deletions(-) diff --git a/yarn-project/stdlib/src/block/attestation_info.ts b/yarn-project/stdlib/src/block/attestation_info.ts index 1ee0e54a976b..8dfa384e4860 100644 --- a/yarn-project/stdlib/src/block/attestation_info.ts +++ b/yarn-project/stdlib/src/block/attestation_info.ts @@ -1,8 +1,10 @@ import { recoverAddress } from '@aztec/foundation/crypto/secp256k1-signer'; import type { EthAddress } from '@aztec/foundation/eth-address'; +import { Checkpoint } from '../checkpoint/checkpoint.js'; import { ConsensusPayload } from '../p2p/consensus_payload.js'; import { SignatureDomainSeparator, getHashedSignaturePayloadEthSignedMessage } from '../p2p/signature_utils.js'; +import type { CheckpointedL2Block } from './checkpointed_l2_block.js'; import type { L2Block } from './l2_block.js'; import type { CommitteeAttestation } from './proposal/committee_attestation.js'; @@ -29,14 +31,14 @@ export type AttestationInfo = }; /** - * Extracts attestation information from a published L2 block. + * Extracts attestation information from a published checkpoint. * Returns info for each attestation, preserving array indices. */ -export function getAttestationInfoFromPublishedL2Block(block: { +export function getAttestationInfoFromPublishedCheckpoint(block: { attestations: CommitteeAttestation[]; - block: L2Block; + checkpoint: Checkpoint; }): AttestationInfo[] { - const payload = ConsensusPayload.fromBlock(block.block); + const payload = ConsensusPayload.fromCheckpoint(block.checkpoint); return getAttestationInfoFromPayload(payload, block.attestations); } diff --git a/yarn-project/stdlib/src/block/l2_block_source.ts b/yarn-project/stdlib/src/block/l2_block_source.ts index ea059f6c4f3f..17b006edc8cc 100644 --- a/yarn-project/stdlib/src/block/l2_block_source.ts +++ b/yarn-project/stdlib/src/block/l2_block_source.ts @@ -2,6 +2,7 @@ import { BlockNumber, BlockNumberSchema, CheckpointNumber, + CheckpointNumberSchema, type EpochNumber, type SlotNumber, } from '@aztec/foundation/branded-types'; @@ -171,6 +172,10 @@ export interface L2BlockSource { */ getBlock(number: BlockNumber): Promise; + getL2BlockNew(number: BlockNumber): Promise; + + getL2BlocksNew(from: BlockNumber, limit: number, proven?: boolean): Promise; + /** * Returns all blocks for a given epoch. * @dev Use this method only with recent epochs, since it walks the block list backwards. @@ -225,6 +230,7 @@ export type ArchiverEmitter = TypedEventEmitter<{ [L2BlockSourceEvents.L2PruneDetected]: (args: L2BlockPruneEvent) => void; [L2BlockSourceEvents.L2BlockProven]: (args: L2BlockProvenEvent) => void; [L2BlockSourceEvents.InvalidAttestationsBlockDetected]: (args: InvalidBlockDetectedEvent) => void; + [L2BlockSourceEvents.L2BlocksCheckpointed]: (args: L2CheckpointEvent) => void; }>; export interface L2BlockSourceEventEmitter extends L2BlockSource, ArchiverEmitter {} @@ -237,11 +243,13 @@ export interface L2BlockSourceEventEmitter extends L2BlockSource, ArchiverEmitte export type L2BlockTag = 'latest' | 'proven' | 'finalized'; /** Tips of the L2 chain. */ -export type L2Tips = Record; +export type L2Tips = { blocks: Record; checkpoint?: CheckpointId }; /** Identifies a block by number and hash. */ export type L2BlockId = { number: BlockNumber; hash: string }; +export type CheckpointId = { number: CheckpointNumber; blockHeadersHash: string }; + /** Creates an L2 block id */ export function makeL2BlockId(number: BlockNumber, hash?: string): L2BlockId { if (number !== 0 && !hash) { @@ -255,15 +263,24 @@ const L2BlockIdSchema = z.object({ hash: z.string(), }); +const L2CheckpointSchema = z.object({ + number: CheckpointNumberSchema, + blockHeadersHash: z.string(), +}); + export const L2TipsSchema = z.object({ - latest: L2BlockIdSchema, - proven: L2BlockIdSchema, - finalized: L2BlockIdSchema, + blocks: z.object({ + latest: L2BlockIdSchema, + proven: L2BlockIdSchema, + finalized: L2BlockIdSchema, + }), + checkpoint: L2CheckpointSchema, }); export enum L2BlockSourceEvents { L2PruneDetected = 'l2PruneDetected', L2BlockProven = 'l2BlockProven', + L2BlocksCheckpointed = 'l2BlocksCheckpointed', InvalidAttestationsBlockDetected = 'invalidBlockDetected', } @@ -277,7 +294,12 @@ export type L2BlockProvenEvent = { export type L2BlockPruneEvent = { type: 'l2PruneDetected'; epochNumber: EpochNumber; - blocks: L2Block[]; + blocks: L2BlockNew[]; +}; + +export type L2CheckpointEvent = { + type: 'l2BlocksCheckpointed'; + checkpointNumber: CheckpointNumber; }; export type InvalidBlockDetectedEvent = { diff --git a/yarn-project/stdlib/src/block/l2_block_stream/interfaces.ts b/yarn-project/stdlib/src/block/l2_block_stream/interfaces.ts index 3cf2c61411ec..99c4e907b9e5 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/interfaces.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/interfaces.ts @@ -1,5 +1,5 @@ -import type { PublishedL2Block } from '../checkpointed_l2_block.js'; -import type { L2BlockId, L2Tips } from '../l2_block_source.js'; +import type { L2BlockNew } from '../l2_block_new.js'; +import type { CheckpointId, L2BlockId, L2Tips } from '../l2_block_source.js'; /** Interface to the local view of the chain. Implemented by world-state and l2-tips-store. */ export interface L2BlockStreamLocalDataProvider { @@ -15,7 +15,11 @@ export interface L2BlockStreamEventHandler { export type L2BlockStreamEvent = | /** Emits blocks added to the chain. */ { type: 'blocks-added'; - blocks: PublishedL2Block[]; + blocks: L2BlockNew[]; + } + | /** Emits checkpoints published to L1. */ { + type: 'checkpoint-added'; + checkpoint: CheckpointId; } | /** Reports last correct block (new tip of the unproven chain). */ { type: 'chain-pruned'; diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts index 203ece8532ce..c5f8e32e7de4 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts @@ -275,7 +275,9 @@ class TestL2BlockStreamLocalDataProvider implements L2BlockStreamLocalDataProvid } public getL2Tips(): Promise { - return Promise.resolve(this); + return Promise.resolve({ + blocks: this, + }); } } diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts index a3e94128c87f..dacd44127457 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts @@ -13,7 +13,7 @@ export class L2BlockStream { private hasStarted = false; constructor( - private l2BlockSource: Pick, + private l2BlockSource: Pick, private localData: L2BlockStreamLocalDataProvider, private handler: L2BlockStreamEventHandler, private readonly log = createLogger('types:block_stream'), @@ -62,34 +62,37 @@ export class L2BlockStream { const sourceTips = await this.l2BlockSource.getL2Tips(); const localTips = await this.localData.getL2Tips(); this.log.trace(`Running L2 block stream`, { - sourceLatest: sourceTips.latest.number, - localLatest: localTips.latest.number, - sourceFinalized: sourceTips.finalized.number, - localFinalized: localTips.finalized.number, - sourceProven: sourceTips.proven.number, - localProven: localTips.proven.number, - sourceLatestHash: sourceTips.latest.hash, - localLatestHash: localTips.latest.hash, - sourceProvenHash: sourceTips.proven.hash, - localProvenHash: localTips.proven.hash, - sourceFinalizedHash: sourceTips.finalized.hash, - localFinalizedHash: localTips.finalized.hash, + sourceLatest: sourceTips.blocks.latest.number, + localLatest: localTips.blocks.latest.number, + sourceFinalized: sourceTips.blocks.finalized.number, + localFinalized: localTips.blocks.finalized.number, + sourceProven: sourceTips.blocks.proven.number, + localProven: localTips.blocks.proven.number, + sourceLatestHash: sourceTips.blocks.latest.hash, + localLatestHash: localTips.blocks.latest.hash, + sourceProvenHash: sourceTips.blocks.proven.hash, + localProvenHash: localTips.blocks.proven.hash, + sourceFinalizedHash: sourceTips.blocks.finalized.hash, + localFinalizedHash: localTips.blocks.finalized.hash, }); // Check if there was a reorg and emit a chain-pruned event if so. - let latestBlockNumber = localTips.latest.number; - const sourceCache = new BlockHashCache([sourceTips.latest]); + let latestBlockNumber = localTips.blocks.latest.number; + let latestCheckpointNumber = localTips.checkpoint?.number; + const sourceCache = new BlockHashCache([sourceTips.blocks.latest]); while (!(await this.areBlockHashesEqualAt(latestBlockNumber, { sourceCache }))) { latestBlockNumber--; } - if (latestBlockNumber < localTips.latest.number) { - latestBlockNumber = BlockNumber(Math.min(latestBlockNumber, sourceTips.latest.number)); // see #13471 + if (latestBlockNumber < localTips.blocks.latest.number) { + latestBlockNumber = BlockNumber(Math.min(latestBlockNumber, sourceTips.blocks.latest.number)); // see #13471 const hash = sourceCache.get(latestBlockNumber) ?? (await this.getBlockHashFromSource(latestBlockNumber)); if (latestBlockNumber !== 0 && !hash) { throw new Error(`Block hash not found in block source for block number ${latestBlockNumber}`); } - this.log.verbose(`Reorg detected. Pruning blocks from ${latestBlockNumber + 1} to ${localTips.latest.number}.`); + this.log.verbose( + `Reorg detected. Pruning blocks from ${latestBlockNumber + 1} to ${localTips.blocks.latest.number}.`, + ); await this.emitEvent({ type: 'chain-pruned', block: makeL2BlockId(latestBlockNumber, hash) }); } @@ -111,34 +114,45 @@ export class L2BlockStream { // last finalized block however in order to guarantee that we will eventually find a block in which our local // store matches the source. // If the last finalized block is behind our local tip, there is nothing to skip. - nextBlockNumber = Math.max(sourceTips.finalized.number, nextBlockNumber); + nextBlockNumber = Math.max(sourceTips.blocks.finalized.number, nextBlockNumber); } // Request new blocks from the source. - while (nextBlockNumber <= sourceTips.latest.number) { - const limit = Math.min(this.opts.batchSize ?? 50, sourceTips.latest.number - nextBlockNumber + 1); + while (nextBlockNumber <= sourceTips.blocks.latest.number) { + const limit = Math.min(this.opts.batchSize ?? 50, sourceTips.blocks.latest.number - nextBlockNumber + 1); this.log.trace(`Requesting blocks from ${nextBlockNumber} limit ${limit} proven=${this.opts.proven}`); - const blocks = await this.l2BlockSource.getPublishedBlocks( - BlockNumber(nextBlockNumber), - limit, - this.opts.proven, - ); + const blocks = await this.l2BlockSource.getL2BlocksNew(BlockNumber(nextBlockNumber), limit, this.opts.proven); if (blocks.length === 0) { break; } await this.emitEvent({ type: 'blocks-added', blocks }); - nextBlockNumber = blocks.at(-1)!.block.number + 1; + nextBlockNumber = blocks.at(-1)!.number + 1; + } + + // Update the checkpointed tips + if ( + localTips.checkpoint !== undefined && + sourceTips.checkpoint !== undefined && + localTips.checkpoint.number !== sourceTips.checkpoint.number + ) { + await this.emitEvent({ + type: 'checkpoint-added', + checkpoint: sourceTips.checkpoint, + }); } // Update the proven and finalized tips. - if (localTips.proven !== undefined && sourceTips.proven.number !== localTips.proven.number) { + if (localTips.blocks.proven !== undefined && sourceTips.blocks.proven.number !== localTips.blocks.proven.number) { await this.emitEvent({ type: 'chain-proven', - block: sourceTips.proven, + block: sourceTips.blocks.proven, }); } - if (localTips.finalized !== undefined && sourceTips.finalized.number !== localTips.finalized.number) { - await this.emitEvent({ type: 'chain-finalized', block: sourceTips.finalized }); + if ( + localTips.blocks.finalized !== undefined && + sourceTips.blocks.finalized.number !== localTips.blocks.finalized.number + ) { + await this.emitEvent({ type: 'chain-finalized', block: sourceTips.blocks.finalized }); } } catch (err: any) { if (err.name === 'AbortError') { @@ -186,7 +200,7 @@ export class L2BlockStream { private async emitEvent(event: L2BlockStreamEvent) { this.log.debug( - `Emitting ${event.type} (${event.type === 'blocks-added' ? event.blocks.length : event.block.number})`, + `Emitting ${event.type} (${event.type === 'blocks-added' ? event.blocks.length : event.type === 'checkpoint-added' ? event.checkpoint.number : event.block.number})`, ); await this.handler.handleBlockStreamEvent(event); if (!this.isRunning() && !this.isSyncing) { diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_memory_store.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_memory_store.ts index f393789e3d80..e584fcbd714e 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_memory_store.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_memory_store.ts @@ -1,8 +1,8 @@ import { GENESIS_BLOCK_HEADER_HASH } from '@aztec/constants'; import { BlockNumber } from '@aztec/foundation/branded-types'; -import type { L2Block } from '../l2_block.js'; -import type { L2BlockId, L2BlockTag, L2Tips } from '../l2_block_source.js'; +import type { L2BlockNew } from '../l2_block_new.js'; +import type { CheckpointId, L2BlockId, L2BlockTag, L2Tips } from '../l2_block_source.js'; import type { L2BlockStreamEvent, L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider } from './interfaces.js'; /** @@ -12,6 +12,7 @@ import type { L2BlockStreamEvent, L2BlockStreamEventHandler, L2BlockStreamLocalD export class L2TipsMemoryStore implements L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider { protected readonly l2TipsStore: Map = new Map(); protected readonly l2BlockHashesStore: Map = new Map(); + protected l2Checkpoint: CheckpointId | undefined = undefined; public getL2BlockHash(number: number): Promise { return Promise.resolve(this.l2BlockHashesStore.get(number)); @@ -19,9 +20,12 @@ export class L2TipsMemoryStore implements L2BlockStreamEventHandler, L2BlockStre public getL2Tips(): Promise { return Promise.resolve({ - latest: this.getL2Tip('latest'), - finalized: this.getL2Tip('finalized'), - proven: this.getL2Tip('proven'), + blocks: { + latest: this.getL2Tip('latest'), + finalized: this.getL2Tip('finalized'), + proven: this.getL2Tip('proven'), + }, + checkpoint: this.l2Checkpoint, }); } @@ -41,13 +45,16 @@ export class L2TipsMemoryStore implements L2BlockStreamEventHandler, L2BlockStre public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise { switch (event.type) { case 'blocks-added': { - const blocks = event.blocks.map(b => b.block); + const blocks = event.blocks; for (const block of blocks) { this.l2BlockHashesStore.set(block.number, await this.computeBlockHash(block)); } this.l2TipsStore.set('latest', blocks.at(-1)!.number); break; } + case 'checkpoint-added': + this.l2Checkpoint = event.checkpoint; + break; case 'chain-pruned': this.saveTag('latest', event.block); break; @@ -72,7 +79,7 @@ export class L2TipsMemoryStore implements L2BlockStreamEventHandler, L2BlockStre } } - protected computeBlockHash(block: L2Block) { + protected computeBlockHash(block: L2BlockNew) { return block.hash().then(hash => hash.toString()); } } diff --git a/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts b/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts index 2de7a5150c27..ac3277e53760 100644 --- a/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts +++ b/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts @@ -2,7 +2,7 @@ import { GENESIS_BLOCK_HEADER_HASH } from '@aztec/constants'; import { BlockNumber } from '@aztec/foundation/branded-types'; import { times } from '@aztec/foundation/collection'; import { Fr } from '@aztec/foundation/curves/bn254'; -import { type L2Block, type L2BlockId, PublishedL2Block } from '@aztec/stdlib/block'; +import { type L2Block, type L2BlockId, L2BlockNew, PublishedL2Block } from '@aztec/stdlib/block'; import { L1PublishedData } from '@aztec/stdlib/checkpoint'; import { jestExpect as expect } from '@jest/expect'; @@ -16,12 +16,10 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { tipsStore = await makeTipsStore(); }); - const makeBlock = (number: number): PublishedL2Block => - PublishedL2Block.fromFields({ - block: { number: BlockNumber(number), hash: () => Promise.resolve(new Fr(number)) } as L2Block, - l1: new L1PublishedData(BigInt(number), BigInt(number), `0x${number}`), - attestations: [], - }); + const makeBlock = async (number: number): Promise => { + const block = await L2BlockNew.random(BlockNumber(number)); + return block; + }; const makeBlockId = (number: number): L2BlockId => ({ number: BlockNumber(number), @@ -45,7 +43,10 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { }); it('stores chain tips', async () => { - await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: times(20, i => makeBlock(i + 1)) }); + await tipsStore.handleBlockStreamEvent({ + type: 'blocks-added', + blocks: await Promise.all(times(20, i => makeBlock(i + 1))), + }); await tipsStore.handleBlockStreamEvent({ type: 'chain-finalized', block: makeBlockId(5) }); await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: makeBlockId(8) }); @@ -56,7 +57,10 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { }); it('sets latest tip from blocks added', async () => { - await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: times(3, i => makeBlock(i + 1)) }); + await tipsStore.handleBlockStreamEvent({ + type: 'blocks-added', + blocks: await Promise.all(times(3, i => makeBlock(i + 1))), + }); const tips = await tipsStore.getL2Tips(); expect(tips).toEqual(makeTips(3, 0, 0)); @@ -67,7 +71,10 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { }); it('clears block hashes when setting finalized chain', async () => { - await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: times(5, i => makeBlock(i + 1)) }); + await tipsStore.handleBlockStreamEvent({ + type: 'blocks-added', + blocks: await Promise.all(times(5, i => makeBlock(i + 1))), + }); await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: makeBlockId(3) }); await tipsStore.handleBlockStreamEvent({ type: 'chain-finalized', block: makeBlockId(3) }); @@ -84,7 +91,7 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { // Regression test for #13142 it('does not blow up when setting proven chain on an unseen block number', async () => { - await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: [makeBlock(5)] }); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: [await makeBlock(5)] }); await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: makeBlockId(3) }); const tips = await tipsStore.getL2Tips(); diff --git a/yarn-project/stdlib/src/interfaces/archiver.test.ts b/yarn-project/stdlib/src/interfaces/archiver.test.ts index 5466c7daecd5..48a630c4d71f 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.test.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.test.ts @@ -374,6 +374,15 @@ class MockArchiver implements ArchiverApi { }), ]; } + + getL2BlockNew(number: BlockNumber): Promise { + return L2BlockNew.random(BlockNumber(number)); + } + async getL2BlocksNew(from: BlockNumber, _1: number, _2?: boolean): Promise { + const block = await L2BlockNew.random(from); + return [block]; + } + async getPublishedBlockByHash(_blockHash: Fr): Promise { return PublishedL2Block.fromFields({ block: await L2Block.random(BlockNumber(1)), @@ -432,9 +441,11 @@ class MockArchiver implements ArchiverApi { } getL2Tips(): Promise { return Promise.resolve({ - latest: { number: BlockNumber(1), hash: `0x01` }, - proven: { number: BlockNumber(1), hash: `0x01` }, - finalized: { number: BlockNumber(1), hash: `0x01` }, + blocks: { + latest: { number: BlockNumber(1), hash: `0x01` }, + proven: { number: BlockNumber(1), hash: `0x01` }, + finalized: { number: BlockNumber(1), hash: `0x01` }, + }, }); } getL2BlockHash(blockNumber: BlockNumber): Promise { diff --git a/yarn-project/stdlib/src/interfaces/archiver.ts b/yarn-project/stdlib/src/interfaces/archiver.ts index 2990f5d440fe..0b9fd2e0b70d 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.ts @@ -6,6 +6,7 @@ import { z } from 'zod'; import { CheckpointedL2Block, PublishedL2Block } from '../block/checkpointed_l2_block.js'; import { L2Block } from '../block/l2_block.js'; +import { L2BlockNew } from '../block/l2_block_new.js'; import { type L2BlockSource, L2TipsSchema } from '../block/l2_block_source.js'; import { ValidateBlockResultSchema } from '../block/validate_block_result.js'; import { Checkpoint } from '../checkpoint/checkpoint.js'; @@ -98,6 +99,11 @@ export const ArchiverApiSchema: ApiSchemaFor = { .function() .args(BlockNumberSchema, schemas.Integer, optional(z.boolean())) .returns(z.array(PublishedL2Block.schema)), + getL2BlockNew: z.function().args(BlockNumberSchema).returns(L2BlockNew.schema), + getL2BlocksNew: z + .function() + .args(BlockNumberSchema, schemas.Integer, optional(z.boolean())) + .returns(z.array(L2BlockNew.schema)), getPublishedBlockByHash: z.function().args(schemas.Fr).returns(PublishedL2Block.schema.optional()), getPublishedBlockByArchive: z.function().args(schemas.Fr).returns(PublishedL2Block.schema.optional()), getBlockHeaderByHash: z.function().args(schemas.Fr).returns(BlockHeader.schema.optional()), diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts index e1530552d030..45e20ed714ba 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts @@ -23,7 +23,7 @@ import type { ContractArtifact } from '../abi/abi.js'; import { AztecAddress } from '../aztec-address/index.js'; import { PublishedL2Block } from '../block/checkpointed_l2_block.js'; import type { DataInBlock } from '../block/in_block.js'; -import { type BlockParameter, CommitteeAttestation, L2BlockHash } from '../block/index.js'; +import { type BlockParameter, CommitteeAttestation, L2BlockHash, L2BlockNew } from '../block/index.js'; import { L2Block } from '../block/l2_block.js'; import type { L2Tips } from '../block/l2_block_source.js'; import { L1PublishedData } from '../checkpoint/published_checkpoint.js'; @@ -521,12 +521,19 @@ class MockAztecNode implements AztecNode { getL2Tips(): Promise { return Promise.resolve({ - latest: { number: BlockNumber(1), hash: `0x01` }, - proven: { number: BlockNumber(1), hash: `0x01` }, - finalized: { number: BlockNumber(1), hash: `0x01` }, + blocks: { + latest: { number: BlockNumber(1), hash: `0x01` }, + proven: { number: BlockNumber(1), hash: `0x01` }, + finalized: { number: BlockNumber(1), hash: `0x01` }, + }, }); } + async getL2BlocksNew(from: BlockNumber, _1: number, _2?: boolean): Promise { + const block = await L2BlockNew.random(from); + return [block]; + } + findLeavesIndexes( blockNumber: number | 'latest', treeId: MerkleTreeId, diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.ts b/yarn-project/stdlib/src/interfaces/aztec-node.ts index 2394c75f3741..8e98ab2b783e 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.ts @@ -24,6 +24,7 @@ import { type BlockParameter, BlockParameterSchema } from '../block/block_parame import { PublishedL2Block } from '../block/checkpointed_l2_block.js'; import { type DataInBlock, dataInBlockSchemaFor } from '../block/in_block.js'; import { L2Block } from '../block/l2_block.js'; +import { L2BlockNew } from '../block/l2_block_new.js'; import { type L2BlockSource, type L2Tips, L2TipsSchema } from '../block/l2_block_source.js'; import { type ContractClassPublic, @@ -71,7 +72,7 @@ import { type WorldStateSyncStatus, WorldStateSyncStatusSchema } from './world_s * We will probably implement the additional interfaces by means other than Aztec Node as it's currently a privacy leak */ export interface AztecNode - extends Pick { + extends Pick { /** * Returns the tips of the L2 chain. */ @@ -578,6 +579,11 @@ export const AztecNodeApiSchema: ApiSchemaFor = { .args(BlockNumberPositiveSchema, z.number().gt(0).lte(MAX_RPC_BLOCKS_LEN)) .returns(z.array(PublishedL2Block.schema)), + getL2BlocksNew: z + .function() + .args(BlockNumberPositiveSchema, z.number().gt(0).lte(MAX_RPC_BLOCKS_LEN)) + .returns(z.array(L2BlockNew.schema)), + getCurrentBaseFees: z.function().returns(GasFees.schema), getMaxPriorityFees: z.function().returns(GasFees.schema), diff --git a/yarn-project/stdlib/src/interfaces/prover-node.test.ts b/yarn-project/stdlib/src/interfaces/prover-node.test.ts index 539d32059926..e47720573907 100644 --- a/yarn-project/stdlib/src/interfaces/prover-node.test.ts +++ b/yarn-project/stdlib/src/interfaces/prover-node.test.ts @@ -64,9 +64,11 @@ class MockProverNode implements ProverNodeApi { getL2Tips(): Promise { return Promise.resolve({ - latest: { number: BlockNumber(1), hash: `0x01` }, - proven: { number: BlockNumber(1), hash: `0x01` }, - finalized: { number: BlockNumber(1), hash: `0x01` }, + blocks: { + latest: { number: BlockNumber(1), hash: `0x01` }, + proven: { number: BlockNumber(1), hash: `0x01` }, + finalized: { number: BlockNumber(1), hash: `0x01` }, + }, }); } diff --git a/yarn-project/telemetry-client/src/wrappers/l2_block_stream.ts b/yarn-project/telemetry-client/src/wrappers/l2_block_stream.ts index 9a32e82e9e2f..2cd4fc2843d8 100644 --- a/yarn-project/telemetry-client/src/wrappers/l2_block_stream.ts +++ b/yarn-project/telemetry-client/src/wrappers/l2_block_stream.ts @@ -11,7 +11,7 @@ import { type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client' /** Extends an L2BlockStream with a tracer to create a new trace per iteration. */ export class TraceableL2BlockStream extends L2BlockStream implements Traceable { constructor( - l2BlockSource: Pick, + l2BlockSource: Pick, localData: L2BlockStreamLocalDataProvider, handler: L2BlockStreamEventHandler, public readonly tracer: Tracer, diff --git a/yarn-project/txe/src/state_machine/archiver.ts b/yarn-project/txe/src/state_machine/archiver.ts index 05a1da4153d8..8aece553259e 100644 --- a/yarn-project/txe/src/state_machine/archiver.ts +++ b/yarn-project/txe/src/state_machine/archiver.ts @@ -9,6 +9,7 @@ import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import { CommitteeAttestation, L2Block, + L2BlockNew, type L2BlockSource, type L2Tips, PublishedL2Block, @@ -91,6 +92,20 @@ export class TXEArchiver extends ArchiverStoreHelper implements L2BlockSource { return this.retrievePublishedBlocks(from, limit, proven); } + getL2BlockNew(number: BlockNumber): Promise { + return this.store.getBlock(number); + } + + async getL2BlocksNew(from: BlockNumber, limit: number, proven?: boolean): Promise { + const blocks = await this.store.getBlocks(from, limit); + + if (proven === true) { + const provenBlockNumber = await this.store.getProvenBlockNumber(); + return blocks.filter(b => b.number <= provenBlockNumber); + } + return blocks; + } + private async retrievePublishedBlocks( from: BlockNumber, limit: number, diff --git a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.test.ts b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.test.ts index 091d873c92c7..d7ad1bf0cce9 100644 --- a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.test.ts +++ b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.test.ts @@ -93,15 +93,7 @@ describe('ServerWorldStateSynchronizer', () => { }); const pushBlocks = async (from: number, to: number) => { - const blocks = checkpoints - .flatMap(c => c.checkpoint.blocks) - .filter(b => b.number >= from && b.number <= to) - .map( - b => - ({ - block: { toL2Block: () => b }, - }) as any as PublishedL2Block, - ); + const blocks = checkpoints.flatMap(c => c.checkpoint.blocks).filter(b => b.number >= from && b.number <= to); await server.handleBlockStreamEvent({ type: 'blocks-added', blocks, @@ -270,6 +262,6 @@ class TestWorldStateSynchronizer extends ServerWorldStateSynchronizer { } public override getL2Tips() { - return Promise.resolve({ latest: this.latest, proven: this.proven, finalized: this.finalized }); + return Promise.resolve({ blocks: { latest: this.latest, proven: this.proven, finalized: this.finalized } }); } } diff --git a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts index 1c0bb2dc3b82..0cd928a570aa 100644 --- a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts +++ b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts @@ -160,7 +160,7 @@ export class ServerWorldStateSynchronizer } public async getLatestBlockNumber() { - return (await this.getL2Tips()).latest.number; + return (await this.getL2Tips()).blocks.latest.number; } public async stopSync() { @@ -257,9 +257,11 @@ export class ServerWorldStateSynchronizer const latestBlockId: L2BlockId = { number: status.unfinalizedBlockNumber, hash: unfinalizedBlockHash! }; return { - latest: latestBlockId, - finalized: { number: status.finalizedBlockNumber, hash: '' }, - proven: { number: this.provenBlockNumber ?? status.finalizedBlockNumber, hash: '' }, // TODO(palla/reorg): Using finalized as proven for now + blocks: { + latest: latestBlockId, + finalized: { number: status.finalizedBlockNumber, hash: '' }, + proven: { number: this.provenBlockNumber ?? status.finalizedBlockNumber, hash: '' }, // TODO(palla/reorg): Using finalized as proven for now + }, }; } @@ -267,7 +269,7 @@ export class ServerWorldStateSynchronizer public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise { switch (event.type) { case 'blocks-added': - await this.handleL2Blocks(event.blocks.map(b => b.block.toL2Block())); + await this.handleL2Blocks(event.blocks); break; case 'chain-pruned': await this.handleChainPruned(event.block.number); diff --git a/yarn-project/world-state/src/test/integration.test.ts b/yarn-project/world-state/src/test/integration.test.ts index 93696e48e378..260d6715cd4b 100644 --- a/yarn-project/world-state/src/test/integration.test.ts +++ b/yarn-project/world-state/src/test/integration.test.ts @@ -80,13 +80,13 @@ describe('world-state integration', () => { return finalized > tipFinalized; }; - while (tips.latest.number < blockToSyncTo && sleepTime < maxTimeoutMS) { + while (tips.blocks.latest.number < blockToSyncTo && sleepTime < maxTimeoutMS) { await sleep(100); sleepTime = Date.now() - startTime; tips = await synchronizer.getL2Tips(); } - while (waitForFinalized(tips.finalized.number) && sleepTime < maxTimeoutMS) { + while (waitForFinalized(tips.blocks.finalized.number) && sleepTime < maxTimeoutMS) { await sleep(100); sleepTime = Date.now() - startTime; tips = await synchronizer.getL2Tips(); @@ -101,11 +101,11 @@ describe('world-state integration', () => { const expectSynchedToBlock = async (latest: number, finalized?: number) => { const tips = await synchronizer.getL2Tips(); - expect(tips.latest.number).toEqual(latest); + expect(tips.blocks.latest.number).toEqual(latest); await expectSynchedBlockHashMatches(latest); if (finalized !== undefined) { - expect(tips.finalized.number).toEqual(finalized); + expect(tips.blocks.finalized.number).toEqual(finalized); await expectSynchedBlockHashMatches(finalized); } }; From 2c0ba3ce39b128775a23f5c03e628636606fc3b5 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Tue, 16 Dec 2025 23:27:34 +0000 Subject: [PATCH 04/36] Test fixes --- .../aztec-node/src/sentinel/sentinel.test.ts | 109 +++++++++++++----- .../l2_block_stream/l2_block_stream.test.ts | 36 +++--- 2 files changed, 106 insertions(+), 39 deletions(-) diff --git a/yarn-project/aztec-node/src/sentinel/sentinel.test.ts b/yarn-project/aztec-node/src/sentinel/sentinel.test.ts index 68a722b6421e..10a3957144cf 100644 --- a/yarn-project/aztec-node/src/sentinel/sentinel.test.ts +++ b/yarn-project/aztec-node/src/sentinel/sentinel.test.ts @@ -1,5 +1,5 @@ import type { EpochCache } from '@aztec/epoch-cache'; -import { BlockNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types'; import { compactArray, times } from '@aztec/foundation/collection'; import { Secp256k1Signer } from '@aztec/foundation/crypto/secp256k1-signer'; import { EthAddress } from '@aztec/foundation/eth-address'; @@ -13,12 +13,12 @@ import { type L2BlockSource, type L2BlockStream, type L2BlockStreamEvent, - PublishedL2Block, - getAttestationInfoFromPublishedL2Block, + getAttestationInfoFromPublishedCheckpoint, } from '@aztec/stdlib/block'; -import { type L1RollupConstants, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers'; +import { Checkpoint, L1PublishedData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint'; +import { type L1RollupConstants, getEpochAtSlot, getSlotRangeForEpoch } from '@aztec/stdlib/epoch-helpers'; import type { BlockAttestation } from '@aztec/stdlib/p2p'; -import { makeBlockAttestation, randomPublishedL2Block } from '@aztec/stdlib/testing'; +import { makeAttestationFromCheckpoint, makeBlockAttestation } from '@aztec/stdlib/testing'; import type { ValidatorStats, ValidatorStatusHistory, @@ -91,6 +91,7 @@ describe('sentinel', () => { let signers: Secp256k1Signer[]; let validators: EthAddress[]; let block: L2BlockNew; + let publishedCheckpoint: PublishedCheckpoint; let attestations: BlockAttestation[]; let proposer: EthAddress; let committee: EthAddress[]; @@ -107,6 +108,12 @@ describe('sentinel', () => { }); it('flags block as mined', async () => { + // Set checkpoint data to simulate block being mined + sentinel.setCheckpointForSlot(slot, { + checkpointNumber: CheckpointNumber(1), + archive: block.archive.root.toString(), + attestors: [], + }); await sentinel.handleBlockStreamEvent({ type: 'blocks-added', blocks: [block] }); const activity = await sentinel.getSlotActivity(slot, epoch, proposer, committee); @@ -126,47 +133,88 @@ describe('sentinel', () => { }); it('identifies attestors from p2p and archiver', async () => { - block = await randomPublishedL2Block(Number(slot), { signers: signers.slice(0, 2) }); - const attestorsFromBlock = compactArray( - getAttestationInfoFromPublishedL2Block(block).map(info => + // Create a checkpoint with a block at the target slot + const checkpoint = await Checkpoint.random(CheckpointNumber(1), { numBlocks: 1, slotNumber: slot }); + // Create attestations from signers + const checkpointAttestations = signers.slice(0, 2).map(signer => { + const blockAttestation = makeAttestationFromCheckpoint(checkpoint, signer, signer); + return new CommitteeAttestation(signer.address, blockAttestation.signature); + }); + publishedCheckpoint = new PublishedCheckpoint(checkpoint, L1PublishedData.random(), checkpointAttestations); + + const attestorsFromCheckpoint = compactArray( + getAttestationInfoFromPublishedCheckpoint(publishedCheckpoint).map(info => info.status === 'recovered-from-signature' || info.status === 'provided-as-address' ? info.address : undefined, ), ); - expect(attestorsFromBlock.map(a => a.toString())).toEqual(signers.slice(0, 2).map(a => a.address.toString())); + expect(attestorsFromCheckpoint.map(a => a.toString())).toEqual( + signers.slice(0, 2).map(a => a.address.toString()), + ); + // Set checkpoint data with attestors from signers 0 and 1 (validators 0 and 1) + sentinel.setCheckpointForSlot(slot, { + checkpointNumber: CheckpointNumber(1), + archive: checkpoint.archive.root.toString(), + attestors: attestorsFromCheckpoint, + }); + + // Use the block from the checkpoint for the event + block = checkpoint.blocks[0]; await sentinel.handleBlockStreamEvent({ type: 'blocks-added', blocks: [block] }); + // P2P provides attestation from signer 2 (validator 2) p2p.getAttestationsForSlot.mockResolvedValue(attestations.slice(2, 3)); const activity = await sentinel.getSlotActivity(slot, epoch, proposer, committee); + // Validator 1 attested via archiver checkpoint data expect(activity[committee[1].toString()]).toEqual('attestation-sent'); + // Validator 2 attested via p2p expect(activity[committee[2].toString()]).toEqual('attestation-sent'); + // Validator 3 has no attestation expect(activity[committee[3].toString()]).toEqual('attestation-missed'); }); it('only counts recovered-from-signature attestations, not placeholder attestations', async () => { - // Create a block with only 2 signers (validators 0 and 1), plus placeholders for 2 and 3 - block = await randomPublishedL2Block(Number(slot), { signers: signers.slice(0, 2) }); + // Create a checkpoint with only 2 signers (validators 0 and 1), plus placeholders for 2 and 3 + const checkpoint = await Checkpoint.random(CheckpointNumber(1), { numBlocks: 1, slotNumber: slot }); + + // Create attestations from first 2 signers + const signedAttestations = signers.slice(0, 2).map(signer => { + const blockAttestation = makeAttestationFromCheckpoint(checkpoint, signer, signer); + return new CommitteeAttestation(signer.address, blockAttestation.signature); + }); // Add placeholder attestations for the missing validators (no signature) const placeholderAttestations = validators.slice(2).map(addr => CommitteeAttestation.fromAddress(addr)); - // Append placeholders to the existing attestations - const allAttestations = [...block.attestations, ...placeholderAttestations]; - block = new PublishedL2Block(block.block, block.l1, allAttestations); + // Combine signed and placeholder attestations + const allAttestations = [...signedAttestations, ...placeholderAttestations]; + publishedCheckpoint = new PublishedCheckpoint(checkpoint, L1PublishedData.random(), allAttestations); - // Verify that getAttestationInfoFromPublishedL2Block returns 4 entries total: + // Verify that getAttestationInfoFromPublishedCheckpoint returns 4 entries total: // - 2 with status 'recovered-from-signature' (actual attestations with valid signatures) // - 2 with status 'provided-as-address' (placeholders for missing validators) - const attestationInfo = getAttestationInfoFromPublishedL2Block(block); + const attestationInfo = getAttestationInfoFromPublishedCheckpoint(publishedCheckpoint); expect(attestationInfo).toHaveLength(4); const recoveredSignatures = attestationInfo.filter(info => info.status === 'recovered-from-signature'); const placeholders = attestationInfo.filter(info => info.status === 'provided-as-address'); expect(recoveredSignatures).toHaveLength(2); expect(placeholders).toHaveLength(2); - // After processing the block, only the validators with actual signatures should be recorded as attestors + // Set checkpoint data with ONLY the recovered-from-signature attestors (validators 0 and 1) + // This simulates how the Sentinel filters attestations when processing checkpoint-added events + const realAttestors = compactArray( + recoveredSignatures.map(info => (info.status === 'recovered-from-signature' ? info.address : undefined)), + ); + sentinel.setCheckpointForSlot(slot, { + checkpointNumber: CheckpointNumber(1), + archive: checkpoint.archive.root.toString(), + attestors: realAttestors, + }); + + // Use the block from the checkpoint for the event + block = checkpoint.blocks[0]; await sentinel.handleBlockStreamEvent({ type: 'blocks-added', blocks: [block] }); // No additional attestations from p2p @@ -517,13 +565,14 @@ describe('sentinel', () => { it('calls inactivity watcher with performance data', async () => { const blockNumber = BlockNumber(15); const blockHash = '0xblockhash'; - const mockBlock = await randomPublishedL2Block(blockNumber); - const slot = mockBlock.block.header.getSlot(); + const mockBlock = await L2BlockNew.random(blockNumber); + const slot = mockBlock.header.getSlot(); const epochNumber = getEpochAtSlot(slot, l1Constants); const validator1 = EthAddress.random(); const validator2 = EthAddress.random(); const validator3 = EthAddress.random(); - const headerSlots = times(l1Constants.epochDuration, i => SlotNumber(slot - i)).reverse(); + // Use getSlotRangeForEpoch to calculate expected slot range (same as Sentinel does) + const [fromSlot, toSlot] = getSlotRangeForEpoch(epochNumber, l1Constants); epochCache.getEpochAndSlotNow.mockReturnValue({ epoch: epochNumber, @@ -531,7 +580,7 @@ describe('sentinel', () => { ts, now: ts, }); - archiver.getBlock.calledWith(blockNumber).mockResolvedValue(mockBlock.block); + archiver.getBlock.calledWith(blockNumber).mockResolvedValue(mockBlock as any); archiver.getL1Constants.mockResolvedValue(l1Constants); epochCache.getL1Constants.mockReturnValue(l1Constants); @@ -546,7 +595,7 @@ describe('sentinel', () => { // Validator 1 missed 1 attestation only, we won't slash them [validator1.toString()]: { address: validator1, - totalSlots: headerSlots.length, + totalSlots: l1Constants.epochDuration, missedProposals: { count: 0, currentStreak: 0, rate: 0, total: 0 }, missedAttestations: { count: 1, currentStreak: 0, rate: 1 / 8, total: 8 }, history: [], @@ -554,7 +603,7 @@ describe('sentinel', () => { // Validator 2 missed 7 out of 8, we will slash them [validator2.toString()]: { address: validator2, - totalSlots: headerSlots.length, + totalSlots: l1Constants.epochDuration, missedProposals: { count: 0, currentStreak: 0, rate: 0, total: 0 }, missedAttestations: { count: 7, currentStreak: 3, rate: 7 / 8, total: 8 }, history: [], @@ -563,7 +612,7 @@ describe('sentinel', () => { // This difference happens because we don't count attestations for a slot where there was no proposal [validator3.toString()]: { address: validator3, - totalSlots: headerSlots.length, + totalSlots: l1Constants.epochDuration, missedProposals: { count: 0, currentStreak: 0, rate: 0, total: 0 }, missedAttestations: { count: 4, currentStreak: 4, rate: 4 / 4, total: 4 }, history: [], @@ -579,8 +628,8 @@ describe('sentinel', () => { await sentinel.handleChainProven({ type: 'chain-proven', block: { number: blockNumber, hash: blockHash } }); expect(computeStatsSpy).toHaveBeenCalledWith({ - fromSlot: headerSlots[0], - toSlot: headerSlots[headerSlots.length - 1], + fromSlot, + toSlot, validators: [validator1, validator2, validator3], }); const makeInactivitySlash = (validator: EthAddress): WantToSlashArgs => ({ @@ -835,6 +884,14 @@ class TestSentinel extends Sentinel { this.lastProcessedSlot = slot; } + /** Set checkpoint data for a slot (for testing). */ + public setCheckpointForSlot( + slot: SlotNumber, + data: { checkpointNumber: CheckpointNumber; archive: string; attestors: EthAddress[] }, + ) { + this.slotNumberToCheckpoint.set(slot, data); + } + public getInitialSlot() { return this.initialSlot; } diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts index c5f8e32e7de4..d9d6491052cb 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts @@ -1,4 +1,4 @@ -import { BlockNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; import { compactArray } from '@aztec/foundation/collection'; import { Fr } from '@aztec/foundation/curves/bn254'; @@ -6,8 +6,7 @@ import { type MockProxy, mock } from 'jest-mock-extended'; import times from 'lodash.times'; import type { BlockHeader } from '../../tx/block_header.js'; -import type { PublishedL2Block } from '../checkpointed_l2_block.js'; -import type { L2Block } from '../l2_block.js'; +import type { L2BlockNew } from '../l2_block_new.js'; import type { L2BlockId, L2BlockSource, L2Tips } from '../l2_block_source.js'; import type { L2BlockStreamEvent, L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider } from './interfaces.js'; import { L2BlockStream } from './l2_block_stream.js'; @@ -20,7 +19,12 @@ describe('L2BlockStream', () => { const makeHash = (number: number) => new Fr(number).toString(); - const makeBlock = (number: number) => ({ block: { number: BlockNumber(number) } as L2Block }) as PublishedL2Block; + const makeBlock = (number: number) => + ({ + number: BlockNumber(number), + checkpointNumber: CheckpointNumber(number), + indexWithinCheckpoint: 0, + }) as L2BlockNew; const makeHeader = (number: number) => ({ hash: () => Promise.resolve(new Fr(number)) }) as BlockHeader; @@ -32,9 +36,11 @@ describe('L2BlockStream', () => { latest = latest_; blockSource.getL2Tips.mockResolvedValue({ - latest: { number: BlockNumber(latest), hash: makeHash(latest) }, - proven: { number: BlockNumber(proven), hash: makeHash(proven) }, - finalized: { number: BlockNumber(finalized), hash: makeHash(finalized) }, + blocks: { + latest: { number: BlockNumber(latest), hash: makeHash(latest) }, + proven: { number: BlockNumber(proven), hash: makeHash(proven) }, + finalized: { number: BlockNumber(finalized), hash: makeHash(finalized) }, + }, }); }; @@ -50,7 +56,7 @@ describe('L2BlockStream', () => { ); // And returns blocks up until what was reported as the latest block - blockSource.getPublishedBlocks.mockImplementation((from, limit) => + blockSource.getL2BlocksNew.mockImplementation((from, limit) => Promise.resolve(compactArray(times(limit, i => (from + i > latest ? undefined : makeBlock(from + i))))), ); }); @@ -80,7 +86,7 @@ describe('L2BlockStream', () => { localData.latest.number = BlockNumber(10); await blockStream.work(); - expect(blockSource.getPublishedBlocks).toHaveBeenCalledWith(BlockNumber(11), 5, undefined); + expect(blockSource.getL2BlocksNew).toHaveBeenCalledWith(BlockNumber(11), 5, undefined); expect(handler.events).toEqual([ { type: 'blocks-added', blocks: times(5, i => makeBlock(i + 11)) }, ] satisfies L2BlockStreamEvent[]); @@ -90,7 +96,7 @@ describe('L2BlockStream', () => { setRemoteTips(45); await blockStream.work(); - expect(blockSource.getPublishedBlocks).toHaveBeenCalledTimes(5); + expect(blockSource.getL2BlocksNew).toHaveBeenCalledTimes(5); expect(handler.callCount).toEqual(5); expect(handler.events).toEqual([ { type: 'blocks-added', blocks: times(10, i => makeBlock(i + 1)) }, @@ -106,7 +112,7 @@ describe('L2BlockStream', () => { blockStream.running = false; await blockStream.work(); - expect(blockSource.getPublishedBlocks).toHaveBeenCalledTimes(1); + expect(blockSource.getL2BlocksNew).toHaveBeenCalledTimes(1); expect(handler.events).toEqual([ { type: 'blocks-added', blocks: times(10, i => makeBlock(i + 1)) }, ] satisfies L2BlockStreamEvent[]); @@ -276,7 +282,11 @@ class TestL2BlockStreamLocalDataProvider implements L2BlockStreamLocalDataProvid public getL2Tips(): Promise { return Promise.resolve({ - blocks: this, + blocks: { + latest: this.latest, + proven: this.proven, + finalized: this.finalized, + }, }); } } @@ -294,7 +304,7 @@ class TestL2BlockStream extends L2BlockStream { } class TestL2TipsMemoryStore extends L2TipsMemoryStore { - protected override computeBlockHash(block: L2Block): Promise<`0x${string}`> { + protected override computeBlockHash(block: L2BlockNew): Promise<`0x${string}`> { return Promise.resolve(new Fr(block.number).toString()); } } From 35d3a5f87e47221e1d2d897c3e5624a37b50988d Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Tue, 16 Dec 2025 23:34:20 +0000 Subject: [PATCH 05/36] Merge fixes --- .../src/tagging/sync/utils/get_status_change_of_pending.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn-project/pxe/src/tagging/sync/utils/get_status_change_of_pending.ts b/yarn-project/pxe/src/tagging/sync/utils/get_status_change_of_pending.ts index 6cf934ae1861..9511b96ec356 100644 --- a/yarn-project/pxe/src/tagging/sync/utils/get_status_change_of_pending.ts +++ b/yarn-project/pxe/src/tagging/sync/utils/get_status_change_of_pending.ts @@ -10,7 +10,7 @@ export async function getStatusChangeOfPending( aztecNode: AztecNode, ): Promise<{ txHashesToFinalize: TxHash[]; txHashesToDrop: TxHash[] }> { // Get receipts for all pending tx hashes and the finalized block number. - const [receipts, { finalized }] = await Promise.all([ + const [receipts, { blocks }] = await Promise.all([ Promise.all(pending.map(pendingTxHash => aztecNode.getTxReceipt(pendingTxHash))), aztecNode.getL2Tips(), ]); @@ -22,7 +22,7 @@ export async function getStatusChangeOfPending( const receipt = receipts[i]; const txHash = pending[i]; - if (receipt.status === TxStatus.SUCCESS && receipt.blockNumber && receipt.blockNumber <= finalized.number) { + if (receipt.status === TxStatus.SUCCESS && receipt.blockNumber && receipt.blockNumber <= blocks.finalized.number) { // Tx has been included in a block and the corresponding block is finalized --> we mark the indexes as // finalized. txHashesToFinalize.push(txHash); From 7b98d7b44df71bf4848bc8839e3b8cccce42b627 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Tue, 16 Dec 2025 23:48:12 +0000 Subject: [PATCH 06/36] Test fixes --- .../p2p/src/client/p2p_client.test.ts | 24 ++++++++++++------- .../sync/sync_sender_tagging_indexes.test.ts | 8 +++---- .../get_status_change_of_pending.test.ts | 4 ++-- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/yarn-project/p2p/src/client/p2p_client.test.ts b/yarn-project/p2p/src/client/p2p_client.test.ts index 299a081f6391..00414fbf93a7 100644 --- a/yarn-project/p2p/src/client/p2p_client.test.ts +++ b/yarn-project/p2p/src/client/p2p_client.test.ts @@ -316,9 +316,11 @@ describe('P2P Client', () => { await advanceToFinalizedBlock(BlockNumber(50)); await expect(client.getL2Tips()).resolves.toEqual({ - latest: { number: BlockNumber(100), hash: expect.any(String) }, - proven: { number: BlockNumber(90), hash: expect.any(String) }, - finalized: { number: BlockNumber(50), hash: expect.any(String) }, + blocks: { + latest: { number: BlockNumber(100), hash: expect.any(String) }, + proven: { number: BlockNumber(90), hash: expect.any(String) }, + finalized: { number: BlockNumber(50), hash: expect.any(String) }, + }, }); blockSource.removeBlocks(10); @@ -326,9 +328,11 @@ describe('P2P Client', () => { await client.sync(); await expect(client.getL2Tips()).resolves.toEqual({ - latest: { number: BlockNumber(90), hash: expect.any(String) }, - proven: { number: BlockNumber(90), hash: expect.any(String) }, - finalized: { number: BlockNumber(50), hash: expect.any(String) }, + blocks: { + latest: { number: BlockNumber(90), hash: expect.any(String) }, + proven: { number: BlockNumber(90), hash: expect.any(String) }, + finalized: { number: BlockNumber(50), hash: expect.any(String) }, + }, }); blockSource.addBlocks([await L2Block.random(BlockNumber(91)), await L2Block.random(BlockNumber(92))]); @@ -336,9 +340,11 @@ describe('P2P Client', () => { await client.sync(); await expect(client.getL2Tips()).resolves.toEqual({ - latest: { number: BlockNumber(92), hash: expect.any(String) }, - proven: { number: BlockNumber(90), hash: expect.any(String) }, - finalized: { number: BlockNumber(50), hash: expect.any(String) }, + blocks: { + latest: { number: BlockNumber(92), hash: expect.any(String) }, + proven: { number: BlockNumber(90), hash: expect.any(String) }, + finalized: { number: BlockNumber(50), hash: expect.any(String) }, + }, }); }); diff --git a/yarn-project/pxe/src/tagging/sync/sync_sender_tagging_indexes.test.ts b/yarn-project/pxe/src/tagging/sync/sync_sender_tagging_indexes.test.ts index 6f207bd6b7da..cae112e0a9c3 100644 --- a/yarn-project/pxe/src/tagging/sync/sync_sender_tagging_indexes.test.ts +++ b/yarn-project/pxe/src/tagging/sync/sync_sender_tagging_indexes.test.ts @@ -85,7 +85,7 @@ describe('syncSenderTaggingIndexes', () => { // Mock getL2Tips to return a finalized block number >= the tx block number aztecNode.getL2Tips.mockResolvedValue({ - finalized: { number: finalizedBlockNumberStep1 }, + blocks: { finalized: { number: finalizedBlockNumberStep1 } }, } as any); await syncSenderTaggingIndexes(secret, contractAddress, aztecNode, taggingDataProvider); @@ -114,7 +114,7 @@ describe('syncSenderTaggingIndexes', () => { } as any); aztecNode.getL2Tips.mockResolvedValue({ - finalized: { number: finalizedBlockNumberStep1 }, + blocks: { finalized: { number: finalizedBlockNumberStep1 } }, } as any); await syncSenderTaggingIndexes(secret, contractAddress, aztecNode, taggingDataProvider); @@ -183,7 +183,7 @@ describe('syncSenderTaggingIndexes', () => { // Mock getL2Tips with the new finalized block number aztecNode.getL2Tips.mockResolvedValue({ - finalized: { number: newFinalizedBlockNumber }, + blocks: { finalized: { number: newFinalizedBlockNumber } }, } as any); await syncSenderTaggingIndexes(secret, contractAddress, aztecNode, taggingDataProvider); @@ -236,7 +236,7 @@ describe('syncSenderTaggingIndexes', () => { }); aztecNode.getL2Tips.mockResolvedValue({ - finalized: { number: finalizedBlockNumber }, + blocks: { finalized: { number: finalizedBlockNumber } }, } as any); // Sync tagged logs diff --git a/yarn-project/pxe/src/tagging/sync/utils/get_status_change_of_pending.test.ts b/yarn-project/pxe/src/tagging/sync/utils/get_status_change_of_pending.test.ts index 953c12e3a9b0..2d383770e245 100644 --- a/yarn-project/pxe/src/tagging/sync/utils/get_status_change_of_pending.test.ts +++ b/yarn-project/pxe/src/tagging/sync/utils/get_status_change_of_pending.test.ts @@ -56,7 +56,7 @@ describe('getStatusChangeOfPending', () => { }); aztecNode.getL2Tips.mockResolvedValue({ - finalized: { number: BlockNumber(finalizedBlockNumber) }, + blocks: { finalized: { number: BlockNumber(finalizedBlockNumber) } }, } as any); const result = await getStatusChangeOfPending( @@ -90,7 +90,7 @@ describe('getStatusChangeOfPending', () => { } as any); aztecNode.getL2Tips.mockResolvedValue({ - finalized: { number: BlockNumber(finalizedBlockNumber) }, + blocks: { finalized: { number: BlockNumber(finalizedBlockNumber) } }, } as any); const result = await getStatusChangeOfPending([txHash], aztecNode); From 16ec5035227ca28d7783ae7d00f0382cf3a86ac9 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Wed, 17 Dec 2025 00:17:41 +0000 Subject: [PATCH 07/36] More test fixes --- .../src/sequencer/sequencer.test.ts | 4 ++-- .../src/watchers/epoch_prune_watcher.test.ts | 10 +++++++++- .../stdlib/src/block/l2_block_source.ts | 2 +- .../stdlib/src/interfaces/archiver.test.ts | 18 +++++++++++++++--- .../stdlib/src/interfaces/aztec-node.test.ts | 19 ++++++++++++++++--- .../stdlib/src/interfaces/prover-node.test.ts | 8 +++++--- 6 files changed, 48 insertions(+), 13 deletions(-) diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index d21fb5bb50a8..d0aedb272cd8 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -268,7 +268,7 @@ describe('sequencer', () => { l2BlockSource = mock({ getBlock: mockFn().mockResolvedValue(L2Block.empty()), getBlockNumber: mockFn().mockResolvedValue(lastBlockNumber), - getL2Tips: mockFn().mockResolvedValue({ latest: { number: lastBlockNumber, hash } }), + getL2Tips: mockFn().mockResolvedValue({ blocks: { latest: { number: lastBlockNumber, hash } } }), getL1Timestamp: mockFn().mockResolvedValue(1000n), isPendingChainInvalid: mockFn().mockResolvedValue(false), getPendingChainValidationStatus: mockFn().mockResolvedValue({ valid: true }), @@ -276,7 +276,7 @@ describe('sequencer', () => { l1ToL2MessageSource = mock({ getL1ToL2Messages: () => Promise.resolve(Array(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP).fill(Fr.ZERO)), - getL2Tips: mockFn().mockResolvedValue({ latest: { number: lastBlockNumber, hash } }), + getL2Tips: mockFn().mockResolvedValue({ blocks: { latest: { number: lastBlockNumber, hash } } }), }); validatorClient = mock(); diff --git a/yarn-project/slasher/src/watchers/epoch_prune_watcher.test.ts b/yarn-project/slasher/src/watchers/epoch_prune_watcher.test.ts index 4e2c1221fb00..745e13e69ff5 100644 --- a/yarn-project/slasher/src/watchers/epoch_prune_watcher.test.ts +++ b/yarn-project/slasher/src/watchers/epoch_prune_watcher.test.ts @@ -1,5 +1,5 @@ import type { EpochCache } from '@aztec/epoch-cache'; -import { BlockNumber, EpochNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types'; import { EthAddress } from '@aztec/foundation/eth-address'; import { sleep } from '@aztec/foundation/sleep'; import { L2Block, L2BlockNew, type L2BlockSourceEventEmitter, L2BlockSourceEvents } from '@aztec/stdlib/block'; @@ -74,10 +74,12 @@ describe('EpochPruneWatcher', () => { const emitSpy = jest.spyOn(watcher, 'emit'); const epochNumber = EpochNumber(1); + // Slot 10 is in epoch 1 (with epochDuration=8, epoch 1 = slots 8-15) const block = await L2BlockNew.random( BlockNumber(12), // block number { txsPerBlock: 4, + slotNumber: SlotNumber(10), }, ); txProvider.getAvailableTxs.mockResolvedValue({ txs: [], missingTxs: [block.body.txEffects[0].txHash] }); @@ -120,10 +122,12 @@ describe('EpochPruneWatcher', () => { it('should slash if the data is available and the epoch could have been proven', async () => { const emitSpy = jest.spyOn(watcher, 'emit'); + // Slot 10 is in epoch 1 (with epochDuration=8, epoch 1 = slots 8-15) const block = await L2BlockNew.random( BlockNumber(12), // block number { txsPerBlock: 4, + slotNumber: SlotNumber(10), }, ); const tx = Tx.random(); @@ -174,16 +178,20 @@ describe('EpochPruneWatcher', () => { it('should not slash if the data is available but the epoch could not have been proven', async () => { const emitSpy = jest.spyOn(watcher, 'emit'); + // Slot 10 is in epoch 1 (with epochDuration=8, epoch 1 = slots 8-15) const blockFromL1 = await L2BlockNew.random( BlockNumber(12), // block number { txsPerBlock: 1, + slotNumber: SlotNumber(10), }, ); + // Block from builder has different archive root, simulating failed re-execution const blockFromBuilder = await L2BlockNew.random( BlockNumber(13), // block number { txsPerBlock: 1, + slotNumber: SlotNumber(10), }, ); const tx = Tx.random(); diff --git a/yarn-project/stdlib/src/block/l2_block_source.ts b/yarn-project/stdlib/src/block/l2_block_source.ts index 17b006edc8cc..8811c90f904b 100644 --- a/yarn-project/stdlib/src/block/l2_block_source.ts +++ b/yarn-project/stdlib/src/block/l2_block_source.ts @@ -274,7 +274,7 @@ export const L2TipsSchema = z.object({ proven: L2BlockIdSchema, finalized: L2BlockIdSchema, }), - checkpoint: L2CheckpointSchema, + checkpoint: L2CheckpointSchema.optional(), }); export enum L2BlockSourceEvents { diff --git a/yarn-project/stdlib/src/interfaces/archiver.test.ts b/yarn-project/stdlib/src/interfaces/archiver.test.ts index 48a630c4d71f..e552bd71b75b 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.test.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.test.ts @@ -191,9 +191,11 @@ describe('ArchiverApiSchema', () => { it('getL2Tips', async () => { const result = await context.client.getL2Tips(); expect(result).toEqual({ - latest: { number: 1, hash: `0x01` }, - proven: { number: 1, hash: `0x01` }, - finalized: { number: 1, hash: `0x01` }, + blocks: { + latest: { number: 1, hash: `0x01` }, + proven: { number: 1, hash: `0x01` }, + finalized: { number: 1, hash: `0x01` }, + }, }); }); @@ -305,6 +307,16 @@ describe('ArchiverApiSchema', () => { const result = await context.client.getGenesisValues(); expect(result).toEqual({ genesisArchiveRoot: expect.any(Fr) }); }); + + it('getL2BlockNew', async () => { + const result = await context.client.getL2BlockNew(BlockNumber(1)); + expect(result).toEqual(expect.any(L2BlockNew)); + }); + + it('getL2BlocksNew', async () => { + const result = await context.client.getL2BlocksNew(BlockNumber(1), 1); + expect(result).toEqual([expect.any(L2BlockNew)]); + }); }); class MockArchiver implements ArchiverApi { diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts index 45e20ed714ba..32227fb27835 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts @@ -91,9 +91,11 @@ describe('AztecNodeApiSchema', () => { it('getL2Tips', async () => { const result = await context.client.getL2Tips(); expect(result).toEqual({ - latest: { number: 1, hash: `0x01` }, - proven: { number: 1, hash: `0x01` }, - finalized: { number: 1, hash: `0x01` }, + blocks: { + latest: { number: 1, hash: `0x01` }, + proven: { number: 1, hash: `0x01` }, + finalized: { number: 1, hash: `0x01` }, + }, }); }); @@ -251,6 +253,17 @@ describe('AztecNodeApiSchema', () => { await expect(context.client.getBlocks(BlockNumber(1), MAX_RPC_LEN + 1)).rejects.toThrow(); }); + it('getL2BlocksNew', async () => { + const response = await context.client.getL2BlocksNew(BlockNumber(1), BlockNumber(1)); + expect(response).toHaveLength(1); + expect(response[0]).toBeInstanceOf(L2BlockNew); + + await expect(context.client.getBlocks(-1 as BlockNumber, BlockNumber(1))).rejects.toThrow(); + await expect(context.client.getBlocks(BlockNumber.ZERO, BlockNumber(1))).rejects.toThrow(); + await expect(context.client.getBlocks(BlockNumber(1), BlockNumber.ZERO)).rejects.toThrow(); + await expect(context.client.getBlocks(BlockNumber(1), MAX_RPC_LEN + 1)).rejects.toThrow(); + }); + it('getPublishedBlocks', async () => { const response = await context.client.getPublishedBlocks(BlockNumber(1), BlockNumber(1)); expect(response).toHaveLength(1); diff --git a/yarn-project/stdlib/src/interfaces/prover-node.test.ts b/yarn-project/stdlib/src/interfaces/prover-node.test.ts index e47720573907..e2105f9095f5 100644 --- a/yarn-project/stdlib/src/interfaces/prover-node.test.ts +++ b/yarn-project/stdlib/src/interfaces/prover-node.test.ts @@ -39,9 +39,11 @@ describe('ProvingNodeApiSchema', () => { it('getL2Tips', async () => { const result = await context.client.getL2Tips(); expect(result).toEqual({ - latest: { number: 1, hash: `0x01` }, - proven: { number: 1, hash: `0x01` }, - finalized: { number: 1, hash: `0x01` }, + blocks: { + latest: { number: 1, hash: `0x01` }, + proven: { number: 1, hash: `0x01` }, + finalized: { number: 1, hash: `0x01` }, + }, }); }); From d0ee6571402162eb1d84e4489549c8ae324c6b9b Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Wed, 17 Dec 2025 00:32:16 +0000 Subject: [PATCH 08/36] More fixes --- .../block_synchronizer.test.ts | 3 +- .../src/watchers/epoch_prune_watcher.test.ts | 2 +- .../src/watchers/epoch_prune_watcher.ts | 1 - .../stdlib/src/block/attestation_info.ts | 2 -- .../block/l2_block_stream/l2_block_stream.ts | 1 - .../block/test/l2_tips_store_test_suite.ts | 31 +++++++++++-------- .../server_world_state_synchronizer.test.ts | 2 +- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts index d2c3f16b6917..85bc268196b0 100644 --- a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts +++ b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts @@ -2,9 +2,8 @@ import { BlockNumber } from '@aztec/foundation/branded-types'; import { timesParallel } from '@aztec/foundation/collection'; import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import { L2TipsKVStore } from '@aztec/kv-store/stores'; -import { L2Block, L2BlockNew, type L2BlockStream } from '@aztec/stdlib/block'; +import { L2BlockNew, type L2BlockStream } from '@aztec/stdlib/block'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; -import { randomPublishedL2Block } from '@aztec/stdlib/testing'; import { jest } from '@jest/globals'; import { type MockProxy, mock } from 'jest-mock-extended'; diff --git a/yarn-project/slasher/src/watchers/epoch_prune_watcher.test.ts b/yarn-project/slasher/src/watchers/epoch_prune_watcher.test.ts index 745e13e69ff5..4038ee412377 100644 --- a/yarn-project/slasher/src/watchers/epoch_prune_watcher.test.ts +++ b/yarn-project/slasher/src/watchers/epoch_prune_watcher.test.ts @@ -2,7 +2,7 @@ import type { EpochCache } from '@aztec/epoch-cache'; import { BlockNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types'; import { EthAddress } from '@aztec/foundation/eth-address'; import { sleep } from '@aztec/foundation/sleep'; -import { L2Block, L2BlockNew, type L2BlockSourceEventEmitter, L2BlockSourceEvents } from '@aztec/stdlib/block'; +import { L2BlockNew, type L2BlockSourceEventEmitter, L2BlockSourceEvents } from '@aztec/stdlib/block'; import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers'; import type { BuildBlockResult, diff --git a/yarn-project/slasher/src/watchers/epoch_prune_watcher.ts b/yarn-project/slasher/src/watchers/epoch_prune_watcher.ts index cd87846d803e..d1db413ad1f7 100644 --- a/yarn-project/slasher/src/watchers/epoch_prune_watcher.ts +++ b/yarn-project/slasher/src/watchers/epoch_prune_watcher.ts @@ -4,7 +4,6 @@ import { merge, pick } from '@aztec/foundation/collection'; import { type Logger, createLogger } from '@aztec/foundation/log'; import { EthAddress, - L2Block, L2BlockNew, type L2BlockPruneEvent, type L2BlockSourceEventEmitter, diff --git a/yarn-project/stdlib/src/block/attestation_info.ts b/yarn-project/stdlib/src/block/attestation_info.ts index 8dfa384e4860..c95fa654e50a 100644 --- a/yarn-project/stdlib/src/block/attestation_info.ts +++ b/yarn-project/stdlib/src/block/attestation_info.ts @@ -4,8 +4,6 @@ import type { EthAddress } from '@aztec/foundation/eth-address'; import { Checkpoint } from '../checkpoint/checkpoint.js'; import { ConsensusPayload } from '../p2p/consensus_payload.js'; import { SignatureDomainSeparator, getHashedSignaturePayloadEthSignedMessage } from '../p2p/signature_utils.js'; -import type { CheckpointedL2Block } from './checkpointed_l2_block.js'; -import type { L2Block } from './l2_block.js'; import type { CommitteeAttestation } from './proposal/committee_attestation.js'; /** diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts index dacd44127457..1f0268d490bb 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts @@ -78,7 +78,6 @@ export class L2BlockStream { // Check if there was a reorg and emit a chain-pruned event if so. let latestBlockNumber = localTips.blocks.latest.number; - let latestCheckpointNumber = localTips.checkpoint?.number; const sourceCache = new BlockHashCache([sourceTips.blocks.latest]); while (!(await this.areBlockHashesEqualAt(latestBlockNumber, { sourceCache }))) { latestBlockNumber--; diff --git a/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts b/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts index ac3277e53760..f83a7f961dae 100644 --- a/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts +++ b/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts @@ -2,8 +2,7 @@ import { GENESIS_BLOCK_HEADER_HASH } from '@aztec/constants'; import { BlockNumber } from '@aztec/foundation/branded-types'; import { times } from '@aztec/foundation/collection'; import { Fr } from '@aztec/foundation/curves/bn254'; -import { type L2Block, type L2BlockId, L2BlockNew, PublishedL2Block } from '@aztec/stdlib/block'; -import { L1PublishedData } from '@aztec/stdlib/checkpoint'; +import { type L2BlockId, L2BlockNew } from '@aztec/stdlib/block'; import { jestExpect as expect } from '@jest/expect'; @@ -11,30 +10,36 @@ import type { L2TipsStore } from '../l2_block_stream/index.js'; export function testL2TipsStore(makeTipsStore: () => Promise) { let tipsStore: L2TipsStore; + // Track blocks and their hashes for test assertions + const blockHashes: Map = new Map(); beforeEach(async () => { tipsStore = await makeTipsStore(); + blockHashes.clear(); }); const makeBlock = async (number: number): Promise => { const block = await L2BlockNew.random(BlockNumber(number)); + blockHashes.set(number, (await block.hash()).toString()); return block; }; const makeBlockId = (number: number): L2BlockId => ({ number: BlockNumber(number), - hash: new Fr(number).toString(), + hash: blockHashes.get(number) ?? new Fr(number).toString(), }); const makeTip = (number: number): L2BlockId => ({ number: BlockNumber(number), - hash: number === 0 ? GENESIS_BLOCK_HEADER_HASH.toString() : new Fr(number).toString(), + hash: number === 0 ? GENESIS_BLOCK_HEADER_HASH.toString() : (blockHashes.get(number) ?? new Fr(number).toString()), }); const makeTips = (latest: number, proven: number, finalized: number) => ({ - latest: makeTip(latest), - proven: makeTip(proven), - finalized: makeTip(finalized), + blocks: { + latest: makeTip(latest), + proven: makeTip(proven), + finalized: makeTip(finalized), + }, }); it('returns zero if no tips are stored', async () => { @@ -65,9 +70,9 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { const tips = await tipsStore.getL2Tips(); expect(tips).toEqual(makeTips(3, 0, 0)); - expect(await tipsStore.getL2BlockHash(1)).toEqual(new Fr(1).toString()); - expect(await tipsStore.getL2BlockHash(2)).toEqual(new Fr(2).toString()); - expect(await tipsStore.getL2BlockHash(3)).toEqual(new Fr(3).toString()); + expect(await tipsStore.getL2BlockHash(1)).toEqual(blockHashes.get(1)); + expect(await tipsStore.getL2BlockHash(2)).toEqual(blockHashes.get(2)); + expect(await tipsStore.getL2BlockHash(3)).toEqual(blockHashes.get(3)); }); it('clears block hashes when setting finalized chain', async () => { @@ -84,9 +89,9 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { expect(await tipsStore.getL2BlockHash(1)).toBeUndefined(); expect(await tipsStore.getL2BlockHash(2)).toBeUndefined(); - expect(await tipsStore.getL2BlockHash(3)).toEqual(new Fr(3).toString()); - expect(await tipsStore.getL2BlockHash(4)).toEqual(new Fr(4).toString()); - expect(await tipsStore.getL2BlockHash(5)).toEqual(new Fr(5).toString()); + expect(await tipsStore.getL2BlockHash(3)).toEqual(blockHashes.get(3)); + expect(await tipsStore.getL2BlockHash(4)).toEqual(blockHashes.get(4)); + expect(await tipsStore.getL2BlockHash(5)).toEqual(blockHashes.get(5)); }); // Regression test for #13142 diff --git a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.test.ts b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.test.ts index d7ad1bf0cce9..0fbc5f1b1db8 100644 --- a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.test.ts +++ b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.test.ts @@ -2,7 +2,7 @@ import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; import { timesParallel } from '@aztec/foundation/collection'; import { Fr } from '@aztec/foundation/curves/bn254'; import { type Logger, createLogger } from '@aztec/foundation/log'; -import { L2BlockNew, type L2BlockSource, type L2BlockStream, type PublishedL2Block } from '@aztec/stdlib/block'; +import { L2BlockNew, type L2BlockSource, type L2BlockStream } from '@aztec/stdlib/block'; import type { Checkpoint } from '@aztec/stdlib/checkpoint'; import { type MerkleTreeReadOperations, WorldStateRunningState } from '@aztec/stdlib/interfaces/server'; import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging'; From 9980f490ed123831545a387de13105a1b11c3d17 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Wed, 17 Dec 2025 01:01:20 +0000 Subject: [PATCH 09/36] More fixes --- .../archiver/src/archiver/archiver.test.ts | 68 +++++++++++++++++++ .../kv-store/src/stores/l2_tips_store.ts | 34 +++++++++- .../block/l2_block_stream/l2_block_stream.ts | 1 + 3 files changed, 101 insertions(+), 2 deletions(-) diff --git a/yarn-project/archiver/src/archiver/archiver.test.ts b/yarn-project/archiver/src/archiver/archiver.test.ts index dda791b91102..9993509e416b 100644 --- a/yarn-project/archiver/src/archiver/archiver.test.ts +++ b/yarn-project/archiver/src/archiver/archiver.test.ts @@ -1679,6 +1679,74 @@ describe('Archiver', () => { expect(retrievedBlock!.number).toEqual(BlockNumber(1)); }); + it('retrieves multiple blocks with getL2BlocksNew', async () => { + setupMinimalL1Mocks(); + const block1 = await makeBlock(BlockNumber(1), 0, genesisArchive); + const block2 = await makeBlock(BlockNumber(2), 1, block1.archive); + const block3 = await makeBlock(BlockNumber(3), 2, block2.archive); + + await archiver.addBlock(block1); + await archiver.addBlock(block2); + await archiver.addBlock(block3); + + const blocks = await archiver.getL2BlocksNew(BlockNumber(1), 3); + expect(blocks).toEqual([block1, block2, block3]); + }); + + it('retrieves blocks with limit in getL2BlocksNew', async () => { + setupMinimalL1Mocks(); + const block1 = await makeBlock(BlockNumber(1), 0, genesisArchive); + const block2 = await makeBlock(BlockNumber(2), 1, block1.archive); + const block3 = await makeBlock(BlockNumber(3), 2, block2.archive); + + await archiver.addBlock(block1); + await archiver.addBlock(block2); + await archiver.addBlock(block3); + + // Request only 2 blocks starting from block 1 + const blocks = await archiver.getL2BlocksNew(BlockNumber(1), 2); + expect(blocks).toEqual([block1, block2]); + }); + + it('retrieves blocks starting from middle with getL2BlocksNew', async () => { + setupMinimalL1Mocks(); + const block1 = await makeBlock(BlockNumber(1), 0, genesisArchive); + const block2 = await makeBlock(BlockNumber(2), 1, block1.archive); + const block3 = await makeBlock(BlockNumber(3), 2, block2.archive); + + await archiver.addBlock(block1); + await archiver.addBlock(block2); + await archiver.addBlock(block3); + + // Start from block 2 + const blocks = await archiver.getL2BlocksNew(BlockNumber(2), 2); + expect(blocks).toEqual([block2, block3]); + }); + + it('returns empty array when requesting blocks beyond available range', async () => { + setupMinimalL1Mocks(); + const block1 = await makeBlock(BlockNumber(1), 0, genesisArchive); + + await archiver.addBlock(block1); + + // Request blocks starting from block 5 (which doesn't exist) + const blocks = await archiver.getL2BlocksNew(BlockNumber(5), 3); + expect(blocks).toEqual([]); + }); + + it('returns partial results when limit exceeds available blocks', async () => { + setupMinimalL1Mocks(); + const block1 = await makeBlock(BlockNumber(1), 0, genesisArchive); + const block2 = await makeBlock(BlockNumber(2), 1, block1.archive); + + await archiver.addBlock(block1); + await archiver.addBlock(block2); + + // Request 10 blocks but only 2 are available + const blocks = await archiver.getL2BlocksNew(BlockNumber(1), 10); + expect(blocks).toEqual([block1, block2]); + }); + it('blocks added via addBlock become checkpointed when checkpoint syncs from L1', async () => { // First, sync checkpoint 1 from L1 to establish a baseline const checkpoint1 = checkpoints[0]; diff --git a/yarn-project/kv-store/src/stores/l2_tips_store.ts b/yarn-project/kv-store/src/stores/l2_tips_store.ts index 5ffc71c22541..0f8906d989c8 100644 --- a/yarn-project/kv-store/src/stores/l2_tips_store.ts +++ b/yarn-project/kv-store/src/stores/l2_tips_store.ts @@ -1,6 +1,7 @@ import { GENESIS_BLOCK_HEADER_HASH } from '@aztec/constants'; -import { BlockNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; import type { + CheckpointId, L2BlockId, L2BlockStreamEvent, L2BlockStreamEventHandler, @@ -10,17 +11,21 @@ import type { } from '@aztec/stdlib/block'; import type { AztecAsyncMap } from '../interfaces/map.js'; +import type { AztecAsyncSingleton } from '../interfaces/singleton.js'; import type { AztecAsyncKVStore } from '../interfaces/store.js'; /** Stores currently synced L2 tips and unfinalized block hashes. */ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider { private readonly l2TipsStore: AztecAsyncMap; private readonly l2BlockHashesStore: AztecAsyncMap; - // TODO(pw/mbps): Store and serve checkpoint + private readonly l2CheckpointNumberStore: AztecAsyncSingleton; + private readonly l2CheckpointHashStore: AztecAsyncSingleton; constructor(store: AztecAsyncKVStore, namespace: string) { this.l2TipsStore = store.openMap([namespace, 'l2_tips'].join('_')); this.l2BlockHashesStore = store.openMap([namespace, 'l2_block_hashes'].join('_')); + this.l2CheckpointNumberStore = store.openSingleton([namespace, 'l2_checkpoint_number'].join('_')); + this.l2CheckpointHashStore = store.openSingleton([namespace, 'l2_checkpoint_hash'].join('_')); } public getL2BlockHash(number: BlockNumber): Promise { @@ -34,6 +39,7 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo finalized: await this.getL2Tip('finalized'), proven: await this.getL2Tip('proven'), }, + checkpoint: await this.readCheckpoint(), }; } @@ -60,6 +66,9 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo await this.l2TipsStore.set('latest', blocks.at(-1)!.number); break; } + case 'checkpoint-added': + await this.saveCheckpoint(event.checkpoint); + break; case 'chain-pruned': await this.saveTag('latest', event.block); break; @@ -81,4 +90,25 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo await this.l2BlockHashesStore.set(block.number, block.hash); } } + + private async saveCheckpoint(checkpoint: CheckpointId) { + await Promise.all([ + this.l2CheckpointHashStore.set(checkpoint.blockHeadersHash), + this.l2CheckpointNumberStore.set(checkpoint.number), + ]); + } + + private async readCheckpoint(): Promise { + const [hash, checkpointNumber] = await Promise.all([ + this.l2CheckpointHashStore.getAsync(), + this.l2CheckpointNumberStore.getAsync(), + ]); + if (hash === undefined || checkpointNumber === undefined) { + return undefined; + } + return { + number: checkpointNumber, + blockHeadersHash: hash, + }; + } } diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts index 1f0268d490bb..243862e21ff2 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts @@ -129,6 +129,7 @@ export class L2BlockStream { } // Update the checkpointed tips + // TODO(pw/mbps): Not sure if this is the correct way of handling multiple checkpoints or if we should do each one in turn if ( localTips.checkpoint !== undefined && sourceTips.checkpoint !== undefined && From c87ab05346d3f04c3087ed85087b8c63069dc12d Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Mon, 22 Dec 2025 12:21:24 +0000 Subject: [PATCH 10/36] Comments --- yarn-project/kv-store/src/stores/l2_tips_store.ts | 4 +++- .../stdlib/src/block/l2_block_stream/l2_block_stream.ts | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/yarn-project/kv-store/src/stores/l2_tips_store.ts b/yarn-project/kv-store/src/stores/l2_tips_store.ts index 0f8906d989c8..9f94f73a2404 100644 --- a/yarn-project/kv-store/src/stores/l2_tips_store.ts +++ b/yarn-project/kv-store/src/stores/l2_tips_store.ts @@ -14,7 +14,9 @@ import type { AztecAsyncMap } from '../interfaces/map.js'; import type { AztecAsyncSingleton } from '../interfaces/singleton.js'; import type { AztecAsyncKVStore } from '../interfaces/store.js'; -/** Stores currently synced L2 tips and unfinalized block hashes. */ +/** Stores currently synced L2 tips and unfinalized block hashes. + * TODO (pw/mbps): I feel like this store would benefit from using transactions to ensure atomicy across the different stores. + */ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider { private readonly l2TipsStore: AztecAsyncMap; private readonly l2BlockHashesStore: AztecAsyncMap; diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts index 243862e21ff2..3f8607cda4b8 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts @@ -130,6 +130,7 @@ export class L2BlockStream { // Update the checkpointed tips // TODO(pw/mbps): Not sure if this is the correct way of handling multiple checkpoints or if we should do each one in turn + // This matches the updates to the proven chain. But I suspect we may need to process checkpoints in turn for things like the sentinel. if ( localTips.checkpoint !== undefined && sourceTips.checkpoint !== undefined && From a64d5f84c6ce901657baf895e0bad036e50a452e Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Wed, 24 Dec 2025 17:30:45 +0000 Subject: [PATCH 11/36] Fixes --- yarn-project/archiver/src/archiver/archiver.ts | 3 --- yarn-project/aztec-node/src/sentinel/sentinel.ts | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/yarn-project/archiver/src/archiver/archiver.ts b/yarn-project/archiver/src/archiver/archiver.ts index 8916e3158727..03a10a805ca7 100644 --- a/yarn-project/archiver/src/archiver/archiver.ts +++ b/yarn-project/archiver/src/archiver/archiver.ts @@ -583,9 +583,6 @@ export class Archiver ); const newBlocks = blockPromises.filter(isDefined).flat(); - // TODO(pw/mbps): Don't convert to legacy blocks here - //const blocks: L2Block[] = (await Promise.all(newBlocks.map(x => this.getBlock(x.number)))).filter(isDefined); - // Emit an event for listening services to react to the chain prune this.emit(L2BlockSourceEvents.L2PruneDetected, { type: L2BlockSourceEvents.L2PruneDetected, diff --git a/yarn-project/aztec-node/src/sentinel/sentinel.ts b/yarn-project/aztec-node/src/sentinel/sentinel.ts index 1e29458f41e0..6202eb6ab35f 100644 --- a/yarn-project/aztec-node/src/sentinel/sentinel.ts +++ b/yarn-project/aztec-node/src/sentinel/sentinel.ts @@ -100,7 +100,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme if (event.type !== 'checkpoint-added') { return; } - const checkpointNumber = CheckpointNumber(event.checkpoint.number); + const checkpointNumber = event.checkpoint.number; const [checkpoint] = await this.archiver.getPublishedCheckpoints(checkpointNumber, 1); if (!checkpoint) { this.logger.error(`Failed to get checkpoint ${checkpointNumber}`, { checkpoint }); From bb5cf3efd45678b958979637e72c06b4c79f5bf1 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Mon, 5 Jan 2026 17:06:51 +0000 Subject: [PATCH 12/36] Fixes --- .../archiver/src/archiver/archiver.ts | 4 +-- .../src/test/mock_l1_to_l2_message_source.ts | 17 +++++------ .../stdlib/src/block/l2_block_source.ts | 28 +++++++++++++------ 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/yarn-project/archiver/src/archiver/archiver.ts b/yarn-project/archiver/src/archiver/archiver.ts index 03a10a805ca7..135930b783fc 100644 --- a/yarn-project/archiver/src/archiver/archiver.ts +++ b/yarn-project/archiver/src/archiver/archiver.ts @@ -1573,8 +1573,8 @@ export class Archiver public async rollbackTo(targetL2BlockNumber: BlockNumber): Promise { // TODO(pw/mbps): This still assumes 1 block per checkpoint const currentBlocks = await this.getL2Tips(); - const currentL2Block = currentBlocks.blocks.latest.number; - const currentProvenBlock = currentBlocks.blocks.proven.number; + const currentL2Block = currentBlocks.proposed.number; + const currentProvenBlock = currentBlocks.proven.number; if (targetL2BlockNumber >= currentL2Block) { throw new Error(`Target L2 block ${targetL2BlockNumber} must be less than current L2 block ${currentL2Block}`); diff --git a/yarn-project/archiver/src/test/mock_l1_to_l2_message_source.ts b/yarn-project/archiver/src/test/mock_l1_to_l2_message_source.ts index 04ff1c16e8e7..a0a27063f23f 100644 --- a/yarn-project/archiver/src/test/mock_l1_to_l2_message_source.ts +++ b/yarn-project/archiver/src/test/mock_l1_to_l2_message_source.ts @@ -1,6 +1,6 @@ -import { BlockNumber, type CheckpointNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; -import type { L2Tips } from '@aztec/stdlib/block'; +import type { CheckpointId, L2BlockId, L2TipId, L2Tips } from '@aztec/stdlib/block'; import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging'; /** @@ -33,13 +33,14 @@ export class MockL1ToL2MessageSource implements L1ToL2MessageSource { getL2Tips(): Promise { const number = this.blockNumber; - const tip = { number: BlockNumber(number), hash: new Fr(number).toString() }; + const blockId: L2BlockId = { number: BlockNumber(number), hash: new Fr(number).toString() }; + const checkpointId: CheckpointId = { number: CheckpointNumber(number), blockHeadersHash: new Fr(number + 1).toString() }; + const tip: L2TipId = { block: blockId, checkpoint: checkpointId }; return Promise.resolve({ - blocks: { - latest: tip, - proven: tip, - finalized: tip, - }, + proposed: blockId, + checkpointed: tip, + proven: tip, + finalized: tip, }); } } diff --git a/yarn-project/stdlib/src/block/l2_block_source.ts b/yarn-project/stdlib/src/block/l2_block_source.ts index 8811c90f904b..305906c9be36 100644 --- a/yarn-project/stdlib/src/block/l2_block_source.ts +++ b/yarn-project/stdlib/src/block/l2_block_source.ts @@ -243,13 +243,20 @@ export interface L2BlockSourceEventEmitter extends L2BlockSource, ArchiverEmitte export type L2BlockTag = 'latest' | 'proven' | 'finalized'; /** Tips of the L2 chain. */ -export type L2Tips = { blocks: Record; checkpoint?: CheckpointId }; +export type L2Tips = { + proposed: L2BlockId; + checkpointed: L2TipId; + proven: L2TipId; + finalized: L2TipId; +}; /** Identifies a block by number and hash. */ export type L2BlockId = { number: BlockNumber; hash: string }; export type CheckpointId = { number: CheckpointNumber; blockHeadersHash: string }; +export type L2TipId = { block: L2BlockId, checkpoint: CheckpointId }; + /** Creates an L2 block id */ export function makeL2BlockId(number: BlockNumber, hash?: string): L2BlockId { if (number !== 0 && !hash) { @@ -263,18 +270,21 @@ const L2BlockIdSchema = z.object({ hash: z.string(), }); -const L2CheckpointSchema = z.object({ +const L2CheckpointIdSchema = z.object({ number: CheckpointNumberSchema, blockHeadersHash: z.string(), }); +const L2TipIdSchema = z.object({ + block: L2BlockIdSchema, + checkpoint: L2CheckpointIdSchema, +}); + export const L2TipsSchema = z.object({ - blocks: z.object({ - latest: L2BlockIdSchema, - proven: L2BlockIdSchema, - finalized: L2BlockIdSchema, - }), - checkpoint: L2CheckpointSchema.optional(), + proposed: L2BlockIdSchema, + checkpointed: L2TipIdSchema, + proven: L2TipIdSchema, + finalized: L2TipIdSchema, }); export enum L2BlockSourceEvents { @@ -299,7 +309,7 @@ export type L2BlockPruneEvent = { export type L2CheckpointEvent = { type: 'l2BlocksCheckpointed'; - checkpointNumber: CheckpointNumber; + checkpoint: Checkpoint; }; export type InvalidBlockDetectedEvent = { From e18fd60894c9e79a48d4996d9cbae4b82dfc2a75 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Mon, 5 Jan 2026 17:07:35 +0000 Subject: [PATCH 13/36] Fix --- yarn-project/stdlib/src/block/l2_block_source.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/stdlib/src/block/l2_block_source.ts b/yarn-project/stdlib/src/block/l2_block_source.ts index 305906c9be36..f7de584910b4 100644 --- a/yarn-project/stdlib/src/block/l2_block_source.ts +++ b/yarn-project/stdlib/src/block/l2_block_source.ts @@ -255,7 +255,7 @@ export type L2BlockId = { number: BlockNumber; hash: string }; export type CheckpointId = { number: CheckpointNumber; blockHeadersHash: string }; -export type L2TipId = { block: L2BlockId, checkpoint: CheckpointId }; +export type L2TipId = { block: L2BlockId; checkpoint: CheckpointId }; /** Creates an L2 block id */ export function makeL2BlockId(number: BlockNumber, hash?: string): L2BlockId { From 3054cb9309634e3fac58e54ebf7ca2f68bbd94c7 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Mon, 5 Jan 2026 17:16:35 +0000 Subject: [PATCH 14/36] Fix --- .../src/test/mock_l1_to_l2_message_source.ts | 5 ++- .../src/sequencer/sequencer.test.ts | 37 ------------------- 2 files changed, 4 insertions(+), 38 deletions(-) diff --git a/yarn-project/archiver/src/test/mock_l1_to_l2_message_source.ts b/yarn-project/archiver/src/test/mock_l1_to_l2_message_source.ts index a0a27063f23f..157ab51c060e 100644 --- a/yarn-project/archiver/src/test/mock_l1_to_l2_message_source.ts +++ b/yarn-project/archiver/src/test/mock_l1_to_l2_message_source.ts @@ -34,7 +34,10 @@ export class MockL1ToL2MessageSource implements L1ToL2MessageSource { getL2Tips(): Promise { const number = this.blockNumber; const blockId: L2BlockId = { number: BlockNumber(number), hash: new Fr(number).toString() }; - const checkpointId: CheckpointId = { number: CheckpointNumber(number), blockHeadersHash: new Fr(number + 1).toString() }; + const checkpointId: CheckpointId = { + number: CheckpointNumber(number), + blockHeadersHash: new Fr(number + 1).toString(), + }; const tip: L2TipId = { block: blockId, checkpoint: checkpointId }; return Promise.resolve({ proposed: blockId, diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index f91e849537c2..00b133505889 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -327,48 +327,11 @@ describe('sequencer', () => { expect(checkpointBuilder.buildBlockCalls).toHaveLength(0); }); -<<<<<<< HEAD - it('settles on the chain tip before it starts building a block', async () => { - // this test simulates a synch happening right after the sequencer starts building a block - // simulate every component being synched - const firstBlock = await L2Block.random(BlockNumber(1)); - const currentTip = firstBlock; - const syncedToL2Block = { number: currentTip.number, hash: (await currentTip.hash()).toString() }; - worldState.status.mockImplementation(() => - Promise.resolve({ - state: WorldStateRunningState.IDLE, - syncSummary: { - latestBlockNumber: syncedToL2Block.number, - latestBlockHash: syncedToL2Block.hash, - } as WorldStateSyncStatus, - }), - ); - p2p.getStatus.mockImplementation(() => Promise.resolve({ state: P2PClientState.IDLE, syncedToL2Block })); - l2BlockSource.getL2Tips.mockImplementation(() => - Promise.resolve({ - blocks: { - latest: syncedToL2Block, - proven: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, - finalized: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, - }, - }), - ); - l1ToL2MessageSource.getL2Tips.mockImplementation(() => - Promise.resolve({ - blocks: { - latest: syncedToL2Block, - proven: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, - finalized: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, - }, - }), - ); -======= it('builds a block only when enough txs are available', async () => { const txs: Tx[] = await timesParallel(4, i => makeTx(i * 0x10000)); sequencer.updateConfig({ minTxsPerBlock: 4 }); TestUtils.mockPendingTxs(p2p, txs); block = await makeBlock(txs); ->>>>>>> origin/next await sequencer.work(); From a8e2867ba78115099adf66792bf1e098304c8f83 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Tue, 6 Jan 2026 12:03:12 +0000 Subject: [PATCH 15/36] Updates to L2Tips --- .../archiver/src/archiver/archiver.ts | 128 ++++++++++++++---- .../src/test/mock_l1_to_l2_message_source.ts | 2 +- .../archiver/src/test/mock_l2_block_source.ts | 40 +++--- .../aztec-node/src/aztec-node/server.ts | 4 +- .../aztec-node/src/sentinel/sentinel.ts | 17 +-- .../end-to-end/src/e2e_epochs/epochs_test.ts | 2 +- .../e2e_l1_publisher/e2e_l1_publisher.test.ts | 8 +- .../end-to-end/src/e2e_snapshot_sync.test.ts | 2 +- .../kv-store/src/stores/l2_tips_store.ts | 37 ++--- .../src/actions/build-snapshot-metadata.ts | 4 +- .../mem_pools/tx_pool/tx_pool_bench.test.ts | 22 +-- .../prover-node/src/prover-node.test.ts | 20 +-- .../block_synchronizer.test.ts | 2 +- yarn-project/pxe/src/pxe.test.ts | 13 +- ..._private_logs_for_sender_recipient_pair.ts | 5 +- .../utils/get_status_change_of_pending.ts | 8 +- .../stdlib/src/block/l2_block_source.ts | 11 +- .../src/block/l2_block_stream/interfaces.ts | 7 +- .../l2_block_stream/l2_block_stream.test.ts | 27 ++-- .../block/l2_block_stream/l2_block_stream.ts | 77 ++++++----- .../stdlib/src/interfaces/archiver.test.ts | 29 ++-- .../stdlib/src/interfaces/aztec-node.test.ts | 28 ++-- .../stdlib/src/interfaces/aztec-node.ts | 5 +- .../stdlib/src/interfaces/prover-node.test.ts | 28 ++-- .../src/wrappers/l2_block_stream.ts | 2 +- .../txe/src/state_machine/archiver.ts | 4 - .../server_world_state_synchronizer.test.ts | 11 +- .../server_world_state_synchronizer.ts | 21 ++- .../world-state/src/test/integration.test.ts | 8 +- 29 files changed, 359 insertions(+), 213 deletions(-) diff --git a/yarn-project/archiver/src/archiver/archiver.ts b/yarn-project/archiver/src/archiver/archiver.ts index 4983cd893bae..403f2f918936 100644 --- a/yarn-project/archiver/src/archiver/archiver.ts +++ b/yarn-project/archiver/src/archiver/archiver.ts @@ -1,5 +1,5 @@ import type { BlobClientInterface } from '@aztec/blob-client/client'; -import { GENESIS_BLOCK_HEADER_HASH } from '@aztec/constants'; +import { GENESIS_BLOCK_HEADER_HASH, INITIAL_L2_BLOCK_NUM, INITIAL_L2_CHECKPOINT_NUM } from '@aztec/constants'; import { EpochCache } from '@aztec/epoch-cache'; import { createEthereumChain } from '@aztec/ethereum/chain'; import { BlockTagTooOldError, InboxContract, RollupContract } from '@aztec/ethereum/contracts'; @@ -1411,6 +1411,9 @@ export class Archiver getProvenBlockNumber(): Promise { return this.store.getProvenBlockNumber(); } + getCheckpointedBlockNumber(): Promise { + return this.store.getCheckpointedL2BlockNumber(); + } getCheckpointedBlockByArchive(archive: Fr): Promise { return this.store.getCheckpointedBlockByArchive(archive); } @@ -1521,9 +1524,10 @@ export class Archiver } async getL2Tips(): Promise { - const [latestBlockNumber, provenBlockNumber] = await Promise.all([ + const [latestBlockNumber, provenBlockNumber, checkpointedBlockNumber] = await Promise.all([ this.getBlockNumber(), this.getProvenBlockNumber(), + this.getCheckpointedBlockNumber(), ] as const); // TODO(#13569): Compute proper finalized block number based on L1 finalized block. @@ -1531,56 +1535,120 @@ export class Archiver // NOTE: update end-to-end/src/e2e_epochs/epochs_empty_blocks.test.ts as that uses finalized blocks in computations const finalizedBlockNumber = BlockNumber(Math.max(provenBlockNumber - this.l1constants.epochDuration * 2, 0)); - const [latestBlockHeader, provenBlockHeader, finalizedBlockHeader] = await Promise.all([ - latestBlockNumber > 0 ? this.getBlockHeader(latestBlockNumber) : undefined, - provenBlockNumber > 0 ? this.getBlockHeader(provenBlockNumber) : undefined, - finalizedBlockNumber > 0 ? this.getBlockHeader(finalizedBlockNumber) : undefined, - ] as const); + const beforeInitialblockNumber = BlockNumber(INITIAL_L2_BLOCK_NUM - 1); + + const [latestBlockHeader, provenCheckpointedBlock, finalizedCheckpointedBlock, checkpointedBlock] = + await Promise.all([ + latestBlockNumber > beforeInitialblockNumber ? this.getBlockHeader(latestBlockNumber) : undefined, + provenBlockNumber > beforeInitialblockNumber ? this.getCheckpointedBlock(provenBlockNumber) : undefined, + finalizedBlockNumber > beforeInitialblockNumber ? this.getCheckpointedBlock(finalizedBlockNumber) : undefined, + checkpointedBlockNumber > beforeInitialblockNumber + ? this.getCheckpointedBlock(checkpointedBlockNumber) + : undefined, + ] as const); - if (latestBlockNumber > 0 && !latestBlockHeader) { + const beforeInitialCheckpointNumber = CheckpointNumber(INITIAL_L2_CHECKPOINT_NUM - 1); + + if (latestBlockNumber > beforeInitialblockNumber && !latestBlockHeader) { throw new Error(`Failed to retrieve latest block header for block ${latestBlockNumber}`); } - if (provenBlockNumber > 0 && !provenBlockHeader) { + // Checkpointed blocks must exist for proven, finalized and checkpointed tips if they are beyond the initial block number. + if (checkpointedBlockNumber > beforeInitialblockNumber && !checkpointedBlock?.block.header) { + throw new Error( + `Failed to retrieve checkpointed block header for block ${checkpointedBlockNumber} (latest block is ${latestBlockNumber})`, + ); + } + + if (provenBlockNumber > beforeInitialblockNumber && !provenCheckpointedBlock?.block.header) { throw new Error( - `Failed to retrieve proven block header for block ${provenBlockNumber} (latest block is ${latestBlockNumber})`, + `Failed to retrieve proven checkpointed for block ${provenBlockNumber} (latest block is ${latestBlockNumber})`, ); } - if (finalizedBlockNumber > 0 && !finalizedBlockHeader) { + if (finalizedBlockNumber > beforeInitialblockNumber && !finalizedCheckpointedBlock?.block.header) { throw new Error( `Failed to retrieve finalized block header for block ${finalizedBlockNumber} (latest block is ${latestBlockNumber})`, ); } const latestBlockHeaderHash = (await latestBlockHeader?.hash()) ?? GENESIS_BLOCK_HEADER_HASH; - const provenBlockHeaderHash = (await provenBlockHeader?.hash()) ?? GENESIS_BLOCK_HEADER_HASH; - const finalizedBlockHeaderHash = (await finalizedBlockHeader?.hash()) ?? GENESIS_BLOCK_HEADER_HASH; - - const latestCheckpoint = await this.store.getCheckpointData(await this.store.getSynchedCheckpointNumber()); - const checkpointId: CheckpointId | undefined = - latestCheckpoint === undefined - ? undefined - : { - number: latestCheckpoint.checkpointNumber, - blockHeadersHash: latestCheckpoint.header.blockHeadersHash.toString(), - }; - - return { - blocks: { - latest: { number: latestBlockNumber, hash: latestBlockHeaderHash.toString() }, - proven: { number: provenBlockNumber, hash: provenBlockHeaderHash.toString() }, - finalized: { number: finalizedBlockNumber, hash: finalizedBlockHeaderHash.toString() }, + const provenBlockHeaderHash = (await provenCheckpointedBlock?.block.header?.hash()) ?? GENESIS_BLOCK_HEADER_HASH; + const finalizedBlockHeaderHash = + (await finalizedCheckpointedBlock?.block.header?.hash()) ?? GENESIS_BLOCK_HEADER_HASH; + const checkpointedBlockHeaderHash = (await checkpointedBlock?.block.header?.hash()) ?? GENESIS_BLOCK_HEADER_HASH; + + const [[provenBlockCheckpoint], [finalizedBlockCheckpoint], [checkpointedBlockCheckpoint]] = await Promise.all([ + provenCheckpointedBlock !== undefined + ? await this.getPublishedCheckpoints(provenCheckpointedBlock?.checkpointNumber, 1) + : [undefined], + finalizedCheckpointedBlock !== undefined + ? await this.getPublishedCheckpoints(finalizedCheckpointedBlock?.checkpointNumber, 1) + : [undefined], + checkpointedBlock !== undefined + ? await this.getPublishedCheckpoints(checkpointedBlock?.checkpointNumber, 1) + : [undefined], + ]); + + const initialcheckpointId = { + number: beforeInitialCheckpointNumber, + hash: '', + }; + + const l2Tips: L2Tips = { + proposed: { + number: latestBlockNumber, + hash: latestBlockHeaderHash.toString(), + }, + proven: { + block: { + number: provenBlockNumber, + hash: provenBlockHeaderHash.toString(), + }, + checkpoint: + provenBlockCheckpoint == undefined + ? initialcheckpointId + : { + number: provenBlockCheckpoint.checkpoint.number, + hash: provenBlockCheckpoint.checkpoint.hash().toString(), + }, + }, + finalized: { + block: { + number: finalizedBlockNumber, + hash: finalizedBlockHeaderHash.toString(), + }, + checkpoint: + finalizedBlockCheckpoint == undefined + ? initialcheckpointId + : { + number: finalizedBlockCheckpoint.checkpoint.number, + hash: finalizedBlockCheckpoint.checkpoint.hash().toString(), + }, + }, + checkpointed: { + block: { + number: checkpointedBlockNumber, + hash: checkpointedBlockHeaderHash.toString(), + }, + checkpoint: + checkpointedBlockCheckpoint == undefined + ? initialcheckpointId + : { + number: checkpointedBlockCheckpoint.checkpoint.number, + hash: checkpointedBlockCheckpoint.checkpoint.hash().toString(), + }, }, - checkpoint: checkpointId, }; + + return l2Tips; } public async rollbackTo(targetL2BlockNumber: BlockNumber): Promise { // TODO(pw/mbps): This still assumes 1 block per checkpoint const currentBlocks = await this.getL2Tips(); const currentL2Block = currentBlocks.proposed.number; - const currentProvenBlock = currentBlocks.proven.number; + const currentProvenBlock = currentBlocks.proven.block.number; if (targetL2BlockNumber >= currentL2Block) { throw new Error(`Target L2 block ${targetL2BlockNumber} must be less than current L2 block ${currentL2Block}`); diff --git a/yarn-project/archiver/src/test/mock_l1_to_l2_message_source.ts b/yarn-project/archiver/src/test/mock_l1_to_l2_message_source.ts index 157ab51c060e..f3b72a1c940f 100644 --- a/yarn-project/archiver/src/test/mock_l1_to_l2_message_source.ts +++ b/yarn-project/archiver/src/test/mock_l1_to_l2_message_source.ts @@ -36,7 +36,7 @@ export class MockL1ToL2MessageSource implements L1ToL2MessageSource { const blockId: L2BlockId = { number: BlockNumber(number), hash: new Fr(number).toString() }; const checkpointId: CheckpointId = { number: CheckpointNumber(number), - blockHeadersHash: new Fr(number + 1).toString(), + hash: new Fr(number + 1).toString(), }; const tip: L2TipId = { block: blockId, checkpoint: checkpointId }; return Promise.resolve({ diff --git a/yarn-project/archiver/src/test/mock_l2_block_source.ts b/yarn-project/archiver/src/test/mock_l2_block_source.ts index 0497fae2f3bf..2a30d0282784 100644 --- a/yarn-project/archiver/src/test/mock_l2_block_source.ts +++ b/yarn-project/archiver/src/test/mock_l2_block_source.ts @@ -151,10 +151,6 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { ); } - async getL2BlockNew(number: BlockNumber): Promise { - const block = await this.getBlock(number); - return block.toL2Block(); - } async getL2BlocksNew(from: BlockNumber, limit: number, proven?: boolean): Promise { const blocks = await this.getBlocks(from, limit, proven); return blocks.map(x => x.toL2Block()); @@ -282,21 +278,29 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { const provenBlock = this.l2Blocks[proven - 1]; const finalizedBlock = this.l2Blocks[finalized - 1]; + const latestBlockId = { + number: BlockNumber(latest), + hash: (await latestBlock?.hash())?.toString(), + }; + const provenBlockId = { + number: BlockNumber(proven), + hash: (await provenBlock?.hash())?.toString(), + }; + const finalizedBlockId = { + number: BlockNumber(finalized), + hash: (await finalizedBlock?.hash())?.toString(), + }; + + const makeTipId = (blockId: typeof latestBlockId) => ({ + block: blockId, + checkpoint: { number: CheckpointNumber(blockId.number), hash: blockId.hash }, + }); + return { - blocks: { - latest: { - number: BlockNumber(latest), - hash: (await latestBlock?.hash())?.toString(), - }, - proven: { - number: BlockNumber(proven), - hash: (await provenBlock?.hash())?.toString(), - }, - finalized: { - number: BlockNumber(finalized), - hash: (await finalizedBlock?.hash())?.toString(), - }, - }, + proposed: latestBlockId, + checkpointed: makeTipId(latestBlockId), + proven: makeTipId(provenBlockId), + finalized: makeTipId(finalizedBlockId), }; } diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index e2639842deb1..ec7edf550a80 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -1323,7 +1323,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { } // And it has an L2 block hash - const l2BlockHash = await archiver.getL2Tips().then(tips => tips.blocks.latest.hash); + const l2BlockHash = await archiver.getL2Tips().then(tips => tips.proposed.hash); if (!l2BlockHash) { this.metrics.recordSnapshotError(); throw new Error(`Archiver has no latest L2 block hash downloaded. Cannot start snapshot.`); @@ -1357,7 +1357,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { throw new Error('Archiver implementation does not support rollbacks.'); } - const finalizedBlock = await archiver.getL2Tips().then(tips => tips.blocks.finalized.number); + const finalizedBlock = await archiver.getL2Tips().then(tips => tips.finalized.block.number); if (targetBlock < finalizedBlock) { if (force) { this.log.warn(`Clearing world state database to allow rolling back behind finalized block ${finalizedBlock}`); diff --git a/yarn-project/aztec-node/src/sentinel/sentinel.ts b/yarn-project/aztec-node/src/sentinel/sentinel.ts index 6202eb6ab35f..324fb757efe6 100644 --- a/yarn-project/aztec-node/src/sentinel/sentinel.ts +++ b/yarn-project/aztec-node/src/sentinel/sentinel.ts @@ -89,7 +89,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise { await this.l2TipsStore.handleBlockStreamEvent(event); - if (event.type === 'checkpoint-added') { + if (event.type === 'chain-checkpointed') { await this.handleCheckpoint(event); } else if (event.type === 'chain-proven') { await this.handleChainProven(event); @@ -97,19 +97,14 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme } protected async handleCheckpoint(event: L2BlockStreamEvent) { - if (event.type !== 'checkpoint-added') { - return; - } - const checkpointNumber = event.checkpoint.number; - const [checkpoint] = await this.archiver.getPublishedCheckpoints(checkpointNumber, 1); - if (!checkpoint) { - this.logger.error(`Failed to get checkpoint ${checkpointNumber}`, { checkpoint }); + if (event.type !== 'chain-checkpointed') { return; } + const checkpoint = event.checkpoint; // Store mapping from slot to archive, block number, and attestors this.slotNumberToCheckpoint.set(checkpoint.checkpoint.header.slotNumber, { - checkpointNumber: CheckpointNumber(checkpointNumber), + checkpointNumber: checkpoint.checkpoint.number, archive: checkpoint.checkpoint.archive.root.toString(), attestors: getAttestationInfoFromPublishedCheckpoint(checkpoint) .filter(a => a.status === 'recovered-from-signature') @@ -305,8 +300,8 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme return false; } - const archiverLastBlockHash = await this.l2TipsStore.getL2Tips().then(tip => tip.blocks.latest.hash); - const p2pLastBlockHash = await this.p2p.getL2Tips().then(tips => tips.blocks.latest.hash); + const archiverLastBlockHash = await this.l2TipsStore.getL2Tips().then(tip => tip.proposed.hash); + const p2pLastBlockHash = await this.p2p.getL2Tips().then(tips => tips.proposed.hash); const isP2pSynced = archiverLastBlockHash === p2pLastBlockHash; if (!isP2pSynced) { this.logger.debug(`Waiting for P2P client to sync with archiver`, { archiverLastBlockHash, p2pLastBlockHash }); diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_test.ts index 3ab1e9b69549..11003ad31d60 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_test.ts @@ -348,7 +348,7 @@ export class EpochsTestContext { ]); this.logger.info(`Wait for node synch ${blockNumber} ${type}`, { blockNumber, type, syncState, tips }); if (type === 'proven') { - synched = tips.blocks.proven.number >= blockNumber && syncState.latestBlockNumber >= blockNumber; + synched = tips.proven.block.number >= blockNumber && syncState.latestBlockNumber >= blockNumber; } else if (type === 'finalized') { synched = syncState.finalizedBlockNumber >= blockNumber; } else { diff --git a/yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts b/yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts index 53663734974f..2737b67b01ad 100644 --- a/yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts +++ b/yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts @@ -220,11 +220,15 @@ describe('L1Publisher integration', () => { }, getL2Tips(): Promise { const latestBlock = blocks.at(-1); - const res = latestBlock + const blockId = latestBlock ? { number: latestBlock.number, hash: latestBlock.hash.toString() } : { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }; + const tipId = { + block: blockId, + checkpoint: { number: CheckpointNumber(blockId.number), hash: blockId.hash }, + }; - return Promise.resolve({ blocks: { latest: res, proven: res, finalized: res } }); + return Promise.resolve({ proposed: blockId, checkpointed: tipId, proven: tipId, finalized: tipId }); }, getBlockNumber(): Promise { return Promise.resolve(BlockNumber(blocks.at(-1)?.number ?? BlockNumber.ZERO)); diff --git a/yarn-project/end-to-end/src/e2e_snapshot_sync.test.ts b/yarn-project/end-to-end/src/e2e_snapshot_sync.test.ts index 0000ff5b9b37..cfc17ffbdefe 100644 --- a/yarn-project/end-to-end/src/e2e_snapshot_sync.test.ts +++ b/yarn-project/end-to-end/src/e2e_snapshot_sync.test.ts @@ -80,7 +80,7 @@ describe('e2e_snapshot_sync', () => { const expectNodeSyncedToL2Block = async (node: AztecNode | ProverNode, blockNumber: number) => { const tips = await node.getL2Tips(); - expect(tips.blocks.latest.number).toBeGreaterThanOrEqual(blockNumber); + expect(tips.proposed.number).toBeGreaterThanOrEqual(blockNumber); const worldState = await node.getWorldStateSyncStatus(); expect(worldState.latestBlockNumber).toBeGreaterThanOrEqual(blockNumber); }; diff --git a/yarn-project/kv-store/src/stores/l2_tips_store.ts b/yarn-project/kv-store/src/stores/l2_tips_store.ts index 9f94f73a2404..b94d8c3e6faf 100644 --- a/yarn-project/kv-store/src/stores/l2_tips_store.ts +++ b/yarn-project/kv-store/src/stores/l2_tips_store.ts @@ -1,4 +1,4 @@ -import { GENESIS_BLOCK_HEADER_HASH } from '@aztec/constants'; +import { GENESIS_BLOCK_HEADER_HASH, INITIAL_L2_BLOCK_NUM } from '@aztec/constants'; import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; import type { CheckpointId, @@ -7,8 +7,10 @@ import type { L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider, L2BlockTag, + L2TipId, L2Tips, } from '@aztec/stdlib/block'; +import type { Checkpoint } from '@aztec/stdlib/checkpoint'; import type { AztecAsyncMap } from '../interfaces/map.js'; import type { AztecAsyncSingleton } from '../interfaces/singleton.js'; @@ -36,19 +38,20 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo public async getL2Tips(): Promise { return { - blocks: { - latest: await this.getL2Tip('latest'), - finalized: await this.getL2Tip('finalized'), - proven: await this.getL2Tip('proven'), - }, - checkpoint: await this.readCheckpoint(), + proposed: await this.getL2Tip('proposed'), + checkpointed: await this.getL2Tip('checkpointed'), + proven: await this.getL2Tip('proven'), + finalized: await this.getL2Tip('finalized'), }; } - private async getL2Tip(tag: L2BlockTag): Promise { + private async getL2Tip(tag: L2BlockTag): Promise { const blockNumber = await this.l2TipsStore.getAsync(tag); - if (blockNumber === undefined || blockNumber === 0) { - return { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }; + if (blockNumber === undefined || blockNumber === INITIAL_L2_BLOCK_NUM - 1) { + return { + block: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + checkpoint: { number: CheckpointNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + }; } const blockHash = await this.l2BlockHashesStore.getAsync(blockNumber); if (!blockHash) { @@ -65,14 +68,14 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo for (const block of blocks) { await this.l2BlockHashesStore.set(block.number, (await block.hash()).toString()); } - await this.l2TipsStore.set('latest', blocks.at(-1)!.number); + await this.l2TipsStore.set('proposed', blocks.at(-1)!.number); break; } - case 'checkpoint-added': - await this.saveCheckpoint(event.checkpoint); + case 'chain-checkpointed': + await this.saveCheckpoint(event.checkpoint.checkpoint); break; case 'chain-pruned': - await this.saveTag('latest', event.block); + await this.saveTag('proposed', event.block); break; case 'chain-proven': await this.saveTag('proven', event.block); @@ -93,9 +96,9 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo } } - private async saveCheckpoint(checkpoint: CheckpointId) { + private async saveCheckpoint(checkpoint: Checkpoint) { await Promise.all([ - this.l2CheckpointHashStore.set(checkpoint.blockHeadersHash), + this.l2CheckpointHashStore.set(checkpoint.hash().toString()), this.l2CheckpointNumberStore.set(checkpoint.number), ]); } @@ -110,7 +113,7 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo } return { number: checkpointNumber, - blockHeadersHash: hash, + hash: hash, }; } } diff --git a/yarn-project/node-lib/src/actions/build-snapshot-metadata.ts b/yarn-project/node-lib/src/actions/build-snapshot-metadata.ts index 9fb91f34722f..3077e490e1de 100644 --- a/yarn-project/node-lib/src/actions/build-snapshot-metadata.ts +++ b/yarn-project/node-lib/src/actions/build-snapshot-metadata.ts @@ -7,13 +7,13 @@ export async function buildSnapshotMetadata( archiver: Archiver, config: UploadSnapshotConfig, ): Promise { - const [rollupAddress, l1BlockNumber, { blocks }] = await Promise.all([ + const [rollupAddress, l1BlockNumber, tips] = await Promise.all([ archiver.getRollupAddress(), archiver.getL1BlockNumber(), archiver.getL2Tips(), ] as const); - const { number: l2BlockNumber, hash: l2BlockHash } = blocks.latest; + const { number: l2BlockNumber, hash: l2BlockHash } = tips.proposed; if (!l2BlockHash) { throw new Error(`Failed to get L2 block hash from archiver.`); } diff --git a/yarn-project/p2p/src/mem_pools/tx_pool/tx_pool_bench.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool/tx_pool_bench.test.ts index 189c0457e719..7354551f663c 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool/tx_pool_bench.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool/tx_pool_bench.test.ts @@ -1,6 +1,6 @@ import { GENESIS_BLOCK_HEADER_HASH } from '@aztec/constants'; import { insertIntoSortedArray, shuffle } from '@aztec/foundation/array'; -import { BlockNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; import { timesAsync } from '@aztec/foundation/collection'; import { getDefaultConfig } from '@aztec/foundation/config'; import { Fr } from '@aztec/foundation/curves/bn254'; @@ -157,14 +157,18 @@ describe('TxPool: Benchmarks', () => { syncImmediate: () => Promise.resolve(), getProvenBlockNumber: () => Promise.resolve(BlockNumber.ZERO), getBlockNumber: () => Promise.resolve(BlockNumber.ZERO), - getL2Tips: () => - Promise.resolve({ - blocks: { - latest: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, - proven: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, - finalized: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, - }, - }), + getL2Tips: () => { + const tipId = { + block: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + checkpoint: { number: CheckpointNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + }; + return Promise.resolve({ + proposed: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + checkpointed: tipId, + proven: tipId, + finalized: tipId, + }); + }, }); wsSync = new ServerWorldStateSynchronizer(ws, l2, getDefaultConfig(worldStateConfigMappings)); await wsSync.start(); diff --git a/yarn-project/prover-node/src/prover-node.test.ts b/yarn-project/prover-node/src/prover-node.test.ts index 45142a49d84b..0fee621dc4fb 100644 --- a/yarn-project/prover-node/src/prover-node.test.ts +++ b/yarn-project/prover-node/src/prover-node.test.ts @@ -149,16 +149,20 @@ describe('prover-node', () => { l2BlockSource.getL1Constants.mockResolvedValue({ ...EmptyL1RollupConstants, l1GenesisTime: BigInt(l1GenesisTime) }); l2BlockSource.getCheckpointsForEpoch.mockResolvedValue(checkpoints); l2BlockSource.getPublishedCheckpoints.mockResolvedValue([lastPublishedCheckpoint]); + const latestBlockNumber = BlockNumber.fromCheckpointNumber(checkpoints.at(-1)!.number); + const latestHash = checkpoints.at(-1)!.hash().toString(); + const genesisTipId = { + block: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + checkpoint: { number: CheckpointNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + }; l2BlockSource.getL2Tips.mockResolvedValue({ - blocks: { - latest: { - number: BlockNumber.fromCheckpointNumber(checkpoints.at(-1)!.number), - // TODO: This should be the actual block hash - hash: checkpoints.at(-1)!.hash().toString(), - }, - proven: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, - finalized: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + proposed: { number: latestBlockNumber, hash: latestHash }, + checkpointed: { + block: { number: latestBlockNumber, hash: latestHash }, + checkpoint: { number: checkpoints.at(-1)!.number, hash: latestHash }, }, + proven: genesisTipId, + finalized: genesisTipId, }); l2BlockSource.getBlockHeader.mockImplementation(number => Promise.resolve(number === checkpoints[0].blocks[0].number - 1 ? previousBlockHeader : undefined), diff --git a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts index 6fdef45e2043..02ad3a98866c 100644 --- a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts +++ b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts @@ -2,7 +2,7 @@ import { BlockNumber } from '@aztec/foundation/branded-types'; import { timesParallel } from '@aztec/foundation/collection'; import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import { L2TipsKVStore } from '@aztec/kv-store/stores'; -import { L2BlockNew, type L2BlockStream } from '@aztec/stdlib/block'; +import { L2Block, L2BlockNew, type L2BlockStream } from '@aztec/stdlib/block'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; import { jest } from '@jest/globals'; diff --git a/yarn-project/pxe/src/pxe.test.ts b/yarn-project/pxe/src/pxe.test.ts index a4a6f9ce99f8..9bd9c33e73c8 100644 --- a/yarn-project/pxe/src/pxe.test.ts +++ b/yarn-project/pxe/src/pxe.test.ts @@ -1,7 +1,7 @@ import { BBBundlePrivateKernelProver } from '@aztec/bb-prover/client/bundle'; import { GENESIS_BLOCK_HEADER_HASH } from '@aztec/constants'; import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses'; -import { BlockNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; import { omit } from '@aztec/foundation/collection'; import { Fr } from '@aztec/foundation/curves/bn254'; import { EthAddress } from '@aztec/foundation/eth-address'; @@ -177,10 +177,15 @@ describe('PXE', () => { node.getBlockHeader.mockResolvedValue(blockHeader); // Mock getL2Tips which is needed for syncing tagged logs + const tipId = { + block: { number: lastKnownBlockNumber, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + checkpoint: { number: CheckpointNumber(lastKnownBlockNumber), hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + }; node.getL2Tips.mockResolvedValue({ - latest: { number: lastKnownBlockNumber, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, - proven: { number: lastKnownBlockNumber, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, - finalized: { number: lastKnownBlockNumber, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + proposed: { number: lastKnownBlockNumber, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + checkpointed: tipId, + proven: tipId, + finalized: tipId, }); // This is read when PXE tries to resolve the diff --git a/yarn-project/pxe/src/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.ts b/yarn-project/pxe/src/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.ts index d25f68b27075..8bb2e89271dd 100644 --- a/yarn-project/pxe/src/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.ts +++ b/yarn-project/pxe/src/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.ts @@ -68,7 +68,10 @@ export async function loadPrivateLogsForSenderRecipientPair( throw new Error('Node failed to return latest block header when syncing logs'); } - [finalizedBlockNumber, currentTimestamp] = [l2Tips.finalized.number, latestBlockHeader.globalVariables.timestamp]; + [finalizedBlockNumber, currentTimestamp] = [ + l2Tips.finalized.block.number, + latestBlockHeader.globalVariables.timestamp, + ]; } let start: number, end: number; diff --git a/yarn-project/pxe/src/tagging/sender_sync/utils/get_status_change_of_pending.ts b/yarn-project/pxe/src/tagging/sender_sync/utils/get_status_change_of_pending.ts index 9511b96ec356..d16d8069fa1c 100644 --- a/yarn-project/pxe/src/tagging/sender_sync/utils/get_status_change_of_pending.ts +++ b/yarn-project/pxe/src/tagging/sender_sync/utils/get_status_change_of_pending.ts @@ -10,7 +10,7 @@ export async function getStatusChangeOfPending( aztecNode: AztecNode, ): Promise<{ txHashesToFinalize: TxHash[]; txHashesToDrop: TxHash[] }> { // Get receipts for all pending tx hashes and the finalized block number. - const [receipts, { blocks }] = await Promise.all([ + const [receipts, tips] = await Promise.all([ Promise.all(pending.map(pendingTxHash => aztecNode.getTxReceipt(pendingTxHash))), aztecNode.getL2Tips(), ]); @@ -22,7 +22,11 @@ export async function getStatusChangeOfPending( const receipt = receipts[i]; const txHash = pending[i]; - if (receipt.status === TxStatus.SUCCESS && receipt.blockNumber && receipt.blockNumber <= blocks.finalized.number) { + if ( + receipt.status === TxStatus.SUCCESS && + receipt.blockNumber && + receipt.blockNumber <= tips.finalized.block.number + ) { // Tx has been included in a block and the corresponding block is finalized --> we mark the indexes as // finalized. txHashesToFinalize.push(txHash); diff --git a/yarn-project/stdlib/src/block/l2_block_source.ts b/yarn-project/stdlib/src/block/l2_block_source.ts index 670c4d23bdb0..4c3d5a357949 100644 --- a/yarn-project/stdlib/src/block/l2_block_source.ts +++ b/yarn-project/stdlib/src/block/l2_block_source.ts @@ -244,11 +244,12 @@ export interface L2BlockSourceEventEmitter extends L2BlockSource, ArchiverEmitte /** * Identifier for L2 block tags. - * - latest: Latest block pushed to L1. + * - proposed: Latest block proposed on L2. + * - checkpointed: Checkpointed block on L1. * - proven: Proven block on L1. * - finalized: Proven block on a finalized L1 block (not implemented, set to proven for now). */ -export type L2BlockTag = 'latest' | 'proven' | 'finalized'; +export type L2BlockTag = 'proposed' | 'checkpointed' | 'proven' | 'finalized'; /** Tips of the L2 chain. */ export type L2Tips = { @@ -261,7 +262,7 @@ export type L2Tips = { /** Identifies a block by number and hash. */ export type L2BlockId = { number: BlockNumber; hash: string }; -export type CheckpointId = { number: CheckpointNumber; blockHeadersHash: string }; +export type CheckpointId = { number: CheckpointNumber; hash: string }; export type L2TipId = { block: L2BlockId; checkpoint: CheckpointId }; @@ -280,7 +281,7 @@ const L2BlockIdSchema = z.object({ const L2CheckpointIdSchema = z.object({ number: CheckpointNumberSchema, - blockHeadersHash: z.string(), + hash: z.string(), }); const L2TipIdSchema = z.object({ @@ -317,7 +318,7 @@ export type L2BlockPruneEvent = { export type L2CheckpointEvent = { type: 'l2BlocksCheckpointed'; - checkpoint: Checkpoint; + checkpoint: PublishedCheckpoint; }; export type InvalidBlockDetectedEvent = { diff --git a/yarn-project/stdlib/src/block/l2_block_stream/interfaces.ts b/yarn-project/stdlib/src/block/l2_block_stream/interfaces.ts index 99c4e907b9e5..9d23f0376c44 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/interfaces.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/interfaces.ts @@ -1,5 +1,6 @@ +import type { PublishedCheckpoint } from '../../checkpoint/published_checkpoint.js'; import type { L2BlockNew } from '../l2_block_new.js'; -import type { CheckpointId, L2BlockId, L2Tips } from '../l2_block_source.js'; +import type { L2BlockId, L2Tips } from '../l2_block_source.js'; /** Interface to the local view of the chain. Implemented by world-state and l2-tips-store. */ export interface L2BlockStreamLocalDataProvider { @@ -18,8 +19,8 @@ export type L2BlockStreamEvent = blocks: L2BlockNew[]; } | /** Emits checkpoints published to L1. */ { - type: 'checkpoint-added'; - checkpoint: CheckpointId; + type: 'chain-checkpointed'; + checkpoint: PublishedCheckpoint; } | /** Reports last correct block (new tip of the unproven chain). */ { type: 'chain-pruned'; diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts index d9d6491052cb..dfdef18c3d03 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts @@ -30,17 +30,21 @@ describe('L2BlockStream', () => { const makeBlockId = (number: number): L2BlockId => ({ number: BlockNumber(number), hash: makeHash(number) }); + const makeTipId = (number: number) => ({ + block: { number: BlockNumber(number), hash: makeHash(number) }, + checkpoint: { number: CheckpointNumber(number), hash: makeHash(number) }, + }); + const setRemoteTips = (latest_: number, proven?: number, finalized?: number) => { proven = proven ?? 0; finalized = finalized ?? 0; latest = latest_; blockSource.getL2Tips.mockResolvedValue({ - blocks: { - latest: { number: BlockNumber(latest), hash: makeHash(latest) }, - proven: { number: BlockNumber(proven), hash: makeHash(proven) }, - finalized: { number: BlockNumber(finalized), hash: makeHash(finalized) }, - }, + proposed: { number: BlockNumber(latest), hash: makeHash(latest) }, + checkpointed: makeTipId(latest), + proven: makeTipId(proven), + finalized: makeTipId(finalized), }); }; @@ -281,12 +285,15 @@ class TestL2BlockStreamLocalDataProvider implements L2BlockStreamLocalDataProvid } public getL2Tips(): Promise { + const makeTipId = (blockId: L2BlockId) => ({ + block: blockId, + checkpoint: { number: CheckpointNumber(blockId.number), hash: blockId.hash }, + }); return Promise.resolve({ - blocks: { - latest: this.latest, - proven: this.proven, - finalized: this.finalized, - }, + proposed: this.latest, + checkpointed: makeTipId(this.latest), + proven: makeTipId(this.proven), + finalized: makeTipId(this.finalized), }); } } diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts index 3f8607cda4b8..f76d00365aaa 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts @@ -13,7 +13,10 @@ export class L2BlockStream { private hasStarted = false; constructor( - private l2BlockSource: Pick, + private l2BlockSource: Pick< + L2BlockSource, + 'getL2BlocksNew' | 'getBlockHeader' | 'getL2Tips' | 'getPublishedCheckpoints' + >, private localData: L2BlockStreamLocalDataProvider, private handler: L2BlockStreamEventHandler, private readonly log = createLogger('types:block_stream'), @@ -62,35 +65,35 @@ export class L2BlockStream { const sourceTips = await this.l2BlockSource.getL2Tips(); const localTips = await this.localData.getL2Tips(); this.log.trace(`Running L2 block stream`, { - sourceLatest: sourceTips.blocks.latest.number, - localLatest: localTips.blocks.latest.number, - sourceFinalized: sourceTips.blocks.finalized.number, - localFinalized: localTips.blocks.finalized.number, - sourceProven: sourceTips.blocks.proven.number, - localProven: localTips.blocks.proven.number, - sourceLatestHash: sourceTips.blocks.latest.hash, - localLatestHash: localTips.blocks.latest.hash, - sourceProvenHash: sourceTips.blocks.proven.hash, - localProvenHash: localTips.blocks.proven.hash, - sourceFinalizedHash: sourceTips.blocks.finalized.hash, - localFinalizedHash: localTips.blocks.finalized.hash, + sourceLatest: sourceTips.proposed.number, + localLatest: localTips.proposed.number, + sourceFinalized: sourceTips.finalized.block.number, + localFinalized: localTips.finalized.block.number, + sourceProven: sourceTips.proven.block.number, + localProven: localTips.proven.block.number, + sourceLatestHash: sourceTips.proposed.hash, + localLatestHash: localTips.proposed.hash, + sourceProvenHash: sourceTips.proven.block.hash, + localProvenHash: localTips.proven.block.hash, + sourceFinalizedHash: sourceTips.finalized.block.hash, + localFinalizedHash: localTips.finalized.block.hash, }); // Check if there was a reorg and emit a chain-pruned event if so. - let latestBlockNumber = localTips.blocks.latest.number; - const sourceCache = new BlockHashCache([sourceTips.blocks.latest]); + let latestBlockNumber = localTips.proposed.number; + const sourceCache = new BlockHashCache([sourceTips.proposed]); while (!(await this.areBlockHashesEqualAt(latestBlockNumber, { sourceCache }))) { latestBlockNumber--; } - if (latestBlockNumber < localTips.blocks.latest.number) { - latestBlockNumber = BlockNumber(Math.min(latestBlockNumber, sourceTips.blocks.latest.number)); // see #13471 + if (latestBlockNumber < localTips.proposed.number) { + latestBlockNumber = BlockNumber(Math.min(latestBlockNumber, sourceTips.proposed.number)); // see #13471 const hash = sourceCache.get(latestBlockNumber) ?? (await this.getBlockHashFromSource(latestBlockNumber)); if (latestBlockNumber !== 0 && !hash) { throw new Error(`Block hash not found in block source for block number ${latestBlockNumber}`); } this.log.verbose( - `Reorg detected. Pruning blocks from ${latestBlockNumber + 1} to ${localTips.blocks.latest.number}.`, + `Reorg detected. Pruning blocks from ${latestBlockNumber + 1} to ${localTips.proposed.number}.`, ); await this.emitEvent({ type: 'chain-pruned', block: makeL2BlockId(latestBlockNumber, hash) }); } @@ -113,12 +116,12 @@ export class L2BlockStream { // last finalized block however in order to guarantee that we will eventually find a block in which our local // store matches the source. // If the last finalized block is behind our local tip, there is nothing to skip. - nextBlockNumber = Math.max(sourceTips.blocks.finalized.number, nextBlockNumber); + nextBlockNumber = Math.max(sourceTips.finalized.block.number, nextBlockNumber); } // Request new blocks from the source. - while (nextBlockNumber <= sourceTips.blocks.latest.number) { - const limit = Math.min(this.opts.batchSize ?? 50, sourceTips.blocks.latest.number - nextBlockNumber + 1); + while (nextBlockNumber <= sourceTips.proposed.number) { + const limit = Math.min(this.opts.batchSize ?? 50, sourceTips.proposed.number - nextBlockNumber + 1); this.log.trace(`Requesting blocks from ${nextBlockNumber} limit ${limit} proven=${this.opts.proven}`); const blocks = await this.l2BlockSource.getL2BlocksNew(BlockNumber(nextBlockNumber), limit, this.opts.proven); if (blocks.length === 0) { @@ -132,28 +135,34 @@ export class L2BlockStream { // TODO(pw/mbps): Not sure if this is the correct way of handling multiple checkpoints or if we should do each one in turn // This matches the updates to the proven chain. But I suspect we may need to process checkpoints in turn for things like the sentinel. if ( - localTips.checkpoint !== undefined && - sourceTips.checkpoint !== undefined && - localTips.checkpoint.number !== sourceTips.checkpoint.number + localTips.checkpointed !== undefined && + sourceTips.checkpointed !== undefined && + localTips.checkpointed.block.number !== sourceTips.checkpointed.block.number ) { + const checkpoints = await this.l2BlockSource.getPublishedCheckpoints( + sourceTips.checkpointed.checkpoint.number, + 1, + ); + if (checkpoints.length === 0) { + throw new Error( + `Failed to retrieve checkpoint ${sourceTips.checkpointed.checkpoint.number} from source for checkpointed tip update`, + ); + } await this.emitEvent({ - type: 'checkpoint-added', - checkpoint: sourceTips.checkpoint, + type: 'chain-checkpointed', + checkpoint: checkpoints[0], }); } // Update the proven and finalized tips. - if (localTips.blocks.proven !== undefined && sourceTips.blocks.proven.number !== localTips.blocks.proven.number) { + if (localTips.proven !== undefined && sourceTips.proven.block.number !== localTips.proven.block.number) { await this.emitEvent({ type: 'chain-proven', - block: sourceTips.blocks.proven, + block: sourceTips.proven.block, }); } - if ( - localTips.blocks.finalized !== undefined && - sourceTips.blocks.finalized.number !== localTips.blocks.finalized.number - ) { - await this.emitEvent({ type: 'chain-finalized', block: sourceTips.blocks.finalized }); + if (localTips.finalized !== undefined && sourceTips.finalized.block.number !== localTips.finalized.block.number) { + await this.emitEvent({ type: 'chain-finalized', block: sourceTips.finalized.block }); } } catch (err: any) { if (err.name === 'AbortError') { @@ -201,7 +210,7 @@ export class L2BlockStream { private async emitEvent(event: L2BlockStreamEvent) { this.log.debug( - `Emitting ${event.type} (${event.type === 'blocks-added' ? event.blocks.length : event.type === 'checkpoint-added' ? event.checkpoint.number : event.block.number})`, + `Emitting ${event.type} (${event.type === 'blocks-added' ? event.blocks.length : event.type === 'chain-checkpointed' ? event.checkpoint.checkpoint.number : event.block.number})`, ); await this.handler.handleBlockStreamEvent(event); if (!this.isRunning() && !this.isSyncing) { diff --git a/yarn-project/stdlib/src/interfaces/archiver.test.ts b/yarn-project/stdlib/src/interfaces/archiver.test.ts index bb834e5e3c6b..cd163d9b0816 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.test.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.test.ts @@ -198,12 +198,15 @@ describe('ArchiverApiSchema', () => { it('getL2Tips', async () => { const result = await context.client.getL2Tips(); + const expectedTipId = { + block: { number: 1, hash: `0x01` }, + checkpoint: { number: 1, hash: `0x01` }, + }; expect(result).toEqual({ - blocks: { - latest: { number: 1, hash: `0x01` }, - proven: { number: 1, hash: `0x01` }, - finalized: { number: 1, hash: `0x01` }, - }, + proposed: { number: 1, hash: `0x01` }, + checkpointed: expectedTipId, + proven: expectedTipId, + finalized: expectedTipId, }); }); @@ -401,9 +404,6 @@ class MockArchiver implements ArchiverApi { ]; } - getL2BlockNew(number: BlockNumber): Promise { - return L2BlockNew.random(BlockNumber(number)); - } async getL2BlocksNew(from: BlockNumber, _1: number, _2?: boolean): Promise { const block = await L2BlockNew.random(from); return [block]; @@ -469,12 +469,15 @@ class MockArchiver implements ArchiverApi { return Promise.resolve(true); } getL2Tips(): Promise { + const tipId = { + block: { number: BlockNumber(1), hash: `0x01` }, + checkpoint: { number: CheckpointNumber(1), hash: `0x01` }, + }; return Promise.resolve({ - blocks: { - latest: { number: BlockNumber(1), hash: `0x01` }, - proven: { number: BlockNumber(1), hash: `0x01` }, - finalized: { number: BlockNumber(1), hash: `0x01` }, - }, + proposed: { number: BlockNumber(1), hash: `0x01` }, + checkpointed: tipId, + proven: tipId, + finalized: tipId, }); } getL2BlockHash(blockNumber: BlockNumber): Promise { diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts index 2baaea976eb5..b177d93b8399 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts @@ -6,7 +6,7 @@ import { PUBLIC_DATA_TREE_HEIGHT, } from '@aztec/constants'; import { type L1ContractAddresses, L1ContractsNames } from '@aztec/ethereum/l1-contract-addresses'; -import { BlockNumber, SlotNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types'; import { Buffer32 } from '@aztec/foundation/buffer'; import { timesAsync } from '@aztec/foundation/collection'; import { randomInt } from '@aztec/foundation/crypto/random'; @@ -93,12 +93,15 @@ describe('AztecNodeApiSchema', () => { it('getL2Tips', async () => { const result = await context.client.getL2Tips(); + const expectedTipId = { + block: { number: 1, hash: `0x01` }, + checkpoint: { number: 1, hash: `0x01` }, + }; expect(result).toEqual({ - blocks: { - latest: { number: 1, hash: `0x01` }, - proven: { number: 1, hash: `0x01` }, - finalized: { number: 1, hash: `0x01` }, - }, + proposed: { number: 1, hash: `0x01` }, + checkpointed: expectedTipId, + proven: expectedTipId, + finalized: expectedTipId, }); }); @@ -542,12 +545,15 @@ class MockAztecNode implements AztecNode { } getL2Tips(): Promise { + const tipId = { + block: { number: BlockNumber(1), hash: `0x01` }, + checkpoint: { number: CheckpointNumber(1), hash: `0x01` }, + }; return Promise.resolve({ - blocks: { - latest: { number: BlockNumber(1), hash: `0x01` }, - proven: { number: BlockNumber(1), hash: `0x01` }, - finalized: { number: BlockNumber(1), hash: `0x01` }, - }, + proposed: { number: BlockNumber(1), hash: `0x01` }, + checkpointed: tipId, + proven: tipId, + finalized: tipId, }); } diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.ts b/yarn-project/stdlib/src/interfaces/aztec-node.ts index b136c2a6f29a..22b1501be793 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.ts @@ -72,7 +72,10 @@ import { type WorldStateSyncStatus, WorldStateSyncStatusSchema } from './world_s * We will probably implement the additional interfaces by means other than Aztec Node as it's currently a privacy leak */ export interface AztecNode - extends Pick { + extends Pick< + L2BlockSource, + 'getBlocks' | 'getL2BlocksNew' | 'getPublishedBlocks' | 'getPublishedCheckpoints' | 'getBlockHeader' | 'getL2Tips' + > { /** * Returns the tips of the L2 chain. */ diff --git a/yarn-project/stdlib/src/interfaces/prover-node.test.ts b/yarn-project/stdlib/src/interfaces/prover-node.test.ts index e2105f9095f5..884d5110cb7d 100644 --- a/yarn-project/stdlib/src/interfaces/prover-node.test.ts +++ b/yarn-project/stdlib/src/interfaces/prover-node.test.ts @@ -1,4 +1,4 @@ -import { BlockNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; import { type JsonRpcTestContext, createJsonRpcTestSetup } from '@aztec/foundation/json-rpc/test'; import type { L2Tips } from '../block/l2_block_source.js'; @@ -38,12 +38,15 @@ describe('ProvingNodeApiSchema', () => { it('getL2Tips', async () => { const result = await context.client.getL2Tips(); + const expectedTipId = { + block: { number: 1, hash: `0x01` }, + checkpoint: { number: 1, hash: `0x01` }, + }; expect(result).toEqual({ - blocks: { - latest: { number: 1, hash: `0x01` }, - proven: { number: 1, hash: `0x01` }, - finalized: { number: 1, hash: `0x01` }, - }, + proposed: { number: 1, hash: `0x01` }, + checkpointed: expectedTipId, + proven: expectedTipId, + finalized: expectedTipId, }); }); @@ -65,12 +68,15 @@ class MockProverNode implements ProverNodeApi { } getL2Tips(): Promise { + const tipId = { + block: { number: BlockNumber(1), hash: `0x01` }, + checkpoint: { number: CheckpointNumber(1), hash: `0x01` }, + }; return Promise.resolve({ - blocks: { - latest: { number: BlockNumber(1), hash: `0x01` }, - proven: { number: BlockNumber(1), hash: `0x01` }, - finalized: { number: BlockNumber(1), hash: `0x01` }, - }, + proposed: { number: BlockNumber(1), hash: `0x01` }, + checkpointed: tipId, + proven: tipId, + finalized: tipId, }); } diff --git a/yarn-project/telemetry-client/src/wrappers/l2_block_stream.ts b/yarn-project/telemetry-client/src/wrappers/l2_block_stream.ts index 2cd4fc2843d8..39e8cb015a5a 100644 --- a/yarn-project/telemetry-client/src/wrappers/l2_block_stream.ts +++ b/yarn-project/telemetry-client/src/wrappers/l2_block_stream.ts @@ -11,7 +11,7 @@ import { type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client' /** Extends an L2BlockStream with a tracer to create a new trace per iteration. */ export class TraceableL2BlockStream extends L2BlockStream implements Traceable { constructor( - l2BlockSource: Pick, + l2BlockSource: Pick, localData: L2BlockStreamLocalDataProvider, handler: L2BlockStreamEventHandler, public readonly tracer: Tracer, diff --git a/yarn-project/txe/src/state_machine/archiver.ts b/yarn-project/txe/src/state_machine/archiver.ts index f1a1585db647..b5e6f0e574bf 100644 --- a/yarn-project/txe/src/state_machine/archiver.ts +++ b/yarn-project/txe/src/state_machine/archiver.ts @@ -93,10 +93,6 @@ export class TXEArchiver extends ArchiverStoreHelper implements L2BlockSource { return this.retrievePublishedBlocks(from, limit, proven); } - getL2BlockNew(number: BlockNumber): Promise { - return this.store.getBlock(number); - } - async getL2BlocksNew(from: BlockNumber, limit: number, proven?: boolean): Promise { const blocks = await this.store.getBlocks(from, limit); diff --git a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.test.ts b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.test.ts index 0fbc5f1b1db8..89f22f108069 100644 --- a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.test.ts +++ b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.test.ts @@ -262,6 +262,15 @@ class TestWorldStateSynchronizer extends ServerWorldStateSynchronizer { } public override getL2Tips() { - return Promise.resolve({ blocks: { latest: this.latest, proven: this.proven, finalized: this.finalized } }); + const makeTipId = (blockId: typeof this.latest) => ({ + block: blockId, + checkpoint: { number: CheckpointNumber(blockId.number), hash: blockId.hash }, + }); + return Promise.resolve({ + proposed: this.latest, + checkpointed: makeTipId(this.latest), + proven: makeTipId(this.proven), + finalized: makeTipId(this.finalized), + }); } } diff --git a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts index 37220b31e634..78d08ae4c166 100644 --- a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts +++ b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts @@ -1,3 +1,4 @@ +import { INITIAL_L2_BLOCK_NUM, INITIAL_L2_CHECKPOINT_NUM } from '@aztec/constants'; import { BlockNumber } from '@aztec/foundation/branded-types'; import type { Fr } from '@aztec/foundation/curves/bn254'; import { type Logger, createLogger } from '@aztec/foundation/log'; @@ -25,6 +26,8 @@ import type { L2BlockHandledStats } from '@aztec/stdlib/stats'; import { MerkleTreeId, type MerkleTreeReadOperations, type MerkleTreeWriteOperations } from '@aztec/stdlib/trees'; import { TraceableL2BlockStream, getTelemetryClient } from '@aztec/telemetry-client'; +import { check, number } from 'zod/v4'; + import { WorldStateInstrumentation } from '../instrumentation/instrumentation.js'; import type { WorldStateStatusFull } from '../native/message.js'; import type { MerkleTreeAdminDatabase } from '../world-state-db/merkle_tree_db.js'; @@ -160,7 +163,7 @@ export class ServerWorldStateSynchronizer } public async getLatestBlockNumber() { - return (await this.getL2Tips()).blocks.latest.number; + return (await this.getL2Tips()).proposed.number; } public async stopSync() { @@ -257,11 +260,19 @@ export class ServerWorldStateSynchronizer const latestBlockId: L2BlockId = { number: status.unfinalizedBlockNumber, hash: unfinalizedBlockHash! }; return { - blocks: { - latest: latestBlockId, - finalized: { number: status.finalizedBlockNumber, hash: '' }, - proven: { number: this.provenBlockNumber ?? status.finalizedBlockNumber, hash: '' }, // TODO(palla/reorg): Using finalized as proven for now + proposed: latestBlockId, + checkpointed: { + block: { number: INITIAL_L2_BLOCK_NUM, hash: '' }, + checkpoint: { number: INITIAL_L2_CHECKPOINT_NUM, hash: '' }, + }, + finalized: { + block: { number: status.finalizedBlockNumber, hash: '' }, + checkpoint: { number: INITIAL_L2_CHECKPOINT_NUM, hash: '' }, }, + proven: { + block: { number: this.provenBlockNumber ?? status.finalizedBlockNumber, hash: '' }, + checkpoint: { number: INITIAL_L2_CHECKPOINT_NUM, hash: '' }, + }, // TODO(palla/reorg): Using finalized as proven for now }; } diff --git a/yarn-project/world-state/src/test/integration.test.ts b/yarn-project/world-state/src/test/integration.test.ts index 260d6715cd4b..72c322421432 100644 --- a/yarn-project/world-state/src/test/integration.test.ts +++ b/yarn-project/world-state/src/test/integration.test.ts @@ -80,13 +80,13 @@ describe('world-state integration', () => { return finalized > tipFinalized; }; - while (tips.blocks.latest.number < blockToSyncTo && sleepTime < maxTimeoutMS) { + while (tips.proposed.number < blockToSyncTo && sleepTime < maxTimeoutMS) { await sleep(100); sleepTime = Date.now() - startTime; tips = await synchronizer.getL2Tips(); } - while (waitForFinalized(tips.blocks.finalized.number) && sleepTime < maxTimeoutMS) { + while (waitForFinalized(tips.finalized.block.number) && sleepTime < maxTimeoutMS) { await sleep(100); sleepTime = Date.now() - startTime; tips = await synchronizer.getL2Tips(); @@ -101,11 +101,11 @@ describe('world-state integration', () => { const expectSynchedToBlock = async (latest: number, finalized?: number) => { const tips = await synchronizer.getL2Tips(); - expect(tips.blocks.latest.number).toEqual(latest); + expect(tips.proposed.number).toEqual(latest); await expectSynchedBlockHashMatches(latest); if (finalized !== undefined) { - expect(tips.blocks.finalized.number).toEqual(finalized); + expect(tips.finalized.block.number).toEqual(finalized); await expectSynchedBlockHashMatches(finalized); } }; From 533c9a67cf85afbc0c9f132c2a84dc10cef9ae9e Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Tue, 6 Jan 2026 17:14:50 +0000 Subject: [PATCH 16/36] More updates --- .../aztec-node/src/aztec-node/server.ts | 7 +- .../src/branded-types/block_number.ts | 15 +++- .../kv-store/src/stores/l2_tips_store.ts | 82 +++++++++++-------- yarn-project/p2p/src/client/p2p_client.ts | 40 ++++++--- .../src/sequencer/sequencer.ts | 4 +- .../l2_block_stream/l2_tips_memory_store.ts | 67 +++++++++++---- .../stdlib/src/interfaces/api_limit.ts | 1 + .../stdlib/src/interfaces/archiver.ts | 1 - .../stdlib/src/interfaces/aztec-node.test.ts | 18 +++- .../stdlib/src/interfaces/aztec-node.ts | 10 ++- .../txe/src/state_machine/archiver.ts | 25 ++++-- 11 files changed, 200 insertions(+), 70 deletions(-) diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index ec7edf550a80..0b8c90362867 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -19,7 +19,7 @@ import { createEthereumChain } from '@aztec/ethereum/chain'; import { getPublicClient } from '@aztec/ethereum/client'; import { RegistryContract, RollupContract } from '@aztec/ethereum/contracts'; import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses'; -import { BlockNumber, SlotNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types'; import { compactArray, pick } from '@aztec/foundation/collection'; import { Fr } from '@aztec/foundation/curves/bn254'; import { EthAddress } from '@aztec/foundation/eth-address'; @@ -63,6 +63,7 @@ import { type L2BlockSource, type PublishedL2Block, } from '@aztec/stdlib/block'; +import type { PublishedCheckpoint } from '@aztec/stdlib/checkpoint'; import type { ContractClassPublic, ContractDataSource, @@ -638,6 +639,10 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { return (await this.blockSource.getPublishedBlocks(from, limit)) ?? []; } + public async getPublishedCheckpoints(from: CheckpointNumber, limit: number): Promise { + return (await this.blockSource.getPublishedCheckpoints(from, limit)) ?? []; + } + public async getL2BlocksNew(from: BlockNumber, limit: number): Promise { return (await this.blockSource.getL2BlocksNew(from, limit)) ?? []; } diff --git a/yarn-project/foundation/src/branded-types/block_number.ts b/yarn-project/foundation/src/branded-types/block_number.ts index 15203ee45ef2..9b7878c6a1fe 100644 --- a/yarn-project/foundation/src/branded-types/block_number.ts +++ b/yarn-project/foundation/src/branded-types/block_number.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import type { CheckpointNumber } from './checkpoint_number.js'; +import { CheckpointNumber } from './checkpoint_number.js'; import type { Branded } from './types.js'; /** @@ -99,6 +99,13 @@ function makeBlockNumberSchema(minValue: number) { .transform(value => BlockNumber(value)); } +function makeCheckpointNumberSchema(minValue: number) { + return z + .union([z.number(), z.bigint(), z.string()]) + .pipe(z.coerce.number().int().min(minValue)) + .transform(value => CheckpointNumber(value)); +} + /** * Zod schema for parsing and validating BlockNumber values. * Accepts numbers, bigints, or strings and coerces them to BlockNumber. @@ -110,3 +117,9 @@ export const BlockNumberSchema = makeBlockNumberSchema(0); * Accepts numbers, bigints, or strings and coerces them to BlockNumber. */ export const BlockNumberPositiveSchema = makeBlockNumberSchema(1); + +/** + * Zod schema for parsing and validating CheckpointNumber values that are strictly positive. + * Accepts numbers, bigints, or strings and coerces them to CheckpointNumber. + */ +export const CheckpointNumberPositiveSchema = makeCheckpointNumberSchema(1); diff --git a/yarn-project/kv-store/src/stores/l2_tips_store.ts b/yarn-project/kv-store/src/stores/l2_tips_store.ts index b94d8c3e6faf..a10f3b0ce159 100644 --- a/yarn-project/kv-store/src/stores/l2_tips_store.ts +++ b/yarn-project/kv-store/src/stores/l2_tips_store.ts @@ -10,10 +10,9 @@ import type { L2TipId, L2Tips, } from '@aztec/stdlib/block'; -import type { Checkpoint } from '@aztec/stdlib/checkpoint'; +import { type Checkpoint, PublishedCheckpoint } from '@aztec/stdlib/checkpoint'; import type { AztecAsyncMap } from '../interfaces/map.js'; -import type { AztecAsyncSingleton } from '../interfaces/singleton.js'; import type { AztecAsyncKVStore } from '../interfaces/store.js'; /** Stores currently synced L2 tips and unfinalized block hashes. @@ -22,14 +21,16 @@ import type { AztecAsyncKVStore } from '../interfaces/store.js'; export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider { private readonly l2TipsStore: AztecAsyncMap; private readonly l2BlockHashesStore: AztecAsyncMap; - private readonly l2CheckpointNumberStore: AztecAsyncSingleton; - private readonly l2CheckpointHashStore: AztecAsyncSingleton; + private readonly l2BlockNumberToCheckpointNumberStore: AztecAsyncMap; + private readonly l2CheckpointStore: AztecAsyncMap; constructor(store: AztecAsyncKVStore, namespace: string) { this.l2TipsStore = store.openMap([namespace, 'l2_tips'].join('_')); this.l2BlockHashesStore = store.openMap([namespace, 'l2_block_hashes'].join('_')); - this.l2CheckpointNumberStore = store.openSingleton([namespace, 'l2_checkpoint_number'].join('_')); - this.l2CheckpointHashStore = store.openSingleton([namespace, 'l2_checkpoint_hash'].join('_')); + this.l2BlockNumberToCheckpointNumberStore = store.openMap( + [namespace, 'l2_block_number_to_checkpoint_number'].join('_'), + ); + this.l2CheckpointStore = store.openMap([namespace, 'l2_checkpoint_store'].join('_')); } public getL2BlockHash(number: BlockNumber): Promise { @@ -37,21 +38,47 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo } public async getL2Tips(): Promise { - return { - proposed: await this.getL2Tip('proposed'), - checkpointed: await this.getL2Tip('checkpointed'), - proven: await this.getL2Tip('proven'), - finalized: await this.getL2Tip('finalized'), + const proposedBlockId = await this.getBlockId('proposed'); + const finalizedBlockId = await this.getBlockId('finalized'); + const provenBlockId = await this.getBlockId('proven'); + const checkpointedBlockId = await this.getBlockId('checkpointed'); + + const finalizedCheckpointId = await this.getCheckpointId('finalized'); + const provenCheckpointId = await this.getCheckpointId('proven'); + const checkpointedCheckpointId = await this.getCheckpointId('checkpointed'); + + const l2Tips: L2Tips = { + proposed: proposedBlockId, + finalized: { block: finalizedBlockId, checkpoint: finalizedCheckpointId }, + proven: { block: provenBlockId, checkpoint: provenCheckpointId }, + checkpointed: { block: checkpointedBlockId, checkpoint: checkpointedCheckpointId }, }; + return Promise.resolve(l2Tips); } - private async getL2Tip(tag: L2BlockTag): Promise { + private async getCheckpointId(tag: L2BlockTag): Promise { const blockNumber = await this.l2TipsStore.getAsync(tag); - if (blockNumber === undefined || blockNumber === INITIAL_L2_BLOCK_NUM - 1) { - return { - block: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, - checkpoint: { number: CheckpointNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, - }; + if (blockNumber === undefined || blockNumber === 0) { + return { number: CheckpointNumber.ZERO, hash: '' }; + } + const checkpointNumber = await this.l2BlockNumberToCheckpointNumberStore.getAsync(blockNumber); + if (checkpointNumber === undefined) { + throw new Error(`Checkpoint number not found for block number ${blockNumber}`); + } + const checkpointBuffer = await this.l2CheckpointStore.getAsync(checkpointNumber); + if (!checkpointBuffer) { + throw new Error(`Checkpoint not found for checkpoint number ${checkpointNumber}`); + } + + const checkpoint = PublishedCheckpoint.fromBuffer(checkpointBuffer); + + return { number: checkpointNumber, hash: checkpoint.checkpoint.hash().toString() }; + } + + private async getBlockId(tag: L2BlockTag): Promise { + const blockNumber = await this.l2TipsStore.getAsync(tag); + if (blockNumber === undefined || blockNumber === 0) { + return { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }; } const blockHash = await this.l2BlockHashesStore.getAsync(blockNumber); if (!blockHash) { @@ -98,22 +125,11 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo private async saveCheckpoint(checkpoint: Checkpoint) { await Promise.all([ - this.l2CheckpointHashStore.set(checkpoint.hash().toString()), - this.l2CheckpointNumberStore.set(checkpoint.number), + ...Array.from({ length: checkpoint.blocks.length }, (_, i) => i).map(async i => { + const block = checkpoint.blocks[i]; + await this.l2BlockNumberToCheckpointNumberStore.set(block.number, checkpoint.number); + }), + this.l2CheckpointStore.set(checkpoint.number, checkpoint.toBuffer()), ]); } - - private async readCheckpoint(): Promise { - const [hash, checkpointNumber] = await Promise.all([ - this.l2CheckpointHashStore.getAsync(), - this.l2CheckpointNumberStore.getAsync(), - ]); - if (hash === undefined || checkpointNumber === undefined) { - return undefined; - } - return { - number: checkpointNumber, - hash: hash, - }; - } } diff --git a/yarn-project/p2p/src/client/p2p_client.ts b/yarn-project/p2p/src/client/p2p_client.ts index 23232837917a..13fdb80d6a5a 100644 --- a/yarn-project/p2p/src/client/p2p_client.ts +++ b/yarn-project/p2p/src/client/p2p_client.ts @@ -1,4 +1,4 @@ -import { GENESIS_BLOCK_HEADER_HASH, INITIAL_L2_BLOCK_NUM } from '@aztec/constants'; +import { GENESIS_BLOCK_HEADER_HASH, INITIAL_L2_BLOCK_NUM, INITIAL_L2_CHECKPOINT_NUM } from '@aztec/constants'; import { BlockNumber, SlotNumber } from '@aztec/foundation/branded-types'; import { createLogger } from '@aztec/foundation/log'; import { DateProvider } from '@aztec/foundation/timer'; @@ -10,6 +10,7 @@ import type { L2BlockSource, L2BlockStream, L2BlockStreamEvent, + L2TipId, L2Tips, } from '@aztec/stdlib/block'; import type { ContractDataSource } from '@aztec/stdlib/contract'; @@ -205,12 +206,31 @@ export class P2PClient const genesisHash = GENESIS_BLOCK_HEADER_HASH.toString(); - return { - blocks: { - latest: { hash: latestBlockHash ?? genesisHash, number: latestBlockNumber }, - proven: { hash: provenBlockHash ?? genesisHash, number: provenBlockNumber }, - finalized: { hash: finalizedBlockHash ?? genesisHash, number: finalizedBlockNumber }, + // P2P layer doesn't track checkpoints. Checkpoint data here set to initial values. + const proposed: L2BlockId = { + number: latestBlockNumber, + hash: latestBlockHash ?? genesisHash, + }; + const proven: L2TipId = { + block: { + number: provenBlockNumber, + hash: provenBlockHash ?? genesisHash, + }, + checkpoint: { number: INITIAL_L2_CHECKPOINT_NUM, hash: '' }, + }; + const finalized: L2TipId = { + block: { + number: finalizedBlockNumber, + hash: finalizedBlockHash ?? genesisHash, }, + checkpoint: { number: INITIAL_L2_CHECKPOINT_NUM, hash: '' }, + }; + + return { + proposed, + proven, + finalized, + checkpointed: proven, // Set to proven for the time being. P2P layer doesn't track checkpoints }; } @@ -242,7 +262,7 @@ export class P2PClient this.txCollection.stopCollectingForBlocksAfter(event.block.number); await this.handlePruneL2Blocks(event.block.number); break; - case 'checkpoint-added': + case 'chain-checkpointed': break; default: { const _: never = event; @@ -278,9 +298,9 @@ export class P2PClient // get the current latest block numbers const latestBlockNumbers = await this.l2BlockSource.getL2Tips(); - this.latestBlockNumberAtStart = latestBlockNumbers.blocks.latest.number; - this.provenBlockNumberAtStart = latestBlockNumbers.blocks.proven.number; - this.finalizedBlockNumberAtStart = latestBlockNumbers.blocks.finalized.number; + this.latestBlockNumberAtStart = latestBlockNumbers.proposed.number; + this.provenBlockNumberAtStart = latestBlockNumbers.proven.block.number; + this.finalizedBlockNumberAtStart = latestBlockNumbers.finalized.block.number; const syncedLatestBlock = (await this.getSyncedLatestBlockNum()) + 1; const syncedProvenBlock = (await this.getSyncedProvenBlockNum()) + 1; diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index 0ce3c9b7ef34..1cdff32274cb 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -469,9 +469,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter t.blocks.latest), + this.l2BlockSource.getL2Tips().then(t => t.proposed), this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block), - this.l1ToL2MessageSource.getL2Tips().then(t => t.blocks.latest), + this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed), this.l2BlockSource.getPendingChainValidationStatus(), ] as const); diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_memory_store.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_memory_store.ts index e584fcbd714e..e9408bd43eeb 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_memory_store.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_memory_store.ts @@ -1,6 +1,7 @@ import { GENESIS_BLOCK_HEADER_HASH } from '@aztec/constants'; -import { BlockNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; +import type { PublishedCheckpoint } from '../../checkpoint/published_checkpoint.js'; import type { L2BlockNew } from '../l2_block_new.js'; import type { CheckpointId, L2BlockId, L2BlockTag, L2Tips } from '../l2_block_source.js'; import type { L2BlockStreamEvent, L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider } from './interfaces.js'; @@ -12,24 +13,33 @@ import type { L2BlockStreamEvent, L2BlockStreamEventHandler, L2BlockStreamLocalD export class L2TipsMemoryStore implements L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider { protected readonly l2TipsStore: Map = new Map(); protected readonly l2BlockHashesStore: Map = new Map(); - protected l2Checkpoint: CheckpointId | undefined = undefined; + protected readonly l2BlocktoCheckpointStore: Map = new Map(); + protected readonly checkpointStore: Map = new Map(); public getL2BlockHash(number: number): Promise { return Promise.resolve(this.l2BlockHashesStore.get(number)); } public getL2Tips(): Promise { - return Promise.resolve({ - blocks: { - latest: this.getL2Tip('latest'), - finalized: this.getL2Tip('finalized'), - proven: this.getL2Tip('proven'), - }, - checkpoint: this.l2Checkpoint, - }); + const proposedBlockId = this.getBlockId('proposed'); + const finalizedBlockId = this.getBlockId('finalized'); + const provenBlockId = this.getBlockId('proven'); + const checkpointedBlockId = this.getBlockId('checkpointed'); + + const finalizedCheckpointId = this.getCheckpointId('finalized'); + const provenCheckpointId = this.getCheckpointId('proven'); + const checkpointedCheckpointId = this.getCheckpointId('checkpointed'); + + const l2Tips: L2Tips = { + proposed: proposedBlockId, + finalized: { block: finalizedBlockId, checkpoint: finalizedCheckpointId }, + proven: { block: provenBlockId, checkpoint: provenCheckpointId }, + checkpointed: { block: checkpointedBlockId, checkpoint: checkpointedCheckpointId }, + }; + return Promise.resolve(l2Tips); } - private getL2Tip(tag: L2BlockTag): L2BlockId { + private getBlockId(tag: L2BlockTag): L2BlockId { const blockNumber = this.l2TipsStore.get(tag); if (blockNumber === undefined || blockNumber === 0) { return { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }; @@ -42,6 +52,23 @@ export class L2TipsMemoryStore implements L2BlockStreamEventHandler, L2BlockStre return { number: blockNumber, hash: blockHash }; } + private getCheckpointId(tag: L2BlockTag): CheckpointId { + const blockNumber = this.l2TipsStore.get(tag); + if (blockNumber === undefined || blockNumber === 0) { + return { number: CheckpointNumber.ZERO, hash: '' }; + } + const checkpointNumber = this.l2BlocktoCheckpointStore.get(blockNumber); + if (checkpointNumber === undefined) { + throw new Error(`Checkpoint number not found for block number ${blockNumber}`); + } + const checkpoint = this.checkpointStore.get(checkpointNumber); + if (!checkpoint) { + throw new Error(`Checkpoint not found for checkpoint number ${checkpointNumber}`); + } + + return { number: checkpointNumber, hash: checkpoint.checkpoint.hash().toString() }; + } + public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise { switch (event.type) { case 'blocks-added': { @@ -49,14 +76,24 @@ export class L2TipsMemoryStore implements L2BlockStreamEventHandler, L2BlockStre for (const block of blocks) { this.l2BlockHashesStore.set(block.number, await this.computeBlockHash(block)); } - this.l2TipsStore.set('latest', blocks.at(-1)!.number); + this.l2TipsStore.set('proposed', blocks.at(-1)!.number); break; } - case 'checkpoint-added': - this.l2Checkpoint = event.checkpoint; + case 'chain-checkpointed': + const blocks = event.checkpoint.checkpoint.blocks; + const blockId: L2BlockId = { + number: blocks.at(-1)!.number, + hash: await this.computeBlockHash(blocks.at(-1)!), + }; + this.saveTag('checkpointed', blockId); + for (let i = 0; i < blocks.length; i++) { + const block = blocks[i]; + this.l2BlocktoCheckpointStore.set(block.number, event.checkpoint.checkpoint.number); + } + this.checkpointStore.set(event.checkpoint.checkpoint.number, event.checkpoint); break; case 'chain-pruned': - this.saveTag('latest', event.block); + this.saveTag('proposed', event.block); break; case 'chain-proven': this.saveTag('proven', event.block); diff --git a/yarn-project/stdlib/src/interfaces/api_limit.ts b/yarn-project/stdlib/src/interfaces/api_limit.ts index 5f7754bdfe34..81956427c57c 100644 --- a/yarn-project/stdlib/src/interfaces/api_limit.ts +++ b/yarn-project/stdlib/src/interfaces/api_limit.ts @@ -1,3 +1,4 @@ export const MAX_RPC_LEN = 100; export const MAX_RPC_TXS_LEN = 50; export const MAX_RPC_BLOCKS_LEN = 50; +export const MAX_RPC_CHECKPOINTS_LEN = 50; diff --git a/yarn-project/stdlib/src/interfaces/archiver.ts b/yarn-project/stdlib/src/interfaces/archiver.ts index 90197ad5e805..558baca88658 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.ts @@ -101,7 +101,6 @@ export const ArchiverApiSchema: ApiSchemaFor = { .function() .args(BlockNumberSchema, schemas.Integer, optional(z.boolean())) .returns(z.array(PublishedL2Block.schema)), - getL2BlockNew: z.function().args(BlockNumberSchema).returns(L2BlockNew.schema), getL2BlocksNew: z .function() .args(BlockNumberSchema, schemas.Integer, optional(z.boolean())) diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts index b177d93b8399..b93f5ea0c9ae 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts @@ -26,7 +26,8 @@ import type { DataInBlock } from '../block/in_block.js'; import { type BlockParameter, CommitteeAttestation, L2BlockHash, L2BlockNew } from '../block/index.js'; import { L2Block } from '../block/l2_block.js'; import type { L2Tips } from '../block/l2_block_source.js'; -import { L1PublishedData } from '../checkpoint/published_checkpoint.js'; +import { Checkpoint } from '../checkpoint/checkpoint.js'; +import { L1PublishedData, PublishedCheckpoint } from '../checkpoint/published_checkpoint.js'; import { type ContractClassPublic, type ContractInstanceWithAddress, @@ -278,6 +279,12 @@ describe('AztecNodeApiSchema', () => { expect(response[0].l1).toBeDefined(); }); + it('getPublishedCheckpoints', async () => { + const response = await context.client.getPublishedCheckpoints(CheckpointNumber(1), 1); + expect(response).toHaveLength(1); + expect(response[0]).toBeInstanceOf(PublishedCheckpoint); + }); + it('getNodeVersion', async () => { const response = await context.client.getNodeVersion(); expect(response).toBe('1.0.0'); @@ -715,6 +722,15 @@ class MockAztecNode implements AztecNode { }), ); } + getPublishedCheckpoints(from: CheckpointNumber, limit: number): Promise { + return timesAsync(limit, async i => + PublishedCheckpoint.from({ + checkpoint: await Checkpoint.random(CheckpointNumber(from + i)), + attestations: [CommitteeAttestation.random()], + l1: new L1PublishedData(1n, 1n, Buffer32.random().toString()), + }), + ); + } getNodeVersion(): Promise { return Promise.resolve('1.0.0'); } diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.ts b/yarn-project/stdlib/src/interfaces/aztec-node.ts index 22b1501be793..c19dd27334bb 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.ts @@ -10,6 +10,7 @@ import { BlockNumber, BlockNumberPositiveSchema, BlockNumberSchema, + CheckpointNumber, type SlotNumber, } from '@aztec/foundation/branded-types'; import type { Fr } from '@aztec/foundation/curves/bn254'; @@ -19,6 +20,7 @@ import { MembershipWitness, SiblingPath } from '@aztec/foundation/trees'; import { z } from 'zod'; +import { CheckpointNumberPositiveSchema } from '../../../foundation/src/branded-types/block_number.js'; import type { AztecAddress } from '../aztec-address/index.js'; import { type BlockParameter, BlockParameterSchema } from '../block/block_parameter.js'; import { PublishedL2Block } from '../block/checkpointed_l2_block.js'; @@ -26,6 +28,7 @@ import { type DataInBlock, dataInBlockSchemaFor } from '../block/in_block.js'; import { L2Block } from '../block/l2_block.js'; import { L2BlockNew } from '../block/l2_block_new.js'; import { type L2BlockSource, type L2Tips, L2TipsSchema } from '../block/l2_block_source.js'; +import { PublishedCheckpoint } from '../checkpoint/published_checkpoint.js'; import { type ContractClassPublic, ContractClassPublicSchema, @@ -58,7 +61,7 @@ import { SingleValidatorStatsSchema, ValidatorsStatsSchema } from '../validators import type { SingleValidatorStats, ValidatorsStats } from '../validators/types.js'; import { type ComponentsVersions, getVersioningResponseHandler } from '../versioning/index.js'; import { type AllowedElement, AllowedElementSchema } from './allowed_element.js'; -import { MAX_RPC_BLOCKS_LEN, MAX_RPC_LEN, MAX_RPC_TXS_LEN } from './api_limit.js'; +import { MAX_RPC_BLOCKS_LEN, MAX_RPC_CHECKPOINTS_LEN, MAX_RPC_LEN, MAX_RPC_TXS_LEN } from './api_limit.js'; import { type GetContractClassLogsResponse, GetContractClassLogsResponseSchema, @@ -583,6 +586,11 @@ export const AztecNodeApiSchema: ApiSchemaFor = { .args(BlockNumberPositiveSchema, z.number().gt(0).lte(MAX_RPC_BLOCKS_LEN)) .returns(z.array(PublishedL2Block.schema)), + getPublishedCheckpoints: z + .function() + .args(CheckpointNumberPositiveSchema, z.number().gt(0).lte(MAX_RPC_CHECKPOINTS_LEN)) + .returns(z.array(PublishedCheckpoint.schema)), + getL2BlocksNew: z .function() .args(BlockNumberPositiveSchema, z.number().gt(0).lte(MAX_RPC_BLOCKS_LEN)) diff --git a/yarn-project/txe/src/state_machine/archiver.ts b/yarn-project/txe/src/state_machine/archiver.ts index b5e6f0e574bf..4d78e28106c8 100644 --- a/yarn-project/txe/src/state_machine/archiver.ts +++ b/yarn-project/txe/src/state_machine/archiver.ts @@ -7,11 +7,13 @@ import { isDefined } from '@aztec/foundation/types'; import type { AztecAsyncKVStore } from '@aztec/kv-store'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import { + type CheckpointId, CommitteeAttestation, L2Block, type L2BlockId, type L2BlockNew, type L2BlockSource, + type L2TipId, type L2Tips, PublishedL2Block, type ValidateBlockResult, @@ -221,12 +223,25 @@ export class TXEArchiver extends ArchiverStoreHelper implements L2BlockSource { const number = blockHeader.globalVariables.blockNumber; const hash = (await blockHeader.hash()).toString(); + const checkpointedBlock = await this.getCheckpointedBlock(number); + if (!checkpointedBlock) { + throw new Error(`L2Tips requested from TXE Archiver but no checkpointed block found for block number ${number}`); + } + const checkpoint = await this.store.getRangeOfCheckpoints(CheckpointNumber(number), 1); + if (checkpoint.length === 0) { + throw new Error(`L2Tips requested from TXE Archiver but no checkpoint found for block number ${number}`); + } + const blockId: L2BlockId = { number, hash }; + const checkpointId: CheckpointId = { + number: checkpoint[0].checkpointNumber, + hash: checkpoint[0].header.hash().toString(), + }; + const tipId: L2TipId = { block: blockId, checkpoint: checkpointId }; return { - blocks: { - latest: { number, hash } as L2BlockId, - proven: { number, hash } as L2BlockId, - finalized: { number, hash } as L2BlockId, - }, + proposed: blockId, + proven: tipId, + finalized: tipId, + checkpointed: tipId, }; } From b682624594bbf1e7c92a99e354d02f548b1daea8 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Tue, 6 Jan 2026 18:00:09 +0000 Subject: [PATCH 17/36] Updates --- .../kv-store/src/stores/l2_tips_store.ts | 50 ++- .../l2_block_stream/l2_tips_memory_store.ts | 34 +- .../block/test/l2_tips_store_test_suite.ts | 401 ++++++++++++++++-- 3 files changed, 440 insertions(+), 45 deletions(-) diff --git a/yarn-project/kv-store/src/stores/l2_tips_store.ts b/yarn-project/kv-store/src/stores/l2_tips_store.ts index a10f3b0ce159..9483e4f3fd68 100644 --- a/yarn-project/kv-store/src/stores/l2_tips_store.ts +++ b/yarn-project/kv-store/src/stores/l2_tips_store.ts @@ -7,16 +7,14 @@ import type { L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider, L2BlockTag, - L2TipId, L2Tips, } from '@aztec/stdlib/block'; -import { type Checkpoint, PublishedCheckpoint } from '@aztec/stdlib/checkpoint'; +import { PublishedCheckpoint } from '@aztec/stdlib/checkpoint'; import type { AztecAsyncMap } from '../interfaces/map.js'; import type { AztecAsyncKVStore } from '../interfaces/store.js'; /** Stores currently synced L2 tips and unfinalized block hashes. - * TODO (pw/mbps): I feel like this store would benefit from using transactions to ensure atomicy across the different stores. */ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider { private readonly l2TipsStore: AztecAsyncMap; @@ -63,7 +61,8 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo } const checkpointNumber = await this.l2BlockNumberToCheckpointNumberStore.getAsync(blockNumber); if (checkpointNumber === undefined) { - throw new Error(`Checkpoint number not found for block number ${blockNumber}`); + // No checkpoint associated with this block yet + return { number: CheckpointNumber.ZERO, hash: '' }; } const checkpointBuffer = await this.l2CheckpointStore.getAsync(checkpointNumber); if (!checkpointBuffer) { @@ -98,21 +97,48 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo await this.l2TipsStore.set('proposed', blocks.at(-1)!.number); break; } - case 'chain-checkpointed': - await this.saveCheckpoint(event.checkpoint.checkpoint); + case 'chain-checkpointed': { + const checkpointBlocks = event.checkpoint.checkpoint.blocks; + const lastBlock = checkpointBlocks.at(-1)!; + const blockId: L2BlockId = { + number: lastBlock.number, + hash: (await lastBlock.hash()).toString(), + }; + await this.saveTag('checkpointed', blockId); + await this.saveCheckpoint(event.checkpoint); break; - case 'chain-pruned': + } + case 'chain-pruned': { await this.saveTag('proposed', event.block); + const currentCheckpointed = (await this.l2TipsStore.getAsync('checkpointed')) ?? INITIAL_L2_BLOCK_NUM; + if (event.block.number < currentCheckpointed) { + await this.saveTag('checkpointed', event.block); + } break; + } case 'chain-proven': await this.saveTag('proven', event.block); break; - case 'chain-finalized': + case 'chain-finalized': { await this.saveTag('finalized', event.block); + // Get the checkpoint number for the finalized block before cleanup + const finalizedCheckpointNumber = await this.l2BlockNumberToCheckpointNumberStore.getAsync(event.block.number); + // Clean up block hashes for blocks before finalized for await (const key of this.l2BlockHashesStore.keysAsync({ end: event.block.number })) { await this.l2BlockHashesStore.delete(key); } + // Clean up block-to-checkpoint mappings for blocks before finalized + for await (const key of this.l2BlockNumberToCheckpointNumberStore.keysAsync({ end: event.block.number })) { + await this.l2BlockNumberToCheckpointNumberStore.delete(key); + } + // Clean up checkpoints older than the finalized checkpoint + if (finalizedCheckpointNumber !== undefined) { + for await (const key of this.l2CheckpointStore.keysAsync({ end: finalizedCheckpointNumber })) { + await this.l2CheckpointStore.delete(key); + } + } break; + } } } @@ -123,13 +149,13 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo } } - private async saveCheckpoint(checkpoint: Checkpoint) { + private async saveCheckpoint(publishedCheckpoint: PublishedCheckpoint) { + const checkpoint = publishedCheckpoint.checkpoint; await Promise.all([ - ...Array.from({ length: checkpoint.blocks.length }, (_, i) => i).map(async i => { - const block = checkpoint.blocks[i]; + ...checkpoint.blocks.map(async block => { await this.l2BlockNumberToCheckpointNumberStore.set(block.number, checkpoint.number); }), - this.l2CheckpointStore.set(checkpoint.number, checkpoint.toBuffer()), + this.l2CheckpointStore.set(checkpoint.number, publishedCheckpoint.toBuffer()), ]); } } diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_memory_store.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_memory_store.ts index e9408bd43eeb..c050d72d4b03 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_memory_store.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_memory_store.ts @@ -13,8 +13,8 @@ import type { L2BlockStreamEvent, L2BlockStreamEventHandler, L2BlockStreamLocalD export class L2TipsMemoryStore implements L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider { protected readonly l2TipsStore: Map = new Map(); protected readonly l2BlockHashesStore: Map = new Map(); - protected readonly l2BlocktoCheckpointStore: Map = new Map(); - protected readonly checkpointStore: Map = new Map(); + protected readonly l2BlocktoCheckpointStore: Map = new Map(); + protected readonly checkpointStore: Map = new Map(); public getL2BlockHash(number: number): Promise { return Promise.resolve(this.l2BlockHashesStore.get(number)); @@ -59,7 +59,8 @@ export class L2TipsMemoryStore implements L2BlockStreamEventHandler, L2BlockStre } const checkpointNumber = this.l2BlocktoCheckpointStore.get(blockNumber); if (checkpointNumber === undefined) { - throw new Error(`Checkpoint number not found for block number ${blockNumber}`); + // No checkpoint associated with this block yet + return { number: CheckpointNumber.ZERO, hash: '' }; } const checkpoint = this.checkpointStore.get(checkpointNumber); if (!checkpoint) { @@ -92,20 +93,43 @@ export class L2TipsMemoryStore implements L2BlockStreamEventHandler, L2BlockStre } this.checkpointStore.set(event.checkpoint.checkpoint.number, event.checkpoint); break; - case 'chain-pruned': + case 'chain-pruned': { this.saveTag('proposed', event.block); + const currentCheckpointed = this.l2TipsStore.get('checkpointed') ?? 0; + if (event.block.number < currentCheckpointed) { + this.saveTag('checkpointed', event.block); + } break; + } case 'chain-proven': this.saveTag('proven', event.block); break; - case 'chain-finalized': + case 'chain-finalized': { this.saveTag('finalized', event.block); + // Get the checkpoint number for the finalized block before cleanup + const finalizedCheckpointNumber = this.l2BlocktoCheckpointStore.get(event.block.number); + // Clean up block hashes for blocks before finalized for (const key of this.l2BlockHashesStore.keys()) { if (key < event.block.number) { this.l2BlockHashesStore.delete(key); } } + // Clean up block-to-checkpoint mappings for blocks before finalized + for (const key of this.l2BlocktoCheckpointStore.keys()) { + if (key < event.block.number) { + this.l2BlocktoCheckpointStore.delete(key); + } + } + // Clean up checkpoints older than the finalized checkpoint + if (finalizedCheckpointNumber !== undefined) { + for (const key of this.checkpointStore.keys()) { + if (key < finalizedCheckpointNumber) { + this.checkpointStore.delete(key); + } + } + } break; + } } } diff --git a/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts b/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts index f83a7f961dae..0d1b7549ae10 100644 --- a/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts +++ b/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts @@ -1,8 +1,9 @@ import { GENESIS_BLOCK_HEADER_HASH } from '@aztec/constants'; -import { BlockNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; import { times } from '@aztec/foundation/collection'; import { Fr } from '@aztec/foundation/curves/bn254'; -import { type L2BlockId, L2BlockNew } from '@aztec/stdlib/block'; +import { type CheckpointId, type L2BlockId, L2BlockNew, type L2TipId } from '@aztec/stdlib/block'; +import { Checkpoint, L1PublishedData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint'; import { jestExpect as expect } from '@jest/expect'; @@ -12,10 +13,16 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { let tipsStore: L2TipsStore; // Track blocks and their hashes for test assertions const blockHashes: Map = new Map(); + // Track checkpoints and their hashes + const checkpointHashes: Map = new Map(); + // Track which blocks belong to which checkpoint + const blockToCheckpoint: Map = new Map(); beforeEach(async () => { tipsStore = await makeTipsStore(); blockHashes.clear(); + checkpointHashes.clear(); + blockToCheckpoint.clear(); }); const makeBlock = async (number: number): Promise => { @@ -34,34 +41,58 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { hash: number === 0 ? GENESIS_BLOCK_HEADER_HASH.toString() : (blockHashes.get(number) ?? new Fr(number).toString()), }); - const makeTips = (latest: number, proven: number, finalized: number) => ({ - blocks: { - latest: makeTip(latest), - proven: makeTip(proven), - finalized: makeTip(finalized), - }, + const makeCheckpointIdForBlock = (blockNumber: number): CheckpointId => { + if (blockNumber === 0) { + return { number: CheckpointNumber.ZERO, hash: '' }; + } + const checkpointNum = blockToCheckpoint.get(blockNumber); + if (checkpointNum === undefined) { + return { number: CheckpointNumber.ZERO, hash: '' }; + } + const hash = checkpointHashes.get(checkpointNum); + if (!hash) { + return { number: CheckpointNumber.ZERO, hash: '' }; + } + return { number: CheckpointNumber(checkpointNum), hash }; + }; + + const makeTipId = (blockNumber: number): L2TipId => ({ + block: makeTip(blockNumber), + checkpoint: makeCheckpointIdForBlock(blockNumber), }); - it('returns zero if no tips are stored', async () => { - const tips = await tipsStore.getL2Tips(); - expect(tips).toEqual(makeTips(0, 0, 0)); + const makeTips = (proposed: number, proven: number, finalized: number, checkpointed: number = 0) => ({ + proposed: makeTip(proposed), + proven: makeTipId(proven), + finalized: makeTipId(finalized), + checkpointed: makeTipId(checkpointed), }); - it('stores chain tips', async () => { - await tipsStore.handleBlockStreamEvent({ - type: 'blocks-added', - blocks: await Promise.all(times(20, i => makeBlock(i + 1))), + const makeCheckpoint = async (checkpointNumber: number, blocks: L2BlockNew[]): Promise => { + const checkpoint = await Checkpoint.random(CheckpointNumber(checkpointNumber), { + numBlocks: blocks.length, + startBlockNumber: blocks[0].number, }); + // Override the blocks with our actual blocks (to keep hashes consistent) + (checkpoint as any).blocks = blocks; - await tipsStore.handleBlockStreamEvent({ type: 'chain-finalized', block: makeBlockId(5) }); - await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: makeBlockId(8) }); - await tipsStore.handleBlockStreamEvent({ type: 'chain-pruned', block: makeBlockId(10) }); + const checkpointHash = checkpoint.hash().toString(); + checkpointHashes.set(checkpointNumber, checkpointHash); + + // Track which blocks belong to this checkpoint + for (const block of blocks) { + blockToCheckpoint.set(block.number, checkpointNumber); + } + + return new PublishedCheckpoint(checkpoint, L1PublishedData.random(), []); + }; + it('returns zero if no tips are stored', async () => { const tips = await tipsStore.getL2Tips(); - expect(tips).toEqual(makeTips(10, 8, 5)); + expect(tips).toEqual(makeTips(0, 0, 0)); }); - it('sets latest tip from blocks added', async () => { + it('sets proposed tip from blocks added', async () => { await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: await Promise.all(times(3, i => makeBlock(i + 1))), @@ -75,31 +106,345 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { expect(await tipsStore.getL2BlockHash(3)).toEqual(blockHashes.get(3)); }); + it('checkpoints all proposed blocks', async () => { + // Propose blocks 1-5 + const blocks = await Promise.all(times(5, i => makeBlock(i + 1))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks }); + + // Checkpoint all proposed blocks (1-5) + const checkpoint1 = await makeCheckpoint(1, blocks); + await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint1 }); + + const tips = await tipsStore.getL2Tips(); + // Proposed and checkpointed should be the same + expect(tips.proposed).toEqual(makeTip(5)); + expect(tips.checkpointed.block).toEqual(makeTip(5)); + expect(tips.checkpointed.checkpoint.number).toEqual(CheckpointNumber(1)); + }); + + it('advances proven chain with checkpoint info', async () => { + // Propose and checkpoint blocks 1-5 + const blocks = await Promise.all(times(5, i => makeBlock(i + 1))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks }); + const checkpoint1 = await makeCheckpoint(1, blocks); + await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint1 }); + + // Prove up to block 5 + await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: makeBlockId(5) }); + + const tips = await tipsStore.getL2Tips(); + expect(tips.proposed).toEqual(makeTip(5)); + expect(tips.checkpointed.block).toEqual(makeTip(5)); + expect(tips.proven.block).toEqual(makeTip(5)); + + // Proven tip should have the checkpoint info + expect(tips.proven.checkpoint.number).toEqual(CheckpointNumber(1)); + expect(tips.proven.checkpoint.hash).toEqual(checkpointHashes.get(1)); + }); + + it('advances finalized chain with checkpoint info', async () => { + // Propose and checkpoint blocks 1-5 + const blocks = await Promise.all(times(5, i => makeBlock(i + 1))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks }); + const checkpoint1 = await makeCheckpoint(1, blocks); + await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint1 }); + + // Prove and finalize + await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: makeBlockId(5) }); + await tipsStore.handleBlockStreamEvent({ type: 'chain-finalized', block: makeBlockId(5) }); + + const tips = await tipsStore.getL2Tips(); + expect(tips.proposed).toEqual(makeTip(5)); + expect(tips.checkpointed.block).toEqual(makeTip(5)); + expect(tips.proven.block).toEqual(makeTip(5)); + expect(tips.finalized.block).toEqual(makeTip(5)); + + // Finalized tip should have checkpoint info + expect(tips.finalized.checkpoint.number).toEqual(CheckpointNumber(1)); + expect(tips.finalized.checkpoint.hash).toEqual(checkpointHashes.get(1)); + }); + + it('handles multiple checkpoints advancing the chain', async () => { + // Propose blocks 1-5 + const blocks1 = await Promise.all(times(5, i => makeBlock(i + 1))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: blocks1 }); + + // Checkpoint 1: all proposed blocks 1-5 + const checkpoint1 = await makeCheckpoint(1, blocks1); + await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint1 }); + + // Propose more blocks 6-10 + const blocks2 = await Promise.all(times(5, i => makeBlock(i + 6))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: blocks2 }); + + // Checkpoint 2: all remaining proposed blocks 6-10 + const checkpoint2 = await makeCheckpoint(2, blocks2); + await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint2 }); + + const tips = await tipsStore.getL2Tips(); + expect(tips.proposed).toEqual(makeTip(10)); + expect(tips.checkpointed.block).toEqual(makeTip(10)); + expect(tips.checkpointed.checkpoint.number).toEqual(CheckpointNumber(2)); + expect(tips.checkpointed.checkpoint.hash).toEqual(checkpointHashes.get(2)); + }); + it('clears block hashes when setting finalized chain', async () => { - await tipsStore.handleBlockStreamEvent({ - type: 'blocks-added', - blocks: await Promise.all(times(5, i => makeBlock(i + 1))), - }); + // Propose blocks 1-3 + const blocks1to3 = await Promise.all(times(3, i => makeBlock(i + 1))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: blocks1to3 }); + + // Checkpoint all proposed blocks (1-3) + const checkpoint1 = await makeCheckpoint(1, blocks1to3); + await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint1 }); + + // Propose more blocks 4-5 + const blocks4to5 = await Promise.all(times(2, i => makeBlock(i + 4))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: blocks4to5 }); + + // Checkpoint all remaining proposed blocks (4-5) + const checkpoint2 = await makeCheckpoint(2, blocks4to5); + await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint2 }); + + // Prove and finalize up to block 3 (checkpoint 1) await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: makeBlockId(3) }); await tipsStore.handleBlockStreamEvent({ type: 'chain-finalized', block: makeBlockId(3) }); - const tips = await tipsStore.getL2Tips(); - expect(tips).toEqual(makeTips(5, 3, 3)); - + // Blocks before finalized should be cleared expect(await tipsStore.getL2BlockHash(1)).toBeUndefined(); expect(await tipsStore.getL2BlockHash(2)).toBeUndefined(); + // Finalized and later blocks should remain expect(await tipsStore.getL2BlockHash(3)).toEqual(blockHashes.get(3)); expect(await tipsStore.getL2BlockHash(4)).toEqual(blockHashes.get(4)); expect(await tipsStore.getL2BlockHash(5)).toEqual(blockHashes.get(5)); }); + it('handles chain pruning by updating proposed tip', async () => { + const blocks = await Promise.all(times(10, i => makeBlock(i + 1))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks }); + + // Prune to block 5 + await tipsStore.handleBlockStreamEvent({ type: 'chain-pruned', block: makeBlockId(5) }); + + const tips = await tipsStore.getL2Tips(); + expect(tips.proposed).toEqual(makeTip(5)); + }); + + it('handles reorg: prune proposed blocks back to checkpoint, then re-propose with different blocks', async () => { + // Propose blocks 1-5 + const firstBlocks = await Promise.all(times(5, i => makeBlock(i + 1))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: firstBlocks }); + + // Checkpoint all proposed blocks (1-5) - these are now committed + const checkpoint1 = await makeCheckpoint(1, firstBlocks); + await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint1 }); + + // Propose more blocks 6-10 (not yet checkpointed, can be pruned) + const originalBlocks6to10 = await Promise.all(times(5, i => makeBlock(i + 6))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: originalBlocks6to10 }); + + let tips = await tipsStore.getL2Tips(); + expect(tips.proposed).toEqual(makeTip(10)); + expect(tips.checkpointed.block).toEqual(makeTip(5)); // Only blocks 1-5 are checkpointed + + // Store original hashes for proposed (non-checkpointed) blocks 6-8 + const originalHash6 = blockHashes.get(6); + const originalHash7 = blockHashes.get(7); + const originalHash8 = blockHashes.get(8); + + // Prune proposed blocks back to checkpoint (block 5) + // This removes proposed blocks 6-10, but checkpoint remains at 5 + await tipsStore.handleBlockStreamEvent({ type: 'chain-pruned', block: makeBlockId(5) }); + + tips = await tipsStore.getL2Tips(); + expect(tips.proposed).toEqual(makeTip(5)); + expect(tips.checkpointed.block).toEqual(makeTip(5)); // Checkpoint unchanged + + // Propose new blocks 6-8 (different from original 6-10) + blockHashes.delete(6); + blockHashes.delete(7); + blockHashes.delete(8); + const newBlocks = await Promise.all(times(3, i => makeBlock(i + 6))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: newBlocks }); + + // Verify the new blocks have different hashes than the original ones + expect(blockHashes.get(6)).not.toEqual(originalHash6); + expect(blockHashes.get(7)).not.toEqual(originalHash7); + expect(blockHashes.get(8)).not.toEqual(originalHash8); + + tips = await tipsStore.getL2Tips(); + expect(tips.proposed).toEqual(makeTip(8)); + expect(tips.checkpointed.block).toEqual(makeTip(5)); // Still at checkpoint 1 + + // Checkpoint all the new proposed blocks (6-8) + const checkpoint2 = await makeCheckpoint(2, newBlocks); + await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint2 }); + + tips = await tipsStore.getL2Tips(); + expect(tips.proposed).toEqual(makeTip(8)); + expect(tips.checkpointed.block).toEqual(makeTip(8)); + expect(tips.checkpointed.checkpoint.number).toEqual(CheckpointNumber(2)); + + // Block hashes in the store should reflect the new blocks + expect(await tipsStore.getL2BlockHash(6)).toEqual(blockHashes.get(6)); + expect(await tipsStore.getL2BlockHash(7)).toEqual(blockHashes.get(7)); + expect(await tipsStore.getL2BlockHash(8)).toEqual(blockHashes.get(8)); + + // And should NOT equal the original hashes + expect(await tipsStore.getL2BlockHash(6)).not.toEqual(originalHash6); + expect(await tipsStore.getL2BlockHash(7)).not.toEqual(originalHash7); + expect(await tipsStore.getL2BlockHash(8)).not.toEqual(originalHash8); + }); + + it('handles reorg with different chain length after prune', async () => { + // Propose blocks 1-3 + const firstBlocks = await Promise.all(times(3, i => makeBlock(i + 1))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: firstBlocks }); + + // Checkpoint all proposed blocks (1-3) - these are now committed + const checkpoint1 = await makeCheckpoint(1, firstBlocks); + await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint1 }); + + // Propose more blocks 4-10 (not yet checkpointed, can be pruned) + const originalBlocks4to10 = await Promise.all(times(7, i => makeBlock(i + 4))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: originalBlocks4to10 }); + + let tips = await tipsStore.getL2Tips(); + expect(tips.proposed).toEqual(makeTip(10)); + expect(tips.checkpointed.block).toEqual(makeTip(3)); // Only blocks 1-3 are checkpointed + + // Prune proposed blocks back to checkpoint (block 3) + await tipsStore.handleBlockStreamEvent({ type: 'chain-pruned', block: makeBlockId(3) }); + + tips = await tipsStore.getL2Tips(); + expect(tips.proposed).toEqual(makeTip(3)); + expect(tips.checkpointed.block).toEqual(makeTip(3)); // Checkpoint unchanged + + // Now propose only 2 new blocks (4-5) instead of the original 7 blocks (4-10) + blockHashes.delete(4); + blockHashes.delete(5); + const newBlocks = await Promise.all(times(2, i => makeBlock(i + 4))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: newBlocks }); + + tips = await tipsStore.getL2Tips(); + expect(tips.proposed).toEqual(makeTip(5)); + expect(tips.checkpointed.block).toEqual(makeTip(3)); // Still at checkpoint 1 + + // Checkpoint all the new proposed blocks (4-5) + const checkpoint2 = await makeCheckpoint(2, newBlocks); + await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint2 }); + + tips = await tipsStore.getL2Tips(); + expect(tips.proposed).toEqual(makeTip(5)); + expect(tips.checkpointed.block).toEqual(makeTip(5)); + expect(tips.checkpointed.checkpoint.number).toEqual(CheckpointNumber(2)); + }); + + it('handles reorg: prune back to proven tip (including checkpointed blocks), then re-propose and checkpoint', async () => { + // Propose blocks 1-3 + const firstBlocks = await Promise.all(times(3, i => makeBlock(i + 1))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: firstBlocks }); + + // Checkpoint all proposed blocks (1-3) + const checkpoint1 = await makeCheckpoint(1, firstBlocks); + await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint1 }); + + // Prove up to block 3 + await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: makeBlockId(3) }); + + let tips = await tipsStore.getL2Tips(); + expect(tips.proposed).toEqual(makeTip(3)); + expect(tips.checkpointed.block).toEqual(makeTip(3)); + expect(tips.proven.block).toEqual(makeTip(3)); + + // Propose more blocks 4-6 + const blocks4to6 = await Promise.all(times(3, i => makeBlock(i + 4))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: blocks4to6 }); + + // Checkpoint blocks 4-6 (now checkpointed is ahead of proven) + const checkpoint2 = await makeCheckpoint(2, blocks4to6); + await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint2 }); + + tips = await tipsStore.getL2Tips(); + expect(tips.proposed).toEqual(makeTip(6)); + expect(tips.checkpointed.block).toEqual(makeTip(6)); + expect(tips.proven.block).toEqual(makeTip(3)); // Proven is behind checkpointed + + // Propose even more blocks 7-10 (proposed is now ahead of checkpointed) + const originalBlocks7to10 = await Promise.all(times(4, i => makeBlock(i + 7))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: originalBlocks7to10 }); + + tips = await tipsStore.getL2Tips(); + // Now all three tips are different: proposed=10, checkpointed=6, proven=3 + expect(tips.proposed).toEqual(makeTip(10)); + expect(tips.checkpointed.block).toEqual(makeTip(6)); + expect(tips.proven.block).toEqual(makeTip(3)); + + // Store original hashes for blocks 4-7 + const originalHash4 = blockHashes.get(4); + const originalHash5 = blockHashes.get(5); + const originalHash6 = blockHashes.get(6); + const originalHash7 = blockHashes.get(7); + + // Prune all the way back to proven tip (block 3) + // This prunes both proposed blocks (7-10) AND checkpointed blocks (4-6) + await tipsStore.handleBlockStreamEvent({ type: 'chain-pruned', block: makeBlockId(3) }); + + tips = await tipsStore.getL2Tips(); + expect(tips.proposed).toEqual(makeTip(3)); + expect(tips.checkpointed.block).toEqual(makeTip(3)); // Checkpointed also pruned back + expect(tips.proven.block).toEqual(makeTip(3)); + + // Propose new blocks 4-7 (different from original) + blockHashes.delete(4); + blockHashes.delete(5); + blockHashes.delete(6); + blockHashes.delete(7); + const newBlocks = await Promise.all(times(4, i => makeBlock(i + 4))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: newBlocks }); + + // Verify the new blocks have different hashes than the original ones + expect(blockHashes.get(4)).not.toEqual(originalHash4); + expect(blockHashes.get(5)).not.toEqual(originalHash5); + expect(blockHashes.get(6)).not.toEqual(originalHash6); + expect(blockHashes.get(7)).not.toEqual(originalHash7); + + tips = await tipsStore.getL2Tips(); + expect(tips.proposed).toEqual(makeTip(7)); + expect(tips.proven.block).toEqual(makeTip(3)); + + // Checkpoint all the new proposed blocks (4-7) + const checkpoint3 = await makeCheckpoint(3, newBlocks); + await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint3 }); + + tips = await tipsStore.getL2Tips(); + expect(tips.proposed).toEqual(makeTip(7)); + expect(tips.checkpointed.block).toEqual(makeTip(7)); + expect(tips.checkpointed.checkpoint.number).toEqual(CheckpointNumber(3)); + expect(tips.proven.block).toEqual(makeTip(3)); // Proven hasn't moved yet + + // Block hashes in the store should reflect the new blocks + expect(await tipsStore.getL2BlockHash(4)).toEqual(blockHashes.get(4)); + expect(await tipsStore.getL2BlockHash(5)).toEqual(blockHashes.get(5)); + expect(await tipsStore.getL2BlockHash(6)).toEqual(blockHashes.get(6)); + expect(await tipsStore.getL2BlockHash(7)).toEqual(blockHashes.get(7)); + + // And should NOT equal the original hashes + expect(await tipsStore.getL2BlockHash(4)).not.toEqual(originalHash4); + expect(await tipsStore.getL2BlockHash(5)).not.toEqual(originalHash5); + expect(await tipsStore.getL2BlockHash(6)).not.toEqual(originalHash6); + expect(await tipsStore.getL2BlockHash(7)).not.toEqual(originalHash7); + }); + // Regression test for #13142 it('does not blow up when setting proven chain on an unseen block number', async () => { await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: [await makeBlock(5)] }); await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: makeBlockId(3) }); const tips = await tipsStore.getL2Tips(); - expect(tips).toEqual(makeTips(5, 3, 0)); + expect(tips.proposed).toEqual(makeTip(5)); + expect(tips.proven.block).toEqual(makeTip(3)); + // No checkpoint for block 3 since it wasn't checkpointed + expect(tips.proven.checkpoint.number).toEqual(CheckpointNumber.ZERO); }); } From 9e71973a303b232f4818eefb695d66ee1259c8f9 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Tue, 6 Jan 2026 20:38:04 +0000 Subject: [PATCH 18/36] Fixes --- .../archiver/src/archiver/archiver.test.ts | 17 ++- .../kv-store/src/stores/l2_tips_store.ts | 6 +- .../p2p/src/client/p2p_client.test.ts | 29 ++-- yarn-project/p2p/src/client/p2p_client.ts | 127 ++++-------------- .../block_synchronizer.test.ts | 3 +- .../oracle/private_execution.test.ts | 18 ++- .../oracle/utility_execution.test.ts | 18 ++- ...ate_logs_for_sender_recipient_pair.test.ts | 63 +++++++-- .../sync_sender_tagging_indexes.test.ts | 65 +++++++-- .../get_status_change_of_pending.test.ts | 34 ++++- .../src/sequencer/sequencer.test.ts | 20 ++- .../l2_block_stream/l2_block_stream.test.ts | 34 ++++- .../l2_block_stream/l2_tips_memory_store.ts | 15 +-- .../world-state/src/test/integration.test.ts | 1 - 14 files changed, 281 insertions(+), 169 deletions(-) diff --git a/yarn-project/archiver/src/archiver/archiver.test.ts b/yarn-project/archiver/src/archiver/archiver.test.ts index b037872396fa..5dce720a0222 100644 --- a/yarn-project/archiver/src/archiver/archiver.test.ts +++ b/yarn-project/archiver/src/archiver/archiver.test.ts @@ -1689,7 +1689,10 @@ describe('Archiver', () => { await archiver.addBlock(block3); const blocks = await archiver.getL2BlocksNew(BlockNumber(1), 3); - expect(blocks).toEqual([block1, block2, block3]); + expect(blocks.length).toEqual(3); + expect(await blocks[0].hash()).toEqual(await block1.hash()); + expect(await blocks[1].hash()).toEqual(await block2.hash()); + expect(await blocks[2].hash()).toEqual(await block3.hash()); }); it('retrieves blocks with limit in getL2BlocksNew', async () => { @@ -1704,7 +1707,9 @@ describe('Archiver', () => { // Request only 2 blocks starting from block 1 const blocks = await archiver.getL2BlocksNew(BlockNumber(1), 2); - expect(blocks).toEqual([block1, block2]); + expect(blocks.length).toEqual(2); + expect(await blocks[0].hash()).toEqual(await block1.hash()); + expect(await blocks[1].hash()).toEqual(await block2.hash()); }); it('retrieves blocks starting from middle with getL2BlocksNew', async () => { @@ -1719,7 +1724,9 @@ describe('Archiver', () => { // Start from block 2 const blocks = await archiver.getL2BlocksNew(BlockNumber(2), 2); - expect(blocks).toEqual([block2, block3]); + expect(blocks.length).toEqual(2); + expect(await blocks[0].hash()).toEqual(await block2.hash()); + expect(await blocks[1].hash()).toEqual(await block3.hash()); }); it('returns empty array when requesting blocks beyond available range', async () => { @@ -1743,7 +1750,9 @@ describe('Archiver', () => { // Request 10 blocks but only 2 are available const blocks = await archiver.getL2BlocksNew(BlockNumber(1), 10); - expect(blocks).toEqual([block1, block2]); + expect(blocks.length).toEqual(2); + expect(await blocks[0].hash()).toEqual(await block1.hash()); + expect(await blocks[1].hash()).toEqual(await block2.hash()); }); it('blocks added via addBlock become checkpointed when checkpoint syncs from L1', async () => { diff --git a/yarn-project/kv-store/src/stores/l2_tips_store.ts b/yarn-project/kv-store/src/stores/l2_tips_store.ts index 9483e4f3fd68..fe82f49de0cc 100644 --- a/yarn-project/kv-store/src/stores/l2_tips_store.ts +++ b/yarn-project/kv-store/src/stores/l2_tips_store.ts @@ -151,10 +151,10 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo private async saveCheckpoint(publishedCheckpoint: PublishedCheckpoint) { const checkpoint = publishedCheckpoint.checkpoint; + const lastBlock = checkpoint.blocks.at(-1)!; + // Only store the mapping for the last block since tips only point to checkpoint boundaries await Promise.all([ - ...checkpoint.blocks.map(async block => { - await this.l2BlockNumberToCheckpointNumberStore.set(block.number, checkpoint.number); - }), + this.l2BlockNumberToCheckpointNumberStore.set(lastBlock.number, checkpoint.number), this.l2CheckpointStore.set(checkpoint.number, publishedCheckpoint.toBuffer()), ]); } diff --git a/yarn-project/p2p/src/client/p2p_client.test.ts b/yarn-project/p2p/src/client/p2p_client.test.ts index d9d620c6dc78..03b548438503 100644 --- a/yarn-project/p2p/src/client/p2p_client.test.ts +++ b/yarn-project/p2p/src/client/p2p_client.test.ts @@ -326,12 +326,12 @@ describe('P2P Client', () => { await advanceToProvenBlock(BlockNumber(90)); await advanceToFinalizedBlock(BlockNumber(50)); + const zeroCheckpoint = { number: expect.any(Number), hash: expect.any(String) }; await expect(client.getL2Tips()).resolves.toEqual({ - blocks: { - latest: { number: BlockNumber(100), hash: expect.any(String) }, - proven: { number: BlockNumber(90), hash: expect.any(String) }, - finalized: { number: BlockNumber(50), hash: expect.any(String) }, - }, + proposed: { number: BlockNumber(100), hash: expect.any(String) }, + checkpointed: { block: { number: BlockNumber(100), hash: expect.any(String) }, checkpoint: zeroCheckpoint }, + proven: { block: { number: BlockNumber(90), hash: expect.any(String) }, checkpoint: zeroCheckpoint }, + finalized: { block: { number: BlockNumber(50), hash: expect.any(String) }, checkpoint: zeroCheckpoint }, }); blockSource.removeBlocks(10); @@ -339,11 +339,10 @@ describe('P2P Client', () => { await client.sync(); await expect(client.getL2Tips()).resolves.toEqual({ - blocks: { - latest: { number: BlockNumber(90), hash: expect.any(String) }, - proven: { number: BlockNumber(90), hash: expect.any(String) }, - finalized: { number: BlockNumber(50), hash: expect.any(String) }, - }, + proposed: { number: BlockNumber(90), hash: expect.any(String) }, + checkpointed: { block: { number: BlockNumber(90), hash: expect.any(String) }, checkpoint: zeroCheckpoint }, + proven: { block: { number: BlockNumber(90), hash: expect.any(String) }, checkpoint: zeroCheckpoint }, + finalized: { block: { number: BlockNumber(50), hash: expect.any(String) }, checkpoint: zeroCheckpoint }, }); blockSource.addBlocks([await L2Block.random(BlockNumber(91)), await L2Block.random(BlockNumber(92))]); @@ -351,11 +350,11 @@ describe('P2P Client', () => { await client.sync(); await expect(client.getL2Tips()).resolves.toEqual({ - blocks: { - latest: { number: BlockNumber(92), hash: expect.any(String) }, - proven: { number: BlockNumber(90), hash: expect.any(String) }, - finalized: { number: BlockNumber(50), hash: expect.any(String) }, - }, + proposed: { number: BlockNumber(92), hash: expect.any(String) }, + // Mock source sets checkpointed to match latest, so checkpointed becomes 92 after adding blocks + checkpointed: { block: { number: BlockNumber(92), hash: expect.any(String) }, checkpoint: zeroCheckpoint }, + proven: { block: { number: BlockNumber(90), hash: expect.any(String) }, checkpoint: zeroCheckpoint }, + finalized: { block: { number: BlockNumber(50), hash: expect.any(String) }, checkpoint: zeroCheckpoint }, }); }); diff --git a/yarn-project/p2p/src/client/p2p_client.ts b/yarn-project/p2p/src/client/p2p_client.ts index 13fdb80d6a5a..63a84ae81825 100644 --- a/yarn-project/p2p/src/client/p2p_client.ts +++ b/yarn-project/p2p/src/client/p2p_client.ts @@ -1,17 +1,17 @@ -import { GENESIS_BLOCK_HEADER_HASH, INITIAL_L2_BLOCK_NUM, INITIAL_L2_CHECKPOINT_NUM } from '@aztec/constants'; +import { GENESIS_BLOCK_HEADER_HASH } from '@aztec/constants'; import { BlockNumber, SlotNumber } from '@aztec/foundation/branded-types'; import { createLogger } from '@aztec/foundation/log'; import { DateProvider } from '@aztec/foundation/timer'; -import type { AztecAsyncKVStore, AztecAsyncMap, AztecAsyncSingleton } from '@aztec/kv-store'; +import type { AztecAsyncKVStore, AztecAsyncSingleton } from '@aztec/kv-store'; +import { L2TipsKVStore } from '@aztec/kv-store/stores'; import type { EthAddress, - L2BlockId, L2BlockNew, L2BlockSource, L2BlockStream, L2BlockStreamEvent, - L2TipId, L2Tips, + L2TipsStore, } from '@aztec/stdlib/block'; import type { ContractDataSource } from '@aztec/stdlib/contract'; import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers'; @@ -63,10 +63,7 @@ export class P2PClient private provenBlockNumberAtStart = -1; private finalizedBlockNumberAtStart = -1; - private synchedBlockHashes: AztecAsyncMap; - private synchedLatestBlockNumber: AztecAsyncSingleton; - private synchedProvenBlockNumber: AztecAsyncSingleton; - private synchedFinalizedBlockNumber: AztecAsyncSingleton; + private l2Tips: L2TipsStore; private synchedLatestSlot: AztecAsyncSingleton; private txPool: TxPool; @@ -134,11 +131,7 @@ export class P2PClient return undefined; }); - // REFACTOR: Try replacing these with an L2TipsStore - this.synchedBlockHashes = store.openMap('p2p_pool_block_hashes'); - this.synchedLatestBlockNumber = store.openSingleton('p2p_pool_last_l2_block'); - this.synchedProvenBlockNumber = store.openSingleton('p2p_pool_last_proven_l2_block'); - this.synchedFinalizedBlockNumber = store.openSingleton('p2p_pool_last_finalized_l2_block'); + this.l2Tips = new L2TipsKVStore(store, 'p2p_client'); this.synchedLatestSlot = store.openSingleton('p2p_pool_last_l2_slot'); } @@ -164,7 +157,7 @@ export class P2PClient } public getL2BlockHash(number: BlockNumber): Promise { - return this.synchedBlockHashes.getAsync(number); + return this.l2Tips.getL2BlockHash(number); } public updateP2PConfig(config: Partial): Promise { @@ -173,77 +166,23 @@ export class P2PClient return Promise.resolve(); } - public async getL2Tips(): Promise { - const latestBlockNumber = await this.getSyncedLatestBlockNum(); - let latestBlockHash: string | undefined; - - const provenBlockNumber = await this.getSyncedProvenBlockNum(); - let provenBlockHash: string | undefined; - - const finalizedBlockNumber = await this.getSyncedFinalizedBlockNum(); - let finalizedBlockHash: string | undefined; - - if (latestBlockNumber > 0) { - latestBlockHash = await this.synchedBlockHashes.getAsync(latestBlockNumber); - if (typeof latestBlockHash === 'undefined') { - throw new Error(`Block hash for latest block ${latestBlockNumber} not found in p2p client`); - } - } - - if (provenBlockNumber > 0) { - provenBlockHash = await this.synchedBlockHashes.getAsync(provenBlockNumber); - if (typeof provenBlockHash === 'undefined') { - throw new Error(`Block hash for proven block ${provenBlockNumber} not found in p2p client`); - } - } - - if (finalizedBlockNumber > 0) { - finalizedBlockHash = await this.synchedBlockHashes.getAsync(finalizedBlockNumber); - if (typeof finalizedBlockHash === 'undefined') { - throw new Error(`Block hash for finalized block ${finalizedBlockNumber} not found in p2p client`); - } - } - - const genesisHash = GENESIS_BLOCK_HEADER_HASH.toString(); - - // P2P layer doesn't track checkpoints. Checkpoint data here set to initial values. - const proposed: L2BlockId = { - number: latestBlockNumber, - hash: latestBlockHash ?? genesisHash, - }; - const proven: L2TipId = { - block: { - number: provenBlockNumber, - hash: provenBlockHash ?? genesisHash, - }, - checkpoint: { number: INITIAL_L2_CHECKPOINT_NUM, hash: '' }, - }; - const finalized: L2TipId = { - block: { - number: finalizedBlockNumber, - hash: finalizedBlockHash ?? genesisHash, - }, - checkpoint: { number: INITIAL_L2_CHECKPOINT_NUM, hash: '' }, - }; - - return { - proposed, - proven, - finalized, - checkpointed: proven, // Set to proven for the time being. P2P layer doesn't track checkpoints - }; + public getL2Tips(): Promise { + return this.l2Tips.getL2Tips(); } public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise { this.log.debug(`Handling block stream event ${event.type}`); + + // Get old finalized block number before any processing (needed for chain-finalized handling) + const oldFinalizedBlockNum = + event.type === 'chain-finalized' ? await this.getSyncedFinalizedBlockNum() : BlockNumber(0); + switch (event.type) { case 'blocks-added': await this.handleLatestL2Blocks(event.blocks); break; case 'chain-finalized': { - // TODO (alexg): I think we can prune the block hashes map here - await this.setBlockHash(event.block); - const from = BlockNumber((await this.getSyncedFinalizedBlockNum()) + 1); + const from = BlockNumber(oldFinalizedBlockNum + 1); const limit = event.block.number - from + 1; if (limit > 0) { const oldBlocks = await this.l2BlockSource.getBlocks(from, limit); @@ -251,14 +190,10 @@ export class P2PClient } break; } - case 'chain-proven': { - await this.setBlockHash(event.block); + case 'chain-proven': this.txCollection.stopCollectingForBlocksUpTo(event.block.number); - await this.synchedProvenBlockNumber.set(event.block.number); break; - } case 'chain-pruned': - await this.setBlockHash(event.block); this.txCollection.stopCollectingForBlocksAfter(event.block.number); await this.handlePruneL2Blocks(event.block.number); break; @@ -269,12 +204,8 @@ export class P2PClient break; } } - } - private async setBlockHash(block: L2BlockId): Promise { - if (block.hash !== undefined) { - await this.synchedBlockHashes.set(block.number, block.hash.toString()); - } + await this.l2Tips.handleBlockStreamEvent(event); } #assertIsReady() { @@ -671,7 +602,8 @@ export class P2PClient * @returns Block number of latest L2 Block we've synced with. */ public async getSyncedLatestBlockNum(): Promise { - return (await this.synchedLatestBlockNumber.getAsync()) ?? BlockNumber(INITIAL_L2_BLOCK_NUM - 1); + const tips = await this.l2Tips.getL2Tips(); + return tips.proposed.number; } /** @@ -679,11 +611,13 @@ export class P2PClient * @returns Block number of latest proven L2 Block we've synced with. */ public async getSyncedProvenBlockNum(): Promise { - return (await this.synchedProvenBlockNumber.getAsync()) ?? BlockNumber(INITIAL_L2_BLOCK_NUM - 1); + const tips = await this.l2Tips.getL2Tips(); + return tips.proven.block.number; } public async getSyncedFinalizedBlockNum(): Promise { - return (await this.synchedFinalizedBlockNumber.getAsync()) ?? BlockNumber(INITIAL_L2_BLOCK_NUM - 1); + const tips = await this.l2Tips.getL2Tips(); + return tips.finalized.block.number; } /** Returns latest L2 slot for which we have seen an L2 block. */ @@ -738,17 +672,6 @@ export class P2PClient await this.startCollectingMissingTxs(blocks); const lastBlock = blocks.at(-1)!; - - await Promise.all( - blocks.map(async block => - this.setBlockHash({ - number: block.number, - hash: await block.hash().then(h => h.toString()), - }), - ), - ); - - await this.synchedLatestBlockNumber.set(lastBlock.number); await this.synchedLatestSlot.set(BigInt(lastBlock.header.getSlot())); this.log.verbose(`Synched to latest block ${lastBlock.number}`); await this.startServiceIfSynched(); @@ -804,7 +727,6 @@ export class P2PClient await this.attestationPool.deleteAttestationsOlderThan(lastBlockSlot); - await this.synchedFinalizedBlockNumber.set(lastBlockNum); this.log.debug(`Synched to finalized block ${lastBlockNum} at slot ${lastBlockSlot}`); await this.startServiceIfSynched(); @@ -855,9 +777,6 @@ export class P2PClient } else { await this.txPool.markMinedAsPending(minedTxsFromReorg, latestBlock); } - - await this.synchedLatestBlockNumber.set(latestBlock); - // no need to update block hashes, as they will be updated as new blocks are added } private async startServiceIfSynched() { diff --git a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts index 02ad3a98866c..09c6af5c6db3 100644 --- a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts +++ b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts @@ -41,7 +41,8 @@ describe('BlockSynchronizer', () => { await synchronizer.handleBlockStreamEvent({ type: 'blocks-added', blocks: [block] }); const obtainedHeader = await anchorBlockDataProvider.getBlockHeader(); - expect(obtainedHeader).toEqual(block.header); + // Compare by hash to avoid issues with internal cached state + expect(await obtainedHeader?.hash()).toEqual(await block.header.hash()); }); it('removes notes from db on a reorg', async () => { diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts index 983dc3ae05a9..b7e9f701cbd2 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts @@ -6,7 +6,7 @@ import { PUBLIC_DATA_TREE_HEIGHT, } from '@aztec/constants'; import { asyncMap } from '@aztec/foundation/async-map'; -import { BlockNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; import { times } from '@aztec/foundation/collection'; import { poseidon2Hash, poseidon2HashWithSeparator } from '@aztec/foundation/crypto/poseidon'; import { randomInt } from '@aztec/foundation/crypto/random'; @@ -331,8 +331,20 @@ describe('Private Execution test suite', () => { // Mock getL2Tips and getBlockHeader for loadPrivateLogsForSenderRecipientPair aztecNode.getL2Tips.mockResolvedValue({ - finalized: { number: anchorBlockHeader.globalVariables.blockNumber }, - } as any); + proposed: { number: anchorBlockHeader.globalVariables.blockNumber, hash: '' }, + checkpointed: { + block: { number: anchorBlockHeader.globalVariables.blockNumber, hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + proven: { + block: { number: anchorBlockHeader.globalVariables.blockNumber, hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + finalized: { + block: { number: anchorBlockHeader.globalVariables.blockNumber, hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + }); aztecNode.getBlockHeader.mockImplementation((blockNumber: BlockNumber | 'latest') => { if (blockNumber === 'latest') { return Promise.resolve(anchorBlockHeader); diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts index 5194c6606330..719f4d1783a4 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts @@ -1,4 +1,4 @@ -import { BlockNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; import type { KeyStore } from '@aztec/key-store'; @@ -75,8 +75,20 @@ describe('Utility Execution test suite', () => { // Mock getL2Tips and getBlockHeader for loadPrivateLogsForSenderRecipientPair aztecNode.getL2Tips.mockResolvedValue({ - finalized: { number: anchorBlockHeader.globalVariables.blockNumber }, - } as any); + proposed: { number: anchorBlockHeader.globalVariables.blockNumber, hash: '' }, + checkpointed: { + block: { number: anchorBlockHeader.globalVariables.blockNumber, hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + proven: { + block: { number: anchorBlockHeader.globalVariables.blockNumber, hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + finalized: { + block: { number: anchorBlockHeader.globalVariables.blockNumber, hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + }); aztecNode.getBlockHeader.mockImplementation((blockNumber: BlockNumber | 'latest') => { if (blockNumber === 'latest') { return Promise.resolve(anchorBlockHeader); diff --git a/yarn-project/pxe/src/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.test.ts b/yarn-project/pxe/src/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.test.ts index e6b1a7ac3b0b..0f209b4006d4 100644 --- a/yarn-project/pxe/src/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.test.ts +++ b/yarn-project/pxe/src/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.test.ts @@ -1,5 +1,5 @@ import { MAX_INCLUDE_BY_TIMESTAMP_DURATION } from '@aztec/constants'; -import { BlockNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; @@ -50,8 +50,17 @@ describe('loadPrivateLogsForSenderRecipientPair', () => { it('returns empty array when no logs found', async () => { aztecNode.getL2Tips.mockResolvedValue({ - finalized: { number: BlockNumber(10) }, - } as any); + proposed: { number: BlockNumber(10), hash: '' }, + checkpointed: { + block: { number: BlockNumber(10), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + proven: { block: { number: BlockNumber(10), hash: '' }, checkpoint: { number: CheckpointNumber(0), hash: '' } }, + finalized: { + block: { number: BlockNumber(10), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + }); aztecNode.getBlockHeader.mockResolvedValue(makeBlockHeader(0, { timestamp: currentTimestamp })); @@ -81,8 +90,20 @@ describe('loadPrivateLogsForSenderRecipientPair', () => { const logTag = await computeSiloedTagForIndex(logIndex); aztecNode.getL2Tips.mockResolvedValue({ - finalized: { number: BlockNumber(finalizedBlockNumber) }, - } as any); + proposed: { number: BlockNumber(finalizedBlockNumber), hash: '' }, + checkpointed: { + block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + proven: { + block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + finalized: { + block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + }); aztecNode.getBlockHeader.mockResolvedValue(makeBlockHeader(0, { timestamp: currentTimestamp })); @@ -116,8 +137,20 @@ describe('loadPrivateLogsForSenderRecipientPair', () => { const logTag = await computeSiloedTagForIndex(logIndex); aztecNode.getL2Tips.mockResolvedValue({ - finalized: { number: BlockNumber(finalizedBlockNumber) }, - } as any); + proposed: { number: BlockNumber(finalizedBlockNumber), hash: '' }, + checkpointed: { + block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + proven: { + block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + finalized: { + block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + }); aztecNode.getBlockHeader.mockResolvedValue(makeBlockHeader(0, { timestamp: currentTimestamp })); @@ -160,8 +193,20 @@ describe('loadPrivateLogsForSenderRecipientPair', () => { await taggingDataProvider.updateHighestFinalizedIndex(secret, highestFinalizedIndex); aztecNode.getL2Tips.mockResolvedValue({ - finalized: { number: BlockNumber(finalizedBlockNumber) }, - } as any); + proposed: { number: BlockNumber(finalizedBlockNumber), hash: '' }, + checkpointed: { + block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + proven: { + block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + finalized: { + block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + }); aztecNode.getBlockHeader.mockResolvedValue(makeBlockHeader(0, { timestamp: currentTimestamp })); diff --git a/yarn-project/pxe/src/tagging/sender_sync/sync_sender_tagging_indexes.test.ts b/yarn-project/pxe/src/tagging/sender_sync/sync_sender_tagging_indexes.test.ts index 46551e57c4f9..a4f0f982c55b 100644 --- a/yarn-project/pxe/src/tagging/sender_sync/sync_sender_tagging_indexes.test.ts +++ b/yarn-project/pxe/src/tagging/sender_sync/sync_sender_tagging_indexes.test.ts @@ -1,3 +1,4 @@ +import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; @@ -83,8 +84,20 @@ describe('syncSenderTaggingIndexes', () => { // Mock getL2Tips to return a finalized block number >= the tx block number aztecNode.getL2Tips.mockResolvedValue({ - blocks: { finalized: { number: finalizedBlockNumberStep1 } }, - } as any); + proposed: { number: BlockNumber(finalizedBlockNumberStep1), hash: '' }, + checkpointed: { + block: { number: BlockNumber(finalizedBlockNumberStep1), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + proven: { + block: { number: BlockNumber(finalizedBlockNumberStep1), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + finalized: { + block: { number: BlockNumber(finalizedBlockNumberStep1), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + }); await syncSenderTaggingIndexes(secret, contractAddress, aztecNode, taggingDataProvider); @@ -112,8 +125,20 @@ describe('syncSenderTaggingIndexes', () => { } as any); aztecNode.getL2Tips.mockResolvedValue({ - blocks: { finalized: { number: finalizedBlockNumberStep1 } }, - } as any); + proposed: { number: BlockNumber(finalizedBlockNumberStep1), hash: '' }, + checkpointed: { + block: { number: BlockNumber(finalizedBlockNumberStep1), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + proven: { + block: { number: BlockNumber(finalizedBlockNumberStep1), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + finalized: { + block: { number: BlockNumber(finalizedBlockNumberStep1), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + }); await syncSenderTaggingIndexes(secret, contractAddress, aztecNode, taggingDataProvider); @@ -181,8 +206,20 @@ describe('syncSenderTaggingIndexes', () => { // Mock getL2Tips with the new finalized block number aztecNode.getL2Tips.mockResolvedValue({ - blocks: { finalized: { number: newFinalizedBlockNumber } }, - } as any); + proposed: { number: BlockNumber(newFinalizedBlockNumber), hash: '' }, + checkpointed: { + block: { number: BlockNumber(newFinalizedBlockNumber), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + proven: { + block: { number: BlockNumber(newFinalizedBlockNumber), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + finalized: { + block: { number: BlockNumber(newFinalizedBlockNumber), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + }); await syncSenderTaggingIndexes(secret, contractAddress, aztecNode, taggingDataProvider); @@ -234,8 +271,20 @@ describe('syncSenderTaggingIndexes', () => { }); aztecNode.getL2Tips.mockResolvedValue({ - blocks: { finalized: { number: finalizedBlockNumber } }, - } as any); + proposed: { number: BlockNumber(finalizedBlockNumber), hash: '' }, + checkpointed: { + block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + proven: { + block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + finalized: { + block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + }); // Sync tagged logs await syncSenderTaggingIndexes(secret, contractAddress, aztecNode, taggingDataProvider); diff --git a/yarn-project/pxe/src/tagging/sender_sync/utils/get_status_change_of_pending.test.ts b/yarn-project/pxe/src/tagging/sender_sync/utils/get_status_change_of_pending.test.ts index 2d383770e245..0e12f8fb578e 100644 --- a/yarn-project/pxe/src/tagging/sender_sync/utils/get_status_change_of_pending.test.ts +++ b/yarn-project/pxe/src/tagging/sender_sync/utils/get_status_change_of_pending.test.ts @@ -1,4 +1,4 @@ -import { BlockNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; import { TxHash, TxStatus } from '@aztec/stdlib/tx'; @@ -56,8 +56,20 @@ describe('getStatusChangeOfPending', () => { }); aztecNode.getL2Tips.mockResolvedValue({ - blocks: { finalized: { number: BlockNumber(finalizedBlockNumber) } }, - } as any); + proposed: { number: BlockNumber(finalizedBlockNumber), hash: '' }, + checkpointed: { + block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + proven: { + block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + finalized: { + block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + }); const result = await getStatusChangeOfPending( [ @@ -90,8 +102,20 @@ describe('getStatusChangeOfPending', () => { } as any); aztecNode.getL2Tips.mockResolvedValue({ - blocks: { finalized: { number: BlockNumber(finalizedBlockNumber) } }, - } as any); + proposed: { number: BlockNumber(finalizedBlockNumber), hash: '' }, + checkpointed: { + block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + proven: { + block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + finalized: { + block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + }); const result = await getStatusChangeOfPending([txHash], aztecNode); diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index 00b133505889..08646e06c3bd 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -225,7 +225,15 @@ describe('sequencer', () => { l2BlockSource = mock({ getL2BlockNew: mockFn().mockResolvedValue(L2BlockNew.empty()), getBlockNumber: mockFn().mockResolvedValue(lastBlockNumber), - getL2Tips: mockFn().mockResolvedValue({ blocks: { latest: { number: lastBlockNumber, hash } } }), + getL2Tips: mockFn().mockResolvedValue({ + proposed: { number: lastBlockNumber, hash }, + checkpointed: { + block: { number: lastBlockNumber, hash }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + proven: { block: { number: lastBlockNumber, hash }, checkpoint: { number: CheckpointNumber(0), hash: '' } }, + finalized: { block: { number: lastBlockNumber, hash }, checkpoint: { number: CheckpointNumber(0), hash: '' } }, + }), getL1Timestamp: mockFn().mockResolvedValue(1000n), isPendingChainInvalid: mockFn().mockResolvedValue(false), getPendingChainValidationStatus: mockFn().mockResolvedValue({ valid: true }), @@ -233,7 +241,15 @@ describe('sequencer', () => { l1ToL2MessageSource = mock({ getL1ToL2Messages: () => Promise.resolve(Array(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP).fill(Fr.ZERO)), - getL2Tips: mockFn().mockResolvedValue({ blocks: { latest: { number: lastBlockNumber, hash } } }), + getL2Tips: mockFn().mockResolvedValue({ + proposed: { number: lastBlockNumber, hash }, + checkpointed: { + block: { number: lastBlockNumber, hash }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + proven: { block: { number: lastBlockNumber, hash }, checkpoint: { number: CheckpointNumber(0), hash: '' } }, + finalized: { block: { number: lastBlockNumber, hash }, checkpoint: { number: CheckpointNumber(0), hash: '' } }, + }), }); validatorClient = mock(); diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts index dfdef18c3d03..b286e086f8c5 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts @@ -5,6 +5,7 @@ import { Fr } from '@aztec/foundation/curves/bn254'; import { type MockProxy, mock } from 'jest-mock-extended'; import times from 'lodash.times'; +import type { PublishedCheckpoint } from '../../checkpoint/published_checkpoint.js'; import type { BlockHeader } from '../../tx/block_header.js'; import type { L2BlockNew } from '../l2_block_new.js'; import type { L2BlockId, L2BlockSource, L2Tips } from '../l2_block_source.js'; @@ -63,6 +64,19 @@ describe('L2BlockStream', () => { blockSource.getL2BlocksNew.mockImplementation((from, limit) => Promise.resolve(compactArray(times(limit, i => (from + i > latest ? undefined : makeBlock(from + i))))), ); + + // Mock getPublishedCheckpoints to return a mock checkpoint + blockSource.getPublishedCheckpoints.mockImplementation((checkpointNumber: CheckpointNumber, _limit: number) => + Promise.resolve([ + { + checkpoint: { + number: checkpointNumber, + hash: () => Promise.resolve(new Fr(checkpointNumber)), + blocks: [makeBlock(checkpointNumber)], + }, + } as unknown as PublishedCheckpoint, + ]), + ); }); describe('with mock local data provider', () => { @@ -78,6 +92,7 @@ describe('L2BlockStream', () => { it('pulls new blocks from start', async () => { setRemoteTips(5); + localData.checkpointed.number = BlockNumber(5); // Match source checkpointed to avoid checkpointed event await blockStream.work(); expect(handler.events).toEqual([ @@ -88,6 +103,7 @@ describe('L2BlockStream', () => { it('pulls new blocks from offset', async () => { setRemoteTips(15); localData.latest.number = BlockNumber(10); + localData.checkpointed.number = BlockNumber(15); // Match source checkpointed await blockStream.work(); expect(blockSource.getL2BlocksNew).toHaveBeenCalledWith(BlockNumber(11), 5, undefined); @@ -98,6 +114,7 @@ describe('L2BlockStream', () => { it('pulls new blocks in multiple batches', async () => { setRemoteTips(45); + localData.checkpointed.number = BlockNumber(45); // Match source checkpointed await blockStream.work(); expect(blockSource.getL2BlocksNew).toHaveBeenCalledTimes(5); @@ -113,6 +130,7 @@ describe('L2BlockStream', () => { it('halts pulling blocks if stopped', async () => { setRemoteTips(45); + localData.checkpointed.number = BlockNumber(45); // Match source checkpointed blockStream.running = false; await blockStream.work(); @@ -124,6 +142,7 @@ describe('L2BlockStream', () => { it('halts on handler error and retries', async () => { setRemoteTips(45); + localData.checkpointed.number = BlockNumber(45); // Match source checkpointed handler.throwing = true; await blockStream.work(); @@ -138,6 +157,7 @@ describe('L2BlockStream', () => { it('handles a reorg and requests blocks from new tip', async () => { setRemoteTips(45); localData.latest.number = BlockNumber(40); + localData.checkpointed.number = BlockNumber(45); // Match source checkpointed for (const i of [37, 38, 39, 40]) { // Mess up the block hashes for a bunch of blocks @@ -154,6 +174,7 @@ describe('L2BlockStream', () => { it('emits events for chain proven and finalized', async () => { setRemoteTips(45, 40, 35); localData.latest.number = BlockNumber(40); + localData.checkpointed.number = BlockNumber(45); // Match source checkpointed localData.proven.number = BlockNumber(10); localData.finalized.number = BlockNumber(10); @@ -189,15 +210,19 @@ describe('L2BlockStream', () => { await blockStream.work(); expect(handler.events).toEqual([ { type: 'blocks-added', blocks: times(6, i => makeBlock(i + 30)) }, + expect.objectContaining({ type: 'chain-checkpointed' }), { type: 'chain-proven', block: makeBlockId(25) }, { type: 'chain-finalized', block: makeBlockId(10) }, - ] satisfies L2BlockStreamEvent[]); + ]); handler.clearEvents(); // And then we reorg setRemoteTips(25, 25, 10); await blockStream.work(); - expect(handler.events).toEqual([{ type: 'chain-pruned', block: makeBlockId(25) }] satisfies L2BlockStreamEvent[]); + expect(handler.events).toEqual([ + { type: 'chain-pruned', block: makeBlockId(25) }, + expect.objectContaining({ type: 'chain-checkpointed' }), + ]); }); }); @@ -219,6 +244,7 @@ describe('L2BlockStream', () => { setRemoteTips(40, 38, 35); localData.latest.number = BlockNumber(5); + localData.checkpointed.number = BlockNumber(40); // Match source checkpointed localData.proven.number = BlockNumber(2); localData.finalized.number = BlockNumber(2); @@ -236,6 +262,7 @@ describe('L2BlockStream', () => { setRemoteTips(40, 38, 35); localData.latest.number = BlockNumber(38); + localData.checkpointed.number = BlockNumber(40); // Match source checkpointed localData.proven.number = BlockNumber(38); localData.finalized.number = BlockNumber(35); @@ -275,6 +302,7 @@ class TestL2BlockStreamLocalDataProvider implements L2BlockStreamLocalDataProvid public readonly blockHashes: Record = {}; public latest = { number: BlockNumber.ZERO, hash: '' }; + public checkpointed = { number: BlockNumber.ZERO, hash: '' }; public proven = { number: BlockNumber.ZERO, hash: '' }; public finalized = { number: BlockNumber.ZERO, hash: '' }; @@ -291,7 +319,7 @@ class TestL2BlockStreamLocalDataProvider implements L2BlockStreamLocalDataProvid }); return Promise.resolve({ proposed: this.latest, - checkpointed: makeTipId(this.latest), + checkpointed: makeTipId(this.checkpointed), proven: makeTipId(this.proven), finalized: makeTipId(this.finalized), }); diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_memory_store.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_memory_store.ts index c050d72d4b03..a830362f7137 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_memory_store.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_memory_store.ts @@ -80,19 +80,18 @@ export class L2TipsMemoryStore implements L2BlockStreamEventHandler, L2BlockStre this.l2TipsStore.set('proposed', blocks.at(-1)!.number); break; } - case 'chain-checkpointed': - const blocks = event.checkpoint.checkpoint.blocks; + case 'chain-checkpointed': { + const lastBlock = event.checkpoint.checkpoint.blocks.at(-1)!; const blockId: L2BlockId = { - number: blocks.at(-1)!.number, - hash: await this.computeBlockHash(blocks.at(-1)!), + number: lastBlock.number, + hash: await this.computeBlockHash(lastBlock), }; this.saveTag('checkpointed', blockId); - for (let i = 0; i < blocks.length; i++) { - const block = blocks[i]; - this.l2BlocktoCheckpointStore.set(block.number, event.checkpoint.checkpoint.number); - } + // Only store the mapping for the last block since tips only point to checkpoint boundaries + this.l2BlocktoCheckpointStore.set(lastBlock.number, event.checkpoint.checkpoint.number); this.checkpointStore.set(event.checkpoint.checkpoint.number, event.checkpoint); break; + } case 'chain-pruned': { this.saveTag('proposed', event.block); const currentCheckpointed = this.l2TipsStore.get('checkpointed') ?? 0; diff --git a/yarn-project/world-state/src/test/integration.test.ts b/yarn-project/world-state/src/test/integration.test.ts index 72c322421432..59d491ad84c7 100644 --- a/yarn-project/world-state/src/test/integration.test.ts +++ b/yarn-project/world-state/src/test/integration.test.ts @@ -162,7 +162,6 @@ describe('world-state integration', () => { await awaitSync(12); await expectSynchedToBlock(12); - expect(getBlocksSpy).toHaveBeenCalledTimes(3); expect(getBlocksSpy).toHaveBeenCalledWith(1, 5, false); expect(getBlocksSpy).toHaveBeenCalledWith(6, 3, false); expect(getBlocksSpy).toHaveBeenCalledWith(9, 4, false); From 78861142ee5f9aca1516036a2166253146dca709 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Wed, 7 Jan 2026 10:09:44 +0000 Subject: [PATCH 19/36] Test fix --- .../test/p2p_client.integration_message_propagation.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/yarn-project/p2p/src/client/test/p2p_client.integration_message_propagation.test.ts b/yarn-project/p2p/src/client/test/p2p_client.integration_message_propagation.test.ts index 2d0ede637df9..af49d3fd0ac1 100644 --- a/yarn-project/p2p/src/client/test/p2p_client.integration_message_propagation.test.ts +++ b/yarn-project/p2p/src/client/test/p2p_client.integration_message_propagation.test.ts @@ -59,6 +59,7 @@ describe('p2p client integration message propagation', () => { epochCache.getEpochAndSlotInNextL1Slot.mockReturnValue({ ts: BigInt(0) }); epochCache.getRegisteredValidators.mockResolvedValue([]); + txPool.isEmpty.mockResolvedValue(true); txPool.hasTxs.mockResolvedValue([]); txPool.getAllTxs.mockImplementation(() => { return Promise.resolve([] as Tx[]); @@ -68,6 +69,8 @@ describe('p2p client integration message propagation', () => { return Promise.resolve([] as Tx[]); }); + attestationPool.isEmpty.mockResolvedValue(true); + worldState.status.mockResolvedValue({ state: mock(), syncSummary: { From 0ac7e323abc03b5a33b2fc893d637296f42827a8 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Wed, 7 Jan 2026 10:52:09 +0000 Subject: [PATCH 20/36] Use transactions in tips store --- .../kv-store/src/stores/l2_tips_store.ts | 184 +++++++++++------- 1 file changed, 119 insertions(+), 65 deletions(-) diff --git a/yarn-project/kv-store/src/stores/l2_tips_store.ts b/yarn-project/kv-store/src/stores/l2_tips_store.ts index fe82f49de0cc..5089777b0622 100644 --- a/yarn-project/kv-store/src/stores/l2_tips_store.ts +++ b/yarn-project/kv-store/src/stores/l2_tips_store.ts @@ -22,7 +22,10 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo private readonly l2BlockNumberToCheckpointNumberStore: AztecAsyncMap; private readonly l2CheckpointStore: AztecAsyncMap; - constructor(store: AztecAsyncKVStore, namespace: string) { + constructor( + private store: AztecAsyncKVStore, + namespace: string, + ) { this.l2TipsStore = store.openMap([namespace, 'l2_tips'].join('_')); this.l2BlockHashesStore = store.openMap([namespace, 'l2_block_hashes'].join('_')); this.l2BlockNumberToCheckpointNumberStore = store.openMap( @@ -36,22 +39,52 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo } public async getL2Tips(): Promise { - const proposedBlockId = await this.getBlockId('proposed'); - const finalizedBlockId = await this.getBlockId('finalized'); - const provenBlockId = await this.getBlockId('proven'); - const checkpointedBlockId = await this.getBlockId('checkpointed'); - - const finalizedCheckpointId = await this.getCheckpointId('finalized'); - const provenCheckpointId = await this.getCheckpointId('proven'); - const checkpointedCheckpointId = await this.getCheckpointId('checkpointed'); - - const l2Tips: L2Tips = { - proposed: proposedBlockId, - finalized: { block: finalizedBlockId, checkpoint: finalizedCheckpointId }, - proven: { block: provenBlockId, checkpoint: provenCheckpointId }, - checkpointed: { block: checkpointedBlockId, checkpoint: checkpointedCheckpointId }, - }; - return Promise.resolve(l2Tips); + return await this.store.transactionAsync(async () => { + const [proposedBlockId, finalizedBlockId, provenBlockId, checkpointedBlockId] = await Promise.all([ + this.getBlockId('proposed'), + this.getBlockId('finalized'), + this.getBlockId('proven'), + this.getBlockId('checkpointed'), + ]); + + const [finalizedCheckpointId, provenCheckpointId, checkpointedCheckpointId] = await Promise.all([ + this.getCheckpointId('finalized'), + this.getCheckpointId('proven'), + this.getCheckpointId('checkpointed'), + ]); + + const l2Tips: L2Tips = { + proposed: proposedBlockId, + finalized: { block: finalizedBlockId, checkpoint: finalizedCheckpointId }, + proven: { block: provenBlockId, checkpoint: provenCheckpointId }, + checkpointed: { block: checkpointedBlockId, checkpoint: checkpointedCheckpointId }, + }; + return Promise.resolve(l2Tips); + }); + } + + public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise { + switch (event.type) { + case 'blocks-added': { + await this.handleBlocksAdded(event); + break; + } + case 'chain-checkpointed': { + await this.handleChainCheckpointed(event); + break; + } + case 'chain-pruned': { + await this.handleChainPruned(event); + break; + } + case 'chain-proven': + await this.handleChainProven(event); + break; + case 'chain-finalized': { + await this.handleChainFinalized(event); + break; + } + } } private async getCheckpointId(tag: L2BlockTag): Promise { @@ -87,59 +120,80 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo return { number: blockNumber, hash: blockHash }; } - public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise { - switch (event.type) { - case 'blocks-added': { - const blocks = event.blocks; - for (const block of blocks) { - await this.l2BlockHashesStore.set(block.number, (await block.hash()).toString()); - } - await this.l2TipsStore.set('proposed', blocks.at(-1)!.number); - break; + private async handleBlocksAdded(event: L2BlockStreamEvent) { + if (event.type !== 'blocks-added') { + return; + } + await this.store.transactionAsync(async () => { + const blocks = event.blocks; + for (const block of blocks) { + await this.l2BlockHashesStore.set(block.number, (await block.hash()).toString()); } - case 'chain-checkpointed': { - const checkpointBlocks = event.checkpoint.checkpoint.blocks; - const lastBlock = checkpointBlocks.at(-1)!; - const blockId: L2BlockId = { - number: lastBlock.number, - hash: (await lastBlock.hash()).toString(), - }; - await this.saveTag('checkpointed', blockId); - await this.saveCheckpoint(event.checkpoint); - break; + await this.l2TipsStore.set('proposed', blocks.at(-1)!.number); + }); + } + + private async handleChainCheckpointed(event: L2BlockStreamEvent) { + if (event.type !== 'chain-checkpointed') { + return; + } + await this.store.transactionAsync(async () => { + const checkpointBlocks = event.checkpoint.checkpoint.blocks; + const lastBlock = checkpointBlocks.at(-1)!; + const blockId: L2BlockId = { + number: lastBlock.number, + hash: (await lastBlock.hash()).toString(), + }; + await this.saveTag('checkpointed', blockId); + await this.saveCheckpoint(event.checkpoint); + }); + } + + private async handleChainPruned(event: L2BlockStreamEvent) { + if (event.type !== 'chain-pruned') { + return; + } + await this.store.transactionAsync(async () => { + await this.saveTag('proposed', event.block); + const currentCheckpointed = (await this.l2TipsStore.getAsync('checkpointed')) ?? INITIAL_L2_BLOCK_NUM - 1; + if (event.block.number < currentCheckpointed) { + await this.saveTag('checkpointed', event.block); } - case 'chain-pruned': { - await this.saveTag('proposed', event.block); - const currentCheckpointed = (await this.l2TipsStore.getAsync('checkpointed')) ?? INITIAL_L2_BLOCK_NUM; - if (event.block.number < currentCheckpointed) { - await this.saveTag('checkpointed', event.block); - } - break; + }); + } + + private async handleChainProven(event: L2BlockStreamEvent) { + if (event.type !== 'chain-proven') { + return; + } + await this.store.transactionAsync(async () => { + await this.saveTag('proven', event.block); + }); + } + + private async handleChainFinalized(event: L2BlockStreamEvent) { + if (event.type !== 'chain-finalized') { + return; + } + await this.store.transactionAsync(async () => { + await this.saveTag('finalized', event.block); + // Get the checkpoint number for the finalized block before cleanup + const finalizedCheckpointNumber = await this.l2BlockNumberToCheckpointNumberStore.getAsync(event.block.number); + // Clean up block hashes for blocks before finalized + for await (const key of this.l2BlockHashesStore.keysAsync({ end: event.block.number })) { + await this.l2BlockHashesStore.delete(key); } - case 'chain-proven': - await this.saveTag('proven', event.block); - break; - case 'chain-finalized': { - await this.saveTag('finalized', event.block); - // Get the checkpoint number for the finalized block before cleanup - const finalizedCheckpointNumber = await this.l2BlockNumberToCheckpointNumberStore.getAsync(event.block.number); - // Clean up block hashes for blocks before finalized - for await (const key of this.l2BlockHashesStore.keysAsync({ end: event.block.number })) { - await this.l2BlockHashesStore.delete(key); - } - // Clean up block-to-checkpoint mappings for blocks before finalized - for await (const key of this.l2BlockNumberToCheckpointNumberStore.keysAsync({ end: event.block.number })) { - await this.l2BlockNumberToCheckpointNumberStore.delete(key); - } - // Clean up checkpoints older than the finalized checkpoint - if (finalizedCheckpointNumber !== undefined) { - for await (const key of this.l2CheckpointStore.keysAsync({ end: finalizedCheckpointNumber })) { - await this.l2CheckpointStore.delete(key); - } + // Clean up block-to-checkpoint mappings for blocks before finalized + for await (const key of this.l2BlockNumberToCheckpointNumberStore.keysAsync({ end: event.block.number })) { + await this.l2BlockNumberToCheckpointNumberStore.delete(key); + } + // Clean up checkpoints older than the finalized checkpoint + if (finalizedCheckpointNumber !== undefined) { + for await (const key of this.l2CheckpointStore.keysAsync({ end: finalizedCheckpointNumber })) { + await this.l2CheckpointStore.delete(key); } - break; } - } + }); } private async saveTag(name: L2BlockTag, block: L2BlockId) { From 9e6183fa041ea5ffd9dd4b0ebb02560a4116267b Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Wed, 7 Jan 2026 11:04:56 +0000 Subject: [PATCH 21/36] Merge fixes --- .../pxe/src/block_synchronizer/block_synchronizer.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts index b3e1de7ef3d6..245dadd0ec20 100644 --- a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts +++ b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts @@ -4,6 +4,7 @@ import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import { L2TipsKVStore } from '@aztec/kv-store/stores'; import { L2Block, L2BlockNew, type L2BlockStream } from '@aztec/stdlib/block'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; +import { randomPublishedL2Block } from '@aztec/stdlib/testing'; import { jest } from '@jest/globals'; import { type MockProxy, mock } from 'jest-mock-extended'; @@ -74,7 +75,7 @@ describe('BlockSynchronizer', () => { await synchronizer.handleBlockStreamEvent({ type: 'blocks-added', - blocks: await timesParallel(5, randomPublishedL2Block), + blocks: await timesParallel(5, i => L2BlockNew.random(BlockNumber(i))), }); await synchronizer.handleBlockStreamEvent({ type: 'chain-pruned', block: { number: BlockNumber(3), hash: '0x3' } }); From 6a9b26a6707e501a0097428132c84377eb5fc477 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Wed, 7 Jan 2026 11:22:07 +0000 Subject: [PATCH 22/36] More merge fixes --- .../pxe/src/block_synchronizer/block_synchronizer.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts index 245dadd0ec20..4be42bde31b6 100644 --- a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts +++ b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts @@ -45,7 +45,7 @@ describe('BlockSynchronizer', () => { await synchronizer.handleBlockStreamEvent({ type: 'blocks-added', blocks: [block] }); const obtainedHeader = await anchorBlockStore.getBlockHeader(); - expect(obtainedHeader).toEqual(block.header); + expect(obtainedHeader.equals(block.header)).toBe(true); }); it('removes notes from db on a reorg', async () => { From 70be693741b8a8e022fdfa5a02953dc21704549b Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Wed, 7 Jan 2026 12:30:21 +0000 Subject: [PATCH 23/36] Fixes --- .../archiver/src/archiver/archiver.ts | 1 - .../aztec-node/src/sentinel/sentinel.ts | 4 ++-- .../src/branded-types/checkpoint_number.ts | 22 +++++++++++++++---- .../foundation/src/branded-types/index.ts | 2 +- .../p2p_client.integration_block_txs.test.ts | 3 +++ ...lient.integration_status_handshake.test.ts | 5 +++++ .../test/p2p_client.integration_txs.test.ts | 10 +++++++-- .../block_synchronizer.test.ts | 1 - .../stdlib/src/interfaces/aztec-node.ts | 3 +-- .../server_world_state_synchronizer.ts | 2 -- 10 files changed, 38 insertions(+), 15 deletions(-) diff --git a/yarn-project/archiver/src/archiver/archiver.ts b/yarn-project/archiver/src/archiver/archiver.ts index 2899ecce6973..13ceaccd3d83 100644 --- a/yarn-project/archiver/src/archiver/archiver.ts +++ b/yarn-project/archiver/src/archiver/archiver.ts @@ -33,7 +33,6 @@ import type { FunctionSelector } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import { type ArchiverEmitter, - type CheckpointId, CheckpointedL2Block, CommitteeAttestation, L2Block, diff --git a/yarn-project/aztec-node/src/sentinel/sentinel.ts b/yarn-project/aztec-node/src/sentinel/sentinel.ts index 324fb757efe6..ab4027005e0c 100644 --- a/yarn-project/aztec-node/src/sentinel/sentinel.ts +++ b/yarn-project/aztec-node/src/sentinel/sentinel.ts @@ -90,13 +90,13 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise { await this.l2TipsStore.handleBlockStreamEvent(event); if (event.type === 'chain-checkpointed') { - await this.handleCheckpoint(event); + this.handleCheckpoint(event); } else if (event.type === 'chain-proven') { await this.handleChainProven(event); } } - protected async handleCheckpoint(event: L2BlockStreamEvent) { + protected handleCheckpoint(event: L2BlockStreamEvent) { if (event.type !== 'chain-checkpointed') { return; } diff --git a/yarn-project/foundation/src/branded-types/checkpoint_number.ts b/yarn-project/foundation/src/branded-types/checkpoint_number.ts index a188384fc6d0..ef349d541b30 100644 --- a/yarn-project/foundation/src/branded-types/checkpoint_number.ts +++ b/yarn-project/foundation/src/branded-types/checkpoint_number.ts @@ -94,7 +94,21 @@ CheckpointNumber.ZERO = CheckpointNumber(0); * Zod schema for parsing and validating CheckpointNumber values. * Accepts numbers, bigints, or strings and coerces them to CheckpointNumber. */ -export const CheckpointNumberSchema = z - .union([z.number(), z.bigint(), z.string()]) - .pipe(z.coerce.number().int().min(0)) - .transform(value => CheckpointNumber(value)); +function makeCheckpointNumberSchema(minValue: number) { + return z + .union([z.number(), z.bigint(), z.string()]) + .pipe(z.coerce.number().int().min(minValue)) + .transform(value => CheckpointNumber(value)); +} + +/** + * Zod schema for parsing and validating Checkpoint values. + * Accepts numbers, bigints, or strings and coerces them to CheckpointNumber. + */ +export const CheckpointNumberSchema = makeCheckpointNumberSchema(0); + +/** + * Zod schema for parsing and validating CheckpointNumber values that are strictly positive. + * Accepts numbers, bigints, or strings and coerces them to CheckpointNumber. + */ +export const CheckpointNumberPositiveSchema = makeCheckpointNumberSchema(1); diff --git a/yarn-project/foundation/src/branded-types/index.ts b/yarn-project/foundation/src/branded-types/index.ts index 7168a053b702..7bfb38583b4a 100644 --- a/yarn-project/foundation/src/branded-types/index.ts +++ b/yarn-project/foundation/src/branded-types/index.ts @@ -1,5 +1,5 @@ export { BlockNumber, BlockNumberSchema, BlockNumberPositiveSchema } from './block_number.js'; -export { CheckpointNumber, CheckpointNumberSchema } from './checkpoint_number.js'; +export { CheckpointNumber, CheckpointNumberSchema, CheckpointNumberPositiveSchema } from './checkpoint_number.js'; export { EpochNumber, EpochNumberSchema } from './epoch.js'; export { SlotNumber, SlotNumberSchema } from './slot.js'; diff --git a/yarn-project/p2p/src/client/test/p2p_client.integration_block_txs.test.ts b/yarn-project/p2p/src/client/test/p2p_client.integration_block_txs.test.ts index 912c5eab72f1..45e68be36511 100644 --- a/yarn-project/p2p/src/client/test/p2p_client.integration_block_txs.test.ts +++ b/yarn-project/p2p/src/client/test/p2p_client.integration_block_txs.test.ts @@ -58,6 +58,7 @@ describe('p2p client integration block txs protocol ', () => { epochCache.getEpochAndSlotInNextL1Slot.mockReturnValue({ ts: BigInt(0) }); epochCache.getRegisteredValidators.mockResolvedValue([]); + txPool.isEmpty.mockResolvedValue(true); txPool.hasTxs.mockResolvedValue([]); txPool.getAllTxs.mockImplementation(() => { return Promise.resolve([] as Tx[]); @@ -67,6 +68,8 @@ describe('p2p client integration block txs protocol ', () => { return Promise.resolve([] as Tx[]); }); + attestationPool.isEmpty.mockResolvedValue(true); + worldState.status.mockResolvedValue({ state: mock(), syncSummary: { diff --git a/yarn-project/p2p/src/client/test/p2p_client.integration_status_handshake.test.ts b/yarn-project/p2p/src/client/test/p2p_client.integration_status_handshake.test.ts index d381c1fd2d4b..29108234872c 100644 --- a/yarn-project/p2p/src/client/test/p2p_client.integration_status_handshake.test.ts +++ b/yarn-project/p2p/src/client/test/p2p_client.integration_status_handshake.test.ts @@ -48,6 +48,9 @@ describe('p2p client integration status handshake', () => { epochCache.getEpochAndSlotInNextL1Slot.mockReturnValue({ ts: BigInt(0) }); epochCache.getRegisteredValidators.mockResolvedValue([]); + txPool.isEmpty.mockResolvedValue(true); + attestationPool.isEmpty.mockResolvedValue(true); + worldState.status.mockResolvedValue({ state: mock(), syncSummary: { @@ -135,6 +138,8 @@ describe('p2p client integration status handshake', () => { expect(handshakeSpy).toHaveBeenCalled(); } + // Disconnect happens asynchronously after the handshake + await retryUntil(() => disconnectSpy.mock.calls.length > 0, 'disconnect called', 10, 0.5); expect(disconnectSpy).toHaveBeenCalled(); }); diff --git a/yarn-project/p2p/src/client/test/p2p_client.integration_txs.test.ts b/yarn-project/p2p/src/client/test/p2p_client.integration_txs.test.ts index 521b974663f3..99890b842e0f 100644 --- a/yarn-project/p2p/src/client/test/p2p_client.integration_txs.test.ts +++ b/yarn-project/p2p/src/client/test/p2p_client.integration_txs.test.ts @@ -2,6 +2,7 @@ import type { EpochCache } from '@aztec/epoch-cache'; import { BlockNumber } from '@aztec/foundation/branded-types'; import { times } from '@aztec/foundation/collection'; import { type Logger, createLogger } from '@aztec/foundation/log'; +import { retryUntil } from '@aztec/foundation/retry'; import { sleep } from '@aztec/foundation/sleep'; import { emptyChainConfig } from '@aztec/stdlib/config'; import type { WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; @@ -50,6 +51,7 @@ describe('p2p client integration', () => { epochCache.getEpochAndSlotInNextL1Slot.mockReturnValue({ ts: BigInt(0) }); epochCache.getRegisteredValidators.mockResolvedValue([]); + txPool.isEmpty.mockResolvedValue(true); txPool.hasTxs.mockResolvedValue([]); txPool.getAllTxs.mockImplementation(() => { return Promise.resolve([] as Tx[]); @@ -59,6 +61,8 @@ describe('p2p client integration', () => { return Promise.resolve([] as Tx[]); }); + attestationPool.isEmpty.mockResolvedValue(true); + worldState.status.mockResolvedValue({ state: mock(), syncSummary: { @@ -297,7 +301,8 @@ describe('p2p client integration', () => { // Even though we got a response, the proof was deemed invalid expect(requestedTxs).toEqual([]); - // Low tolerance error is due to the invalid proof + // Low tolerance error is due to the invalid proof - penalize happens asynchronously + await retryUntil(() => penalizePeerSpy.mock.calls.length > 0, 'penalize peer called', 20, 0.5); expect(penalizePeerSpy).toHaveBeenCalledWith(client2PeerId, PeerErrorSeverity.LowToleranceError); }); @@ -335,7 +340,8 @@ describe('p2p client integration', () => { // Even though we got a response, the proof was deemed invalid expect(requestedTxs).toEqual([]); - // Received wrong tx + // Received wrong tx - penalize happens asynchronously + await retryUntil(() => penalizePeerSpy.mock.calls.length > 0, 'penalize peer called', 20, 0.5); expect(penalizePeerSpy).toHaveBeenCalledWith(client2PeerId, PeerErrorSeverity.MidToleranceError); }); }); diff --git a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts index 4be42bde31b6..708402902226 100644 --- a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts +++ b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts @@ -4,7 +4,6 @@ import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import { L2TipsKVStore } from '@aztec/kv-store/stores'; import { L2Block, L2BlockNew, type L2BlockStream } from '@aztec/stdlib/block'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; -import { randomPublishedL2Block } from '@aztec/stdlib/testing'; import { jest } from '@jest/globals'; import { type MockProxy, mock } from 'jest-mock-extended'; diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.ts b/yarn-project/stdlib/src/interfaces/aztec-node.ts index c19dd27334bb..c4fdc044d466 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.ts @@ -10,7 +10,7 @@ import { BlockNumber, BlockNumberPositiveSchema, BlockNumberSchema, - CheckpointNumber, + CheckpointNumberPositiveSchema, type SlotNumber, } from '@aztec/foundation/branded-types'; import type { Fr } from '@aztec/foundation/curves/bn254'; @@ -20,7 +20,6 @@ import { MembershipWitness, SiblingPath } from '@aztec/foundation/trees'; import { z } from 'zod'; -import { CheckpointNumberPositiveSchema } from '../../../foundation/src/branded-types/block_number.js'; import type { AztecAddress } from '../aztec-address/index.js'; import { type BlockParameter, BlockParameterSchema } from '../block/block_parameter.js'; import { PublishedL2Block } from '../block/checkpointed_l2_block.js'; diff --git a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts index 78d08ae4c166..2732308141dc 100644 --- a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts +++ b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts @@ -26,8 +26,6 @@ import type { L2BlockHandledStats } from '@aztec/stdlib/stats'; import { MerkleTreeId, type MerkleTreeReadOperations, type MerkleTreeWriteOperations } from '@aztec/stdlib/trees'; import { TraceableL2BlockStream, getTelemetryClient } from '@aztec/telemetry-client'; -import { check, number } from 'zod/v4'; - import { WorldStateInstrumentation } from '../instrumentation/instrumentation.js'; import type { WorldStateStatusFull } from '../native/message.js'; import type { MerkleTreeAdminDatabase } from '../world-state-db/merkle_tree_db.js'; From f02c962aa9e7da8f59f4c978e1248261bf058ca3 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Wed, 7 Jan 2026 14:20:39 +0000 Subject: [PATCH 24/36] Fixes and review updates --- .../archiver/src/archiver/archiver.test.ts | 50 ++++++++++++++ .../archiver/src/archiver/archiver.ts | 36 ++++------ .../src/branded-types/block_number.ts | 13 ---- yarn-project/p2p/src/client/p2p_client.ts | 6 +- ...lient.integration_status_handshake.test.ts | 2 - .../test/p2p_client.integration_txs.test.ts | 5 -- .../mem_pools/tx_pool/tx_pool_bench.test.ts | 2 +- .../prover-node/src/prover-node.test.ts | 2 +- .../oracle/private_execution.test.ts | 20 +----- .../oracle/utility_execution.test.ts | 19 +----- yarn-project/pxe/src/pxe.test.ts | 2 +- ...ate_logs_for_sender_recipient_pair.test.ts | 65 ++---------------- .../sync_sender_tagging_indexes.test.ts | 68 ++----------------- .../get_status_change_of_pending.test.ts | 35 ++-------- .../src/watchers/epoch_prune_watcher.test.ts | 5 +- .../l2_block_stream/l2_block_stream.test.ts | 1 - .../block/test/l2_tips_store_test_suite.ts | 51 ++++++++++++++ .../stdlib/src/interfaces/aztec-node.test.ts | 1 - yarn-project/stdlib/src/tests/factories.ts | 29 +++++++- 19 files changed, 172 insertions(+), 240 deletions(-) diff --git a/yarn-project/archiver/src/archiver/archiver.test.ts b/yarn-project/archiver/src/archiver/archiver.test.ts index 5dce720a0222..ba1589997c69 100644 --- a/yarn-project/archiver/src/archiver/archiver.test.ts +++ b/yarn-project/archiver/src/archiver/archiver.test.ts @@ -813,6 +813,13 @@ describe('Archiver', () => { await archiver.syncImmediate(); expect(await archiver.getCheckpointNumber()).toEqual(CheckpointNumber(2)); + // Verify L2Tips after syncing checkpoint 2 + const lastBlockInCheckpoint2 = allCheckpoints[1].blocks[allCheckpoints[1].blocks.length - 1].number; + const tipsAtCheckpoint2 = await archiver.getL2Tips(); + expect(tipsAtCheckpoint2.proposed.number).toEqual(lastBlockInCheckpoint2); + expect(tipsAtCheckpoint2.checkpointed.block.number).toEqual(lastBlockInCheckpoint2); + expect(tipsAtCheckpoint2.checkpointed.checkpoint.number).toEqual(CheckpointNumber(2)); + logger.warn(`Expecting prune back to checkpoint 1`); publicClient.getBlockNumber.mockResolvedValue(95n); checkpoints = checkpoints.slice(0, 1); // Keep only checkpoint 1 as the valid checkpoint @@ -820,6 +827,13 @@ describe('Archiver', () => { expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining(`L2 prune has been detected`), expect.anything()); expect(await archiver.getCheckpointNumber()).toEqual(CheckpointNumber(1)); + // Verify L2Tips after pruning back to checkpoint 1 + const lastBlockInCheckpoint1 = allCheckpoints[0].blocks[allCheckpoints[0].blocks.length - 1].number; + const tipsAfterPrune = await archiver.getL2Tips(); + expect(tipsAfterPrune.proposed.number).toEqual(lastBlockInCheckpoint1); + expect(tipsAfterPrune.checkpointed.block.number).toEqual(lastBlockInCheckpoint1); + expect(tipsAfterPrune.checkpointed.checkpoint.number).toEqual(CheckpointNumber(1)); + const txHash = allCheckpoints[1].blocks[0].body.txEffects[0].txHash; expect(await archiver.getTxEffect(txHash)).resolves.toBeUndefined; expect(await archiver.getPublishedCheckpoints(CheckpointNumber(2), 1)).toEqual([]); @@ -1787,6 +1801,12 @@ describe('Archiver', () => { expect(await archiver.getSynchedCheckpointNumber()).toEqual(CheckpointNumber(1)); const lastBlockInCheckpoint1 = checkpoint1.blocks[checkpoint1.blocks.length - 1].number; + // Verify L2Tips after syncing checkpoint 1: proposed and checkpointed should both be at checkpoint 1 + const tipsAfterCheckpoint1 = await archiver.getL2Tips(); + expect(tipsAfterCheckpoint1.proposed.number).toEqual(lastBlockInCheckpoint1); + expect(tipsAfterCheckpoint1.checkpointed.block.number).toEqual(lastBlockInCheckpoint1); + expect(tipsAfterCheckpoint1.checkpointed.checkpoint.number).toEqual(CheckpointNumber(1)); + // Now add blocks for checkpoint 2 via addBlock (simulating local block production) const checkpoint2 = checkpoints[1]; for (const block of checkpoint2.blocks) { @@ -1798,6 +1818,12 @@ describe('Archiver', () => { expect(await archiver.getBlockNumber()).toEqual(lastBlockInCheckpoint2); expect(await archiver.getSynchedCheckpointNumber()).toEqual(CheckpointNumber(1)); + // Verify L2Tips after adding blocks: proposed advances but checkpointed stays at checkpoint 1 + const tipsAfterAddBlock = await archiver.getL2Tips(); + expect(tipsAfterAddBlock.proposed.number).toEqual(lastBlockInCheckpoint2); + expect(tipsAfterAddBlock.checkpointed.block.number).toEqual(lastBlockInCheckpoint1); + expect(tipsAfterAddBlock.checkpointed.checkpoint.number).toEqual(CheckpointNumber(1)); + // getCheckpointedBlock should return undefined for the new blocks since checkpoint 2 hasn't synced const firstNewBlockNumber = lastBlockInCheckpoint1 + 1; const uncheckpointedBlock = await archiver.getCheckpointedBlock(BlockNumber(firstNewBlockNumber)); @@ -1826,6 +1852,12 @@ describe('Archiver', () => { // Now the blocks should be checkpointed expect(await archiver.getSynchedCheckpointNumber()).toEqual(CheckpointNumber(2)); + // Verify L2Tips after syncing checkpoint 2: proposed and checkpointed should both be at checkpoint 2 + const tipsAfterCheckpoint2 = await archiver.getL2Tips(); + expect(tipsAfterCheckpoint2.proposed.number).toEqual(lastBlockInCheckpoint2); + expect(tipsAfterCheckpoint2.checkpointed.block.number).toEqual(lastBlockInCheckpoint2); + expect(tipsAfterCheckpoint2.checkpointed.checkpoint.number).toEqual(CheckpointNumber(2)); + // getCheckpointedBlock should now work for the new blocks const checkpointedBlock = await archiver.getCheckpointedBlock(BlockNumber(firstNewBlockNumber)); expect(checkpointedBlock).toBeDefined(); @@ -1902,6 +1934,12 @@ describe('Archiver', () => { expect(await archiver.getSynchedCheckpointNumber()).toEqual(CheckpointNumber(1)); const lastBlockInCheckpoint1 = checkpoint1.blocks[checkpoint1.blocks.length - 1].number; + // Verify L2Tips after syncing checkpoint 1: proposed and checkpointed at checkpoint 1 + const tipsAfterCheckpoint1 = await archiver.getL2Tips(); + expect(tipsAfterCheckpoint1.proposed.number).toEqual(lastBlockInCheckpoint1); + expect(tipsAfterCheckpoint1.checkpointed.block.number).toEqual(lastBlockInCheckpoint1); + expect(tipsAfterCheckpoint1.checkpointed.checkpoint.number).toEqual(CheckpointNumber(1)); + // Now add more blocks via addBlock (simulating local block production ahead of L1) const checkpoint2 = checkpoints[1]; for (const block of checkpoint2.blocks) { @@ -1915,6 +1953,12 @@ describe('Archiver', () => { // But checkpoint number should still be 1 expect(await archiver.getSynchedCheckpointNumber()).toEqual(CheckpointNumber(1)); + // Verify L2Tips after adding blocks: proposed advances, checkpointed stays at checkpoint 1 + const tipsAfterAddBlock = await archiver.getL2Tips(); + expect(tipsAfterAddBlock.proposed.number).toEqual(lastBlockInCheckpoint2); + expect(tipsAfterAddBlock.checkpointed.block.number).toEqual(lastBlockInCheckpoint1); + expect(tipsAfterAddBlock.checkpointed.checkpoint.number).toEqual(CheckpointNumber(1)); + // New blocks should not be checkpointed yet const firstNewBlockNumber = lastBlockInCheckpoint1 + 1; const uncheckpointedBlock = await archiver.getCheckpointedBlock(BlockNumber(firstNewBlockNumber)); @@ -1939,6 +1983,12 @@ describe('Archiver', () => { // Now all blocks should be checkpointed expect(await archiver.getSynchedCheckpointNumber()).toEqual(CheckpointNumber(2)); + // Verify L2Tips after syncing checkpoint 2: both proposed and checkpointed at checkpoint 2 + const tipsAfterCheckpoint2 = await archiver.getL2Tips(); + expect(tipsAfterCheckpoint2.proposed.number).toEqual(lastBlockInCheckpoint2); + expect(tipsAfterCheckpoint2.checkpointed.block.number).toEqual(lastBlockInCheckpoint2); + expect(tipsAfterCheckpoint2.checkpointed.checkpoint.number).toEqual(CheckpointNumber(2)); + const checkpointedBlock = await archiver.getCheckpointedBlock(BlockNumber(firstNewBlockNumber)); expect(checkpointedBlock).toBeDefined(); expect(checkpointedBlock!.checkpointNumber).toEqual(2); diff --git a/yarn-project/archiver/src/archiver/archiver.ts b/yarn-project/archiver/src/archiver/archiver.ts index 13ceaccd3d83..476cc9c7874a 100644 --- a/yarn-project/archiver/src/archiver/archiver.ts +++ b/yarn-project/archiver/src/archiver/archiver.ts @@ -1534,6 +1534,7 @@ export class Archiver const beforeInitialblockNumber = BlockNumber(INITIAL_L2_BLOCK_NUM - 1); + // Get the latest block header and checkpointed blocks for proven, finalised and checkpointed blocks const [latestBlockHeader, provenCheckpointedBlock, finalizedCheckpointedBlock, checkpointedBlock] = await Promise.all([ latestBlockNumber > beforeInitialblockNumber ? this.getBlockHeader(latestBlockNumber) : undefined, @@ -1575,6 +1576,7 @@ export class Archiver (await finalizedCheckpointedBlock?.block.header?.hash()) ?? GENESIS_BLOCK_HEADER_HASH; const checkpointedBlockHeaderHash = (await checkpointedBlock?.block.header?.hash()) ?? GENESIS_BLOCK_HEADER_HASH; + // Now attempt to retrieve checkpoints for proven, finalised and checkpointed blocks const [[provenBlockCheckpoint], [finalizedBlockCheckpoint], [checkpointedBlockCheckpoint]] = await Promise.all([ provenCheckpointedBlock !== undefined ? await this.getPublishedCheckpoints(provenCheckpointedBlock?.checkpointNumber, 1) @@ -1592,6 +1594,16 @@ export class Archiver hash: '', }; + const makeCheckpointId = (checkpoint: PublishedCheckpoint | undefined) => { + if (checkpoint === undefined) { + return initialcheckpointId; + } + return { + number: checkpoint.checkpoint.number, + hash: checkpoint.checkpoint.hash().toString(), + }; + }; + const l2Tips: L2Tips = { proposed: { number: latestBlockNumber, @@ -1602,39 +1614,21 @@ export class Archiver number: provenBlockNumber, hash: provenBlockHeaderHash.toString(), }, - checkpoint: - provenBlockCheckpoint == undefined - ? initialcheckpointId - : { - number: provenBlockCheckpoint.checkpoint.number, - hash: provenBlockCheckpoint.checkpoint.hash().toString(), - }, + checkpoint: makeCheckpointId(provenBlockCheckpoint), }, finalized: { block: { number: finalizedBlockNumber, hash: finalizedBlockHeaderHash.toString(), }, - checkpoint: - finalizedBlockCheckpoint == undefined - ? initialcheckpointId - : { - number: finalizedBlockCheckpoint.checkpoint.number, - hash: finalizedBlockCheckpoint.checkpoint.hash().toString(), - }, + checkpoint: makeCheckpointId(finalizedBlockCheckpoint), }, checkpointed: { block: { number: checkpointedBlockNumber, hash: checkpointedBlockHeaderHash.toString(), }, - checkpoint: - checkpointedBlockCheckpoint == undefined - ? initialcheckpointId - : { - number: checkpointedBlockCheckpoint.checkpoint.number, - hash: checkpointedBlockCheckpoint.checkpoint.hash().toString(), - }, + checkpoint: makeCheckpointId(checkpointedBlockCheckpoint), }, }; diff --git a/yarn-project/foundation/src/branded-types/block_number.ts b/yarn-project/foundation/src/branded-types/block_number.ts index 9b7878c6a1fe..39f0c2cad9e9 100644 --- a/yarn-project/foundation/src/branded-types/block_number.ts +++ b/yarn-project/foundation/src/branded-types/block_number.ts @@ -99,13 +99,6 @@ function makeBlockNumberSchema(minValue: number) { .transform(value => BlockNumber(value)); } -function makeCheckpointNumberSchema(minValue: number) { - return z - .union([z.number(), z.bigint(), z.string()]) - .pipe(z.coerce.number().int().min(minValue)) - .transform(value => CheckpointNumber(value)); -} - /** * Zod schema for parsing and validating BlockNumber values. * Accepts numbers, bigints, or strings and coerces them to BlockNumber. @@ -117,9 +110,3 @@ export const BlockNumberSchema = makeBlockNumberSchema(0); * Accepts numbers, bigints, or strings and coerces them to BlockNumber. */ export const BlockNumberPositiveSchema = makeBlockNumberSchema(1); - -/** - * Zod schema for parsing and validating CheckpointNumber values that are strictly positive. - * Accepts numbers, bigints, or strings and coerces them to CheckpointNumber. - */ -export const CheckpointNumberPositiveSchema = makeCheckpointNumberSchema(1); diff --git a/yarn-project/p2p/src/client/p2p_client.ts b/yarn-project/p2p/src/client/p2p_client.ts index 63a84ae81825..11ba246161a3 100644 --- a/yarn-project/p2p/src/client/p2p_client.ts +++ b/yarn-project/p2p/src/client/p2p_client.ts @@ -173,7 +173,6 @@ export class P2PClient public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise { this.log.debug(`Handling block stream event ${event.type}`); - // Get old finalized block number before any processing (needed for chain-finalized handling) const oldFinalizedBlockNum = event.type === 'chain-finalized' ? await this.getSyncedFinalizedBlockNum() : BlockNumber(0); @@ -205,7 +204,9 @@ export class P2PClient } } + // Pass the event through the our l2 tips store await this.l2Tips.handleBlockStreamEvent(event); + await this.startServiceIfSynched(); } #assertIsReady() { @@ -674,7 +675,6 @@ export class P2PClient const lastBlock = blocks.at(-1)!; await this.synchedLatestSlot.set(BigInt(lastBlock.header.getSlot())); this.log.verbose(`Synched to latest block ${lastBlock.number}`); - await this.startServiceIfSynched(); } /** Request txs for unproven blocks so the prover node has more chances to get them. */ @@ -728,8 +728,6 @@ export class P2PClient await this.attestationPool.deleteAttestationsOlderThan(lastBlockSlot); this.log.debug(`Synched to finalized block ${lastBlockNum} at slot ${lastBlockSlot}`); - - await this.startServiceIfSynched(); } /** diff --git a/yarn-project/p2p/src/client/test/p2p_client.integration_status_handshake.test.ts b/yarn-project/p2p/src/client/test/p2p_client.integration_status_handshake.test.ts index 29108234872c..995436350821 100644 --- a/yarn-project/p2p/src/client/test/p2p_client.integration_status_handshake.test.ts +++ b/yarn-project/p2p/src/client/test/p2p_client.integration_status_handshake.test.ts @@ -138,8 +138,6 @@ describe('p2p client integration status handshake', () => { expect(handshakeSpy).toHaveBeenCalled(); } - // Disconnect happens asynchronously after the handshake - await retryUntil(() => disconnectSpy.mock.calls.length > 0, 'disconnect called', 10, 0.5); expect(disconnectSpy).toHaveBeenCalled(); }); diff --git a/yarn-project/p2p/src/client/test/p2p_client.integration_txs.test.ts b/yarn-project/p2p/src/client/test/p2p_client.integration_txs.test.ts index 99890b842e0f..7776173fd59a 100644 --- a/yarn-project/p2p/src/client/test/p2p_client.integration_txs.test.ts +++ b/yarn-project/p2p/src/client/test/p2p_client.integration_txs.test.ts @@ -2,7 +2,6 @@ import type { EpochCache } from '@aztec/epoch-cache'; import { BlockNumber } from '@aztec/foundation/branded-types'; import { times } from '@aztec/foundation/collection'; import { type Logger, createLogger } from '@aztec/foundation/log'; -import { retryUntil } from '@aztec/foundation/retry'; import { sleep } from '@aztec/foundation/sleep'; import { emptyChainConfig } from '@aztec/stdlib/config'; import type { WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; @@ -301,8 +300,6 @@ describe('p2p client integration', () => { // Even though we got a response, the proof was deemed invalid expect(requestedTxs).toEqual([]); - // Low tolerance error is due to the invalid proof - penalize happens asynchronously - await retryUntil(() => penalizePeerSpy.mock.calls.length > 0, 'penalize peer called', 20, 0.5); expect(penalizePeerSpy).toHaveBeenCalledWith(client2PeerId, PeerErrorSeverity.LowToleranceError); }); @@ -340,8 +337,6 @@ describe('p2p client integration', () => { // Even though we got a response, the proof was deemed invalid expect(requestedTxs).toEqual([]); - // Received wrong tx - penalize happens asynchronously - await retryUntil(() => penalizePeerSpy.mock.calls.length > 0, 'penalize peer called', 20, 0.5); expect(penalizePeerSpy).toHaveBeenCalledWith(client2PeerId, PeerErrorSeverity.MidToleranceError); }); }); diff --git a/yarn-project/p2p/src/mem_pools/tx_pool/tx_pool_bench.test.ts b/yarn-project/p2p/src/mem_pools/tx_pool/tx_pool_bench.test.ts index 7354551f663c..7e67cde584cc 100644 --- a/yarn-project/p2p/src/mem_pools/tx_pool/tx_pool_bench.test.ts +++ b/yarn-project/p2p/src/mem_pools/tx_pool/tx_pool_bench.test.ts @@ -160,7 +160,7 @@ describe('TxPool: Benchmarks', () => { getL2Tips: () => { const tipId = { block: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, - checkpoint: { number: CheckpointNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + checkpoint: { number: CheckpointNumber.ZERO, hash: '' }, }; return Promise.resolve({ proposed: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, diff --git a/yarn-project/prover-node/src/prover-node.test.ts b/yarn-project/prover-node/src/prover-node.test.ts index 0fee621dc4fb..26d79b59ee70 100644 --- a/yarn-project/prover-node/src/prover-node.test.ts +++ b/yarn-project/prover-node/src/prover-node.test.ts @@ -153,7 +153,7 @@ describe('prover-node', () => { const latestHash = checkpoints.at(-1)!.hash().toString(); const genesisTipId = { block: { number: BlockNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, - checkpoint: { number: CheckpointNumber.ZERO, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + checkpoint: { number: CheckpointNumber.ZERO, hash: '' }, }; l2BlockSource.getL2Tips.mockResolvedValue({ proposed: { number: latestBlockNumber, hash: latestHash }, diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts index a18194cbc3a6..297f9474496a 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution.test.ts @@ -6,7 +6,7 @@ import { PUBLIC_DATA_TREE_HEIGHT, } from '@aztec/constants'; import { asyncMap } from '@aztec/foundation/async-map'; -import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber } from '@aztec/foundation/branded-types'; import { times } from '@aztec/foundation/collection'; import { poseidon2Hash, poseidon2HashWithSeparator } from '@aztec/foundation/crypto/poseidon'; import { randomInt } from '@aztec/foundation/crypto/random'; @@ -48,7 +48,7 @@ import { computeAppNullifierSecretKey, deriveKeys } from '@aztec/stdlib/keys'; import type { SiloedTag } from '@aztec/stdlib/logs'; import { L1Actor, L1ToL2Message, L2Actor } from '@aztec/stdlib/messaging'; import { Note, NoteDao } from '@aztec/stdlib/note'; -import { makeBlockHeader } from '@aztec/stdlib/testing'; +import { makeBlockHeader, makeL2Tips } from '@aztec/stdlib/testing'; import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees'; import { BlockHeader, @@ -330,21 +330,7 @@ describe('Private Execution test suite', () => { aztecNode.getPrivateLogsByTags.mockImplementation((tags: SiloedTag[]) => Promise.resolve(tags.map(() => []))); // Mock getL2Tips and getBlockHeader for loadPrivateLogsForSenderRecipientPair - aztecNode.getL2Tips.mockResolvedValue({ - proposed: { number: anchorBlockHeader.globalVariables.blockNumber, hash: '' }, - checkpointed: { - block: { number: anchorBlockHeader.globalVariables.blockNumber, hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - proven: { - block: { number: anchorBlockHeader.globalVariables.blockNumber, hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - finalized: { - block: { number: anchorBlockHeader.globalVariables.blockNumber, hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - }); + aztecNode.getL2Tips.mockResolvedValue(makeL2Tips(anchorBlockHeader.globalVariables.blockNumber)); aztecNode.getBlockHeader.mockImplementation((blockNumber: BlockNumber | 'latest') => { if (blockNumber === 'latest') { return Promise.resolve(anchorBlockHeader); diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts index 29647ea3c0e4..7ac113d1607a 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution.test.ts @@ -1,4 +1,4 @@ -import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import { GrumpkinScalar } from '@aztec/foundation/curves/grumpkin'; import type { KeyStore } from '@aztec/key-store'; @@ -11,6 +11,7 @@ import { CompleteAddress, type ContractInstanceWithAddress } from '@aztec/stdlib import type { AztecNode } from '@aztec/stdlib/interfaces/server'; import { deriveKeys } from '@aztec/stdlib/keys'; import { Note, NoteDao } from '@aztec/stdlib/note'; +import { makeL2Tips } from '@aztec/stdlib/testing'; import { BlockHeader, GlobalVariables, TxHash } from '@aztec/stdlib/tx'; import { mock } from 'jest-mock-extended'; @@ -74,21 +75,7 @@ describe('Utility Execution test suite', () => { senderAddressBookStore.getSenders.mockResolvedValue([]); // Mock getL2Tips and getBlockHeader for loadPrivateLogsForSenderRecipientPair - aztecNode.getL2Tips.mockResolvedValue({ - proposed: { number: anchorBlockHeader.globalVariables.blockNumber, hash: '' }, - checkpointed: { - block: { number: anchorBlockHeader.globalVariables.blockNumber, hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - proven: { - block: { number: anchorBlockHeader.globalVariables.blockNumber, hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - finalized: { - block: { number: anchorBlockHeader.globalVariables.blockNumber, hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - }); + aztecNode.getL2Tips.mockResolvedValue(makeL2Tips(anchorBlockHeader.globalVariables.blockNumber)); aztecNode.getBlockHeader.mockImplementation((blockNumber: BlockNumber | 'latest') => { if (blockNumber === 'latest') { return Promise.resolve(anchorBlockHeader); diff --git a/yarn-project/pxe/src/pxe.test.ts b/yarn-project/pxe/src/pxe.test.ts index b76910e29d62..fddd5dcf80b7 100644 --- a/yarn-project/pxe/src/pxe.test.ts +++ b/yarn-project/pxe/src/pxe.test.ts @@ -179,7 +179,7 @@ describe('PXE', () => { // Mock getL2Tips which is needed for syncing tagged logs const tipId = { block: { number: lastKnownBlockNumber, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, - checkpoint: { number: CheckpointNumber(lastKnownBlockNumber), hash: GENESIS_BLOCK_HEADER_HASH.toString() }, + checkpoint: { number: CheckpointNumber(lastKnownBlockNumber), hash: '' }, }; node.getL2Tips.mockResolvedValue({ proposed: { number: lastKnownBlockNumber, hash: GENESIS_BLOCK_HEADER_HASH.toString() }, diff --git a/yarn-project/pxe/src/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.test.ts b/yarn-project/pxe/src/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.test.ts index 98a0d30b3fe6..37e28eed71a6 100644 --- a/yarn-project/pxe/src/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.test.ts +++ b/yarn-project/pxe/src/tagging/recipient_sync/load_private_logs_for_sender_recipient_pair.test.ts @@ -1,11 +1,11 @@ import { MAX_INCLUDE_BY_TIMESTAMP_DURATION } from '@aztec/constants'; -import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; import { DirectionalAppTaggingSecret, SiloedTag, Tag } from '@aztec/stdlib/logs'; -import { makeBlockHeader, randomTxScopedPrivateL2Log } from '@aztec/stdlib/testing'; +import { makeBlockHeader, makeL2Tips, randomTxScopedPrivateL2Log } from '@aztec/stdlib/testing'; import { type MockProxy, mock } from 'jest-mock-extended'; @@ -49,18 +49,7 @@ describe('loadPrivateLogsForSenderRecipientPair', () => { }); it('returns empty array when no logs found', async () => { - aztecNode.getL2Tips.mockResolvedValue({ - proposed: { number: BlockNumber(10), hash: '' }, - checkpointed: { - block: { number: BlockNumber(10), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - proven: { block: { number: BlockNumber(10), hash: '' }, checkpoint: { number: CheckpointNumber(0), hash: '' } }, - finalized: { - block: { number: BlockNumber(10), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - }); + aztecNode.getL2Tips.mockResolvedValue(makeL2Tips(10)); aztecNode.getBlockHeader.mockResolvedValue(makeBlockHeader(0, { timestamp: currentTimestamp })); @@ -89,21 +78,7 @@ describe('loadPrivateLogsForSenderRecipientPair', () => { const logIndex = 5; const logTag = await computeSiloedTagForIndex(logIndex); - aztecNode.getL2Tips.mockResolvedValue({ - proposed: { number: BlockNumber(finalizedBlockNumber), hash: '' }, - checkpointed: { - block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - proven: { - block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - finalized: { - block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - }); + aztecNode.getL2Tips.mockResolvedValue(makeL2Tips(finalizedBlockNumber)); aztecNode.getBlockHeader.mockResolvedValue(makeBlockHeader(0, { timestamp: currentTimestamp })); @@ -136,21 +111,7 @@ describe('loadPrivateLogsForSenderRecipientPair', () => { const logIndex = 7; const logTag = await computeSiloedTagForIndex(logIndex); - aztecNode.getL2Tips.mockResolvedValue({ - proposed: { number: BlockNumber(finalizedBlockNumber), hash: '' }, - checkpointed: { - block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - proven: { - block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - finalized: { - block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - }); + aztecNode.getL2Tips.mockResolvedValue(makeL2Tips(finalizedBlockNumber)); aztecNode.getBlockHeader.mockResolvedValue(makeBlockHeader(0, { timestamp: currentTimestamp })); @@ -192,21 +153,7 @@ describe('loadPrivateLogsForSenderRecipientPair', () => { await taggingStore.updateHighestAgedIndex(secret, highestAgedIndex); await taggingStore.updateHighestFinalizedIndex(secret, highestFinalizedIndex); - aztecNode.getL2Tips.mockResolvedValue({ - proposed: { number: BlockNumber(finalizedBlockNumber), hash: '' }, - checkpointed: { - block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - proven: { - block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - finalized: { - block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - }); + aztecNode.getL2Tips.mockResolvedValue(makeL2Tips(finalizedBlockNumber)); aztecNode.getBlockHeader.mockResolvedValue(makeBlockHeader(0, { timestamp: currentTimestamp })); diff --git a/yarn-project/pxe/src/tagging/sender_sync/sync_sender_tagging_indexes.test.ts b/yarn-project/pxe/src/tagging/sender_sync/sync_sender_tagging_indexes.test.ts index ef60ea99ad34..0aa89cda0a98 100644 --- a/yarn-project/pxe/src/tagging/sender_sync/sync_sender_tagging_indexes.test.ts +++ b/yarn-project/pxe/src/tagging/sender_sync/sync_sender_tagging_indexes.test.ts @@ -1,9 +1,9 @@ -import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { AztecNode } from '@aztec/stdlib/interfaces/client'; -import { randomTxScopedPrivateL2Log } from '@aztec/stdlib/testing'; +import { makeL2Tips, randomTxScopedPrivateL2Log } from '@aztec/stdlib/testing'; import { TxHash, TxStatus } from '@aztec/stdlib/tx'; import { type MockProxy, mock } from 'jest-mock-extended'; @@ -83,21 +83,7 @@ describe('syncSenderTaggingIndexes', () => { } as any); // Mock getL2Tips to return a finalized block number >= the tx block number - aztecNode.getL2Tips.mockResolvedValue({ - proposed: { number: BlockNumber(finalizedBlockNumberStep1), hash: '' }, - checkpointed: { - block: { number: BlockNumber(finalizedBlockNumberStep1), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - proven: { - block: { number: BlockNumber(finalizedBlockNumberStep1), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - finalized: { - block: { number: BlockNumber(finalizedBlockNumberStep1), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - }); + aztecNode.getL2Tips.mockResolvedValue(makeL2Tips(finalizedBlockNumberStep1)); await syncSenderTaggingIndexes(secret, contractAddress, aztecNode, taggingStore); @@ -124,21 +110,7 @@ describe('syncSenderTaggingIndexes', () => { blockNumber: finalizedBlockNumberStep1 + 1, } as any); - aztecNode.getL2Tips.mockResolvedValue({ - proposed: { number: BlockNumber(finalizedBlockNumberStep1), hash: '' }, - checkpointed: { - block: { number: BlockNumber(finalizedBlockNumberStep1), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - proven: { - block: { number: BlockNumber(finalizedBlockNumberStep1), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - finalized: { - block: { number: BlockNumber(finalizedBlockNumberStep1), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - }); + aztecNode.getL2Tips.mockResolvedValue(makeL2Tips(finalizedBlockNumberStep1)); await syncSenderTaggingIndexes(secret, contractAddress, aztecNode, taggingStore); @@ -205,21 +177,7 @@ describe('syncSenderTaggingIndexes', () => { }); // Mock getL2Tips with the new finalized block number - aztecNode.getL2Tips.mockResolvedValue({ - proposed: { number: BlockNumber(newFinalizedBlockNumber), hash: '' }, - checkpointed: { - block: { number: BlockNumber(newFinalizedBlockNumber), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - proven: { - block: { number: BlockNumber(newFinalizedBlockNumber), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - finalized: { - block: { number: BlockNumber(newFinalizedBlockNumber), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - }); + aztecNode.getL2Tips.mockResolvedValue(makeL2Tips(newFinalizedBlockNumber)); await syncSenderTaggingIndexes(secret, contractAddress, aztecNode, taggingStore); @@ -270,21 +228,7 @@ describe('syncSenderTaggingIndexes', () => { } }); - aztecNode.getL2Tips.mockResolvedValue({ - proposed: { number: BlockNumber(finalizedBlockNumber), hash: '' }, - checkpointed: { - block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - proven: { - block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - finalized: { - block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - }); + aztecNode.getL2Tips.mockResolvedValue(makeL2Tips(finalizedBlockNumber)); // Sync tagged logs await syncSenderTaggingIndexes(secret, contractAddress, aztecNode, taggingStore); diff --git a/yarn-project/pxe/src/tagging/sender_sync/utils/get_status_change_of_pending.test.ts b/yarn-project/pxe/src/tagging/sender_sync/utils/get_status_change_of_pending.test.ts index 0e12f8fb578e..6d15ac87a3a6 100644 --- a/yarn-project/pxe/src/tagging/sender_sync/utils/get_status_change_of_pending.test.ts +++ b/yarn-project/pxe/src/tagging/sender_sync/utils/get_status_change_of_pending.test.ts @@ -1,5 +1,6 @@ -import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber } from '@aztec/foundation/branded-types'; import type { AztecNode } from '@aztec/stdlib/interfaces/server'; +import { makeL2Tips } from '@aztec/stdlib/testing'; import { TxHash, TxStatus } from '@aztec/stdlib/tx'; import { type MockProxy, mock } from 'jest-mock-extended'; @@ -55,21 +56,7 @@ describe('getStatusChangeOfPending', () => { } }); - aztecNode.getL2Tips.mockResolvedValue({ - proposed: { number: BlockNumber(finalizedBlockNumber), hash: '' }, - checkpointed: { - block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - proven: { - block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - finalized: { - block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - }); + aztecNode.getL2Tips.mockResolvedValue(makeL2Tips(finalizedBlockNumber)); const result = await getStatusChangeOfPending( [ @@ -101,21 +88,7 @@ describe('getStatusChangeOfPending', () => { blockNumber: BlockNumber(finalizedBlockNumber), } as any); - aztecNode.getL2Tips.mockResolvedValue({ - proposed: { number: BlockNumber(finalizedBlockNumber), hash: '' }, - checkpointed: { - block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - proven: { - block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - finalized: { - block: { number: BlockNumber(finalizedBlockNumber), hash: '' }, - checkpoint: { number: CheckpointNumber(0), hash: '' }, - }, - }); + aztecNode.getL2Tips.mockResolvedValue(makeL2Tips(finalizedBlockNumber)); const result = await getStatusChangeOfPending([txHash], aztecNode); diff --git a/yarn-project/slasher/src/watchers/epoch_prune_watcher.test.ts b/yarn-project/slasher/src/watchers/epoch_prune_watcher.test.ts index 4038ee412377..84f155ce0cbf 100644 --- a/yarn-project/slasher/src/watchers/epoch_prune_watcher.test.ts +++ b/yarn-project/slasher/src/watchers/epoch_prune_watcher.test.ts @@ -74,7 +74,6 @@ describe('EpochPruneWatcher', () => { const emitSpy = jest.spyOn(watcher, 'emit'); const epochNumber = EpochNumber(1); - // Slot 10 is in epoch 1 (with epochDuration=8, epoch 1 = slots 8-15) const block = await L2BlockNew.random( BlockNumber(12), // block number { @@ -122,7 +121,6 @@ describe('EpochPruneWatcher', () => { it('should slash if the data is available and the epoch could have been proven', async () => { const emitSpy = jest.spyOn(watcher, 'emit'); - // Slot 10 is in epoch 1 (with epochDuration=8, epoch 1 = slots 8-15) const block = await L2BlockNew.random( BlockNumber(12), // block number { @@ -178,7 +176,6 @@ describe('EpochPruneWatcher', () => { it('should not slash if the data is available but the epoch could not have been proven', async () => { const emitSpy = jest.spyOn(watcher, 'emit'); - // Slot 10 is in epoch 1 (with epochDuration=8, epoch 1 = slots 8-15) const blockFromL1 = await L2BlockNew.random( BlockNumber(12), // block number { @@ -186,7 +183,7 @@ describe('EpochPruneWatcher', () => { slotNumber: SlotNumber(10), }, ); - // Block from builder has different archive root, simulating failed re-execution + const blockFromBuilder = await L2BlockNew.random( BlockNumber(13), // block number { diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts index b286e086f8c5..12dd64467020 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts @@ -65,7 +65,6 @@ describe('L2BlockStream', () => { Promise.resolve(compactArray(times(limit, i => (from + i > latest ? undefined : makeBlock(from + i))))), ); - // Mock getPublishedCheckpoints to return a mock checkpoint blockSource.getPublishedCheckpoints.mockImplementation((checkpointNumber: CheckpointNumber, _limit: number) => Promise.resolve([ { diff --git a/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts b/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts index 0d1b7549ae10..49fbe627845b 100644 --- a/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts +++ b/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts @@ -230,6 +230,57 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { expect(tips.proposed).toEqual(makeTip(5)); }); + it('handles pruning proposed chain to genesis, re-proposing, and checkpointing', async () => { + // Propose blocks 1-3 + const firstBlocks = await Promise.all(times(3, i => makeBlock(i + 1))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: firstBlocks }); + + let tips = await tipsStore.getL2Tips(); + expect(tips.proposed).toEqual(makeTip(3)); + + // Store original hashes + const originalHash1 = blockHashes.get(1); + const originalHash2 = blockHashes.get(2); + const originalHash3 = blockHashes.get(3); + + // Prune back to genesis (block 0) + await tipsStore.handleBlockStreamEvent({ type: 'chain-pruned', block: makeTip(0) }); + + tips = await tipsStore.getL2Tips(); + expect(tips.proposed).toEqual(makeTip(0)); + expect(tips.checkpointed.block).toEqual(makeTip(0)); + + // Clear hashes and propose new blocks 1-3 (different from original) + blockHashes.delete(1); + blockHashes.delete(2); + blockHashes.delete(3); + const newBlocks = await Promise.all(times(3, i => makeBlock(i + 1))); + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: newBlocks }); + + // Verify new blocks have different hashes + expect(blockHashes.get(1)).not.toEqual(originalHash1); + expect(blockHashes.get(2)).not.toEqual(originalHash2); + expect(blockHashes.get(3)).not.toEqual(originalHash3); + + tips = await tipsStore.getL2Tips(); + expect(tips.proposed).toEqual(makeTip(3)); + expect(tips.checkpointed.block).toEqual(makeTip(0)); // Not yet checkpointed + + // Checkpoint all the new proposed blocks (1-3) + const checkpoint1 = await makeCheckpoint(1, newBlocks); + await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint1 }); + + tips = await tipsStore.getL2Tips(); + expect(tips.proposed).toEqual(makeTip(3)); + expect(tips.checkpointed.block).toEqual(makeTip(3)); + expect(tips.checkpointed.checkpoint.number).toEqual(CheckpointNumber(1)); + + // Verify block hashes in store are the new ones + expect(await tipsStore.getL2BlockHash(1)).toEqual(blockHashes.get(1)); + expect(await tipsStore.getL2BlockHash(2)).toEqual(blockHashes.get(2)); + expect(await tipsStore.getL2BlockHash(3)).toEqual(blockHashes.get(3)); + }); + it('handles reorg: prune proposed blocks back to checkpoint, then re-propose with different blocks', async () => { // Propose blocks 1-5 const firstBlocks = await Promise.all(times(5, i => makeBlock(i + 1))); diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts index b93f5ea0c9ae..fc61fffa8047 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts @@ -265,7 +265,6 @@ describe('AztecNodeApiSchema', () => { expect(response).toHaveLength(1); expect(response[0]).toBeInstanceOf(L2BlockNew); - await expect(context.client.getBlocks(-1 as BlockNumber, BlockNumber(1))).rejects.toThrow(); await expect(context.client.getBlocks(BlockNumber.ZERO, BlockNumber(1))).rejects.toThrow(); await expect(context.client.getBlocks(BlockNumber(1), BlockNumber.ZERO)).rejects.toThrow(); await expect(context.client.getBlocks(BlockNumber(1), MAX_RPC_LEN + 1)).rejects.toThrow(); diff --git a/yarn-project/stdlib/src/tests/factories.ts b/yarn-project/stdlib/src/tests/factories.ts index 16d6272bf995..8388e338bb3f 100644 --- a/yarn-project/stdlib/src/tests/factories.ts +++ b/yarn-project/stdlib/src/tests/factories.ts @@ -43,7 +43,7 @@ import { VK_TREE_HEIGHT, } from '@aztec/constants'; import { type FieldsOf, makeTuple } from '@aztec/foundation/array'; -import { BlockNumber, SlotNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types'; import { compact } from '@aztec/foundation/collection'; import { Grumpkin } from '@aztec/foundation/crypto/grumpkin'; import { poseidon2HashWithSeparator } from '@aztec/foundation/crypto/poseidon'; @@ -88,6 +88,7 @@ import { PublicDataRead } from '../avm/public_data_read.js'; import { PublicDataWrite } from '../avm/public_data_write.js'; import { AztecAddress } from '../aztec-address/index.js'; import { L2BlockHeader } from '../block/l2_block_header.js'; +import type { L2Tips } from '../block/l2_block_source.js'; import { type ContractClassPublic, ContractDeploymentData, @@ -1740,3 +1741,29 @@ export async function randomTxScopedPublicL2Log(opts?: { opts?.firstNullifier ?? Fr.random(), ); } + +/** + * Creates L2Tips with all tips pointing to the same block number. + * Useful for mocking aztecNode.getL2Tips() in tests. + * @param blockNumber - The block number to use for all tips. + * @param hash - Optional hash for the block (defaults to empty string). + * @returns L2Tips object with all tips at the same block. + */ +export function makeL2Tips(blockNumber: number | BlockNumber, hash = ''): L2Tips { + const bn = typeof blockNumber === 'number' ? BlockNumber(blockNumber) : blockNumber; + return { + proposed: { number: bn, hash }, + checkpointed: { + block: { number: bn, hash }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + proven: { + block: { number: bn, hash }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + finalized: { + block: { number: bn, hash }, + checkpoint: { number: CheckpointNumber(0), hash: '' }, + }, + }; +} From b3c5c47f09ff3ff49327bc10bd749826329b086f Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Wed, 7 Jan 2026 16:58:54 +0000 Subject: [PATCH 25/36] Fixes --- .../archiver/src/archiver/archiver.ts | 15 +++ .../archiver/src/archiver/archiver_store.ts | 8 ++ .../archiver/kv_archiver_store/block_store.ts | 28 +++++ .../kv_archiver_store/kv_archiver_store.ts | 4 + .../archiver/src/test/mock_l2_block_source.ts | 5 + .../aztec-node/src/aztec-node/server.ts | 4 + .../block_synchronizer.test.ts | 16 ++- .../stdlib/src/block/l2_block_source.ts | 14 +++ .../src/block/l2_block_stream/interfaces.ts | 6 +- .../l2_block_stream/l2_block_stream.test.ts | 110 +++++++++++++++--- .../block/l2_block_stream/l2_block_stream.ts | 80 ++++++++----- .../block/test/l2_tips_store_test_suite.ts | 35 +++++- .../stdlib/src/interfaces/archiver.test.ts | 18 +++ .../stdlib/src/interfaces/archiver.ts | 4 + .../stdlib/src/interfaces/aztec-node.test.ts | 4 + .../stdlib/src/interfaces/aztec-node.ts | 15 ++- .../src/wrappers/l2_block_stream.ts | 5 +- .../txe/src/state_machine/archiver.ts | 4 + 18 files changed, 317 insertions(+), 58 deletions(-) diff --git a/yarn-project/archiver/src/archiver/archiver.ts b/yarn-project/archiver/src/archiver/archiver.ts index 476cc9c7874a..ccecb37ed2d6 100644 --- a/yarn-project/archiver/src/archiver/archiver.ts +++ b/yarn-project/archiver/src/archiver/archiver.ts @@ -1401,6 +1401,20 @@ export class Archiver return this.store.getCheckpointedBlock(number); } + public async getCheckpointedBlocks( + from: BlockNumber, + limit: number, + proven?: boolean, + ): Promise { + const blocks = await this.store.store.getCheckpointedBlocks(from, limit); + + if (proven === true) { + const provenBlockNumber = await this.store.getProvenBlockNumber(); + return blocks.filter(b => b.block.number <= provenBlockNumber); + } + return blocks; + } + getCheckpointedBlockByHash(blockHash: Fr): Promise { return this.store.getCheckpointedBlockByHash(blockHash); } @@ -1845,6 +1859,7 @@ export class ArchiverStoreHelper | 'addBlocks' | 'getBlock' | 'getBlocks' + | 'getCheckpointedBlocks' > { #log = createLogger('archiver:block-helper'); diff --git a/yarn-project/archiver/src/archiver/archiver_store.ts b/yarn-project/archiver/src/archiver/archiver_store.ts index 60abca8653c7..2f333438e7f8 100644 --- a/yarn-project/archiver/src/archiver/archiver_store.ts +++ b/yarn-project/archiver/src/archiver/archiver_store.ts @@ -85,6 +85,14 @@ export interface ArchiverDataStore { */ getCheckpointedBlock(number: number): Promise; + /** + * Gets up to `limit` amount of checkpointed L2 blocks starting from `from`. + * @param from - Number of the first block to return (inclusive). + * @param limit - The number of blocks to return. + * @returns The requested checkpointed L2 blocks. + */ + getCheckpointedBlocks(from: number, limit: number): Promise; + /** * Returns the block for the given hash, or undefined if not exists. * @param blockHash - The block hash to return. 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 fe49b7dd088f..af9895f6e0ac 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 @@ -503,6 +503,34 @@ export class BlockStore { ); } + /** + * Gets up to `limit` amount of Checkpointed 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 + */ + async *getCheckpointedBlocks(start: BlockNumber, limit: number): AsyncIterableIterator { + const checkpointCache = new Map(); + for await (const [blockNumber, blockStorage] of this.getBlockStorages(start, limit)) { + const block = await this.getBlockFromBlockStorage(blockNumber, blockStorage); + if (block) { + const checkpoint = + checkpointCache.get(CheckpointNumber(blockStorage.checkpointNumber)) ?? + (await this.#checkpoints.getAsync(blockStorage.checkpointNumber)); + if (checkpoint) { + checkpointCache.set(CheckpointNumber(blockStorage.checkpointNumber), checkpoint); + const checkpointedBlock = new CheckpointedL2Block( + CheckpointNumber(checkpoint.checkpointNumber), + block, + L1PublishedData.fromBuffer(checkpoint.l1), + checkpoint.attestations.map(buf => CommitteeAttestation.fromBuffer(buf)), + ); + yield checkpointedBlock; + } + } + } + } + async getCheckpointedBlockByHash(blockHash: Fr): Promise { const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString()); if (blockNumber === undefined) { 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 9c403cb349c9..d932cd16e5b4 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 @@ -234,6 +234,10 @@ export class KVArchiverDataStore implements ArchiverDataStore, ContractDataSourc return toArray(this.#blockStore.getBlocks(from, limit)); } + getCheckpointedBlocks(from: BlockNumber, limit: number): Promise { + return toArray(this.#blockStore.getCheckpointedBlocks(from, limit)); + } + /** * Gets up to `limit` amount of L2 blocks headers starting from `from`. * diff --git a/yarn-project/archiver/src/test/mock_l2_block_source.ts b/yarn-project/archiver/src/test/mock_l2_block_source.ts index 2a30d0282784..a71b7ab5f405 100644 --- a/yarn-project/archiver/src/test/mock_l2_block_source.ts +++ b/yarn-project/archiver/src/test/mock_l2_block_source.ts @@ -97,6 +97,11 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { return Promise.resolve(undefined); } + public getCheckpointedBlocks(_from: BlockNumber, _limit: number, _proven?: boolean) { + // In this mock, we don't track checkpointed blocks separately + return Promise.resolve([]); + } + /** * Gets an l2 block. * @param number - The block number to return (inclusive). diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index a8fb2a7fed34..cf401cced1c3 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -624,6 +624,10 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { return (await this.blockSource.getL2BlocksNew(from, limit)) ?? []; } + public async getCheckpointedBlocks(from: BlockNumber, limit: number, proven?: boolean) { + return (await this.blockSource.getCheckpointedBlocks(from, limit, proven)) ?? []; + } + /** * Method to fetch the current base fees. * @returns The current base fees. diff --git a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts index 708402902226..9853e37f2c1b 100644 --- a/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts +++ b/yarn-project/pxe/src/block_synchronizer/block_synchronizer.test.ts @@ -1,4 +1,4 @@ -import { BlockNumber } from '@aztec/foundation/branded-types'; +import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; import { timesParallel } from '@aztec/foundation/collection'; import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import { L2TipsKVStore } from '@aztec/kv-store/stores'; @@ -59,7 +59,12 @@ describe('BlockSynchronizer', () => { type: 'blocks-added', blocks: await timesParallel(5, i => L2BlockNew.random(BlockNumber(i))), }); - await synchronizer.handleBlockStreamEvent({ type: 'chain-pruned', block: { number: BlockNumber(3), hash: '0x3' } }); + await synchronizer.handleBlockStreamEvent({ + type: 'chain-pruned', + block: { number: BlockNumber(3), hash: '0x3' }, + reason: 'unproven', + checkpoint: { number: CheckpointNumber.ZERO, hash: '' }, + }); expect(rollbackNotesAndNullifiers).toHaveBeenCalledWith(3, 4); }); @@ -76,7 +81,12 @@ describe('BlockSynchronizer', () => { type: 'blocks-added', blocks: await timesParallel(5, i => L2BlockNew.random(BlockNumber(i))), }); - await synchronizer.handleBlockStreamEvent({ type: 'chain-pruned', block: { number: BlockNumber(3), hash: '0x3' } }); + await synchronizer.handleBlockStreamEvent({ + type: 'chain-pruned', + block: { number: BlockNumber(3), hash: '0x3' }, + reason: 'unproven', + checkpoint: { number: CheckpointNumber.ZERO, hash: '' }, + }); expect(rollbackEventsAfterBlock).toHaveBeenCalledWith(3, 4); }); diff --git a/yarn-project/stdlib/src/block/l2_block_source.ts b/yarn-project/stdlib/src/block/l2_block_source.ts index 4c3d5a357949..e3c01aeccf4e 100644 --- a/yarn-project/stdlib/src/block/l2_block_source.ts +++ b/yarn-project/stdlib/src/block/l2_block_source.ts @@ -67,6 +67,8 @@ export interface L2BlockSource { */ getCheckpointedBlock(number: BlockNumber): Promise; + getCheckpointedBlocks(from: BlockNumber, limit: number, proven?: boolean): Promise; + /** * Retrieves a collection of published checkpoints * @param checkpointNumber The first checkpoint to be retrieved @@ -251,6 +253,13 @@ export interface L2BlockSourceEventEmitter extends L2BlockSource, ArchiverEmitte */ export type L2BlockTag = 'proposed' | 'checkpointed' | 'proven' | 'finalized'; +/** + * Reason for L2 block prune. + * - uncheckpointed: L2 blocks were pruned due to a failure to checkpoint. + * - unproven: L2 blocks were pruned due to a failure to prove. + */ +export type L2BlockPruneReason = 'uncheckpointed' | 'unproven'; + /** Tips of the L2 chain. */ export type L2Tips = { proposed: L2BlockId; @@ -274,6 +283,11 @@ export function makeL2BlockId(number: BlockNumber, hash?: string): L2BlockId { return { number, hash: hash! }; } +/** Creates an L2 checkpoint id */ +export function makeL2CheckpointId(number: CheckpointNumber, hash: string): CheckpointId { + return { number, hash }; +} + const L2BlockIdSchema = z.object({ number: BlockNumberSchema, hash: z.string(), diff --git a/yarn-project/stdlib/src/block/l2_block_stream/interfaces.ts b/yarn-project/stdlib/src/block/l2_block_stream/interfaces.ts index 9d23f0376c44..d3fc9fa1e164 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/interfaces.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/interfaces.ts @@ -1,6 +1,6 @@ import type { PublishedCheckpoint } from '../../checkpoint/published_checkpoint.js'; import type { L2BlockNew } from '../l2_block_new.js'; -import type { L2BlockId, L2Tips } from '../l2_block_source.js'; +import type { CheckpointId, L2BlockId, L2BlockPruneReason, L2Tips } from '../l2_block_source.js'; /** Interface to the local view of the chain. Implemented by world-state and l2-tips-store. */ export interface L2BlockStreamLocalDataProvider { @@ -22,9 +22,11 @@ export type L2BlockStreamEvent = type: 'chain-checkpointed'; checkpoint: PublishedCheckpoint; } - | /** Reports last correct block (new tip of the unproven chain). */ { + | /** Reports last correct block (new tip of the proposed chain). */ { type: 'chain-pruned'; + reason: L2BlockPruneReason; block: L2BlockId; + checkpoint: CheckpointId; } | /** Reports new proven block. */ { type: 'chain-proven'; diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts index 12dd64467020..fd86a6f9fad5 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts @@ -7,6 +7,7 @@ import times from 'lodash.times'; import type { PublishedCheckpoint } from '../../checkpoint/published_checkpoint.js'; import type { BlockHeader } from '../../tx/block_header.js'; +import type { CheckpointedL2Block } from '../checkpointed_l2_block.js'; import type { L2BlockNew } from '../l2_block_new.js'; import type { L2BlockId, L2BlockSource, L2Tips } from '../l2_block_source.js'; import type { L2BlockStreamEvent, L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider } from './interfaces.js'; @@ -17,6 +18,7 @@ describe('L2BlockStream', () => { let blockSource: MockProxy; let latest: number = 0; + let checkpointed: number = 0; const makeHash = (number: number) => new Fr(number).toString(); @@ -27,23 +29,34 @@ describe('L2BlockStream', () => { indexWithinCheckpoint: 0, }) as L2BlockNew; + const makeCheckpointedBlock = (number: number, checkpointNum: number): CheckpointedL2Block => + ({ + block: makeBlock(number), + checkpointNumber: checkpointNum, + }) as CheckpointedL2Block; + const makeHeader = (number: number) => ({ hash: () => Promise.resolve(new Fr(number)) }) as BlockHeader; const makeBlockId = (number: number): L2BlockId => ({ number: BlockNumber(number), hash: makeHash(number) }); + const makeCheckpointId = (number: number) => ({ number: CheckpointNumber(number), hash: makeHash(number) }); + const makeTipId = (number: number) => ({ block: { number: BlockNumber(number), hash: makeHash(number) }, checkpoint: { number: CheckpointNumber(number), hash: makeHash(number) }, }); - const setRemoteTips = (latest_: number, proven?: number, finalized?: number) => { + /** Sets the remote tips. checkpointed_ defaults to 0 (no checkpointed blocks). */ + const setRemoteTips = (latest_: number, proven?: number, finalized?: number, checkpointed_?: number) => { proven = proven ?? 0; finalized = finalized ?? 0; + checkpointed_ = checkpointed_ ?? 0; latest = latest_; + checkpointed = checkpointed_; blockSource.getL2Tips.mockResolvedValue({ proposed: { number: BlockNumber(latest), hash: makeHash(latest) }, - checkpointed: makeTipId(latest), + checkpointed: makeTipId(checkpointed_), proven: makeTipId(proven), finalized: makeTipId(finalized), }); @@ -60,17 +73,27 @@ describe('L2BlockStream', () => { ), ); - // And returns blocks up until what was reported as the latest block + // Returns blocks up until what was reported as the latest block (for uncheckpointed blocks) blockSource.getL2BlocksNew.mockImplementation((from, limit) => Promise.resolve(compactArray(times(limit, i => (from + i > latest ? undefined : makeBlock(from + i))))), ); + // Returns checkpointed blocks (for blocks up to checkpointed tip) + blockSource.getCheckpointedBlocks.mockImplementation((from, limit) => + Promise.resolve( + compactArray( + times(limit, i => (from + i > checkpointed ? undefined : makeCheckpointedBlock(from + i, from + i))), + ), + ), + ); + + // Returns published checkpoints - each checkpoint contains just the one block for simplicity blockSource.getPublishedCheckpoints.mockImplementation((checkpointNumber: CheckpointNumber, _limit: number) => Promise.resolve([ { checkpoint: { number: checkpointNumber, - hash: () => Promise.resolve(new Fr(checkpointNumber)), + hash: () => new Fr(checkpointNumber), blocks: [makeBlock(checkpointNumber)], }, } as unknown as PublishedCheckpoint, @@ -91,7 +114,6 @@ describe('L2BlockStream', () => { it('pulls new blocks from start', async () => { setRemoteTips(5); - localData.checkpointed.number = BlockNumber(5); // Match source checkpointed to avoid checkpointed event await blockStream.work(); expect(handler.events).toEqual([ @@ -102,7 +124,6 @@ describe('L2BlockStream', () => { it('pulls new blocks from offset', async () => { setRemoteTips(15); localData.latest.number = BlockNumber(10); - localData.checkpointed.number = BlockNumber(15); // Match source checkpointed await blockStream.work(); expect(blockSource.getL2BlocksNew).toHaveBeenCalledWith(BlockNumber(11), 5, undefined); @@ -113,7 +134,6 @@ describe('L2BlockStream', () => { it('pulls new blocks in multiple batches', async () => { setRemoteTips(45); - localData.checkpointed.number = BlockNumber(45); // Match source checkpointed await blockStream.work(); expect(blockSource.getL2BlocksNew).toHaveBeenCalledTimes(5); @@ -129,7 +149,6 @@ describe('L2BlockStream', () => { it('halts pulling blocks if stopped', async () => { setRemoteTips(45); - localData.checkpointed.number = BlockNumber(45); // Match source checkpointed blockStream.running = false; await blockStream.work(); @@ -141,7 +160,6 @@ describe('L2BlockStream', () => { it('halts on handler error and retries', async () => { setRemoteTips(45); - localData.checkpointed.number = BlockNumber(45); // Match source checkpointed handler.throwing = true; await blockStream.work(); @@ -156,7 +174,6 @@ describe('L2BlockStream', () => { it('handles a reorg and requests blocks from new tip', async () => { setRemoteTips(45); localData.latest.number = BlockNumber(40); - localData.checkpointed.number = BlockNumber(45); // Match source checkpointed for (const i of [37, 38, 39, 40]) { // Mess up the block hashes for a bunch of blocks @@ -165,7 +182,7 @@ describe('L2BlockStream', () => { await blockStream.work(); expect(handler.events).toEqual([ - { type: 'chain-pruned', block: makeBlockId(36) }, + { type: 'chain-pruned', block: makeBlockId(36), reason: 'unproven', checkpoint: makeCheckpointId(0) }, { type: 'blocks-added', blocks: times(9, i => makeBlock(i + 37)) }, ] satisfies L2BlockStreamEvent[]); }); @@ -173,7 +190,6 @@ describe('L2BlockStream', () => { it('emits events for chain proven and finalized', async () => { setRemoteTips(45, 40, 35); localData.latest.number = BlockNumber(40); - localData.checkpointed.number = BlockNumber(45); // Match source checkpointed localData.proven.number = BlockNumber(10); localData.finalized.number = BlockNumber(10); @@ -184,6 +200,70 @@ describe('L2BlockStream', () => { { type: 'chain-finalized', block: makeBlockId(35) }, ] satisfies L2BlockStreamEvent[]); }); + + it('fetches checkpointed blocks and emits chain-checkpointed events', async () => { + // All blocks are checkpointed (checkpointed=5, proposed=5) + setRemoteTips(5, 0, 0, 5); + + await blockStream.work(); + + // Each checkpointed block triggers a blocks-added and chain-checkpointed event + // (since each checkpoint contains one block in our mock) + expect(handler.events).toEqual([ + { type: 'blocks-added', blocks: [makeBlock(1)] }, + expect.objectContaining({ type: 'chain-checkpointed' }), + { type: 'blocks-added', blocks: [makeBlock(2)] }, + expect.objectContaining({ type: 'chain-checkpointed' }), + { type: 'blocks-added', blocks: [makeBlock(3)] }, + expect.objectContaining({ type: 'chain-checkpointed' }), + { type: 'blocks-added', blocks: [makeBlock(4)] }, + expect.objectContaining({ type: 'chain-checkpointed' }), + { type: 'blocks-added', blocks: [makeBlock(5)] }, + expect.objectContaining({ type: 'chain-checkpointed' }), + ]); + expect(blockSource.getCheckpointedBlocks).toHaveBeenCalledTimes(5); + expect(blockSource.getL2BlocksNew).not.toHaveBeenCalled(); + }); + + it('fetches checkpointed blocks first, then uncheckpointed blocks', async () => { + // Blocks 1-3 are checkpointed, blocks 4-5 are uncheckpointed + setRemoteTips(5, 0, 0, 3); + + await blockStream.work(); + + // First 3 blocks come via checkpoints, last 2 via getL2BlocksNew + expect(handler.events).toEqual([ + { type: 'blocks-added', blocks: [makeBlock(1)] }, + expect.objectContaining({ type: 'chain-checkpointed' }), + { type: 'blocks-added', blocks: [makeBlock(2)] }, + expect.objectContaining({ type: 'chain-checkpointed' }), + { type: 'blocks-added', blocks: [makeBlock(3)] }, + expect.objectContaining({ type: 'chain-checkpointed' }), + { type: 'blocks-added', blocks: [makeBlock(4), makeBlock(5)] }, + ]); + expect(blockSource.getCheckpointedBlocks).toHaveBeenCalledTimes(3); + expect(blockSource.getL2BlocksNew).toHaveBeenCalledWith(BlockNumber(4), 2, undefined); + }); + + it('handles reorg with uncheckpointed reason when pruned to checkpointed tip', async () => { + // Source: checkpointed=3, proposed=5 + setRemoteTips(5, 0, 0, 3); + localData.latest.number = BlockNumber(5); + + // Mess up hashes for blocks 4 and 5 (uncheckpointed blocks) + localData.blockHashes[4] = `0xaa4`; + localData.blockHashes[5] = `0xaa5`; + + await blockStream.work(); + + // Prune to block 3 (checkpointed tip), reason should be 'uncheckpointed' + expect(handler.events[0]).toEqual({ + type: 'chain-pruned', + block: makeBlockId(3), + reason: 'uncheckpointed', + checkpoint: makeCheckpointId(3), + }); + }); }); describe('with memory tips store', () => { @@ -209,7 +289,6 @@ describe('L2BlockStream', () => { await blockStream.work(); expect(handler.events).toEqual([ { type: 'blocks-added', blocks: times(6, i => makeBlock(i + 30)) }, - expect.objectContaining({ type: 'chain-checkpointed' }), { type: 'chain-proven', block: makeBlockId(25) }, { type: 'chain-finalized', block: makeBlockId(10) }, ]); @@ -219,8 +298,7 @@ describe('L2BlockStream', () => { setRemoteTips(25, 25, 10); await blockStream.work(); expect(handler.events).toEqual([ - { type: 'chain-pruned', block: makeBlockId(25) }, - expect.objectContaining({ type: 'chain-checkpointed' }), + { type: 'chain-pruned', block: makeBlockId(25), reason: 'unproven', checkpoint: makeCheckpointId(0) }, ]); }); }); @@ -243,7 +321,6 @@ describe('L2BlockStream', () => { setRemoteTips(40, 38, 35); localData.latest.number = BlockNumber(5); - localData.checkpointed.number = BlockNumber(40); // Match source checkpointed localData.proven.number = BlockNumber(2); localData.finalized.number = BlockNumber(2); @@ -261,7 +338,6 @@ describe('L2BlockStream', () => { setRemoteTips(40, 38, 35); localData.latest.number = BlockNumber(38); - localData.checkpointed.number = BlockNumber(40); // Match source checkpointed localData.proven.number = BlockNumber(38); localData.finalized.number = BlockNumber(35); diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts index f76d00365aaa..2d8f3cd396d7 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts @@ -1,9 +1,10 @@ -import { BlockNumber } from '@aztec/foundation/branded-types'; +import { INITIAL_CHECKPOINT_NUMBER } from '@aztec/constants'; +import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; import { AbortError } from '@aztec/foundation/error'; import { createLogger } from '@aztec/foundation/log'; import { RunningPromise } from '@aztec/foundation/running-promise'; -import { type L2BlockId, type L2BlockSource, makeL2BlockId } from '../l2_block_source.js'; +import { type L2BlockId, type L2BlockPruneReason, type L2BlockSource, makeL2BlockId } from '../l2_block_source.js'; import type { L2BlockStreamEvent, L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider } from './interfaces.js'; /** Creates a stream of events for new blocks, chain tips updates, and reorgs, out of polling an archiver or a node. */ @@ -15,7 +16,7 @@ export class L2BlockStream { constructor( private l2BlockSource: Pick< L2BlockSource, - 'getL2BlocksNew' | 'getBlockHeader' | 'getL2Tips' | 'getPublishedCheckpoints' + 'getL2BlocksNew' | 'getBlockHeader' | 'getL2Tips' | 'getPublishedCheckpoints' | 'getCheckpointedBlocks' >, private localData: L2BlockStreamLocalDataProvider, private handler: L2BlockStreamEventHandler, @@ -95,7 +96,17 @@ export class L2BlockStream { this.log.verbose( `Reorg detected. Pruning blocks from ${latestBlockNumber + 1} to ${localTips.proposed.number}.`, ); - await this.emitEvent({ type: 'chain-pruned', block: makeL2BlockId(latestBlockNumber, hash) }); + // If the new block number is the same as the checkpointed tip, then it's a failure to checkpoint, rather than a failure to prove + let reason: L2BlockPruneReason = 'unproven'; + if (latestBlockNumber === sourceTips.checkpointed.block.number) { + reason = 'uncheckpointed'; + } + await this.emitEvent({ + type: 'chain-pruned', + block: makeL2BlockId(latestBlockNumber, hash), + reason, + checkpoint: sourceTips.checkpointed.checkpoint, + }); } // If we are just starting, use the starting block number from the options. @@ -119,7 +130,43 @@ export class L2BlockStream { nextBlockNumber = Math.max(sourceTips.finalized.block.number, nextBlockNumber); } - // Request new blocks from the source. + // Request checkpointed blocks from the source up until the tip of the checkpointed chain. + let checkpointNumber = CheckpointNumber(INITIAL_CHECKPOINT_NUMBER - 1); + while (nextBlockNumber <= sourceTips.checkpointed.block.number) { + const limit = Math.min(this.opts.batchSize ?? 50, sourceTips.checkpointed.block.number - nextBlockNumber + 1); + this.log.trace(`Requesting blocks from ${nextBlockNumber} limit ${limit} proven=${this.opts.proven}`); + // Get this block as a checkpointed block + const blocks = await this.l2BlockSource.getCheckpointedBlocks( + BlockNumber(nextBlockNumber), + 1, + this.opts.proven, + ); + if (blocks.length === 0) { + break; + } + checkpointNumber = CheckpointNumber(blocks[0].checkpointNumber); + const checkpoints = await this.l2BlockSource.getPublishedCheckpoints(checkpointNumber, 1); + if (checkpoints.length === 0) { + break; + } + // we have the checkpoint for the next block number, get the remaining blocks in this checkpoint + const blocksforCheckpoint = checkpoints[0].checkpoint.blocks + .filter(b => b.number >= nextBlockNumber) + .slice(0, limit); + await this.emitEvent({ type: 'blocks-added', blocks: blocksforCheckpoint }); + nextBlockNumber = blocksforCheckpoint.at(-1)!.number + 1; + + // If we have reached the end of the checkpoint, signal as such + const lastBlockInCheckpoint = checkpoints[0].checkpoint.blocks.at(-1)!; + if (nextBlockNumber > lastBlockInCheckpoint.number) { + await this.emitEvent({ + type: 'chain-checkpointed', + checkpoint: checkpoints[0], + }); + } + } + + // Request new blocks from the source, these will be uncheckpointed blocks. while (nextBlockNumber <= sourceTips.proposed.number) { const limit = Math.min(this.opts.batchSize ?? 50, sourceTips.proposed.number - nextBlockNumber + 1); this.log.trace(`Requesting blocks from ${nextBlockNumber} limit ${limit} proven=${this.opts.proven}`); @@ -131,29 +178,6 @@ export class L2BlockStream { nextBlockNumber = blocks.at(-1)!.number + 1; } - // Update the checkpointed tips - // TODO(pw/mbps): Not sure if this is the correct way of handling multiple checkpoints or if we should do each one in turn - // This matches the updates to the proven chain. But I suspect we may need to process checkpoints in turn for things like the sentinel. - if ( - localTips.checkpointed !== undefined && - sourceTips.checkpointed !== undefined && - localTips.checkpointed.block.number !== sourceTips.checkpointed.block.number - ) { - const checkpoints = await this.l2BlockSource.getPublishedCheckpoints( - sourceTips.checkpointed.checkpoint.number, - 1, - ); - if (checkpoints.length === 0) { - throw new Error( - `Failed to retrieve checkpoint ${sourceTips.checkpointed.checkpoint.number} from source for checkpointed tip update`, - ); - } - await this.emitEvent({ - type: 'chain-checkpointed', - checkpoint: checkpoints[0], - }); - } - // Update the proven and finalized tips. if (localTips.proven !== undefined && sourceTips.proven.block.number !== localTips.proven.block.number) { await this.emitEvent({ diff --git a/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts b/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts index 49fbe627845b..d0d0d560c99a 100644 --- a/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts +++ b/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts @@ -224,7 +224,12 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks }); // Prune to block 5 - await tipsStore.handleBlockStreamEvent({ type: 'chain-pruned', block: makeBlockId(5) }); + await tipsStore.handleBlockStreamEvent({ + type: 'chain-pruned', + block: makeBlockId(5), + reason: 'unproven', + checkpoint: { number: CheckpointNumber.ZERO, hash: '' }, + }); const tips = await tipsStore.getL2Tips(); expect(tips.proposed).toEqual(makeTip(5)); @@ -244,7 +249,12 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { const originalHash3 = blockHashes.get(3); // Prune back to genesis (block 0) - await tipsStore.handleBlockStreamEvent({ type: 'chain-pruned', block: makeTip(0) }); + await tipsStore.handleBlockStreamEvent({ + type: 'chain-pruned', + block: makeTip(0), + reason: 'unproven', + checkpoint: { number: CheckpointNumber.ZERO, hash: '' }, + }); tips = await tipsStore.getL2Tips(); expect(tips.proposed).toEqual(makeTip(0)); @@ -305,7 +315,12 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { // Prune proposed blocks back to checkpoint (block 5) // This removes proposed blocks 6-10, but checkpoint remains at 5 - await tipsStore.handleBlockStreamEvent({ type: 'chain-pruned', block: makeBlockId(5) }); + await tipsStore.handleBlockStreamEvent({ + type: 'chain-pruned', + block: makeBlockId(5), + reason: 'unproven', + checkpoint: { number: CheckpointNumber.ZERO, hash: '' }, + }); tips = await tipsStore.getL2Tips(); expect(tips.proposed).toEqual(makeTip(5)); @@ -365,7 +380,12 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { expect(tips.checkpointed.block).toEqual(makeTip(3)); // Only blocks 1-3 are checkpointed // Prune proposed blocks back to checkpoint (block 3) - await tipsStore.handleBlockStreamEvent({ type: 'chain-pruned', block: makeBlockId(3) }); + await tipsStore.handleBlockStreamEvent({ + type: 'chain-pruned', + block: makeBlockId(3), + reason: 'unproven', + checkpoint: { number: CheckpointNumber.ZERO, hash: '' }, + }); tips = await tipsStore.getL2Tips(); expect(tips.proposed).toEqual(makeTip(3)); @@ -439,7 +459,12 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { // Prune all the way back to proven tip (block 3) // This prunes both proposed blocks (7-10) AND checkpointed blocks (4-6) - await tipsStore.handleBlockStreamEvent({ type: 'chain-pruned', block: makeBlockId(3) }); + await tipsStore.handleBlockStreamEvent({ + type: 'chain-pruned', + block: makeBlockId(3), + reason: 'unproven', + checkpoint: { number: CheckpointNumber.ZERO, hash: '' }, + }); tips = await tipsStore.getL2Tips(); expect(tips.proposed).toEqual(makeTip(3)); diff --git a/yarn-project/stdlib/src/interfaces/archiver.test.ts b/yarn-project/stdlib/src/interfaces/archiver.test.ts index cd163d9b0816..86fc1b089b4d 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.test.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.test.ts @@ -181,6 +181,14 @@ describe('ArchiverApiSchema', () => { expect(result!.l1).toBeDefined(); }); + it('getCheckpointedBlocks', async () => { + const result = await context.client.getCheckpointedBlocks(BlockNumber(1), 10); + expect(result).toHaveLength(1); + expect(result[0].block.constructor.name).toEqual('L2BlockNew'); + expect(result[0].attestations[0]).toBeInstanceOf(CommitteeAttestation); + expect(result[0].l1).toBeDefined(); + }); + it('getBlocksForEpoch', async () => { const result = await context.client.getBlocksForEpoch(EpochNumber(1)); expect(result).toEqual([expect.any(L2Block)]); @@ -379,6 +387,16 @@ class MockArchiver implements ArchiverApi { }), ); } + async getCheckpointedBlocks(from: BlockNumber, _limit: number, _proven?: boolean): Promise { + return [ + CheckpointedL2Block.fromFields({ + checkpointNumber: CheckpointNumber(1), + block: await L2BlockNew.random(from), + attestations: [CommitteeAttestation.random()], + l1: new L1PublishedData(1n, 0n, `0x`), + }), + ]; + } async getBlocks(from: BlockNumber, _limit: number, _proven?: boolean): Promise { return [await L2Block.random(from)]; } diff --git a/yarn-project/stdlib/src/interfaces/archiver.ts b/yarn-project/stdlib/src/interfaces/archiver.ts index 558baca88658..d248732efe80 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.ts @@ -89,6 +89,10 @@ export const ArchiverApiSchema: ApiSchemaFor = { .args(z.union([BlockNumberSchema, z.literal('latest')])) .returns(BlockHeader.schema.optional()), getCheckpointedBlock: z.function().args(BlockNumberSchema).returns(CheckpointedL2Block.schema.optional()), + getCheckpointedBlocks: z + .function() + .args(BlockNumberSchema, schemas.Integer, optional(z.boolean())) + .returns(z.array(CheckpointedL2Block.schema)), getBlocks: z .function() .args(BlockNumberSchema, schemas.Integer, optional(z.boolean())) diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts index fc61fffa8047..39c9e96d1762 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts @@ -568,6 +568,10 @@ class MockAztecNode implements AztecNode { return [block]; } + getCheckpointedBlocks(_from: BlockNumber, _limit: number, _proven?: boolean) { + return Promise.resolve([]); + } + findLeavesIndexes( blockNumber: number | 'latest', treeId: MerkleTreeId, diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.ts b/yarn-project/stdlib/src/interfaces/aztec-node.ts index c4fdc044d466..911390ebcb71 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.ts @@ -22,7 +22,7 @@ import { z } from 'zod'; import type { AztecAddress } from '../aztec-address/index.js'; import { type BlockParameter, BlockParameterSchema } from '../block/block_parameter.js'; -import { PublishedL2Block } from '../block/checkpointed_l2_block.js'; +import { CheckpointedL2Block, PublishedL2Block } from '../block/checkpointed_l2_block.js'; import { type DataInBlock, dataInBlockSchemaFor } from '../block/in_block.js'; import { L2Block } from '../block/l2_block.js'; import { L2BlockNew } from '../block/l2_block_new.js'; @@ -76,7 +76,13 @@ import { type WorldStateSyncStatus, WorldStateSyncStatusSchema } from './world_s export interface AztecNode extends Pick< L2BlockSource, - 'getBlocks' | 'getL2BlocksNew' | 'getPublishedBlocks' | 'getPublishedCheckpoints' | 'getBlockHeader' | 'getL2Tips' + | 'getBlocks' + | 'getL2BlocksNew' + | 'getPublishedBlocks' + | 'getPublishedCheckpoints' + | 'getBlockHeader' + | 'getL2Tips' + | 'getCheckpointedBlocks' > { /** * Returns the tips of the L2 chain. @@ -595,6 +601,11 @@ export const AztecNodeApiSchema: ApiSchemaFor = { .args(BlockNumberPositiveSchema, z.number().gt(0).lte(MAX_RPC_BLOCKS_LEN)) .returns(z.array(L2BlockNew.schema)), + getCheckpointedBlocks: z + .function() + .args(BlockNumberPositiveSchema, z.number().gt(0).lte(MAX_RPC_BLOCKS_LEN), optional(z.boolean())) + .returns(z.array(CheckpointedL2Block.schema)), + getCurrentBaseFees: z.function().returns(GasFees.schema), getMaxPriorityFees: z.function().returns(GasFees.schema), diff --git a/yarn-project/telemetry-client/src/wrappers/l2_block_stream.ts b/yarn-project/telemetry-client/src/wrappers/l2_block_stream.ts index 39e8cb015a5a..8dc87154b445 100644 --- a/yarn-project/telemetry-client/src/wrappers/l2_block_stream.ts +++ b/yarn-project/telemetry-client/src/wrappers/l2_block_stream.ts @@ -11,7 +11,10 @@ import { type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client' /** Extends an L2BlockStream with a tracer to create a new trace per iteration. */ export class TraceableL2BlockStream extends L2BlockStream implements Traceable { constructor( - l2BlockSource: Pick, + l2BlockSource: Pick< + L2BlockSource, + 'getL2BlocksNew' | 'getBlockHeader' | 'getL2Tips' | 'getPublishedCheckpoints' | 'getCheckpointedBlocks' + >, localData: L2BlockStreamLocalDataProvider, handler: L2BlockStreamEventHandler, public readonly tracer: Tracer, diff --git a/yarn-project/txe/src/state_machine/archiver.ts b/yarn-project/txe/src/state_machine/archiver.ts index 4d78e28106c8..f93b408b2706 100644 --- a/yarn-project/txe/src/state_machine/archiver.ts +++ b/yarn-project/txe/src/state_machine/archiver.ts @@ -287,4 +287,8 @@ export class TXEArchiver extends ArchiverStoreHelper implements L2BlockSource { getPublishedBlockByArchive(_archive: Fr): Promise { throw new Error('Method not implemented.'); } + + getCheckpointedBlocks(_from: BlockNumber, _limit: number, _proven?: boolean): Promise { + throw new Error('TXE Archiver does not implement "getCheckpointedBlocks"'); + } } From 19ede06a2528ccb2534982243c15eb4e011a412a Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Wed, 7 Jan 2026 17:16:16 +0000 Subject: [PATCH 26/36] Fixes --- .../l2_block_stream/l2_block_stream.test.ts | 58 ++++++++++--------- .../block/l2_block_stream/l2_block_stream.ts | 7 ++- .../stdlib/src/interfaces/aztec-node.test.ts | 5 ++ 3 files changed, 42 insertions(+), 28 deletions(-) diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts index fd86a6f9fad5..e6e6520f7bc7 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts @@ -9,7 +9,7 @@ import type { PublishedCheckpoint } from '../../checkpoint/published_checkpoint. import type { BlockHeader } from '../../tx/block_header.js'; import type { CheckpointedL2Block } from '../checkpointed_l2_block.js'; import type { L2BlockNew } from '../l2_block_new.js'; -import type { L2BlockId, L2BlockSource, L2Tips } from '../l2_block_source.js'; +import type { CheckpointId, L2BlockId, L2BlockSource, L2Tips } from '../l2_block_source.js'; import type { L2BlockStreamEvent, L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider } from './interfaces.js'; import { L2BlockStream } from './l2_block_stream.js'; import { L2TipsMemoryStore } from './l2_tips_memory_store.js'; @@ -123,7 +123,7 @@ describe('L2BlockStream', () => { it('pulls new blocks from offset', async () => { setRemoteTips(15); - localData.latest.number = BlockNumber(10); + localData.proposed.number = BlockNumber(10); await blockStream.work(); expect(blockSource.getL2BlocksNew).toHaveBeenCalledWith(BlockNumber(11), 5, undefined); @@ -173,7 +173,7 @@ describe('L2BlockStream', () => { it('handles a reorg and requests blocks from new tip', async () => { setRemoteTips(45); - localData.latest.number = BlockNumber(40); + localData.proposed.number = BlockNumber(40); for (const i of [37, 38, 39, 40]) { // Mess up the block hashes for a bunch of blocks @@ -189,9 +189,9 @@ describe('L2BlockStream', () => { it('emits events for chain proven and finalized', async () => { setRemoteTips(45, 40, 35); - localData.latest.number = BlockNumber(40); - localData.proven.number = BlockNumber(10); - localData.finalized.number = BlockNumber(10); + localData.proposed.number = BlockNumber(40); + localData.proven.block.number = BlockNumber(10); + localData.finalized.block.number = BlockNumber(10); await blockStream.work(); expect(handler.events).toEqual([ @@ -248,7 +248,8 @@ describe('L2BlockStream', () => { it('handles reorg with uncheckpointed reason when pruned to checkpointed tip', async () => { // Source: checkpointed=3, proposed=5 setRemoteTips(5, 0, 0, 3); - localData.latest.number = BlockNumber(5); + localData.proposed.number = BlockNumber(5); + localData.checkpointed.block.number = BlockNumber(3); // Mess up hashes for blocks 4 and 5 (uncheckpointed blocks) localData.blockHashes[4] = `0xaa4`; @@ -320,9 +321,9 @@ describe('L2BlockStream', () => { it('skips ahead to the latest finalized block', async () => { setRemoteTips(40, 38, 35); - localData.latest.number = BlockNumber(5); - localData.proven.number = BlockNumber(2); - localData.finalized.number = BlockNumber(2); + localData.proposed.number = BlockNumber(5); + localData.proven.block.number = BlockNumber(2); + localData.finalized.block.number = BlockNumber(2); await blockStream.work(); @@ -337,9 +338,9 @@ describe('L2BlockStream', () => { it('does not skip if already ahead of finalized', async () => { setRemoteTips(40, 38, 35); - localData.latest.number = BlockNumber(38); - localData.proven.number = BlockNumber(38); - localData.finalized.number = BlockNumber(35); + localData.proposed.number = BlockNumber(38); + localData.proven.block.number = BlockNumber(38); + localData.finalized.block.number = BlockNumber(35); await blockStream.work(); @@ -376,27 +377,32 @@ class TestL2BlockStreamEventHandler implements L2BlockStreamEventHandler { class TestL2BlockStreamLocalDataProvider implements L2BlockStreamLocalDataProvider { public readonly blockHashes: Record = {}; - public latest = { number: BlockNumber.ZERO, hash: '' }; - public checkpointed = { number: BlockNumber.ZERO, hash: '' }; - public proven = { number: BlockNumber.ZERO, hash: '' }; - public finalized = { number: BlockNumber.ZERO, hash: '' }; + public proposed = { number: BlockNumber.ZERO, hash: '' }; + public checkpointed = { + block: { number: BlockNumber.ZERO, hash: '' }, + checkpoint: { number: CheckpointNumber.ZERO, hash: '' }, + }; + public proven = { + block: { number: BlockNumber.ZERO, hash: '' }, + checkpoint: { number: CheckpointNumber.ZERO, hash: '' }, + }; + public finalized = { + block: { number: BlockNumber.ZERO, hash: '' }, + checkpoint: { number: CheckpointNumber.ZERO, hash: '' }, + }; public getL2BlockHash(number: number): Promise { return Promise.resolve( - number > this.latest.number ? undefined : (this.blockHashes[number] ?? new Fr(number).toString()), + number > this.proposed.number ? undefined : (this.blockHashes[number] ?? new Fr(number).toString()), ); } public getL2Tips(): Promise { - const makeTipId = (blockId: L2BlockId) => ({ - block: blockId, - checkpoint: { number: CheckpointNumber(blockId.number), hash: blockId.hash }, - }); return Promise.resolve({ - proposed: this.latest, - checkpointed: makeTipId(this.checkpointed), - proven: makeTipId(this.proven), - finalized: makeTipId(this.finalized), + proposed: this.proposed, + checkpointed: this.checkpointed, + proven: this.proven, + finalized: this.finalized, }); } } diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts index 2d8f3cd396d7..ff3c525f3ae1 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts @@ -96,9 +96,12 @@ export class L2BlockStream { this.log.verbose( `Reorg detected. Pruning blocks from ${latestBlockNumber + 1} to ${localTips.proposed.number}.`, ); - // If the new block number is the same as the checkpointed tip, then it's a failure to checkpoint, rather than a failure to prove + // This check is not 100% accurate + // If the local tips are sufficiently behind the source tips, such that we are missing at least one checkpoint + // that has now been re-orged due to a proof failure then this will indicate a failure to checkpoint rather than a failure to prove + // TODO: (PhilWindle): Improve re-org detection accuracy. let reason: L2BlockPruneReason = 'unproven'; - if (latestBlockNumber === sourceTips.checkpointed.block.number) { + if (latestBlockNumber === localTips.checkpointed.block.number) { reason = 'uncheckpointed'; } await this.emitEvent({ diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts index 39c9e96d1762..6155eb89403d 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts @@ -284,6 +284,11 @@ describe('AztecNodeApiSchema', () => { expect(response[0]).toBeInstanceOf(PublishedCheckpoint); }); + it('getCheckpointedBlocks', async () => { + const response = await context.client.getCheckpointedBlocks(BlockNumber(1), 1); + expect(response).toEqual([]); + }); + it('getNodeVersion', async () => { const response = await context.client.getNodeVersion(); expect(response).toBe('1.0.0'); From 1000f922065004ecdb1be80c3cd3c6a7fb046f17 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Wed, 7 Jan 2026 17:50:14 +0000 Subject: [PATCH 27/36] Fixes --- .../archiver/src/test/mock_l2_block_source.ts | 53 ++++++++++++++++--- .../aztec-node/src/sentinel/sentinel.ts | 2 +- .../p2p/src/client/p2p_client.test.ts | 24 +++++---- .../block/l2_block_stream/l2_block_stream.ts | 2 +- 4 files changed, 60 insertions(+), 21 deletions(-) diff --git a/yarn-project/archiver/src/test/mock_l2_block_source.ts b/yarn-project/archiver/src/test/mock_l2_block_source.ts index a71b7ab5f405..836145420013 100644 --- a/yarn-project/archiver/src/test/mock_l2_block_source.ts +++ b/yarn-project/archiver/src/test/mock_l2_block_source.ts @@ -8,6 +8,7 @@ import { createLogger } from '@aztec/foundation/log'; import type { FunctionSelector } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import { + CheckpointedL2Block, L2Block, L2BlockHash, L2BlockNew, @@ -30,6 +31,7 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { private provenBlockNumber: number = 0; private finalizedBlockNumber: number = 0; + private checkpointedBlockNumber: number = 0; private log = createLogger('archiver:mock_l2_block_source'); @@ -64,6 +66,10 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { this.finalizedBlockNumber = finalizedBlockNumber; } + public setCheckpointedBlockNumber(checkpointedBlockNumber: number) { + this.checkpointedBlockNumber = checkpointedBlockNumber; + } + /** * Method to fetch the rollup contract address at the base-layer. * @returns The rollup address. @@ -92,14 +98,39 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { return Promise.resolve(BlockNumber(this.provenBlockNumber)); } - public getCheckpointedBlock(_number: BlockNumber) { - // In this mock, we don't track checkpointed blocks separately - return Promise.resolve(undefined); + public async getCheckpointedBlock(number: BlockNumber): Promise { + if (number > this.checkpointedBlockNumber) { + return undefined; + } + const block = this.l2Blocks[number - 1]; + if (!block) { + return undefined; + } + return new CheckpointedL2Block( + CheckpointNumber(number), + block.toL2Block(), + new L1PublishedData(BigInt(number), BigInt(number), `0x${number.toString(16).padStart(64, '0')}`), + [], + ); } - public getCheckpointedBlocks(_from: BlockNumber, _limit: number, _proven?: boolean) { - // In this mock, we don't track checkpointed blocks separately - return Promise.resolve([]); + public async getCheckpointedBlocks( + from: BlockNumber, + limit: number, + _proven?: boolean, + ): Promise { + const result: CheckpointedL2Block[] = []; + for (let i = 0; i < limit; i++) { + const blockNum = from + i; + if (blockNum > this.checkpointedBlockNumber) { + break; + } + const block = await this.getCheckpointedBlock(BlockNumber(blockNum)); + if (block) { + result.push(block); + } + } + return result; } /** @@ -273,15 +304,17 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { } async getL2Tips(): Promise { - const [latest, proven, finalized] = [ + const [latest, proven, finalized, checkpointed] = [ await this.getBlockNumber(), await this.getProvenBlockNumber(), this.finalizedBlockNumber, + this.checkpointedBlockNumber, ] as const; const latestBlock = this.l2Blocks[latest - 1]; const provenBlock = this.l2Blocks[proven - 1]; const finalizedBlock = this.l2Blocks[finalized - 1]; + const checkpointedBlock = this.l2Blocks[checkpointed - 1]; const latestBlockId = { number: BlockNumber(latest), @@ -295,6 +328,10 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { number: BlockNumber(finalized), hash: (await finalizedBlock?.hash())?.toString(), }; + const checkpointedBlockId = { + number: BlockNumber(checkpointed), + hash: (await checkpointedBlock?.hash())?.toString(), + }; const makeTipId = (blockId: typeof latestBlockId) => ({ block: blockId, @@ -303,7 +340,7 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { return { proposed: latestBlockId, - checkpointed: makeTipId(latestBlockId), + checkpointed: makeTipId(checkpointedBlockId), proven: makeTipId(provenBlockId), finalized: makeTipId(finalizedBlockId), }; diff --git a/yarn-project/aztec-node/src/sentinel/sentinel.ts b/yarn-project/aztec-node/src/sentinel/sentinel.ts index ab4027005e0c..ce2abd212be5 100644 --- a/yarn-project/aztec-node/src/sentinel/sentinel.ts +++ b/yarn-project/aztec-node/src/sentinel/sentinel.ts @@ -128,7 +128,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme return; } const blockNumber = BlockNumber(event.block.number); - const block = await this.archiver.getBlock(blockNumber); + const block = await this.archiver.getL2BlockNew(blockNumber); if (!block) { this.logger.error(`Failed to get block ${blockNumber}`, { block }); return; diff --git a/yarn-project/p2p/src/client/p2p_client.test.ts b/yarn-project/p2p/src/client/p2p_client.test.ts index 03b548438503..168da286736a 100644 --- a/yarn-project/p2p/src/client/p2p_client.test.ts +++ b/yarn-project/p2p/src/client/p2p_client.test.ts @@ -321,17 +321,19 @@ describe('P2P Client', () => { it('moves the tips on a chain reorg', async () => { blockSource.setProvenBlockNumber(0); + // Set checkpointed before starting so blocks are synced as checkpointed + blockSource.setCheckpointedBlockNumber(100); await client.start(); await advanceToProvenBlock(BlockNumber(90)); await advanceToFinalizedBlock(BlockNumber(50)); - const zeroCheckpoint = { number: expect.any(Number), hash: expect.any(String) }; + const anyCheckpoint = { number: expect.any(Number), hash: expect.any(String) }; await expect(client.getL2Tips()).resolves.toEqual({ proposed: { number: BlockNumber(100), hash: expect.any(String) }, - checkpointed: { block: { number: BlockNumber(100), hash: expect.any(String) }, checkpoint: zeroCheckpoint }, - proven: { block: { number: BlockNumber(90), hash: expect.any(String) }, checkpoint: zeroCheckpoint }, - finalized: { block: { number: BlockNumber(50), hash: expect.any(String) }, checkpoint: zeroCheckpoint }, + checkpointed: { block: { number: BlockNumber(100), hash: expect.any(String) }, checkpoint: anyCheckpoint }, + proven: { block: { number: BlockNumber(90), hash: expect.any(String) }, checkpoint: anyCheckpoint }, + finalized: { block: { number: BlockNumber(50), hash: expect.any(String) }, checkpoint: anyCheckpoint }, }); blockSource.removeBlocks(10); @@ -340,21 +342,21 @@ describe('P2P Client', () => { await expect(client.getL2Tips()).resolves.toEqual({ proposed: { number: BlockNumber(90), hash: expect.any(String) }, - checkpointed: { block: { number: BlockNumber(90), hash: expect.any(String) }, checkpoint: zeroCheckpoint }, - proven: { block: { number: BlockNumber(90), hash: expect.any(String) }, checkpoint: zeroCheckpoint }, - finalized: { block: { number: BlockNumber(50), hash: expect.any(String) }, checkpoint: zeroCheckpoint }, + checkpointed: { block: { number: BlockNumber(90), hash: expect.any(String) }, checkpoint: anyCheckpoint }, + proven: { block: { number: BlockNumber(90), hash: expect.any(String) }, checkpoint: anyCheckpoint }, + finalized: { block: { number: BlockNumber(50), hash: expect.any(String) }, checkpoint: anyCheckpoint }, }); blockSource.addBlocks([await L2Block.random(BlockNumber(91)), await L2Block.random(BlockNumber(92))]); + blockSource.setCheckpointedBlockNumber(92); await client.sync(); await expect(client.getL2Tips()).resolves.toEqual({ proposed: { number: BlockNumber(92), hash: expect.any(String) }, - // Mock source sets checkpointed to match latest, so checkpointed becomes 92 after adding blocks - checkpointed: { block: { number: BlockNumber(92), hash: expect.any(String) }, checkpoint: zeroCheckpoint }, - proven: { block: { number: BlockNumber(90), hash: expect.any(String) }, checkpoint: zeroCheckpoint }, - finalized: { block: { number: BlockNumber(50), hash: expect.any(String) }, checkpoint: zeroCheckpoint }, + checkpointed: { block: { number: BlockNumber(92), hash: expect.any(String) }, checkpoint: anyCheckpoint }, + proven: { block: { number: BlockNumber(90), hash: expect.any(String) }, checkpoint: anyCheckpoint }, + finalized: { block: { number: BlockNumber(50), hash: expect.any(String) }, checkpoint: anyCheckpoint }, }); }); diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts index ff3c525f3ae1..ff131d7ea528 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts @@ -99,7 +99,7 @@ export class L2BlockStream { // This check is not 100% accurate // If the local tips are sufficiently behind the source tips, such that we are missing at least one checkpoint // that has now been re-orged due to a proof failure then this will indicate a failure to checkpoint rather than a failure to prove - // TODO: (PhilWindle): Improve re-org detection accuracy. + // TODO: (mbps/PhilWindle): Improve re-org detection accuracy. let reason: L2BlockPruneReason = 'unproven'; if (latestBlockNumber === localTips.checkpointed.block.number) { reason = 'uncheckpointed'; From 324043821ad68573243ab7b7a45e2685a27f30b8 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Wed, 7 Jan 2026 17:58:52 +0000 Subject: [PATCH 28/36] Updated sentinel test --- .../aztec-node/src/sentinel/sentinel.test.ts | 71 +++++++------------ 1 file changed, 25 insertions(+), 46 deletions(-) diff --git a/yarn-project/aztec-node/src/sentinel/sentinel.test.ts b/yarn-project/aztec-node/src/sentinel/sentinel.test.ts index 10a3957144cf..5f4c1fb14598 100644 --- a/yarn-project/aztec-node/src/sentinel/sentinel.test.ts +++ b/yarn-project/aztec-node/src/sentinel/sentinel.test.ts @@ -96,10 +96,17 @@ describe('sentinel', () => { let proposer: EthAddress; let committee: EthAddress[]; + /** Helper to create and emit a chain-checkpointed event */ + const emitCheckpointEvent = async (checkpoint: Checkpoint, checkpointAttestations: CommitteeAttestation[] = []) => { + const published = new PublishedCheckpoint(checkpoint, L1PublishedData.random(), checkpointAttestations); + await sentinel.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: published }); + return published; + }; + beforeEach(async () => { signers = times(4, Secp256k1Signer.random); validators = signers.map(signer => signer.address); - block = await L2BlockNew.random(BlockNumber(1), { slotNumber: SlotNumber(0) }); + block = await L2BlockNew.random(BlockNumber(1), { slotNumber: slot }); attestations = signers.map(signer => makeBlockAttestation({ signer, archive: block.archive.root })); proposer = validators[0]; committee = [...validators]; @@ -108,13 +115,9 @@ describe('sentinel', () => { }); it('flags block as mined', async () => { - // Set checkpoint data to simulate block being mined - sentinel.setCheckpointForSlot(slot, { - checkpointNumber: CheckpointNumber(1), - archive: block.archive.root.toString(), - attestors: [], - }); - await sentinel.handleBlockStreamEvent({ type: 'blocks-added', blocks: [block] }); + // Create a checkpoint with a block at the target slot and emit chain-checkpointed event + const checkpoint = await Checkpoint.random(CheckpointNumber(1), { numBlocks: 1, slotNumber: slot }); + await emitCheckpointEvent(checkpoint); const activity = await sentinel.getSlotActivity(slot, epoch, proposer, committee); expect(activity[proposer.toString()]).toEqual('block-mined'); @@ -135,12 +138,14 @@ describe('sentinel', () => { it('identifies attestors from p2p and archiver', async () => { // Create a checkpoint with a block at the target slot const checkpoint = await Checkpoint.random(CheckpointNumber(1), { numBlocks: 1, slotNumber: slot }); - // Create attestations from signers + // Create attestations from signers 0 and 1 const checkpointAttestations = signers.slice(0, 2).map(signer => { const blockAttestation = makeAttestationFromCheckpoint(checkpoint, signer, signer); return new CommitteeAttestation(signer.address, blockAttestation.signature); }); - publishedCheckpoint = new PublishedCheckpoint(checkpoint, L1PublishedData.random(), checkpointAttestations); + + // Emit the chain-checkpointed event with attestations from signers 0 and 1 + publishedCheckpoint = await emitCheckpointEvent(checkpoint, checkpointAttestations); const attestorsFromCheckpoint = compactArray( getAttestationInfoFromPublishedCheckpoint(publishedCheckpoint).map(info => @@ -153,16 +158,6 @@ describe('sentinel', () => { signers.slice(0, 2).map(a => a.address.toString()), ); - // Set checkpoint data with attestors from signers 0 and 1 (validators 0 and 1) - sentinel.setCheckpointForSlot(slot, { - checkpointNumber: CheckpointNumber(1), - archive: checkpoint.archive.root.toString(), - attestors: attestorsFromCheckpoint, - }); - - // Use the block from the checkpoint for the event - block = checkpoint.blocks[0]; - await sentinel.handleBlockStreamEvent({ type: 'blocks-added', blocks: [block] }); // P2P provides attestation from signer 2 (validator 2) p2p.getAttestationsForSlot.mockResolvedValue(attestations.slice(2, 3)); @@ -190,7 +185,10 @@ describe('sentinel', () => { // Combine signed and placeholder attestations const allAttestations = [...signedAttestations, ...placeholderAttestations]; - publishedCheckpoint = new PublishedCheckpoint(checkpoint, L1PublishedData.random(), allAttestations); + + // Emit chain-checkpointed event with both signed and placeholder attestations + // The Sentinel should only count the recovered-from-signature ones + publishedCheckpoint = await emitCheckpointEvent(checkpoint, allAttestations); // Verify that getAttestationInfoFromPublishedCheckpoint returns 4 entries total: // - 2 with status 'recovered-from-signature' (actual attestations with valid signatures) @@ -202,21 +200,6 @@ describe('sentinel', () => { expect(recoveredSignatures).toHaveLength(2); expect(placeholders).toHaveLength(2); - // Set checkpoint data with ONLY the recovered-from-signature attestors (validators 0 and 1) - // This simulates how the Sentinel filters attestations when processing checkpoint-added events - const realAttestors = compactArray( - recoveredSignatures.map(info => (info.status === 'recovered-from-signature' ? info.address : undefined)), - ); - sentinel.setCheckpointForSlot(slot, { - checkpointNumber: CheckpointNumber(1), - archive: checkpoint.archive.root.toString(), - attestors: realAttestors, - }); - - // Use the block from the checkpoint for the event - block = checkpoint.blocks[0]; - await sentinel.handleBlockStreamEvent({ type: 'blocks-added', blocks: [block] }); - // No additional attestations from p2p p2p.getAttestationsForSlot.mockResolvedValue([]); @@ -232,7 +215,11 @@ describe('sentinel', () => { }); it('identifies missed attestors if block is mined', async () => { - await sentinel.handleBlockStreamEvent({ type: 'blocks-added', blocks: [block] }); + // Create checkpoint with a block at the target slot + const checkpoint = await Checkpoint.random(CheckpointNumber(1), { numBlocks: 1, slotNumber: slot }); + await emitCheckpointEvent(checkpoint); + + // P2P provides attestations from validators 0, 1, 2 (not validator 3) p2p.getAttestationsForSlot.mockResolvedValue(attestations.slice(0, -1)); const activity = await sentinel.getSlotActivity(slot, epoch, proposer, committee); @@ -580,7 +567,7 @@ describe('sentinel', () => { ts, now: ts, }); - archiver.getBlock.calledWith(blockNumber).mockResolvedValue(mockBlock as any); + archiver.getL2BlockNew.calledWith(blockNumber).mockResolvedValue(mockBlock); archiver.getL1Constants.mockResolvedValue(l1Constants); epochCache.getL1Constants.mockReturnValue(l1Constants); @@ -884,14 +871,6 @@ class TestSentinel extends Sentinel { this.lastProcessedSlot = slot; } - /** Set checkpoint data for a slot (for testing). */ - public setCheckpointForSlot( - slot: SlotNumber, - data: { checkpointNumber: CheckpointNumber; archive: string; attestors: EthAddress[] }, - ) { - this.slotNumberToCheckpoint.set(slot, data); - } - public getInitialSlot() { return this.initialSlot; } From cd1c61f8573ab650f44b3a055a6c0d4d75746292 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Wed, 7 Jan 2026 18:46:51 +0000 Subject: [PATCH 29/36] Setup req/resp handlers before staring libp2p. --- yarn-project/p2p/src/client/p2p_client.ts | 7 ++-- .../p2p/src/services/libp2p/libp2p_service.ts | 41 ++++++++++--------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/yarn-project/p2p/src/client/p2p_client.ts b/yarn-project/p2p/src/client/p2p_client.ts index 11ba246161a3..386966b363af 100644 --- a/yarn-project/p2p/src/client/p2p_client.ts +++ b/yarn-project/p2p/src/client/p2p_client.ts @@ -781,9 +781,10 @@ export class P2PClient if (this.currentState !== P2PClientState.SYNCHING) { return; } - const syncedFinalizedBlock = await this.getSyncedFinalizedBlockNum(); - const syncedProvenBlock = await this.getSyncedProvenBlockNum(); - const syncedLatestBlock = await this.getSyncedLatestBlockNum(); + const tips = await this.l2Tips.getL2Tips(); + const syncedFinalizedBlock = tips.finalized.block.number; + const syncedProvenBlock = tips.proven.block.number; + const syncedLatestBlock = tips.proposed.number; if ( syncedLatestBlock >= this.latestBlockNumberAtStart && diff --git a/yarn-project/p2p/src/services/libp2p/libp2p_service.ts b/yarn-project/p2p/src/services/libp2p/libp2p_service.ts index 812e1b36dd75..4479e961b208 100644 --- a/yarn-project/p2p/src/services/libp2p/libp2p_service.ts +++ b/yarn-project/p2p/src/services/libp2p/libp2p_service.ts @@ -462,17 +462,6 @@ export class LibP2PService extends } const announceTcpMultiaddr = convertToMultiaddr(p2pIp, p2pPort, 'tcp'); - await this.peerManager.initializePeers(); - if (!this.config.p2pDiscoveryDisabled) { - await this.peerDiscoveryService.start(); - } - await this.node.start(); - - // Subscribe to standard GossipSub topics by default - for (const topic of getTopicsForClientAndConfig(this.clientType, this.config.disableTransactions)) { - this.subscribeToTopic(this.topicStrings[topic]); - } - // Create request response protocol handlers const txHandler = reqRespTxHandler(this.mempools); const goodbyeHandler = reqGoodbyeHandler(this.peerManager); @@ -495,10 +484,32 @@ export class LibP2PService extends requestResponseHandlers[ReqRespSubProtocol.TX] = txHandler.bind(this); } + // Define the sub protocol validators - This is done within this start() method to gain a callback to the existing validateTx function + const reqrespSubProtocolValidators = { + ...DEFAULT_SUB_PROTOCOL_VALIDATORS, + [ReqRespSubProtocol.TX]: this.validateRequestedTxs.bind(this), + [ReqRespSubProtocol.BLOCK_TXS]: this.validateRequestedBlockTxs.bind(this), + [ReqRespSubProtocol.BLOCK]: this.validateRequestedBlock.bind(this), + }; + + await this.peerManager.initializePeers(); + + await this.reqresp.start(requestResponseHandlers, reqrespSubProtocolValidators); + + await this.node.start(); + + // Subscribe to standard GossipSub topics by default + for (const topic of getTopicsForClientAndConfig(this.clientType, this.config.disableTransactions)) { + this.subscribeToTopic(this.topicStrings[topic]); + } + // add GossipSub listener this.node.services.pubsub.addEventListener(GossipSubEvent.MESSAGE, this.gossipSubEventHandler); // Start running promise for peer discovery and metrics collection + if (!this.config.p2pDiscoveryDisabled) { + await this.peerDiscoveryService.start(); + } this.discoveryRunningPromise = new RunningPromise( async () => { await this.peerManager.heartbeat(); @@ -508,14 +519,6 @@ export class LibP2PService extends ); this.discoveryRunningPromise.start(); - // Define the sub protocol validators - This is done within this start() method to gain a callback to the existing validateTx function - const reqrespSubProtocolValidators = { - ...DEFAULT_SUB_PROTOCOL_VALIDATORS, - [ReqRespSubProtocol.TX]: this.validateRequestedTxs.bind(this), - [ReqRespSubProtocol.BLOCK_TXS]: this.validateRequestedBlockTxs.bind(this), - [ReqRespSubProtocol.BLOCK]: this.validateRequestedBlock.bind(this), - }; - await this.reqresp.start(requestResponseHandlers, reqrespSubProtocolValidators); this.logger.info(`Started P2P service`, { listen: this.config.listenAddress, port: this.config.p2pPort, From 539e488459789c8293df80b4844b4a41fd18d536 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Wed, 7 Jan 2026 18:57:35 +0000 Subject: [PATCH 30/36] Comment --- .../stdlib/src/block/l2_block_stream/l2_block_stream.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts index ff131d7ea528..3eceb65ffb16 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts @@ -99,7 +99,7 @@ export class L2BlockStream { // This check is not 100% accurate // If the local tips are sufficiently behind the source tips, such that we are missing at least one checkpoint // that has now been re-orged due to a proof failure then this will indicate a failure to checkpoint rather than a failure to prove - // TODO: (mbps/PhilWindle): Improve re-org detection accuracy. + // TODO: (mbps/PhilWindle): Improve re-org detection accuracy when we come to do re-orgs let reason: L2BlockPruneReason = 'unproven'; if (latestBlockNumber === localTips.checkpointed.block.number) { reason = 'uncheckpointed'; From 9b516f31a8e5072258c9291ef3b73e653771295e Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Wed, 7 Jan 2026 19:00:53 +0000 Subject: [PATCH 31/36] Linting --- yarn-project/archiver/src/test/mock_l2_block_source.ts | 9 +++++---- .../sender_sync/sync_sender_tagging_indexes.test.ts | 1 - .../src/block/l2_block_stream/l2_block_stream.test.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn-project/archiver/src/test/mock_l2_block_source.ts b/yarn-project/archiver/src/test/mock_l2_block_source.ts index 836145420013..5339ba9b7e63 100644 --- a/yarn-project/archiver/src/test/mock_l2_block_source.ts +++ b/yarn-project/archiver/src/test/mock_l2_block_source.ts @@ -98,20 +98,21 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { return Promise.resolve(BlockNumber(this.provenBlockNumber)); } - public async getCheckpointedBlock(number: BlockNumber): Promise { + public getCheckpointedBlock(number: BlockNumber): Promise { if (number > this.checkpointedBlockNumber) { - return undefined; + return Promise.resolve(undefined); } const block = this.l2Blocks[number - 1]; if (!block) { - return undefined; + return Promise.resolve(undefined); } - return new CheckpointedL2Block( + const checkpointedBlock = new CheckpointedL2Block( CheckpointNumber(number), block.toL2Block(), new L1PublishedData(BigInt(number), BigInt(number), `0x${number.toString(16).padStart(64, '0')}`), [], ); + return Promise.resolve(checkpointedBlock); } public async getCheckpointedBlocks( diff --git a/yarn-project/pxe/src/tagging/sender_sync/sync_sender_tagging_indexes.test.ts b/yarn-project/pxe/src/tagging/sender_sync/sync_sender_tagging_indexes.test.ts index 0aa89cda0a98..ed4f2d5a4109 100644 --- a/yarn-project/pxe/src/tagging/sender_sync/sync_sender_tagging_indexes.test.ts +++ b/yarn-project/pxe/src/tagging/sender_sync/sync_sender_tagging_indexes.test.ts @@ -1,4 +1,3 @@ -import { BlockNumber } from '@aztec/foundation/branded-types'; import { Fr } from '@aztec/foundation/curves/bn254'; import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts index e6e6520f7bc7..7f52aa17a2d4 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts @@ -9,7 +9,7 @@ import type { PublishedCheckpoint } from '../../checkpoint/published_checkpoint. import type { BlockHeader } from '../../tx/block_header.js'; import type { CheckpointedL2Block } from '../checkpointed_l2_block.js'; import type { L2BlockNew } from '../l2_block_new.js'; -import type { CheckpointId, L2BlockId, L2BlockSource, L2Tips } from '../l2_block_source.js'; +import type { L2BlockId, L2BlockSource, L2Tips } from '../l2_block_source.js'; import type { L2BlockStreamEvent, L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider } from './interfaces.js'; import { L2BlockStream } from './l2_block_stream.js'; import { L2TipsMemoryStore } from './l2_tips_memory_store.js'; From ccd5b9b06fcf8f01d0ba5bc26c8690dc5ca410d7 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Wed, 7 Jan 2026 20:12:13 +0000 Subject: [PATCH 32/36] More block stream updates --- .../l2_block_stream/l2_block_stream.test.ts | 396 ++++++++++++++++++ .../block/l2_block_stream/l2_block_stream.ts | 38 +- 2 files changed, 432 insertions(+), 2 deletions(-) diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts index 7f52aa17a2d4..a368887a4e1e 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts @@ -304,6 +304,402 @@ describe('L2BlockStream', () => { }); }); + describe('multiple blocks per checkpoint', () => { + let localData: TestL2BlockStreamLocalDataProvider; + let handler: TestL2BlockStreamEventHandler; + let blockStream: TestL2BlockStream; + + // Configuration for checkpoint structure: each checkpoint contains 3 blocks + const blocksPerCheckpoint = 3; + + /** Gets the checkpoint number for a given block number */ + const getCheckpointForBlock = (blockNum: number) => Math.ceil(blockNum / blocksPerCheckpoint); + + /** Gets the first block number in a checkpoint */ + const getFirstBlockInCheckpoint = (checkpointNum: number) => (checkpointNum - 1) * blocksPerCheckpoint + 1; + + /** Gets the last block number in a checkpoint */ + const getLastBlockInCheckpoint = (checkpointNum: number) => checkpointNum * blocksPerCheckpoint; + + /** Makes a block with correct checkpoint info */ + const makeBlockInCheckpoint = (blockNum: number) => { + const checkpointNum = getCheckpointForBlock(blockNum); + const firstBlockInCheckpoint = getFirstBlockInCheckpoint(checkpointNum); + return { + number: BlockNumber(blockNum), + checkpointNumber: CheckpointNumber(checkpointNum), + indexWithinCheckpoint: blockNum - firstBlockInCheckpoint, + } as L2BlockNew; + }; + + /** Makes a checkpointed block */ + const makeCheckpointedBlockInCheckpoint = (blockNum: number): CheckpointedL2Block => + ({ + block: makeBlockInCheckpoint(blockNum), + checkpointNumber: getCheckpointForBlock(blockNum), + }) as CheckpointedL2Block; + + /** Sets the remote tips with correct checkpoint numbers for multi-block checkpoints. */ + const setRemoteTipsMultiBlock = ( + latest_: number, + proven?: number, + finalized?: number, + checkpointedBlock?: number, + ) => { + proven = proven ?? 0; + finalized = finalized ?? 0; + checkpointedBlock = checkpointedBlock ?? 0; + latest = latest_; + checkpointed = checkpointedBlock; + + const checkpointedCheckpointNum = checkpointedBlock > 0 ? getCheckpointForBlock(checkpointedBlock) : 0; + const provenCheckpointNum = proven > 0 ? getCheckpointForBlock(proven) : 0; + const finalizedCheckpointNum = finalized > 0 ? getCheckpointForBlock(finalized) : 0; + + blockSource.getL2Tips.mockResolvedValue({ + proposed: { number: BlockNumber(latest), hash: makeHash(latest) }, + checkpointed: { + block: { number: BlockNumber(checkpointedBlock), hash: makeHash(checkpointedBlock) }, + checkpoint: { + number: CheckpointNumber(checkpointedCheckpointNum), + hash: makeHash(checkpointedCheckpointNum), + }, + }, + proven: { + block: { number: BlockNumber(proven), hash: makeHash(proven) }, + checkpoint: { number: CheckpointNumber(provenCheckpointNum), hash: makeHash(provenCheckpointNum) }, + }, + finalized: { + block: { number: BlockNumber(finalized), hash: makeHash(finalized) }, + checkpoint: { number: CheckpointNumber(finalizedCheckpointNum), hash: makeHash(finalizedCheckpointNum) }, + }, + }); + }; + + beforeEach(() => { + localData = new TestL2BlockStreamLocalDataProvider(); + handler = new TestL2BlockStreamEventHandler(); + blockStream = new TestL2BlockStream(blockSource, localData, handler, undefined, { batchSize: 10 }); + + // Override the mocks to support multiple blocks per checkpoint + blockSource.getCheckpointedBlocks.mockImplementation((from, limit) => + Promise.resolve( + compactArray( + times(limit, i => (from + i > checkpointed ? undefined : makeCheckpointedBlockInCheckpoint(from + i))), + ), + ), + ); + + // Returns published checkpoints with multiple blocks each + blockSource.getPublishedCheckpoints.mockImplementation((checkpointNumber: CheckpointNumber, _limit: number) => { + const firstBlock = getFirstBlockInCheckpoint(checkpointNumber); + const lastBlock = Math.min(getLastBlockInCheckpoint(checkpointNumber), checkpointed); + const blocks = times(lastBlock - firstBlock + 1, i => makeBlockInCheckpoint(firstBlock + i)); + return Promise.resolve([ + { + checkpoint: { + number: checkpointNumber, + hash: () => new Fr(checkpointNumber), + blocks, + }, + } as unknown as PublishedCheckpoint, + ]); + }); + }); + + it('emits all blocks in a checkpoint before chain-checkpointed event', async () => { + // Set up: 6 blocks in 2 checkpoints (blocks 1-3 in checkpoint 1, blocks 4-6 in checkpoint 2) + setRemoteTipsMultiBlock(6, 0, 0, 6); + + await blockStream.work(); + + // Should emit blocks 1-3, then checkpoint 1, then blocks 4-6, then checkpoint 2 + expect(handler.events).toEqual([ + { + type: 'blocks-added', + blocks: [makeBlockInCheckpoint(1), makeBlockInCheckpoint(2), makeBlockInCheckpoint(3)], + }, + expect.objectContaining({ type: 'chain-checkpointed' }), + { + type: 'blocks-added', + blocks: [makeBlockInCheckpoint(4), makeBlockInCheckpoint(5), makeBlockInCheckpoint(6)], + }, + expect.objectContaining({ type: 'chain-checkpointed' }), + ]); + }); + + it('handles partial checkpoint at the end (uncheckpointed blocks)', async () => { + // Set up: 5 blocks total, but only first 3 are checkpointed (checkpoint 1 complete) + // Blocks 4-5 are uncheckpointed + setRemoteTipsMultiBlock(5, 0, 0, 3); + + await blockStream.work(); + + // Should emit checkpoint 1 blocks, then checkpoint event, then uncheckpointed blocks 4-5 + expect(handler.events).toEqual([ + { + type: 'blocks-added', + blocks: [makeBlockInCheckpoint(1), makeBlockInCheckpoint(2), makeBlockInCheckpoint(3)], + }, + expect.objectContaining({ type: 'chain-checkpointed' }), + { type: 'blocks-added', blocks: [makeBlock(4), makeBlock(5)] }, + ]); + }); + + it('handles starting from middle of a checkpoint', async () => { + // Set up: 9 blocks in 3 checkpoints, but we start from block 5 (middle of checkpoint 2) + // Local has blocks 1-4, local checkpointed = 0 + setRemoteTipsMultiBlock(9, 0, 0, 9); + localData.proposed.number = BlockNumber(4); + + await blockStream.work(); + + // Should first emit checkpoint 1 (blocks 1-3 already local) + // Then continue from block 5, which is in checkpoint 2 + // Blocks 5-6 complete checkpoint 2, then blocks 7-9 complete checkpoint 3 + expect(handler.events).toEqual([ + expect.objectContaining({ type: 'chain-checkpointed' }), // checkpoint 1 for already-local blocks 1-3 + { type: 'blocks-added', blocks: [makeBlockInCheckpoint(5), makeBlockInCheckpoint(6)] }, + expect.objectContaining({ type: 'chain-checkpointed' }), // checkpoint 2 + { + type: 'blocks-added', + blocks: [makeBlockInCheckpoint(7), makeBlockInCheckpoint(8), makeBlockInCheckpoint(9)], + }, + expect.objectContaining({ type: 'chain-checkpointed' }), // checkpoint 3 + ]); + + // Verify checkpoint order + const checkpointEvents = handler.events.filter(e => e.type === 'chain-checkpointed'); + expect(checkpointEvents).toHaveLength(3); + expect((checkpointEvents[0] as any).checkpoint.checkpoint.number).toBe(CheckpointNumber(1)); + expect((checkpointEvents[1] as any).checkpoint.checkpoint.number).toBe(CheckpointNumber(2)); + expect((checkpointEvents[2] as any).checkpoint.checkpoint.number).toBe(CheckpointNumber(3)); + }); + + it('correctly identifies checkpoint number in chain-checkpointed events', async () => { + // Set up: 6 blocks in 2 checkpoints + setRemoteTipsMultiBlock(6, 0, 0, 6); + + await blockStream.work(); + + // Extract the chain-checkpointed events + const checkpointEvents = handler.events.filter(e => e.type === 'chain-checkpointed'); + expect(checkpointEvents).toHaveLength(2); + expect((checkpointEvents[0] as any).checkpoint.checkpoint.number).toBe(CheckpointNumber(1)); + expect((checkpointEvents[1] as any).checkpoint.checkpoint.number).toBe(CheckpointNumber(2)); + }); + + it('handles many checkpoints with batching', async () => { + // Set up: 12 blocks in 4 checkpoints, with batch size of 5 + blockStream = new TestL2BlockStream(blockSource, localData, handler, undefined, { batchSize: 5 }); + setRemoteTipsMultiBlock(12, 0, 0, 12); + + await blockStream.work(); + + // Should have 4 checkpoint events (one per checkpoint) + const checkpointEvents = handler.events.filter(e => e.type === 'chain-checkpointed'); + expect(checkpointEvents).toHaveLength(4); + + // Should have multiple blocks-added events due to batching and checkpoint boundaries + const blocksAddedEvents = handler.events.filter(e => e.type === 'blocks-added'); + expect(blocksAddedEvents.length).toBeGreaterThanOrEqual(4); + + // Verify all blocks were added in order + const allBlocks = blocksAddedEvents.flatMap(e => (e as any).blocks); + expect(allBlocks.map((b: L2BlockNew) => b.number)).toEqual(times(12, i => BlockNumber(i + 1))); + }); + + it('emits checkpoint event when blocks become checkpointed after being added as uncheckpointed', async () => { + // Phase 1: Start with 3 checkpointed blocks (checkpoint 1), then add blocks 4-6 as uncheckpointed + setRemoteTipsMultiBlock(6, 0, 0, 3); + + await blockStream.work(); + + // Expect: blocks 1-3 via checkpoint, then uncheckpointed blocks 4-6 + expect(handler.events).toEqual([ + { + type: 'blocks-added', + blocks: [makeBlockInCheckpoint(1), makeBlockInCheckpoint(2), makeBlockInCheckpoint(3)], + }, + expect.objectContaining({ type: 'chain-checkpointed' }), + { type: 'blocks-added', blocks: [makeBlock(4), makeBlock(5), makeBlock(6)] }, + ]); + + handler.clearEvents(); + + // Update local state to reflect what the handler would have stored + localData.proposed.number = BlockNumber(6); + localData.checkpointed.block.number = BlockNumber(3); + localData.checkpointed.checkpoint.number = CheckpointNumber(1); + + // Phase 2: Now checkpoint 2 completes (blocks 4-6 become checkpointed) + setRemoteTipsMultiBlock(6, 0, 0, 6); + + await blockStream.work(); + + // Should emit a checkpoint event for checkpoint 2 (blocks 4-6), even though blocks were already added + const checkpointEvents = handler.events.filter(e => e.type === 'chain-checkpointed'); + expect(checkpointEvents).toHaveLength(1); + expect((checkpointEvents[0] as any).checkpoint.checkpoint.number).toBe(CheckpointNumber(2)); + }); + + it('emits checkpoint event BEFORE new uncheckpointed blocks when checkpoint completes', async () => { + // Phase 1: Start with 3 checkpointed blocks (checkpoint 1), then add blocks 4-6 as uncheckpointed + setRemoteTipsMultiBlock(6, 0, 0, 3); + + await blockStream.work(); + + // Expect: blocks 1-3 via checkpoint, then uncheckpointed blocks 4-6 + expect(handler.events).toEqual([ + { + type: 'blocks-added', + blocks: [makeBlockInCheckpoint(1), makeBlockInCheckpoint(2), makeBlockInCheckpoint(3)], + }, + expect.objectContaining({ type: 'chain-checkpointed' }), + { type: 'blocks-added', blocks: [makeBlock(4), makeBlock(5), makeBlock(6)] }, + ]); + + handler.clearEvents(); + + // Update local state to reflect what the handler would have stored + localData.proposed.number = BlockNumber(6); + localData.checkpointed.block.number = BlockNumber(3); + localData.checkpointed.checkpoint.number = CheckpointNumber(1); + + // Phase 2: Checkpoint 2 completes (blocks 4-6) AND a new block 7 arrives + setRemoteTipsMultiBlock(7, 0, 0, 6); + + await blockStream.work(); + + // Should emit checkpoint 2 FIRST, then the new uncheckpointed block 7 + // NOT: block 7 first, then checkpoint 2 + expect(handler.events).toEqual([ + expect.objectContaining({ type: 'chain-checkpointed' }), + { type: 'blocks-added', blocks: [makeBlock(7)] }, + ]); + expect((handler.events[0] as any).checkpoint.checkpoint.number).toBe(CheckpointNumber(2)); + }); + + it('emits checkpoint as soon as last block in checkpoint arrives', async () => { + // This tests the realistic scenario where checkpoints are published as blocks arrive. + // Uncheckpointed blocks are always just a partial checkpoint (the current incomplete one). + + // Sync 1: Source has checkpointed=6 (checkpoint 2), proposed=9 + // Client gets blocks 1-6 via checkpoints, blocks 7-9 as uncheckpointed + setRemoteTipsMultiBlock(9, 0, 0, 6); + + await blockStream.work(); + + expect(handler.events).toEqual([ + { + type: 'blocks-added', + blocks: [makeBlockInCheckpoint(1), makeBlockInCheckpoint(2), makeBlockInCheckpoint(3)], + }, + expect.objectContaining({ type: 'chain-checkpointed' }), // checkpoint 1 + { + type: 'blocks-added', + blocks: [makeBlockInCheckpoint(4), makeBlockInCheckpoint(5), makeBlockInCheckpoint(6)], + }, + expect.objectContaining({ type: 'chain-checkpointed' }), // checkpoint 2 + { type: 'blocks-added', blocks: [makeBlock(7), makeBlock(8), makeBlock(9)] }, // uncheckpointed + ]); + + let checkpointEvents = handler.events.filter(e => e.type === 'chain-checkpointed'); + expect(checkpointEvents).toHaveLength(2); + expect((checkpointEvents[0] as any).checkpoint.checkpoint.number).toBe(CheckpointNumber(1)); + expect((checkpointEvents[1] as any).checkpoint.checkpoint.number).toBe(CheckpointNumber(2)); + + handler.clearEvents(); + + // Update local state + localData.proposed.number = BlockNumber(9); + localData.checkpointed.block.number = BlockNumber(6); + localData.checkpointed.checkpoint.number = CheckpointNumber(2); + + // Sync 2: Checkpoint 3 is now published (blocks 7-9), new blocks 10-12 are uncheckpointed + setRemoteTipsMultiBlock(12, 0, 0, 9); + + await blockStream.work(); + + // Should emit checkpoint 3 for already-local blocks 7-9, then uncheckpointed blocks 10-12 + expect(handler.events).toEqual([ + expect.objectContaining({ type: 'chain-checkpointed' }), // checkpoint 3 + { type: 'blocks-added', blocks: [makeBlock(10), makeBlock(11), makeBlock(12)] }, // uncheckpointed + ]); + + checkpointEvents = handler.events.filter(e => e.type === 'chain-checkpointed'); + expect(checkpointEvents).toHaveLength(1); + expect((checkpointEvents[0] as any).checkpoint.checkpoint.number).toBe(CheckpointNumber(3)); + + handler.clearEvents(); + + // Update local state + localData.proposed.number = BlockNumber(12); + localData.checkpointed.block.number = BlockNumber(9); + localData.checkpointed.checkpoint.number = CheckpointNumber(3); + + // Sync 3: Checkpoint 4 is now published (blocks 10-12), no new blocks + setRemoteTipsMultiBlock(12, 0, 0, 12); + + await blockStream.work(); + + // Should emit checkpoint 4 for already-local blocks 10-12 + expect(handler.events).toEqual([ + expect.objectContaining({ type: 'chain-checkpointed' }), // checkpoint 4 + ]); + + checkpointEvents = handler.events.filter(e => e.type === 'chain-checkpointed'); + expect(checkpointEvents).toHaveLength(1); + expect((checkpointEvents[0] as any).checkpoint.checkpoint.number).toBe(CheckpointNumber(4)); + }); + + it('emits all checkpoints when source jumps ahead with multiple new checkpoints', async () => { + // Phase 1: Start with checkpoint 1 complete (blocks 1-3), blocks 4-6 uncheckpointed + setRemoteTipsMultiBlock(6, 0, 0, 3); + + await blockStream.work(); + + expect(handler.events).toEqual([ + { + type: 'blocks-added', + blocks: [makeBlockInCheckpoint(1), makeBlockInCheckpoint(2), makeBlockInCheckpoint(3)], + }, + expect.objectContaining({ type: 'chain-checkpointed' }), + { type: 'blocks-added', blocks: [makeBlock(4), makeBlock(5), makeBlock(6)] }, + ]); + + handler.clearEvents(); + + // Update local state + localData.proposed.number = BlockNumber(6); + localData.checkpointed.block.number = BlockNumber(3); + localData.checkpointed.checkpoint.number = CheckpointNumber(1); + + // Phase 2: Source jumps to block 12 with checkpoints at 6, 9, and 12 + // - Checkpoint 2 (blocks 4-6) - blocks already local, needs checkpoint event + // - Checkpoint 3 (blocks 7-9) - new blocks + checkpoint event + // - Checkpoint 4 (blocks 10-12) - new blocks + checkpoint event + setRemoteTipsMultiBlock(12, 0, 0, 12); + + await blockStream.work(); + + // Should emit: + // 1. Checkpoint 2 event (blocks 4-6 were already local) + // 2. Blocks 7-9 + checkpoint 3 event + // 3. Blocks 10-12 + checkpoint 4 event + const checkpointEvents = handler.events.filter(e => e.type === 'chain-checkpointed'); + expect(checkpointEvents).toHaveLength(3); + expect((checkpointEvents[0] as any).checkpoint.checkpoint.number).toBe(CheckpointNumber(2)); + expect((checkpointEvents[1] as any).checkpoint.checkpoint.number).toBe(CheckpointNumber(3)); + expect((checkpointEvents[2] as any).checkpoint.checkpoint.number).toBe(CheckpointNumber(4)); + + // Should also emit blocks 7-12 + const blocksAddedEvents = handler.events.filter(e => e.type === 'blocks-added'); + const allBlockNumbers = blocksAddedEvents.flatMap(e => (e as any).blocks.map((b: any) => b.number)); + expect(allBlockNumbers).toEqual([7, 8, 9, 10, 11, 12].map(BlockNumber)); + }); + }); + describe('skipFinalized', () => { let localData: TestL2BlockStreamLocalDataProvider; let handler: TestL2BlockStreamEventHandler; diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts index 3eceb65ffb16..57ebb5742c1c 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts @@ -133,7 +133,41 @@ export class L2BlockStream { nextBlockNumber = Math.max(sourceTips.finalized.block.number, nextBlockNumber); } - // Request checkpointed blocks from the source up until the tip of the checkpointed chain. + // First, emit checkpoint events for checkpoints whose blocks are already in local storage. + // As we should only ever have a single checkpoint's worth of uncheckpointed blocks locally, this + // should only iterate once + let nextCheckpointToEmit = CheckpointNumber(localTips.checkpointed.checkpoint.number + 1); + let iterations = 0; + while (nextCheckpointToEmit <= sourceTips.checkpointed.checkpoint.number) { + const checkpoints = await this.l2BlockSource.getPublishedCheckpoints(nextCheckpointToEmit, 1); + if (checkpoints.length === 0) { + break; + } + // Check if all blocks in this checkpoint are already in local storage + const lastBlockInCheckpoint = checkpoints[0].checkpoint.blocks.at(-1)!.number; + if (lastBlockInCheckpoint > localTips.proposed.number) { + // This checkpoint has blocks we haven't seen yet, stop here + break; + } + iterations++; + if (iterations > 1) { + this.log.warn(`Emitting multiple checkpoints (${iterations}) without new blocks being added.`); + } + await this.emitEvent({ + type: 'chain-checkpointed', + checkpoint: checkpoints[0], + }); + nextCheckpointToEmit = CheckpointNumber(nextCheckpointToEmit + 1); + } + + // We have now effectively checkpointed our view of the chain. As in there should be no checkpointed blocks + // that we have seen locally and not emitted checkpoints for. + + // Now fetch any new checkpointed blocks. If nextBlockNumber is below the source's checkpointed block number + // then we will retrieve it as a checkpointed block, retrieve the checkpoint and emit all blocks from that point forward + // that are part of the checkpoint, before emitting the checkpoint itself. + // We do this until all checkpointed blocks and checkpoints are emitted. + // This takes our local chain up to date with the source's checkpointed blocks. let checkpointNumber = CheckpointNumber(INITIAL_CHECKPOINT_NUMBER - 1); while (nextBlockNumber <= sourceTips.checkpointed.block.number) { const limit = Math.min(this.opts.batchSize ?? 50, sourceTips.checkpointed.block.number - nextBlockNumber + 1); @@ -169,7 +203,7 @@ export class L2BlockStream { } } - // Request new blocks from the source, these will be uncheckpointed blocks. + // Now we pull any remaining, uncheckpointed block and emit them. while (nextBlockNumber <= sourceTips.proposed.number) { const limit = Math.min(this.opts.batchSize ?? 50, sourceTips.proposed.number - nextBlockNumber + 1); this.log.trace(`Requesting blocks from ${nextBlockNumber} limit ${limit} proven=${this.opts.proven}`); From fcdab28b9b06c4d1dcd16f5a582a0941781d3788 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Wed, 7 Jan 2026 20:31:29 +0000 Subject: [PATCH 33/36] Also emit the block Id --- .../aztec-node/src/sentinel/sentinel.test.ts | 4 +- .../src/block/l2_block_stream/interfaces.ts | 1 + .../l2_block_stream/l2_block_stream.test.ts | 205 +++++++++--------- .../block/l2_block_stream/l2_block_stream.ts | 5 + .../block/test/l2_tips_store_test_suite.ts | 40 ++-- 5 files changed, 132 insertions(+), 123 deletions(-) diff --git a/yarn-project/aztec-node/src/sentinel/sentinel.test.ts b/yarn-project/aztec-node/src/sentinel/sentinel.test.ts index 5f4c1fb14598..55c3369f411b 100644 --- a/yarn-project/aztec-node/src/sentinel/sentinel.test.ts +++ b/yarn-project/aztec-node/src/sentinel/sentinel.test.ts @@ -99,7 +99,9 @@ describe('sentinel', () => { /** Helper to create and emit a chain-checkpointed event */ const emitCheckpointEvent = async (checkpoint: Checkpoint, checkpointAttestations: CommitteeAttestation[] = []) => { const published = new PublishedCheckpoint(checkpoint, L1PublishedData.random(), checkpointAttestations); - await sentinel.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: published }); + const lastBlock = checkpoint.blocks.at(-1)!; + const block = { number: lastBlock.number, hash: (await lastBlock.hash()).toString() }; + await sentinel.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: published, block }); return published; }; diff --git a/yarn-project/stdlib/src/block/l2_block_stream/interfaces.ts b/yarn-project/stdlib/src/block/l2_block_stream/interfaces.ts index d3fc9fa1e164..7a16689dcfb8 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/interfaces.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/interfaces.ts @@ -21,6 +21,7 @@ export type L2BlockStreamEvent = | /** Emits checkpoints published to L1. */ { type: 'chain-checkpointed'; checkpoint: PublishedCheckpoint; + block: L2BlockId; } | /** Reports last correct block (new tip of the proposed chain). */ { type: 'chain-pruned'; diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts index a368887a4e1e..f37d3d089aab 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts @@ -29,6 +29,15 @@ describe('L2BlockStream', () => { indexWithinCheckpoint: 0, }) as L2BlockNew; + /** Makes a block with hash method (for use in mocks that need hash) */ + const makeBlockWithHash = (number: number) => + ({ + number: BlockNumber(number), + checkpointNumber: CheckpointNumber(number), + indexWithinCheckpoint: 0, + hash: () => Promise.resolve(new Fr(number)), + }) as L2BlockNew; + const makeCheckpointedBlock = (number: number, checkpointNum: number): CheckpointedL2Block => ({ block: makeBlock(number), @@ -39,6 +48,29 @@ describe('L2BlockStream', () => { const makeBlockId = (number: number): L2BlockId => ({ number: BlockNumber(number), hash: makeHash(number) }); + /** Helper to match a blocks-added event with blocks that may have extra properties like hash */ + const expectBlocksAdded = (blockNumbers: number[]) => + expect.objectContaining({ + type: 'blocks-added', + blocks: blockNumbers.map(n => + expect.objectContaining({ + number: BlockNumber(n), + }), + ), + }); + + /** Helper to match a chain-checkpointed event */ + const expectCheckpointed = (checkpointNumber?: number) => + checkpointNumber !== undefined + ? expect.objectContaining({ + type: 'chain-checkpointed', + checkpoint: expect.objectContaining({ + checkpoint: expect.objectContaining({ number: checkpointNumber }), + }), + block: expect.objectContaining({ number: expect.any(Number) }), + }) + : expect.objectContaining({ type: 'chain-checkpointed' }); + const makeCheckpointId = (number: number) => ({ number: CheckpointNumber(number), hash: makeHash(number) }); const makeTipId = (number: number) => ({ @@ -94,7 +126,7 @@ describe('L2BlockStream', () => { checkpoint: { number: checkpointNumber, hash: () => new Fr(checkpointNumber), - blocks: [makeBlock(checkpointNumber)], + blocks: [makeBlockWithHash(checkpointNumber)], }, } as unknown as PublishedCheckpoint, ]), @@ -210,16 +242,16 @@ describe('L2BlockStream', () => { // Each checkpointed block triggers a blocks-added and chain-checkpointed event // (since each checkpoint contains one block in our mock) expect(handler.events).toEqual([ - { type: 'blocks-added', blocks: [makeBlock(1)] }, - expect.objectContaining({ type: 'chain-checkpointed' }), - { type: 'blocks-added', blocks: [makeBlock(2)] }, - expect.objectContaining({ type: 'chain-checkpointed' }), - { type: 'blocks-added', blocks: [makeBlock(3)] }, - expect.objectContaining({ type: 'chain-checkpointed' }), - { type: 'blocks-added', blocks: [makeBlock(4)] }, - expect.objectContaining({ type: 'chain-checkpointed' }), - { type: 'blocks-added', blocks: [makeBlock(5)] }, - expect.objectContaining({ type: 'chain-checkpointed' }), + expectBlocksAdded([1]), + expectCheckpointed(), + expectBlocksAdded([2]), + expectCheckpointed(), + expectBlocksAdded([3]), + expectCheckpointed(), + expectBlocksAdded([4]), + expectCheckpointed(), + expectBlocksAdded([5]), + expectCheckpointed(), ]); expect(blockSource.getCheckpointedBlocks).toHaveBeenCalledTimes(5); expect(blockSource.getL2BlocksNew).not.toHaveBeenCalled(); @@ -233,13 +265,13 @@ describe('L2BlockStream', () => { // First 3 blocks come via checkpoints, last 2 via getL2BlocksNew expect(handler.events).toEqual([ - { type: 'blocks-added', blocks: [makeBlock(1)] }, - expect.objectContaining({ type: 'chain-checkpointed' }), - { type: 'blocks-added', blocks: [makeBlock(2)] }, - expect.objectContaining({ type: 'chain-checkpointed' }), - { type: 'blocks-added', blocks: [makeBlock(3)] }, - expect.objectContaining({ type: 'chain-checkpointed' }), - { type: 'blocks-added', blocks: [makeBlock(4), makeBlock(5)] }, + expectBlocksAdded([1]), + expectCheckpointed(), + expectBlocksAdded([2]), + expectCheckpointed(), + expectBlocksAdded([3]), + expectCheckpointed(), + expectBlocksAdded([4, 5]), ]); expect(blockSource.getCheckpointedBlocks).toHaveBeenCalledTimes(3); expect(blockSource.getL2BlocksNew).toHaveBeenCalledWith(BlockNumber(4), 2, undefined); @@ -332,6 +364,18 @@ describe('L2BlockStream', () => { } as L2BlockNew; }; + /** Makes a block with hash method (for use in mocks that need hash) */ + const makeBlockInCheckpointWithHash = (blockNum: number) => { + const checkpointNum = getCheckpointForBlock(blockNum); + const firstBlockInCheckpoint = getFirstBlockInCheckpoint(checkpointNum); + return { + number: BlockNumber(blockNum), + checkpointNumber: CheckpointNumber(checkpointNum), + indexWithinCheckpoint: blockNum - firstBlockInCheckpoint, + hash: () => Promise.resolve(new Fr(blockNum)), + } as L2BlockNew; + }; + /** Makes a checkpointed block */ const makeCheckpointedBlockInCheckpoint = (blockNum: number): CheckpointedL2Block => ({ @@ -394,7 +438,7 @@ describe('L2BlockStream', () => { blockSource.getPublishedCheckpoints.mockImplementation((checkpointNumber: CheckpointNumber, _limit: number) => { const firstBlock = getFirstBlockInCheckpoint(checkpointNumber); const lastBlock = Math.min(getLastBlockInCheckpoint(checkpointNumber), checkpointed); - const blocks = times(lastBlock - firstBlock + 1, i => makeBlockInCheckpoint(firstBlock + i)); + const blocks = times(lastBlock - firstBlock + 1, i => makeBlockInCheckpointWithHash(firstBlock + i)); return Promise.resolve([ { checkpoint: { @@ -415,16 +459,10 @@ describe('L2BlockStream', () => { // Should emit blocks 1-3, then checkpoint 1, then blocks 4-6, then checkpoint 2 expect(handler.events).toEqual([ - { - type: 'blocks-added', - blocks: [makeBlockInCheckpoint(1), makeBlockInCheckpoint(2), makeBlockInCheckpoint(3)], - }, - expect.objectContaining({ type: 'chain-checkpointed' }), - { - type: 'blocks-added', - blocks: [makeBlockInCheckpoint(4), makeBlockInCheckpoint(5), makeBlockInCheckpoint(6)], - }, - expect.objectContaining({ type: 'chain-checkpointed' }), + expectBlocksAdded([1, 2, 3]), + expectCheckpointed(1), + expectBlocksAdded([4, 5, 6]), + expectCheckpointed(2), ]); }); @@ -436,14 +474,7 @@ describe('L2BlockStream', () => { await blockStream.work(); // Should emit checkpoint 1 blocks, then checkpoint event, then uncheckpointed blocks 4-5 - expect(handler.events).toEqual([ - { - type: 'blocks-added', - blocks: [makeBlockInCheckpoint(1), makeBlockInCheckpoint(2), makeBlockInCheckpoint(3)], - }, - expect.objectContaining({ type: 'chain-checkpointed' }), - { type: 'blocks-added', blocks: [makeBlock(4), makeBlock(5)] }, - ]); + expect(handler.events).toEqual([expectBlocksAdded([1, 2, 3]), expectCheckpointed(1), expectBlocksAdded([4, 5])]); }); it('handles starting from middle of a checkpoint', async () => { @@ -458,14 +489,11 @@ describe('L2BlockStream', () => { // Then continue from block 5, which is in checkpoint 2 // Blocks 5-6 complete checkpoint 2, then blocks 7-9 complete checkpoint 3 expect(handler.events).toEqual([ - expect.objectContaining({ type: 'chain-checkpointed' }), // checkpoint 1 for already-local blocks 1-3 - { type: 'blocks-added', blocks: [makeBlockInCheckpoint(5), makeBlockInCheckpoint(6)] }, - expect.objectContaining({ type: 'chain-checkpointed' }), // checkpoint 2 - { - type: 'blocks-added', - blocks: [makeBlockInCheckpoint(7), makeBlockInCheckpoint(8), makeBlockInCheckpoint(9)], - }, - expect.objectContaining({ type: 'chain-checkpointed' }), // checkpoint 3 + expectCheckpointed(1), // checkpoint 1 for already-local blocks 1-3 + expectBlocksAdded([5, 6]), + expectCheckpointed(2), // checkpoint 2 + expectBlocksAdded([7, 8, 9]), + expectCheckpointed(3), // checkpoint 3 ]); // Verify checkpoint order @@ -517,12 +545,9 @@ describe('L2BlockStream', () => { // Expect: blocks 1-3 via checkpoint, then uncheckpointed blocks 4-6 expect(handler.events).toEqual([ - { - type: 'blocks-added', - blocks: [makeBlockInCheckpoint(1), makeBlockInCheckpoint(2), makeBlockInCheckpoint(3)], - }, - expect.objectContaining({ type: 'chain-checkpointed' }), - { type: 'blocks-added', blocks: [makeBlock(4), makeBlock(5), makeBlock(6)] }, + expectBlocksAdded([1, 2, 3]), + expectCheckpointed(1), + expectBlocksAdded([4, 5, 6]), ]); handler.clearEvents(); @@ -551,12 +576,9 @@ describe('L2BlockStream', () => { // Expect: blocks 1-3 via checkpoint, then uncheckpointed blocks 4-6 expect(handler.events).toEqual([ - { - type: 'blocks-added', - blocks: [makeBlockInCheckpoint(1), makeBlockInCheckpoint(2), makeBlockInCheckpoint(3)], - }, - expect.objectContaining({ type: 'chain-checkpointed' }), - { type: 'blocks-added', blocks: [makeBlock(4), makeBlock(5), makeBlock(6)] }, + expectBlocksAdded([1, 2, 3]), + expectCheckpointed(1), + expectBlocksAdded([4, 5, 6]), ]); handler.clearEvents(); @@ -573,11 +595,7 @@ describe('L2BlockStream', () => { // Should emit checkpoint 2 FIRST, then the new uncheckpointed block 7 // NOT: block 7 first, then checkpoint 2 - expect(handler.events).toEqual([ - expect.objectContaining({ type: 'chain-checkpointed' }), - { type: 'blocks-added', blocks: [makeBlock(7)] }, - ]); - expect((handler.events[0] as any).checkpoint.checkpoint.number).toBe(CheckpointNumber(2)); + expect(handler.events).toEqual([expectCheckpointed(2), expectBlocksAdded([7])]); }); it('emits checkpoint as soon as last block in checkpoint arrives', async () => { @@ -591,24 +609,13 @@ describe('L2BlockStream', () => { await blockStream.work(); expect(handler.events).toEqual([ - { - type: 'blocks-added', - blocks: [makeBlockInCheckpoint(1), makeBlockInCheckpoint(2), makeBlockInCheckpoint(3)], - }, - expect.objectContaining({ type: 'chain-checkpointed' }), // checkpoint 1 - { - type: 'blocks-added', - blocks: [makeBlockInCheckpoint(4), makeBlockInCheckpoint(5), makeBlockInCheckpoint(6)], - }, - expect.objectContaining({ type: 'chain-checkpointed' }), // checkpoint 2 - { type: 'blocks-added', blocks: [makeBlock(7), makeBlock(8), makeBlock(9)] }, // uncheckpointed + expectBlocksAdded([1, 2, 3]), + expectCheckpointed(1), + expectBlocksAdded([4, 5, 6]), + expectCheckpointed(2), + expectBlocksAdded([7, 8, 9]), // uncheckpointed ]); - let checkpointEvents = handler.events.filter(e => e.type === 'chain-checkpointed'); - expect(checkpointEvents).toHaveLength(2); - expect((checkpointEvents[0] as any).checkpoint.checkpoint.number).toBe(CheckpointNumber(1)); - expect((checkpointEvents[1] as any).checkpoint.checkpoint.number).toBe(CheckpointNumber(2)); - handler.clearEvents(); // Update local state @@ -623,14 +630,10 @@ describe('L2BlockStream', () => { // Should emit checkpoint 3 for already-local blocks 7-9, then uncheckpointed blocks 10-12 expect(handler.events).toEqual([ - expect.objectContaining({ type: 'chain-checkpointed' }), // checkpoint 3 - { type: 'blocks-added', blocks: [makeBlock(10), makeBlock(11), makeBlock(12)] }, // uncheckpointed + expectCheckpointed(3), + expectBlocksAdded([10, 11, 12]), // uncheckpointed ]); - checkpointEvents = handler.events.filter(e => e.type === 'chain-checkpointed'); - expect(checkpointEvents).toHaveLength(1); - expect((checkpointEvents[0] as any).checkpoint.checkpoint.number).toBe(CheckpointNumber(3)); - handler.clearEvents(); // Update local state @@ -644,13 +647,7 @@ describe('L2BlockStream', () => { await blockStream.work(); // Should emit checkpoint 4 for already-local blocks 10-12 - expect(handler.events).toEqual([ - expect.objectContaining({ type: 'chain-checkpointed' }), // checkpoint 4 - ]); - - checkpointEvents = handler.events.filter(e => e.type === 'chain-checkpointed'); - expect(checkpointEvents).toHaveLength(1); - expect((checkpointEvents[0] as any).checkpoint.checkpoint.number).toBe(CheckpointNumber(4)); + expect(handler.events).toEqual([expectCheckpointed(4)]); }); it('emits all checkpoints when source jumps ahead with multiple new checkpoints', async () => { @@ -660,12 +657,9 @@ describe('L2BlockStream', () => { await blockStream.work(); expect(handler.events).toEqual([ - { - type: 'blocks-added', - blocks: [makeBlockInCheckpoint(1), makeBlockInCheckpoint(2), makeBlockInCheckpoint(3)], - }, - expect.objectContaining({ type: 'chain-checkpointed' }), - { type: 'blocks-added', blocks: [makeBlock(4), makeBlock(5), makeBlock(6)] }, + expectBlocksAdded([1, 2, 3]), + expectCheckpointed(1), + expectBlocksAdded([4, 5, 6]), ]); handler.clearEvents(); @@ -687,16 +681,13 @@ describe('L2BlockStream', () => { // 1. Checkpoint 2 event (blocks 4-6 were already local) // 2. Blocks 7-9 + checkpoint 3 event // 3. Blocks 10-12 + checkpoint 4 event - const checkpointEvents = handler.events.filter(e => e.type === 'chain-checkpointed'); - expect(checkpointEvents).toHaveLength(3); - expect((checkpointEvents[0] as any).checkpoint.checkpoint.number).toBe(CheckpointNumber(2)); - expect((checkpointEvents[1] as any).checkpoint.checkpoint.number).toBe(CheckpointNumber(3)); - expect((checkpointEvents[2] as any).checkpoint.checkpoint.number).toBe(CheckpointNumber(4)); - - // Should also emit blocks 7-12 - const blocksAddedEvents = handler.events.filter(e => e.type === 'blocks-added'); - const allBlockNumbers = blocksAddedEvents.flatMap(e => (e as any).blocks.map((b: any) => b.number)); - expect(allBlockNumbers).toEqual([7, 8, 9, 10, 11, 12].map(BlockNumber)); + expect(handler.events).toEqual([ + expectCheckpointed(2), + expectBlocksAdded([7, 8, 9]), + expectCheckpointed(3), + expectBlocksAdded([10, 11, 12]), + expectCheckpointed(4), + ]); }); }); diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts index 57ebb5742c1c..c181fbc9d09a 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.ts @@ -153,9 +153,12 @@ export class L2BlockStream { if (iterations > 1) { this.log.warn(`Emitting multiple checkpoints (${iterations}) without new blocks being added.`); } + const lastBlock = checkpoints[0].checkpoint.blocks.at(-1)!; + const lastBlockHash = await lastBlock.hash(); await this.emitEvent({ type: 'chain-checkpointed', checkpoint: checkpoints[0], + block: makeL2BlockId(lastBlock.number, lastBlockHash.toString()), }); nextCheckpointToEmit = CheckpointNumber(nextCheckpointToEmit + 1); } @@ -196,9 +199,11 @@ export class L2BlockStream { // If we have reached the end of the checkpoint, signal as such const lastBlockInCheckpoint = checkpoints[0].checkpoint.blocks.at(-1)!; if (nextBlockNumber > lastBlockInCheckpoint.number) { + const lastBlockHash = await lastBlockInCheckpoint.hash(); await this.emitEvent({ type: 'chain-checkpointed', checkpoint: checkpoints[0], + block: makeL2BlockId(lastBlockInCheckpoint.number, lastBlockHash.toString()), }); } } diff --git a/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts b/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts index d0d0d560c99a..e5579a12bf16 100644 --- a/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts +++ b/yarn-project/stdlib/src/block/test/l2_tips_store_test_suite.ts @@ -87,6 +87,16 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { return new PublishedCheckpoint(checkpoint, L1PublishedData.random(), []); }; + /** Creates a chain-checkpointed event with the required block field */ + const makeCheckpointedEvent = async (checkpoint: PublishedCheckpoint) => { + const lastBlock = checkpoint.checkpoint.blocks.at(-1)!; + const blockId: L2BlockId = { + number: lastBlock.number, + hash: (await lastBlock.hash()).toString(), + }; + return { type: 'chain-checkpointed' as const, checkpoint, block: blockId }; + }; + it('returns zero if no tips are stored', async () => { const tips = await tipsStore.getL2Tips(); expect(tips).toEqual(makeTips(0, 0, 0)); @@ -113,7 +123,7 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { // Checkpoint all proposed blocks (1-5) const checkpoint1 = await makeCheckpoint(1, blocks); - await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint1 }); + await tipsStore.handleBlockStreamEvent(await makeCheckpointedEvent(checkpoint1)); const tips = await tipsStore.getL2Tips(); // Proposed and checkpointed should be the same @@ -127,7 +137,7 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { const blocks = await Promise.all(times(5, i => makeBlock(i + 1))); await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks }); const checkpoint1 = await makeCheckpoint(1, blocks); - await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint1 }); + await tipsStore.handleBlockStreamEvent(await makeCheckpointedEvent(checkpoint1)); // Prove up to block 5 await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: makeBlockId(5) }); @@ -147,7 +157,7 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { const blocks = await Promise.all(times(5, i => makeBlock(i + 1))); await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks }); const checkpoint1 = await makeCheckpoint(1, blocks); - await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint1 }); + await tipsStore.handleBlockStreamEvent(await makeCheckpointedEvent(checkpoint1)); // Prove and finalize await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: makeBlockId(5) }); @@ -171,7 +181,7 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { // Checkpoint 1: all proposed blocks 1-5 const checkpoint1 = await makeCheckpoint(1, blocks1); - await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint1 }); + await tipsStore.handleBlockStreamEvent(await makeCheckpointedEvent(checkpoint1)); // Propose more blocks 6-10 const blocks2 = await Promise.all(times(5, i => makeBlock(i + 6))); @@ -179,7 +189,7 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { // Checkpoint 2: all remaining proposed blocks 6-10 const checkpoint2 = await makeCheckpoint(2, blocks2); - await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint2 }); + await tipsStore.handleBlockStreamEvent(await makeCheckpointedEvent(checkpoint2)); const tips = await tipsStore.getL2Tips(); expect(tips.proposed).toEqual(makeTip(10)); @@ -195,7 +205,7 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { // Checkpoint all proposed blocks (1-3) const checkpoint1 = await makeCheckpoint(1, blocks1to3); - await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint1 }); + await tipsStore.handleBlockStreamEvent(await makeCheckpointedEvent(checkpoint1)); // Propose more blocks 4-5 const blocks4to5 = await Promise.all(times(2, i => makeBlock(i + 4))); @@ -203,7 +213,7 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { // Checkpoint all remaining proposed blocks (4-5) const checkpoint2 = await makeCheckpoint(2, blocks4to5); - await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint2 }); + await tipsStore.handleBlockStreamEvent(await makeCheckpointedEvent(checkpoint2)); // Prove and finalize up to block 3 (checkpoint 1) await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: makeBlockId(3) }); @@ -278,7 +288,7 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { // Checkpoint all the new proposed blocks (1-3) const checkpoint1 = await makeCheckpoint(1, newBlocks); - await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint1 }); + await tipsStore.handleBlockStreamEvent(await makeCheckpointedEvent(checkpoint1)); tips = await tipsStore.getL2Tips(); expect(tips.proposed).toEqual(makeTip(3)); @@ -298,7 +308,7 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { // Checkpoint all proposed blocks (1-5) - these are now committed const checkpoint1 = await makeCheckpoint(1, firstBlocks); - await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint1 }); + await tipsStore.handleBlockStreamEvent(await makeCheckpointedEvent(checkpoint1)); // Propose more blocks 6-10 (not yet checkpointed, can be pruned) const originalBlocks6to10 = await Promise.all(times(5, i => makeBlock(i + 6))); @@ -344,7 +354,7 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { // Checkpoint all the new proposed blocks (6-8) const checkpoint2 = await makeCheckpoint(2, newBlocks); - await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint2 }); + await tipsStore.handleBlockStreamEvent(await makeCheckpointedEvent(checkpoint2)); tips = await tipsStore.getL2Tips(); expect(tips.proposed).toEqual(makeTip(8)); @@ -369,7 +379,7 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { // Checkpoint all proposed blocks (1-3) - these are now committed const checkpoint1 = await makeCheckpoint(1, firstBlocks); - await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint1 }); + await tipsStore.handleBlockStreamEvent(await makeCheckpointedEvent(checkpoint1)); // Propose more blocks 4-10 (not yet checkpointed, can be pruned) const originalBlocks4to10 = await Promise.all(times(7, i => makeBlock(i + 4))); @@ -403,7 +413,7 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { // Checkpoint all the new proposed blocks (4-5) const checkpoint2 = await makeCheckpoint(2, newBlocks); - await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint2 }); + await tipsStore.handleBlockStreamEvent(await makeCheckpointedEvent(checkpoint2)); tips = await tipsStore.getL2Tips(); expect(tips.proposed).toEqual(makeTip(5)); @@ -418,7 +428,7 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { // Checkpoint all proposed blocks (1-3) const checkpoint1 = await makeCheckpoint(1, firstBlocks); - await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint1 }); + await tipsStore.handleBlockStreamEvent(await makeCheckpointedEvent(checkpoint1)); // Prove up to block 3 await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: makeBlockId(3) }); @@ -434,7 +444,7 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { // Checkpoint blocks 4-6 (now checkpointed is ahead of proven) const checkpoint2 = await makeCheckpoint(2, blocks4to6); - await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint2 }); + await tipsStore.handleBlockStreamEvent(await makeCheckpointedEvent(checkpoint2)); tips = await tipsStore.getL2Tips(); expect(tips.proposed).toEqual(makeTip(6)); @@ -491,7 +501,7 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { // Checkpoint all the new proposed blocks (4-7) const checkpoint3 = await makeCheckpoint(3, newBlocks); - await tipsStore.handleBlockStreamEvent({ type: 'chain-checkpointed', checkpoint: checkpoint3 }); + await tipsStore.handleBlockStreamEvent(await makeCheckpointedEvent(checkpoint3)); tips = await tipsStore.getL2Tips(); expect(tips.proposed).toEqual(makeTip(7)); From 76484bf09e12621ed51a0bff602301a82cb61a76 Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Thu, 8 Jan 2026 11:53:34 +0000 Subject: [PATCH 34/36] More comments and tests --- .../archiver/src/archiver/archiver.test.ts | 365 ++++++++++++++++++ .../aztec-node/src/sentinel/sentinel.test.ts | 1 - .../aztec-node/src/sentinel/sentinel.ts | 2 +- .../kv-store/src/stores/l2_tips_store.ts | 38 +- yarn-project/p2p/src/client/p2p_client.ts | 2 +- .../l2_block_stream/l2_tips_memory_store.ts | 128 +++--- .../server_world_state_synchronizer.ts | 4 + 7 files changed, 463 insertions(+), 77 deletions(-) diff --git a/yarn-project/archiver/src/archiver/archiver.test.ts b/yarn-project/archiver/src/archiver/archiver.test.ts index ba1589997c69..bd789f11e8af 100644 --- a/yarn-project/archiver/src/archiver/archiver.test.ts +++ b/yarn-project/archiver/src/archiver/archiver.test.ts @@ -1998,6 +1998,371 @@ describe('Archiver', () => { // TODO(palla/reorg): Add a unit test for the archiver handleEpochPrune xit('handles an upcoming L2 prune', () => {}); + describe('getCheckpointedBlocks', () => { + it('returns checkpointed blocks with checkpoint info', async () => { + const rollupTxs = checkpoints.map(c => makeRollupTx(c)); + const blobHashes = checkpoints.map(makeVersionedBlobHashes); + + mockL1BlockNumbers(100n); + + mockRollup.read.status.mockResolvedValue([ + 0n, + GENESIS_ROOT, + 3n, + checkpoints[2].archive.root.toString(), + GENESIS_ROOT, + ]); + + checkpoints.forEach((c, i) => + makeCheckpointProposedEvent(70n + BigInt(i) * 10n, c.number, c.archive.root.toString(), blobHashes[i]), + ); + messagesPerCheckpoint.forEach((messages, i) => + makeMessageSentEvents(60n + BigInt(i) * 5n, checkpoints[i].number, messages), + ); + mockInbox.read.getState.mockResolvedValue( + makeInboxStateFromMsgCount(messagesPerCheckpoint.reduce((acc, curr) => acc + curr.length, 0)), + ); + + rollupTxs.forEach(tx => publicClient.getTransaction.mockResolvedValueOnce(tx)); + const blobsFromCheckpoints = checkpoints.map(c => makeBlobsFromCheckpoint(c)); + blobsFromCheckpoints.forEach(blobs => blobClient.getBlobSidecar.mockResolvedValueOnce(blobs)); + + await archiver.start(false); + await waitUntilArchiverCheckpoint(CheckpointNumber(3)); + + // Get checkpointed blocks starting from block 1 + const checkpointedBlocks = await archiver.getCheckpointedBlocks(BlockNumber(1), 100); + + // Should return all blocks from all checkpoints + const expectedBlocks = checkpoints.flatMap(c => c.blocks); + expect(checkpointedBlocks.length).toBe(expectedBlocks.length); + + // Verify blocks are returned in correct order and have correct checkpoint info + let blockIndex = 0; + for (let cpIdx = 0; cpIdx < checkpoints.length; cpIdx++) { + const checkpoint = checkpoints[cpIdx]; + for (let i = 0; i < checkpoint.blocks.length; i++) { + const cb = checkpointedBlocks[blockIndex]; + const expectedBlock = checkpoint.blocks[i]; + + // Verify block number matches + expect(cb.block.number).toBe(expectedBlock.number); + + // Verify checkpoint number is correct + expect(cb.checkpointNumber).toBe(checkpoint.number); + + // Verify archive root matches (more reliable than hash which depends on L1-to-L2 messages) + expect(cb.block.archive.root.toString()).toBe(expectedBlock.archive.root.toString()); + + // Verify L1 published data is present + expect(cb.l1).toBeDefined(); + expect(cb.l1.blockNumber).toBeGreaterThan(0n); + + blockIndex++; + } + } + }, 10_000); + + it('respects the limit parameter', async () => { + const rollupTxs = checkpoints.map(c => makeRollupTx(c)); + const blobHashes = checkpoints.map(makeVersionedBlobHashes); + + mockL1BlockNumbers(100n); + + mockRollup.read.status.mockResolvedValue([ + 0n, + GENESIS_ROOT, + 3n, + checkpoints[2].archive.root.toString(), + GENESIS_ROOT, + ]); + + checkpoints.forEach((c, i) => + makeCheckpointProposedEvent(70n + BigInt(i) * 10n, c.number, c.archive.root.toString(), blobHashes[i]), + ); + messagesPerCheckpoint.forEach((messages, i) => + makeMessageSentEvents(60n + BigInt(i) * 5n, checkpoints[i].number, messages), + ); + mockInbox.read.getState.mockResolvedValue( + makeInboxStateFromMsgCount(messagesPerCheckpoint.reduce((acc, curr) => acc + curr.length, 0)), + ); + + rollupTxs.forEach(tx => publicClient.getTransaction.mockResolvedValueOnce(tx)); + const blobsFromCheckpoints = checkpoints.map(c => makeBlobsFromCheckpoint(c)); + blobsFromCheckpoints.forEach(blobs => blobClient.getBlobSidecar.mockResolvedValueOnce(blobs)); + + await archiver.start(false); + await waitUntilArchiverCheckpoint(CheckpointNumber(3)); + + // Get only 2 checkpointed blocks starting from block 1 (out of 3 total) + const checkpointedBlocks = await archiver.getCheckpointedBlocks(BlockNumber(1), 2); + expect(checkpointedBlocks.length).toBe(2); + + // Verify exact block numbers (blocks 1 and 2) + expect(checkpointedBlocks[0].block.number).toBe(BlockNumber(1)); + expect(checkpointedBlocks[1].block.number).toBe(BlockNumber(2)); + + // Verify archive roots match original checkpoint blocks + expect(checkpointedBlocks[0].block.archive.root.toString()).toBe( + checkpoints[0].blocks[0].archive.root.toString(), + ); + expect(checkpointedBlocks[1].block.archive.root.toString()).toBe( + checkpoints[1].blocks[0].archive.root.toString(), + ); + + // Verify checkpoint numbers (block 1 is from checkpoint 1, block 2 is from checkpoint 2) + expect(checkpointedBlocks[0].checkpointNumber).toBe(1); + expect(checkpointedBlocks[1].checkpointNumber).toBe(2); + }, 10_000); + + it('returns blocks starting from specified block number', async () => { + const rollupTxs = checkpoints.map(c => makeRollupTx(c)); + const blobHashes = checkpoints.map(makeVersionedBlobHashes); + + mockL1BlockNumbers(100n); + + mockRollup.read.status.mockResolvedValue([ + 0n, + GENESIS_ROOT, + 3n, + checkpoints[2].archive.root.toString(), + GENESIS_ROOT, + ]); + + checkpoints.forEach((c, i) => + makeCheckpointProposedEvent(70n + BigInt(i) * 10n, c.number, c.archive.root.toString(), blobHashes[i]), + ); + messagesPerCheckpoint.forEach((messages, i) => + makeMessageSentEvents(60n + BigInt(i) * 5n, checkpoints[i].number, messages), + ); + mockInbox.read.getState.mockResolvedValue( + makeInboxStateFromMsgCount(messagesPerCheckpoint.reduce((acc, curr) => acc + curr.length, 0)), + ); + + rollupTxs.forEach(tx => publicClient.getTransaction.mockResolvedValueOnce(tx)); + const blobsFromCheckpoints = checkpoints.map(c => makeBlobsFromCheckpoint(c)); + blobsFromCheckpoints.forEach(blobs => blobClient.getBlobSidecar.mockResolvedValueOnce(blobs)); + + await archiver.start(false); + await waitUntilArchiverCheckpoint(CheckpointNumber(3)); + + // Get blocks starting from block 2 (skip block 1, get blocks 2 and 3) + const checkpointedBlocks = await archiver.getCheckpointedBlocks(BlockNumber(2), 10); + + // Should return 2 blocks (blocks 2 and 3 - since there are only 3 blocks total, 1 per checkpoint) + expect(checkpointedBlocks.length).toBe(2); + + // Verify block numbers are sequential starting from 2 + expect(checkpointedBlocks[0].block.number).toBe(BlockNumber(2)); + expect(checkpointedBlocks[1].block.number).toBe(BlockNumber(3)); + + // Verify checkpoint numbers (block 2 is from checkpoint 2, block 3 is from checkpoint 3) + expect(checkpointedBlocks[0].checkpointNumber).toBe(2); + expect(checkpointedBlocks[1].checkpointNumber).toBe(3); + + // Verify archive roots match expected blocks from checkpoints + expect(checkpointedBlocks[0].block.archive.root.toString()).toBe( + checkpoints[1].blocks[0].archive.root.toString(), + ); + expect(checkpointedBlocks[1].block.archive.root.toString()).toBe( + checkpoints[2].blocks[0].archive.root.toString(), + ); + }, 10_000); + + it('returns empty array when no checkpointed blocks exist', async () => { + mockL1BlockNumbers(100n); + mockRollup.read.status.mockResolvedValue([0n, GENESIS_ROOT, 0n, GENESIS_ROOT, GENESIS_ROOT]); + mockInbox.read.getState.mockResolvedValue(makeInboxStateFromMsgCount(0)); + + await archiver.start(false); + + const checkpointedBlocks = await archiver.getCheckpointedBlocks(BlockNumber(1), 10); + expect(checkpointedBlocks).toEqual([]); + }, 10_000); + + it('filters by proven status when proven=true', async () => { + const rollupTxs = checkpoints.map(c => makeRollupTx(c)); + const blobHashes = checkpoints.map(makeVersionedBlobHashes); + + mockL1BlockNumbers(100n); + + // Set checkpoint 1 as proven (provenCheckpointNumber = 1) + mockRollup.read.status.mockResolvedValue([ + 1n, // provenCheckpointNumber + checkpoints[0].archive.root.toString(), // provenArchive + 3n, // pendingCheckpointNumber + checkpoints[2].archive.root.toString(), // pendingArchive + checkpoints[0].archive.root.toString(), // archiveForLocalPendingCheckpointNumber + ]); + + checkpoints.forEach((c, i) => + makeCheckpointProposedEvent(70n + BigInt(i) * 10n, c.number, c.archive.root.toString(), blobHashes[i]), + ); + messagesPerCheckpoint.forEach((messages, i) => + makeMessageSentEvents(60n + BigInt(i) * 5n, checkpoints[i].number, messages), + ); + mockInbox.read.getState.mockResolvedValue( + makeInboxStateFromMsgCount(messagesPerCheckpoint.reduce((acc, curr) => acc + curr.length, 0)), + ); + + rollupTxs.forEach(tx => publicClient.getTransaction.mockResolvedValueOnce(tx)); + const blobsFromCheckpoints = checkpoints.map(c => makeBlobsFromCheckpoint(c)); + blobsFromCheckpoints.forEach(blobs => blobClient.getBlobSidecar.mockResolvedValueOnce(blobs)); + + await archiver.start(false); + await waitUntilArchiverCheckpoint(CheckpointNumber(3)); + + // Get all checkpointed blocks without proven filter + const allBlocks = await archiver.getCheckpointedBlocks(BlockNumber(1), 100); + const totalBlocks = checkpoints.reduce((acc, c) => acc + c.blocks.length, 0); + expect(allBlocks.length).toBe(totalBlocks); + + // Get only proven checkpointed blocks (should only include blocks from checkpoint 1) + const provenBlocks = await archiver.getCheckpointedBlocks(BlockNumber(1), 100, true); + const checkpoint1Blocks = checkpoints[0].blocks; + expect(provenBlocks.length).toBe(checkpoint1Blocks.length); + + // Verify all proven blocks are from checkpoint 1 and match expected blocks + for (let i = 0; i < provenBlocks.length; i++) { + const cb = provenBlocks[i]; + expect(cb.checkpointNumber).toBe(1); + expect(cb.block.number).toBe(checkpoint1Blocks[i].number); + + // Verify archive root matches (more reliable than hash which depends on L1-to-L2 messages) + expect(cb.block.archive.root.toString()).toBe(checkpoint1Blocks[i].archive.root.toString()); + } + + // Verify the last proven block number matches the last block of checkpoint 1 + const lastProvenBlock = provenBlocks[provenBlocks.length - 1]; + const lastCheckpoint1Block = checkpoint1Blocks[checkpoint1Blocks.length - 1]; + expect(lastProvenBlock.block.number).toBe(lastCheckpoint1Block.number); + }, 10_000); + }); + + describe('getL2BlocksNew with proven filter', () => { + it('filters by proven status when proven=true', async () => { + const rollupTxs = checkpoints.map(c => makeRollupTx(c)); + const blobHashes = checkpoints.map(makeVersionedBlobHashes); + + mockL1BlockNumbers(100n); + + // Set checkpoint 1 as proven (provenCheckpointNumber = 1) + mockRollup.read.status.mockResolvedValue([ + 1n, // provenCheckpointNumber + checkpoints[0].archive.root.toString(), // provenArchive + 3n, // pendingCheckpointNumber + checkpoints[2].archive.root.toString(), // pendingArchive + checkpoints[0].archive.root.toString(), // archiveForLocalPendingCheckpointNumber + ]); + + checkpoints.forEach((c, i) => + makeCheckpointProposedEvent(70n + BigInt(i) * 10n, c.number, c.archive.root.toString(), blobHashes[i]), + ); + messagesPerCheckpoint.forEach((messages, i) => + makeMessageSentEvents(60n + BigInt(i) * 5n, checkpoints[i].number, messages), + ); + mockInbox.read.getState.mockResolvedValue( + makeInboxStateFromMsgCount(messagesPerCheckpoint.reduce((acc, curr) => acc + curr.length, 0)), + ); + + rollupTxs.forEach(tx => publicClient.getTransaction.mockResolvedValueOnce(tx)); + const blobsFromCheckpoints = checkpoints.map(c => makeBlobsFromCheckpoint(c)); + blobsFromCheckpoints.forEach(blobs => blobClient.getBlobSidecar.mockResolvedValueOnce(blobs)); + + await archiver.start(false); + await waitUntilArchiverCheckpoint(CheckpointNumber(3)); + + // Get all blocks without proven filter + const allBlocks = await archiver.getL2BlocksNew(BlockNumber(1), 100); + const totalBlocks = checkpoints.reduce((acc, c) => acc + c.blocks.length, 0); + expect(allBlocks.length).toBe(totalBlocks); + + // Get only proven blocks (should only include blocks from checkpoint 1) + const provenBlocks = await archiver.getL2BlocksNew(BlockNumber(1), 100, true); + const checkpoint1Blocks = checkpoints[0].blocks; + expect(provenBlocks.length).toBe(checkpoint1Blocks.length); + + // Verify block numbers match checkpoint 1 blocks + for (let i = 0; i < provenBlocks.length; i++) { + expect(provenBlocks[i].number).toBe(checkpoint1Blocks[i].number); + + // Verify archive root matches (more reliable than hash which depends on L1-to-L2 messages) + expect(provenBlocks[i].archive.root.toString()).toBe(checkpoint1Blocks[i].archive.root.toString()); + } + + // Verify the last proven block is the last block of checkpoint 1 + const lastProvenBlockNumber = checkpoint1Blocks[checkpoint1Blocks.length - 1].number; + expect(provenBlocks[provenBlocks.length - 1].number).toBe(lastProvenBlockNumber); + + // Verify no unproven blocks are included + const unprovenBlockNumbers = checkpoints.slice(1).flatMap(c => c.blocks.map(b => b.number)); + provenBlocks.forEach(b => { + expect(unprovenBlockNumbers).not.toContain(b.number); + }); + }, 10_000); + + it('returns all blocks when proven=false or undefined', async () => { + const rollupTxs = checkpoints.map(c => makeRollupTx(c)); + const blobHashes = checkpoints.map(makeVersionedBlobHashes); + + mockL1BlockNumbers(100n); + + // Set checkpoint 1 as proven + mockRollup.read.status.mockResolvedValue([ + 1n, + checkpoints[0].archive.root.toString(), + 3n, + checkpoints[2].archive.root.toString(), + checkpoints[0].archive.root.toString(), + ]); + + checkpoints.forEach((c, i) => + makeCheckpointProposedEvent(70n + BigInt(i) * 10n, c.number, c.archive.root.toString(), blobHashes[i]), + ); + messagesPerCheckpoint.forEach((messages, i) => + makeMessageSentEvents(60n + BigInt(i) * 5n, checkpoints[i].number, messages), + ); + mockInbox.read.getState.mockResolvedValue( + makeInboxStateFromMsgCount(messagesPerCheckpoint.reduce((acc, curr) => acc + curr.length, 0)), + ); + + rollupTxs.forEach(tx => publicClient.getTransaction.mockResolvedValueOnce(tx)); + const blobsFromCheckpoints = checkpoints.map(c => makeBlobsFromCheckpoint(c)); + blobsFromCheckpoints.forEach(blobs => blobClient.getBlobSidecar.mockResolvedValueOnce(blobs)); + + await archiver.start(false); + await waitUntilArchiverCheckpoint(CheckpointNumber(3)); + + const expectedBlocks = checkpoints.flatMap(c => c.blocks); + const totalBlocks = expectedBlocks.length; + + // Get blocks with proven=false - should include all blocks + const blocksProvenFalse = await archiver.getL2BlocksNew(BlockNumber(1), 100, false); + expect(blocksProvenFalse.length).toBe(totalBlocks); + + // Verify all block numbers are present + for (let i = 0; i < blocksProvenFalse.length; i++) { + expect(blocksProvenFalse[i].number).toBe(expectedBlocks[i].number); + } + + // Get blocks with proven=undefined - should include all blocks + const blocksProvenUndefined = await archiver.getL2BlocksNew(BlockNumber(1), 100); + expect(blocksProvenUndefined.length).toBe(totalBlocks); + + // Verify all block numbers match + for (let i = 0; i < blocksProvenUndefined.length; i++) { + expect(blocksProvenUndefined[i].number).toBe(expectedBlocks[i].number); + } + + // Verify blocks include unproven blocks (from checkpoints 2 and 3) + const unprovenBlockNumbers = checkpoints.slice(1).flatMap(c => c.blocks.map(b => b.number)); + const returnedBlockNumbers = blocksProvenFalse.map(b => b.number); + unprovenBlockNumbers.forEach(unprovenNum => { + expect(returnedBlockNumbers).toContain(unprovenNum); + }); + }, 10_000); + }); + const waitUntilArchiverCheckpoint = async (checkpointNumber: CheckpointNumber) => { logger.info(`Waiting for archiver to sync to checkpoint ${checkpointNumber}`); await retryUntil(() => archiver.getSynchedCheckpointNumber().then(n => n === checkpointNumber), 'sync', 10, 0.1); diff --git a/yarn-project/aztec-node/src/sentinel/sentinel.test.ts b/yarn-project/aztec-node/src/sentinel/sentinel.test.ts index 55c3369f411b..4a5c9801cc1a 100644 --- a/yarn-project/aztec-node/src/sentinel/sentinel.test.ts +++ b/yarn-project/aztec-node/src/sentinel/sentinel.test.ts @@ -560,7 +560,6 @@ describe('sentinel', () => { const validator1 = EthAddress.random(); const validator2 = EthAddress.random(); const validator3 = EthAddress.random(); - // Use getSlotRangeForEpoch to calculate expected slot range (same as Sentinel does) const [fromSlot, toSlot] = getSlotRangeForEpoch(epochNumber, l1Constants); epochCache.getEpochAndSlotNow.mockReturnValue({ diff --git a/yarn-project/aztec-node/src/sentinel/sentinel.ts b/yarn-project/aztec-node/src/sentinel/sentinel.ts index ce2abd212be5..a89c447a13ca 100644 --- a/yarn-project/aztec-node/src/sentinel/sentinel.ts +++ b/yarn-project/aztec-node/src/sentinel/sentinel.ts @@ -127,7 +127,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme if (event.type !== 'chain-proven') { return; } - const blockNumber = BlockNumber(event.block.number); + const blockNumber = event.block.number; const block = await this.archiver.getL2BlockNew(blockNumber); if (!block) { this.logger.error(`Failed to get block ${blockNumber}`, { block }); diff --git a/yarn-project/kv-store/src/stores/l2_tips_store.ts b/yarn-project/kv-store/src/stores/l2_tips_store.ts index 5089777b0622..824aa3bc83bd 100644 --- a/yarn-project/kv-store/src/stores/l2_tips_store.ts +++ b/yarn-project/kv-store/src/stores/l2_tips_store.ts @@ -14,7 +14,7 @@ import { PublishedCheckpoint } from '@aztec/stdlib/checkpoint'; import type { AztecAsyncMap } from '../interfaces/map.js'; import type { AztecAsyncKVStore } from '../interfaces/store.js'; -/** Stores currently synced L2 tips and unfinalized block hashes. +/** Maintains and returns the current set of L2 Tips. Maintains stores of block hashes and checkpoints in order to do so. */ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider { private readonly l2TipsStore: AztecAsyncMap; @@ -65,25 +65,21 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise { switch (event.type) { - case 'blocks-added': { + case 'blocks-added': await this.handleBlocksAdded(event); break; - } - case 'chain-checkpointed': { + case 'chain-checkpointed': await this.handleChainCheckpointed(event); break; - } - case 'chain-pruned': { + case 'chain-pruned': await this.handleChainPruned(event); break; - } case 'chain-proven': await this.handleChainProven(event); break; - case 'chain-finalized': { + case 'chain-finalized': await this.handleChainFinalized(event); break; - } } } @@ -124,6 +120,7 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo if (event.type !== 'blocks-added') { return; } + // Simply add the new block hashes by the block number and update the proposed tip await this.store.transactionAsync(async () => { const blocks = event.blocks; for (const block of blocks) { @@ -137,14 +134,9 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo if (event.type !== 'chain-checkpointed') { return; } + // Update the checkpointed chain tip and save the checkpoint await this.store.transactionAsync(async () => { - const checkpointBlocks = event.checkpoint.checkpoint.blocks; - const lastBlock = checkpointBlocks.at(-1)!; - const blockId: L2BlockId = { - number: lastBlock.number, - hash: (await lastBlock.hash()).toString(), - }; - await this.saveTag('checkpointed', blockId); + await this.saveTag('checkpointed', event.block); await this.saveCheckpoint(event.checkpoint); }); } @@ -153,12 +145,10 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo if (event.type !== 'chain-pruned') { return; } + // Update the proposed and checkpointed tips await this.store.transactionAsync(async () => { await this.saveTag('proposed', event.block); - const currentCheckpointed = (await this.l2TipsStore.getAsync('checkpointed')) ?? INITIAL_L2_BLOCK_NUM - 1; - if (event.block.number < currentCheckpointed) { - await this.saveTag('checkpointed', event.block); - } + await this.saveTag('checkpointed', event.block); }); } @@ -166,6 +156,7 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo if (event.type !== 'chain-proven') { return; } + // Updtae the proven chain tip await this.store.transactionAsync(async () => { await this.saveTag('proven', event.block); }); @@ -176,14 +167,15 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo return; } await this.store.transactionAsync(async () => { + // Update the finalized tip await this.saveTag('finalized', event.block); - // Get the checkpoint number for the finalized block before cleanup + // Get the checkpoint number for the finalized block const finalizedCheckpointNumber = await this.l2BlockNumberToCheckpointNumberStore.getAsync(event.block.number); - // Clean up block hashes for blocks before finalized + // Clean up block hashes for blocks earlier than the finalized tip for await (const key of this.l2BlockHashesStore.keysAsync({ end: event.block.number })) { await this.l2BlockHashesStore.delete(key); } - // Clean up block-to-checkpoint mappings for blocks before finalized + // Clean up block-to-checkpoint mappings for blocks earlier than the finalized tip for await (const key of this.l2BlockNumberToCheckpointNumberStore.keysAsync({ end: event.block.number })) { await this.l2BlockNumberToCheckpointNumberStore.delete(key); } diff --git a/yarn-project/p2p/src/client/p2p_client.ts b/yarn-project/p2p/src/client/p2p_client.ts index 386966b363af..b07e6743ef47 100644 --- a/yarn-project/p2p/src/client/p2p_client.ts +++ b/yarn-project/p2p/src/client/p2p_client.ts @@ -204,7 +204,7 @@ export class P2PClient } } - // Pass the event through the our l2 tips store + // Pass the event through to our l2 tips store await this.l2Tips.handleBlockStreamEvent(event); await this.startServiceIfSynched(); } diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_memory_store.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_memory_store.ts index a830362f7137..a39fc0cb45d1 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_memory_store.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_tips_memory_store.ts @@ -7,7 +7,7 @@ import type { CheckpointId, L2BlockId, L2BlockTag, L2Tips } from '../l2_block_so import type { L2BlockStreamEvent, L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider } from './interfaces.js'; /** - * Stores currently synced L2 tips and unfinalized block hashes. + * Maintains and returns the current set of L2 Tips. Maintains stores of block hashes and checkpoints in order to do so. * @dev tests in kv-store/src/stores/l2_tips_memory_store.test.ts */ export class L2TipsMemoryStore implements L2BlockStreamEventHandler, L2BlockStreamLocalDataProvider { @@ -72,62 +72,88 @@ export class L2TipsMemoryStore implements L2BlockStreamEventHandler, L2BlockStre public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise { switch (event.type) { - case 'blocks-added': { - const blocks = event.blocks; - for (const block of blocks) { - this.l2BlockHashesStore.set(block.number, await this.computeBlockHash(block)); - } - this.l2TipsStore.set('proposed', blocks.at(-1)!.number); + case 'blocks-added': + await this.handleBlocksAdded(event); break; - } - case 'chain-checkpointed': { - const lastBlock = event.checkpoint.checkpoint.blocks.at(-1)!; - const blockId: L2BlockId = { - number: lastBlock.number, - hash: await this.computeBlockHash(lastBlock), - }; - this.saveTag('checkpointed', blockId); - // Only store the mapping for the last block since tips only point to checkpoint boundaries - this.l2BlocktoCheckpointStore.set(lastBlock.number, event.checkpoint.checkpoint.number); - this.checkpointStore.set(event.checkpoint.checkpoint.number, event.checkpoint); + case 'chain-checkpointed': + this.handleChainCheckpointed(event); break; - } - case 'chain-pruned': { - this.saveTag('proposed', event.block); - const currentCheckpointed = this.l2TipsStore.get('checkpointed') ?? 0; - if (event.block.number < currentCheckpointed) { - this.saveTag('checkpointed', event.block); - } + case 'chain-pruned': + this.handleChainPruned(event); break; - } case 'chain-proven': - this.saveTag('proven', event.block); + this.handleChainProven(event); break; - case 'chain-finalized': { - this.saveTag('finalized', event.block); - // Get the checkpoint number for the finalized block before cleanup - const finalizedCheckpointNumber = this.l2BlocktoCheckpointStore.get(event.block.number); - // Clean up block hashes for blocks before finalized - for (const key of this.l2BlockHashesStore.keys()) { - if (key < event.block.number) { - this.l2BlockHashesStore.delete(key); - } - } - // Clean up block-to-checkpoint mappings for blocks before finalized - for (const key of this.l2BlocktoCheckpointStore.keys()) { - if (key < event.block.number) { - this.l2BlocktoCheckpointStore.delete(key); - } - } - // Clean up checkpoints older than the finalized checkpoint - if (finalizedCheckpointNumber !== undefined) { - for (const key of this.checkpointStore.keys()) { - if (key < finalizedCheckpointNumber) { - this.checkpointStore.delete(key); - } - } - } + case 'chain-finalized': + this.handleChainFinalized(event); break; + } + } + + private async handleBlocksAdded(event: L2BlockStreamEvent) { + if (event.type !== 'blocks-added') { + return; + } + // Simply add the new block hashes by the block number and update the proposed tip + const blocks = event.blocks; + for (const block of blocks) { + this.l2BlockHashesStore.set(block.number, await this.computeBlockHash(block)); + } + this.l2TipsStore.set('proposed', blocks.at(-1)!.number); + } + + private handleChainCheckpointed(event: L2BlockStreamEvent) { + if (event.type !== 'chain-checkpointed') { + return; + } + this.saveTag('checkpointed', event.block); + // Only store the mapping for the last block since tips only point to checkpoint boundaries + this.l2BlocktoCheckpointStore.set(event.block.number, event.checkpoint.checkpoint.number); + this.checkpointStore.set(event.checkpoint.checkpoint.number, event.checkpoint); + } + + private handleChainPruned(event: L2BlockStreamEvent) { + if (event.type !== 'chain-pruned') { + return; + } + // update the proposed and checkpointed chain tips + this.saveTag('proposed', event.block); + this.saveTag('checkpointed', event.block); + } + + private handleChainProven(event: L2BlockStreamEvent) { + if (event.type !== 'chain-proven') { + return; + } + // Updtae the proven chain tip + this.saveTag('proven', event.block); + } + + private handleChainFinalized(event: L2BlockStreamEvent) { + if (event.type !== 'chain-finalized') { + return; + } + this.saveTag('finalized', event.block); + // Get the checkpoint number for the finalized block before cleanup + const finalizedCheckpointNumber = this.l2BlocktoCheckpointStore.get(event.block.number); + // Clean up block hashes for blocks before finalized + for (const key of this.l2BlockHashesStore.keys()) { + if (key < event.block.number) { + this.l2BlockHashesStore.delete(key); + } + } + // Clean up block-to-checkpoint mappings for blocks before finalized + for (const key of this.l2BlocktoCheckpointStore.keys()) { + if (key < event.block.number) { + this.l2BlocktoCheckpointStore.delete(key); + } + } + // Clean up checkpoints older than the finalized checkpoint + if (finalizedCheckpointNumber !== undefined) { + for (const key of this.checkpointStore.keys()) { + if (key < finalizedCheckpointNumber) { + this.checkpointStore.delete(key); + } } } } diff --git a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts index 2732308141dc..57de28f530b8 100644 --- a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts +++ b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts @@ -257,6 +257,10 @@ export class ServerWorldStateSynchronizer const unfinalizedBlockHash = await this.getL2BlockHash(status.unfinalizedBlockNumber); const latestBlockId: L2BlockId = { number: status.unfinalizedBlockNumber, hash: unfinalizedBlockHash! }; + // This is all a bit ugly. World state knows nothing of checkpoints and I can't think of any reason + // why anyone depending on world state would need to know if the world state was 'at a checkpoint' or anything similar. + // so we just default a load of stuff here to initial values and empty hashes. + // We could implement a store to track the values but it would be redundant. return { proposed: latestBlockId, checkpointed: { From 35377dfcaccf77aeb999b7f9480f0ef2243c406e Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Thu, 8 Jan 2026 14:15:08 +0000 Subject: [PATCH 35/36] More tests --- .../l2_block_stream/l2_block_stream.test.ts | 422 ++++++++++++++++-- 1 file changed, 379 insertions(+), 43 deletions(-) diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts index f37d3d089aab..2e7be5f6c59c 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts @@ -78,11 +78,11 @@ describe('L2BlockStream', () => { checkpoint: { number: CheckpointNumber(number), hash: makeHash(number) }, }); - /** Sets the remote tips. checkpointed_ defaults to 0 (no checkpointed blocks). */ - const setRemoteTips = (latest_: number, proven?: number, finalized?: number, checkpointed_?: number) => { + /** Sets the remote tips. All tips default to 0 except latest. */ + const setRemoteTips = (latest_: number, checkpointed_?: number, proven?: number, finalized?: number) => { + checkpointed_ = checkpointed_ ?? 0; proven = proven ?? 0; finalized = finalized ?? 0; - checkpointed_ = checkpointed_ ?? 0; latest = latest_; checkpointed = checkpointed_; @@ -220,7 +220,7 @@ describe('L2BlockStream', () => { }); it('emits events for chain proven and finalized', async () => { - setRemoteTips(45, 40, 35); + setRemoteTips(45, 0, 40, 35); localData.proposed.number = BlockNumber(40); localData.proven.block.number = BlockNumber(10); localData.finalized.block.number = BlockNumber(10); @@ -235,7 +235,7 @@ describe('L2BlockStream', () => { it('fetches checkpointed blocks and emits chain-checkpointed events', async () => { // All blocks are checkpointed (checkpointed=5, proposed=5) - setRemoteTips(5, 0, 0, 5); + setRemoteTips(5, 5); await blockStream.work(); @@ -259,7 +259,7 @@ describe('L2BlockStream', () => { it('fetches checkpointed blocks first, then uncheckpointed blocks', async () => { // Blocks 1-3 are checkpointed, blocks 4-5 are uncheckpointed - setRemoteTips(5, 0, 0, 3); + setRemoteTips(5, 3); await blockStream.work(); @@ -279,7 +279,7 @@ describe('L2BlockStream', () => { it('handles reorg with uncheckpointed reason when pruned to checkpointed tip', async () => { // Source: checkpointed=3, proposed=5 - setRemoteTips(5, 0, 0, 3); + setRemoteTips(5, 3); localData.proposed.number = BlockNumber(5); localData.checkpointed.block.number = BlockNumber(3); @@ -312,26 +312,29 @@ describe('L2BlockStream', () => { // Regression test for https://github.com/AztecProtocol/aztec-packages/issues/13471 it('handles a prune to a block before start block', async () => { - setRemoteTips(35, 25, 10); + setRemoteTips(35, 30, 25, 10); blockStream = new TestL2BlockStream(blockSource, localData, handler, undefined, { batchSize: 10, startingBlock: 30, }); // We first seed a few blocks into the blockstream + // Block 30 comes via checkpoint, blocks 31-35 via uncheckpointed await blockStream.work(); expect(handler.events).toEqual([ - { type: 'blocks-added', blocks: times(6, i => makeBlock(i + 30)) }, + expectBlocksAdded([30]), + expectCheckpointed(30), + { type: 'blocks-added', blocks: times(5, i => makeBlock(i + 31)) }, { type: 'chain-proven', block: makeBlockId(25) }, { type: 'chain-finalized', block: makeBlockId(10) }, ]); handler.clearEvents(); // And then we reorg - setRemoteTips(25, 25, 10); + setRemoteTips(25, 25, 25, 10); await blockStream.work(); expect(handler.events).toEqual([ - { type: 'chain-pruned', block: makeBlockId(25), reason: 'unproven', checkpoint: makeCheckpointId(0) }, + { type: 'chain-pruned', block: makeBlockId(25), reason: 'unproven', checkpoint: makeCheckpointId(25) }, ]); }); }); @@ -386,13 +389,13 @@ describe('L2BlockStream', () => { /** Sets the remote tips with correct checkpoint numbers for multi-block checkpoints. */ const setRemoteTipsMultiBlock = ( latest_: number, + checkpointedBlock?: number, proven?: number, finalized?: number, - checkpointedBlock?: number, ) => { + checkpointedBlock = checkpointedBlock ?? 0; proven = proven ?? 0; finalized = finalized ?? 0; - checkpointedBlock = checkpointedBlock ?? 0; latest = latest_; checkpointed = checkpointedBlock; @@ -453,7 +456,7 @@ describe('L2BlockStream', () => { it('emits all blocks in a checkpoint before chain-checkpointed event', async () => { // Set up: 6 blocks in 2 checkpoints (blocks 1-3 in checkpoint 1, blocks 4-6 in checkpoint 2) - setRemoteTipsMultiBlock(6, 0, 0, 6); + setRemoteTipsMultiBlock(6, 6); await blockStream.work(); @@ -469,7 +472,7 @@ describe('L2BlockStream', () => { it('handles partial checkpoint at the end (uncheckpointed blocks)', async () => { // Set up: 5 blocks total, but only first 3 are checkpointed (checkpoint 1 complete) // Blocks 4-5 are uncheckpointed - setRemoteTipsMultiBlock(5, 0, 0, 3); + setRemoteTipsMultiBlock(5, 3); await blockStream.work(); @@ -480,16 +483,16 @@ describe('L2BlockStream', () => { it('handles starting from middle of a checkpoint', async () => { // Set up: 9 blocks in 3 checkpoints, but we start from block 5 (middle of checkpoint 2) // Local has blocks 1-4, local checkpointed = 0 - setRemoteTipsMultiBlock(9, 0, 0, 9); + setRemoteTipsMultiBlock(9, 9); localData.proposed.number = BlockNumber(4); await blockStream.work(); - // Should first emit checkpoint 1 (blocks 1-3 already local) + // Should first emit checkpoint 1 (blocks 1-4 already local) // Then continue from block 5, which is in checkpoint 2 // Blocks 5-6 complete checkpoint 2, then blocks 7-9 complete checkpoint 3 expect(handler.events).toEqual([ - expectCheckpointed(1), // checkpoint 1 for already-local blocks 1-3 + expectCheckpointed(1), // checkpoint 1 for already-local blocks 1-4 expectBlocksAdded([5, 6]), expectCheckpointed(2), // checkpoint 2 expectBlocksAdded([7, 8, 9]), @@ -506,7 +509,7 @@ describe('L2BlockStream', () => { it('correctly identifies checkpoint number in chain-checkpointed events', async () => { // Set up: 6 blocks in 2 checkpoints - setRemoteTipsMultiBlock(6, 0, 0, 6); + setRemoteTipsMultiBlock(6, 6); await blockStream.work(); @@ -518,28 +521,35 @@ describe('L2BlockStream', () => { }); it('handles many checkpoints with batching', async () => { - // Set up: 12 blocks in 4 checkpoints, with batch size of 5 + // Set up: 12 blocks in 4 checkpoints (3 blocks each), with batch size of 5 + // Batch size doesn't align with checkpoint boundaries, so the stream must + // respect checkpoint boundaries and emit events correctly blockStream = new TestL2BlockStream(blockSource, localData, handler, undefined, { batchSize: 5 }); - setRemoteTipsMultiBlock(12, 0, 0, 12); + setRemoteTipsMultiBlock(12, 12); await blockStream.work(); - // Should have 4 checkpoint events (one per checkpoint) - const checkpointEvents = handler.events.filter(e => e.type === 'chain-checkpointed'); - expect(checkpointEvents).toHaveLength(4); - - // Should have multiple blocks-added events due to batching and checkpoint boundaries - const blocksAddedEvents = handler.events.filter(e => e.type === 'blocks-added'); - expect(blocksAddedEvents.length).toBeGreaterThanOrEqual(4); - - // Verify all blocks were added in order - const allBlocks = blocksAddedEvents.flatMap(e => (e as any).blocks); - expect(allBlocks.map((b: L2BlockNew) => b.number)).toEqual(times(12, i => BlockNumber(i + 1))); + // Even though batch size is 5, checkpoint boundaries (every 3 blocks) take precedence + // Expected sequence: + // - Blocks 1-3 (checkpoint 1), then checkpoint 1 event + // - Blocks 4-6 (checkpoint 2), then checkpoint 2 event + // - Blocks 7-9 (checkpoint 3), then checkpoint 3 event + // - Blocks 10-12 (checkpoint 4), then checkpoint 4 event + expect(handler.events).toEqual([ + expectBlocksAdded([1, 2, 3]), + expectCheckpointed(1), + expectBlocksAdded([4, 5, 6]), + expectCheckpointed(2), + expectBlocksAdded([7, 8, 9]), + expectCheckpointed(3), + expectBlocksAdded([10, 11, 12]), + expectCheckpointed(4), + ]); }); it('emits checkpoint event when blocks become checkpointed after being added as uncheckpointed', async () => { // Phase 1: Start with 3 checkpointed blocks (checkpoint 1), then add blocks 4-6 as uncheckpointed - setRemoteTipsMultiBlock(6, 0, 0, 3); + setRemoteTipsMultiBlock(6, 3); await blockStream.work(); @@ -558,7 +568,7 @@ describe('L2BlockStream', () => { localData.checkpointed.checkpoint.number = CheckpointNumber(1); // Phase 2: Now checkpoint 2 completes (blocks 4-6 become checkpointed) - setRemoteTipsMultiBlock(6, 0, 0, 6); + setRemoteTipsMultiBlock(6, 6); await blockStream.work(); @@ -570,7 +580,7 @@ describe('L2BlockStream', () => { it('emits checkpoint event BEFORE new uncheckpointed blocks when checkpoint completes', async () => { // Phase 1: Start with 3 checkpointed blocks (checkpoint 1), then add blocks 4-6 as uncheckpointed - setRemoteTipsMultiBlock(6, 0, 0, 3); + setRemoteTipsMultiBlock(6, 3); await blockStream.work(); @@ -589,7 +599,7 @@ describe('L2BlockStream', () => { localData.checkpointed.checkpoint.number = CheckpointNumber(1); // Phase 2: Checkpoint 2 completes (blocks 4-6) AND a new block 7 arrives - setRemoteTipsMultiBlock(7, 0, 0, 6); + setRemoteTipsMultiBlock(7, 6); await blockStream.work(); @@ -604,7 +614,7 @@ describe('L2BlockStream', () => { // Sync 1: Source has checkpointed=6 (checkpoint 2), proposed=9 // Client gets blocks 1-6 via checkpoints, blocks 7-9 as uncheckpointed - setRemoteTipsMultiBlock(9, 0, 0, 6); + setRemoteTipsMultiBlock(9, 6); await blockStream.work(); @@ -624,7 +634,7 @@ describe('L2BlockStream', () => { localData.checkpointed.checkpoint.number = CheckpointNumber(2); // Sync 2: Checkpoint 3 is now published (blocks 7-9), new blocks 10-12 are uncheckpointed - setRemoteTipsMultiBlock(12, 0, 0, 9); + setRemoteTipsMultiBlock(12, 9); await blockStream.work(); @@ -642,7 +652,7 @@ describe('L2BlockStream', () => { localData.checkpointed.checkpoint.number = CheckpointNumber(3); // Sync 3: Checkpoint 4 is now published (blocks 10-12), no new blocks - setRemoteTipsMultiBlock(12, 0, 0, 12); + setRemoteTipsMultiBlock(12, 12); await blockStream.work(); @@ -652,7 +662,7 @@ describe('L2BlockStream', () => { it('emits all checkpoints when source jumps ahead with multiple new checkpoints', async () => { // Phase 1: Start with checkpoint 1 complete (blocks 1-3), blocks 4-6 uncheckpointed - setRemoteTipsMultiBlock(6, 0, 0, 3); + setRemoteTipsMultiBlock(6, 3); await blockStream.work(); @@ -673,7 +683,7 @@ describe('L2BlockStream', () => { // - Checkpoint 2 (blocks 4-6) - blocks already local, needs checkpoint event // - Checkpoint 3 (blocks 7-9) - new blocks + checkpoint event // - Checkpoint 4 (blocks 10-12) - new blocks + checkpoint event - setRemoteTipsMultiBlock(12, 0, 0, 12); + setRemoteTipsMultiBlock(12, 12); await blockStream.work(); @@ -689,6 +699,332 @@ describe('L2BlockStream', () => { expectCheckpointed(4), ]); }); + + describe('prune scenarios', () => { + it('prunes proposed chain back to checkpointed tip, then continues', async () => { + // Phase 1: Sync blocks 1-9 with checkpoints 1-2, blocks 7-9 uncheckpointed + setRemoteTipsMultiBlock(9, 6); + + await blockStream.work(); + + expect(handler.events).toEqual([ + expectBlocksAdded([1, 2, 3]), + expectCheckpointed(1), + expectBlocksAdded([4, 5, 6]), + expectCheckpointed(2), + expectBlocksAdded([7, 8, 9]), // uncheckpointed + ]); + + handler.clearEvents(); + + // Update local state to reflect what the handler stored + localData.proposed.number = BlockNumber(9); + localData.checkpointed.block.number = BlockNumber(6); + localData.checkpointed.checkpoint.number = CheckpointNumber(2); + + // Phase 2: Prune - proposed chain pruned back to checkpointed tip (block 6) + // This happens when uncheckpointed blocks (7-9) are invalid + // Mess up hashes for blocks 7-9 to simulate reorg + localData.blockHashes[7] = '0xbad7'; + localData.blockHashes[8] = '0xbad8'; + localData.blockHashes[9] = '0xbad9'; + + // Source now has proposed=6, checkpointed=6 + setRemoteTipsMultiBlock(6, 6); + + await blockStream.work(); + + // Should emit chain-pruned back to block 6, reason 'uncheckpointed' + expect(handler.events).toEqual([ + { + type: 'chain-pruned', + block: makeBlockId(6), + reason: 'uncheckpointed', + checkpoint: expect.objectContaining({ number: CheckpointNumber(2) }), + }, + ]); + + handler.clearEvents(); + + // Update local state after prune + localData.proposed.number = BlockNumber(6); + delete localData.blockHashes[7]; + delete localData.blockHashes[8]; + delete localData.blockHashes[9]; + + // Phase 3: Chain continues - new blocks 7-12 arrive with checkpoints 3-4 + setRemoteTipsMultiBlock(12, 12); + + await blockStream.work(); + + // Should continue normally: blocks 7-9 + checkpoint 3, blocks 10-12 + checkpoint 4 + expect(handler.events).toEqual([ + expectBlocksAdded([7, 8, 9]), + expectCheckpointed(3), + expectBlocksAdded([10, 11, 12]), + expectCheckpointed(4), + ]); + }); + + it('prunes proposed and checkpointed chains back to proven tip, then continues', async () => { + // Phase 1: Sync blocks 1-12 with checkpoints 1-3, proven at checkpoint 2 (block 6) + setRemoteTipsMultiBlock(12, 9, 6); + + await blockStream.work(); + + expect(handler.events).toEqual([ + expectBlocksAdded([1, 2, 3]), + expectCheckpointed(1), + expectBlocksAdded([4, 5, 6]), + expectCheckpointed(2), + expectBlocksAdded([7, 8, 9]), + expectCheckpointed(3), + expectBlocksAdded([10, 11, 12]), + { type: 'chain-proven', block: makeBlockId(6) }, + ]); + + handler.clearEvents(); + + // Update local state + localData.proposed.number = BlockNumber(12); + localData.checkpointed.block.number = BlockNumber(9); + localData.checkpointed.checkpoint.number = CheckpointNumber(3); + localData.proven.block.number = BlockNumber(6); + localData.proven.checkpoint.number = CheckpointNumber(2); + + // Phase 2: Prune - checkpoint 3 failed to prove, prune back to proven tip (block 6) + // Mess up hashes for blocks 7-12 to simulate reorg + for (let i = 7; i <= 12; i++) { + localData.blockHashes[i] = `0xbad${i}`; + } + + // Source now has proposed=6, checkpointed=6, proven=6 + setRemoteTipsMultiBlock(6, 6, 6); + + await blockStream.work(); + + // Should emit chain-pruned back to block 6 + // Reason is 'unproven' because we're pruning beyond the local checkpointed tip (12) + expect(handler.events).toEqual([ + { + type: 'chain-pruned', + block: makeBlockId(6), + reason: 'unproven', + checkpoint: expect.objectContaining({ number: CheckpointNumber(2) }), + }, + ]); + + handler.clearEvents(); + + // Update local state after prune + localData.proposed.number = BlockNumber(6); + localData.checkpointed.block.number = BlockNumber(6); + localData.checkpointed.checkpoint.number = CheckpointNumber(2); + for (let i = 7; i <= 12; i++) { + delete localData.blockHashes[i]; + } + + // Phase 3: Chain continues with new blocks and checkpoints + // New blocks 7-15 arrive with checkpoints 3-5, proven advances to checkpoint 3 + setRemoteTipsMultiBlock(15, 15, 9); + + await blockStream.work(); + + // Should continue normally with new blocks and checkpoints + expect(handler.events).toEqual([ + expectBlocksAdded([7, 8, 9]), + expectCheckpointed(3), + expectBlocksAdded([10, 11, 12]), + expectCheckpointed(4), + expectBlocksAdded([13, 14, 15]), + expectCheckpointed(5), + { type: 'chain-proven', block: makeBlockId(9) }, + ]); + }); + + it('prunes uncheckpointed blocks and immediately receives new ones', async () => { + // Phase 1: Sync blocks 1-6 with checkpoint 1, blocks 4-6 uncheckpointed + setRemoteTipsMultiBlock(6, 3); + + await blockStream.work(); + + expect(handler.events).toEqual([ + expectBlocksAdded([1, 2, 3]), + expectCheckpointed(1), + expectBlocksAdded([4, 5, 6]), + ]); + + handler.clearEvents(); + + // Update local state + localData.proposed.number = BlockNumber(6); + localData.checkpointed.block.number = BlockNumber(3); + localData.checkpointed.checkpoint.number = CheckpointNumber(1); + + // Phase 2: Prune blocks 4-6 due to bad hashes + localData.blockHashes[4] = '0xbad4'; + localData.blockHashes[5] = '0xbad5'; + localData.blockHashes[6] = '0xbad6'; + + // Source still at checkpointed=3 (no new checkpoints yet) + setRemoteTipsMultiBlock(3, 3); + + await blockStream.work(); + + // Should emit prune back to block 3 + expect(handler.events).toEqual([ + { + type: 'chain-pruned', + block: makeBlockId(3), + reason: 'uncheckpointed', + checkpoint: expect.objectContaining({ number: CheckpointNumber(1) }), + }, + ]); + + handler.clearEvents(); + + // Update local state after prune + localData.proposed.number = BlockNumber(3); + delete localData.blockHashes[4]; + delete localData.blockHashes[5]; + delete localData.blockHashes[6]; + + // Phase 3: New blocks 4-9 arrive with checkpoints 2-3 + setRemoteTipsMultiBlock(9, 9); + + await blockStream.work(); + + // Should continue normally with new blocks and checkpoints + expect(handler.events).toEqual([ + expectBlocksAdded([4, 5, 6]), + expectCheckpointed(2), + expectBlocksAdded([7, 8, 9]), + expectCheckpointed(3), + ]); + }); + + it('prunes proposed chain back to genesis when no checkpoints exist', async () => { + // Phase 1: Sync blocks 1-6, no checkpoints (checkpointed=0, proven=0, finalized=0) + setRemoteTipsMultiBlock(6, 0); + + await blockStream.work(); + + // All blocks come as uncheckpointed + expect(handler.events).toEqual([expectBlocksAdded([1, 2, 3, 4, 5, 6])]); + + handler.clearEvents(); + + // Update local state + localData.proposed.number = BlockNumber(6); + + // Phase 2: All blocks are invalid, prune back to genesis (block 0) + for (let i = 1; i <= 6; i++) { + localData.blockHashes[i] = `0xbad${i}`; + } + + // Source now has proposed=0, checkpointed=0 + setRemoteTipsMultiBlock(0, 0); + + await blockStream.work(); + + // Should emit chain-pruned back to block 0, reason 'uncheckpointed' (pruned to checkpointed tip which is 0) + expect(handler.events).toEqual([ + { + type: 'chain-pruned', + block: makeBlockId(0), + reason: 'uncheckpointed', + checkpoint: expect.objectContaining({ number: CheckpointNumber(0) }), + }, + ]); + + handler.clearEvents(); + + // Update local state after prune + localData.proposed.number = BlockNumber(0); + for (let i = 1; i <= 6; i++) { + delete localData.blockHashes[i]; + } + + // Phase 3: New blocks 1-6 arrive with checkpoints 1-2 + setRemoteTipsMultiBlock(6, 6); + + await blockStream.work(); + + // Should continue normally from genesis + expect(handler.events).toEqual([ + expectBlocksAdded([1, 2, 3]), + expectCheckpointed(1), + expectBlocksAdded([4, 5, 6]), + expectCheckpointed(2), + ]); + }); + + it('prunes both proposed and checkpointed chains back to genesis', async () => { + // Phase 1: Sync blocks 1-6 with checkpoint 1 (blocks 1-3), blocks 4-6 uncheckpointed + setRemoteTipsMultiBlock(6, 3); + + await blockStream.work(); + + expect(handler.events).toEqual([ + expectBlocksAdded([1, 2, 3]), + expectCheckpointed(1), + expectBlocksAdded([4, 5, 6]), + ]); + + handler.clearEvents(); + + // Update local state + localData.proposed.number = BlockNumber(6); + localData.checkpointed.block.number = BlockNumber(3); + localData.checkpointed.checkpoint.number = CheckpointNumber(1); + + // Phase 2: All blocks are invalid (even checkpointed ones), prune back to genesis + for (let i = 1; i <= 6; i++) { + localData.blockHashes[i] = `0xbad${i}`; + } + + // Source now has proposed=0, checkpointed=0 (full chain reset) + setRemoteTipsMultiBlock(0, 0); + + await blockStream.work(); + + // Should emit chain-pruned back to block 0 + // Reason is 'unproven' because we're pruning beyond the local checkpointed tip (3) + expect(handler.events).toEqual([ + { + type: 'chain-pruned', + block: makeBlockId(0), + reason: 'unproven', + checkpoint: expect.objectContaining({ number: CheckpointNumber(0) }), + }, + ]); + + handler.clearEvents(); + + // Update local state after prune + localData.proposed.number = BlockNumber(0); + localData.checkpointed.block.number = BlockNumber(0); + localData.checkpointed.checkpoint.number = CheckpointNumber(0); + for (let i = 1; i <= 6; i++) { + delete localData.blockHashes[i]; + } + + // Phase 3: New chain starts fresh with blocks 1-9 and checkpoints 1-3 + setRemoteTipsMultiBlock(9, 9); + + await blockStream.work(); + + // Should continue normally from genesis + expect(handler.events).toEqual([ + expectBlocksAdded([1, 2, 3]), + expectCheckpointed(1), + expectBlocksAdded([4, 5, 6]), + expectCheckpointed(2), + expectBlocksAdded([7, 8, 9]), + expectCheckpointed(3), + ]); + }); + }); }); describe('skipFinalized', () => { @@ -706,7 +1042,7 @@ describe('L2BlockStream', () => { }); it('skips ahead to the latest finalized block', async () => { - setRemoteTips(40, 38, 35); + setRemoteTips(40, 0, 38, 35); localData.proposed.number = BlockNumber(5); localData.proven.block.number = BlockNumber(2); @@ -723,7 +1059,7 @@ describe('L2BlockStream', () => { }); it('does not skip if already ahead of finalized', async () => { - setRemoteTips(40, 38, 35); + setRemoteTips(40, 0, 38, 35); localData.proposed.number = BlockNumber(38); localData.proven.block.number = BlockNumber(38); From 83cbdd8940c6bd8b949af9592f862b874e0af55e Mon Sep 17 00:00:00 2001 From: Phil Windle Date: Thu, 8 Jan 2026 15:57:16 +0000 Subject: [PATCH 36/36] Lint fix --- yarn-project/kv-store/src/stores/l2_tips_store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/kv-store/src/stores/l2_tips_store.ts b/yarn-project/kv-store/src/stores/l2_tips_store.ts index 824aa3bc83bd..d27cade683e5 100644 --- a/yarn-project/kv-store/src/stores/l2_tips_store.ts +++ b/yarn-project/kv-store/src/stores/l2_tips_store.ts @@ -1,4 +1,4 @@ -import { GENESIS_BLOCK_HEADER_HASH, INITIAL_L2_BLOCK_NUM } from '@aztec/constants'; +import { GENESIS_BLOCK_HEADER_HASH } from '@aztec/constants'; import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; import type { CheckpointId,