@aztec/archiver 0.86.0 → 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 +62 -5
  2. package/dest/archiver/archiver.d.ts.map +1 -1
  3. package/dest/archiver/archiver.js +362 -91
  4. package/dest/archiver/archiver_store.d.ts +33 -17
  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 +364 -62
  8. package/dest/archiver/data_retrieval.d.ts +23 -14
  9. package/dest/archiver/data_retrieval.d.ts.map +1 -1
  10. package/dest/archiver/data_retrieval.js +86 -38
  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 +43 -6
  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 +24 -14
  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 +37 -11
  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 +431 -108
  41. package/src/archiver/archiver_store.ts +37 -18
  42. package/src/archiver/archiver_store_test_suite.ts +307 -52
  43. package/src/archiver/data_retrieval.ts +130 -53
  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 +46 -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 +61 -17
  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
@@ -1,18 +1,17 @@
1
1
  import { Blob, BlobDeserializationError } from '@aztec/blob-lib';
2
2
  import type { BlobSinkClientInterface } from '@aztec/blob-sink/client';
3
- import type { EpochProofPublicInputArgs, ViemPublicClient } from '@aztec/ethereum';
3
+ import type { EpochProofPublicInputArgs, ViemClient, ViemPublicClient } from '@aztec/ethereum';
4
4
  import { asyncPool } from '@aztec/foundation/async-pool';
5
+ import { Buffer16, Buffer32 } from '@aztec/foundation/buffer';
5
6
  import type { EthAddress } from '@aztec/foundation/eth-address';
6
7
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
7
8
  import { Fr } from '@aztec/foundation/fields';
8
9
  import { type Logger, createLogger } from '@aztec/foundation/log';
9
- import { numToUInt32BE } from '@aztec/foundation/serialize';
10
10
  import { ForwarderAbi, type InboxAbi, RollupAbi } from '@aztec/l1-artifacts';
11
11
  import { Body, L2Block } from '@aztec/stdlib/block';
12
- import { InboxLeaf } from '@aztec/stdlib/messaging';
13
12
  import { Proof } from '@aztec/stdlib/proofs';
14
13
  import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
15
- import { BlockHeader } from '@aztec/stdlib/tx';
14
+ import { BlockHeader, GlobalVariables, ProposedBlockHeader, StateReference } from '@aztec/stdlib/tx';
16
15
 
17
16
  import {
18
17
  type GetContractEventsReturnType,
@@ -25,8 +24,68 @@ import {
25
24
 
26
25
  import { NoBlobBodiesFoundError } from './errors.js';
27
26
  import type { DataRetrieval } from './structs/data_retrieval.js';
27
+ import type { InboxMessage } from './structs/inbox_message.js';
28
28
  import type { L1PublishedData, PublishedL2Block } from './structs/published.js';
29
29
 
30
+ export type RetrievedL2Block = {
31
+ l2BlockNumber: bigint;
32
+ archiveRoot: Fr;
33
+ stateReference: StateReference;
34
+ header: ProposedBlockHeader;
35
+ body: Body;
36
+ l1: L1PublishedData;
37
+ chainId: Fr;
38
+ version: Fr;
39
+ signatures: Signature[];
40
+ };
41
+
42
+ export function retrievedBlockToPublishedL2Block(retrievedBlock: RetrievedL2Block): PublishedL2Block {
43
+ const {
44
+ l2BlockNumber,
45
+ archiveRoot,
46
+ stateReference,
47
+ header: proposedHeader,
48
+ body,
49
+ l1,
50
+ chainId,
51
+ version,
52
+ signatures,
53
+ } = retrievedBlock;
54
+
55
+ const archive = new AppendOnlyTreeSnapshot(
56
+ archiveRoot,
57
+ Number(l2BlockNumber + 1n), // nextAvailableLeafIndex
58
+ );
59
+
60
+ const globalVariables = GlobalVariables.from({
61
+ chainId,
62
+ version,
63
+ blockNumber: new Fr(l2BlockNumber),
64
+ slotNumber: proposedHeader.slotNumber,
65
+ timestamp: new Fr(proposedHeader.timestamp),
66
+ coinbase: proposedHeader.coinbase,
67
+ feeRecipient: proposedHeader.feeRecipient,
68
+ gasFees: proposedHeader.gasFees,
69
+ });
70
+
71
+ const header = BlockHeader.from({
72
+ lastArchive: new AppendOnlyTreeSnapshot(proposedHeader.lastArchiveRoot, Number(l2BlockNumber)),
73
+ contentCommitment: proposedHeader.contentCommitment,
74
+ state: stateReference,
75
+ globalVariables,
76
+ totalFees: body.txEffects.reduce((accum, txEffect) => accum.add(txEffect.transactionFee), Fr.ZERO),
77
+ totalManaUsed: proposedHeader.totalManaUsed,
78
+ });
79
+
80
+ const block = new L2Block(archive, header, body);
81
+
82
+ return {
83
+ block,
84
+ l1,
85
+ signatures,
86
+ };
87
+ }
88
+
30
89
  /**
31
90
  * Fetches new L2 blocks.
32
91
  * @param publicClient - The viem public client to use for transaction retrieval.
@@ -43,8 +102,11 @@ export async function retrieveBlocksFromRollup(
43
102
  searchStartBlock: bigint,
44
103
  searchEndBlock: bigint,
45
104
  logger: Logger = createLogger('archiver'),
46
- ): Promise<PublishedL2Block[]> {
47
- const retrievedBlocks: PublishedL2Block[] = [];
105
+ ): Promise<RetrievedL2Block[]> {
106
+ const retrievedBlocks: RetrievedL2Block[] = [];
107
+
108
+ let rollupConstants: { chainId: Fr; version: Fr } | undefined;
109
+
48
110
  do {
49
111
  if (searchStartBlock > searchEndBlock) {
50
112
  break;
@@ -68,11 +130,17 @@ export async function retrieveBlocksFromRollup(
68
130
  `Got ${l2BlockProposedLogs.length} L2 block processed logs for L2 blocks ${l2BlockProposedLogs[0].args.blockNumber}-${lastLog.args.blockNumber} between L1 blocks ${searchStartBlock}-${searchEndBlock}`,
69
131
  );
70
132
 
133
+ if (rollupConstants === undefined) {
134
+ const [chainId, version] = await Promise.all([publicClient.getChainId(), rollup.read.getVersion()]);
135
+ rollupConstants = { chainId: new Fr(chainId), version: new Fr(version) };
136
+ }
137
+
71
138
  const newBlocks = await processL2BlockProposedLogs(
72
139
  rollup,
73
140
  publicClient,
74
141
  blobSinkClient,
75
142
  l2BlockProposedLogs,
143
+ rollupConstants,
76
144
  logger,
77
145
  );
78
146
  retrievedBlocks.push(...newBlocks);
@@ -90,14 +158,15 @@ export async function retrieveBlocksFromRollup(
90
158
  * @param logs - L2BlockProposed logs.
91
159
  * @returns - An array blocks.
92
160
  */
93
- export async function processL2BlockProposedLogs(
161
+ async function processL2BlockProposedLogs(
94
162
  rollup: GetContractReturnType<typeof RollupAbi, ViemPublicClient>,
95
163
  publicClient: ViemPublicClient,
96
164
  blobSinkClient: BlobSinkClientInterface,
97
165
  logs: GetContractEventsReturnType<typeof RollupAbi, 'L2BlockProposed'>,
166
+ { chainId, version }: { chainId: Fr; version: Fr },
98
167
  logger: Logger,
99
- ): Promise<PublishedL2Block[]> {
100
- const retrievedBlocks: PublishedL2Block[] = [];
168
+ ): Promise<RetrievedL2Block[]> {
169
+ const retrievedBlocks: RetrievedL2Block[] = [];
101
170
  await asyncPool(10, logs, async log => {
102
171
  const l2BlockNumber = log.args.blockNumber!;
103
172
  const archive = log.args.archive!;
@@ -122,7 +191,7 @@ export async function processL2BlockProposedLogs(
122
191
  timestamp: await getL1BlockTime(publicClient, log.blockNumber),
123
192
  };
124
193
 
125
- retrievedBlocks.push({ ...block, l1 });
194
+ retrievedBlocks.push({ ...block, l1, chainId, version });
126
195
  logger.trace(`Retrieved L2 block ${l2BlockNumber} from L1 tx ${log.transactionHash}`, {
127
196
  l1BlockNumber: log.blockNumber,
128
197
  l2BlockNumber,
@@ -187,7 +256,7 @@ function extractRollupProposeCalldata(forwarderData: Hex, rollupAddress: Hex): H
187
256
  if (rollupFunctionName === 'propose') {
188
257
  return callData;
189
258
  }
190
- } catch (err) {
259
+ } catch {
191
260
  // Skip invalid function data
192
261
  continue;
193
262
  }
@@ -202,7 +271,7 @@ function extractRollupProposeCalldata(forwarderData: Hex, rollupAddress: Hex): H
202
271
  * TODO: Add retries and error management.
203
272
  * @param publicClient - The viem public client to use for transaction retrieval.
204
273
  * @param txHash - Hash of the tx that published it.
205
- * @param l2BlockNum - L2 block number.
274
+ * @param l2BlockNumber - L2 block number.
206
275
  * @returns L2 block from the calldata, deserialized
207
276
  */
208
277
  async function getBlockFromRollupTx(
@@ -210,10 +279,10 @@ async function getBlockFromRollupTx(
210
279
  blobSinkClient: BlobSinkClientInterface,
211
280
  txHash: `0x${string}`,
212
281
  blobHashes: Buffer[], // TODO(md): buffer32?
213
- l2BlockNum: bigint,
282
+ l2BlockNumber: bigint,
214
283
  rollupAddress: Hex,
215
284
  logger: Logger,
216
- ): Promise<Omit<PublishedL2Block, 'l1'>> {
285
+ ): Promise<Omit<RetrievedL2Block, 'l1' | 'chainId' | 'version'>> {
217
286
  const { input: forwarderData, blockHash } = await publicClient.getTransaction({ hash: txHash });
218
287
 
219
288
  const rollupData = extractRollupProposeCalldata(forwarderData, rollupAddress);
@@ -226,10 +295,11 @@ async function getBlockFromRollupTx(
226
295
  throw new Error(`Unexpected rollup method called ${rollupFunctionName}`);
227
296
  }
228
297
 
229
- const [decodedArgs, signatures] = rollupArgs! as readonly [
298
+ const [decodedArgs, signatures, _blobInput] = rollupArgs! as readonly [
230
299
  {
231
300
  header: Hex;
232
301
  archive: Hex;
302
+ stateReference: Hex;
233
303
  blockHash: Hex;
234
304
  oracleInput: {
235
305
  feeAssetPriceModifier: bigint;
@@ -240,10 +310,10 @@ async function getBlockFromRollupTx(
240
310
  Hex,
241
311
  ];
242
312
 
243
- const header = BlockHeader.fromBuffer(Buffer.from(hexToBytes(decodedArgs.header)));
313
+ const header = ProposedBlockHeader.fromBuffer(Buffer.from(hexToBytes(decodedArgs.header)));
244
314
  const blobBodies = await blobSinkClient.getBlobSidecar(blockHash, blobHashes);
245
315
  if (blobBodies.length === 0) {
246
- throw new NoBlobBodiesFoundError(Number(l2BlockNum));
316
+ throw new NoBlobBodiesFoundError(Number(l2BlockNumber));
247
317
  }
248
318
 
249
319
  let blockFields: Fr[];
@@ -259,23 +329,30 @@ async function getBlockFromRollupTx(
259
329
  }
260
330
 
261
331
  // The blob source gives us blockFields, and we must construct the body from them:
262
- const blockBody = Body.fromBlobFields(blockFields);
332
+ const body = Body.fromBlobFields(blockFields);
263
333
 
264
- const blockNumberFromHeader = header.globalVariables.blockNumber.toBigInt();
334
+ const archiveRoot = new Fr(Buffer.from(hexToBytes(decodedArgs.archive)));
265
335
 
266
- if (blockNumberFromHeader !== l2BlockNum) {
267
- throw new Error(`Block number mismatch: expected ${l2BlockNum} but got ${blockNumberFromHeader}`);
268
- }
336
+ const stateReference = StateReference.fromBuffer(Buffer.from(hexToBytes(decodedArgs.stateReference)));
269
337
 
270
- const archive = AppendOnlyTreeSnapshot.fromBuffer(
271
- Buffer.concat([
272
- Buffer.from(hexToBytes(decodedArgs.archive)), // L2Block.archive.root
273
- numToUInt32BE(Number(l2BlockNum + 1n)), // L2Block.archive.nextAvailableLeafIndex
274
- ]),
275
- );
338
+ return {
339
+ l2BlockNumber,
340
+ archiveRoot,
341
+ stateReference,
342
+ header,
343
+ body,
344
+ signatures: signatures.map(Signature.fromViemSignature),
345
+ };
346
+ }
276
347
 
277
- const block = new L2Block(archive, header, blockBody);
278
- return { block, signatures: signatures.map(Signature.fromViemSignature) };
348
+ /** Given an L1 to L2 message, retrieves its corresponding event from the Inbox. */
349
+ export async function retrieveL1ToL2Message(
350
+ inbox: GetContractReturnType<typeof InboxAbi, ViemClient>,
351
+ leaf: Fr,
352
+ fromBlock: bigint,
353
+ ): Promise<InboxMessage | undefined> {
354
+ const logs = await inbox.getEvents.MessageSent({ hash: leaf.toString() }, { fromBlock });
355
+ return mapLogsInboxMessage(logs)[0];
279
356
  }
280
357
 
281
358
  /**
@@ -288,39 +365,39 @@ async function getBlockFromRollupTx(
288
365
  * @returns An array of InboxLeaf and next eth block to search from.
289
366
  */
290
367
  export async function retrieveL1ToL2Messages(
291
- inbox: GetContractReturnType<typeof InboxAbi, ViemPublicClient>,
368
+ inbox: GetContractReturnType<typeof InboxAbi, ViemClient>,
292
369
  searchStartBlock: bigint,
293
370
  searchEndBlock: bigint,
294
- ): Promise<DataRetrieval<InboxLeaf>> {
295
- const retrievedL1ToL2Messages: InboxLeaf[] = [];
296
- do {
297
- if (searchStartBlock > searchEndBlock) {
298
- break;
299
- }
300
-
371
+ ): Promise<InboxMessage[]> {
372
+ const retrievedL1ToL2Messages: InboxMessage[] = [];
373
+ while (searchStartBlock <= searchEndBlock) {
301
374
  const messageSentLogs = (
302
- await inbox.getEvents.MessageSent(
303
- {},
304
- {
305
- fromBlock: searchStartBlock,
306
- toBlock: searchEndBlock,
307
- },
308
- )
375
+ await inbox.getEvents.MessageSent({}, { fromBlock: searchStartBlock, toBlock: searchEndBlock })
309
376
  ).filter(log => log.blockNumber! >= searchStartBlock && log.blockNumber! <= searchEndBlock);
310
377
 
311
378
  if (messageSentLogs.length === 0) {
312
379
  break;
313
380
  }
314
381
 
315
- for (const log of messageSentLogs) {
316
- const { index, hash } = log.args;
317
- retrievedL1ToL2Messages.push(new InboxLeaf(index!, Fr.fromHexString(hash!)));
318
- }
382
+ retrievedL1ToL2Messages.push(...mapLogsInboxMessage(messageSentLogs));
383
+ searchStartBlock = messageSentLogs.at(-1)!.blockNumber + 1n;
384
+ }
319
385
 
320
- // handles the case when there are no new messages:
321
- searchStartBlock = (messageSentLogs.findLast(msgLog => !!msgLog)?.blockNumber || searchStartBlock) + 1n;
322
- } while (searchStartBlock <= searchEndBlock);
323
- return { lastProcessedL1BlockNumber: searchStartBlock - 1n, retrievedData: retrievedL1ToL2Messages };
386
+ return retrievedL1ToL2Messages;
387
+ }
388
+
389
+ function mapLogsInboxMessage(logs: GetContractEventsReturnType<typeof InboxAbi, 'MessageSent'>): InboxMessage[] {
390
+ return logs.map(log => {
391
+ const { index, hash, l2BlockNumber, rollingHash } = log.args;
392
+ return {
393
+ index: index!,
394
+ leaf: Fr.fromHexString(hash!),
395
+ l1BlockNumber: log.blockNumber,
396
+ l1BlockHash: Buffer32.fromString(log.blockHash),
397
+ l2BlockNumber: l2BlockNumber!,
398
+ rollingHash: Buffer16.fromString(rollingHash!),
399
+ };
400
+ });
324
401
  }
325
402
 
326
403
  /** Retrieves L2ProofVerified events from the rollup contract. */
@@ -3,3 +3,24 @@ export class NoBlobBodiesFoundError extends Error {
3
3
  super(`No blob bodies found for block ${l2BlockNum}`);
4
4
  }
5
5
  }
6
+
7
+ export class InitialBlockNumberNotSequentialError extends Error {
8
+ constructor(
9
+ public readonly newBlockNumber: number,
10
+ public readonly previousBlockNumber: number | undefined,
11
+ ) {
12
+ super(
13
+ `Cannot insert new block ${newBlockNumber} given previous block number in store is ${
14
+ previousBlockNumber ?? 'undefined'
15
+ }`,
16
+ );
17
+ }
18
+ }
19
+
20
+ export class BlockNumberNotSequentialError extends Error {
21
+ constructor(newBlockNumber: number, previous: number | undefined) {
22
+ super(
23
+ `Cannot insert new block ${newBlockNumber} given previous block number in batch is ${previous ?? 'undefined'}`,
24
+ );
25
+ }
26
+ }
@@ -28,7 +28,10 @@ export class ArchiverInstrumentation {
28
28
 
29
29
  private log = createLogger('archiver:instrumentation');
30
30
 
31
- private constructor(private telemetry: TelemetryClient, lmdbStats?: LmdbStatsCallback) {
31
+ private constructor(
32
+ private telemetry: TelemetryClient,
33
+ lmdbStats?: LmdbStatsCallback,
34
+ ) {
32
35
  this.tracer = telemetry.getTracer('Archiver');
33
36
  const meter = telemetry.getMeter('Archiver');
34
37
  this.blockHeight = meter.createGauge(Metrics.ARCHIVER_BLOCK_HEIGHT, {
@@ -8,6 +8,7 @@ import { Body, L2Block, L2BlockHash } from '@aztec/stdlib/block';
8
8
  import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
9
9
  import { BlockHeader, type IndexedTxEffect, TxHash, TxReceipt } from '@aztec/stdlib/tx';
10
10
 
11
+ import { BlockNumberNotSequentialError, InitialBlockNumberNotSequentialError } from '../errors.js';
11
12
  import type { L1PublishedData, PublishedL2Block } from '../structs/published.js';
12
13
 
13
14
  export { TxReceipt, type TxEffect, type TxHash } from '@aztec/stdlib/tx';
@@ -37,9 +38,6 @@ export class BlockStore {
37
38
  /** Stores l2 block number of the last proven block */
38
39
  #lastProvenL2Block: AztecAsyncSingleton<number>;
39
40
 
40
- /** Stores l2 epoch number of the last proven epoch */
41
- #lastProvenL2Epoch: AztecAsyncSingleton<number>;
42
-
43
41
  /** Index mapping transaction hash (as a string) to its location in a block */
44
42
  #txIndex: AztecAsyncMap<string, BlockIndexValue>;
45
43
 
@@ -55,7 +53,6 @@ export class BlockStore {
55
53
  this.#contractIndex = db.openMap('archiver_contract_index');
56
54
  this.#lastSynchedL1Block = db.openSingleton('archiver_last_synched_l1_block');
57
55
  this.#lastProvenL2Block = db.openSingleton('archiver_last_proven_l2_block');
58
- this.#lastProvenL2Epoch = db.openSingleton('archiver_last_proven_l2_epoch');
59
56
  }
60
57
 
61
58
  /**
@@ -63,13 +60,32 @@ export class BlockStore {
63
60
  * @param blocks - The L2 blocks to be added to the store.
64
61
  * @returns True if the operation is successful.
65
62
  */
66
- async addBlocks(blocks: PublishedL2Block[]): Promise<boolean> {
63
+ async addBlocks(blocks: PublishedL2Block[], opts: { force?: boolean } = {}): Promise<boolean> {
67
64
  if (blocks.length === 0) {
68
65
  return true;
69
66
  }
70
67
 
71
68
  return await this.db.transactionAsync(async () => {
69
+ // Check that the block immediately before the first block to be added is present in the store.
70
+ const firstBlockNumber = blocks[0].block.number;
71
+ const [previousBlockNumber] = await toArray(
72
+ this.#blocks.keysAsync({ reverse: true, limit: 1, end: firstBlockNumber - 1 }),
73
+ );
74
+ const hasPreviousBlock =
75
+ firstBlockNumber === INITIAL_L2_BLOCK_NUM ||
76
+ (previousBlockNumber !== undefined && previousBlockNumber === firstBlockNumber - 1);
77
+ if (!opts.force && !hasPreviousBlock) {
78
+ throw new InitialBlockNumberNotSequentialError(firstBlockNumber, previousBlockNumber);
79
+ }
80
+
81
+ // Iterate over blocks array and insert them, checking that the block numbers are sequential.
82
+ let previousBlock: PublishedL2Block | undefined = undefined;
72
83
  for (const block of blocks) {
84
+ if (!opts.force && previousBlock && previousBlock.block.number + 1 !== block.block.number) {
85
+ throw new BlockNumberNotSequentialError(block.block.number, previousBlock.block.number);
86
+ }
87
+ previousBlock = block;
88
+
73
89
  await this.#blocks.set(block.block.number, {
74
90
  header: block.block.header.toBuffer(),
75
91
  archive: block.block.archive.toBuffer(),
@@ -104,6 +120,11 @@ export class BlockStore {
104
120
  throw new Error(`Can only unwind blocks from the tip (requested ${from} but current tip is ${last})`);
105
121
  }
106
122
 
123
+ const proven = await this.getProvenL2BlockNumber();
124
+ if (from - blocksToUnwind < proven) {
125
+ await this.setProvenL2BlockNumber(from - blocksToUnwind);
126
+ }
127
+
107
128
  for (let i = 0; i < blocksToUnwind; i++) {
108
129
  const blockNumber = from - i;
109
130
  const block = await this.getBlock(blockNumber);
@@ -130,7 +151,7 @@ export class BlockStore {
130
151
  * @returns The requested L2 blocks
131
152
  */
132
153
  async *getBlocks(start: number, limit: number): AsyncIterableIterator<PublishedL2Block> {
133
- for await (const [blockNumber, blockStorage] of this.#blocks.entriesAsync(this.#computeBlockRange(start, limit))) {
154
+ for await (const [blockNumber, blockStorage] of this.getBlockStorages(start, limit)) {
134
155
  const block = await this.getBlockFromBlockStorage(blockNumber, blockStorage);
135
156
  if (block) {
136
157
  yield block;
@@ -158,7 +179,7 @@ export class BlockStore {
158
179
  * @returns The requested L2 block headers
159
180
  */
160
181
  async *getBlockHeaders(start: number, limit: number): AsyncIterableIterator<BlockHeader> {
161
- for await (const [blockNumber, blockStorage] of this.#blocks.entriesAsync(this.#computeBlockRange(start, limit))) {
182
+ for await (const [blockNumber, blockStorage] of this.getBlockStorages(start, limit)) {
162
183
  const header = BlockHeader.fromBuffer(blockStorage.header);
163
184
  if (header.getBlockNumber() !== blockNumber) {
164
185
  throw new Error(
@@ -169,6 +190,19 @@ export class BlockStore {
169
190
  }
170
191
  }
171
192
 
193
+ private async *getBlockStorages(start: number, limit: number) {
194
+ let expectedBlockNumber = start;
195
+ for await (const [blockNumber, blockStorage] of this.#blocks.entriesAsync(this.#computeBlockRange(start, limit))) {
196
+ if (blockNumber !== expectedBlockNumber) {
197
+ throw new Error(
198
+ `Block number mismatch when iterating blocks from archive (expected ${expectedBlockNumber} but got ${blockNumber})`,
199
+ );
200
+ }
201
+ expectedBlockNumber++;
202
+ yield [blockNumber, blockStorage] as const;
203
+ }
204
+ }
205
+
172
206
  private async getBlockFromBlockStorage(blockNumber: number, blockStorage: BlockStorage) {
173
207
  const header = BlockHeader.fromBuffer(blockStorage.header);
174
208
  const archive = AppendOnlyTreeSnapshot.fromBuffer(blockStorage.archive);
@@ -283,7 +317,11 @@ export class BlockStore {
283
317
  }
284
318
 
285
319
  async getProvenL2BlockNumber(): Promise<number> {
286
- return (await this.#lastProvenL2Block.getAsync()) ?? 0;
320
+ const [latestBlockNumber, provenBlockNumber] = await Promise.all([
321
+ this.getSynchedL2BlockNumber(),
322
+ this.#lastProvenL2Block.getAsync(),
323
+ ]);
324
+ return (provenBlockNumber ?? 0) > latestBlockNumber ? latestBlockNumber : (provenBlockNumber ?? 0);
287
325
  }
288
326
 
289
327
  setProvenL2BlockNumber(blockNumber: number) {
@@ -15,22 +15,30 @@ type ContractInstanceUpdateKey = [string, number] | [string, number, number];
15
15
  */
16
16
  export class ContractInstanceStore {
17
17
  #contractInstances: AztecAsyncMap<string, Buffer>;
18
+ #contractInstanceDeployedAt: AztecAsyncMap<string, number>;
18
19
  #contractInstanceUpdates: AztecAsyncMap<ContractInstanceUpdateKey, Buffer>;
19
20
 
20
- constructor(db: AztecAsyncKVStore) {
21
+ constructor(private db: AztecAsyncKVStore) {
21
22
  this.#contractInstances = db.openMap('archiver_contract_instances');
23
+ this.#contractInstanceDeployedAt = db.openMap('archiver_contract_instances_deployment_block_number');
22
24
  this.#contractInstanceUpdates = db.openMap('archiver_contract_instance_updates');
23
25
  }
24
26
 
25
- addContractInstance(contractInstance: ContractInstanceWithAddress): Promise<void> {
26
- return this.#contractInstances.set(
27
- contractInstance.address.toString(),
28
- new SerializableContractInstance(contractInstance).toBuffer(),
29
- );
27
+ addContractInstance(contractInstance: ContractInstanceWithAddress, blockNumber: number): Promise<void> {
28
+ return this.db.transactionAsync(async () => {
29
+ await this.#contractInstances.set(
30
+ contractInstance.address.toString(),
31
+ new SerializableContractInstance(contractInstance).toBuffer(),
32
+ );
33
+ await this.#contractInstanceDeployedAt.set(contractInstance.address.toString(), blockNumber);
34
+ });
30
35
  }
31
36
 
32
37
  deleteContractInstance(contractInstance: ContractInstanceWithAddress): Promise<void> {
33
- return this.#contractInstances.delete(contractInstance.address.toString());
38
+ return this.db.transactionAsync(async () => {
39
+ await this.#contractInstances.delete(contractInstance.address.toString());
40
+ await this.#contractInstanceDeployedAt.delete(contractInstance.address.toString());
41
+ });
34
42
  }
35
43
 
36
44
  getUpdateKey(contractAddress: AztecAddress, blockNumber: number, logIndex?: number): ContractInstanceUpdateKey {
@@ -71,6 +79,7 @@ export class ContractInstanceStore {
71
79
  const queryResult = await this.#contractInstanceUpdates
72
80
  .valuesAsync({
73
81
  reverse: true,
82
+ start: this.getUpdateKey(address, 0), // Make sure we only look at updates for this contract
74
83
  end: this.getUpdateKey(address, blockNumber + 1), // No update can match this key since it doesn't have a log index. We want the highest key <= blockNumber
75
84
  limit: 1,
76
85
  })
@@ -104,4 +113,8 @@ export class ContractInstanceStore {
104
113
  );
105
114
  return instance;
106
115
  }
116
+
117
+ getContractInstanceDeploymentBlockNumber(address: AztecAddress): Promise<number | undefined> {
118
+ return this.#contractInstanceDeployedAt.getAsync(address.toString());
119
+ }
107
120
  }