Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion yarn-project/archiver/src/archiver-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('Archiver Store', () => {
const tracer = getTelemetryClient().getTracer('');
instrumentation = mock<ArchiverInstrumentation>({ 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),
Expand Down Expand Up @@ -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));
});
});
});
67 changes: 66 additions & 1 deletion yarn-project/archiver/src/archiver-sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ describe('Archiver Sync', () => {
instrumentation = mock<ArchiverInstrumentation>({ 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,
Expand Down Expand Up @@ -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;

Expand Down
11 changes: 5 additions & 6 deletions yarn-project/archiver/src/archiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
6 changes: 2 additions & 4 deletions yarn-project/archiver/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -32,14 +31,13 @@ export const ARCHIVER_STORE_NAME = 'archiver';
/** Creates an archiver store. */
export async function createArchiverStore(
userConfig: Pick<ArchiverConfig, 'archiverStoreMapSizeKb' | 'maxLogs'> & DataStoreConfig,
l1Constants: Pick<L1RollupConstants, 'epochDuration'>,
) {
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);
}

/**
Expand All @@ -54,7 +52,7 @@ export async function createArchiver(
deps: ArchiverDeps,
opts: { blockUntilSync: boolean } = { blockUntilSync: true },
): Promise<Archiver> {
const archiverStore = await createArchiverStore(config, { epochDuration: config.aztecEpochDuration });
const archiverStore = await createArchiverStore(config);
await registerProtocolContracts(archiverStore);

// Create Ethereum clients
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions yarn-project/archiver/src/modules/data_store_updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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<boolean> {
return this.updateContractDataOnDb(block, Operation.Store);
Expand Down
24 changes: 24 additions & 0 deletions yarn-project/archiver/src/modules/l1_synchronizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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<void> {
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([
Expand Down
41 changes: 30 additions & 11 deletions yarn-project/archiver/src/store/block_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -97,6 +96,9 @@ export class BlockStore {
/** Stores last proven checkpoint */
#lastProvenCheckpoint: AztecAsyncSingleton<number>;

/** Stores last finalized checkpoint (proven at or before the finalized L1 block) */
#lastFinalizedCheckpoint: AztecAsyncSingleton<number>;

/** Stores the pending chain validation status */
#pendingChainValidationStatus: AztecAsyncSingleton<Buffer>;

Expand All @@ -111,10 +113,7 @@ export class BlockStore {

#log = createLogger('archiver:block_store');

constructor(
private db: AztecAsyncKVStore,
private l1Constants: Pick<L1RollupConstants, 'epochDuration'>,
) {
constructor(private db: AztecAsyncKVStore) {
this.#blocks = db.openMap('archiver_blocks');
this.#blockTxs = db.openMap('archiver_block_txs');
this.#txEffects = db.openMap('archiver_tx_effects');
Expand All @@ -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<BlockNumber> {
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);
}

/**
Expand Down Expand Up @@ -976,6 +981,20 @@ export class BlockStore {
return result;
}

async getFinalizedCheckpointNumber(): Promise<CheckpointNumber> {
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<Pick<Range<number>, 'start' | 'limit'>> {
if (limit < 1) {
throw new Error(`Invalid limit: ${limit}`);
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/archiver/src/store/kv_archiver_store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand Down
20 changes: 17 additions & 3 deletions yarn-project/archiver/src/store/kv_archiver_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -71,9 +70,8 @@ export class KVArchiverDataStore implements ContractDataSource {
constructor(
private db: AztecAsyncKVStore,
logsMaxPageSize: number = 1000,
l1Constants: Pick<L1RollupConstants, 'epochDuration'>,
) {
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);
Expand Down Expand Up @@ -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<CheckpointNumber> {
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);
}
Expand Down
Loading
Loading