diff --git a/yarn-project/archiver/src/archiver-sync.test.ts b/yarn-project/archiver/src/archiver-sync.test.ts index 11cd821b297d..76a12af1f559 100644 --- a/yarn-project/archiver/src/archiver-sync.test.ts +++ b/yarn-project/archiver/src/archiver-sync.test.ts @@ -30,6 +30,7 @@ import { Archiver, type ArchiverEmitter } from './archiver.js'; import type { ArchiverInstrumentation } from './modules/instrumentation.js'; import { ArchiverL1Synchronizer } from './modules/l1_synchronizer.js'; import { KVArchiverDataStore } from './store/kv_archiver_store.js'; +import { L2TipsCache } from './store/l2_tips_cache.js'; import { FakeL1State } from './test/fake_l1_state.js'; describe('Archiver Sync', () => { @@ -116,6 +117,9 @@ describe('Archiver Sync', () => { // Create event emitter shared by archiver and synchronizer const events = new EventEmitter() as ArchiverEmitter; + // Create L2 tips cache shared by archiver and synchronizer + const l2TipsCache = new L2TipsCache(archiverStore.blockStore); + // Create the L1 synchronizer synchronizer = new ArchiverL1Synchronizer( publicClient, @@ -132,6 +136,7 @@ describe('Archiver Sync', () => { l1Constants, events, instrumentation.tracer, + l2TipsCache, syncLogger, ); @@ -147,6 +152,7 @@ describe('Archiver Sync', () => { l1Constants, synchronizer, events, + l2TipsCache, ); }); diff --git a/yarn-project/archiver/src/archiver.ts b/yarn-project/archiver/src/archiver.ts index 9eefe8d2d66c..4aec6f3c9e69 100644 --- a/yarn-project/archiver/src/archiver.ts +++ b/yarn-project/archiver/src/archiver.ts @@ -1,5 +1,4 @@ import type { BlobClientInterface } from '@aztec/blob-client/client'; -import { GENESIS_BLOCK_HEADER_HASH, INITIAL_L2_BLOCK_NUM } from '@aztec/constants'; import { EpochCache } from '@aztec/epoch-cache'; import { BlockTagTooOldError, RollupContract } from '@aztec/ethereum/contracts'; import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses'; @@ -15,8 +14,6 @@ import { RunningPromise, makeLoggingErrorHandler } from '@aztec/foundation/runni import { DateProvider } from '@aztec/foundation/timer'; import { type ArchiverEmitter, - type CheckpointId, - GENESIS_CHECKPOINT_HEADER_HASH, L2Block, type L2BlockSink, type L2Tips, @@ -41,6 +38,7 @@ import { ArchiverDataStoreUpdater } from './modules/data_store_updater.js'; import type { ArchiverInstrumentation } from './modules/instrumentation.js'; import type { ArchiverL1Synchronizer } from './modules/l1_synchronizer.js'; import type { KVArchiverDataStore } from './store/kv_archiver_store.js'; +import { L2TipsCache } from './store/l2_tips_cache.js'; /** Export ArchiverEmitter for use in factory and tests. */ export type { ArchiverEmitter }; @@ -83,6 +81,9 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra /** Helper to handle updates to the store */ private readonly updater: ArchiverDataStoreUpdater; + /** In-memory cache for L2 chain tips. */ + private readonly l2TipsCache: L2TipsCache; + public readonly tracer: Tracer; /** @@ -122,6 +123,7 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra protected override readonly l1Constants: L1RollupConstants & { l1StartBlockHash: Buffer32; genesisArchiveRoot: Fr }, synchronizer: ArchiverL1Synchronizer, events: ArchiverEmitter, + l2TipsCache?: L2TipsCache, private readonly log: Logger = createLogger('archiver'), ) { super(dataStore, l1Constants); @@ -130,7 +132,8 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra this.initialSyncPromise = promiseWithResolvers(); this.synchronizer = synchronizer; this.events = events; - this.updater = new ArchiverDataStoreUpdater(this.dataStore); + this.l2TipsCache = l2TipsCache ?? new L2TipsCache(this.dataStore.blockStore); + this.updater = new ArchiverDataStoreUpdater(this.dataStore, this.l2TipsCache); // Running promise starts with a small interval inbetween runs, so all iterations needed for the initial sync // are done as fast as possible. This then gets updated once the initial sync completes. @@ -391,111 +394,8 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra return true; } - public async getL2Tips(): Promise { - const [latestBlockNumber, provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber] = await Promise.all([ - this.getBlockNumber(), - this.getProvenBlockNumber(), - this.getCheckpointedL2BlockNumber(), - this.getFinalizedL2BlockNumber(), - ] as const); - - 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, - provenBlockNumber > beforeInitialblockNumber ? this.getCheckpointedBlock(provenBlockNumber) : undefined, - finalizedBlockNumber > beforeInitialblockNumber ? this.getCheckpointedBlock(finalizedBlockNumber) : undefined, - checkpointedBlockNumber > beforeInitialblockNumber - ? this.getCheckpointedBlock(checkpointedBlockNumber) - : undefined, - ] as const); - - if (latestBlockNumber > beforeInitialblockNumber && !latestBlockHeader) { - throw new Error(`Failed to retrieve latest block header for block ${latestBlockNumber}`); - } - - // 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 checkpointed for block ${provenBlockNumber} (latest block is ${latestBlockNumber})`, - ); - } - - 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 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; - - // Now attempt to retrieve checkpoints for proven, finalised and checkpointed blocks - const [[provenBlockCheckpoint], [finalizedBlockCheckpoint], [checkpointedBlockCheckpoint]] = await Promise.all([ - provenCheckpointedBlock !== undefined - ? await this.getCheckpoints(provenCheckpointedBlock?.checkpointNumber, 1) - : [undefined], - finalizedCheckpointedBlock !== undefined - ? await this.getCheckpoints(finalizedCheckpointedBlock?.checkpointNumber, 1) - : [undefined], - checkpointedBlock !== undefined ? await this.getCheckpoints(checkpointedBlock?.checkpointNumber, 1) : [undefined], - ]); - - const initialcheckpointId: CheckpointId = { - number: CheckpointNumber.ZERO, - hash: GENESIS_CHECKPOINT_HEADER_HASH.toString(), - }; - - 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, - hash: latestBlockHeaderHash.toString(), - }, - proven: { - block: { - number: provenBlockNumber, - hash: provenBlockHeaderHash.toString(), - }, - checkpoint: makeCheckpointId(provenBlockCheckpoint), - }, - finalized: { - block: { - number: finalizedBlockNumber, - hash: finalizedBlockHeaderHash.toString(), - }, - checkpoint: makeCheckpointId(finalizedBlockCheckpoint), - }, - checkpointed: { - block: { - number: checkpointedBlockNumber, - hash: checkpointedBlockHeaderHash.toString(), - }, - checkpoint: makeCheckpointId(checkpointedBlockCheckpoint), - }, - }; - - return l2Tips; + public getL2Tips(): Promise { + return this.l2TipsCache.getL2Tips(); } public async rollbackTo(targetL2BlockNumber: BlockNumber): Promise { @@ -532,7 +432,7 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra await this.store.setMessageSynchedL1Block({ l1BlockNumber: targetL1BlockNumber, l1BlockHash: targetL1BlockHash }); if (targetL2BlockNumber < currentProvenBlock) { this.log.info(`Clearing proven L2 block number`); - await this.store.setProvenCheckpointNumber(CheckpointNumber.ZERO); + await this.updater.setProvenCheckpointNumber(CheckpointNumber.ZERO); } // TODO(palla/reorg): Set the finalized block when we add support for it. // if (targetL2BlockNumber < currentFinalizedBlock) { diff --git a/yarn-project/archiver/src/factory.ts b/yarn-project/archiver/src/factory.ts index eeb5090c406e..dc0ca5552d85 100644 --- a/yarn-project/archiver/src/factory.ts +++ b/yarn-project/archiver/src/factory.ts @@ -25,6 +25,7 @@ import { type ArchiverConfig, mapArchiverConfig } from './config.js'; import { ArchiverInstrumentation } from './modules/instrumentation.js'; import { ArchiverL1Synchronizer } from './modules/l1_synchronizer.js'; import { ARCHIVER_DB_VERSION, KVArchiverDataStore } from './store/kv_archiver_store.js'; +import { L2TipsCache } from './store/l2_tips_cache.js'; export const ARCHIVER_STORE_NAME = 'archiver'; @@ -128,6 +129,9 @@ export async function createArchiver( // Create the event emitter that will be shared by archiver and synchronizer const events = new EventEmitter() as ArchiverEmitter; + // Create L2 tips cache shared by archiver and synchronizer + const l2TipsCache = new L2TipsCache(archiverStore.blockStore); + // Create the L1 synchronizer const synchronizer = new ArchiverL1Synchronizer( publicClient, @@ -144,6 +148,8 @@ export async function createArchiver( l1Constants, events, instrumentation.tracer, + l2TipsCache, + undefined, // log (use default) ); const archiver = new Archiver( @@ -158,6 +164,7 @@ export async function createArchiver( l1Constants, synchronizer, events, + l2TipsCache, ); await archiver.start(opts.blockUntilSync); diff --git a/yarn-project/archiver/src/index.ts b/yarn-project/archiver/src/index.ts index 224884764f17..51aa5f45706c 100644 --- a/yarn-project/archiver/src/index.ts +++ b/yarn-project/archiver/src/index.ts @@ -8,5 +8,6 @@ export * from './config.js'; export { type L1PublishedData } from './structs/published.js'; export { KVArchiverDataStore, ARCHIVER_DB_VERSION } from './store/kv_archiver_store.js'; export { ContractInstanceStore } from './store/contract_instance_store.js'; +export { L2TipsCache } from './store/l2_tips_cache.js'; export { retrieveCheckpointsFromRollup, retrieveL2ProofVerifiedEvents } from './l1/data_retrieval.js'; diff --git a/yarn-project/archiver/src/modules/data_store_updater.ts b/yarn-project/archiver/src/modules/data_store_updater.ts index 1df274146880..dd2e6becd57a 100644 --- a/yarn-project/archiver/src/modules/data_store_updater.ts +++ b/yarn-project/archiver/src/modules/data_store_updater.ts @@ -25,6 +25,7 @@ import type { UInt64 } from '@aztec/stdlib/types'; import groupBy from 'lodash.groupby'; import type { KVArchiverDataStore } from '../store/kv_archiver_store.js'; +import type { L2TipsCache } from '../store/l2_tips_cache.js'; /** Operation type for contract data updates. */ enum Operation { @@ -44,7 +45,10 @@ type ReconcileCheckpointsResult = { export class ArchiverDataStoreUpdater { private readonly log = createLogger('archiver:store_updater'); - constructor(private store: KVArchiverDataStore) {} + constructor( + private store: KVArchiverDataStore, + private l2TipsCache?: L2TipsCache, + ) {} /** * Adds proposed blocks to the store with contract class/instance extraction from logs. @@ -56,11 +60,11 @@ export class ArchiverDataStoreUpdater { * @param pendingChainValidationStatus - Optional validation status to set. * @returns True if the operation is successful. */ - public addProposedBlocks( + public async addProposedBlocks( blocks: L2Block[], pendingChainValidationStatus?: ValidateCheckpointResult, ): Promise { - return this.store.transactionAsync(async () => { + const result = await this.store.transactionAsync(async () => { await this.store.addProposedBlocks(blocks); const opResults = await Promise.all([ @@ -72,8 +76,10 @@ export class ArchiverDataStoreUpdater { ...blocks.map(block => this.addContractDataToDb(block)), ]); + await this.l2TipsCache?.refresh(); return opResults.every(Boolean); }); + return result; } /** @@ -87,11 +93,11 @@ export class ArchiverDataStoreUpdater { * @param pendingChainValidationStatus - Optional validation status to set. * @returns Result with information about any pruned blocks. */ - public addCheckpoints( + public async addCheckpoints( checkpoints: PublishedCheckpoint[], pendingChainValidationStatus?: ValidateCheckpointResult, ): Promise { - return this.store.transactionAsync(async () => { + const result = await this.store.transactionAsync(async () => { // Before adding checkpoints, check for conflicts with local blocks if any const { prunedBlocks, lastAlreadyInsertedBlockNumber } = await this.pruneMismatchingLocalBlocks(checkpoints); @@ -111,8 +117,10 @@ export class ArchiverDataStoreUpdater { ...newBlocks.map(block => this.addContractDataToDb(block)), ]); + await this.l2TipsCache?.refresh(); return { prunedBlocks, lastAlreadyInsertedBlockNumber }; }); + return result; } /** @@ -197,8 +205,8 @@ export class ArchiverDataStoreUpdater { * @returns The removed blocks. * @throws Error if any block to be removed is checkpointed. */ - public removeUncheckpointedBlocksAfter(blockNumber: BlockNumber): Promise { - return this.store.transactionAsync(async () => { + public async removeUncheckpointedBlocksAfter(blockNumber: BlockNumber): Promise { + const result = await this.store.transactionAsync(async () => { // Verify we're only removing uncheckpointed blocks const lastCheckpointedBlockNumber = await this.store.getCheckpointedL2BlockNumber(); if (blockNumber < lastCheckpointedBlockNumber) { @@ -207,8 +215,11 @@ export class ArchiverDataStoreUpdater { ); } - return await this.removeBlocksAfter(blockNumber); + const result = await this.removeBlocksAfter(blockNumber); + await this.l2TipsCache?.refresh(); + return result; }); + return result; } /** @@ -238,17 +249,31 @@ export class ArchiverDataStoreUpdater { * @returns True if the operation is successful. */ public async removeCheckpointsAfter(checkpointNumber: CheckpointNumber): Promise { - const { blocksRemoved = [] } = await this.store.removeCheckpointsAfter(checkpointNumber); - - const opResults = await Promise.all([ - // Prune rolls back to the last proven block, which is by definition valid - this.store.setPendingChainValidationStatus({ valid: true }), - // Remove contract data for all blocks being removed - ...blocksRemoved.map(block => this.removeContractDataFromDb(block)), - this.store.deleteLogs(blocksRemoved), - ]); + return await this.store.transactionAsync(async () => { + const { blocksRemoved = [] } = await this.store.removeCheckpointsAfter(checkpointNumber); + + const opResults = await Promise.all([ + // Prune rolls back to the last proven block, which is by definition valid + this.store.setPendingChainValidationStatus({ valid: true }), + // Remove contract data for all blocks being removed + ...blocksRemoved.map(block => this.removeContractDataFromDb(block)), + this.store.deleteLogs(blocksRemoved), + ]); - return opResults.every(Boolean); + await this.l2TipsCache?.refresh(); + return opResults.every(Boolean); + }); + } + + /** + * Updates the proven checkpoint number and refreshes the L2 tips cache. + * @param checkpointNumber - The checkpoint number to set as proven. + */ + public async setProvenCheckpointNumber(checkpointNumber: CheckpointNumber): Promise { + await this.store.transactionAsync(async () => { + await this.store.setProvenCheckpointNumber(checkpointNumber); + await this.l2TipsCache?.refresh(); + }); } /** Extracts and stores contract data from a single block. */ diff --git a/yarn-project/archiver/src/modules/l1_synchronizer.ts b/yarn-project/archiver/src/modules/l1_synchronizer.ts index 7d8992c09616..c2dbca60d559 100644 --- a/yarn-project/archiver/src/modules/l1_synchronizer.ts +++ b/yarn-project/archiver/src/modules/l1_synchronizer.ts @@ -28,6 +28,7 @@ import { retrievedToPublishedCheckpoint, } from '../l1/data_retrieval.js'; import type { KVArchiverDataStore } from '../store/kv_archiver_store.js'; +import type { L2TipsCache } from '../store/l2_tips_cache.js'; import type { InboxMessage } from '../structs/inbox_message.js'; import { ArchiverDataStoreUpdater } from './data_store_updater.js'; import type { ArchiverInstrumentation } from './instrumentation.js'; @@ -77,9 +78,10 @@ export class ArchiverL1Synchronizer implements Traceable { private readonly l1Constants: L1RollupConstants & { l1StartBlockHash: Buffer32; genesisArchiveRoot: Fr }, private readonly events: ArchiverEmitter, tracer: Tracer, + l2TipsCache?: L2TipsCache, private readonly log: Logger = createLogger('archiver:l1-sync'), ) { - this.updater = new ArchiverDataStoreUpdater(this.store); + this.updater = new ArchiverDataStoreUpdater(this.store, l2TipsCache); this.tracer = tracer; } @@ -550,7 +552,7 @@ export class ArchiverL1Synchronizer implements Traceable { if (provenCheckpointNumber === 0) { const localProvenCheckpointNumber = await this.store.getProvenCheckpointNumber(); if (localProvenCheckpointNumber !== provenCheckpointNumber) { - await this.store.setProvenCheckpointNumber(provenCheckpointNumber); + await this.updater.setProvenCheckpointNumber(provenCheckpointNumber); this.log.info(`Rolled back proven chain to checkpoint ${provenCheckpointNumber}`, { provenCheckpointNumber }); } } @@ -582,7 +584,7 @@ export class ArchiverL1Synchronizer implements Traceable { ) { const localProvenCheckpointNumber = await this.store.getProvenCheckpointNumber(); if (localProvenCheckpointNumber !== provenCheckpointNumber) { - await this.store.setProvenCheckpointNumber(provenCheckpointNumber); + await this.updater.setProvenCheckpointNumber(provenCheckpointNumber); this.log.info(`Updated proven chain to checkpoint ${provenCheckpointNumber}`, { provenCheckpointNumber }); const provenSlotNumber = localCheckpointForDestinationProvenCheckpointNumber.header.slotNumber; const provenEpochNumber: EpochNumber = getEpochAtSlot(provenSlotNumber, this.l1Constants); diff --git a/yarn-project/archiver/src/store/kv_archiver_store.ts b/yarn-project/archiver/src/store/kv_archiver_store.ts index ed31527c65ed..24447c10ae5a 100644 --- a/yarn-project/archiver/src/store/kv_archiver_store.ts +++ b/yarn-project/archiver/src/store/kv_archiver_store.ts @@ -80,6 +80,11 @@ export class KVArchiverDataStore implements ContractDataSource { this.#contractInstanceStore = new ContractInstanceStore(db); } + /** Returns the underlying block store. Used by L2TipsCache. */ + get blockStore(): BlockStore { + return this.#blockStore; + } + /** Opens a new transaction to the underlying store and runs all operations within it. */ public transactionAsync(callback: () => Promise): Promise { return this.db.transactionAsync(callback); diff --git a/yarn-project/archiver/src/store/l2_tips_cache.ts b/yarn-project/archiver/src/store/l2_tips_cache.ts new file mode 100644 index 000000000000..64a0192e7624 --- /dev/null +++ b/yarn-project/archiver/src/store/l2_tips_cache.ts @@ -0,0 +1,89 @@ +import { GENESIS_BLOCK_HEADER_HASH, INITIAL_L2_BLOCK_NUM } from '@aztec/constants'; +import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types'; +import { type BlockData, type CheckpointId, GENESIS_CHECKPOINT_HEADER_HASH, type L2Tips } from '@aztec/stdlib/block'; + +import type { BlockStore } from './block_store.js'; + +/** + * In-memory cache for L2 chain tips (proposed, checkpointed, proven, finalized). + * Populated from the BlockStore on first access, then kept up-to-date by the ArchiverDataStoreUpdater. + * Refresh calls should happen within the store transaction that mutates block data to ensure consistency. + */ +export class L2TipsCache { + #tipsPromise: Promise | undefined; + + constructor(private blockStore: BlockStore) {} + + /** Returns the cached L2 tips. Loads from the block store on first call. */ + public getL2Tips(): Promise { + return (this.#tipsPromise ??= this.loadFromStore()); + } + + /** Reloads the L2 tips from the block store. Should be called within the store transaction that mutates data. */ + public async refresh(): Promise { + this.#tipsPromise = this.loadFromStore(); + await this.#tipsPromise; + } + + private async loadFromStore(): Promise { + const [latestBlockNumber, provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber] = await Promise.all([ + this.blockStore.getLatestBlockNumber(), + this.blockStore.getProvenBlockNumber(), + this.blockStore.getCheckpointedL2BlockNumber(), + this.blockStore.getFinalizedL2BlockNumber(), + ]); + + const genesisBlockHeader = { + blockHash: GENESIS_BLOCK_HEADER_HASH, + checkpointNumber: CheckpointNumber.ZERO, + } as const; + const beforeInitialBlockNumber = BlockNumber(INITIAL_L2_BLOCK_NUM - 1); + + const getBlockData = (blockNumber: BlockNumber) => + blockNumber > beforeInitialBlockNumber ? this.blockStore.getBlockData(blockNumber) : genesisBlockHeader; + + const [latestBlockData, provenBlockData, checkpointedBlockData, finalizedBlockData] = await Promise.all( + [latestBlockNumber, provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber].map(getBlockData), + ); + + if (!latestBlockData || !provenBlockData || !finalizedBlockData || !checkpointedBlockData) { + throw new Error('Failed to load block data for L2 tips'); + } + + const [provenCheckpointId, finalizedCheckpointId, checkpointedCheckpointId] = await Promise.all([ + this.getCheckpointIdForBlock(provenBlockData), + this.getCheckpointIdForBlock(finalizedBlockData), + this.getCheckpointIdForBlock(checkpointedBlockData), + ]); + + return { + proposed: { number: latestBlockNumber, hash: latestBlockData.blockHash.toString() }, + proven: { + block: { number: provenBlockNumber, hash: provenBlockData.blockHash.toString() }, + checkpoint: provenCheckpointId, + }, + finalized: { + block: { number: finalizedBlockNumber, hash: finalizedBlockData.blockHash.toString() }, + checkpoint: finalizedCheckpointId, + }, + checkpointed: { + block: { number: checkpointedBlockNumber, hash: checkpointedBlockData.blockHash.toString() }, + checkpoint: checkpointedCheckpointId, + }, + }; + } + + private async getCheckpointIdForBlock(blockData: Pick): Promise { + const checkpointData = await this.blockStore.getCheckpointData(blockData.checkpointNumber); + if (!checkpointData) { + return { + number: CheckpointNumber.ZERO, + hash: GENESIS_CHECKPOINT_HEADER_HASH.toString(), + }; + } + return { + number: checkpointData.checkpointNumber, + hash: checkpointData.header.hash().toString(), + }; + } +}