@aztec/archiver 0.86.0-starknet.1 → 0.87.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 (57) hide show
  1. package/dest/archiver/archiver.d.ts +20 -8
  2. package/dest/archiver/archiver.d.ts.map +1 -1
  3. package/dest/archiver/archiver.js +259 -65
  4. package/dest/archiver/archiver_store.d.ts +30 -15
  5. package/dest/archiver/archiver_store.d.ts.map +1 -1
  6. package/dest/archiver/archiver_store_test_suite.d.ts.map +1 -1
  7. package/dest/archiver/archiver_store_test_suite.js +357 -55
  8. package/dest/archiver/data_retrieval.d.ts +5 -3
  9. package/dest/archiver/data_retrieval.d.ts.map +1 -1
  10. package/dest/archiver/data_retrieval.js +41 -23
  11. package/dest/archiver/errors.d.ts +8 -0
  12. package/dest/archiver/errors.d.ts.map +1 -1
  13. package/dest/archiver/errors.js +12 -0
  14. package/dest/archiver/instrumentation.d.ts.map +1 -1
  15. package/dest/archiver/kv_archiver_store/block_store.d.ts +4 -1
  16. package/dest/archiver/kv_archiver_store/block_store.d.ts.map +1 -1
  17. package/dest/archiver/kv_archiver_store/block_store.js +34 -5
  18. package/dest/archiver/kv_archiver_store/contract_instance_store.d.ts +3 -1
  19. package/dest/archiver/kv_archiver_store/contract_instance_store.d.ts.map +1 -1
  20. package/dest/archiver/kv_archiver_store/contract_instance_store.js +17 -3
  21. package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts +19 -11
  22. package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts.map +1 -1
  23. package/dest/archiver/kv_archiver_store/kv_archiver_store.js +30 -10
  24. package/dest/archiver/kv_archiver_store/log_store.d.ts.map +1 -1
  25. package/dest/archiver/kv_archiver_store/log_store.js +1 -1
  26. package/dest/archiver/kv_archiver_store/message_store.d.ts +21 -15
  27. package/dest/archiver/kv_archiver_store/message_store.d.ts.map +1 -1
  28. package/dest/archiver/kv_archiver_store/message_store.js +150 -48
  29. package/dest/archiver/structs/inbox_message.d.ts +14 -0
  30. package/dest/archiver/structs/inbox_message.d.ts.map +1 -0
  31. package/dest/archiver/structs/inbox_message.js +38 -0
  32. package/dest/rpc/index.d.ts +1 -1
  33. package/dest/test/mock_l2_block_source.d.ts +2 -0
  34. package/dest/test/mock_l2_block_source.d.ts.map +1 -1
  35. package/dest/test/mock_l2_block_source.js +8 -1
  36. package/dest/test/mock_structs.d.ts +9 -0
  37. package/dest/test/mock_structs.d.ts.map +1 -0
  38. package/dest/test/mock_structs.js +37 -0
  39. package/package.json +15 -15
  40. package/src/archiver/archiver.ts +297 -79
  41. package/src/archiver/archiver_store.ts +34 -15
  42. package/src/archiver/archiver_store_test_suite.ts +300 -45
  43. package/src/archiver/data_retrieval.ts +47 -30
  44. package/src/archiver/errors.ts +21 -0
  45. package/src/archiver/instrumentation.ts +4 -1
  46. package/src/archiver/kv_archiver_store/block_store.ts +37 -8
  47. package/src/archiver/kv_archiver_store/contract_instance_store.ts +20 -7
  48. package/src/archiver/kv_archiver_store/kv_archiver_store.ts +47 -15
  49. package/src/archiver/kv_archiver_store/log_store.ts +6 -2
  50. package/src/archiver/kv_archiver_store/message_store.ts +209 -53
  51. package/src/archiver/structs/inbox_message.ts +40 -0
  52. package/src/test/mock_l2_block_source.ts +9 -1
  53. package/src/test/mock_structs.ts +49 -0
  54. package/dest/archiver/memory_archiver_store/l1_to_l2_message_store.d.ts +0 -23
  55. package/dest/archiver/memory_archiver_store/l1_to_l2_message_store.d.ts.map +0 -1
  56. package/dest/archiver/memory_archiver_store/l1_to_l2_message_store.js +0 -49
  57. package/src/archiver/memory_archiver_store/l1_to_l2_message_store.ts +0 -61
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/archiver",
3
- "version": "0.86.0-starknet.1",
3
+ "version": "0.87.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dest/index.js",
@@ -62,17 +62,17 @@
62
62
  ]
63
63
  },
64
64
  "dependencies": {
65
- "@aztec/blob-lib": "0.86.0-starknet.1",
66
- "@aztec/blob-sink": "0.86.0-starknet.1",
67
- "@aztec/constants": "0.86.0-starknet.1",
68
- "@aztec/ethereum": "0.86.0-starknet.1",
69
- "@aztec/foundation": "0.86.0-starknet.1",
70
- "@aztec/kv-store": "0.86.0-starknet.1",
71
- "@aztec/l1-artifacts": "0.86.0-starknet.1",
72
- "@aztec/noir-protocol-circuits-types": "0.86.0-starknet.1",
73
- "@aztec/protocol-contracts": "0.86.0-starknet.1",
74
- "@aztec/stdlib": "0.86.0-starknet.1",
75
- "@aztec/telemetry-client": "0.86.0-starknet.1",
65
+ "@aztec/blob-lib": "0.87.0",
66
+ "@aztec/blob-sink": "0.87.0",
67
+ "@aztec/constants": "0.87.0",
68
+ "@aztec/ethereum": "0.87.0",
69
+ "@aztec/foundation": "0.87.0",
70
+ "@aztec/kv-store": "0.87.0",
71
+ "@aztec/l1-artifacts": "0.87.0",
72
+ "@aztec/noir-protocol-circuits-types": "0.87.0",
73
+ "@aztec/protocol-contracts": "0.87.0",
74
+ "@aztec/stdlib": "0.87.0",
75
+ "@aztec/telemetry-client": "0.87.0",
76
76
  "debug": "^4.3.4",
77
77
  "lodash.groupby": "^4.6.0",
78
78
  "lodash.omit": "^4.5.0",
@@ -86,12 +86,12 @@
86
86
  "@types/jest": "^29.5.0",
87
87
  "@types/lodash.groupby": "^4.6.9",
88
88
  "@types/lodash.omit": "^4.5.7",
89
- "@types/node": "^18.15.11",
89
+ "@types/node": "^22.15.17",
90
90
  "concurrently": "^8.0.1",
91
91
  "jest": "^29.5.0",
92
92
  "jest-mock-extended": "^3.0.4",
93
93
  "ts-node": "^10.9.1",
94
- "typescript": "^5.0.4"
94
+ "typescript": "^5.3.3"
95
95
  },
96
96
  "files": [
97
97
  "dest",
@@ -100,6 +100,6 @@
100
100
  ],
101
101
  "types": "./dest/index.d.ts",
102
102
  "engines": {
103
- "node": ">=18"
103
+ "node": ">=20.10"
104
104
  }
105
105
  }
@@ -1,6 +1,14 @@
1
1
  import type { BlobSinkClientInterface } from '@aztec/blob-sink/client';
2
- import { BlockTagTooOldError, RollupContract, type ViemPublicClient, createEthereumChain } from '@aztec/ethereum';
2
+ import {
3
+ BlockTagTooOldError,
4
+ InboxContract,
5
+ type L1BlockId,
6
+ RollupContract,
7
+ type ViemPublicClient,
8
+ createEthereumChain,
9
+ } from '@aztec/ethereum';
3
10
  import { maxBigint } from '@aztec/foundation/bigint';
11
+ import { Buffer16, Buffer32 } from '@aztec/foundation/buffer';
4
12
  import type { EthAddress } from '@aztec/foundation/eth-address';
5
13
  import { Fr } from '@aztec/foundation/fields';
6
14
  import { type Logger, createLogger } from '@aztec/foundation/log';
@@ -8,7 +16,8 @@ import { RunningPromise, makeLoggingErrorHandler } from '@aztec/foundation/runni
8
16
  import { sleep } from '@aztec/foundation/sleep';
9
17
  import { count } from '@aztec/foundation/string';
10
18
  import { elapsed } from '@aztec/foundation/timer';
11
- import { InboxAbi, RollupAbi } from '@aztec/l1-artifacts';
19
+ import type { CustomRange } from '@aztec/kv-store';
20
+ import { RollupAbi } from '@aztec/l1-artifacts';
12
21
  import {
13
22
  ContractClassRegisteredEvent,
14
23
  PrivateFunctionBroadcastedEvent,
@@ -48,24 +57,25 @@ import {
48
57
  import type { GetContractClassLogsResponse, GetPublicLogsResponse } from '@aztec/stdlib/interfaces/client';
49
58
  import type { L2LogsSource } from '@aztec/stdlib/interfaces/server';
50
59
  import { ContractClassLog, type LogFilter, type PrivateLog, type PublicLog, TxScopedL2Log } from '@aztec/stdlib/logs';
51
- import type { InboxLeaf, L1ToL2MessageSource } from '@aztec/stdlib/messaging';
60
+ import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
52
61
  import { type BlockHeader, type IndexedTxEffect, TxHash, TxReceipt } from '@aztec/stdlib/tx';
53
62
  import { Attributes, type TelemetryClient, type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client';
54
63
 
55
64
  import { EventEmitter } from 'events';
56
65
  import groupBy from 'lodash.groupby';
57
- import { type GetContractReturnType, createPublicClient, fallback, getContract, http } from 'viem';
66
+ import { type GetContractReturnType, createPublicClient, fallback, http } from 'viem';
58
67
 
59
68
  import type { ArchiverDataStore, ArchiverL1SynchPoint } from './archiver_store.js';
60
69
  import type { ArchiverConfig } from './config.js';
61
70
  import {
62
71
  retrieveBlocksFromRollup,
72
+ retrieveL1ToL2Message,
63
73
  retrieveL1ToL2Messages,
64
74
  retrievedBlockToPublishedL2Block,
65
75
  } from './data_retrieval.js';
66
- import { NoBlobBodiesFoundError } from './errors.js';
76
+ import { InitialBlockNumberNotSequentialError, NoBlobBodiesFoundError } from './errors.js';
67
77
  import { ArchiverInstrumentation } from './instrumentation.js';
68
- import type { DataRetrieval } from './structs/data_retrieval.js';
78
+ import type { InboxMessage } from './structs/inbox_message.js';
69
79
  import type { PublishedL2Block } from './structs/published.js';
70
80
 
71
81
  /**
@@ -85,7 +95,7 @@ export class Archiver extends EventEmitter implements ArchiveSource, Traceable {
85
95
  private runningPromise?: RunningPromise;
86
96
 
87
97
  private rollup: RollupContract;
88
- private inbox: GetContractReturnType<typeof InboxAbi, ViemPublicClient>;
98
+ private inbox: InboxContract;
89
99
 
90
100
  private store: ArchiverStoreHelper;
91
101
 
@@ -112,7 +122,7 @@ export class Archiver extends EventEmitter implements ArchiveSource, Traceable {
112
122
  private readonly config: { pollingIntervalMs: number; batchSize: number },
113
123
  private readonly blobSinkClient: BlobSinkClientInterface,
114
124
  private readonly instrumentation: ArchiverInstrumentation,
115
- private readonly l1constants: L1RollupConstants,
125
+ private readonly l1constants: L1RollupConstants & { l1StartBlockHash: Buffer32 },
116
126
  private readonly log: Logger = createLogger('archiver'),
117
127
  ) {
118
128
  super();
@@ -121,12 +131,7 @@ export class Archiver extends EventEmitter implements ArchiveSource, Traceable {
121
131
  this.store = new ArchiverStoreHelper(dataStore);
122
132
 
123
133
  this.rollup = new RollupContract(publicClient, l1Addresses.rollupAddress);
124
-
125
- this.inbox = getContract({
126
- address: l1Addresses.inboxAddress.toString(),
127
- abi: InboxAbi,
128
- client: publicClient,
129
- });
134
+ this.inbox = new InboxContract(publicClient, l1Addresses.inboxAddress);
130
135
  }
131
136
 
132
137
  /**
@@ -156,6 +161,10 @@ export class Archiver extends EventEmitter implements ArchiveSource, Traceable {
156
161
  rollup.getL1GenesisTime(),
157
162
  ] as const);
158
163
 
164
+ const l1StartBlockHash = await publicClient
165
+ .getBlock({ blockNumber: l1StartBlock, includeTransactions: false })
166
+ .then(block => Buffer32.fromString(block.hash));
167
+
159
168
  const {
160
169
  aztecEpochDuration: epochDuration,
161
170
  aztecSlotDuration: slotDuration,
@@ -163,17 +172,29 @@ export class Archiver extends EventEmitter implements ArchiveSource, Traceable {
163
172
  aztecProofSubmissionWindow: proofSubmissionWindow,
164
173
  } = config;
165
174
 
175
+ const l1Constants = {
176
+ l1StartBlockHash,
177
+ l1StartBlock,
178
+ l1GenesisTime,
179
+ epochDuration,
180
+ slotDuration,
181
+ ethereumSlotDuration,
182
+ proofSubmissionWindow,
183
+ };
184
+
185
+ const opts = {
186
+ pollingIntervalMs: config.archiverPollingIntervalMS ?? 10_000,
187
+ batchSize: config.archiverBatchSize ?? 100,
188
+ };
189
+
166
190
  const archiver = new Archiver(
167
191
  publicClient,
168
192
  config.l1Contracts,
169
193
  archiverStore,
170
- {
171
- pollingIntervalMs: config.archiverPollingIntervalMS ?? 10_000,
172
- batchSize: config.archiverBatchSize ?? 100,
173
- },
194
+ opts,
174
195
  deps.blobSinkClient,
175
196
  await ArchiverInstrumentation.new(deps.telemetry, () => archiverStore.estimateSize()),
176
- { l1StartBlock, l1GenesisTime, epochDuration, slotDuration, ethereumSlotDuration, proofSubmissionWindow },
197
+ l1Constants,
177
198
  );
178
199
  await archiver.start(blockUntilSynced);
179
200
  return archiver;
@@ -252,16 +273,21 @@ export class Archiver extends EventEmitter implements ArchiveSource, Traceable {
252
273
  *
253
274
  * This code does not handle reorgs.
254
275
  */
255
- const { l1StartBlock } = this.l1constants;
256
- const { blocksSynchedTo = l1StartBlock, messagesSynchedTo = l1StartBlock } = await this.store.getSynchPoint();
257
- const currentL1BlockNumber = await this.publicClient.getBlockNumber();
276
+ const { l1StartBlock, l1StartBlockHash } = this.l1constants;
277
+ const {
278
+ blocksSynchedTo = l1StartBlock,
279
+ messagesSynchedTo = { l1BlockNumber: l1StartBlock, l1BlockHash: l1StartBlockHash },
280
+ } = await this.store.getSynchPoint();
281
+
282
+ const currentL1Block = await this.publicClient.getBlock({ includeTransactions: false });
283
+ const currentL1BlockNumber = currentL1Block.number;
284
+ const currentL1BlockHash = Buffer32.fromString(currentL1Block.hash);
258
285
 
259
286
  if (initialRun) {
260
287
  this.log.info(
261
- `Starting archiver sync to rollup contract ${this.l1Addresses.rollupAddress.toString()} from L1 block ${Math.min(
262
- Number(blocksSynchedTo),
263
- Number(messagesSynchedTo),
264
- )} to current L1 block ${currentL1BlockNumber}`,
288
+ `Starting archiver sync to rollup contract ${this.l1Addresses.rollupAddress.toString()} from L1 block ${blocksSynchedTo}` +
289
+ ` to current L1 block ${currentL1BlockNumber} with hash ${currentL1BlockHash.toString()}`,
290
+ { blocksSynchedTo, messagesSynchedTo },
265
291
  );
266
292
  }
267
293
 
@@ -285,7 +311,7 @@ export class Archiver extends EventEmitter implements ArchiveSource, Traceable {
285
311
  */
286
312
 
287
313
  // ********** Events that are processed per L1 block **********
288
- await this.handleL1ToL2Messages(messagesSynchedTo, currentL1BlockNumber);
314
+ await this.handleL1ToL2Messages(messagesSynchedTo, currentL1BlockNumber, currentL1BlockHash);
289
315
 
290
316
  // Get L1 timestamp for the current block
291
317
  const currentL1Timestamp =
@@ -401,38 +427,149 @@ export class Archiver extends EventEmitter implements ArchiveSource, Traceable {
401
427
  return [nextStart, nextEnd];
402
428
  }
403
429
 
404
- private async handleL1ToL2Messages(messagesSynchedTo: bigint, currentL1BlockNumber: bigint) {
405
- this.log.trace(`Handling L1 to L2 messages from ${messagesSynchedTo} to ${currentL1BlockNumber}.`);
406
- if (currentL1BlockNumber <= messagesSynchedTo) {
430
+ private async handleL1ToL2Messages(
431
+ messagesSyncPoint: L1BlockId,
432
+ currentL1BlockNumber: bigint,
433
+ currentL1BlockHash: Buffer32,
434
+ ) {
435
+ this.log.trace(`Handling L1 to L2 messages from ${messagesSyncPoint.l1BlockNumber} to ${currentL1BlockNumber}.`);
436
+ if (currentL1BlockNumber <= messagesSyncPoint.l1BlockNumber) {
407
437
  return;
408
438
  }
409
439
 
410
- const localTotalMessageCount = await this.store.getTotalL1ToL2MessageCount();
411
- const destinationTotalMessageCount = await this.inbox.read.totalMessagesInserted();
440
+ // Load remote and local inbox states.
441
+ const localMessagesInserted = await this.store.getTotalL1ToL2MessageCount();
442
+ const localLastMessage = await this.store.getLastL1ToL2Message();
443
+ const remoteMessagesState = await this.inbox.getState({ blockNumber: currentL1BlockNumber });
412
444
 
413
- if (localTotalMessageCount === destinationTotalMessageCount) {
414
- await this.store.setMessageSynchedL1BlockNumber(currentL1BlockNumber);
415
- this.log.trace(
416
- `Retrieved no new L1 to L2 messages between L1 blocks ${messagesSynchedTo + 1n} and ${currentL1BlockNumber}.`,
445
+ this.log.trace(`Retrieved remote inbox state at L1 block ${currentL1BlockNumber}.`, {
446
+ localMessagesInserted,
447
+ localLastMessage,
448
+ remoteMessagesState,
449
+ });
450
+
451
+ // Compare message count and rolling hash. If they match, no need to retrieve anything.
452
+ if (
453
+ remoteMessagesState.totalMessagesInserted === localMessagesInserted &&
454
+ remoteMessagesState.messagesRollingHash.equals(localLastMessage?.rollingHash ?? Buffer16.ZERO)
455
+ ) {
456
+ this.log.debug(
457
+ `No L1 to L2 messages to query between L1 blocks ${messagesSyncPoint.l1BlockNumber} and ${currentL1BlockNumber}.`,
417
458
  );
459
+ await this.store.setMessageSynchedL1Block({
460
+ l1BlockHash: currentL1BlockHash,
461
+ l1BlockNumber: currentL1BlockNumber,
462
+ });
418
463
  return;
419
464
  }
420
465
 
421
- // Retrieve messages in batches. Each batch is estimated to acommodate up to L2 'blockBatchSize' blocks,
422
- let searchStartBlock: bigint = messagesSynchedTo;
423
- let searchEndBlock: bigint = messagesSynchedTo;
466
+ // Check if our syncpoint is still valid. If not, there was an L1 reorg and we need to re-retrieve messages.
467
+ // Note that we need to fetch it from logs and not from inbox state at the syncpoint l1 block number, since it
468
+ // could be older than 128 blocks and non-archive nodes cannot resolve it.
469
+ if (localLastMessage) {
470
+ const remoteLastMessage = await this.retrieveL1ToL2Message(localLastMessage.leaf);
471
+ this.log.trace(`Retrieved remote message for local last`, { remoteLastMessage, localLastMessage });
472
+ if (!remoteLastMessage || !remoteLastMessage.rollingHash.equals(localLastMessage.rollingHash)) {
473
+ this.log.warn(`Rolling back L1 to L2 messages due to hash mismatch or msg not found.`, {
474
+ remoteLastMessage,
475
+ messagesSyncPoint,
476
+ localLastMessage,
477
+ });
478
+
479
+ messagesSyncPoint = await this.rollbackL1ToL2Messages(localLastMessage, messagesSyncPoint);
480
+ this.log.debug(`Rolled back L1 to L2 messages to L1 block ${messagesSyncPoint.l1BlockNumber}.`, {
481
+ messagesSyncPoint,
482
+ });
483
+ }
484
+ }
485
+
486
+ // Retrieve and save messages in batches. Each batch is estimated to acommodate up to L2 'blockBatchSize' blocks,
487
+ let searchStartBlock: bigint = 0n;
488
+ let searchEndBlock: bigint = messagesSyncPoint.l1BlockNumber;
489
+
490
+ let lastMessage: InboxMessage | undefined;
491
+ let messageCount = 0;
492
+
424
493
  do {
425
494
  [searchStartBlock, searchEndBlock] = this.nextRange(searchEndBlock, currentL1BlockNumber);
426
495
  this.log.trace(`Retrieving L1 to L2 messages between L1 blocks ${searchStartBlock} and ${searchEndBlock}.`);
427
- const retrievedL1ToL2Messages = await retrieveL1ToL2Messages(this.inbox, searchStartBlock, searchEndBlock);
496
+ const messages = await retrieveL1ToL2Messages(this.inbox.getContract(), searchStartBlock, searchEndBlock);
428
497
  this.log.verbose(
429
- `Retrieved ${retrievedL1ToL2Messages.retrievedData.length} new L1 to L2 messages between L1 blocks ${searchStartBlock} and ${searchEndBlock}.`,
498
+ `Retrieved ${messages.length} new L1 to L2 messages between L1 blocks ${searchStartBlock} and ${searchEndBlock}.`,
430
499
  );
431
- await this.store.addL1ToL2Messages(retrievedL1ToL2Messages);
432
- for (const msg of retrievedL1ToL2Messages.retrievedData) {
433
- this.log.debug(`Downloaded L1 to L2 message`, { leaf: msg.leaf.toString(), index: msg.index });
500
+ await this.store.addL1ToL2Messages(messages);
501
+ for (const msg of messages) {
502
+ this.log.debug(`Downloaded L1 to L2 message`, { ...msg, leaf: msg.leaf.toString() });
503
+ lastMessage = msg;
504
+ messageCount++;
434
505
  }
435
506
  } while (searchEndBlock < currentL1BlockNumber);
507
+
508
+ // Log stats for messages retrieved (if any).
509
+ if (messageCount > 0) {
510
+ this.log.info(
511
+ `Retrieved ${messageCount} new L1 to L2 messages up to message with index ${lastMessage?.index} for L2 block ${lastMessage?.l2BlockNumber}`,
512
+ { lastMessage, messageCount },
513
+ );
514
+ }
515
+
516
+ // Warn if the resulting rolling hash does not match the remote state we had retrieved.
517
+ if (lastMessage && !lastMessage.rollingHash.equals(remoteMessagesState.messagesRollingHash)) {
518
+ this.log.warn(`Last message retrieved rolling hash does not match remote state.`, {
519
+ lastMessage,
520
+ remoteMessagesState,
521
+ });
522
+ }
523
+ }
524
+
525
+ private retrieveL1ToL2Message(leaf: Fr): Promise<InboxMessage | undefined> {
526
+ return retrieveL1ToL2Message(this.inbox.getContract(), leaf, this.l1constants.l1StartBlock);
527
+ }
528
+
529
+ private async rollbackL1ToL2Messages(localLastMessage: InboxMessage, messagesSyncPoint: L1BlockId) {
530
+ // Slowly go back through our messages until we find the last common message.
531
+ // We could query the logs in batch as an optimization, but the depth of the reorg should not be deep, and this
532
+ // is a very rare case, so it's fine to query one log at a time.
533
+ let commonMsg: undefined | InboxMessage;
534
+ this.log.verbose(`Searching most recent common L1 to L2 message at or before index ${localLastMessage.index}`);
535
+ for await (const msg of this.store.iterateL1ToL2Messages({ reverse: true, end: localLastMessage.index })) {
536
+ const remoteMsg = await this.retrieveL1ToL2Message(msg.leaf);
537
+ const logCtx = { remoteMsg, localMsg: msg };
538
+ if (remoteMsg && remoteMsg.rollingHash.equals(msg.rollingHash)) {
539
+ this.log.verbose(
540
+ `Found most recent common L1 to L2 message at index ${msg.index} on L1 block ${msg.l1BlockNumber}`,
541
+ logCtx,
542
+ );
543
+ commonMsg = remoteMsg;
544
+ break;
545
+ } else if (remoteMsg) {
546
+ this.log.debug(`Local L1 to L2 message with index ${msg.index} has different rolling hash`, logCtx);
547
+ } else {
548
+ this.log.debug(`Local L1 to L2 message with index ${msg.index} not found on L1`, logCtx);
549
+ }
550
+ }
551
+
552
+ // Delete everything after the common message we found.
553
+ const lastGoodIndex = commonMsg?.index;
554
+ this.log.warn(`Deleting all local L1 to L2 messages after index ${lastGoodIndex ?? 'undefined'}`);
555
+ await this.store.removeL1ToL2Messages(lastGoodIndex !== undefined ? lastGoodIndex + 1n : 0n);
556
+
557
+ // Update the syncpoint so the loop below reprocesses the changed messages. We go to the block before
558
+ // the last common one, so we force reprocessing it, in case new messages were added on that same L1 block
559
+ // after the last common message.
560
+ const syncPointL1BlockNumber = commonMsg ? commonMsg.l1BlockNumber - 1n : this.l1constants.l1StartBlock;
561
+ const syncPointL1BlockHash = await this.getL1BlockHash(syncPointL1BlockNumber);
562
+ messagesSyncPoint = { l1BlockNumber: syncPointL1BlockNumber, l1BlockHash: syncPointL1BlockHash };
563
+ await this.store.setMessageSynchedL1Block(messagesSyncPoint);
564
+ return messagesSyncPoint;
565
+ }
566
+
567
+ private async getL1BlockHash(l1BlockNumber: bigint): Promise<Buffer32> {
568
+ const block = await this.publicClient.getBlock({ blockNumber: l1BlockNumber, includeTransactions: false });
569
+ if (!block) {
570
+ throw new Error(`Missing L1 block ${l1BlockNumber}`);
571
+ }
572
+ return Buffer32.fromString(block.hash);
436
573
  }
437
574
 
438
575
  private async handleL2blocks(blocksSynchedTo: bigint, currentL1BlockNumber: bigint) {
@@ -607,11 +744,32 @@ export class Archiver extends EventEmitter implements ArchiveSource, Traceable {
607
744
  });
608
745
  }
609
746
 
610
- const [processDuration] = await elapsed(() => this.store.addBlocks(publishedBlocks));
611
- this.instrumentation.processNewBlocks(
612
- processDuration / publishedBlocks.length,
613
- publishedBlocks.map(b => b.block),
614
- );
747
+ try {
748
+ const [processDuration] = await elapsed(() => this.store.addBlocks(publishedBlocks));
749
+ this.instrumentation.processNewBlocks(
750
+ processDuration / publishedBlocks.length,
751
+ publishedBlocks.map(b => b.block),
752
+ );
753
+ } catch (err) {
754
+ if (err instanceof InitialBlockNumberNotSequentialError) {
755
+ const { previousBlockNumber, newBlockNumber } = err;
756
+ const previousBlock = previousBlockNumber
757
+ ? await this.store.getPublishedBlock(previousBlockNumber)
758
+ : undefined;
759
+ const updatedL1SyncPoint = previousBlock?.l1.blockNumber ?? this.l1constants.l1StartBlock;
760
+ await this.store.setBlockSynchedL1BlockNumber(updatedL1SyncPoint);
761
+ this.log.warn(
762
+ `Attempting to insert block ${newBlockNumber} with previous block ${previousBlockNumber}. Rolling back L1 sync point to ${updatedL1SyncPoint} to try and fetch the missing blocks.`,
763
+ {
764
+ previousBlockNumber,
765
+ previousBlockHash: await previousBlock?.block.hash(),
766
+ newBlockNumber,
767
+ updatedL1SyncPoint,
768
+ },
769
+ );
770
+ }
771
+ throw err;
772
+ }
615
773
 
616
774
  for (const block of publishedBlocks) {
617
775
  this.log.info(`Downloaded L2 block ${block.block.number}`, {
@@ -619,6 +777,8 @@ export class Archiver extends EventEmitter implements ArchiveSource, Traceable {
619
777
  blockNumber: block.block.number,
620
778
  txCount: block.block.body.txEffects.length,
621
779
  globalVariables: block.block.header.globalVariables.toInspect(),
780
+ archiveRoot: block.block.archive.root.toString(),
781
+ archiveNextLeafIndex: block.block.archive.nextAvailableLeafIndex,
622
782
  });
623
783
  }
624
784
  lastRetrievedBlock = publishedBlocks.at(-1) ?? lastRetrievedBlock;
@@ -837,11 +997,11 @@ export class Archiver extends EventEmitter implements ArchiveSource, Traceable {
837
997
  if (number < 0) {
838
998
  number = await this.store.getSynchedL2BlockNumber();
839
999
  }
840
- if (number == 0) {
1000
+ if (number === 0) {
841
1001
  return undefined;
842
1002
  }
843
- const blocks = await this.store.getPublishedBlocks(number, 1);
844
- return blocks.length === 0 ? undefined : blocks[0].block;
1003
+ const publishedBlock = await this.store.getPublishedBlock(number);
1004
+ return publishedBlock?.block;
845
1005
  }
846
1006
 
847
1007
  public async getBlockHeader(number: number | 'latest'): Promise<BlockHeader | undefined> {
@@ -1014,6 +1174,40 @@ export class Archiver extends EventEmitter implements ArchiveSource, Traceable {
1014
1174
  } as L2BlockId,
1015
1175
  };
1016
1176
  }
1177
+
1178
+ public async rollbackTo(targetL2BlockNumber: number): Promise<void> {
1179
+ const currentBlocks = await this.getL2Tips();
1180
+ const currentL2Block = currentBlocks.latest.number;
1181
+ const currentProvenBlock = currentBlocks.proven.number;
1182
+ // const currentFinalizedBlock = currentBlocks.finalized.number;
1183
+
1184
+ if (targetL2BlockNumber >= currentL2Block) {
1185
+ throw new Error(`Target L2 block ${targetL2BlockNumber} must be less than current L2 block ${currentL2Block}`);
1186
+ }
1187
+ const blocksToUnwind = currentL2Block - targetL2BlockNumber;
1188
+ const targetL2Block = await this.store.getPublishedBlock(targetL2BlockNumber);
1189
+ if (!targetL2Block) {
1190
+ throw new Error(`Target L2 block ${targetL2BlockNumber} not found`);
1191
+ }
1192
+ const targetL1BlockNumber = targetL2Block.l1.blockNumber;
1193
+ const targetL1BlockHash = await this.getL1BlockHash(targetL1BlockNumber);
1194
+ this.log.info(`Unwinding ${blocksToUnwind} blocks from L2 block ${currentL2Block}`);
1195
+ await this.store.unwindBlocks(currentL2Block, blocksToUnwind);
1196
+ this.log.info(`Unwinding L1 to L2 messages to ${targetL2BlockNumber}`);
1197
+ await this.store.rollbackL1ToL2MessagesToL2Block(targetL2BlockNumber);
1198
+ this.log.info(`Setting L1 syncpoints to ${targetL1BlockNumber}`);
1199
+ await this.store.setBlockSynchedL1BlockNumber(targetL1BlockNumber);
1200
+ await this.store.setMessageSynchedL1Block({ l1BlockNumber: targetL1BlockNumber, l1BlockHash: targetL1BlockHash });
1201
+ if (targetL2BlockNumber < currentProvenBlock) {
1202
+ this.log.info(`Clearing proven L2 block number`);
1203
+ await this.store.setProvenL2BlockNumber(0);
1204
+ }
1205
+ // TODO(palla/reorg): Set the finalized block when we add support for it.
1206
+ // if (targetL2BlockNumber < currentFinalizedBlock) {
1207
+ // this.log.info(`Clearing finalized L2 block number`);
1208
+ // await this.store.setFinalizedL2BlockNumber(0);
1209
+ // }
1210
+ }
1017
1211
  }
1018
1212
 
1019
1213
  enum Operation {
@@ -1042,6 +1236,7 @@ export class ArchiverStoreHelper
1042
1236
  | 'addFunctions'
1043
1237
  | 'backupTo'
1044
1238
  | 'close'
1239
+ | 'transactionAsync'
1045
1240
  >
1046
1241
  {
1047
1242
  #log = createLogger('archiver:block-helper');
@@ -1182,34 +1377,42 @@ export class ArchiverStoreHelper
1182
1377
  return true;
1183
1378
  }
1184
1379
 
1185
- async addBlocks(blocks: PublishedL2Block[]): Promise<boolean> {
1186
- const opResults = await Promise.all([
1187
- this.store.addLogs(blocks.map(block => block.block)),
1188
- // Unroll all logs emitted during the retrieved blocks and extract any contract classes and instances from them
1189
- ...blocks.map(async block => {
1190
- const contractClassLogs = block.block.body.txEffects.flatMap(txEffect => txEffect.contractClassLogs);
1191
- // ContractInstanceDeployed event logs are broadcast in privateLogs.
1192
- const privateLogs = block.block.body.txEffects.flatMap(txEffect => txEffect.privateLogs);
1193
- const publicLogs = block.block.body.txEffects.flatMap(txEffect => txEffect.publicLogs);
1194
- return (
1195
- await Promise.all([
1196
- this.#updateRegisteredContractClasses(contractClassLogs, block.block.number, Operation.Store),
1197
- this.#updateDeployedContractInstances(privateLogs, block.block.number, Operation.Store),
1198
- this.#updateUpdatedContractInstances(publicLogs, block.block.number, Operation.Store),
1199
- this.#storeBroadcastedIndividualFunctions(contractClassLogs, block.block.number),
1200
- ])
1201
- ).every(Boolean);
1202
- }),
1203
- this.store.addBlocks(blocks),
1204
- ]);
1205
-
1206
- return opResults.every(Boolean);
1380
+ public addBlocks(blocks: PublishedL2Block[]): Promise<boolean> {
1381
+ // Add the blocks to the store. Store will throw if the blocks are not in order, there are gaps,
1382
+ // or if the previous block is not in the store.
1383
+ return this.store.transactionAsync(async () => {
1384
+ await this.store.addBlocks(blocks);
1385
+
1386
+ const opResults = await Promise.all([
1387
+ this.store.addLogs(blocks.map(block => block.block)),
1388
+ // Unroll all logs emitted during the retrieved blocks and extract any contract classes and instances from them
1389
+ ...blocks.map(async block => {
1390
+ const contractClassLogs = block.block.body.txEffects.flatMap(txEffect => txEffect.contractClassLogs);
1391
+ // ContractInstanceDeployed event logs are broadcast in privateLogs.
1392
+ const privateLogs = block.block.body.txEffects.flatMap(txEffect => txEffect.privateLogs);
1393
+ const publicLogs = block.block.body.txEffects.flatMap(txEffect => txEffect.publicLogs);
1394
+ return (
1395
+ await Promise.all([
1396
+ this.#updateRegisteredContractClasses(contractClassLogs, block.block.number, Operation.Store),
1397
+ this.#updateDeployedContractInstances(privateLogs, block.block.number, Operation.Store),
1398
+ this.#updateUpdatedContractInstances(publicLogs, block.block.number, Operation.Store),
1399
+ this.#storeBroadcastedIndividualFunctions(contractClassLogs, block.block.number),
1400
+ ])
1401
+ ).every(Boolean);
1402
+ }),
1403
+ ]);
1404
+
1405
+ return opResults.every(Boolean);
1406
+ });
1207
1407
  }
1208
1408
 
1209
- async unwindBlocks(from: number, blocksToUnwind: number): Promise<boolean> {
1409
+ public async unwindBlocks(from: number, blocksToUnwind: number): Promise<boolean> {
1210
1410
  const last = await this.getSynchedL2BlockNumber();
1211
1411
  if (from != last) {
1212
- throw new Error(`Can only remove from the tip`);
1412
+ throw new Error(`Cannot unwind blocks from block ${from} when the last block is ${last}`);
1413
+ }
1414
+ if (blocksToUnwind <= 0) {
1415
+ throw new Error(`Cannot unwind ${blocksToUnwind} blocks`);
1213
1416
  }
1214
1417
 
1215
1418
  // from - blocksToUnwind = the new head, so + 1 for what we need to remove
@@ -1242,6 +1445,9 @@ export class ArchiverStoreHelper
1242
1445
  getPublishedBlocks(from: number, limit: number): Promise<PublishedL2Block[]> {
1243
1446
  return this.store.getPublishedBlocks(from, limit);
1244
1447
  }
1448
+ getPublishedBlock(number: number): Promise<PublishedL2Block | undefined> {
1449
+ return this.store.getPublishedBlock(number);
1450
+ }
1245
1451
  getBlockHeaders(from: number, limit: number): Promise<BlockHeader[]> {
1246
1452
  return this.store.getBlockHeaders(from, limit);
1247
1453
  }
@@ -1251,7 +1457,7 @@ export class ArchiverStoreHelper
1251
1457
  getSettledTxReceipt(txHash: TxHash): Promise<TxReceipt | undefined> {
1252
1458
  return this.store.getSettledTxReceipt(txHash);
1253
1459
  }
1254
- addL1ToL2Messages(messages: DataRetrieval<InboxLeaf>): Promise<boolean> {
1460
+ addL1ToL2Messages(messages: InboxMessage[]): Promise<void> {
1255
1461
  return this.store.addL1ToL2Messages(messages);
1256
1462
  }
1257
1463
  getL1ToL2Messages(blockNumber: bigint): Promise<Fr[]> {
@@ -1284,8 +1490,8 @@ export class ArchiverStoreHelper
1284
1490
  setBlockSynchedL1BlockNumber(l1BlockNumber: bigint): Promise<void> {
1285
1491
  return this.store.setBlockSynchedL1BlockNumber(l1BlockNumber);
1286
1492
  }
1287
- setMessageSynchedL1BlockNumber(l1BlockNumber: bigint): Promise<void> {
1288
- return this.store.setMessageSynchedL1BlockNumber(l1BlockNumber);
1493
+ setMessageSynchedL1Block(l1Block: L1BlockId): Promise<void> {
1494
+ return this.store.setMessageSynchedL1Block(l1Block);
1289
1495
  }
1290
1496
  getSynchPoint(): Promise<ArchiverL1SynchPoint> {
1291
1497
  return this.store.getSynchPoint();
@@ -1314,4 +1520,16 @@ export class ArchiverStoreHelper
1314
1520
  estimateSize(): Promise<{ mappingSize: number; physicalFileSize: number; actualSize: number; numItems: number }> {
1315
1521
  return this.store.estimateSize();
1316
1522
  }
1523
+ rollbackL1ToL2MessagesToL2Block(targetBlockNumber: number | bigint): Promise<void> {
1524
+ return this.store.rollbackL1ToL2MessagesToL2Block(targetBlockNumber);
1525
+ }
1526
+ iterateL1ToL2Messages(range: CustomRange<bigint> = {}): AsyncIterableIterator<InboxMessage> {
1527
+ return this.store.iterateL1ToL2Messages(range);
1528
+ }
1529
+ removeL1ToL2Messages(startIndex: bigint): Promise<void> {
1530
+ return this.store.removeL1ToL2Messages(startIndex);
1531
+ }
1532
+ getLastL1ToL2Message(): Promise<InboxMessage | undefined> {
1533
+ return this.store.getLastL1ToL2Message();
1534
+ }
1317
1535
  }