diff --git a/yarn-project/archiver/src/archiver-store.test.ts b/yarn-project/archiver/src/archiver-store.test.ts index 950b90008ea8..220586472a74 100644 --- a/yarn-project/archiver/src/archiver-store.test.ts +++ b/yarn-project/archiver/src/archiver-store.test.ts @@ -61,7 +61,7 @@ describe('Archiver Store', () => { const tracer = getTelemetryClient().getTracer(''); instrumentation = mock({ isEnabled: () => true, tracer }); - archiverStore = new KVArchiverDataStore(await openTmpStore('archiver_test'), 1000, { epochDuration: 4 }); + archiverStore = new KVArchiverDataStore(await openTmpStore('archiver_test'), 1000); l1Constants = { l1GenesisTime: BigInt(now), @@ -543,5 +543,47 @@ describe('Archiver Store', () => { expect(await archiver.getSynchedCheckpointNumber()).toEqual(CheckpointNumber(2)); expect(await archiver.getProvenCheckpointNumber()).toEqual(CheckpointNumber(1)); }); + + it('rolls back finalized checkpoint number when target is before finalized block', async () => { + const genesisArchive = new AppendOnlyTreeSnapshot(new Fr(GENESIS_ARCHIVE_ROOT), 1); + // Checkpoint 1: blocks 1-2, Checkpoint 2: blocks 3-4, Checkpoint 3: blocks 5-6 + const testCheckpoints = await makeChainedCheckpoints(3, { + previousArchive: genesisArchive, + blocksPerCheckpoint: 2, + }); + await archiverStore.addCheckpoints(testCheckpoints); + + // Mark checkpoints 1 and 2 as proven and finalized + await archiverStore.setProvenCheckpointNumber(CheckpointNumber(2)); + await archiverStore.setFinalizedCheckpointNumber(CheckpointNumber(2)); + expect(await archiver.getFinalizedL2BlockNumber()).toEqual(BlockNumber(4)); + + // Roll back to block 2 (end of checkpoint 1), which is before finalized block 4 + await archiver.rollbackTo(BlockNumber(2)); + + expect(await archiver.getSynchedCheckpointNumber()).toEqual(CheckpointNumber(1)); + expect(await archiver.getFinalizedL2BlockNumber()).toEqual(BlockNumber(2)); + }); + + it('preserves finalized checkpoint number when target is after finalized block', async () => { + const genesisArchive = new AppendOnlyTreeSnapshot(new Fr(GENESIS_ARCHIVE_ROOT), 1); + // Checkpoint 1: blocks 1-2, Checkpoint 2: blocks 3-4, Checkpoint 3: blocks 5-6 + const testCheckpoints = await makeChainedCheckpoints(3, { + previousArchive: genesisArchive, + blocksPerCheckpoint: 2, + }); + await archiverStore.addCheckpoints(testCheckpoints); + + // Mark checkpoint 1 as finalized, checkpoint 2 as proven + await archiverStore.setProvenCheckpointNumber(CheckpointNumber(2)); + await archiverStore.setFinalizedCheckpointNumber(CheckpointNumber(1)); + expect(await archiver.getFinalizedL2BlockNumber()).toEqual(BlockNumber(2)); + + // Roll back to block 4 (end of checkpoint 2), which is after finalized block 2 + await archiver.rollbackTo(BlockNumber(4)); + + expect(await archiver.getSynchedCheckpointNumber()).toEqual(CheckpointNumber(2)); + expect(await archiver.getFinalizedL2BlockNumber()).toEqual(BlockNumber(2)); + }); }); }); diff --git a/yarn-project/archiver/src/archiver-sync.test.ts b/yarn-project/archiver/src/archiver-sync.test.ts index 1779d1362a3e..b18efc1062cd 100644 --- a/yarn-project/archiver/src/archiver-sync.test.ts +++ b/yarn-project/archiver/src/archiver-sync.test.ts @@ -94,7 +94,7 @@ describe('Archiver Sync', () => { instrumentation = mock({ isEnabled: () => true, tracer }); // Create archiver store - archiverStore = new KVArchiverDataStore(await openTmpStore('archiver_sync_test'), 1000, { epochDuration: 32 }); + archiverStore = new KVArchiverDataStore(await openTmpStore('archiver_sync_test'), 1000); const contractAddresses = { registryAddress, @@ -1224,6 +1224,71 @@ describe('Archiver Sync', () => { }, 15_000); }); + describe('finalized checkpoint', () => { + it('reports no finalized blocks before any checkpoint is proven', async () => { + fake.setL1BlockNumber(100n); + fake.setFinalizedL1BlockNumber(100n); + await archiver.syncImmediate(); + + const tips = await archiver.getL2Tips(); + expect(tips.finalized.checkpoint.number).toEqual(CheckpointNumber(0)); + expect(tips.finalized.block.number).toEqual(BlockNumber(0)); + }); + + it('updates finalized checkpoint when the L1 finalized block is at or past the proven checkpoint L1 block', async () => { + const { checkpoint: cp1 } = await fake.addCheckpoint(CheckpointNumber(1), { + l1BlockNumber: 70n, + messagesL1BlockNumber: 50n, + numL1ToL2Messages: 3, + }); + + // Sync all checkpoints + fake.setL1BlockNumber(100n); + await archiver.syncImmediate(); + + // Mark checkpoint 1 as proven and advance L1 so proven is registered + fake.markCheckpointAsProven(CheckpointNumber(1)); + fake.setL1BlockNumber(101n); + await archiver.syncImmediate(); + expect(await archiver.getProvenCheckpointNumber()).toEqual(CheckpointNumber(1)); + + // Finalized L1 block is at or past where checkpoint 1 was published (70) + fake.setFinalizedL1BlockNumber(70n); + fake.setL1BlockNumber(102n); + await archiver.syncImmediate(); + + const tips = await archiver.getL2Tips(); + const lastBlockInCp1 = cp1.blocks.at(-1)!.number; + expect(tips.finalized.checkpoint.number).toEqual(CheckpointNumber(1)); + expect(tips.finalized.block.number).toEqual(lastBlockInCp1); + }); + + it('does not advance finalized checkpoint when finalized L1 block is before the proven checkpoint', async () => { + await fake.addCheckpoint(CheckpointNumber(1), { + l1BlockNumber: 70n, + messagesL1BlockNumber: 50n, + numL1ToL2Messages: 3, + }); + + fake.setL1BlockNumber(100n); + await archiver.syncImmediate(); + + fake.markCheckpointAsProven(CheckpointNumber(1)); + fake.setL1BlockNumber(101n); + await archiver.syncImmediate(); + expect(await archiver.getProvenCheckpointNumber()).toEqual(CheckpointNumber(1)); + + // Finalized L1 block is before where checkpoint 1 was published (70) + fake.setFinalizedL1BlockNumber(50n); + fake.setL1BlockNumber(102n); + await archiver.syncImmediate(); + + const tips = await archiver.getL2Tips(); + expect(tips.finalized.checkpoint.number).toEqual(CheckpointNumber(0)); + expect(tips.finalized.block.number).toEqual(BlockNumber(0)); + }); + }); + describe('checkpointing local proposed blocks', () => { let pruneSpy: jest.Mock; diff --git a/yarn-project/archiver/src/archiver.ts b/yarn-project/archiver/src/archiver.ts index 28c0cfa720ab..6073b4926ec9 100644 --- a/yarn-project/archiver/src/archiver.ts +++ b/yarn-project/archiver/src/archiver.ts @@ -456,11 +456,10 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra this.log.info(`Rolling back proven L2 checkpoint to ${targetCheckpointNumber}`); await this.updater.setProvenCheckpointNumber(targetCheckpointNumber); } - // TODO(palla/reorg): Set the finalized block when we add support for it. - // const currentFinalizedBlock = currentBlocks.finalized.block.number; - // if (targetL2BlockNumber < currentFinalizedBlock) { - // this.log.info(`Rolling back finalized L2 checkpoint to ${targetCheckpointNumber}`); - // await this.updater.setFinalizedCheckpointNumber(targetCheckpointNumber); - // } + const currentFinalizedBlock = currentBlocks.finalized.block.number; + if (targetL2BlockNumber < currentFinalizedBlock) { + this.log.info(`Rolling back finalized L2 checkpoint to ${targetCheckpointNumber}`); + await this.updater.setFinalizedCheckpointNumber(targetCheckpointNumber); + } } } diff --git a/yarn-project/archiver/src/factory.ts b/yarn-project/archiver/src/factory.ts index c0273939a28b..bd6b0485eb64 100644 --- a/yarn-project/archiver/src/factory.ts +++ b/yarn-project/archiver/src/factory.ts @@ -13,7 +13,6 @@ import { BundledProtocolContractsProvider } from '@aztec/protocol-contracts/prov import { FunctionType, decodeFunctionSignature } from '@aztec/stdlib/abi'; import type { ArchiverEmitter } from '@aztec/stdlib/block'; import { type ContractClassPublic, computePublicBytecodeCommitment } from '@aztec/stdlib/contract'; -import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers'; import type { DataStoreConfig } from '@aztec/stdlib/kv-store'; import { getTelemetryClient } from '@aztec/telemetry-client'; @@ -32,14 +31,13 @@ export const ARCHIVER_STORE_NAME = 'archiver'; /** Creates an archiver store. */ export async function createArchiverStore( userConfig: Pick & DataStoreConfig, - l1Constants: Pick, ) { const config = { ...userConfig, dataStoreMapSizeKb: userConfig.archiverStoreMapSizeKb ?? userConfig.dataStoreMapSizeKb, }; const store = await createStore(ARCHIVER_STORE_NAME, ARCHIVER_DB_VERSION, config); - return new KVArchiverDataStore(store, config.maxLogs, l1Constants); + return new KVArchiverDataStore(store, config.maxLogs); } /** @@ -54,7 +52,7 @@ export async function createArchiver( deps: ArchiverDeps, opts: { blockUntilSync: boolean } = { blockUntilSync: true }, ): Promise { - const archiverStore = await createArchiverStore(config, { epochDuration: config.aztecEpochDuration }); + const archiverStore = await createArchiverStore(config); await registerProtocolContracts(archiverStore); // Create Ethereum clients diff --git a/yarn-project/archiver/src/modules/data_store_updater.test.ts b/yarn-project/archiver/src/modules/data_store_updater.test.ts index 94721e4c22ea..a003ec745374 100644 --- a/yarn-project/archiver/src/modules/data_store_updater.test.ts +++ b/yarn-project/archiver/src/modules/data_store_updater.test.ts @@ -42,7 +42,7 @@ describe('ArchiverDataStoreUpdater', () => { let instanceAddress: AztecAddress; beforeEach(async () => { - store = new KVArchiverDataStore(await openTmpStore('data_store_updater_test'), 1000, { epochDuration: 32 }); + store = new KVArchiverDataStore(await openTmpStore('data_store_updater_test'), 1000); updater = new ArchiverDataStoreUpdater(store); // Create contract class log from sample fixture data diff --git a/yarn-project/archiver/src/modules/data_store_updater.ts b/yarn-project/archiver/src/modules/data_store_updater.ts index 83864240f01d..50d3787d185e 100644 --- a/yarn-project/archiver/src/modules/data_store_updater.ts +++ b/yarn-project/archiver/src/modules/data_store_updater.ts @@ -281,6 +281,17 @@ export class ArchiverDataStoreUpdater { }); } + /** + * Updates the finalized checkpoint number and refreshes the L2 tips cache. + * @param checkpointNumber - The checkpoint number to set as finalized. + */ + public async setFinalizedCheckpointNumber(checkpointNumber: CheckpointNumber): Promise { + await this.store.transactionAsync(async () => { + await this.store.setFinalizedCheckpointNumber(checkpointNumber); + await this.l2TipsCache?.refresh(); + }); + } + /** Extracts and stores contract data from a single block. */ private addContractDataToDb(block: L2Block): Promise { return this.updateContractDataOnDb(block, Operation.Store); diff --git a/yarn-project/archiver/src/modules/l1_synchronizer.ts b/yarn-project/archiver/src/modules/l1_synchronizer.ts index 221d50336fb7..8c4348a4e666 100644 --- a/yarn-project/archiver/src/modules/l1_synchronizer.ts +++ b/yarn-project/archiver/src/modules/l1_synchronizer.ts @@ -217,6 +217,9 @@ export class ArchiverL1Synchronizer implements Traceable { this.instrumentation.updateL1BlockHeight(currentL1BlockNumber); } + // Update the finalized L2 checkpoint based on L1 finality. + await this.updateFinalizedCheckpoint(); + // After syncing has completed, update the current l1 block number and timestamp, // otherwise we risk announcing to the world that we've synced to a given point, // but the corresponding blocks have not been processed (see #12631). @@ -232,6 +235,27 @@ export class ArchiverL1Synchronizer implements Traceable { }); } + /** Query L1 for its finalized block and update the finalized checkpoint accordingly. */ + private async updateFinalizedCheckpoint(): Promise { + try { + const finalizedL1Block = await this.publicClient.getBlock({ blockTag: 'finalized', includeTransactions: false }); + const finalizedL1BlockNumber = finalizedL1Block.number; + const finalizedCheckpointNumber = await this.rollup.getProvenCheckpointNumber({ + blockNumber: finalizedL1BlockNumber, + }); + const localFinalizedCheckpointNumber = await this.store.getFinalizedCheckpointNumber(); + if (localFinalizedCheckpointNumber !== finalizedCheckpointNumber) { + await this.updater.setFinalizedCheckpointNumber(finalizedCheckpointNumber); + this.log.info(`Updated finalized chain to checkpoint ${finalizedCheckpointNumber}`, { + finalizedCheckpointNumber, + finalizedL1BlockNumber, + }); + } + } catch (err) { + this.log.warn(`Failed to update finalized checkpoint: ${err}`); + } + } + /** Prune all proposed local blocks that should have been checkpointed by now. */ private async pruneUncheckpointedBlocks(currentL1Timestamp: bigint) { const [lastCheckpointedBlockNumber, lastProposedBlockNumber] = await Promise.all([ diff --git a/yarn-project/archiver/src/store/block_store.ts b/yarn-project/archiver/src/store/block_store.ts index a9ec9a501c85..c3ea543c7931 100644 --- a/yarn-project/archiver/src/store/block_store.ts +++ b/yarn-project/archiver/src/store/block_store.ts @@ -20,7 +20,6 @@ import { serializeValidateCheckpointResult, } from '@aztec/stdlib/block'; import { type CheckpointData, L1PublishedData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint'; -import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers'; import { CheckpointHeader } from '@aztec/stdlib/rollup'; import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees'; import { @@ -97,6 +96,9 @@ export class BlockStore { /** Stores last proven checkpoint */ #lastProvenCheckpoint: AztecAsyncSingleton; + /** Stores last finalized checkpoint (proven at or before the finalized L1 block) */ + #lastFinalizedCheckpoint: AztecAsyncSingleton; + /** Stores the pending chain validation status */ #pendingChainValidationStatus: AztecAsyncSingleton; @@ -111,10 +113,7 @@ export class BlockStore { #log = createLogger('archiver:block_store'); - constructor( - private db: AztecAsyncKVStore, - private l1Constants: Pick, - ) { + constructor(private db: AztecAsyncKVStore) { this.#blocks = db.openMap('archiver_blocks'); this.#blockTxs = db.openMap('archiver_block_txs'); this.#txEffects = db.openMap('archiver_tx_effects'); @@ -123,21 +122,27 @@ export class BlockStore { this.#blockArchiveIndex = db.openMap('archiver_block_archive_index'); this.#lastSynchedL1Block = db.openSingleton('archiver_last_synched_l1_block'); this.#lastProvenCheckpoint = db.openSingleton('archiver_last_proven_l2_checkpoint'); + this.#lastFinalizedCheckpoint = db.openSingleton('archiver_last_finalized_l2_checkpoint'); this.#pendingChainValidationStatus = db.openSingleton('archiver_pending_chain_validation_status'); this.#checkpoints = db.openMap('archiver_checkpoints'); this.#slotToCheckpoint = db.openMap('archiver_slot_to_checkpoint'); } /** - * Computes the finalized block number based on the proven block number. - * A block is considered finalized when it's 2 epochs behind the proven block. - * TODO(#13569): Compute proper finalized block number based on L1 finalized block. - * TODO(palla/mbps): Even the provisional computation is wrong, since it should subtract checkpoints, not blocks + * Returns the finalized L2 block number. An L2 block is finalized when it was proven + * in an L1 block that has itself been finalized on Ethereum. * @returns The finalized block number. */ async getFinalizedL2BlockNumber(): Promise { - const provenBlockNumber = await this.getProvenBlockNumber(); - return BlockNumber(Math.max(provenBlockNumber - this.l1Constants.epochDuration * 2, 0)); + const finalizedCheckpointNumber = await this.getFinalizedCheckpointNumber(); + if (finalizedCheckpointNumber === INITIAL_CHECKPOINT_NUMBER - 1) { + return BlockNumber(INITIAL_L2_BLOCK_NUM - 1); + } + const checkpointStorage = await this.#checkpoints.getAsync(finalizedCheckpointNumber); + if (!checkpointStorage) { + throw new CheckpointNotFoundError(finalizedCheckpointNumber); + } + return BlockNumber(checkpointStorage.startBlock + checkpointStorage.blockCount - 1); } /** @@ -976,6 +981,20 @@ export class BlockStore { return result; } + async getFinalizedCheckpointNumber(): Promise { + const [latestCheckpointNumber, finalizedCheckpointNumber] = await Promise.all([ + this.getLatestCheckpointNumber(), + this.#lastFinalizedCheckpoint.getAsync(), + ]); + return (finalizedCheckpointNumber ?? 0) > latestCheckpointNumber + ? latestCheckpointNumber + : CheckpointNumber(finalizedCheckpointNumber ?? 0); + } + + setFinalizedCheckpointNumber(checkpointNumber: CheckpointNumber) { + return this.#lastFinalizedCheckpoint.set(checkpointNumber); + } + #computeBlockRange(start: BlockNumber, limit: number): Required, 'start' | 'limit'>> { if (limit < 1) { throw new Error(`Invalid limit: ${limit}`); diff --git a/yarn-project/archiver/src/store/kv_archiver_store.test.ts b/yarn-project/archiver/src/store/kv_archiver_store.test.ts index d05044ded8d2..14a9ed83f8a3 100644 --- a/yarn-project/archiver/src/store/kv_archiver_store.test.ts +++ b/yarn-project/archiver/src/store/kv_archiver_store.test.ts @@ -89,7 +89,7 @@ describe('KVArchiverDataStore', () => { }; beforeEach(async () => { - store = new KVArchiverDataStore(await openTmpStore('archiver_test'), 1000, { epochDuration: 32 }); + store = new KVArchiverDataStore(await openTmpStore('archiver_test'), 1000); // Create checkpoints sequentially to ensure archive roots are chained properly. // Each block's header.lastArchive must equal the previous block's archive. publishedCheckpoints = []; diff --git a/yarn-project/archiver/src/store/kv_archiver_store.ts b/yarn-project/archiver/src/store/kv_archiver_store.ts index d46075e2a588..f70b9a4cbc4f 100644 --- a/yarn-project/archiver/src/store/kv_archiver_store.ts +++ b/yarn-project/archiver/src/store/kv_archiver_store.ts @@ -22,7 +22,6 @@ import type { ExecutablePrivateFunctionWithMembershipProof, UtilityFunctionWithMembershipProof, } from '@aztec/stdlib/contract'; -import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers'; import type { GetContractClassLogsResponse, GetPublicLogsResponse } from '@aztec/stdlib/interfaces/client'; import type { LogFilter, SiloedTag, Tag, TxScopedL2Log } from '@aztec/stdlib/logs'; import type { BlockHeader, TxHash, TxReceipt } from '@aztec/stdlib/tx'; @@ -71,9 +70,8 @@ export class KVArchiverDataStore implements ContractDataSource { constructor( private db: AztecAsyncKVStore, logsMaxPageSize: number = 1000, - l1Constants: Pick, ) { - this.#blockStore = new BlockStore(db, l1Constants); + this.#blockStore = new BlockStore(db); this.#logStore = new LogStore(db, this.#blockStore, logsMaxPageSize); this.#messageStore = new MessageStore(db); this.#contractClassStore = new ContractClassStore(db); @@ -542,6 +540,22 @@ export class KVArchiverDataStore implements ContractDataSource { await this.#blockStore.setProvenCheckpointNumber(checkpointNumber); } + /** + * Gets the number of the latest finalized checkpoint processed. + * @returns The number of the latest finalized checkpoint processed. + */ + getFinalizedCheckpointNumber(): Promise { + return this.#blockStore.getFinalizedCheckpointNumber(); + } + + /** + * Stores the number of the latest finalized checkpoint processed. + * @param checkpointNumber - The number of the latest finalized checkpoint processed. + */ + async setFinalizedCheckpointNumber(checkpointNumber: CheckpointNumber) { + await this.#blockStore.setFinalizedCheckpointNumber(checkpointNumber); + } + async setBlockSynchedL1BlockNumber(l1BlockNumber: bigint) { await this.#blockStore.setSynchedL1BlockNumber(l1BlockNumber); } diff --git a/yarn-project/archiver/src/test/fake_l1_state.ts b/yarn-project/archiver/src/test/fake_l1_state.ts index b05fd2f8e505..71e9f84c65f7 100644 --- a/yarn-project/archiver/src/test/fake_l1_state.ts +++ b/yarn-project/archiver/src/test/fake_l1_state.ts @@ -150,8 +150,12 @@ export class FakeL1State { // Computed from checkpoints based on L1 block visibility private pendingCheckpointNumber: CheckpointNumber = CheckpointNumber(0); + // The L1 block number reported as "finalized" (defaults to the start block) + private finalizedL1BlockNumber: bigint; + constructor(private readonly config: FakeL1StateConfig) { this.l1BlockNumber = config.l1StartBlock; + this.finalizedL1BlockNumber = config.l1StartBlock; this.lastArchive = new AppendOnlyTreeSnapshot(config.genesisArchiveRoot, 1); } @@ -283,11 +287,30 @@ export class FakeL1State { this.updatePendingCheckpointNumber(); } + /** Sets the L1 block number that will be reported as "finalized". */ + setFinalizedL1BlockNumber(blockNumber: bigint): void { + this.finalizedL1BlockNumber = blockNumber; + } + /** Marks a checkpoint as proven. Updates provenCheckpointNumber. */ markCheckpointAsProven(checkpointNumber: CheckpointNumber): void { this.provenCheckpointNumber = checkpointNumber; } + /** + * Simulates what `rollup.getProvenCheckpointNumber({ blockNumber: atL1Block })` would return. + */ + getProvenCheckpointNumberAtL1Block(atL1Block: bigint): CheckpointNumber { + if (this.provenCheckpointNumber === 0) { + return CheckpointNumber(0); + } + const checkpoint = this.checkpoints.find(cp => cp.checkpointNumber === this.provenCheckpointNumber); + if (checkpoint && checkpoint.l1BlockNumber <= atL1Block) { + return this.provenCheckpointNumber; + } + return CheckpointNumber(0); + } + /** Sets the target committee size for attestation validation. */ setTargetCommitteeSize(size: number): void { this.targetCommitteeSize = size; @@ -406,6 +429,11 @@ export class FakeL1State { }); }); + mockRollup.getProvenCheckpointNumber.mockImplementation((options?: { blockNumber?: bigint }) => { + const atBlock = options?.blockNumber ?? this.l1BlockNumber; + return Promise.resolve(this.getProvenCheckpointNumberAtL1Block(atBlock)); + }); + mockRollup.canPruneAtTime.mockImplementation(() => Promise.resolve(this.canPruneResult)); // Mock the wrapper method for fetching checkpoint events @@ -449,10 +477,13 @@ export class FakeL1State { publicClient.getChainId.mockResolvedValue(1); publicClient.getBlockNumber.mockImplementation(() => Promise.resolve(this.l1BlockNumber)); - // Use async function pattern that existing test uses for getBlock - - publicClient.getBlock.mockImplementation((async (args: { blockNumber?: bigint } = {}) => { - const blockNum = args.blockNumber ?? (await publicClient.getBlockNumber()); + publicClient.getBlock.mockImplementation((async (args: { blockNumber?: bigint; blockTag?: string } = {}) => { + let blockNum: bigint; + if (args.blockTag === 'finalized') { + blockNum = this.finalizedL1BlockNumber; + } else { + blockNum = args.blockNumber ?? (await publicClient.getBlockNumber()); + } return { number: blockNum, timestamp: BigInt(blockNum) * BigInt(this.config.ethereumSlotDuration) + this.config.l1GenesisTime, diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_l1_reorgs.parallel.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_l1_reorgs.parallel.test.ts index f327326dbeef..13b9f9b7207b 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_l1_reorgs.parallel.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_l1_reorgs.parallel.test.ts @@ -73,6 +73,8 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { maxTxsPerBlock: 1, enforceTimeTable: true, aztecProofSubmissionEpochs: 1, + // Use 32 slots/epoch (matching real Ethereum mainnet) + anvilSlotsInAnEpoch: 32, }); ({ proverDelayer, sequencerDelayer, context, logger, monitor, L1_BLOCK_TIME_IN_S, L2_SLOT_DURATION_IN_S } = test); node = context.aztecNode; @@ -101,11 +103,31 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { const getProvenCheckpointNumber = (node: AztecNode) => node.getL2Tips().then(tips => tips.proven.checkpoint.number); it('prunes L2 blocks if a proof is removed due to an L1 reorg', async () => { + /** Logs a full state snapshot: L1 latest/finalized and archiver L2 tips. */ + const logState = async (label: string) => { + const [l1Latest, l1Finalized, archiverTips] = await Promise.all([ + test.l1Client.getBlockNumber(), + test.l1Client.getBlock({ blockTag: 'finalized', includeTransactions: false }).then(b => b.number), + archiver.getL2Tips(), + ]); + logger.warn(`[state:${label}]`, { + l1Latest, + l1Finalized, + l2Proposed: archiverTips.proposed.number, + l2Checkpointed: archiverTips.checkpointed.block.number, + l2Proven: archiverTips.proven.block.number, + provenCheckpoint: archiverTips.proven.checkpoint.number, + l2Finalized: archiverTips.finalized.block.number, + finalizedCheckpoint: archiverTips.finalized.checkpoint.number, + }); + }; + // Send txs to trigger multi-block checkpoints await sendTransactions(TX_COUNT); // Capture initial chain state const initialProvenCheckpoint = (await monitor.run(true)).provenCheckpointNumber; + await logState('initial'); // Wait until we have proven something and the nodes have caught up const epochDurationSeconds = test.constants.epochDuration * test.constants.slotDuration; @@ -130,12 +152,23 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { epochDurationSeconds * 4 * 1000, ); + logger.warn( + `Proof for checkpoint ${provenBlockEvent.provenCheckpointNumber} mined at L1 block ${provenBlockEvent.l1BlockNumber}`, + ); + await logState('proof-landed'); + // Stop the prover node (by stopping its hosting aztec node) so it doesn't re-submit the proof after we've removed it - logger.warn(`Proof for block ${provenBlockEvent.provenCheckpointNumber} mined, stopping prover node`); + logger.warn(`Stopping prover node`); await test.proverNodes[0].stop(); + await logState('prover-stopped'); // And remove the proof from L1 - await context.cheatCodes.eth.reorgTo(provenBlockEvent.l1BlockNumber - 1); + const reorgTarget = provenBlockEvent.l1BlockNumber - 1; + logger.warn( + `Reorging L1 from current tip to block ${reorgTarget} (removing proof block ${provenBlockEvent.l1BlockNumber})`, + ); + await context.cheatCodes.eth.reorgTo(reorgTarget); + await logState('after-reorg'); expect((await monitor.run(true)).provenCheckpointNumber).toEqual(initialProvenCheckpoint); // Wait until the end of the proof submission window for the epoch of the proven checkpoint @@ -143,6 +176,7 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { CheckpointNumber(provenBlockEvent.provenCheckpointNumber), ); await test.waitUntilLastSlotOfProofSubmissionWindow(provenCheckpointEpoch); + await logState('after-submission-window'); // Ensure that a new node sees the reorg logger.warn(`Syncing new node to test reorg`); @@ -164,6 +198,7 @@ describe('e2e_epochs/epochs_l1_reorgs', () => { L2_SLOT_DURATION_IN_S * 4, 0.1, ); + await logState('old-node-synced'); expect(await getCheckpointNumber(node)).toBeWithin(monitor.checkpointNumber - 1, monitor.checkpointNumber + 1); // Verify multi-block checkpoints were built diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_multiple.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_multiple.test.ts index 6703786b0f88..e5cda00adecd 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_multiple.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_multiple.test.ts @@ -4,14 +4,12 @@ import { BlockNumber } from '@aztec/foundation/branded-types'; import { jest } from '@jest/globals'; -import type { EndToEndContext } from '../fixtures/utils.js'; import { EpochsTestContext, WORLD_STATE_CHECKPOINT_HISTORY } from './epochs_test.js'; jest.setTimeout(1000 * 60 * 15); // Assumes one block per checkpoint describe('e2e_epochs/epochs_multiple', () => { - let context: EndToEndContext; let rollup: RollupContract; let logger: Logger; @@ -19,7 +17,7 @@ describe('e2e_epochs/epochs_multiple', () => { beforeEach(async () => { test = await EpochsTestContext.setup(); - ({ context, rollup, logger } = test); + ({ rollup, logger } = test); }); afterEach(async () => { @@ -48,11 +46,13 @@ describe('e2e_epochs/epochs_multiple', () => { await test.waitForNodeToSync(epochEndBlockNumber, 'proven'); await test.verifyHistoricBlock(epochEndBlockNumber, true); - // Check that finalized blocks are purged from world state - // Right now finalization means a checkpoint is two L2 epochs deep. If this rule changes then this test needs to be updated. - // This test is setup as 1 block per checkpoint + // Check that finalized blocks are purged from world state. + // Anvil is started with --slots-in-an-epoch 1, so 'finalized' = latest - 2. By the time + // we reach this point the proof has been on L1 for many blocks, so the finalized L1 block + // is past the proof submission block, making finalized checkpoint == proven checkpoint. + // This test is setup as 1 block per checkpoint. const provenBlockNumber = epochEndBlockNumber; - const finalizedBlockNumber = Math.max(provenBlockNumber - context.config.aztecEpochDuration * 2, 0); + const finalizedBlockNumber = provenBlockNumber; const expectedOldestHistoricBlock = Math.max(finalizedBlockNumber - WORLD_STATE_CHECKPOINT_HISTORY + 1, 1); const expectedBlockRemoved = expectedOldestHistoricBlock - 1; await test.waitForNodeToSync(BlockNumber(expectedOldestHistoricBlock), 'historic'); diff --git a/yarn-project/end-to-end/src/e2e_pruned_blocks.test.ts b/yarn-project/end-to-end/src/e2e_pruned_blocks.test.ts index 50a8db649dd6..cccf826d1619 100644 --- a/yarn-project/end-to-end/src/e2e_pruned_blocks.test.ts +++ b/yarn-project/end-to-end/src/e2e_pruned_blocks.test.ts @@ -3,7 +3,6 @@ import type { Logger } from '@aztec/aztec.js/log'; import type { AztecNode } from '@aztec/aztec.js/node'; import { MerkleTreeId } from '@aztec/aztec.js/trees'; import type { Wallet } from '@aztec/aztec.js/wallet'; -import { CheatCodes } from '@aztec/aztec/testing'; import { retryUntil } from '@aztec/foundation/retry'; import { TokenContract } from '@aztec/noir-contracts.js/Token'; import type { AztecNodeAdmin } from '@aztec/stdlib/interfaces/client'; @@ -18,7 +17,6 @@ describe('e2e_pruned_blocks', () => { let aztecNode: AztecNode; let aztecNodeAdmin: AztecNodeAdmin | undefined; - let cheatCodes: CheatCodes; let wallet: Wallet; @@ -32,7 +30,6 @@ describe('e2e_pruned_blocks', () => { // Don't make this value too high since we need to mine this number of empty blocks, which is relatively slow. const WORLD_STATE_CHECKPOINT_HISTORY = 2; - const EPOCH_LENGTH = 2; const WORLD_STATE_CHECK_INTERVAL_MS = 300; const ARCHIVER_POLLING_INTERVAL_MS = 300; @@ -40,13 +37,11 @@ describe('e2e_pruned_blocks', () => { ({ aztecNode, aztecNodeAdmin, - cheatCodes, logger, teardown, wallet, accounts: [admin, sender, recipient], } = await setup(3, { - aztecEpochDuration: EPOCH_LENGTH, worldStateCheckpointHistory: WORLD_STATE_CHECKPOINT_HISTORY, worldStateBlockCheckIntervalMS: WORLD_STATE_CHECK_INTERVAL_MS, archiverPollingIntervalMS: ARCHIVER_POLLING_INTERVAL_MS, @@ -90,13 +85,13 @@ describe('e2e_pruned_blocks', () => { .data, ).toBeGreaterThan(0); - // We now mine dummy blocks, mark them as proven and wait for the node to process them, which should result in older - // blocks (notably the one with the minted note) being pruned. Given world state prunes based on the finalized tip, - // and we are defining the finalized tip as two epochs behind the proven one, we need to mine two extra epochs. - // This test assumes 1 block per checkpoint + // Mine enough blocks so the first mint block gets pruned. The test infrastructure auto-proves every + // checkpoint as it lands, and with slotsInAnEpoch=1 Anvil reports finalized = latest - 2, so + // finalization lags proving by just 2 L1 blocks. We mine WORLD_STATE_CHECKPOINT_HISTORY + 3 blocks: + // WORLD_STATE_CHECKPOINT_HISTORY to push the first mint block far enough back in history, and 3 to + // account for the 2-block finality lag plus one buffer. await aztecNodeAdmin!.setConfig({ minTxsPerBlock: 0 }); - await waitBlocks(WORLD_STATE_CHECKPOINT_HISTORY + EPOCH_LENGTH * 2 + 1); - await cheatCodes.rollup.markAsProven(); + await waitBlocks(WORLD_STATE_CHECKPOINT_HISTORY + 3); // The same historical query we performed before should now fail since this block is not available anymore. We poll // the node for a bit until it processes the blocks we marked as proven, causing the historical query to fail. diff --git a/yarn-project/end-to-end/src/fixtures/setup.ts b/yarn-project/end-to-end/src/fixtures/setup.ts index 517da8f7498e..6fc4a61524b3 100644 --- a/yarn-project/end-to-end/src/fixtures/setup.ts +++ b/yarn-project/end-to-end/src/fixtures/setup.ts @@ -185,6 +185,11 @@ export type SetupOptions = { anvilAccounts?: number; /** Port to start anvil (defaults to 8545) */ anvilPort?: number; + /** + * Number of slots per epoch for Anvil's finality simulation. + * Anvil reports `finalized = latest - slotsInAnEpoch * 2`. + */ + anvilSlotsInAnEpoch?: number; /** Key to use for publishing L1 contracts */ l1PublisherKey?: SecretValue<`0x${string}`>; /** ZkPassport configuration (domain, scope, mock verifier) */ @@ -305,6 +310,7 @@ export async function setup( l1BlockTime: opts.ethereumSlotDuration, accounts: opts.anvilAccounts, port: opts.anvilPort ?? (process.env.ANVIL_PORT ? parseInt(process.env.ANVIL_PORT) : undefined), + slotsInAnEpoch: opts.anvilSlotsInAnEpoch, }); anvil = res.anvil; config.l1RpcUrls = [res.rpcUrl]; diff --git a/yarn-project/ethereum/src/contracts/rollup.ts b/yarn-project/ethereum/src/contracts/rollup.ts index 40e10d287284..c4a88521df33 100644 --- a/yarn-project/ethereum/src/contracts/rollup.ts +++ b/yarn-project/ethereum/src/contracts/rollup.ts @@ -503,8 +503,9 @@ export class RollupContract { return CheckpointNumber.fromBigInt(await this.rollup.read.getPendingCheckpointNumber()); } - async getProvenCheckpointNumber(): Promise { - return CheckpointNumber.fromBigInt(await this.rollup.read.getProvenCheckpointNumber()); + async getProvenCheckpointNumber(options?: { blockNumber?: bigint }): Promise { + await checkBlockTag(options?.blockNumber, this.client); + return CheckpointNumber.fromBigInt(await this.rollup.read.getProvenCheckpointNumber(options)); } async getSlotNumber(): Promise { diff --git a/yarn-project/ethereum/src/test/start_anvil.ts b/yarn-project/ethereum/src/test/start_anvil.ts index de9665478a98..ba2f8b573604 100644 --- a/yarn-project/ethereum/src/test/start_anvil.ts +++ b/yarn-project/ethereum/src/test/start_anvil.ts @@ -26,6 +26,13 @@ export async function startAnvil( chainId?: number; /** The hardfork to use (e.g. 'cancun', 'latest'). */ hardfork?: string; + /** + * Number of slots per epoch used by anvil to compute the 'finalized' and 'safe' block tags. + * Anvil reports `finalized = latest - slotsInAnEpoch * 2`. + * Defaults to 1 so the finalized block advances immediately, making tests that check + * L1-finality-based logic work without needing hundreds of mined blocks. + */ + slotsInAnEpoch?: number; } = {}, ): Promise<{ anvil: Anvil; methodCalls?: string[]; rpcUrl: string; stop: () => Promise }> { const anvilBinary = resolve(dirname(fileURLToPath(import.meta.url)), '../../', 'scripts/anvil_kill_wrapper.sh'); @@ -55,6 +62,7 @@ export async function startAnvil( if (opts.hardfork !== undefined) { args.push('--hardfork', opts.hardfork); } + args.push('--slots-in-an-epoch', String(opts.slotsInAnEpoch ?? 1)); const child = spawn(anvilBinary, args, { stdio: ['ignore', 'pipe', 'pipe'], diff --git a/yarn-project/node-lib/src/actions/snapshot-sync.ts b/yarn-project/node-lib/src/actions/snapshot-sync.ts index 03d90ca67c51..b3dd1f954f54 100644 --- a/yarn-project/node-lib/src/actions/snapshot-sync.ts +++ b/yarn-project/node-lib/src/actions/snapshot-sync.ts @@ -61,7 +61,7 @@ export async function trySnapshotSync(config: SnapshotSyncConfig, log: Logger) { // Create an archiver store to check the current state (do this only once) log.verbose(`Creating temporary archiver data store`); - const archiverStore = await createArchiverStore(config, { epochDuration: config.aztecEpochDuration }); + const archiverStore = await createArchiverStore(config); let archiverL1BlockNumber: bigint | undefined; let archiverL2BlockNumber: number | undefined; try { diff --git a/yarn-project/prover-node/src/actions/rerun-epoch-proving-job.ts b/yarn-project/prover-node/src/actions/rerun-epoch-proving-job.ts index 2e619fa93be5..b472c31fc8c9 100644 --- a/yarn-project/prover-node/src/actions/rerun-epoch-proving-job.ts +++ b/yarn-project/prover-node/src/actions/rerun-epoch-proving-job.ts @@ -30,7 +30,7 @@ export async function rerunEpochProvingJob( const telemetry = getTelemetryClient(); const metrics = new ProverNodeJobMetrics(telemetry.getMeter('prover-job'), telemetry.getTracer('prover-job')); const worldState = await createWorldState(config); - const archiver = await createArchiverStore(config, { epochDuration: config.aztecEpochDuration }); + const archiver = await createArchiverStore(config); const publicProcessorFactory = new PublicProcessorFactory(archiver, undefined, undefined, log.getBindings()); const publisher = { submitEpochProof: () => Promise.resolve(true) }; diff --git a/yarn-project/stdlib/src/block/l2_block_source.ts b/yarn-project/stdlib/src/block/l2_block_source.ts index 39368d09ad99..64d2448a4fd1 100644 --- a/yarn-project/stdlib/src/block/l2_block_source.ts +++ b/yarn-project/stdlib/src/block/l2_block_source.ts @@ -68,9 +68,8 @@ export interface L2BlockSource { getCheckpointedL2BlockNumber(): Promise; /** - * Computes the finalized block number based on the proven block number. - * A block is considered finalized when it's 2 epochs behind the proven block. - * TODO(#13569): Compute proper finalized block number based on L1 finalized block. + * Returns the finalized L2 block number. A block is finalized when it was proven + * in an L1 block that has itself been finalized on Ethereum. * @returns The finalized block number. */ getFinalizedL2BlockNumber(): Promise; diff --git a/yarn-project/stdlib/src/tx/tx_receipt.ts b/yarn-project/stdlib/src/tx/tx_receipt.ts index 3b67b0057ba5..4cdb8f0eb6ed 100644 --- a/yarn-project/stdlib/src/tx/tx_receipt.ts +++ b/yarn-project/stdlib/src/tx/tx_receipt.ts @@ -15,7 +15,7 @@ export enum TxStatus { PROPOSED = 'proposed', CHECKPOINTED = 'checkpointed', PROVEN = 'proven', - FINALIZED = 'finalized', // TODO(#13569): Implement finalized status properly + FINALIZED = 'finalized', } /** Tx status sorted by finalization progress. */ diff --git a/yarn-project/txe/src/state_machine/archiver.ts b/yarn-project/txe/src/state_machine/archiver.ts index c601ed33ce85..8252f0e50816 100644 --- a/yarn-project/txe/src/state_machine/archiver.ts +++ b/yarn-project/txe/src/state_machine/archiver.ts @@ -17,7 +17,7 @@ export class TXEArchiver extends ArchiverDataSourceBase { private readonly updater = new ArchiverDataStoreUpdater(this.store); constructor(db: AztecAsyncKVStore) { - const store = new KVArchiverDataStore(db, 9999, { epochDuration: 32 }); + const store = new KVArchiverDataStore(db, 9999); super(store); } diff --git a/yarn-project/validator-client/src/validator.integration.test.ts b/yarn-project/validator-client/src/validator.integration.test.ts index 58a2520fbf3b..3d76c3dd1ac5 100644 --- a/yarn-project/validator-client/src/validator.integration.test.ts +++ b/yarn-project/validator-client/src/validator.integration.test.ts @@ -96,14 +96,11 @@ describe('ValidatorClient Integration', () => { /** Creates a new validator and dependencies */ const createValidatorContext = async (privateKey: Hex<32>): Promise => { // Create archiver store and NoopL1Archiver - const archiverStore = await createArchiverStore( - { - archiverStoreMapSizeKb: 1024 * 1024, - dataDirectory: undefined, - dataStoreMapSizeKb: 1024 * 1024, - }, - { epochDuration: l1Constants.epochDuration }, - ); + const archiverStore = await createArchiverStore({ + archiverStoreMapSizeKb: 1024 * 1024, + dataDirectory: undefined, + dataStoreMapSizeKb: 1024 * 1024, + }); await registerProtocolContracts(archiverStore); const archiver = await createNoopL1Archiver(archiverStore, { ...l1Constants, genesisArchiveRoot }); await archiver.start();