@aztec/archiver 0.0.1-commit.b655e406 → 0.0.1-commit.fce3e4f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dest/archiver/archiver.d.ts +30 -20
  2. package/dest/archiver/archiver.d.ts.map +1 -1
  3. package/dest/archiver/archiver.js +294 -208
  4. package/dest/archiver/archiver_store.d.ts +1 -1
  5. package/dest/archiver/archiver_store_test_suite.d.ts +1 -1
  6. package/dest/archiver/archiver_store_test_suite.d.ts.map +1 -1
  7. package/dest/archiver/archiver_store_test_suite.js +5 -4
  8. package/dest/archiver/config.d.ts +1 -1
  9. package/dest/archiver/config.d.ts.map +1 -1
  10. package/dest/archiver/config.js +5 -0
  11. package/dest/archiver/data_retrieval.d.ts +17 -17
  12. package/dest/archiver/data_retrieval.d.ts.map +1 -1
  13. package/dest/archiver/data_retrieval.js +110 -86
  14. package/dest/archiver/errors.d.ts +1 -1
  15. package/dest/archiver/errors.d.ts.map +1 -1
  16. package/dest/archiver/index.d.ts +1 -1
  17. package/dest/archiver/instrumentation.d.ts +3 -3
  18. package/dest/archiver/instrumentation.d.ts.map +1 -1
  19. package/dest/archiver/kv_archiver_store/block_store.d.ts +1 -1
  20. package/dest/archiver/kv_archiver_store/block_store.d.ts.map +1 -1
  21. package/dest/archiver/kv_archiver_store/contract_class_store.d.ts +1 -1
  22. package/dest/archiver/kv_archiver_store/contract_class_store.d.ts.map +1 -1
  23. package/dest/archiver/kv_archiver_store/contract_instance_store.d.ts +1 -1
  24. package/dest/archiver/kv_archiver_store/contract_instance_store.d.ts.map +1 -1
  25. package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts +2 -2
  26. package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts.map +1 -1
  27. package/dest/archiver/kv_archiver_store/log_store.d.ts +1 -1
  28. package/dest/archiver/kv_archiver_store/log_store.d.ts.map +1 -1
  29. package/dest/archiver/kv_archiver_store/message_store.d.ts +1 -1
  30. package/dest/archiver/kv_archiver_store/message_store.d.ts.map +1 -1
  31. package/dest/archiver/structs/data_retrieval.d.ts +1 -1
  32. package/dest/archiver/structs/inbox_message.d.ts +1 -1
  33. package/dest/archiver/structs/published.d.ts +3 -2
  34. package/dest/archiver/structs/published.d.ts.map +1 -1
  35. package/dest/archiver/validation.d.ts +10 -4
  36. package/dest/archiver/validation.d.ts.map +1 -1
  37. package/dest/archiver/validation.js +29 -21
  38. package/dest/factory.d.ts +1 -1
  39. package/dest/index.d.ts +2 -2
  40. package/dest/index.d.ts.map +1 -1
  41. package/dest/index.js +1 -1
  42. package/dest/rpc/index.d.ts +2 -2
  43. package/dest/test/index.d.ts +1 -1
  44. package/dest/test/mock_archiver.d.ts +1 -1
  45. package/dest/test/mock_archiver.d.ts.map +1 -1
  46. package/dest/test/mock_l1_to_l2_message_source.d.ts +1 -1
  47. package/dest/test/mock_l1_to_l2_message_source.d.ts.map +1 -1
  48. package/dest/test/mock_l2_block_source.d.ts +7 -6
  49. package/dest/test/mock_l2_block_source.d.ts.map +1 -1
  50. package/dest/test/mock_l2_block_source.js +1 -1
  51. package/dest/test/mock_structs.d.ts +1 -1
  52. package/package.json +17 -17
  53. package/src/archiver/archiver.ts +380 -244
  54. package/src/archiver/archiver_store_test_suite.ts +5 -4
  55. package/src/archiver/config.ts +5 -0
  56. package/src/archiver/data_retrieval.ts +156 -125
  57. package/src/archiver/instrumentation.ts +2 -2
  58. package/src/archiver/structs/published.ts +2 -1
  59. package/src/archiver/validation.ts +52 -27
  60. package/src/index.ts +1 -1
  61. package/src/test/mock_l2_block_source.ts +7 -6
@@ -9,6 +9,7 @@ import {
9
9
  createEthereumChain,
10
10
  } from '@aztec/ethereum';
11
11
  import { maxBigint } from '@aztec/foundation/bigint';
12
+ import { EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
12
13
  import { Buffer16, Buffer32 } from '@aztec/foundation/buffer';
13
14
  import { merge, pick } from '@aztec/foundation/collection';
14
15
  import type { EthAddress } from '@aztec/foundation/eth-address';
@@ -16,7 +17,6 @@ import { Fr } from '@aztec/foundation/fields';
16
17
  import { type Logger, createLogger } from '@aztec/foundation/log';
17
18
  import { type PromiseWithResolvers, promiseWithResolvers } from '@aztec/foundation/promise';
18
19
  import { RunningPromise, makeLoggingErrorHandler } from '@aztec/foundation/running-promise';
19
- import { sleep } from '@aztec/foundation/sleep';
20
20
  import { count } from '@aztec/foundation/string';
21
21
  import { DateProvider, Timer, elapsed } from '@aztec/foundation/timer';
22
22
  import type { CustomRange } from '@aztec/kv-store';
@@ -34,12 +34,14 @@ import type { FunctionSelector } from '@aztec/stdlib/abi';
34
34
  import type { AztecAddress } from '@aztec/stdlib/aztec-address';
35
35
  import {
36
36
  type ArchiverEmitter,
37
- type L2Block,
37
+ L2Block,
38
38
  type L2BlockId,
39
39
  type L2BlockSource,
40
40
  L2BlockSourceEvents,
41
41
  type L2Tips,
42
+ PublishedL2Block,
42
43
  } from '@aztec/stdlib/block';
44
+ import type { Checkpoint, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
43
45
  import {
44
46
  type ContractClassPublic,
45
47
  type ContractDataSource,
@@ -62,10 +64,10 @@ import type { GetContractClassLogsResponse, GetPublicLogsResponse } from '@aztec
62
64
  import type { L2LogsSource } from '@aztec/stdlib/interfaces/server';
63
65
  import { ContractClassLog, type LogFilter, type PrivateLog, type PublicLog, TxScopedL2Log } from '@aztec/stdlib/logs';
64
66
  import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
67
+ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
65
68
  import { type BlockHeader, type IndexedTxEffect, TxHash, TxReceipt } from '@aztec/stdlib/tx';
66
69
  import type { UInt64 } from '@aztec/stdlib/types';
67
70
  import {
68
- Attributes,
69
71
  type TelemetryClient,
70
72
  type Traceable,
71
73
  type Tracer,
@@ -75,21 +77,20 @@ import {
75
77
 
76
78
  import { EventEmitter } from 'events';
77
79
  import groupBy from 'lodash.groupby';
78
- import { type GetContractReturnType, createPublicClient, fallback, http } from 'viem';
80
+ import { type GetContractReturnType, type Hex, createPublicClient, fallback, http } from 'viem';
79
81
 
80
82
  import type { ArchiverDataStore, ArchiverL1SynchPoint } from './archiver_store.js';
81
83
  import type { ArchiverConfig } from './config.js';
82
84
  import {
83
- retrieveBlocksFromRollup,
85
+ retrieveCheckpointsFromRollup,
84
86
  retrieveL1ToL2Message,
85
87
  retrieveL1ToL2Messages,
86
- retrievedBlockToPublishedL2Block,
88
+ retrievedToPublishedCheckpoint,
87
89
  } from './data_retrieval.js';
88
90
  import { InitialBlockNumberNotSequentialError, NoBlobBodiesFoundError } from './errors.js';
89
91
  import { ArchiverInstrumentation } from './instrumentation.js';
90
92
  import type { InboxMessage } from './structs/inbox_message.js';
91
- import type { PublishedL2Block } from './structs/published.js';
92
- import { type ValidateBlockResult, validateBlockAttestations } from './validation.js';
93
+ import { type ValidateBlockResult, validateCheckpointAttestations } from './validation.js';
93
94
 
94
95
  /**
95
96
  * Helper interface to combine all sources this archiver implementation provides.
@@ -108,17 +109,28 @@ function mapArchiverConfig(config: Partial<ArchiverConfig>) {
108
109
  pollingIntervalMs: config.archiverPollingIntervalMS,
109
110
  batchSize: config.archiverBatchSize,
110
111
  skipValidateBlockAttestations: config.skipValidateBlockAttestations,
112
+ maxAllowedEthClientDriftSeconds: config.maxAllowedEthClientDriftSeconds,
111
113
  };
112
114
  }
113
115
 
116
+ type RollupStatus = {
117
+ provenCheckpointNumber: number;
118
+ provenArchive: Hex;
119
+ pendingCheckpointNumber: number;
120
+ pendingArchive: Hex;
121
+ validationResult: ValidateBlockResult | undefined;
122
+ lastRetrievedCheckpoint?: PublishedCheckpoint;
123
+ lastL1BlockWithCheckpoint?: bigint;
124
+ };
125
+
114
126
  /**
115
- * Pulls L2 blocks in a non-blocking manner and provides interface for their retrieval.
127
+ * Pulls checkpoints in a non-blocking manner and provides interface for their retrieval.
116
128
  * Responsible for handling robust L1 polling so that other components do not need to
117
129
  * concern themselves with it.
118
130
  */
119
131
  export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implements ArchiveSource, Traceable {
120
- /** A loop in which we will be continually fetching new L2 blocks. */
121
- private runningPromise?: RunningPromise;
132
+ /** A loop in which we will be continually fetching new checkpoints. */
133
+ private runningPromise: RunningPromise;
122
134
 
123
135
  private rollup: RollupContract;
124
136
  private inbox: InboxContract;
@@ -146,9 +158,15 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
146
158
  private readonly publicClient: ViemPublicClient,
147
159
  private readonly l1Addresses: { rollupAddress: EthAddress; inboxAddress: EthAddress; registryAddress: EthAddress },
148
160
  readonly dataStore: ArchiverDataStore,
149
- private config: { pollingIntervalMs: number; batchSize: number; skipValidateBlockAttestations?: boolean },
161
+ private config: {
162
+ pollingIntervalMs: number;
163
+ batchSize: number;
164
+ skipValidateBlockAttestations?: boolean;
165
+ maxAllowedEthClientDriftSeconds: number;
166
+ },
150
167
  private readonly blobSinkClient: BlobSinkClientInterface,
151
168
  private readonly epochCache: EpochCache,
169
+ private readonly dateProvider: DateProvider,
152
170
  private readonly instrumentation: ArchiverInstrumentation,
153
171
  private readonly l1constants: L1RollupConstants & { l1StartBlockHash: Buffer32; genesisArchiveRoot: Fr },
154
172
  private readonly log: Logger = createLogger('archiver'),
@@ -161,6 +179,15 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
161
179
  this.rollup = new RollupContract(publicClient, l1Addresses.rollupAddress);
162
180
  this.inbox = new InboxContract(publicClient, l1Addresses.inboxAddress);
163
181
  this.initialSyncPromise = promiseWithResolvers();
182
+
183
+ // Running promise starts with a small interval inbetween runs, so all iterations needed for the initial sync
184
+ // are done as fast as possible. This then gets updated once the initial sync completes.
185
+ this.runningPromise = new RunningPromise(
186
+ () => this.sync(),
187
+ this.log,
188
+ this.config.pollingIntervalMs / 10,
189
+ makeLoggingErrorHandler(this.log, NoBlobBodiesFoundError, BlockTagTooOldError),
190
+ );
164
191
  }
165
192
 
166
193
  /**
@@ -209,7 +236,10 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
209
236
  genesisArchiveRoot: Fr.fromHexString(genesisArchiveRoot),
210
237
  };
211
238
 
212
- const opts = merge({ pollingIntervalMs: 10_000, batchSize: 100 }, mapArchiverConfig(config));
239
+ const opts = merge(
240
+ { pollingIntervalMs: 10_000, batchSize: 100, maxAllowedEthClientDriftSeconds: 300 },
241
+ mapArchiverConfig(config),
242
+ );
213
243
 
214
244
  const epochCache = deps.epochCache ?? (await EpochCache.create(config.l1Contracts.rollupAddress, config, deps));
215
245
  const telemetry = deps.telemetry ?? getTelemetryClient();
@@ -221,6 +251,7 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
221
251
  opts,
222
252
  deps.blobSinkClient,
223
253
  epochCache,
254
+ deps.dateProvider ?? new DateProvider(),
224
255
  await ArchiverInstrumentation.new(telemetry, () => archiverStore.estimateSize()),
225
256
  l1Constants,
226
257
  );
@@ -238,38 +269,30 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
238
269
  * @param blockUntilSynced - If true, blocks until the archiver has fully synced.
239
270
  */
240
271
  public async start(blockUntilSynced: boolean): Promise<void> {
241
- if (this.runningPromise) {
272
+ if (this.runningPromise.isRunning()) {
242
273
  throw new Error('Archiver is already running');
243
274
  }
244
275
 
245
276
  await this.blobSinkClient.testSources();
246
-
247
- if (blockUntilSynced) {
248
- while (!(await this.syncSafe(true))) {
249
- this.log.info(`Retrying initial archiver sync in ${this.config.pollingIntervalMs}ms`);
250
- await sleep(this.config.pollingIntervalMs);
251
- }
252
- }
253
-
254
- this.runningPromise = new RunningPromise(
255
- () => this.sync(false),
256
- this.log,
257
- this.config.pollingIntervalMs,
258
- makeLoggingErrorHandler(
259
- this.log,
260
- // Ignored errors will not log to the console
261
- // We ignore NoBlobBodiesFound as the message may not have been passed to the blob sink yet
262
- NoBlobBodiesFoundError,
263
- ),
277
+ await this.testEthereumNodeSynced();
278
+
279
+ // Log initial state for the archiver
280
+ const { l1StartBlock } = this.l1constants;
281
+ const { blocksSynchedTo = l1StartBlock, messagesSynchedTo = l1StartBlock } = await this.store.getSynchPoint();
282
+ const currentL2Block = await this.getBlockNumber();
283
+ this.log.info(
284
+ `Starting archiver sync to rollup contract ${this.l1Addresses.rollupAddress.toString()} from L1 block ${blocksSynchedTo} and L2 block ${currentL2Block}`,
285
+ { blocksSynchedTo, messagesSynchedTo, currentL2Block },
264
286
  );
265
287
 
288
+ // Start sync loop, and return the wait for initial sync if we are asked to block until synced
266
289
  this.runningPromise.start();
290
+ if (blockUntilSynced) {
291
+ return this.waitForInitialSync();
292
+ }
267
293
  }
268
294
 
269
295
  public syncImmediate() {
270
- if (!this.runningPromise) {
271
- throw new Error('Archiver is not running');
272
- }
273
296
  return this.runningPromise.trigger();
274
297
  }
275
298
 
@@ -277,27 +300,26 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
277
300
  return this.initialSyncPromise.promise;
278
301
  }
279
302
 
280
- private async syncSafe(initialRun: boolean) {
281
- try {
282
- await this.sync(initialRun);
283
- return true;
284
- } catch (error) {
285
- if (error instanceof NoBlobBodiesFoundError) {
286
- this.log.error(`Error syncing archiver: ${error.message}`);
287
- } else if (error instanceof BlockTagTooOldError) {
288
- this.log.warn(`Re-running archiver sync: ${error.message}`);
289
- } else {
290
- this.log.error('Error during archiver sync', error);
291
- }
292
- return false;
303
+ /** Checks that the ethereum node we are connected to has a latest timestamp no more than the allowed drift. Throw if not. */
304
+ private async testEthereumNodeSynced() {
305
+ const maxAllowedDelay = this.config.maxAllowedEthClientDriftSeconds;
306
+ if (maxAllowedDelay === 0) {
307
+ return;
308
+ }
309
+ const { number, timestamp: l1Timestamp } = await this.publicClient.getBlock({ includeTransactions: false });
310
+ const currentTime = BigInt(this.dateProvider.nowInSeconds());
311
+ if (currentTime - l1Timestamp > BigInt(maxAllowedDelay)) {
312
+ throw new Error(
313
+ `Ethereum node is out of sync (last block synced ${number} at ${l1Timestamp} vs current time ${currentTime})`,
314
+ );
293
315
  }
294
316
  }
295
317
 
296
318
  /**
297
319
  * Fetches logs from L1 contracts and processes them.
298
320
  */
299
- @trackSpan('Archiver.sync', initialRun => ({ [Attributes.INITIAL_SYNC]: initialRun }))
300
- private async sync(initialRun: boolean) {
321
+ @trackSpan('Archiver.sync')
322
+ private async sync() {
301
323
  /**
302
324
  * We keep track of three "pointers" to L1 blocks:
303
325
  * 1. the last L1 block that published an L2 block
@@ -307,8 +329,6 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
307
329
  * We do this to deal with L1 data providers that are eventually consistent (e.g. Infura).
308
330
  * We guard against seeing block X with no data at one point, and later, the provider processes the block and it has data.
309
331
  * The archiver will stay back, until there's data on L1 that will move the pointers forward.
310
- *
311
- * This code does not handle reorgs.
312
332
  */
313
333
  const { l1StartBlock, l1StartBlockHash } = this.l1constants;
314
334
  const {
@@ -320,13 +340,12 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
320
340
  const currentL1BlockNumber = currentL1Block.number;
321
341
  const currentL1BlockHash = Buffer32.fromString(currentL1Block.hash);
322
342
 
323
- if (initialRun) {
324
- this.log.info(
325
- `Starting archiver sync to rollup contract ${this.l1Addresses.rollupAddress.toString()} from L1 block ${blocksSynchedTo}` +
326
- ` to current L1 block ${currentL1BlockNumber} with hash ${currentL1BlockHash.toString()}`,
327
- { blocksSynchedTo, messagesSynchedTo },
328
- );
329
- }
343
+ this.log.trace(`Starting new archiver sync iteration`, {
344
+ blocksSynchedTo,
345
+ messagesSynchedTo,
346
+ currentL1BlockNumber,
347
+ currentL1BlockHash,
348
+ });
330
349
 
331
350
  // ********** Ensuring Consistency of data pulled from L1 **********
332
351
 
@@ -356,28 +375,45 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
356
375
  ? (await this.publicClient.getBlock({ blockNumber: currentL1BlockNumber })).timestamp
357
376
  : this.l1Timestamp;
358
377
 
359
- // ********** Events that are processed per L2 block **********
378
+ // Warn if the latest L1 block timestamp is too old
379
+ const maxAllowedDelay = this.config.maxAllowedEthClientDriftSeconds;
380
+ const now = this.dateProvider.nowInSeconds();
381
+ if (maxAllowedDelay > 0 && Number(currentL1Timestamp) <= now - maxAllowedDelay) {
382
+ this.log.warn(
383
+ `Latest L1 block ${currentL1BlockNumber} timestamp ${currentL1Timestamp} is too old. Make sure your Ethereum node is synced.`,
384
+ { currentL1BlockNumber, currentL1Timestamp, now, maxAllowedDelay },
385
+ );
386
+ }
387
+
388
+ // ********** Events that are processed per checkpoint **********
360
389
  if (currentL1BlockNumber > blocksSynchedTo) {
361
- // First we retrieve new L2 blocks and store them in the DB. This will also update the
362
- // pending chain validation status, proven block number, and synched L1 block number.
363
- const rollupStatus = await this.handleL2blocks(blocksSynchedTo, currentL1BlockNumber);
390
+ // First we retrieve new checkpoints and L2 blocks and store them in the DB. This will also update the
391
+ // pending chain validation status, proven checkpoint number, and synched L1 block number.
392
+ const rollupStatus = await this.handleCheckpoints(blocksSynchedTo, currentL1BlockNumber);
364
393
  // Then we prune the current epoch if it'd reorg on next submission.
365
- // Note that we don't do this before retrieving L2 blocks because we may need to retrieve
366
- // blocks from more than 2 epochs ago, so we want to make sure we have the latest view of
394
+ // Note that we don't do this before retrieving checkpoints because we may need to retrieve
395
+ // checkpoints from more than 2 epochs ago, so we want to make sure we have the latest view of
367
396
  // the chain locally before we start unwinding stuff. This can be optimized by figuring out
368
- // up to which point we're pruning, and then requesting L2 blocks up to that point only.
397
+ // up to which point we're pruning, and then requesting checkpoints up to that point only.
369
398
  const { rollupCanPrune } = await this.handleEpochPrune(
370
- rollupStatus.provenBlockNumber,
399
+ rollupStatus.provenCheckpointNumber,
371
400
  currentL1BlockNumber,
372
401
  currentL1Timestamp,
373
402
  );
374
403
 
375
- // And lastly we check if we are missing any L2 blocks behind us due to a possible L1 reorg.
404
+ // If the last checkpoint we processed had an invalid attestation, we manually advance the L1 syncpoint
405
+ // past it, since otherwise we'll keep downloading it and reprocessing it on every iteration until
406
+ // we get a valid checkpoint to advance the syncpoint.
407
+ if (!rollupStatus.validationResult?.valid && rollupStatus.lastL1BlockWithCheckpoint !== undefined) {
408
+ await this.store.setBlockSynchedL1BlockNumber(rollupStatus.lastL1BlockWithCheckpoint);
409
+ }
410
+
411
+ // And lastly we check if we are missing any checkpoints behind us due to a possible L1 reorg.
376
412
  // We only do this if rollup cant prune on the next submission. Otherwise we will end up
377
- // re-syncing the blocks we have just unwound above. We also dont do this if the last block is invalid,
413
+ // re-syncing the checkpoints we have just unwound above. We also dont do this if the last checkpoint is invalid,
378
414
  // since the archiver will rightfully refuse to sync up to it.
379
415
  if (!rollupCanPrune && rollupStatus.validationResult?.valid) {
380
- await this.checkForNewBlocksBeforeL1SyncPoint(rollupStatus, blocksSynchedTo, currentL1BlockNumber);
416
+ await this.checkForNewCheckpointsBeforeL1SyncPoint(rollupStatus, blocksSynchedTo, currentL1BlockNumber);
381
417
  }
382
418
 
383
419
  this.instrumentation.updateL1BlockHeight(currentL1BlockNumber);
@@ -388,15 +424,18 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
388
424
  // but the corresponding blocks have not been processed (see #12631).
389
425
  this.l1Timestamp = currentL1Timestamp;
390
426
  this.l1BlockNumber = currentL1BlockNumber;
391
- this.initialSyncComplete = true;
392
- this.initialSyncPromise.resolve();
393
427
 
394
- if (initialRun) {
395
- this.log.info(`Initial archiver sync to L1 block ${currentL1BlockNumber} complete.`, {
428
+ // We resolve the initial sync only once we've caught up with the latest L1 block number (with 1 block grace)
429
+ // so if the initial sync took too long, we still go for another iteration.
430
+ if (!this.initialSyncComplete && currentL1BlockNumber + 1n >= (await this.publicClient.getBlockNumber())) {
431
+ this.log.info(`Initial archiver sync to L1 block ${currentL1BlockNumber} complete`, {
396
432
  l1BlockNumber: currentL1BlockNumber,
397
433
  syncPoint: await this.store.getSynchPoint(),
398
434
  ...(await this.getL2Tips()),
399
435
  });
436
+ this.runningPromise.setPollingIntervalMS(this.config.pollingIntervalMs);
437
+ this.initialSyncComplete = true;
438
+ this.initialSyncPromise.resolve();
400
439
  }
401
440
  }
402
441
 
@@ -414,43 +453,47 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
414
453
  return result;
415
454
  }
416
455
 
417
- /** Checks if there'd be a reorg for the next block submission and start pruning now. */
418
- private async handleEpochPrune(provenBlockNumber: number, currentL1BlockNumber: bigint, currentL1Timestamp: bigint) {
456
+ /** Checks if there'd be a reorg for the next checkpoint submission and start pruning now. */
457
+ private async handleEpochPrune(
458
+ provenCheckpointNumber: number,
459
+ currentL1BlockNumber: bigint,
460
+ currentL1Timestamp: bigint,
461
+ ) {
419
462
  const rollupCanPrune = await this.canPrune(currentL1BlockNumber, currentL1Timestamp);
420
- const localPendingBlockNumber = await this.getBlockNumber();
421
- const canPrune = localPendingBlockNumber > provenBlockNumber && rollupCanPrune;
463
+ const localPendingCheckpointNumber = await this.getSynchedCheckpointNumber();
464
+ const canPrune = localPendingCheckpointNumber > provenCheckpointNumber && rollupCanPrune;
422
465
 
423
466
  if (canPrune) {
424
467
  const timer = new Timer();
425
- const pruneFrom = provenBlockNumber + 1;
468
+ const pruneFrom = provenCheckpointNumber + 1;
426
469
 
427
- const header = await this.getBlockHeader(Number(pruneFrom));
470
+ const header = await this.getCheckpointHeader(Number(pruneFrom));
428
471
  if (header === undefined) {
429
- throw new Error(`Missing block header ${pruneFrom}`);
472
+ throw new Error(`Missing checkpoint header ${pruneFrom}`);
430
473
  }
431
474
 
432
- const pruneFromSlotNumber = header.globalVariables.slotNumber.toBigInt();
433
- const pruneFromEpochNumber = getEpochAtSlot(pruneFromSlotNumber, this.l1constants);
475
+ const pruneFromSlotNumber = header.slotNumber;
476
+ const pruneFromEpochNumber: EpochNumber = getEpochAtSlot(pruneFromSlotNumber, this.l1constants);
434
477
 
435
- const blocksToUnwind = localPendingBlockNumber - provenBlockNumber;
478
+ const checkpointsToUnwind = localPendingCheckpointNumber - provenCheckpointNumber;
436
479
 
437
- const blocks = await this.getBlocks(Number(provenBlockNumber) + 1, Number(blocksToUnwind));
480
+ const checkpoints = await this.getCheckpoints(Number(provenCheckpointNumber) + 1, Number(checkpointsToUnwind));
438
481
 
439
482
  // Emit an event for listening services to react to the chain prune
440
483
  this.emit(L2BlockSourceEvents.L2PruneDetected, {
441
484
  type: L2BlockSourceEvents.L2PruneDetected,
442
485
  epochNumber: pruneFromEpochNumber,
443
- blocks,
486
+ blocks: checkpoints.flatMap(c => L2Block.fromCheckpoint(c)),
444
487
  });
445
488
 
446
489
  this.log.debug(
447
- `L2 prune from ${provenBlockNumber + 1} to ${localPendingBlockNumber} will occur on next block submission.`,
490
+ `L2 prune from ${provenCheckpointNumber + 1} to ${localPendingCheckpointNumber} will occur on next checkpoint submission.`,
448
491
  );
449
- await this.store.unwindBlocks(Number(localPendingBlockNumber), Number(blocksToUnwind));
492
+ await this.unwindCheckpoints(localPendingCheckpointNumber, checkpointsToUnwind);
450
493
  this.log.warn(
451
- `Unwound ${count(blocksToUnwind, 'block')} from L2 block ${localPendingBlockNumber} ` +
452
- `to ${provenBlockNumber} due to predicted reorg at L1 block ${currentL1BlockNumber}. ` +
453
- `Updated L2 latest block is ${await this.getBlockNumber()}.`,
494
+ `Unwound ${count(checkpointsToUnwind, 'checkpoint')} from checkpoint ${localPendingCheckpointNumber} ` +
495
+ `to ${provenCheckpointNumber} due to predicted reorg at L1 block ${currentL1BlockNumber}. ` +
496
+ `Updated latest checkpoint is ${await this.getSynchedCheckpointNumber()}.`,
454
497
  );
455
498
  this.instrumentation.processPrune(timer.ms());
456
499
  // TODO(palla/reorg): Do we need to set the block synched L1 block number here?
@@ -497,7 +540,7 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
497
540
  remoteMessagesState.totalMessagesInserted === localMessagesInserted &&
498
541
  remoteMessagesState.messagesRollingHash.equals(localLastMessage?.rollingHash ?? Buffer16.ZERO)
499
542
  ) {
500
- this.log.debug(
543
+ this.log.trace(
501
544
  `No L1 to L2 messages to query between L1 blocks ${messagesSyncPoint.l1BlockNumber} and ${currentL1BlockNumber}.`,
502
545
  );
503
546
  return;
@@ -629,167 +672,185 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
629
672
  return Buffer32.fromString(block.hash);
630
673
  }
631
674
 
632
- private async handleL2blocks(blocksSynchedTo: bigint, currentL1BlockNumber: bigint) {
633
- const localPendingBlockNumber = await this.getBlockNumber();
675
+ private async handleCheckpoints(blocksSynchedTo: bigint, currentL1BlockNumber: bigint): Promise<RollupStatus> {
676
+ const localPendingCheckpointNumber = await this.getSynchedCheckpointNumber();
634
677
  const initialValidationResult: ValidateBlockResult | undefined = await this.store.getPendingChainValidationStatus();
635
- const [provenBlockNumber, provenArchive, pendingBlockNumber, pendingArchive, archiveForLocalPendingBlockNumber] =
636
- await this.rollup.status(BigInt(localPendingBlockNumber), { blockNumber: currentL1BlockNumber });
678
+ const [
679
+ rollupProvenCheckpointNumber,
680
+ provenArchive,
681
+ rollupPendingCheckpointNumber,
682
+ pendingArchive,
683
+ archiveForLocalPendingCheckpointNumber,
684
+ ] = await this.rollup.status(BigInt(localPendingCheckpointNumber), { blockNumber: currentL1BlockNumber });
685
+ const provenCheckpointNumber = Number(rollupProvenCheckpointNumber);
686
+ const pendingCheckpointNumber = Number(rollupPendingCheckpointNumber);
637
687
  const rollupStatus = {
638
- provenBlockNumber: Number(provenBlockNumber),
688
+ provenCheckpointNumber,
639
689
  provenArchive,
640
- pendingBlockNumber: Number(pendingBlockNumber),
690
+ pendingCheckpointNumber,
641
691
  pendingArchive,
642
692
  validationResult: initialValidationResult,
643
693
  };
644
694
  this.log.trace(`Retrieved rollup status at current L1 block ${currentL1BlockNumber}.`, {
645
- localPendingBlockNumber,
695
+ localPendingCheckpointNumber,
646
696
  blocksSynchedTo,
647
697
  currentL1BlockNumber,
648
- archiveForLocalPendingBlockNumber,
698
+ archiveForLocalPendingCheckpointNumber,
649
699
  ...rollupStatus,
650
700
  });
651
701
 
652
- const updateProvenBlock = async () => {
653
- // Annoying edge case: if proven block is moved back to 0 due to a reorg at the beginning of the chain,
654
- // we need to set it to zero. This is an edge case because we dont have a block zero (initial block is one),
655
- // so localBlockForDestinationProvenBlockNumber would not be found below.
656
- if (provenBlockNumber === 0n) {
657
- const localProvenBlockNumber = await this.store.getProvenL2BlockNumber();
658
- if (localProvenBlockNumber !== Number(provenBlockNumber)) {
659
- await this.store.setProvenL2BlockNumber(Number(provenBlockNumber));
660
- this.log.info(`Rolled back proven chain to block ${provenBlockNumber}`, { provenBlockNumber });
702
+ const updateProvenCheckpoint = async () => {
703
+ // Annoying edge case: if proven checkpoint is moved back to 0 due to a reorg at the beginning of the chain,
704
+ // we need to set it to zero. This is an edge case because we dont have a checkpoint zero (initial checkpoint is one),
705
+ // so localCheckpointForDestinationProvenCheckpointNumber would not be found below.
706
+ if (provenCheckpointNumber === 0) {
707
+ const localProvenCheckpointNumber = await this.getProvenCheckpointNumber();
708
+ if (localProvenCheckpointNumber !== provenCheckpointNumber) {
709
+ await this.setProvenCheckpointNumber(provenCheckpointNumber);
710
+ this.log.info(`Rolled back proven chain to checkpoint ${provenCheckpointNumber}`, { provenCheckpointNumber });
661
711
  }
662
712
  }
663
713
 
664
- const localBlockForDestinationProvenBlockNumber = await this.getBlock(Number(provenBlockNumber));
714
+ const localCheckpointForDestinationProvenCheckpointNumber = await this.getCheckpoint(provenCheckpointNumber);
665
715
 
666
- // Sanity check. I've hit what seems to be a state where the proven block is set to a value greater than the latest
667
- // synched block when requesting L2Tips from the archiver. This is the only place where the proven block is set.
668
- const synched = await this.store.getSynchedL2BlockNumber();
669
- if (localBlockForDestinationProvenBlockNumber && synched < localBlockForDestinationProvenBlockNumber?.number) {
716
+ // Sanity check. I've hit what seems to be a state where the proven checkpoint is set to a value greater than the latest
717
+ // synched checkpoint when requesting L2Tips from the archiver. This is the only place where the proven checkpoint is set.
718
+ const synched = await this.getSynchedCheckpointNumber();
719
+ if (
720
+ localCheckpointForDestinationProvenCheckpointNumber &&
721
+ synched < localCheckpointForDestinationProvenCheckpointNumber.number
722
+ ) {
670
723
  this.log.error(
671
- `Hit local block greater than last synched block: ${localBlockForDestinationProvenBlockNumber.number} > ${synched}`,
724
+ `Hit local checkpoint greater than last synched checkpoint: ${localCheckpointForDestinationProvenCheckpointNumber.number} > ${synched}`,
672
725
  );
673
726
  }
674
727
 
675
728
  this.log.trace(
676
- `Local block for remote proven block ${provenBlockNumber} is ${
677
- localBlockForDestinationProvenBlockNumber?.archive.root.toString() ?? 'undefined'
729
+ `Local checkpoint for remote proven checkpoint ${provenCheckpointNumber} is ${
730
+ localCheckpointForDestinationProvenCheckpointNumber?.archive.root.toString() ?? 'undefined'
678
731
  }`,
679
732
  );
680
733
 
734
+ const lastProvenBlockNumber = await this.getLastBlockNumberInCheckpoint(provenCheckpointNumber);
681
735
  if (
682
- localBlockForDestinationProvenBlockNumber &&
683
- provenArchive === localBlockForDestinationProvenBlockNumber.archive.root.toString()
736
+ localCheckpointForDestinationProvenCheckpointNumber &&
737
+ provenArchive === localCheckpointForDestinationProvenCheckpointNumber.archive.root.toString()
684
738
  ) {
685
- const localProvenBlockNumber = await this.store.getProvenL2BlockNumber();
686
- if (localProvenBlockNumber !== Number(provenBlockNumber)) {
687
- await this.store.setProvenL2BlockNumber(Number(provenBlockNumber));
688
- this.log.info(`Updated proven chain to block ${provenBlockNumber}`, {
689
- provenBlockNumber,
739
+ const localProvenCheckpointNumber = await this.getProvenCheckpointNumber();
740
+ if (localProvenCheckpointNumber !== provenCheckpointNumber) {
741
+ await this.setProvenCheckpointNumber(provenCheckpointNumber);
742
+ this.log.info(`Updated proven chain to checkpoint ${provenCheckpointNumber}`, {
743
+ provenCheckpointNumber,
690
744
  });
691
- const provenSlotNumber =
692
- localBlockForDestinationProvenBlockNumber.header.globalVariables.slotNumber.toBigInt();
693
- const provenEpochNumber = getEpochAtSlot(provenSlotNumber, this.l1constants);
745
+ const provenSlotNumber = localCheckpointForDestinationProvenCheckpointNumber.header.slotNumber;
746
+ const provenEpochNumber: EpochNumber = getEpochAtSlot(provenSlotNumber, this.l1constants);
747
+
694
748
  this.emit(L2BlockSourceEvents.L2BlockProven, {
695
749
  type: L2BlockSourceEvents.L2BlockProven,
696
- blockNumber: provenBlockNumber,
750
+ blockNumber: BigInt(lastProvenBlockNumber),
697
751
  slotNumber: provenSlotNumber,
698
752
  epochNumber: provenEpochNumber,
699
753
  });
700
754
  } else {
701
- this.log.trace(`Proven block ${provenBlockNumber} already stored.`);
755
+ this.log.trace(`Proven checkpoint ${provenCheckpointNumber} already stored.`);
702
756
  }
703
757
  }
704
- this.instrumentation.updateLastProvenBlock(Number(provenBlockNumber));
758
+ this.instrumentation.updateLastProvenBlock(lastProvenBlockNumber);
705
759
  };
706
760
 
707
- // This is an edge case that we only hit if there are no proposed blocks.
708
- // If we have 0 blocks locally and there are no blocks onchain there is nothing to do.
709
- const noBlocks = localPendingBlockNumber === 0 && pendingBlockNumber === 0n;
710
- if (noBlocks) {
761
+ // This is an edge case that we only hit if there are no proposed checkpoints.
762
+ // If we have 0 checkpoints locally and there are no checkpoints onchain there is nothing to do.
763
+ const noCheckpoints = localPendingCheckpointNumber === 0 && pendingCheckpointNumber === 0;
764
+ if (noCheckpoints) {
711
765
  await this.store.setBlockSynchedL1BlockNumber(currentL1BlockNumber);
712
766
  this.log.debug(
713
- `No blocks to retrieve from ${blocksSynchedTo + 1n} to ${currentL1BlockNumber}, no blocks on chain`,
767
+ `No checkpoints to retrieve from ${blocksSynchedTo + 1n} to ${currentL1BlockNumber}, no checkpoints on chain`,
714
768
  );
715
769
  return rollupStatus;
716
770
  }
717
771
 
718
- await updateProvenBlock();
772
+ await updateProvenCheckpoint();
719
773
 
720
774
  // Related to the L2 reorgs of the pending chain. We are only interested in actually addressing a reorg if there
721
- // are any state that could be impacted by it. If we have no blocks, there is no impact.
722
- if (localPendingBlockNumber > 0) {
723
- const localPendingBlock = await this.getBlock(localPendingBlockNumber);
724
- if (localPendingBlock === undefined) {
725
- throw new Error(`Missing block ${localPendingBlockNumber}`);
775
+ // are any state that could be impacted by it. If we have no checkpoints, there is no impact.
776
+ if (localPendingCheckpointNumber > 0) {
777
+ const localPendingCheckpoint = await this.getCheckpoint(localPendingCheckpointNumber);
778
+ if (localPendingCheckpoint === undefined) {
779
+ throw new Error(`Missing checkpoint ${localPendingCheckpointNumber}`);
726
780
  }
727
781
 
728
- const localPendingArchiveRoot = localPendingBlock.archive.root.toString();
729
- const noBlockSinceLast = localPendingBlock && pendingArchive === localPendingArchiveRoot;
730
- if (noBlockSinceLast) {
782
+ const localPendingArchiveRoot = localPendingCheckpoint.archive.root.toString();
783
+ const noCheckpointSinceLast = localPendingCheckpoint && pendingArchive === localPendingArchiveRoot;
784
+ if (noCheckpointSinceLast) {
731
785
  // We believe the following line causes a problem when we encounter L1 re-orgs.
732
786
  // Basically, by setting the synched L1 block number here, we are saying that we have
733
- // processed all blocks up to the current L1 block number and we will not attempt to retrieve logs from
787
+ // processed all checkpoints up to the current L1 block number and we will not attempt to retrieve logs from
734
788
  // this block again (or any blocks before).
735
- // However, in the re-org scenario, our L1 node is temporarily lying to us and we end up potentially missing blocks
789
+ // However, in the re-org scenario, our L1 node is temporarily lying to us and we end up potentially missing checkpoints.
736
790
  // We must only set this block number based on actually retrieved logs.
737
791
  // TODO(#8621): Tackle this properly when we handle L1 Re-orgs.
738
792
  // await this.store.setBlockSynchedL1BlockNumber(currentL1BlockNumber);
739
- this.log.debug(`No blocks to retrieve from ${blocksSynchedTo + 1n} to ${currentL1BlockNumber}`);
793
+ this.log.debug(`No checkpoints to retrieve from ${blocksSynchedTo + 1n} to ${currentL1BlockNumber}`);
740
794
  return rollupStatus;
741
795
  }
742
796
 
743
- const localPendingBlockInChain = archiveForLocalPendingBlockNumber === localPendingArchiveRoot;
744
- if (!localPendingBlockInChain) {
745
- // If our local pending block tip is not in the chain on L1 a "prune" must have happened
797
+ const localPendingCheckpointInChain = archiveForLocalPendingCheckpointNumber === localPendingArchiveRoot;
798
+ if (!localPendingCheckpointInChain) {
799
+ // If our local pending checkpoint tip is not in the chain on L1 a "prune" must have happened
746
800
  // or the L1 have reorged.
747
801
  // In any case, we have to figure out how far into the past the action will take us.
748
- // For simplicity here, we will simply rewind until we end in a block that is also on the chain on L1.
802
+ // For simplicity here, we will simply rewind until we end in a checkpoint that is also on the chain on L1.
749
803
  this.log.debug(
750
- `L2 prune has been detected due to local pending block ${localPendingBlockNumber} not in chain`,
751
- { localPendingBlockNumber, localPendingArchiveRoot, archiveForLocalPendingBlockNumber },
804
+ `L2 prune has been detected due to local pending checkpoint ${localPendingCheckpointNumber} not in chain`,
805
+ { localPendingCheckpointNumber, localPendingArchiveRoot, archiveForLocalPendingCheckpointNumber },
752
806
  );
753
807
 
754
- let tipAfterUnwind = localPendingBlockNumber;
808
+ let tipAfterUnwind = localPendingCheckpointNumber;
755
809
  while (true) {
756
- const candidateBlock = await this.getBlock(Number(tipAfterUnwind));
757
- if (candidateBlock === undefined) {
810
+ const candidateCheckpoint = await this.getCheckpoint(tipAfterUnwind);
811
+ if (candidateCheckpoint === undefined) {
758
812
  break;
759
813
  }
760
814
 
761
- const archiveAtContract = await this.rollup.archiveAt(BigInt(candidateBlock.number));
762
-
763
- if (archiveAtContract === candidateBlock.archive.root.toString()) {
815
+ const archiveAtContract = await this.rollup.archiveAt(BigInt(candidateCheckpoint.number));
816
+ this.log.trace(
817
+ `Checking local checkpoint ${candidateCheckpoint.number} with archive ${candidateCheckpoint.archive.root}`,
818
+ {
819
+ archiveAtContract,
820
+ archiveLocal: candidateCheckpoint.archive.root.toString(),
821
+ },
822
+ );
823
+ if (archiveAtContract === candidateCheckpoint.archive.root.toString()) {
764
824
  break;
765
825
  }
766
826
  tipAfterUnwind--;
767
827
  }
768
828
 
769
- const blocksToUnwind = localPendingBlockNumber - tipAfterUnwind;
770
- await this.store.unwindBlocks(Number(localPendingBlockNumber), Number(blocksToUnwind));
829
+ const checkpointsToUnwind = localPendingCheckpointNumber - tipAfterUnwind;
830
+ await this.unwindCheckpoints(localPendingCheckpointNumber, checkpointsToUnwind);
771
831
 
772
832
  this.log.warn(
773
- `Unwound ${count(blocksToUnwind, 'block')} from L2 block ${localPendingBlockNumber} ` +
774
- `due to mismatched block hashes at L1 block ${currentL1BlockNumber}. ` +
775
- `Updated L2 latest block is ${await this.getBlockNumber()}.`,
833
+ `Unwound ${count(checkpointsToUnwind, 'checkpoint')} from checkpoint ${localPendingCheckpointNumber} ` +
834
+ `due to mismatched checkpoint hashes at L1 block ${currentL1BlockNumber}. ` +
835
+ `Updated L2 latest checkpoint is ${await this.getSynchedCheckpointNumber()}.`,
776
836
  );
777
837
  }
778
838
  }
779
839
 
780
- // Retrieve L2 blocks in batches. Each batch is estimated to accommodate up to L2 'blockBatchSize' blocks,
840
+ // Retrieve checkpoints in batches. Each batch is estimated to accommodate up to 'blockBatchSize' L1 blocks,
781
841
  // computed using the L2 block time vs the L1 block time.
782
842
  let searchStartBlock: bigint = blocksSynchedTo;
783
843
  let searchEndBlock: bigint = blocksSynchedTo;
784
- let lastRetrievedBlock: PublishedL2Block | undefined;
844
+ let lastRetrievedCheckpoint: PublishedCheckpoint | undefined;
845
+ let lastL1BlockWithCheckpoint: bigint | undefined = undefined;
785
846
 
786
847
  do {
787
848
  [searchStartBlock, searchEndBlock] = this.nextRange(searchEndBlock, currentL1BlockNumber);
788
849
 
789
- this.log.trace(`Retrieving L2 blocks from L1 block ${searchStartBlock} to ${searchEndBlock}`);
850
+ this.log.trace(`Retrieving checkpoints from L1 block ${searchStartBlock} to ${searchEndBlock}`);
790
851
 
791
852
  // TODO(md): Retrieve from blob sink then from consensus client, then from peers
792
- const retrievedBlocks = await retrieveBlocksFromRollup(
853
+ const retrievedCheckpoints = await retrieveCheckpointsFromRollup(
793
854
  this.rollup.getContract() as GetContractReturnType<typeof RollupAbi, ViemPublicClient>,
794
855
  this.publicClient,
795
856
  this.blobSinkClient,
@@ -798,31 +859,35 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
798
859
  this.log,
799
860
  );
800
861
 
801
- if (retrievedBlocks.length === 0) {
862
+ if (retrievedCheckpoints.length === 0) {
802
863
  // We are not calling `setBlockSynchedL1BlockNumber` because it may cause sync issues if based off infura.
803
864
  // See further details in earlier comments.
804
- this.log.trace(`Retrieved no new L2 blocks from L1 block ${searchStartBlock} to ${searchEndBlock}`);
865
+ this.log.trace(`Retrieved no new checkpoints from L1 block ${searchStartBlock} to ${searchEndBlock}`);
805
866
  continue;
806
867
  }
807
868
 
808
- const lastProcessedL1BlockNumber = retrievedBlocks[retrievedBlocks.length - 1].l1.blockNumber;
809
869
  this.log.debug(
810
- `Retrieved ${retrievedBlocks.length} new L2 blocks between L1 blocks ${searchStartBlock} and ${searchEndBlock} with last processed L1 block ${lastProcessedL1BlockNumber}.`,
870
+ `Retrieved ${retrievedCheckpoints.length} new checkpoints between L1 blocks ${searchStartBlock} and ${searchEndBlock}`,
871
+ {
872
+ lastProcessedCheckpoint: retrievedCheckpoints[retrievedCheckpoints.length - 1].l1,
873
+ searchStartBlock,
874
+ searchEndBlock,
875
+ },
811
876
  );
812
877
 
813
- const publishedBlocks = await Promise.all(retrievedBlocks.map(b => retrievedBlockToPublishedL2Block(b)));
814
- const validBlocks: PublishedL2Block[] = [];
878
+ const publishedCheckpoints = await Promise.all(retrievedCheckpoints.map(b => retrievedToPublishedCheckpoint(b)));
879
+ const validCheckpoints: PublishedCheckpoint[] = [];
815
880
 
816
- for (const block of publishedBlocks) {
881
+ for (const published of publishedCheckpoints) {
817
882
  const validationResult = this.config.skipValidateBlockAttestations
818
883
  ? { valid: true as const }
819
- : await validateBlockAttestations(block, this.epochCache, this.l1constants, this.log);
884
+ : await validateCheckpointAttestations(published, this.epochCache, this.l1constants, this.log);
820
885
 
821
- // Only update the validation result if it has changed, so we can keep track of the first invalid block
822
- // in case there is a sequence of more than one invalid block, as we need to invalidate the first one.
823
- // There is an exception though: if an invalid block is invalidated and replaced with another invalid block,
886
+ // Only update the validation result if it has changed, so we can keep track of the first invalid checkpoint
887
+ // in case there is a sequence of more than one invalid checkpoint, as we need to invalidate the first one.
888
+ // There is an exception though: if a checkpoint is invalidated and replaced with another invalid checkpoint,
824
889
  // we need to update the validation result, since we need to be able to invalidate the new one.
825
- // See test 'chain progresses if an invalid block is invalidated with an invalid one' for more info.
890
+ // See test 'chain progresses if an invalid checkpoint is invalidated with an invalid one' for more info.
826
891
  if (
827
892
  rollupStatus.validationResult?.valid !== validationResult.valid ||
828
893
  (!rollupStatus.validationResult.valid &&
@@ -833,9 +898,9 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
833
898
  }
834
899
 
835
900
  if (!validationResult.valid) {
836
- this.log.warn(`Skipping block ${block.block.number} due to invalid attestations`, {
837
- blockHash: block.block.hash(),
838
- l1BlockNumber: block.l1.blockNumber,
901
+ this.log.warn(`Skipping checkpoint ${published.checkpoint.number} due to invalid attestations`, {
902
+ checkpointHash: published.checkpoint.hash(),
903
+ l1BlockNumber: published.l1.blockNumber,
839
904
  ...pick(validationResult, 'reason'),
840
905
  });
841
906
 
@@ -845,28 +910,31 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
845
910
  validationResult,
846
911
  });
847
912
 
848
- // We keep consuming blocks if we find an invalid one, since we do not listen for BlockInvalidated events
849
- // We just pretend the invalid ones are not there and keep consuming the next blocks
850
- // Note that this breaks if the committee ever attests to a descendant of an invalid block
913
+ // We keep consuming checkpoints if we find an invalid one, since we do not listen for CheckpointInvalidated events
914
+ // We just pretend the invalid ones are not there and keep consuming the next checkpoints
915
+ // Note that this breaks if the committee ever attests to a descendant of an invalid checkpoint
851
916
  continue;
852
917
  }
853
918
 
854
- validBlocks.push(block);
855
- this.log.debug(`Ingesting new L2 block ${block.block.number} with ${block.block.body.txEffects.length} txs`, {
856
- blockHash: block.block.hash(),
857
- l1BlockNumber: block.l1.blockNumber,
858
- ...block.block.header.globalVariables.toInspect(),
859
- ...block.block.getStats(),
860
- });
919
+ validCheckpoints.push(published);
920
+ this.log.debug(
921
+ `Ingesting new checkpoint ${published.checkpoint.number} with ${published.checkpoint.blocks.length} blocks`,
922
+ {
923
+ checkpointHash: published.checkpoint.hash(),
924
+ l1BlockNumber: published.l1.blockNumber,
925
+ ...published.checkpoint.header.toInspect(),
926
+ blocks: published.checkpoint.blocks.map(b => b.getStats()),
927
+ },
928
+ );
861
929
  }
862
930
 
863
931
  try {
864
932
  const updatedValidationResult =
865
933
  rollupStatus.validationResult === initialValidationResult ? undefined : rollupStatus.validationResult;
866
- const [processDuration] = await elapsed(() => this.store.addBlocks(validBlocks, updatedValidationResult));
934
+ const [processDuration] = await elapsed(() => this.addCheckpoints(validCheckpoints, updatedValidationResult));
867
935
  this.instrumentation.processNewBlocks(
868
- processDuration / validBlocks.length,
869
- validBlocks.map(b => b.block),
936
+ processDuration / validCheckpoints.length,
937
+ validCheckpoints.flatMap(c => c.checkpoint.blocks),
870
938
  );
871
939
  } catch (err) {
872
940
  if (err instanceof InitialBlockNumberNotSequentialError) {
@@ -889,56 +957,56 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
889
957
  throw err;
890
958
  }
891
959
 
892
- for (const block of validBlocks) {
893
- this.log.info(`Downloaded L2 block ${block.block.number}`, {
894
- blockHash: await block.block.hash(),
895
- blockNumber: block.block.number,
896
- txCount: block.block.body.txEffects.length,
897
- globalVariables: block.block.header.globalVariables.toInspect(),
898
- archiveRoot: block.block.archive.root.toString(),
899
- archiveNextLeafIndex: block.block.archive.nextAvailableLeafIndex,
960
+ for (const checkpoint of validCheckpoints) {
961
+ this.log.info(`Downloaded checkpoint ${checkpoint.checkpoint.number}`, {
962
+ checkpointHash: checkpoint.checkpoint.hash(),
963
+ checkpointNumber: checkpoint.checkpoint.number,
964
+ blockCount: checkpoint.checkpoint.blocks.length,
965
+ txCount: checkpoint.checkpoint.blocks.reduce((acc, b) => acc + b.body.txEffects.length, 0),
966
+ header: checkpoint.checkpoint.header.toInspect(),
967
+ archiveRoot: checkpoint.checkpoint.archive.root.toString(),
968
+ archiveNextLeafIndex: checkpoint.checkpoint.archive.nextAvailableLeafIndex,
900
969
  });
901
970
  }
902
- lastRetrievedBlock = validBlocks.at(-1) ?? lastRetrievedBlock;
971
+ lastRetrievedCheckpoint = validCheckpoints.at(-1) ?? lastRetrievedCheckpoint;
972
+ lastL1BlockWithCheckpoint = publishedCheckpoints.at(-1)?.l1.blockNumber ?? lastL1BlockWithCheckpoint;
903
973
  } while (searchEndBlock < currentL1BlockNumber);
904
974
 
905
975
  // Important that we update AFTER inserting the blocks.
906
- await updateProvenBlock();
976
+ await updateProvenCheckpoint();
907
977
 
908
- return { ...rollupStatus, lastRetrievedBlock };
978
+ return { ...rollupStatus, lastRetrievedCheckpoint, lastL1BlockWithCheckpoint };
909
979
  }
910
980
 
911
- private async checkForNewBlocksBeforeL1SyncPoint(
912
- status: {
913
- lastRetrievedBlock?: PublishedL2Block;
914
- pendingBlockNumber: number;
915
- },
981
+ private async checkForNewCheckpointsBeforeL1SyncPoint(
982
+ status: RollupStatus,
916
983
  blocksSynchedTo: bigint,
917
984
  currentL1BlockNumber: bigint,
918
985
  ) {
919
- const { lastRetrievedBlock, pendingBlockNumber } = status;
920
- // Compare the last L2 block we have (either retrieved in this round or loaded from store) with what the
986
+ const { lastRetrievedCheckpoint, pendingCheckpointNumber } = status;
987
+ // Compare the last checkpoint we have (either retrieved in this round or loaded from store) with what the
921
988
  // rollup contract told us was the latest one (pinned at the currentL1BlockNumber).
922
- const latestLocalL2BlockNumber = lastRetrievedBlock?.block.number ?? (await this.store.getSynchedL2BlockNumber());
923
- if (latestLocalL2BlockNumber < pendingBlockNumber) {
989
+ const latestLocalCheckpointNumber =
990
+ lastRetrievedCheckpoint?.checkpoint.number ?? (await this.getSynchedCheckpointNumber());
991
+ if (latestLocalCheckpointNumber < pendingCheckpointNumber) {
924
992
  // Here we have consumed all logs until the `currentL1Block` we pinned at the beginning of the archiver loop,
925
- // but still havent reached the pending block according to the call to the rollup contract.
926
- // We suspect an L1 reorg that added blocks *behind* us. If that is the case, it must have happened between the
927
- // last L2 block we saw and the current one, so we reset the last synched L1 block number. In the edge case we
928
- // don't have one, we go back 2 L1 epochs, which is the deepest possible reorg (assuming Casper is working).
929
- const latestLocalL2Block =
930
- lastRetrievedBlock ??
931
- (latestLocalL2BlockNumber > 0
932
- ? await this.store.getPublishedBlocks(latestLocalL2BlockNumber, 1).then(([b]) => b)
993
+ // but still haven't reached the pending checkpoint according to the call to the rollup contract.
994
+ // We suspect an L1 reorg that added checkpoints *behind* us. If that is the case, it must have happened between
995
+ // the last checkpoint we saw and the current one, so we reset the last synched L1 block number. In the edge case
996
+ // we don't have one, we go back 2 L1 epochs, which is the deepest possible reorg (assuming Casper is working).
997
+ const latestLocalCheckpoint =
998
+ lastRetrievedCheckpoint ??
999
+ (latestLocalCheckpointNumber > 0
1000
+ ? await this.getPublishedCheckpoints(latestLocalCheckpointNumber, 1).then(([c]) => c)
933
1001
  : undefined);
934
- const targetL1BlockNumber = latestLocalL2Block?.l1.blockNumber ?? maxBigint(currentL1BlockNumber - 64n, 0n);
935
- const latestLocalL2BlockArchive = latestLocalL2Block?.block.archive.root.toString();
1002
+ const targetL1BlockNumber = latestLocalCheckpoint?.l1.blockNumber ?? maxBigint(currentL1BlockNumber - 64n, 0n);
1003
+ const latestLocalCheckpointArchive = latestLocalCheckpoint?.checkpoint.archive.root.toString();
936
1004
  this.log.warn(
937
- `Failed to reach L2 block ${pendingBlockNumber} at ${currentL1BlockNumber} (latest is ${latestLocalL2BlockNumber}). ` +
1005
+ `Failed to reach checkpoint ${pendingCheckpointNumber} at ${currentL1BlockNumber} (latest is ${latestLocalCheckpointNumber}). ` +
938
1006
  `Rolling back last synched L1 block number to ${targetL1BlockNumber}.`,
939
1007
  {
940
- latestLocalL2BlockNumber,
941
- latestLocalL2BlockArchive,
1008
+ latestLocalCheckpointNumber,
1009
+ latestLocalCheckpointArchive,
942
1010
  blocksSynchedTo,
943
1011
  currentL1BlockNumber,
944
1012
  ...status,
@@ -946,18 +1014,15 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
946
1014
  );
947
1015
  await this.store.setBlockSynchedL1BlockNumber(targetL1BlockNumber);
948
1016
  } else {
949
- this.log.trace(`No new blocks behind L1 sync point to retrieve.`, {
950
- latestLocalL2BlockNumber,
951
- pendingBlockNumber,
1017
+ this.log.trace(`No new checkpoints behind L1 sync point to retrieve.`, {
1018
+ latestLocalCheckpointNumber,
1019
+ pendingCheckpointNumber,
952
1020
  });
953
1021
  }
954
1022
  }
955
1023
 
956
1024
  /** Resumes the archiver after a stop. */
957
1025
  public resume() {
958
- if (!this.runningPromise) {
959
- throw new Error(`Archiver was never started`);
960
- }
961
1026
  if (this.runningPromise.isRunning()) {
962
1027
  this.log.warn(`Archiver already running`);
963
1028
  }
@@ -971,7 +1036,7 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
971
1036
  */
972
1037
  public async stop(): Promise<void> {
973
1038
  this.log.debug('Stopping...');
974
- await this.runningPromise?.stop();
1039
+ await this.runningPromise.stop();
975
1040
 
976
1041
  this.log.info('Stopped.');
977
1042
  return Promise.resolve();
@@ -1005,26 +1070,26 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
1005
1070
  return Promise.resolve(this.l1Timestamp);
1006
1071
  }
1007
1072
 
1008
- public getL2SlotNumber(): Promise<bigint | undefined> {
1073
+ public getL2SlotNumber(): Promise<SlotNumber | undefined> {
1009
1074
  return Promise.resolve(
1010
1075
  this.l1Timestamp === undefined ? undefined : getSlotAtTimestamp(this.l1Timestamp, this.l1constants),
1011
1076
  );
1012
1077
  }
1013
1078
 
1014
- public getL2EpochNumber(): Promise<bigint | undefined> {
1079
+ public getL2EpochNumber(): Promise<EpochNumber | undefined> {
1015
1080
  return Promise.resolve(
1016
1081
  this.l1Timestamp === undefined ? undefined : getEpochNumberAtTimestamp(this.l1Timestamp, this.l1constants),
1017
1082
  );
1018
1083
  }
1019
1084
 
1020
- public async getBlocksForEpoch(epochNumber: bigint): Promise<L2Block[]> {
1085
+ public async getBlocksForEpoch(epochNumber: EpochNumber): Promise<L2Block[]> {
1021
1086
  const [start, end] = getSlotRangeForEpoch(epochNumber, this.l1constants);
1022
1087
  const blocks: L2Block[] = [];
1023
1088
 
1024
1089
  // Walk the list of blocks backwards and filter by slots matching the requested epoch.
1025
1090
  // We'll typically ask for blocks for a very recent epoch, so we shouldn't need an index here.
1026
1091
  let block = await this.getBlock(await this.store.getSynchedL2BlockNumber());
1027
- const slot = (b: L2Block) => b.header.globalVariables.slotNumber.toBigInt();
1092
+ const slot = (b: L2Block) => b.header.globalVariables.slotNumber;
1028
1093
  while (block && slot(block) >= start) {
1029
1094
  if (slot(block) <= end) {
1030
1095
  blocks.push(block);
@@ -1035,7 +1100,7 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
1035
1100
  return blocks.reverse();
1036
1101
  }
1037
1102
 
1038
- public async getBlockHeadersForEpoch(epochNumber: bigint): Promise<BlockHeader[]> {
1103
+ public async getBlockHeadersForEpoch(epochNumber: EpochNumber): Promise<BlockHeader[]> {
1039
1104
  const [start, end] = getSlotRangeForEpoch(epochNumber, this.l1constants);
1040
1105
  const blocks: BlockHeader[] = [];
1041
1106
 
@@ -1043,7 +1108,7 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
1043
1108
  // We'll typically ask for blocks for a very recent epoch, so we shouldn't need an index here.
1044
1109
  let number = await this.store.getSynchedL2BlockNumber();
1045
1110
  let header = await this.getBlockHeader(number);
1046
- const slot = (b: BlockHeader) => b.globalVariables.slotNumber.toBigInt();
1111
+ const slot = (b: BlockHeader) => b.globalVariables.slotNumber;
1047
1112
  while (header && slot(header) >= start) {
1048
1113
  if (slot(header) <= end) {
1049
1114
  blocks.push(header);
@@ -1053,10 +1118,10 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
1053
1118
  return blocks.reverse();
1054
1119
  }
1055
1120
 
1056
- public async isEpochComplete(epochNumber: bigint): Promise<boolean> {
1121
+ public async isEpochComplete(epochNumber: EpochNumber): Promise<boolean> {
1057
1122
  // The epoch is complete if the current L2 block is the last one in the epoch (or later)
1058
1123
  const header = await this.getBlockHeader('latest');
1059
- const slot = header?.globalVariables.slotNumber.toBigInt();
1124
+ const slot = header ? header.globalVariables.slotNumber : undefined;
1060
1125
  const [_startSlot, endSlot] = getSlotRangeForEpoch(epochNumber, this.l1constants);
1061
1126
  if (slot && slot >= endSlot) {
1062
1127
  return true;
@@ -1086,6 +1151,77 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
1086
1151
  return this.initialSyncComplete;
1087
1152
  }
1088
1153
 
1154
+ public async getPublishedCheckpoints(from: number, limit: number, proven?: boolean): Promise<PublishedCheckpoint[]> {
1155
+ const blocks = await this.getPublishedBlocks(from, limit, proven);
1156
+ return blocks.map(b => b.toPublishedCheckpoint());
1157
+ }
1158
+
1159
+ public async getCheckpoints(from: number, limit: number, proven?: boolean): Promise<Checkpoint[]> {
1160
+ const published = await this.getPublishedCheckpoints(from, limit, proven);
1161
+ return published.map(p => p.checkpoint);
1162
+ }
1163
+
1164
+ public async getCheckpoint(number: number): Promise<Checkpoint | undefined> {
1165
+ if (number < 0) {
1166
+ number = await this.getSynchedCheckpointNumber();
1167
+ }
1168
+ if (number === 0) {
1169
+ return undefined;
1170
+ }
1171
+ const published = await this.getPublishedCheckpoints(number, 1);
1172
+ return published[0]?.checkpoint;
1173
+ }
1174
+
1175
+ public async getCheckpointHeader(number: number | 'latest'): Promise<CheckpointHeader | undefined> {
1176
+ if (number === 'latest') {
1177
+ number = await this.getSynchedCheckpointNumber();
1178
+ }
1179
+ if (number === 0) {
1180
+ return undefined;
1181
+ }
1182
+ const checkpoint = await this.getCheckpoint(number);
1183
+ return checkpoint?.header;
1184
+ }
1185
+
1186
+ public getCheckpointNumber(): Promise<number> {
1187
+ return this.getSynchedCheckpointNumber();
1188
+ }
1189
+
1190
+ public getSynchedCheckpointNumber(): Promise<number> {
1191
+ // TODO: Checkpoint number will no longer be the same as the block number once we support multiple blocks per checkpoint.
1192
+ return this.store.getSynchedL2BlockNumber();
1193
+ }
1194
+
1195
+ public getProvenCheckpointNumber(): Promise<number> {
1196
+ // TODO: Proven checkpoint number will no longer be the same as the proven block number once we support multiple blocks per checkpoint.
1197
+ return this.store.getProvenL2BlockNumber();
1198
+ }
1199
+
1200
+ public setProvenCheckpointNumber(checkpointNumber: number): Promise<void> {
1201
+ // TODO: Proven checkpoint number will no longer be the same as the proven block number once we support multiple blocks per checkpoint.
1202
+ return this.store.setProvenL2BlockNumber(checkpointNumber);
1203
+ }
1204
+
1205
+ public unwindCheckpoints(from: number, checkpointsToUnwind: number): Promise<boolean> {
1206
+ // TODO: This only works if we have one block per checkpoint.
1207
+ return this.store.unwindBlocks(from, checkpointsToUnwind);
1208
+ }
1209
+
1210
+ public getLastBlockNumberInCheckpoint(checkpointNumber: number): Promise<number> {
1211
+ // TODO: Checkpoint number will no longer be the same as the block number once we support multiple blocks per checkpoint.
1212
+ return Promise.resolve(checkpointNumber);
1213
+ }
1214
+
1215
+ public addCheckpoints(
1216
+ checkpoints: PublishedCheckpoint[],
1217
+ pendingChainValidationStatus?: ValidateBlockResult,
1218
+ ): Promise<boolean> {
1219
+ return this.store.addBlocks(
1220
+ checkpoints.map(p => PublishedL2Block.fromPublishedCheckpoint(p)),
1221
+ pendingChainValidationStatus,
1222
+ );
1223
+ }
1224
+
1089
1225
  /**
1090
1226
  * Gets up to `limit` amount of L2 blocks starting from `from`.
1091
1227
  * @param from - Number of the first block to return (inclusive).