@aztec/archiver 0.0.1-commit.5914bae → 0.0.1-commit.59a0419c6

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 (95) hide show
  1. package/README.md +12 -6
  2. package/dest/archiver.d.ts +7 -5
  3. package/dest/archiver.d.ts.map +1 -1
  4. package/dest/archiver.js +18 -5
  5. package/dest/config.d.ts +5 -3
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +15 -3
  8. package/dest/errors.d.ts +44 -2
  9. package/dest/errors.d.ts.map +1 -1
  10. package/dest/errors.js +58 -2
  11. package/dest/factory.d.ts +2 -2
  12. package/dest/factory.d.ts.map +1 -1
  13. package/dest/factory.js +3 -4
  14. package/dest/index.d.ts +3 -2
  15. package/dest/index.d.ts.map +1 -1
  16. package/dest/index.js +2 -1
  17. package/dest/l1/calldata_retriever.d.ts +1 -1
  18. package/dest/l1/calldata_retriever.d.ts.map +1 -1
  19. package/dest/l1/calldata_retriever.js +2 -1
  20. package/dest/l1/data_retrieval.d.ts +24 -12
  21. package/dest/l1/data_retrieval.d.ts.map +1 -1
  22. package/dest/l1/data_retrieval.js +36 -37
  23. package/dest/l1/validate_historical_logs.d.ts +23 -0
  24. package/dest/l1/validate_historical_logs.d.ts.map +1 -0
  25. package/dest/l1/validate_historical_logs.js +108 -0
  26. package/dest/modules/data_source_base.d.ts +6 -4
  27. package/dest/modules/data_source_base.d.ts.map +1 -1
  28. package/dest/modules/data_source_base.js +10 -4
  29. package/dest/modules/data_store_updater.d.ts +15 -10
  30. package/dest/modules/data_store_updater.d.ts.map +1 -1
  31. package/dest/modules/data_store_updater.js +27 -59
  32. package/dest/modules/instrumentation.d.ts +18 -2
  33. package/dest/modules/instrumentation.d.ts.map +1 -1
  34. package/dest/modules/instrumentation.js +32 -6
  35. package/dest/modules/l1_synchronizer.d.ts +6 -2
  36. package/dest/modules/l1_synchronizer.d.ts.map +1 -1
  37. package/dest/modules/l1_synchronizer.js +248 -149
  38. package/dest/modules/validation.d.ts +1 -1
  39. package/dest/modules/validation.d.ts.map +1 -1
  40. package/dest/modules/validation.js +2 -2
  41. package/dest/store/block_store.d.ts +46 -5
  42. package/dest/store/block_store.d.ts.map +1 -1
  43. package/dest/store/block_store.js +225 -63
  44. package/dest/store/contract_class_store.d.ts +2 -3
  45. package/dest/store/contract_class_store.d.ts.map +1 -1
  46. package/dest/store/contract_class_store.js +1 -65
  47. package/dest/store/kv_archiver_store.d.ts +35 -14
  48. package/dest/store/kv_archiver_store.d.ts.map +1 -1
  49. package/dest/store/kv_archiver_store.js +40 -14
  50. package/dest/store/l2_tips_cache.d.ts +2 -1
  51. package/dest/store/l2_tips_cache.d.ts.map +1 -1
  52. package/dest/store/l2_tips_cache.js +27 -7
  53. package/dest/store/log_store.d.ts +6 -3
  54. package/dest/store/log_store.d.ts.map +1 -1
  55. package/dest/store/log_store.js +47 -10
  56. package/dest/store/message_store.d.ts +5 -1
  57. package/dest/store/message_store.d.ts.map +1 -1
  58. package/dest/store/message_store.js +20 -8
  59. package/dest/test/fake_l1_state.d.ts +7 -3
  60. package/dest/test/fake_l1_state.d.ts.map +1 -1
  61. package/dest/test/fake_l1_state.js +50 -10
  62. package/dest/test/mock_l1_to_l2_message_source.d.ts +1 -1
  63. package/dest/test/mock_l1_to_l2_message_source.d.ts.map +1 -1
  64. package/dest/test/mock_l1_to_l2_message_source.js +2 -1
  65. package/dest/test/mock_l2_block_source.d.ts +7 -2
  66. package/dest/test/mock_l2_block_source.d.ts.map +1 -1
  67. package/dest/test/mock_l2_block_source.js +28 -3
  68. package/dest/test/noop_l1_archiver.d.ts +1 -1
  69. package/dest/test/noop_l1_archiver.d.ts.map +1 -1
  70. package/dest/test/noop_l1_archiver.js +4 -2
  71. package/package.json +13 -13
  72. package/src/archiver.ts +33 -8
  73. package/src/config.ts +22 -2
  74. package/src/errors.ts +94 -2
  75. package/src/factory.ts +2 -3
  76. package/src/index.ts +2 -1
  77. package/src/l1/calldata_retriever.ts +2 -1
  78. package/src/l1/data_retrieval.ts +52 -53
  79. package/src/l1/validate_historical_logs.ts +140 -0
  80. package/src/modules/data_source_base.ts +23 -4
  81. package/src/modules/data_store_updater.ts +43 -85
  82. package/src/modules/instrumentation.ts +47 -7
  83. package/src/modules/l1_synchronizer.ts +321 -185
  84. package/src/modules/validation.ts +2 -2
  85. package/src/store/block_store.ts +295 -73
  86. package/src/store/contract_class_store.ts +1 -103
  87. package/src/store/kv_archiver_store.ts +67 -24
  88. package/src/store/l2_tips_cache.ts +58 -13
  89. package/src/store/log_store.ts +62 -20
  90. package/src/store/message_store.ts +26 -9
  91. package/src/structs/inbox_message.ts +1 -1
  92. package/src/test/fake_l1_state.ts +72 -15
  93. package/src/test/mock_l1_to_l2_message_source.ts +1 -0
  94. package/src/test/mock_l2_block_source.ts +37 -2
  95. package/src/test/noop_l1_archiver.ts +3 -1
@@ -10,18 +10,24 @@ import {
10
10
  type BlockData,
11
11
  BlockHash,
12
12
  CheckpointedL2Block,
13
+ type CommitteeAttestation,
13
14
  L2Block,
14
15
  type ValidateCheckpointResult,
15
16
  } from '@aztec/stdlib/block';
16
- import type { CheckpointData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
17
+ import type {
18
+ CheckpointData,
19
+ CommonCheckpointData,
20
+ L1PublishedData,
21
+ ProposedCheckpointData,
22
+ ProposedCheckpointInput,
23
+ PublishedCheckpoint,
24
+ } from '@aztec/stdlib/checkpoint';
17
25
  import type {
18
26
  ContractClassPublic,
19
27
  ContractClassPublicWithCommitment,
20
28
  ContractDataSource,
21
29
  ContractInstanceUpdateWithAddress,
22
30
  ContractInstanceWithAddress,
23
- ExecutablePrivateFunctionWithMembershipProof,
24
- UtilityFunctionWithMembershipProof,
25
31
  } from '@aztec/stdlib/contract';
26
32
  import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
27
33
  import type { GetContractClassLogsResponse, GetPublicLogsResponse } from '@aztec/stdlib/interfaces/client';
@@ -38,7 +44,7 @@ import { ContractInstanceStore } from './contract_instance_store.js';
38
44
  import { LogStore } from './log_store.js';
39
45
  import { MessageStore } from './message_store.js';
40
46
 
41
- export const ARCHIVER_DB_VERSION = 5;
47
+ export const ARCHIVER_DB_VERSION = 6;
42
48
  export const MAX_FUNCTION_SIGNATURES = 1000;
43
49
  export const MAX_FUNCTION_NAME_LEN = 256;
44
50
 
@@ -190,15 +196,6 @@ export class KVArchiverDataStore implements ContractDataSource {
190
196
  return this.#contractClassStore.getBytecodeCommitment(contractClassId);
191
197
  }
192
198
 
193
- /** Adds private functions to a contract class. */
194
- addFunctions(
195
- contractClassId: Fr,
196
- privateFunctions: ExecutablePrivateFunctionWithMembershipProof[],
197
- utilityFunctions: UtilityFunctionWithMembershipProof[],
198
- ): Promise<boolean> {
199
- return this.#contractClassStore.addFunctions(contractClassId, privateFunctions, utilityFunctions);
200
- }
201
-
202
199
  /**
203
200
  * Add new contract instances from an L2 block to the store's list.
204
201
  * @param data - List of contract instances to be added.
@@ -265,7 +262,7 @@ export class KVArchiverDataStore implements ContractDataSource {
265
262
  * @returns The number of the latest block
266
263
  */
267
264
  getLatestBlockNumber(): Promise<BlockNumber> {
268
- return this.#blockStore.getLatestBlockNumber();
265
+ return this.#blockStore.getLatestL2BlockNumber();
269
266
  }
270
267
 
271
268
  /**
@@ -470,10 +467,11 @@ export class KVArchiverDataStore implements ContractDataSource {
470
467
  * array implies no logs match that tag.
471
468
  * @param tags - The tags to search for.
472
469
  * @param page - The page number (0-indexed) for pagination. Returns at most 10 logs per tag per page.
470
+ * @param upToBlockNumber - If set, only return logs from blocks up to and including this block number.
473
471
  */
474
- getPrivateLogsByTags(tags: SiloedTag[], page?: number): Promise<TxScopedL2Log[][]> {
472
+ getPrivateLogsByTags(tags: SiloedTag[], page?: number, upToBlockNumber?: BlockNumber): Promise<TxScopedL2Log[][]> {
475
473
  try {
476
- return this.#logStore.getPrivateLogsByTags(tags, page);
474
+ return this.#logStore.getPrivateLogsByTags(tags, page, upToBlockNumber);
477
475
  } catch (err) {
478
476
  return Promise.reject(err);
479
477
  }
@@ -485,14 +483,16 @@ export class KVArchiverDataStore implements ContractDataSource {
485
483
  * @param contractAddress - The contract address to search logs for.
486
484
  * @param tags - The tags to search for.
487
485
  * @param page - The page number (0-indexed) for pagination. Returns at most 10 logs per tag per page.
486
+ * @param upToBlockNumber - If set, only return logs from blocks up to and including this block number.
488
487
  */
489
488
  getPublicLogsByTagsFromContract(
490
489
  contractAddress: AztecAddress,
491
490
  tags: Tag[],
492
491
  page?: number,
492
+ upToBlockNumber?: BlockNumber,
493
493
  ): Promise<TxScopedL2Log[][]> {
494
494
  try {
495
- return this.#logStore.getPublicLogsByTagsFromContract(contractAddress, tags, page);
495
+ return this.#logStore.getPublicLogsByTagsFromContract(contractAddress, tags, page, upToBlockNumber);
496
496
  } catch (err) {
497
497
  return Promise.reject(err);
498
498
  }
@@ -560,13 +560,6 @@ export class KVArchiverDataStore implements ContractDataSource {
560
560
  await this.#blockStore.setSynchedL1BlockNumber(l1BlockNumber);
561
561
  }
562
562
 
563
- /**
564
- * Stores the l1 block that messages have been synched until
565
- */
566
- async setMessageSynchedL1Block(l1Block: L1BlockId) {
567
- await this.#messageStore.setSynchedL1Block(l1Block);
568
- }
569
-
570
563
  /**
571
564
  * Returns the number of the most recent proven block
572
565
  * @returns The number of the most recent proven block
@@ -599,6 +592,11 @@ export class KVArchiverDataStore implements ContractDataSource {
599
592
  return this.#messageStore.rollbackL1ToL2MessagesToCheckpoint(targetCheckpointNumber);
600
593
  }
601
594
 
595
+ /** Atomically updates the message sync state: the L1 sync point and the inbox tree-in-progress marker. */
596
+ public setMessageSyncState(l1Block: L1BlockId, treeInProgress: bigint | undefined): Promise<void> {
597
+ return this.#messageStore.setMessageSyncState(l1Block, treeInProgress);
598
+ }
599
+
602
600
  /** Returns an async iterator to all L1 to L2 messages on the range. */
603
601
  public iterateL1ToL2Messages(range: CustomRange<bigint> = {}): AsyncIterableIterator<InboxMessage> {
604
602
  return this.#messageStore.iterateL1ToL2Messages(range);
@@ -619,6 +617,51 @@ export class KVArchiverDataStore implements ContractDataSource {
619
617
  return this.#blockStore.setPendingChainValidationStatus(status);
620
618
  }
621
619
 
620
+ /**
621
+ * Gets the L2 block number of the proposed checkpoint.
622
+ * @returns The block number of the proposed checkpoint, or the checkpointed block number if none.
623
+ */
624
+ public getProposedCheckpointL2BlockNumber(): Promise<BlockNumber> {
625
+ return this.#blockStore.getProposedCheckpointL2BlockNumber();
626
+ }
627
+
628
+ /** Returns the checkpoint data at the proposed tip */
629
+ public getProposedCheckpoint(): Promise<CommonCheckpointData | undefined> {
630
+ return this.#blockStore.getProposedCheckpoint();
631
+ }
632
+
633
+ /** Returns the proposed checkpoint data, or undefined if no proposed checkpoint exists. No fallback to confirmed. */
634
+ public getProposedCheckpointOnly(): Promise<ProposedCheckpointData | undefined> {
635
+ return this.#blockStore.getProposedCheckpointOnly();
636
+ }
637
+
638
+ /**
639
+ * Set proposed checkpoint
640
+ * @param proposedCheckpoint
641
+ * @returns
642
+ */
643
+ public setProposedCheckpoint(proposedCheckpoint: ProposedCheckpointInput): Promise<void> {
644
+ return this.#blockStore.setProposedCheckpoint(proposedCheckpoint);
645
+ }
646
+
647
+ /** Deletes the proposed checkpoint from storage. */
648
+ public deleteProposedCheckpoint(): Promise<void> {
649
+ return this.#blockStore.deleteProposedCheckpoint();
650
+ }
651
+
652
+ /**
653
+ * Promotes the proposed checkpoint to a confirmed checkpoint entry.
654
+ * Should only be called after the checkpoint has been validated.
655
+ * @param expectedArchiveRoot - The archive root to match against the proposed checkpoint, to guard against races.
656
+ */
657
+ public promoteProposedToCheckpointed(
658
+ l1: L1PublishedData,
659
+ attestations: CommitteeAttestation[],
660
+ expectedArchiveRoot: Fr,
661
+ ): Promise<void> {
662
+ return this.#blockStore.promoteProposedToCheckpointed(l1, attestations, expectedArchiveRoot);
663
+ }
664
+
622
665
  /**
623
666
  * Gets the number of the latest L2 block processed.
624
667
  * @returns The number of the latest L2 block processed.
@@ -1,6 +1,12 @@
1
- import { GENESIS_BLOCK_HEADER_HASH, INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
1
+ import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
2
2
  import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
3
- import { type BlockData, type CheckpointId, GENESIS_CHECKPOINT_HEADER_HASH, type L2Tips } from '@aztec/stdlib/block';
3
+ import {
4
+ type BlockData,
5
+ type CheckpointId,
6
+ GENESIS_BLOCK_HEADER_HASH,
7
+ GENESIS_CHECKPOINT_HEADER_HASH,
8
+ type L2Tips,
9
+ } from '@aztec/stdlib/block';
4
10
 
5
11
  import type { BlockStore } from './block_store.js';
6
12
 
@@ -26,9 +32,16 @@ export class L2TipsCache {
26
32
  }
27
33
 
28
34
  private async loadFromStore(): Promise<L2Tips> {
29
- const [latestBlockNumber, provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber] = await Promise.all([
30
- this.blockStore.getLatestBlockNumber(),
35
+ const [
36
+ latestBlockNumber,
37
+ provenBlockNumber,
38
+ proposedCheckpointBlockNumber,
39
+ checkpointedBlockNumber,
40
+ finalizedBlockNumber,
41
+ ] = await Promise.all([
42
+ this.blockStore.getLatestL2BlockNumber(),
31
43
  this.blockStore.getProvenBlockNumber(),
44
+ this.blockStore.getProposedCheckpointL2BlockNumber(),
32
45
  this.blockStore.getCheckpointedL2BlockNumber(),
33
46
  this.blockStore.getFinalizedL2BlockNumber(),
34
47
  ]);
@@ -42,19 +55,34 @@ export class L2TipsCache {
42
55
  const getBlockData = (blockNumber: BlockNumber) =>
43
56
  blockNumber > beforeInitialBlockNumber ? this.blockStore.getBlockData(blockNumber) : genesisBlockHeader;
44
57
 
45
- const [latestBlockData, provenBlockData, checkpointedBlockData, finalizedBlockData] = await Promise.all(
46
- [latestBlockNumber, provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber].map(getBlockData),
47
- );
58
+ const [latestBlockData, provenBlockData, proposedCheckpointBlockData, checkpointedBlockData, finalizedBlockData] =
59
+ await Promise.all(
60
+ [
61
+ latestBlockNumber,
62
+ provenBlockNumber,
63
+ proposedCheckpointBlockNumber,
64
+ checkpointedBlockNumber,
65
+ finalizedBlockNumber,
66
+ ].map(getBlockData),
67
+ );
48
68
 
49
- if (!latestBlockData || !provenBlockData || !finalizedBlockData || !checkpointedBlockData) {
69
+ if (
70
+ !latestBlockData ||
71
+ !provenBlockData ||
72
+ !finalizedBlockData ||
73
+ !checkpointedBlockData ||
74
+ !proposedCheckpointBlockData
75
+ ) {
50
76
  throw new Error('Failed to load block data for L2 tips');
51
77
  }
52
78
 
53
- const [provenCheckpointId, finalizedCheckpointId, checkpointedCheckpointId] = await Promise.all([
54
- this.getCheckpointIdForBlock(provenBlockData),
55
- this.getCheckpointIdForBlock(finalizedBlockData),
56
- this.getCheckpointIdForBlock(checkpointedBlockData),
57
- ]);
79
+ const [provenCheckpointId, finalizedCheckpointId, proposedCheckpointId, checkpointedCheckpointId] =
80
+ await Promise.all([
81
+ this.getCheckpointIdForBlock(provenBlockData),
82
+ this.getCheckpointIdForBlock(finalizedBlockData),
83
+ this.getCheckpointIdForProposedCheckpoint(checkpointedBlockData),
84
+ this.getCheckpointIdForBlock(checkpointedBlockData),
85
+ ]);
58
86
 
59
87
  return {
60
88
  proposed: { number: latestBlockNumber, hash: latestBlockData.blockHash.toString() },
@@ -62,6 +90,10 @@ export class L2TipsCache {
62
90
  block: { number: provenBlockNumber, hash: provenBlockData.blockHash.toString() },
63
91
  checkpoint: provenCheckpointId,
64
92
  },
93
+ proposedCheckpoint: {
94
+ block: { number: proposedCheckpointBlockNumber, hash: proposedCheckpointBlockData.blockHash.toString() },
95
+ checkpoint: proposedCheckpointId,
96
+ },
65
97
  finalized: {
66
98
  block: { number: finalizedBlockNumber, hash: finalizedBlockData.blockHash.toString() },
67
99
  checkpoint: finalizedCheckpointId,
@@ -73,6 +105,19 @@ export class L2TipsCache {
73
105
  };
74
106
  }
75
107
 
108
+ private async getCheckpointIdForProposedCheckpoint(
109
+ checkpointedBlockData: Pick<BlockData, 'checkpointNumber'>,
110
+ ): Promise<CheckpointId> {
111
+ const checkpointData = await this.blockStore.getProposedCheckpointOnly();
112
+ if (!checkpointData) {
113
+ return this.getCheckpointIdForBlock(checkpointedBlockData);
114
+ }
115
+ return {
116
+ number: checkpointData.checkpointNumber,
117
+ hash: checkpointData.header.hash().toString(),
118
+ };
119
+ }
120
+
76
121
  private async getCheckpointIdForBlock(blockData: Pick<BlockData, 'checkpointNumber'>): Promise<CheckpointId> {
77
122
  const checkpointData = await this.blockStore.getCheckpointData(blockData.checkpointNumber);
78
123
  if (!checkpointData) {
@@ -1,7 +1,6 @@
1
1
  import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
2
2
  import { BlockNumber } from '@aztec/foundation/branded-types';
3
3
  import { compactArray, filterAsync } from '@aztec/foundation/collection';
4
- import { Fr } from '@aztec/foundation/curves/bn254';
5
4
  import { createLogger } from '@aztec/foundation/log';
6
5
  import { BufferReader, numToUInt32BE } from '@aztec/foundation/serialize';
7
6
  import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store';
@@ -22,6 +21,7 @@ import {
22
21
  } from '@aztec/stdlib/logs';
23
22
  import { TxHash } from '@aztec/stdlib/tx';
24
23
 
24
+ import { OutOfOrderLogInsertionError } from '../errors.js';
25
25
  import type { BlockStore } from './block_store.js';
26
26
 
27
27
  /**
@@ -165,10 +165,21 @@ export class LogStore {
165
165
 
166
166
  for (const taggedLogBuffer of currentPrivateTaggedLogs) {
167
167
  if (taggedLogBuffer.logBuffers && taggedLogBuffer.logBuffers.length > 0) {
168
- privateTaggedLogs.set(
169
- taggedLogBuffer.tag,
170
- taggedLogBuffer.logBuffers!.concat(privateTaggedLogs.get(taggedLogBuffer.tag)!),
171
- );
168
+ const newLogs = privateTaggedLogs.get(taggedLogBuffer.tag)!;
169
+ if (newLogs.length === 0) {
170
+ continue;
171
+ }
172
+ const lastExisting = TxScopedL2Log.fromBuffer(taggedLogBuffer.logBuffers.at(-1)!);
173
+ const firstNew = TxScopedL2Log.fromBuffer(newLogs[0]);
174
+ if (lastExisting.blockNumber > firstNew.blockNumber) {
175
+ throw new OutOfOrderLogInsertionError(
176
+ 'private',
177
+ taggedLogBuffer.tag,
178
+ lastExisting.blockNumber,
179
+ firstNew.blockNumber,
180
+ );
181
+ }
182
+ privateTaggedLogs.set(taggedLogBuffer.tag, taggedLogBuffer.logBuffers.concat(newLogs));
172
183
  }
173
184
  }
174
185
 
@@ -200,10 +211,21 @@ export class LogStore {
200
211
 
201
212
  for (const taggedLogBuffer of currentPublicTaggedLogs) {
202
213
  if (taggedLogBuffer.logBuffers && taggedLogBuffer.logBuffers.length > 0) {
203
- publicTaggedLogs.set(
204
- taggedLogBuffer.tag,
205
- taggedLogBuffer.logBuffers!.concat(publicTaggedLogs.get(taggedLogBuffer.tag)!),
206
- );
214
+ const newLogs = publicTaggedLogs.get(taggedLogBuffer.tag)!;
215
+ if (newLogs.length === 0) {
216
+ continue;
217
+ }
218
+ const lastExisting = TxScopedL2Log.fromBuffer(taggedLogBuffer.logBuffers.at(-1)!);
219
+ const firstNew = TxScopedL2Log.fromBuffer(newLogs[0]);
220
+ if (lastExisting.blockNumber > firstNew.blockNumber) {
221
+ throw new OutOfOrderLogInsertionError(
222
+ 'public',
223
+ taggedLogBuffer.tag,
224
+ lastExisting.blockNumber,
225
+ firstNew.blockNumber,
226
+ );
227
+ }
228
+ publicTaggedLogs.set(taggedLogBuffer.tag, taggedLogBuffer.logBuffers.concat(newLogs));
207
229
  }
208
230
  }
209
231
 
@@ -279,13 +301,11 @@ export class LogStore {
279
301
  }
280
302
 
281
303
  #unpackBlockHash(reader: BufferReader): BlockHash {
282
- const blockHash = reader.remainingBytes() > 0 ? reader.readObject(Fr) : undefined;
283
-
284
- if (!blockHash) {
304
+ if (reader.remainingBytes() === 0) {
285
305
  throw new Error('Failed to read block hash from log entry buffer');
286
306
  }
287
307
 
288
- return new BlockHash(blockHash);
308
+ return BlockHash.fromBuffer(reader);
289
309
  }
290
310
 
291
311
  deleteLogs(blocks: L2Block[]): Promise<boolean> {
@@ -353,17 +373,30 @@ export class LogStore {
353
373
  * array implies no logs match that tag.
354
374
  * @param tags - The tags to search for.
355
375
  * @param page - The page number (0-indexed) for pagination.
376
+ * @param upToBlockNumber - If set, only return logs from blocks up to and including this block number.
356
377
  * @returns An array of log arrays, one per tag. Returns at most MAX_LOGS_PER_TAG logs per tag per page. If
357
378
  * MAX_LOGS_PER_TAG logs are returned for a tag, the caller should fetch the next page to check for more logs.
358
379
  */
359
- async getPrivateLogsByTags(tags: SiloedTag[], page: number = 0): Promise<TxScopedL2Log[][]> {
380
+ async getPrivateLogsByTags(
381
+ tags: SiloedTag[],
382
+ page: number = 0,
383
+ upToBlockNumber?: BlockNumber,
384
+ ): Promise<TxScopedL2Log[][]> {
360
385
  const logs = await Promise.all(tags.map(tag => this.#privateLogsByTag.getAsync(tag.toString())));
386
+
361
387
  const start = page * MAX_LOGS_PER_TAG;
362
388
  const end = start + MAX_LOGS_PER_TAG;
363
389
 
364
- return logs.map(
365
- logBuffers => logBuffers?.slice(start, end).map(logBuffer => TxScopedL2Log.fromBuffer(logBuffer)) ?? [],
366
- );
390
+ return logs.map(logBuffers => {
391
+ const deserialized = logBuffers?.slice(start, end).map(buf => TxScopedL2Log.fromBuffer(buf)) ?? [];
392
+ if (upToBlockNumber !== undefined) {
393
+ const cutoff = deserialized.findIndex(log => log.blockNumber > upToBlockNumber);
394
+ if (cutoff !== -1) {
395
+ return deserialized.slice(0, cutoff);
396
+ }
397
+ }
398
+ return deserialized;
399
+ });
367
400
  }
368
401
 
369
402
  /**
@@ -372,6 +405,7 @@ export class LogStore {
372
405
  * @param contractAddress - The contract address to search logs for.
373
406
  * @param tags - The tags to search for.
374
407
  * @param page - The page number (0-indexed) for pagination.
408
+ * @param upToBlockNumber - If set, only return logs from blocks up to and including this block number.
375
409
  * @returns An array of log arrays, one per tag. Returns at most MAX_LOGS_PER_TAG logs per tag per page. If
376
410
  * MAX_LOGS_PER_TAG logs are returned for a tag, the caller should fetch the next page to check for more logs.
377
411
  */
@@ -379,6 +413,7 @@ export class LogStore {
379
413
  contractAddress: AztecAddress,
380
414
  tags: Tag[],
381
415
  page: number = 0,
416
+ upToBlockNumber?: BlockNumber,
382
417
  ): Promise<TxScopedL2Log[][]> {
383
418
  const logs = await Promise.all(
384
419
  tags.map(tag => {
@@ -389,9 +424,16 @@ export class LogStore {
389
424
  const start = page * MAX_LOGS_PER_TAG;
390
425
  const end = start + MAX_LOGS_PER_TAG;
391
426
 
392
- return logs.map(
393
- logBuffers => logBuffers?.slice(start, end).map(logBuffer => TxScopedL2Log.fromBuffer(logBuffer)) ?? [],
394
- );
427
+ return logs.map(logBuffers => {
428
+ const deserialized = logBuffers?.slice(start, end).map(buf => TxScopedL2Log.fromBuffer(buf)) ?? [];
429
+ if (upToBlockNumber !== undefined) {
430
+ const cutoff = deserialized.findIndex(log => log.blockNumber > upToBlockNumber);
431
+ if (cutoff !== -1) {
432
+ return deserialized.slice(0, cutoff);
433
+ }
434
+ }
435
+ return deserialized;
436
+ });
395
437
  }
396
438
 
397
439
  /**
@@ -14,6 +14,7 @@ import {
14
14
  } from '@aztec/kv-store';
15
15
  import { InboxLeaf } from '@aztec/stdlib/messaging';
16
16
 
17
+ import { L1ToL2MessagesNotReadyError } from '../errors.js';
17
18
  import {
18
19
  type InboxMessage,
19
20
  deserializeInboxMessage,
@@ -40,6 +41,8 @@ export class MessageStore {
40
41
  #lastSynchedL1Block: AztecAsyncSingleton<Buffer>;
41
42
  /** Stores total messages stored */
42
43
  #totalMessageCount: AztecAsyncSingleton<bigint>;
44
+ /** Stores the checkpoint number whose message tree is currently being filled on L1. */
45
+ #inboxTreeInProgress: AztecAsyncSingleton<bigint>;
43
46
 
44
47
  #log = createLogger('archiver:message_store');
45
48
 
@@ -48,6 +51,7 @@ export class MessageStore {
48
51
  this.#l1ToL2MessageIndices = db.openMap('archiver_l1_to_l2_message_indices');
49
52
  this.#lastSynchedL1Block = db.openSingleton('archiver_last_l1_block_id');
50
53
  this.#totalMessageCount = db.openSingleton('archiver_l1_to_l2_message_count');
54
+ this.#inboxTreeInProgress = db.openSingleton('archiver_inbox_tree_in_progress');
51
55
  }
52
56
 
53
57
  public async getTotalL1ToL2MessageCount(): Promise<bigint> {
@@ -157,15 +161,6 @@ export class MessageStore {
157
161
  lastMessage = message;
158
162
  }
159
163
 
160
- // Update the L1 sync point to that of the last message added.
161
- const currentSyncPoint = await this.getSynchedL1Block();
162
- if (!currentSyncPoint || currentSyncPoint.l1BlockNumber < lastMessage!.l1BlockNumber) {
163
- await this.setSynchedL1Block({
164
- l1BlockNumber: lastMessage!.l1BlockNumber,
165
- l1BlockHash: lastMessage!.l1BlockHash,
166
- });
167
- }
168
-
169
164
  // Update total message count with the number of inserted messages.
170
165
  await this.increaseTotalMessageCount(messageCount);
171
166
  });
@@ -185,7 +180,29 @@ export class MessageStore {
185
180
  return msg ? deserializeInboxMessage(msg) : undefined;
186
181
  }
187
182
 
183
+ /** Returns the inbox tree-in-progress checkpoint number from L1, or undefined if not yet set. */
184
+ public getInboxTreeInProgress(): Promise<bigint | undefined> {
185
+ return this.#inboxTreeInProgress.getAsync();
186
+ }
187
+
188
+ /** Atomically updates the message sync state: the L1 sync point and the inbox tree-in-progress marker. */
189
+ public setMessageSyncState(l1Block: L1BlockId, treeInProgress: bigint | undefined): Promise<void> {
190
+ return this.db.transactionAsync(async () => {
191
+ await this.setSynchedL1Block(l1Block);
192
+ if (treeInProgress !== undefined) {
193
+ await this.#inboxTreeInProgress.set(treeInProgress);
194
+ } else {
195
+ await this.#inboxTreeInProgress.delete();
196
+ }
197
+ });
198
+ }
199
+
188
200
  public async getL1ToL2Messages(checkpointNumber: CheckpointNumber): Promise<Fr[]> {
201
+ const treeInProgress = await this.#inboxTreeInProgress.getAsync();
202
+ if (treeInProgress !== undefined && BigInt(checkpointNumber) >= treeInProgress) {
203
+ throw new L1ToL2MessagesNotReadyError(checkpointNumber, treeInProgress);
204
+ }
205
+
189
206
  const messages: Fr[] = [];
190
207
 
191
208
  const [startIndex, endIndex] = InboxLeaf.indexRangeForCheckpoint(checkpointNumber);
@@ -8,7 +8,7 @@ export type InboxMessage = {
8
8
  index: bigint;
9
9
  leaf: Fr;
10
10
  checkpointNumber: CheckpointNumber;
11
- l1BlockNumber: bigint; // L1 block number - NOT Aztec L2
11
+ l1BlockNumber: bigint;
12
12
  l1BlockHash: Buffer32;
13
13
  rollingHash: Buffer16;
14
14
  };
@@ -1,5 +1,6 @@
1
1
  import type { BlobClientInterface } from '@aztec/blob-client/client';
2
2
  import { type Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/blob-lib';
3
+ import { INITIAL_CHECKPOINT_NUMBER } from '@aztec/constants';
3
4
  import type { CheckpointProposedLog, InboxContract, MessageSentLog, RollupContract } from '@aztec/ethereum/contracts';
4
5
  import { MULTI_CALL_3_ADDRESS } from '@aztec/ethereum/contracts';
5
6
  import type { ViemPublicClient } from '@aztec/ethereum/types';
@@ -150,8 +151,10 @@ export class FakeL1State {
150
151
  // Computed from checkpoints based on L1 block visibility
151
152
  private pendingCheckpointNumber: CheckpointNumber = CheckpointNumber(0);
152
153
 
153
- // The L1 block number reported as "finalized" (defaults to the start block)
154
- private finalizedL1BlockNumber: bigint;
154
+ // The L1 block number reported as "finalized" (defaults to the start block).
155
+ // `undefined` simulates the startup window on a fresh devnet where
156
+ // `getBlock({ blockTag: 'finalized' })` fails with "finalized block not found".
157
+ private finalizedL1BlockNumber: bigint | undefined;
155
158
 
156
159
  constructor(private readonly config: FakeL1StateConfig) {
157
160
  this.l1BlockNumber = config.l1StartBlock;
@@ -287,8 +290,11 @@ export class FakeL1State {
287
290
  this.updatePendingCheckpointNumber();
288
291
  }
289
292
 
290
- /** Sets the L1 block number that will be reported as "finalized". */
291
- setFinalizedL1BlockNumber(blockNumber: bigint): void {
293
+ /**
294
+ * Sets the L1 block number that will be reported as "finalized". Pass `undefined` to
295
+ * simulate a chain that does not yet have a finalized block (devnet startup).
296
+ */
297
+ setFinalizedL1BlockNumber(blockNumber: bigint | undefined): void {
292
298
  this.finalizedL1BlockNumber = blockNumber;
293
299
  }
294
300
 
@@ -465,21 +471,44 @@ export class FakeL1State {
465
471
  createMockInboxContract(_publicClient: MockProxy<ViemPublicClient>): MockProxy<InboxContract> {
466
472
  const mockInbox = mock<InboxContract>();
467
473
 
468
- mockInbox.getState.mockImplementation(() =>
469
- Promise.resolve({
470
- messagesRollingHash: this.messagesRollingHash,
471
- totalMessagesInserted: BigInt(this.messages.length),
472
- treeInProgress: 0n,
473
- }),
474
- );
474
+ mockInbox.getState.mockImplementation((opts: { blockTag?: string; blockNumber?: bigint } = {}) => {
475
+ // Filter messages visible at the given block number (or all if not specified)
476
+ const blockNumber = opts.blockNumber ?? this.l1BlockNumber;
477
+ const visibleMessages = this.messages.filter(m => m.l1BlockNumber <= blockNumber);
478
+
479
+ // treeInProgress must be > any sealed checkpoint. On L1, a checkpoint can only be proposed
480
+ // after its messages are sealed, so treeInProgress > checkpointNumber for all published checkpoints.
481
+ const maxFromMessages =
482
+ visibleMessages.length > 0 ? Math.max(...visibleMessages.map(m => Number(m.checkpointNumber))) + 1 : 0;
483
+ const maxFromCheckpoints =
484
+ this.checkpoints.length > 0
485
+ ? Math.max(
486
+ ...this.checkpoints
487
+ .filter(cp => !cp.pruned && cp.l1BlockNumber <= blockNumber)
488
+ .map(cp => Number(cp.checkpointNumber)),
489
+ 0,
490
+ ) + 1
491
+ : 0;
492
+ const treeInProgress = Math.max(maxFromMessages, maxFromCheckpoints, INITIAL_CHECKPOINT_NUMBER);
493
+
494
+ // Compute rolling hash only for visible messages
495
+ const rollingHash =
496
+ visibleMessages.length > 0 ? visibleMessages[visibleMessages.length - 1].rollingHash : Buffer16.ZERO;
497
+
498
+ return Promise.resolve({
499
+ messagesRollingHash: rollingHash,
500
+ totalMessagesInserted: BigInt(visibleMessages.length),
501
+ treeInProgress: BigInt(treeInProgress),
502
+ });
503
+ });
475
504
 
476
505
  // Mock the wrapper methods for fetching message events
477
506
  mockInbox.getMessageSentEvents.mockImplementation((fromBlock: bigint, toBlock: bigint) =>
478
507
  Promise.resolve(this.getMessageSentLogs(fromBlock, toBlock)),
479
508
  );
480
509
 
481
- mockInbox.getMessageSentEventByHash.mockImplementation((hash: string, fromBlock: bigint, toBlock: bigint) =>
482
- Promise.resolve(this.getMessageSentLogs(fromBlock, toBlock, hash)),
510
+ mockInbox.getMessageSentEventByHash.mockImplementation((msgHash: string, aroundL1BlockNumber: bigint) =>
511
+ Promise.resolve(this.getMessageSentLogByHash(msgHash, aroundL1BlockNumber) as MessageSentLog),
483
512
  );
484
513
 
485
514
  return mockInbox;
@@ -495,6 +524,11 @@ export class FakeL1State {
495
524
  publicClient.getBlock.mockImplementation((async (args: { blockNumber?: bigint; blockTag?: string } = {}) => {
496
525
  let blockNum: bigint;
497
526
  if (args.blockTag === 'finalized') {
527
+ if (this.finalizedL1BlockNumber === undefined) {
528
+ throw Object.assign(new Error('finalized block not found'), {
529
+ details: 'finalized block not found',
530
+ });
531
+ }
498
532
  blockNum = this.finalizedL1BlockNumber;
499
533
  } else {
500
534
  blockNum = args.blockNumber ?? (await publicClient.getBlockNumber());
@@ -520,10 +554,10 @@ export class FakeL1State {
520
554
  createMockBlobClient(): MockProxy<BlobClientInterface> {
521
555
  const blobClient = mock<BlobClientInterface>();
522
556
 
523
- // The blockId is the transaction's blockHash, which we set to the checkpoint's archive root
557
+ // The blockId is the L1 block hash, which we derive from the L1 block number
524
558
  blobClient.getBlobSidecar.mockImplementation((blockId: `0x${string}`) =>
525
559
  Promise.resolve(
526
- this.checkpoints.find(cpData => cpData.checkpoint.archive.root.toString() === blockId)?.blobs ?? [],
560
+ this.checkpoints.find(cpData => Buffer32.fromBigInt(cpData.l1BlockNumber).toString() === blockId)?.blobs ?? [],
527
561
  ),
528
562
  );
529
563
 
@@ -599,6 +633,29 @@ export class FakeL1State {
599
633
  }));
600
634
  }
601
635
 
636
+ private getMessageSentLogByHash(msgHash: string, aroundL1BlockNumber: bigint): MessageSentLog | undefined {
637
+ const msg = this.messages.find(
638
+ msg =>
639
+ msg.leaf.toString() === msgHash &&
640
+ msg.l1BlockNumber >= aroundL1BlockNumber - 5n &&
641
+ msg.l1BlockNumber <= aroundL1BlockNumber + 5n,
642
+ );
643
+ if (!msg) {
644
+ return undefined;
645
+ }
646
+ return {
647
+ l1BlockNumber: msg.l1BlockNumber,
648
+ l1BlockHash: Buffer32.fromBigInt(msg.l1BlockNumber),
649
+ l1TransactionHash: `0x${msg.l1BlockNumber.toString(16)}` as `0x${string}`,
650
+ args: {
651
+ checkpointNumber: msg.checkpointNumber,
652
+ index: msg.index,
653
+ leaf: msg.leaf,
654
+ rollingHash: msg.rollingHash,
655
+ },
656
+ };
657
+ }
658
+
602
659
  private async makeRollupTx(
603
660
  checkpoint: Checkpoint,
604
661
  signers: Secp256k1Signer[],
@@ -44,6 +44,7 @@ export class MockL1ToL2MessageSource implements L1ToL2MessageSource {
44
44
  checkpointed: tip,
45
45
  proven: tip,
46
46
  finalized: tip,
47
+ proposedCheckpoint: tip,
47
48
  });
48
49
  }
49
50
  }