@aztec/archiver 0.56.0 → 0.57.0

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 (68) hide show
  1. package/README.md +1 -1
  2. package/dest/archiver/archiver.d.ts +23 -20
  3. package/dest/archiver/archiver.d.ts.map +1 -1
  4. package/dest/archiver/archiver.js +353 -103
  5. package/dest/archiver/archiver_store.d.ts +39 -9
  6. package/dest/archiver/archiver_store.d.ts.map +1 -1
  7. package/dest/archiver/archiver_store_test_suite.d.ts.map +1 -1
  8. package/dest/archiver/archiver_store_test_suite.js +75 -18
  9. package/dest/archiver/config.js +6 -6
  10. package/dest/archiver/data_retrieval.d.ts +2 -3
  11. package/dest/archiver/data_retrieval.d.ts.map +1 -1
  12. package/dest/archiver/data_retrieval.js +21 -20
  13. package/dest/archiver/epoch_helpers.d.ts +15 -0
  14. package/dest/archiver/epoch_helpers.d.ts.map +1 -0
  15. package/dest/archiver/epoch_helpers.js +23 -0
  16. package/dest/archiver/kv_archiver_store/block_store.d.ts +20 -1
  17. package/dest/archiver/kv_archiver_store/block_store.d.ts.map +1 -1
  18. package/dest/archiver/kv_archiver_store/block_store.js +62 -5
  19. package/dest/archiver/kv_archiver_store/contract_class_store.d.ts +2 -1
  20. package/dest/archiver/kv_archiver_store/contract_class_store.d.ts.map +1 -1
  21. package/dest/archiver/kv_archiver_store/contract_class_store.js +11 -4
  22. package/dest/archiver/kv_archiver_store/contract_instance_store.d.ts +1 -0
  23. package/dest/archiver/kv_archiver_store/contract_instance_store.d.ts.map +1 -1
  24. package/dest/archiver/kv_archiver_store/contract_instance_store.js +4 -1
  25. package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts +29 -9
  26. package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts.map +1 -1
  27. package/dest/archiver/kv_archiver_store/kv_archiver_store.js +57 -17
  28. package/dest/archiver/kv_archiver_store/log_store.d.ts +4 -5
  29. package/dest/archiver/kv_archiver_store/log_store.d.ts.map +1 -1
  30. package/dest/archiver/kv_archiver_store/log_store.js +18 -14
  31. package/dest/archiver/kv_archiver_store/message_store.d.ts +1 -0
  32. package/dest/archiver/kv_archiver_store/message_store.d.ts.map +1 -1
  33. package/dest/archiver/kv_archiver_store/message_store.js +10 -3
  34. package/dest/archiver/memory_archiver_store/l1_to_l2_message_store.d.ts +1 -0
  35. package/dest/archiver/memory_archiver_store/l1_to_l2_message_store.d.ts.map +1 -1
  36. package/dest/archiver/memory_archiver_store/l1_to_l2_message_store.js +4 -1
  37. package/dest/archiver/memory_archiver_store/memory_archiver_store.d.ts +23 -22
  38. package/dest/archiver/memory_archiver_store/memory_archiver_store.d.ts.map +1 -1
  39. package/dest/archiver/memory_archiver_store/memory_archiver_store.js +129 -69
  40. package/dest/index.js +2 -1
  41. package/dest/test/index.d.ts +2 -0
  42. package/dest/test/index.d.ts.map +1 -0
  43. package/dest/test/index.js +2 -0
  44. package/dest/test/mock_l2_block_source.d.ts +73 -0
  45. package/dest/test/mock_l2_block_source.d.ts.map +1 -0
  46. package/dest/test/mock_l2_block_source.js +134 -0
  47. package/package.json +15 -11
  48. package/src/archiver/archiver.ts +457 -149
  49. package/src/archiver/archiver_store.ts +44 -16
  50. package/src/archiver/archiver_store_test_suite.ts +91 -52
  51. package/src/archiver/config.ts +5 -5
  52. package/src/archiver/data_retrieval.ts +23 -24
  53. package/src/archiver/epoch_helpers.ts +26 -0
  54. package/src/archiver/kv_archiver_store/block_store.ts +70 -2
  55. package/src/archiver/kv_archiver_store/contract_class_store.ts +18 -5
  56. package/src/archiver/kv_archiver_store/contract_instance_store.ts +4 -0
  57. package/src/archiver/kv_archiver_store/kv_archiver_store.ts +65 -24
  58. package/src/archiver/kv_archiver_store/log_store.ts +18 -18
  59. package/src/archiver/kv_archiver_store/message_store.ts +9 -0
  60. package/src/archiver/memory_archiver_store/l1_to_l2_message_store.ts +4 -0
  61. package/src/archiver/memory_archiver_store/memory_archiver_store.ts +149 -80
  62. package/src/index.ts +1 -0
  63. package/src/test/index.ts +1 -0
  64. package/src/test/mock_l2_block_source.ts +165 -0
  65. package/dest/archiver/kv_archiver_store/proven_store.d.ts +0 -14
  66. package/dest/archiver/kv_archiver_store/proven_store.d.ts.map +0 -1
  67. package/dest/archiver/kv_archiver_store/proven_store.js +0 -30
  68. package/src/archiver/kv_archiver_store/proven_store.ts +0 -34
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  type FromLogType,
3
3
  type GetUnencryptedLogsResponse,
4
+ type InboxLeaf,
4
5
  type L1ToL2MessageSource,
5
6
  type L2Block,
6
7
  type L2BlockL2Logs,
@@ -13,14 +14,16 @@ import {
13
14
  type TxReceipt,
14
15
  type UnencryptedL2Log,
15
16
  } from '@aztec/circuit-types';
16
- import { ContractClassRegisteredEvent, type FunctionSelector } from '@aztec/circuits.js';
17
17
  import {
18
+ ContractClassRegisteredEvent,
18
19
  ContractInstanceDeployedEvent,
20
+ type FunctionSelector,
21
+ type Header,
19
22
  PrivateFunctionBroadcastedEvent,
20
23
  UnconstrainedFunctionBroadcastedEvent,
21
24
  isValidPrivateFunctionMembershipProof,
22
25
  isValidUnconstrainedFunctionMembershipProof,
23
- } from '@aztec/circuits.js/contract';
26
+ } from '@aztec/circuits.js';
24
27
  import { createEthereumChain } from '@aztec/ethereum';
25
28
  import { type ContractArtifact } from '@aztec/foundation/abi';
26
29
  import { type AztecAddress } from '@aztec/foundation/aztec-address';
@@ -52,11 +55,18 @@ import {
52
55
  http,
53
56
  } from 'viem';
54
57
 
55
- import { type ArchiverDataStore } from './archiver_store.js';
58
+ import { type ArchiverDataStore, type ArchiverL1SynchPoint } from './archiver_store.js';
56
59
  import { type ArchiverConfig } from './config.js';
57
60
  import { retrieveBlockFromRollup, retrieveL1ToL2Messages } from './data_retrieval.js';
61
+ import {
62
+ getEpochNumberAtTimestamp,
63
+ getSlotAtTimestamp,
64
+ getSlotRangeForEpoch,
65
+ getTimestampRangeForEpoch,
66
+ } from './epoch_helpers.js';
58
67
  import { ArchiverInstrumentation } from './instrumentation.js';
59
- import { type SingletonDataRetrieval } from './structs/data_retrieval.js';
68
+ import { type DataRetrieval } from './structs/data_retrieval.js';
69
+ import { type L1Published } from './structs/published.js';
60
70
 
61
71
  /**
62
72
  * Helper interface to combine all sources this archiver implementation provides.
@@ -77,6 +87,11 @@ export class Archiver implements ArchiveSource {
77
87
  private rollup: GetContractReturnType<typeof RollupAbi, PublicClient<HttpTransport, Chain>>;
78
88
  private inbox: GetContractReturnType<typeof InboxAbi, PublicClient<HttpTransport, Chain>>;
79
89
 
90
+ private store: ArchiverStoreHelper;
91
+
92
+ public l1BlockNumber: bigint | undefined;
93
+ public l1Timestamp: bigint | undefined;
94
+
80
95
  /**
81
96
  * Creates a new instance of the Archiver.
82
97
  * @param publicClient - A client for interacting with the Ethereum node.
@@ -90,14 +105,16 @@ export class Archiver implements ArchiveSource {
90
105
  constructor(
91
106
  private readonly publicClient: PublicClient<HttpTransport, Chain>,
92
107
  private readonly rollupAddress: EthAddress,
93
- private readonly inboxAddress: EthAddress,
108
+ readonly inboxAddress: EthAddress,
94
109
  private readonly registryAddress: EthAddress,
95
- private readonly store: ArchiverDataStore,
96
- private readonly pollingIntervalMs = 10_000,
110
+ readonly dataStore: ArchiverDataStore,
111
+ private readonly pollingIntervalMs: number,
97
112
  private readonly instrumentation: ArchiverInstrumentation,
98
- private readonly l1StartBlock: bigint = 0n,
113
+ private readonly l1constants: L1RollupConstants = EmptyL1RollupConstants,
99
114
  private readonly log: DebugLogger = createDebugLogger('aztec:archiver'),
100
115
  ) {
116
+ this.store = new ArchiverStoreHelper(dataStore);
117
+
101
118
  this.rollup = getContract({
102
119
  address: rollupAddress.toString(),
103
120
  abi: RollupAbi,
@@ -137,7 +154,10 @@ export class Archiver implements ArchiveSource {
137
154
  client: publicClient,
138
155
  });
139
156
 
140
- const l1StartBlock = await rollup.read.L1_BLOCK_AT_GENESIS();
157
+ const [l1StartBlock, l1GenesisTime] = await Promise.all([
158
+ rollup.read.L1_BLOCK_AT_GENESIS(),
159
+ rollup.read.GENESIS_TIME(),
160
+ ] as const);
141
161
 
142
162
  const archiver = new Archiver(
143
163
  publicClient,
@@ -145,9 +165,9 @@ export class Archiver implements ArchiveSource {
145
165
  config.l1Contracts.inboxAddress,
146
166
  config.l1Contracts.registryAddress,
147
167
  archiverStore,
148
- config.archiverPollingIntervalMS,
168
+ config.archiverPollingIntervalMS ?? 10_000,
149
169
  new ArchiverInstrumentation(telemetry),
150
- BigInt(l1StartBlock),
170
+ { l1StartBlock, l1GenesisTime },
151
171
  );
152
172
  await archiver.start(blockUntilSynced);
153
173
  return archiver;
@@ -199,11 +219,8 @@ export class Archiver implements ArchiveSource {
199
219
  *
200
220
  * This code does not handle reorgs.
201
221
  */
202
- const {
203
- blocksSynchedTo = this.l1StartBlock,
204
- messagesSynchedTo = this.l1StartBlock,
205
- provenLogsSynchedTo = this.l1StartBlock,
206
- } = await this.store.getSynchPoint();
222
+ const { l1StartBlock } = this.l1constants;
223
+ const { blocksSynchedTo = l1StartBlock, messagesSynchedTo = l1StartBlock } = await this.store.getSynchPoint();
207
224
  const currentL1BlockNumber = await this.publicClient.getBlockNumber();
208
225
 
209
226
  // ********** Ensuring Consistency of data pulled from L1 **********
@@ -225,14 +242,17 @@ export class Archiver implements ArchiveSource {
225
242
  * in future but for the time being it should give us the guarantees that we need
226
243
  */
227
244
 
228
- await this.updateLastProvenL2Block(provenLogsSynchedTo, currentL1BlockNumber);
229
-
230
245
  // ********** Events that are processed per L1 block **********
231
-
232
246
  await this.handleL1ToL2Messages(blockUntilSynced, messagesSynchedTo, currentL1BlockNumber);
233
247
 
234
248
  // ********** Events that are processed per L2 block **********
235
249
  await this.handleL2blocks(blockUntilSynced, blocksSynchedTo, currentL1BlockNumber);
250
+
251
+ // Store latest l1 block number and timestamp seen. Used for epoch and slots calculations.
252
+ if (!this.l1BlockNumber || this.l1BlockNumber < currentL1BlockNumber) {
253
+ this.l1Timestamp = await this.publicClient.getBlock({ blockNumber: currentL1BlockNumber }).then(b => b.timestamp);
254
+ this.l1BlockNumber = currentL1BlockNumber;
255
+ }
236
256
  }
237
257
 
238
258
  private async handleL1ToL2Messages(
@@ -244,14 +264,10 @@ export class Archiver implements ArchiveSource {
244
264
  return;
245
265
  }
246
266
 
247
- const retrievedL1ToL2Messages = await retrieveL1ToL2Messages(
248
- this.inbox,
249
- blockUntilSynced,
250
- messagesSynchedTo + 1n,
251
- currentL1BlockNumber,
252
- );
267
+ const localTotalMessageCount = await this.store.getTotalL1ToL2MessageCount();
268
+ const destinationTotalMessageCount = await this.inbox.read.totalMessagesInserted();
253
269
 
254
- if (retrievedL1ToL2Messages.retrievedData.length === 0) {
270
+ if (localTotalMessageCount === destinationTotalMessageCount) {
255
271
  await this.store.setMessageSynchedL1BlockNumber(currentL1BlockNumber);
256
272
  this.log.verbose(
257
273
  `Retrieved no new L1 -> L2 messages between L1 blocks ${messagesSynchedTo + 1n} and ${currentL1BlockNumber}.`,
@@ -259,6 +275,13 @@ export class Archiver implements ArchiveSource {
259
275
  return;
260
276
  }
261
277
 
278
+ const retrievedL1ToL2Messages = await retrieveL1ToL2Messages(
279
+ this.inbox,
280
+ blockUntilSynced,
281
+ messagesSynchedTo + 1n,
282
+ currentL1BlockNumber,
283
+ );
284
+
262
285
  await this.store.addL1ToL2Messages(retrievedL1ToL2Messages);
263
286
 
264
287
  this.log.verbose(
@@ -268,46 +291,90 @@ export class Archiver implements ArchiveSource {
268
291
  );
269
292
  }
270
293
 
271
- private async updateLastProvenL2Block(provenSynchedTo: bigint, currentL1BlockNumber: bigint) {
272
- if (currentL1BlockNumber <= provenSynchedTo) {
273
- return;
274
- }
275
-
276
- const provenBlockNumber = await this.rollup.read.getProvenBlockNumber();
277
- if (provenBlockNumber) {
278
- await this.store.setProvenL2BlockNumber({
279
- retrievedData: Number(provenBlockNumber),
280
- lastProcessedL1BlockNumber: currentL1BlockNumber,
281
- });
282
- }
283
- }
284
-
285
294
  private async handleL2blocks(blockUntilSynced: boolean, blocksSynchedTo: bigint, currentL1BlockNumber: bigint) {
286
295
  if (currentL1BlockNumber <= blocksSynchedTo) {
287
296
  return;
288
297
  }
289
298
 
290
- const lastBlock = await this.getBlock(-1);
291
-
292
- const [, , pendingBlockNumber, pendingArchive, archiveOfMyBlock] = await this.rollup.read.status([
293
- BigInt(lastBlock?.number ?? 0),
294
- ]);
295
-
296
- const noBlocksButInitial = lastBlock === undefined && pendingBlockNumber == 0n;
297
- const noBlockSinceLast =
298
- lastBlock &&
299
- pendingBlockNumber === BigInt(lastBlock.number) &&
300
- pendingArchive === lastBlock.archive.root.toString();
299
+ const localPendingBlockNumber = BigInt(await this.getBlockNumber());
300
+ const [
301
+ provenBlockNumber,
302
+ provenArchive,
303
+ pendingBlockNumber,
304
+ pendingArchive,
305
+ archiveForLocalPendingBlockNumber,
306
+ provenEpochNumber,
307
+ ] = await this.rollup.read.status([localPendingBlockNumber]);
308
+
309
+ const updateProvenBlock = async () => {
310
+ const localBlockForDestinationProvenBlockNumber = await this.getBlock(Number(provenBlockNumber));
311
+ if (
312
+ localBlockForDestinationProvenBlockNumber &&
313
+ provenArchive === localBlockForDestinationProvenBlockNumber.archive.root.toString()
314
+ ) {
315
+ this.log.info(`Updating the proven block number to ${provenBlockNumber} and epoch to ${provenEpochNumber}`);
316
+ await this.store.setProvenL2BlockNumber(Number(provenBlockNumber));
317
+ // if we are here then we must have a valid proven epoch number
318
+ await this.store.setProvenL2EpochNumber(Number(provenEpochNumber));
319
+ }
320
+ };
301
321
 
302
- if (noBlocksButInitial || noBlockSinceLast) {
322
+ // This is an edge case that we only hit if there are no proposed blocks.
323
+ // If we have 0 blocks locally and there are no blocks onchain there is nothing to do.
324
+ const noBlocks = localPendingBlockNumber === 0n && pendingBlockNumber === 0n;
325
+ if (noBlocks) {
303
326
  await this.store.setBlockSynchedL1BlockNumber(currentL1BlockNumber);
304
327
  this.log.verbose(`No blocks to retrieve from ${blocksSynchedTo + 1n} to ${currentL1BlockNumber}`);
305
328
  return;
306
329
  }
307
330
 
308
- if (lastBlock && archiveOfMyBlock !== lastBlock.archive.root.toString()) {
309
- // @todo Either `prune` have been called, or L1 have re-orged deep enough to remove a block.
310
- // Issue#8620 and Issue#8621
331
+ await updateProvenBlock();
332
+
333
+ // Related to the L2 reorgs of the pending chain. We are only interested in actually addressing a reorg if there
334
+ // are any state that could be impacted by it. If we have no blocks, there is no impact.
335
+ if (localPendingBlockNumber > 0) {
336
+ const localPendingBlock = await this.getBlock(Number(localPendingBlockNumber));
337
+ if (localPendingBlock === undefined) {
338
+ throw new Error(`Missing block ${localPendingBlockNumber}`);
339
+ }
340
+
341
+ const noBlockSinceLast = localPendingBlock && pendingArchive === localPendingBlock.archive.root.toString();
342
+ if (noBlockSinceLast) {
343
+ await this.store.setBlockSynchedL1BlockNumber(currentL1BlockNumber);
344
+ this.log.verbose(`No blocks to retrieve from ${blocksSynchedTo + 1n} to ${currentL1BlockNumber}`);
345
+ return;
346
+ }
347
+
348
+ const localPendingBlockInChain = archiveForLocalPendingBlockNumber === localPendingBlock.archive.root.toString();
349
+ if (!localPendingBlockInChain) {
350
+ // If our local pending block tip is not in the chain on L1 a "prune" must have happened
351
+ // or the L1 have reorged.
352
+ // In any case, we have to figure out how far into the past the action will take us.
353
+ // For simplicity here, we will simply rewind until we end in a block that is also on the chain on L1.
354
+ this.log.verbose(`L2 prune have occurred, unwind state`);
355
+
356
+ let tipAfterUnwind = localPendingBlockNumber;
357
+ while (true) {
358
+ const candidateBlock = await this.getBlock(Number(tipAfterUnwind));
359
+ if (candidateBlock === undefined) {
360
+ break;
361
+ }
362
+
363
+ const archiveAtContract = await this.rollup.read.archiveAt([BigInt(candidateBlock.number)]);
364
+
365
+ if (archiveAtContract === candidateBlock.archive.root.toString()) {
366
+ break;
367
+ }
368
+ tipAfterUnwind--;
369
+ }
370
+
371
+ const blocksToUnwind = localPendingBlockNumber - tipAfterUnwind;
372
+ this.log.verbose(
373
+ `Unwinding ${blocksToUnwind} block${blocksToUnwind > 1n ? 's' : ''} from block ${localPendingBlockNumber}`,
374
+ );
375
+
376
+ await this.store.unwindBlocks(Number(localPendingBlockNumber), Number(blocksToUnwind));
377
+ }
311
378
  }
312
379
 
313
380
  this.log.debug(`Retrieving blocks from ${blocksSynchedTo + 1n} to ${currentL1BlockNumber}`);
@@ -321,7 +388,8 @@ export class Archiver implements ArchiveSource {
321
388
  );
322
389
 
323
390
  if (retrievedBlocks.length === 0) {
324
- await this.store.setBlockSynchedL1BlockNumber(currentL1BlockNumber);
391
+ // We are not calling `setBlockSynchedL1BlockNumber` because it may cause sync issues if based off infura.
392
+ // See further details in earlier comments.
325
393
  this.log.verbose(`Retrieved no new blocks from ${blocksSynchedTo + 1n} to ${currentL1BlockNumber}`);
326
394
  return;
327
395
  }
@@ -340,31 +408,10 @@ export class Archiver implements ArchiveSource {
340
408
  .join(',')} with last processed L1 block ${lastProcessedL1BlockNumber}`,
341
409
  );
342
410
 
343
- await Promise.all(
344
- retrievedBlocks.map(block => {
345
- return this.store.addLogs(
346
- block.data.body.noteEncryptedLogs,
347
- block.data.body.encryptedLogs,
348
- block.data.body.unencryptedLogs,
349
- block.data.number,
350
- );
351
- }),
352
- );
353
-
354
- // Unroll all logs emitted during the retrieved blocks and extract any contract classes and instances from them
355
- await Promise.all(
356
- retrievedBlocks.map(async block => {
357
- const blockLogs = block.data.body.txEffects
358
- .flatMap(txEffect => (txEffect ? [txEffect.unencryptedLogs] : []))
359
- .flatMap(txLog => txLog.unrollLogs());
360
- await this.storeRegisteredContractClasses(blockLogs, block.data.number);
361
- await this.storeDeployedContractInstances(blockLogs, block.data.number);
362
- await this.storeBroadcastedIndividualFunctions(blockLogs, block.data.number);
363
- }),
364
- );
365
-
366
411
  const timer = new Timer();
367
412
  await this.store.addBlocks(retrievedBlocks);
413
+ // Important that we update AFTER inserting the blocks.
414
+ await updateProvenBlock();
368
415
  this.instrumentation.processNewBlocks(
369
416
  timer.ms() / retrievedBlocks.length,
370
417
  retrievedBlocks.map(b => b.data),
@@ -373,73 +420,6 @@ export class Archiver implements ArchiveSource {
373
420
  this.log.verbose(`Processed ${retrievedBlocks.length} new L2 blocks up to ${lastL2BlockNumber}`);
374
421
  }
375
422
 
376
- /**
377
- * Extracts and stores contract classes out of ContractClassRegistered events emitted by the class registerer contract.
378
- * @param allLogs - All logs emitted in a bunch of blocks.
379
- */
380
- private async storeRegisteredContractClasses(allLogs: UnencryptedL2Log[], blockNum: number) {
381
- const contractClasses = ContractClassRegisteredEvent.fromLogs(allLogs, ClassRegistererAddress).map(e =>
382
- e.toContractClassPublic(),
383
- );
384
- if (contractClasses.length > 0) {
385
- contractClasses.forEach(c => this.log.verbose(`Registering contract class ${c.id.toString()}`));
386
- await this.store.addContractClasses(contractClasses, blockNum);
387
- }
388
- }
389
-
390
- /**
391
- * Extracts and stores contract instances out of ContractInstanceDeployed events emitted by the canonical deployer contract.
392
- * @param allLogs - All logs emitted in a bunch of blocks.
393
- */
394
- private async storeDeployedContractInstances(allLogs: UnencryptedL2Log[], blockNum: number) {
395
- const contractInstances = ContractInstanceDeployedEvent.fromLogs(allLogs).map(e => e.toContractInstance());
396
- if (contractInstances.length > 0) {
397
- contractInstances.forEach(c => this.log.verbose(`Storing contract instance at ${c.address.toString()}`));
398
- await this.store.addContractInstances(contractInstances, blockNum);
399
- }
400
- }
401
-
402
- private async storeBroadcastedIndividualFunctions(allLogs: UnencryptedL2Log[], _blockNum: number) {
403
- // Filter out private and unconstrained function broadcast events
404
- const privateFnEvents = PrivateFunctionBroadcastedEvent.fromLogs(allLogs, ClassRegistererAddress);
405
- const unconstrainedFnEvents = UnconstrainedFunctionBroadcastedEvent.fromLogs(allLogs, ClassRegistererAddress);
406
-
407
- // Group all events by contract class id
408
- for (const [classIdString, classEvents] of Object.entries(
409
- groupBy([...privateFnEvents, ...unconstrainedFnEvents], e => e.contractClassId.toString()),
410
- )) {
411
- const contractClassId = Fr.fromString(classIdString);
412
- const contractClass = await this.store.getContractClass(contractClassId);
413
- if (!contractClass) {
414
- this.log.warn(`Skipping broadcasted functions as contract class ${contractClassId.toString()} was not found`);
415
- continue;
416
- }
417
-
418
- // Split private and unconstrained functions, and filter out invalid ones
419
- const allFns = classEvents.map(e => e.toFunctionWithMembershipProof());
420
- const privateFns = allFns.filter(
421
- (fn): fn is ExecutablePrivateFunctionWithMembershipProof => 'unconstrainedFunctionsArtifactTreeRoot' in fn,
422
- );
423
- const unconstrainedFns = allFns.filter(
424
- (fn): fn is UnconstrainedFunctionWithMembershipProof => 'privateFunctionsArtifactTreeRoot' in fn,
425
- );
426
- const validPrivateFns = privateFns.filter(fn => isValidPrivateFunctionMembershipProof(fn, contractClass));
427
- const validUnconstrainedFns = unconstrainedFns.filter(fn =>
428
- isValidUnconstrainedFunctionMembershipProof(fn, contractClass),
429
- );
430
- const validFnCount = validPrivateFns.length + validUnconstrainedFns.length;
431
- if (validFnCount !== allFns.length) {
432
- this.log.warn(`Skipping ${allFns.length - validFnCount} invalid functions`);
433
- }
434
-
435
- // Store the functions in the contract class in a single operation
436
- if (validFnCount > 0) {
437
- this.log.verbose(`Storing ${validFnCount} functions for contract class ${contractClassId.toString()}`);
438
- }
439
- await this.store.addFunctions(contractClassId, validPrivateFns, validUnconstrainedFns);
440
- }
441
- }
442
-
443
423
  /**
444
424
  * Stops the archiver.
445
425
  * @returns A promise signalling completion of the stop process.
@@ -460,6 +440,68 @@ export class Archiver implements ArchiveSource {
460
440
  return Promise.resolve(this.registryAddress);
461
441
  }
462
442
 
443
+ public getL1BlockNumber(): bigint {
444
+ const l1BlockNumber = this.l1BlockNumber;
445
+ if (!l1BlockNumber) {
446
+ throw new Error('L1 block number not yet available. Complete an initial sync first.');
447
+ }
448
+ return l1BlockNumber;
449
+ }
450
+
451
+ public getL1Timestamp(): bigint {
452
+ const l1Timestamp = this.l1Timestamp;
453
+ if (!l1Timestamp) {
454
+ throw new Error('L1 timestamp not yet available. Complete an initial sync first.');
455
+ }
456
+ return l1Timestamp;
457
+ }
458
+
459
+ public getL2SlotNumber(): Promise<bigint> {
460
+ return Promise.resolve(getSlotAtTimestamp(this.getL1Timestamp(), this.l1constants));
461
+ }
462
+
463
+ public getL2EpochNumber(): Promise<bigint> {
464
+ return Promise.resolve(getEpochNumberAtTimestamp(this.getL1Timestamp(), this.l1constants));
465
+ }
466
+
467
+ public async getBlocksForEpoch(epochNumber: bigint): Promise<L2Block[]> {
468
+ const [start, end] = getSlotRangeForEpoch(epochNumber);
469
+ const blocks: L2Block[] = [];
470
+
471
+ // Walk the list of blocks backwards and filter by slots matching the requested epoch.
472
+ // We'll typically ask for blocks for a very recent epoch, so we shouldn't need an index here.
473
+ let block = await this.getBlock(await this.store.getSynchedL2BlockNumber());
474
+ const slot = (b: L2Block) => b.header.globalVariables.slotNumber.toBigInt();
475
+ while (block && slot(block) >= start) {
476
+ if (slot(block) <= end) {
477
+ blocks.push(block);
478
+ }
479
+ block = await this.getBlock(block.number - 1);
480
+ }
481
+
482
+ return blocks.reverse();
483
+ }
484
+
485
+ public async isEpochComplete(epochNumber: bigint): Promise<boolean> {
486
+ // The epoch is complete if the current L2 block is the last one in the epoch (or later)
487
+ const header = await this.getBlockHeader('latest');
488
+ const slot = header?.globalVariables.slotNumber.toBigInt();
489
+ const [_startSlot, endSlot] = getSlotRangeForEpoch(epochNumber);
490
+ if (slot && slot >= endSlot) {
491
+ return true;
492
+ }
493
+
494
+ // If not, the epoch may also be complete if the L2 slot has passed without a block
495
+ // We compute this based on the timestamp for the given epoch and the timestamp of the last L1 block
496
+ const l1Timestamp = this.getL1Timestamp();
497
+ const [_startTimestamp, endTimestamp] = getTimestampRangeForEpoch(epochNumber, this.l1constants);
498
+
499
+ // For this computation, we throw in a few extra seconds just for good measure,
500
+ // since we know the next L1 block won't be mined within this range
501
+ const leeway = 3n;
502
+ return l1Timestamp + leeway >= endTimestamp;
503
+ }
504
+
463
505
  /**
464
506
  * Gets up to `limit` amount of L2 blocks starting from `from`.
465
507
  * @param from - Number of the first block to return (inclusive).
@@ -476,7 +518,7 @@ export class Archiver implements ArchiveSource {
476
518
 
477
519
  /**
478
520
  * Gets an l2 block.
479
- * @param number - The block number to return (inclusive).
521
+ * @param number - The block number to return.
480
522
  * @returns The requested L2 block.
481
523
  */
482
524
  public async getBlock(number: number): Promise<L2Block | undefined> {
@@ -484,10 +526,21 @@ export class Archiver implements ArchiveSource {
484
526
  if (number < 0) {
485
527
  number = await this.store.getSynchedL2BlockNumber();
486
528
  }
529
+ if (number == 0) {
530
+ return undefined;
531
+ }
487
532
  const blocks = await this.store.getBlocks(number, 1);
488
533
  return blocks.length === 0 ? undefined : blocks[0].data;
489
534
  }
490
535
 
536
+ public async getBlockHeader(number: number | 'latest'): Promise<Header | undefined> {
537
+ if (number === 'latest') {
538
+ number = await this.store.getSynchedL2BlockNumber();
539
+ }
540
+ const headers = await this.store.getBlockHeaders(number, 1);
541
+ return headers.length === 0 ? undefined : headers[0];
542
+ }
543
+
491
544
  public getTxEffect(txHash: TxHash): Promise<TxEffect | undefined> {
492
545
  return this.store.getTxEffect(txHash);
493
546
  }
@@ -553,9 +606,13 @@ export class Archiver implements ArchiveSource {
553
606
  return this.store.getProvenL2BlockNumber();
554
607
  }
555
608
 
609
+ public getProvenL2EpochNumber(): Promise<number | undefined> {
610
+ return this.store.getProvenL2EpochNumber();
611
+ }
612
+
556
613
  /** Forcefully updates the last proven block number. Use for testing. */
557
- public setProvenBlockNumber(block: SingletonDataRetrieval<number>): Promise<void> {
558
- return this.store.setProvenL2BlockNumber(block);
614
+ public setProvenBlockNumber(blockNumber: number): Promise<void> {
615
+ return this.store.setProvenL2BlockNumber(blockNumber);
559
616
  }
560
617
 
561
618
  public getContractClass(id: Fr): Promise<ContractClassPublic | undefined> {
@@ -597,3 +654,254 @@ export class Archiver implements ArchiveSource {
597
654
  return this.store.getContractArtifact(address);
598
655
  }
599
656
  }
657
+
658
+ enum Operation {
659
+ Store,
660
+ Delete,
661
+ }
662
+
663
+ /**
664
+ * A helper class that we use to deal with some of the logic needed when adding blocks.
665
+ *
666
+ * I would have preferred to not have this type. But it is useful for handling the logic that any
667
+ * store would need to include otherwise while exposing fewer functions and logic directly to the archiver.
668
+ */
669
+ class ArchiverStoreHelper
670
+ implements
671
+ Omit<
672
+ ArchiverDataStore,
673
+ | 'addLogs'
674
+ | 'deleteLogs'
675
+ | 'addContractClasses'
676
+ | 'deleteContractClasses'
677
+ | 'addContractInstances'
678
+ | 'deleteContractInstances'
679
+ | 'addFunctions'
680
+ >
681
+ {
682
+ #log = createDebugLogger('aztec:archiver:block-helper');
683
+
684
+ constructor(private readonly store: ArchiverDataStore) {}
685
+
686
+ /**
687
+ * Extracts and stores contract classes out of ContractClassRegistered events emitted by the class registerer contract.
688
+ * @param allLogs - All logs emitted in a bunch of blocks.
689
+ */
690
+ async #updateRegisteredContractClasses(allLogs: UnencryptedL2Log[], blockNum: number, operation: Operation) {
691
+ const contractClasses = ContractClassRegisteredEvent.fromLogs(allLogs, ClassRegistererAddress).map(e =>
692
+ e.toContractClassPublic(),
693
+ );
694
+ if (contractClasses.length > 0) {
695
+ contractClasses.forEach(c => this.#log.verbose(`Registering contract class ${c.id.toString()}`));
696
+ if (operation == Operation.Store) {
697
+ return await this.store.addContractClasses(contractClasses, blockNum);
698
+ } else if (operation == Operation.Delete) {
699
+ return await this.store.deleteContractClasses(contractClasses, blockNum);
700
+ }
701
+ }
702
+ return true;
703
+ }
704
+
705
+ /**
706
+ * Extracts and stores contract instances out of ContractInstanceDeployed events emitted by the canonical deployer contract.
707
+ * @param allLogs - All logs emitted in a bunch of blocks.
708
+ */
709
+ async #updateDeployedContractInstances(allLogs: UnencryptedL2Log[], blockNum: number, operation: Operation) {
710
+ const contractInstances = ContractInstanceDeployedEvent.fromLogs(allLogs).map(e => e.toContractInstance());
711
+ if (contractInstances.length > 0) {
712
+ contractInstances.forEach(c =>
713
+ this.#log.verbose(`${Operation[operation]} contract instance at ${c.address.toString()}`),
714
+ );
715
+ if (operation == Operation.Store) {
716
+ return await this.store.addContractInstances(contractInstances, blockNum);
717
+ } else if (operation == Operation.Delete) {
718
+ return await this.store.deleteContractInstances(contractInstances, blockNum);
719
+ }
720
+ }
721
+ return true;
722
+ }
723
+
724
+ /**
725
+ * Stores the functions that was broadcasted individually
726
+ *
727
+ * @dev Beware that there is not a delete variant of this, since they are added to contract classes
728
+ * and will be deleted as part of the class if needed.
729
+ *
730
+ * @param allLogs - The logs from the block
731
+ * @param _blockNum - The block number
732
+ * @returns
733
+ */
734
+ async #storeBroadcastedIndividualFunctions(allLogs: UnencryptedL2Log[], _blockNum: number) {
735
+ // Filter out private and unconstrained function broadcast events
736
+ const privateFnEvents = PrivateFunctionBroadcastedEvent.fromLogs(allLogs, ClassRegistererAddress);
737
+ const unconstrainedFnEvents = UnconstrainedFunctionBroadcastedEvent.fromLogs(allLogs, ClassRegistererAddress);
738
+
739
+ // Group all events by contract class id
740
+ for (const [classIdString, classEvents] of Object.entries(
741
+ groupBy([...privateFnEvents, ...unconstrainedFnEvents], e => e.contractClassId.toString()),
742
+ )) {
743
+ const contractClassId = Fr.fromString(classIdString);
744
+ const contractClass = await this.getContractClass(contractClassId);
745
+ if (!contractClass) {
746
+ this.#log.warn(`Skipping broadcasted functions as contract class ${contractClassId.toString()} was not found`);
747
+ continue;
748
+ }
749
+
750
+ // Split private and unconstrained functions, and filter out invalid ones
751
+ const allFns = classEvents.map(e => e.toFunctionWithMembershipProof());
752
+ const privateFns = allFns.filter(
753
+ (fn): fn is ExecutablePrivateFunctionWithMembershipProof => 'unconstrainedFunctionsArtifactTreeRoot' in fn,
754
+ );
755
+ const unconstrainedFns = allFns.filter(
756
+ (fn): fn is UnconstrainedFunctionWithMembershipProof => 'privateFunctionsArtifactTreeRoot' in fn,
757
+ );
758
+ const validPrivateFns = privateFns.filter(fn => isValidPrivateFunctionMembershipProof(fn, contractClass));
759
+ const validUnconstrainedFns = unconstrainedFns.filter(fn =>
760
+ isValidUnconstrainedFunctionMembershipProof(fn, contractClass),
761
+ );
762
+ const validFnCount = validPrivateFns.length + validUnconstrainedFns.length;
763
+ if (validFnCount !== allFns.length) {
764
+ this.#log.warn(`Skipping ${allFns.length - validFnCount} invalid functions`);
765
+ }
766
+
767
+ // Store the functions in the contract class in a single operation
768
+ if (validFnCount > 0) {
769
+ this.#log.verbose(`Storing ${validFnCount} functions for contract class ${contractClassId.toString()}`);
770
+ }
771
+ return await this.store.addFunctions(contractClassId, validPrivateFns, validUnconstrainedFns);
772
+ }
773
+ return true;
774
+ }
775
+
776
+ async addBlocks(blocks: L1Published<L2Block>[]): Promise<boolean> {
777
+ return [
778
+ this.store.addLogs(blocks.map(block => block.data)),
779
+ // Unroll all logs emitted during the retrieved blocks and extract any contract classes and instances from them
780
+ ...(await Promise.all(
781
+ blocks.map(async block => {
782
+ const blockLogs = block.data.body.txEffects
783
+ .flatMap(txEffect => (txEffect ? [txEffect.unencryptedLogs] : []))
784
+ .flatMap(txLog => txLog.unrollLogs());
785
+
786
+ return (
787
+ await Promise.all([
788
+ this.#updateRegisteredContractClasses(blockLogs, block.data.number, Operation.Store),
789
+ this.#updateDeployedContractInstances(blockLogs, block.data.number, Operation.Store),
790
+ this.#storeBroadcastedIndividualFunctions(blockLogs, block.data.number),
791
+ ])
792
+ ).every(Boolean);
793
+ }),
794
+ )),
795
+ this.store.addBlocks(blocks),
796
+ ].every(Boolean);
797
+ }
798
+
799
+ async unwindBlocks(from: number, blocksToUnwind: number): Promise<boolean> {
800
+ const last = await this.getSynchedL2BlockNumber();
801
+ if (from != last) {
802
+ throw new Error(`Can only remove from the tip`);
803
+ }
804
+
805
+ // from - blocksToUnwind = the new head, so + 1 for what we need to remove
806
+ const blocks = await this.getBlocks(from - blocksToUnwind + 1, blocksToUnwind);
807
+
808
+ return [
809
+ // Unroll all logs emitted during the retrieved blocks and extract any contract classes and instances from them
810
+ ...(await Promise.all(
811
+ blocks.map(async block => {
812
+ const blockLogs = block.data.body.txEffects
813
+ .flatMap(txEffect => (txEffect ? [txEffect.unencryptedLogs] : []))
814
+ .flatMap(txLog => txLog.unrollLogs());
815
+ await this.#updateRegisteredContractClasses(blockLogs, block.data.number, Operation.Delete);
816
+ await this.#updateDeployedContractInstances(blockLogs, block.data.number, Operation.Delete);
817
+ }),
818
+ )),
819
+ this.store.deleteLogs(blocks.map(b => b.data)),
820
+ this.store.unwindBlocks(from, blocksToUnwind),
821
+ ].every(Boolean);
822
+ }
823
+
824
+ getBlocks(from: number, limit: number): Promise<L1Published<L2Block>[]> {
825
+ return this.store.getBlocks(from, limit);
826
+ }
827
+ getBlockHeaders(from: number, limit: number): Promise<Header[]> {
828
+ return this.store.getBlockHeaders(from, limit);
829
+ }
830
+ getTxEffect(txHash: TxHash): Promise<TxEffect | undefined> {
831
+ return this.store.getTxEffect(txHash);
832
+ }
833
+ getSettledTxReceipt(txHash: TxHash): Promise<TxReceipt | undefined> {
834
+ return this.store.getSettledTxReceipt(txHash);
835
+ }
836
+ addL1ToL2Messages(messages: DataRetrieval<InboxLeaf>): Promise<boolean> {
837
+ return this.store.addL1ToL2Messages(messages);
838
+ }
839
+ getL1ToL2Messages(blockNumber: bigint): Promise<Fr[]> {
840
+ return this.store.getL1ToL2Messages(blockNumber);
841
+ }
842
+ getL1ToL2MessageIndex(l1ToL2Message: Fr, startIndex: bigint): Promise<bigint | undefined> {
843
+ return this.store.getL1ToL2MessageIndex(l1ToL2Message, startIndex);
844
+ }
845
+ getLogs<TLogType extends LogType>(
846
+ from: number,
847
+ limit: number,
848
+ logType: TLogType,
849
+ ): Promise<L2BlockL2Logs<FromLogType<TLogType>>[]> {
850
+ return this.store.getLogs(from, limit, logType);
851
+ }
852
+ getUnencryptedLogs(filter: LogFilter): Promise<GetUnencryptedLogsResponse> {
853
+ return this.store.getUnencryptedLogs(filter);
854
+ }
855
+ getSynchedL2BlockNumber(): Promise<number> {
856
+ return this.store.getSynchedL2BlockNumber();
857
+ }
858
+ getProvenL2BlockNumber(): Promise<number> {
859
+ return this.store.getProvenL2BlockNumber();
860
+ }
861
+ getProvenL2EpochNumber(): Promise<number | undefined> {
862
+ return this.store.getProvenL2EpochNumber();
863
+ }
864
+ setProvenL2BlockNumber(l2BlockNumber: number): Promise<void> {
865
+ return this.store.setProvenL2BlockNumber(l2BlockNumber);
866
+ }
867
+ setProvenL2EpochNumber(l2EpochNumber: number): Promise<void> {
868
+ return this.store.setProvenL2EpochNumber(l2EpochNumber);
869
+ }
870
+ setBlockSynchedL1BlockNumber(l1BlockNumber: bigint): Promise<void> {
871
+ return this.store.setBlockSynchedL1BlockNumber(l1BlockNumber);
872
+ }
873
+ setMessageSynchedL1BlockNumber(l1BlockNumber: bigint): Promise<void> {
874
+ return this.store.setMessageSynchedL1BlockNumber(l1BlockNumber);
875
+ }
876
+ getSynchPoint(): Promise<ArchiverL1SynchPoint> {
877
+ return this.store.getSynchPoint();
878
+ }
879
+ getContractClass(id: Fr): Promise<ContractClassPublic | undefined> {
880
+ return this.store.getContractClass(id);
881
+ }
882
+ getContractInstance(address: AztecAddress): Promise<ContractInstanceWithAddress | undefined> {
883
+ return this.store.getContractInstance(address);
884
+ }
885
+ getContractClassIds(): Promise<Fr[]> {
886
+ return this.store.getContractClassIds();
887
+ }
888
+ addContractArtifact(address: AztecAddress, contract: ContractArtifact): Promise<void> {
889
+ return this.store.addContractArtifact(address, contract);
890
+ }
891
+ getContractArtifact(address: AztecAddress): Promise<ContractArtifact | undefined> {
892
+ return this.store.getContractArtifact(address);
893
+ }
894
+ getTotalL1ToL2MessageCount(): Promise<bigint> {
895
+ return this.store.getTotalL1ToL2MessageCount();
896
+ }
897
+ }
898
+
899
+ type L1RollupConstants = {
900
+ l1StartBlock: bigint;
901
+ l1GenesisTime: bigint;
902
+ };
903
+
904
+ const EmptyL1RollupConstants: L1RollupConstants = {
905
+ l1StartBlock: 0n,
906
+ l1GenesisTime: 0n,
907
+ };