@aztec-labs/archiver 6.0.0-nightly.20260829

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 (174) hide show
  1. package/README.md +178 -0
  2. package/dest/archiver.d.ts +194 -0
  3. package/dest/archiver.d.ts.map +1 -0
  4. package/dest/archiver.js +948 -0
  5. package/dest/config.d.ts +34 -0
  6. package/dest/config.d.ts.map +1 -0
  7. package/dest/config.js +100 -0
  8. package/dest/errors.d.ts +97 -0
  9. package/dest/errors.d.ts.map +1 -0
  10. package/dest/errors.js +141 -0
  11. package/dest/factory.d.ts +32 -0
  12. package/dest/factory.d.ts.map +1 -0
  13. package/dest/factory.js +183 -0
  14. package/dest/index.d.ts +20 -0
  15. package/dest/index.d.ts.map +1 -0
  16. package/dest/index.js +18 -0
  17. package/dest/interfaces.d.ts +9 -0
  18. package/dest/interfaces.d.ts.map +1 -0
  19. package/dest/interfaces.js +3 -0
  20. package/dest/l1/bin/retrieve-calldata.d.ts +3 -0
  21. package/dest/l1/bin/retrieve-calldata.d.ts.map +1 -0
  22. package/dest/l1/bin/retrieve-calldata.js +152 -0
  23. package/dest/l1/calldata_retriever.d.ts +143 -0
  24. package/dest/l1/calldata_retriever.d.ts.map +1 -0
  25. package/dest/l1/calldata_retriever.js +413 -0
  26. package/dest/l1/data_retrieval.d.ts +102 -0
  27. package/dest/l1/data_retrieval.d.ts.map +1 -0
  28. package/dest/l1/data_retrieval.js +308 -0
  29. package/dest/l1/debug_tx.d.ts +19 -0
  30. package/dest/l1/debug_tx.d.ts.map +1 -0
  31. package/dest/l1/debug_tx.js +73 -0
  32. package/dest/l1/spire_proposer.d.ts +70 -0
  33. package/dest/l1/spire_proposer.d.ts.map +1 -0
  34. package/dest/l1/spire_proposer.js +149 -0
  35. package/dest/l1/trace_tx.d.ts +43 -0
  36. package/dest/l1/trace_tx.d.ts.map +1 -0
  37. package/dest/l1/trace_tx.js +91 -0
  38. package/dest/l1/types.d.ts +12 -0
  39. package/dest/l1/types.d.ts.map +1 -0
  40. package/dest/l1/types.js +3 -0
  41. package/dest/l1/validate_historical_logs.d.ts +23 -0
  42. package/dest/l1/validate_historical_logs.d.ts.map +1 -0
  43. package/dest/l1/validate_historical_logs.js +108 -0
  44. package/dest/l1/validate_trace.d.ts +32 -0
  45. package/dest/l1/validate_trace.d.ts.map +1 -0
  46. package/dest/l1/validate_trace.js +154 -0
  47. package/dest/modules/contract_data_source_adapter.d.ts +25 -0
  48. package/dest/modules/contract_data_source_adapter.d.ts.map +1 -0
  49. package/dest/modules/contract_data_source_adapter.js +32 -0
  50. package/dest/modules/data_source_base.d.ts +116 -0
  51. package/dest/modules/data_source_base.d.ts.map +1 -0
  52. package/dest/modules/data_source_base.js +334 -0
  53. package/dest/modules/data_store_updater.d.ts +105 -0
  54. package/dest/modules/data_store_updater.d.ts.map +1 -0
  55. package/dest/modules/data_store_updater.js +392 -0
  56. package/dest/modules/instrumentation.d.ts +63 -0
  57. package/dest/modules/instrumentation.d.ts.map +1 -0
  58. package/dest/modules/instrumentation.js +166 -0
  59. package/dest/modules/l1_synchronizer.d.ts +77 -0
  60. package/dest/modules/l1_synchronizer.d.ts.map +1 -0
  61. package/dest/modules/l1_synchronizer.js +1369 -0
  62. package/dest/modules/outbox_trees_resolver.d.ts +64 -0
  63. package/dest/modules/outbox_trees_resolver.d.ts.map +1 -0
  64. package/dest/modules/outbox_trees_resolver.js +184 -0
  65. package/dest/modules/validation.d.ts +40 -0
  66. package/dest/modules/validation.d.ts.map +1 -0
  67. package/dest/modules/validation.js +122 -0
  68. package/dest/store/block_store.d.ts +304 -0
  69. package/dest/store/block_store.d.ts.map +1 -0
  70. package/dest/store/block_store.js +1252 -0
  71. package/dest/store/contract_class_store.d.ts +31 -0
  72. package/dest/store/contract_class_store.d.ts.map +1 -0
  73. package/dest/store/contract_class_store.js +98 -0
  74. package/dest/store/contract_instance_store.d.ts +51 -0
  75. package/dest/store/contract_instance_store.d.ts.map +1 -0
  76. package/dest/store/contract_instance_store.js +129 -0
  77. package/dest/store/data_stores.d.ts +68 -0
  78. package/dest/store/data_stores.d.ts.map +1 -0
  79. package/dest/store/data_stores.js +54 -0
  80. package/dest/store/function_names_cache.d.ts +17 -0
  81. package/dest/store/function_names_cache.d.ts.map +1 -0
  82. package/dest/store/function_names_cache.js +30 -0
  83. package/dest/store/l2_tips_cache.d.ts +25 -0
  84. package/dest/store/l2_tips_cache.d.ts.map +1 -0
  85. package/dest/store/l2_tips_cache.js +26 -0
  86. package/dest/store/log_store.d.ts +59 -0
  87. package/dest/store/log_store.d.ts.map +1 -0
  88. package/dest/store/log_store.js +310 -0
  89. package/dest/store/log_store_codec.d.ts +78 -0
  90. package/dest/store/log_store_codec.d.ts.map +1 -0
  91. package/dest/store/log_store_codec.js +110 -0
  92. package/dest/store/message_store.d.ts +111 -0
  93. package/dest/store/message_store.d.ts.map +1 -0
  94. package/dest/store/message_store.js +522 -0
  95. package/dest/structs/data_retrieval.d.ts +27 -0
  96. package/dest/structs/data_retrieval.d.ts.map +1 -0
  97. package/dest/structs/data_retrieval.js +5 -0
  98. package/dest/structs/inbox_message.d.ts +17 -0
  99. package/dest/structs/inbox_message.d.ts.map +1 -0
  100. package/dest/structs/inbox_message.js +33 -0
  101. package/dest/structs/published.d.ts +2 -0
  102. package/dest/structs/published.d.ts.map +1 -0
  103. package/dest/structs/published.js +1 -0
  104. package/dest/test/fake_l1_state.d.ts +220 -0
  105. package/dest/test/fake_l1_state.d.ts.map +1 -0
  106. package/dest/test/fake_l1_state.js +565 -0
  107. package/dest/test/index.d.ts +5 -0
  108. package/dest/test/index.d.ts.map +1 -0
  109. package/dest/test/index.js +6 -0
  110. package/dest/test/mock_archiver.d.ts +35 -0
  111. package/dest/test/mock_archiver.d.ts.map +1 -0
  112. package/dest/test/mock_archiver.js +93 -0
  113. package/dest/test/mock_l1_to_l2_message_source.d.ts +24 -0
  114. package/dest/test/mock_l1_to_l2_message_source.d.ts.map +1 -0
  115. package/dest/test/mock_l1_to_l2_message_source.js +79 -0
  116. package/dest/test/mock_l2_block_source.d.ts +139 -0
  117. package/dest/test/mock_l2_block_source.d.ts.map +1 -0
  118. package/dest/test/mock_l2_block_source.js +489 -0
  119. package/dest/test/mock_structs.d.ts +92 -0
  120. package/dest/test/mock_structs.d.ts.map +1 -0
  121. package/dest/test/mock_structs.js +188 -0
  122. package/dest/test/noop_l1_archiver.d.ts +34 -0
  123. package/dest/test/noop_l1_archiver.d.ts.map +1 -0
  124. package/dest/test/noop_l1_archiver.js +88 -0
  125. package/package.json +108 -0
  126. package/src/archiver.ts +785 -0
  127. package/src/config.ts +133 -0
  128. package/src/errors.ts +211 -0
  129. package/src/factory.ts +262 -0
  130. package/src/index.ts +29 -0
  131. package/src/interfaces.ts +9 -0
  132. package/src/l1/README.md +55 -0
  133. package/src/l1/bin/retrieve-calldata.ts +194 -0
  134. package/src/l1/calldata_retriever.ts +529 -0
  135. package/src/l1/data_retrieval.ts +498 -0
  136. package/src/l1/debug_tx.ts +98 -0
  137. package/src/l1/spire_proposer.ts +151 -0
  138. package/src/l1/trace_tx.ts +127 -0
  139. package/src/l1/types.ts +12 -0
  140. package/src/l1/validate_historical_logs.ts +140 -0
  141. package/src/l1/validate_trace.ts +228 -0
  142. package/src/modules/contract_data_source_adapter.ts +46 -0
  143. package/src/modules/data_source_base.ts +482 -0
  144. package/src/modules/data_store_updater.ts +520 -0
  145. package/src/modules/instrumentation.ts +217 -0
  146. package/src/modules/l1_synchronizer.ts +1296 -0
  147. package/src/modules/outbox_trees_resolver.ts +220 -0
  148. package/src/modules/validation.ts +186 -0
  149. package/src/store/block_store.ts +1604 -0
  150. package/src/store/contract_class_store.ts +126 -0
  151. package/src/store/contract_instance_store.ts +178 -0
  152. package/src/store/data_stores.ts +103 -0
  153. package/src/store/function_names_cache.ts +37 -0
  154. package/src/store/l2_tips_cache.ts +35 -0
  155. package/src/store/log_store.ts +380 -0
  156. package/src/store/log_store_codec.ts +143 -0
  157. package/src/store/message_store.ts +621 -0
  158. package/src/structs/data_retrieval.ts +27 -0
  159. package/src/structs/inbox_message.ts +48 -0
  160. package/src/structs/published.ts +1 -0
  161. package/src/test/fake_l1_state.ts +807 -0
  162. package/src/test/fixtures/debug_traceTransaction-multicall3.json +88 -0
  163. package/src/test/fixtures/debug_traceTransaction-multiplePropose.json +153 -0
  164. package/src/test/fixtures/debug_traceTransaction-proxied.json +122 -0
  165. package/src/test/fixtures/trace_transaction-multicall3.json +65 -0
  166. package/src/test/fixtures/trace_transaction-multiplePropose.json +319 -0
  167. package/src/test/fixtures/trace_transaction-proxied.json +128 -0
  168. package/src/test/fixtures/trace_transaction-randomRevert.json +216 -0
  169. package/src/test/index.ts +7 -0
  170. package/src/test/mock_archiver.ts +122 -0
  171. package/src/test/mock_l1_to_l2_message_source.ts +81 -0
  172. package/src/test/mock_l2_block_source.ts +620 -0
  173. package/src/test/mock_structs.ts +317 -0
  174. package/src/test/noop_l1_archiver.ts +157 -0
@@ -0,0 +1,621 @@
1
+ import type { L1BlockId } from '@aztec-labs/ethereum/l1-types';
2
+ import { Buffer32 } from '@aztec-labs/foundation/buffer';
3
+ import { Fr } from '@aztec-labs/foundation/curves/bn254';
4
+ import { toArray } from '@aztec-labs/foundation/iterable';
5
+ import { createLogger } from '@aztec-labs/foundation/log';
6
+ import { BufferReader, bigintToUInt64BE, numToUInt32BE, serializeToBuffer } from '@aztec-labs/foundation/serialize';
7
+ import {
8
+ type AztecAsyncKVStore,
9
+ type AztecAsyncMap,
10
+ type AztecAsyncMultiMap,
11
+ type AztecAsyncSingleton,
12
+ type CustomRange,
13
+ mapRange,
14
+ } from '@aztec-labs/kv-store';
15
+ import { type InboxBucket, updateInboxRollingHash } from '@aztec-labs/stdlib/messaging';
16
+
17
+ import { InboxBucketBoundaryNotSyncedError, InboxBucketNotSyncedError } from '../errors.js';
18
+ import { type InboxMessage, deserializeInboxMessage, serializeInboxMessage } from '../structs/inbox_message.js';
19
+
20
+ /**
21
+ * Persisted snapshot of an Inbox rolling-hash bucket. Mirrors the fields the on-chain Inbox tracks per bucket, plus
22
+ * the L1 block the bucket was opened in and the index span of its messages, so rollbacks and range queries can work
23
+ * off bucket records alone without scanning messages.
24
+ */
25
+ type BucketSnapshot = {
26
+ inboxRollingHash: Fr;
27
+ totalMsgCount: bigint;
28
+ timestamp: bigint;
29
+ l1BlockNumber: bigint;
30
+ msgCount: number;
31
+ firstMessageIndex: bigint;
32
+ lastMessageIndex: bigint;
33
+ };
34
+
35
+ function serializeBucketSnapshot(snapshot: BucketSnapshot): Buffer {
36
+ return serializeToBuffer([
37
+ snapshot.inboxRollingHash,
38
+ bigintToUInt64BE(snapshot.totalMsgCount),
39
+ bigintToUInt64BE(snapshot.timestamp),
40
+ bigintToUInt64BE(snapshot.l1BlockNumber),
41
+ numToUInt32BE(snapshot.msgCount),
42
+ bigintToUInt64BE(snapshot.firstMessageIndex),
43
+ bigintToUInt64BE(snapshot.lastMessageIndex),
44
+ ]);
45
+ }
46
+
47
+ function deserializeBucketSnapshot(buffer: Buffer): BucketSnapshot {
48
+ const reader = BufferReader.asReader(buffer);
49
+ const inboxRollingHash = reader.readObject(Fr);
50
+ const totalMsgCount = reader.readUInt64();
51
+ const timestamp = reader.readUInt64();
52
+ const l1BlockNumber = reader.readUInt64();
53
+ const msgCount = reader.readNumber();
54
+ const firstMessageIndex = reader.readUInt64();
55
+ const lastMessageIndex = reader.readUInt64();
56
+ return { inboxRollingHash, totalMsgCount, timestamp, l1BlockNumber, msgCount, firstMessageIndex, lastMessageIndex };
57
+ }
58
+
59
+ /** The messages of a single Inbox bucket within an incoming batch, in insertion order. */
60
+ type IncomingBucket = {
61
+ seq: bigint;
62
+ messages: InboxMessage[];
63
+ };
64
+
65
+ /**
66
+ * Splits an incoming batch of messages into per-bucket groups, in delivery order. Messages arrive ordered by index
67
+ * and a bucket's messages are contiguous within that order, so a group ends as soon as the bucket sequence changes.
68
+ */
69
+ function groupMessagesByBucket(messages: InboxMessage[]): IncomingBucket[] {
70
+ const buckets: IncomingBucket[] = [];
71
+ for (const message of messages) {
72
+ const current = buckets.at(-1);
73
+ if (current !== undefined && current.seq === message.bucketSeq) {
74
+ current.messages.push(message);
75
+ } else {
76
+ buckets.push({ seq: message.bucketSeq, messages: [message] });
77
+ }
78
+ }
79
+ return buckets;
80
+ }
81
+
82
+ // The genesis sentinel bucket: sequence 0 with a zero rolling hash and no messages, mirroring the
83
+ // on-chain Inbox's base case. The archiver never ingests a snapshot for it (no message is absorbed into sequence 0), so
84
+ // it is synthesized on read. Consumers use its sequence number and zero total; its deploy-time timestamp is not tracked
85
+ // here and is unused.
86
+ const GENESIS_INBOX_BUCKET: InboxBucket = {
87
+ seq: 0n,
88
+ inboxRollingHash: Fr.ZERO,
89
+ totalMsgCount: 0n,
90
+ timestamp: 0n,
91
+ msgCount: 0,
92
+ lastMessageIndex: 0n,
93
+ };
94
+
95
+ export class MessageStoreError extends Error {
96
+ constructor(
97
+ message: string,
98
+ public readonly inboxMessage: InboxMessage,
99
+ ) {
100
+ super(message);
101
+ this.name = 'MessageStoreError';
102
+ }
103
+ }
104
+
105
+ export class MessageStore {
106
+ /** Maps from message index to serialized InboxMessage */
107
+ #l1ToL2Messages: AztecAsyncMap<number, Buffer>;
108
+ /** Maps from hex-stringified message leaf to its index */
109
+ #l1ToL2MessageIndices: AztecAsyncMap<string, bigint>;
110
+ /** Stores L1 block number and hash of the L1 synchpoint */
111
+ #lastSynchedL1Block: AztecAsyncSingleton<Buffer>;
112
+ /** Stores total messages stored */
113
+ #totalMessageCount: AztecAsyncSingleton<bigint>;
114
+ /** Stores the L1 finalized block as of the last successful message sync. */
115
+ #messagesFinalizedL1Block: AztecAsyncSingleton<Buffer>;
116
+ /** Maps from Inbox bucket sequence number to its serialized snapshot. */
117
+ #inboxBuckets: AztecAsyncMap<number, Buffer>;
118
+ /**
119
+ * Maps from a bucket's L1 timestamp (key) to the sequence numbers of the buckets opened at that timestamp. Holds
120
+ * several sequences per timestamp when a full bucket rolls over within one L1 block, so that deleting one bucket
121
+ * leaves its rollover siblings indexed.
122
+ */
123
+ #bucketTimestampToSeq: AztecAsyncMultiMap<number, number>;
124
+
125
+ #log = createLogger('archiver:message_store');
126
+
127
+ constructor(private db: AztecAsyncKVStore) {
128
+ this.#l1ToL2Messages = db.openMap('archiver_l1_to_l2_messages');
129
+ this.#l1ToL2MessageIndices = db.openMap('archiver_l1_to_l2_message_indices');
130
+ this.#lastSynchedL1Block = db.openSingleton('archiver_last_l1_block_id');
131
+ this.#totalMessageCount = db.openSingleton('archiver_l1_to_l2_message_count');
132
+ this.#messagesFinalizedL1Block = db.openSingleton('archiver_messages_finalized_l1_block');
133
+ this.#inboxBuckets = db.openMap('archiver_inbox_buckets');
134
+ this.#bucketTimestampToSeq = db.openMultiMap('archiver_inbox_bucket_timestamps');
135
+ }
136
+
137
+ public async getTotalL1ToL2MessageCount(): Promise<bigint> {
138
+ return (await this.#totalMessageCount.getAsync()) ?? 0n;
139
+ }
140
+
141
+ /** Gets the last L1 block synced. */
142
+ public async getSynchedL1Block(): Promise<L1BlockId | undefined> {
143
+ const buffer = await this.#lastSynchedL1Block.getAsync();
144
+ if (!buffer) {
145
+ return undefined;
146
+ }
147
+
148
+ const reader = BufferReader.asReader(buffer);
149
+ return { l1BlockNumber: reader.readUInt256(), l1BlockHash: Buffer32.fromBuffer(reader.readBytes(Buffer32.SIZE)) };
150
+ }
151
+
152
+ /** Sets the last L1 block synced */
153
+ public async setSynchedL1Block(l1Block: L1BlockId): Promise<void> {
154
+ const buffer = serializeToBuffer([l1Block.l1BlockNumber, l1Block.l1BlockHash]);
155
+ await this.#lastSynchedL1Block.set(buffer);
156
+ }
157
+
158
+ /** Gets the L1 finalized block as of the last successful message sync. */
159
+ public async getMessagesFinalizedL1Block(): Promise<L1BlockId | undefined> {
160
+ const buffer = await this.#messagesFinalizedL1Block.getAsync();
161
+ if (!buffer) {
162
+ return undefined;
163
+ }
164
+ const reader = BufferReader.asReader(buffer);
165
+ return { l1BlockNumber: reader.readUInt256(), l1BlockHash: Buffer32.fromBuffer(reader.readBytes(Buffer32.SIZE)) };
166
+ }
167
+
168
+ /** Monotonically advances the persisted L1 finalized block for message sync. Never regresses. */
169
+ private async maybeAdvanceFinalizedL1Block(l1Block: L1BlockId): Promise<void> {
170
+ const existing = await this.getMessagesFinalizedL1Block();
171
+ if (existing && l1Block.l1BlockNumber <= existing.l1BlockNumber) {
172
+ return;
173
+ }
174
+ const buffer = serializeToBuffer([l1Block.l1BlockNumber, l1Block.l1BlockHash]);
175
+ await this.#messagesFinalizedL1Block.set(buffer);
176
+ }
177
+
178
+ /**
179
+ * Appends L1 to L2 messages to the store, one whole Inbox bucket at a time.
180
+ *
181
+ * A bucket is opened and closed within a single L1 block, and callers retrieve Inbox logs in whole-L1-block ranges,
182
+ * so every message of a bucket reaches this method in the same call — including the rollover buckets a full block
183
+ * spills into. The bucket snapshots are derived from that: the batch is split per bucket sequence and each bucket
184
+ * gets a single snapshot built from its complete message set. Delivering only part of a bucket already held in the
185
+ * store is rejected, since the snapshot would then undercount the bucket. Delivering a stored bucket again from its
186
+ * first message is allowed: an L1 reorg can replace a bucket's tail, and the re-sync that follows replays the whole
187
+ * L1 block it lives in.
188
+ *
189
+ * Requires messages to be ordered by index and to continue the stored chain. Throws a `MessageStoreError` if
190
+ * messages arrive out of order, if the rolling hash chain breaks, or if a bucket arrives incomplete.
191
+ */
192
+ public addL1ToL2MessageBuckets(messages: InboxMessage[]): Promise<void> {
193
+ if (messages.length === 0) {
194
+ return Promise.resolve();
195
+ }
196
+
197
+ return this.db.transactionAsync(async () => {
198
+ let lastMessage = await this.getLastMessage();
199
+ let messageCount = 0;
200
+
201
+ const incomingBuckets = groupMessagesByBucket(messages);
202
+ await this.assertIncomingBucketsAreComplete(incomingBuckets);
203
+
204
+ for (const message of messages) {
205
+ // Check messages are inserted in increasing order, but allow reinserting messages.
206
+ if (lastMessage && message.index <= lastMessage.index) {
207
+ const existing = await this.#l1ToL2Messages.getAsync(this.indexToKey(message.index));
208
+ if (existing && deserializeInboxMessage(existing).inboxRollingHash.equals(message.inboxRollingHash)) {
209
+ // We reinsert instead of skipping in case the message was re-orged and got added in a different L1 block.
210
+ this.#log.trace(`Reinserting message with index ${message.index} in the store`);
211
+ await this.#l1ToL2Messages.set(this.indexToKey(message.index), serializeInboxMessage(message));
212
+ continue;
213
+ }
214
+
215
+ throw new MessageStoreError(
216
+ `Cannot insert L1 to L2 message with index ${message.index} before last message with index ${lastMessage.index}`,
217
+ message,
218
+ );
219
+ }
220
+
221
+ // Check the compact-indexed messages arrive contiguously: the global insertion index of
222
+ // each message is exactly one past the previous one.
223
+ const expectedIndex = lastMessage === undefined ? 0n : lastMessage.index + 1n;
224
+ if (message.index !== expectedIndex) {
225
+ throw new MessageStoreError(
226
+ `Invalid index ${message.index} for incoming L1 to L2 message ${message.leaf.toString()} ` +
227
+ `(expected ${expectedIndex})`,
228
+ message,
229
+ );
230
+ }
231
+
232
+ // Check the consensus rolling-hash chain is valid: each message's rolling hash must
233
+ // continue the chain from the previously inserted message.
234
+ const previousInboxRollingHash = lastMessage?.inboxRollingHash ?? Fr.ZERO;
235
+ const expectedInboxRollingHash = updateInboxRollingHash(previousInboxRollingHash, message.leaf);
236
+ if (!expectedInboxRollingHash.equals(message.inboxRollingHash)) {
237
+ throw new MessageStoreError(
238
+ `Invalid inbox rolling hash for incoming L1 to L2 message ${message.leaf.toString()} ` +
239
+ `with index ${message.index} ` +
240
+ `(expected ${expectedInboxRollingHash.toString()} from previous hash ${previousInboxRollingHash.toString()} ` +
241
+ `but got ${message.inboxRollingHash.toString()})`,
242
+ message,
243
+ );
244
+ }
245
+
246
+ // Perform the insertions.
247
+ await this.#l1ToL2Messages.set(this.indexToKey(message.index), serializeInboxMessage(message));
248
+ await this.#l1ToL2MessageIndices.set(this.leafToIndexKey(message.leaf), message.index);
249
+ messageCount++;
250
+
251
+ this.#log.trace(`Inserted L1 to L2 message ${message.leaf} with index ${message.index} into the store`);
252
+ lastMessage = message;
253
+ }
254
+
255
+ await this.writeIncomingBucketSnapshots(incomingBuckets);
256
+
257
+ // Update total message count with the number of inserted messages.
258
+ await this.increaseTotalMessageCount(messageCount);
259
+ });
260
+ }
261
+
262
+ /**
263
+ * Rejects a batch that delivers an Inbox bucket the store already holds without replaying it from its first message,
264
+ * or that opens a bucket older than the newest one stored. Either would produce a snapshot that disagrees with the
265
+ * messages it covers, since each snapshot is derived from the batch's messages for that bucket alone.
266
+ */
267
+ private async assertIncomingBucketsAreComplete(incomingBuckets: IncomingBucket[]): Promise<void> {
268
+ const newestStoredSeq = await this.getNewestBucketSeq();
269
+ let previousSeq: bigint | undefined;
270
+ for (const bucket of incomingBuckets) {
271
+ if (previousSeq !== undefined && bucket.seq <= previousSeq) {
272
+ throw new MessageStoreError(
273
+ `Inbox bucket ${bucket.seq} arrives after bucket ${previousSeq} in the same batch`,
274
+ bucket.messages[0],
275
+ );
276
+ }
277
+ previousSeq = bucket.seq;
278
+
279
+ const stored = await this.getBucketSnapshotBySeq(bucket.seq);
280
+ if (stored === undefined) {
281
+ if (newestStoredSeq !== undefined && bucket.seq <= newestStoredSeq) {
282
+ throw new MessageStoreError(
283
+ `Cannot open Inbox bucket ${bucket.seq} after bucket ${newestStoredSeq} has been stored`,
284
+ bucket.messages[0],
285
+ );
286
+ }
287
+ } else if (stored.firstMessageIndex !== bucket.messages[0].index) {
288
+ throw new MessageStoreError(
289
+ `Incomplete Inbox bucket ${bucket.seq}: stored messages start at index ${stored.firstMessageIndex} ` +
290
+ `but the batch starts at index ${bucket.messages[0].index}`,
291
+ bucket.messages[0],
292
+ );
293
+ }
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Writes one snapshot per bucket in the batch, each derived from the bucket's complete message set. Cumulative
299
+ * totals thread forward from the bucket preceding the batch, so a bucket re-delivered with extra messages shifts
300
+ * the totals of the buckets after it within the same batch.
301
+ */
302
+ private async writeIncomingBucketSnapshots(incomingBuckets: IncomingBucket[]): Promise<void> {
303
+ let cumulativeTotal = await this.getTotalMsgCountBeforeBucket(incomingBuckets[0].seq);
304
+ for (const { seq, messages } of incomingBuckets) {
305
+ const lastInBucket = messages.at(-1)!;
306
+ cumulativeTotal += BigInt(messages.length);
307
+ await this.writeBucketSnapshot(seq, {
308
+ inboxRollingHash: lastInBucket.inboxRollingHash,
309
+ totalMsgCount: cumulativeTotal,
310
+ timestamp: lastInBucket.bucketTimestamp,
311
+ l1BlockNumber: lastInBucket.l1BlockNumber,
312
+ msgCount: messages.length,
313
+ firstMessageIndex: messages[0].index,
314
+ lastMessageIndex: lastInBucket.index,
315
+ });
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Gets the L1 to L2 message index in the L1 to L2 message tree.
321
+ * @param l1ToL2Message - The L1 to L2 message.
322
+ * @returns The index of the L1 to L2 message in the L1 to L2 message tree (undefined if not found).
323
+ */
324
+ public getL1ToL2MessageIndex(l1ToL2Message: Fr): Promise<bigint | undefined> {
325
+ return this.#l1ToL2MessageIndices.getAsync(this.leafToIndexKey(l1ToL2Message));
326
+ }
327
+
328
+ public async getLastMessage(): Promise<InboxMessage | undefined> {
329
+ const [msg] = await toArray(this.#l1ToL2Messages.valuesAsync({ reverse: true, limit: 1 }));
330
+ return msg ? deserializeInboxMessage(msg) : undefined;
331
+ }
332
+
333
+ /**
334
+ * Atomically updates the message sync state: the L1 sync point and (optionally) the L1 finalized block as of this
335
+ * sync. The finalized block is advanced monotonically.
336
+ */
337
+ public setMessageSyncState(l1Block: L1BlockId, finalizedL1Block?: L1BlockId): Promise<void> {
338
+ return this.db.transactionAsync(async () => {
339
+ await this.setSynchedL1Block(l1Block);
340
+ if (finalizedL1Block !== undefined) {
341
+ await this.maybeAdvanceFinalizedL1Block(finalizedL1Block);
342
+ }
343
+ });
344
+ }
345
+
346
+ public async *iterateL1ToL2Messages(range: CustomRange<bigint> = {}): AsyncIterableIterator<InboxMessage> {
347
+ const entriesRange = mapRange(range, this.indexToKey);
348
+ for await (const msgBuffer of this.#l1ToL2Messages.valuesAsync(entriesRange)) {
349
+ yield deserializeInboxMessage(msgBuffer);
350
+ }
351
+ }
352
+
353
+ public removeL1ToL2Messages(startIndex: bigint): Promise<void> {
354
+ this.#log.debug(`Deleting L1 to L2 messages from index ${startIndex}`);
355
+ let deleteCount = 0;
356
+
357
+ return this.db.transactionAsync(async () => {
358
+ for await (const [key, msgBuffer] of this.#l1ToL2Messages.entriesAsync({
359
+ start: this.indexToKey(startIndex),
360
+ })) {
361
+ this.#log.trace(`Deleting L1 to L2 message with index ${key - 1} from the store`);
362
+ await this.#l1ToL2Messages.delete(key);
363
+ await this.#l1ToL2MessageIndices.delete(this.leafToIndexKey(deserializeInboxMessage(msgBuffer).leaf));
364
+ deleteCount++;
365
+ }
366
+ await this.increaseTotalMessageCount(-deleteCount);
367
+ await this.rewindBucketsAfterRemoval();
368
+ this.#log.warn(`Deleted ${deleteCount} L1 to L2 messages from index ${startIndex} from the store`);
369
+ });
370
+ }
371
+
372
+ /**
373
+ * Rewinds the Inbox bucket snapshots to match the messages left after a removal. Each bucket past the surviving tip
374
+ * is deleted together with its own timestamp index entry, leaving the entries of the rollover siblings it shares a
375
+ * timestamp with in place.
376
+ *
377
+ * The bucket holding the last surviving message is rewritten from the messages it has left, because a removal can
378
+ * cut inside a bucket: the synchronizer rolls back to the last message it still shares with L1 after a reorg, and
379
+ * that message need not be the last of its bucket. Must run inside the removal transaction, after the total message
380
+ * count has been updated.
381
+ */
382
+ private async rewindBucketsAfterRemoval(): Promise<void> {
383
+ const lastRemaining = await this.getLastMessage();
384
+ const boundarySeq = lastRemaining?.bucketSeq;
385
+
386
+ const deleteFromKey = boundarySeq === undefined ? 0 : this.bucketSeqToKey(boundarySeq) + 1;
387
+ for await (const [seqKey, snapBuffer] of this.#inboxBuckets.entriesAsync({ start: deleteFromKey })) {
388
+ const snapshot = deserializeBucketSnapshot(snapBuffer);
389
+ await this.#bucketTimestampToSeq.deleteValue(this.timestampToKey(snapshot.timestamp), seqKey);
390
+ await this.#inboxBuckets.delete(seqKey);
391
+ }
392
+
393
+ if (lastRemaining === undefined || boundarySeq === undefined) {
394
+ return;
395
+ }
396
+ let msgCount = 0;
397
+ for await (const msg of this.iterateL1ToL2Messages({ reverse: true })) {
398
+ if (msg.bucketSeq !== boundarySeq) {
399
+ break;
400
+ }
401
+ msgCount += 1;
402
+ }
403
+ const stored = await this.getSyncedBucketSnapshot(boundarySeq);
404
+ await this.writeBucketSnapshot(boundarySeq, {
405
+ ...stored,
406
+ inboxRollingHash: lastRemaining.inboxRollingHash,
407
+ totalMsgCount: await this.getTotalL1ToL2MessageCount(),
408
+ msgCount,
409
+ lastMessageIndex: lastRemaining.index,
410
+ });
411
+ }
412
+
413
+ /**
414
+ * Returns the Inbox bucket with the given sequence number, or undefined if it has not been synced. Sequence 0 is
415
+ * the genesis sentinel: the on-chain Inbox reserves it as the "consumed nothing" base case
416
+ * and never absorbs a message into it, so the archiver ingests no snapshot for it; it is synthesized here (rolling
417
+ * hash 0, total 0) so streaming consumers can resolve a genesis parent or an empty checkpoint's last-consumed bucket.
418
+ */
419
+ public async getInboxBucket(seq: bigint): Promise<InboxBucket | undefined> {
420
+ const snapshot = await this.getBucketSnapshotBySeq(seq);
421
+ if (snapshot !== undefined) {
422
+ return this.toInboxBucket(seq, snapshot);
423
+ }
424
+ return seq === 0n ? GENESIS_INBOX_BUCKET : undefined;
425
+ }
426
+
427
+ /**
428
+ * Returns the Inbox bucket whose cumulative message total equals `totalMsgCount`, or undefined if no synced bucket
429
+ * sits on that boundary. Sequence 0 (total 0) is the genesis sentinel base case; otherwise the
430
+ * message at global index `totalMsgCount - 1` is the last message of the bucket with that cumulative total, so its
431
+ * `bucketSeq` resolves the bucket. A total that does not land on a bucket boundary returns undefined.
432
+ */
433
+ public async getInboxBucketByTotalMsgCount(totalMsgCount: bigint): Promise<InboxBucket | undefined> {
434
+ if (totalMsgCount === 0n) {
435
+ return this.getInboxBucket(0n);
436
+ }
437
+ const buffer = await this.#l1ToL2Messages.getAsync(this.indexToKey(totalMsgCount - 1n));
438
+ if (buffer === undefined) {
439
+ return undefined;
440
+ }
441
+ const bucket = await this.getInboxBucket(deserializeInboxMessage(buffer).bucketSeq);
442
+ return bucket !== undefined && bucket.totalMsgCount === totalMsgCount ? bucket : undefined;
443
+ }
444
+
445
+ /**
446
+ * Returns the message leaves in the cumulative Inbox message-count range `[startLeafCount, endLeafCount)`, in
447
+ * insertion order. The bounds are compact L1-to-L2 tree leaf counts, which every block header
448
+ * carries, so consumers can ask for the messages a block or checkpoint consumed without resolving buckets
449
+ * themselves. Both bounds must land on a bucket boundary this archiver has synced; it throws otherwise, since a
450
+ * caller asking for a range always expects the messages in it.
451
+ */
452
+ public async getL1ToL2MessagesBetweenLeafCounts(startLeafCount: bigint, endLeafCount: bigint): Promise<Fr[]> {
453
+ if (startLeafCount > endLeafCount) {
454
+ throw new Error(`Invalid Inbox leaf count range [${startLeafCount}, ${endLeafCount})`);
455
+ }
456
+ const startBucket = await this.getBucketAtBoundary(startLeafCount);
457
+ const endBucket = await this.getBucketAtBoundary(endLeafCount);
458
+ return this.getL1ToL2MessagesBetweenBuckets(startBucket.seq, endBucket.seq);
459
+ }
460
+
461
+ /** Resolves the bucket ending at the given cumulative message count, failing loudly if there is none. */
462
+ private async getBucketAtBoundary(totalMsgCount: bigint): Promise<InboxBucket> {
463
+ const bucket = await this.getInboxBucketByTotalMsgCount(totalMsgCount);
464
+ if (bucket === undefined) {
465
+ throw new InboxBucketBoundaryNotSyncedError(totalMsgCount);
466
+ }
467
+ return bucket;
468
+ }
469
+
470
+ /**
471
+ * Returns the latest Inbox bucket opened at or before the given L1 timestamp, or undefined if every synced bucket
472
+ * was opened strictly after it.
473
+ */
474
+ public async getLatestInboxBucketAtOrBefore(timestamp: bigint): Promise<InboxBucket | undefined> {
475
+ // Bucket timestamps are non-decreasing in sequence number, so the bucket we want is the highest sequence indexed
476
+ // at the largest timestamp at-or-before the requested one. A reverse scan bounded above (inclusively) by that
477
+ // timestamp visits it first; rollover buckets share a timestamp, so keep the highest of its sequences.
478
+ let latestTimestampKey: number | undefined;
479
+ let latestSeqKey: number | undefined;
480
+ for await (const [timestampKey, seqKey] of this.#bucketTimestampToSeq.entriesAsync({
481
+ end: this.timestampToKey(timestamp),
482
+ reverse: true,
483
+ })) {
484
+ if (latestTimestampKey !== undefined && timestampKey !== latestTimestampKey) {
485
+ break;
486
+ }
487
+ latestTimestampKey = timestampKey;
488
+ latestSeqKey = latestSeqKey === undefined ? seqKey : Math.max(latestSeqKey, seqKey);
489
+ }
490
+ return latestSeqKey === undefined ? undefined : this.getInboxBucket(BigInt(latestSeqKey));
491
+ }
492
+
493
+ /**
494
+ * Returns the message leaves absorbed into buckets in the range `(fromExclusive, toInclusive]`, in insertion order.
495
+ * Both bounds must name buckets this archiver has synced, so that an empty result means the
496
+ * range holds no messages rather than hiding an unsynced bound; callers route the
497
+ * `InboxBucketNotSyncedError` to their own catch-up handling. Sequence 0 is the genesis base case and always
498
+ * resolves: the range then starts at the first message of the Inbox.
499
+ */
500
+ public async getL1ToL2MessagesBetweenBuckets(fromExclusive: bigint, toInclusive: bigint): Promise<Fr[]> {
501
+ if (fromExclusive > toInclusive) {
502
+ throw new Error(`Invalid Inbox bucket range (${fromExclusive}, ${toInclusive}]`);
503
+ }
504
+ if (toInclusive === 0n) {
505
+ return [];
506
+ }
507
+ const endIndexExclusive = (await this.getSyncedBucketSnapshot(toInclusive)).lastMessageIndex + 1n;
508
+ const startIndex =
509
+ fromExclusive === 0n ? 0n : (await this.getSyncedBucketSnapshot(fromExclusive)).lastMessageIndex + 1n;
510
+ return this.getMessageLeavesInIndexRange(startIndex, endIndexExclusive);
511
+ }
512
+
513
+ /** Collects the message leaves in the global index range `[startIndex, endIndexExclusive)`, in insertion order. */
514
+ private async getMessageLeavesInIndexRange(startIndex: bigint, endIndexExclusive: bigint): Promise<Fr[]> {
515
+ const leaves: Fr[] = [];
516
+ for await (const msgBuffer of this.#l1ToL2Messages.valuesAsync({
517
+ start: this.indexToKey(startIndex),
518
+ end: this.indexToKey(endIndexExclusive),
519
+ })) {
520
+ leaves.push(deserializeInboxMessage(msgBuffer).leaf);
521
+ }
522
+ return leaves;
523
+ }
524
+
525
+ private async getBucketSnapshotBySeq(seq: bigint): Promise<BucketSnapshot | undefined> {
526
+ const buffer = await this.#inboxBuckets.getAsync(this.bucketSeqToKey(seq));
527
+ return buffer && deserializeBucketSnapshot(buffer);
528
+ }
529
+
530
+ /** Reads a bucket snapshot, failing loudly if the archiver has not synced that bucket. */
531
+ private async getSyncedBucketSnapshot(seq: bigint): Promise<BucketSnapshot> {
532
+ const snapshot = await this.getBucketSnapshotBySeq(seq);
533
+ if (snapshot === undefined) {
534
+ throw new InboxBucketNotSyncedError(seq);
535
+ }
536
+ return snapshot;
537
+ }
538
+
539
+ private async writeBucketSnapshot(seq: bigint, snapshot: BucketSnapshot): Promise<void> {
540
+ // A reorg can re-deliver a bucket from an L1 block mined at a different timestamp, so drop the stale index entry.
541
+ const stored = await this.getBucketSnapshotBySeq(seq);
542
+ if (stored !== undefined && stored.timestamp !== snapshot.timestamp) {
543
+ await this.#bucketTimestampToSeq.deleteValue(this.timestampToKey(stored.timestamp), this.bucketSeqToKey(seq));
544
+ }
545
+ await this.#inboxBuckets.set(this.bucketSeqToKey(seq), serializeBucketSnapshot(snapshot));
546
+ await this.#bucketTimestampToSeq.set(this.timestampToKey(snapshot.timestamp), this.bucketSeqToKey(seq));
547
+ }
548
+
549
+ /** Returns the sequence number of the newest stored bucket, or undefined if none has been stored yet. */
550
+ private async getNewestBucketSeq(): Promise<bigint | undefined> {
551
+ const [seqKey] = await toArray(this.#inboxBuckets.keysAsync({ reverse: true, limit: 1 }));
552
+ return seqKey === undefined ? undefined : BigInt(seqKey);
553
+ }
554
+
555
+ /** Returns the cumulative Inbox message count through the newest stored bucket before the given sequence number. */
556
+ private async getTotalMsgCountBeforeBucket(seq: bigint): Promise<bigint> {
557
+ const [snapBuffer] = await toArray(
558
+ this.#inboxBuckets.valuesAsync({ end: this.bucketSeqToKey(seq) - 1, reverse: true, limit: 1 }),
559
+ );
560
+ return snapBuffer === undefined ? 0n : deserializeBucketSnapshot(snapBuffer).totalMsgCount;
561
+ }
562
+
563
+ private toInboxBucket(seq: bigint, snapshot: BucketSnapshot): InboxBucket {
564
+ return {
565
+ seq,
566
+ inboxRollingHash: snapshot.inboxRollingHash,
567
+ totalMsgCount: snapshot.totalMsgCount,
568
+ timestamp: snapshot.timestamp,
569
+ msgCount: snapshot.msgCount,
570
+ lastMessageIndex: snapshot.lastMessageIndex,
571
+ };
572
+ }
573
+
574
+ private bucketSeqToKey(seq: bigint): number {
575
+ return Number(seq);
576
+ }
577
+
578
+ private timestampToKey(timestamp: bigint): number {
579
+ return Number(timestamp);
580
+ }
581
+
582
+ /**
583
+ * Removes every L1 to L2 message inserted after the given L1 block, so the message store matches the L1 Inbox state
584
+ * as of that block. Used when rolling the archiver back to an earlier checkpoint, whose L1 block is passed here.
585
+ *
586
+ * A bucket lives entirely within one L1 block, so the cut always falls on a bucket boundary and can be found from
587
+ * the bucket snapshots alone, without reading the messages being removed.
588
+ */
589
+ public async rollbackL1ToL2MessagesAfterL1Block(l1BlockNumber: bigint): Promise<void> {
590
+ this.#log.debug(`Deleting L1 to L2 messages inserted after L1 block ${l1BlockNumber}`);
591
+ let removeFromIndex: bigint | undefined;
592
+ for await (const snapBuffer of this.#inboxBuckets.valuesAsync({ reverse: true })) {
593
+ const snapshot = deserializeBucketSnapshot(snapBuffer);
594
+ if (snapshot.l1BlockNumber <= l1BlockNumber) {
595
+ break;
596
+ }
597
+ removeFromIndex = snapshot.firstMessageIndex;
598
+ }
599
+ if (removeFromIndex !== undefined) {
600
+ await this.removeL1ToL2Messages(removeFromIndex);
601
+ }
602
+ }
603
+
604
+ private indexToKey(index: bigint): number {
605
+ return Number(index);
606
+ }
607
+
608
+ private leafToIndexKey(leaf: Fr): string {
609
+ return leaf.toString();
610
+ }
611
+
612
+ private async increaseTotalMessageCount(count: bigint | number): Promise<void> {
613
+ if (count === 0) {
614
+ return;
615
+ }
616
+ return await this.db.transactionAsync(async () => {
617
+ const lastTotalMessageCount = await this.getTotalL1ToL2MessageCount();
618
+ await this.#totalMessageCount.set(lastTotalMessageCount + BigInt(count));
619
+ });
620
+ }
621
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Data retrieved from logs
3
+ */
4
+ export type DataRetrieval<T> = {
5
+ /**
6
+ * Blocknumber of the last L1 block from which we obtained data.
7
+ */
8
+ lastProcessedL1BlockNumber: bigint;
9
+ /**
10
+ * The data returned.
11
+ */
12
+ retrievedData: T[];
13
+ };
14
+
15
+ /**
16
+ * Data retrieved from logs
17
+ */
18
+ export type SingletonDataRetrieval<T> = {
19
+ /**
20
+ * Blocknumber of the last L1 block from which we obtained data.
21
+ */
22
+ lastProcessedL1BlockNumber: bigint;
23
+ /**
24
+ * The data returned.
25
+ */
26
+ retrievedData: T;
27
+ };