@aztec/archiver 4.0.0-nightly.20260113 → 4.0.0-nightly.20260114

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 (47) hide show
  1. package/README.md +139 -22
  2. package/dest/archiver/archive_source_base.d.ts +75 -0
  3. package/dest/archiver/archive_source_base.d.ts.map +1 -0
  4. package/dest/archiver/archive_source_base.js +202 -0
  5. package/dest/archiver/archiver.d.ts +28 -167
  6. package/dest/archiver/archiver.d.ts.map +1 -1
  7. package/dest/archiver/archiver.js +46 -601
  8. package/dest/archiver/archiver_store_updates.d.ts +38 -0
  9. package/dest/archiver/archiver_store_updates.d.ts.map +1 -0
  10. package/dest/archiver/archiver_store_updates.js +212 -0
  11. package/dest/archiver/index.d.ts +3 -2
  12. package/dest/archiver/index.d.ts.map +1 -1
  13. package/dest/archiver/index.js +2 -0
  14. package/dest/archiver/kv_archiver_store/block_store.d.ts +1 -1
  15. package/dest/archiver/kv_archiver_store/block_store.js +1 -1
  16. package/dest/archiver/kv_archiver_store/contract_class_store.d.ts +1 -1
  17. package/dest/archiver/kv_archiver_store/contract_class_store.js +1 -1
  18. package/dest/archiver/kv_archiver_store/contract_instance_store.d.ts +1 -1
  19. package/dest/archiver/kv_archiver_store/contract_instance_store.js +1 -1
  20. package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts +169 -9
  21. package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts.map +1 -1
  22. package/dest/archiver/kv_archiver_store/kv_archiver_store.js +157 -49
  23. package/dest/archiver/l1/data_retrieval.d.ts +9 -11
  24. package/dest/archiver/l1/data_retrieval.d.ts.map +1 -1
  25. package/dest/archiver/l1/data_retrieval.js +32 -51
  26. package/dest/archiver/test/fake_l1_state.d.ts +173 -0
  27. package/dest/archiver/test/fake_l1_state.d.ts.map +1 -0
  28. package/dest/archiver/test/fake_l1_state.js +364 -0
  29. package/package.json +13 -13
  30. package/src/archiver/archive_source_base.ts +339 -0
  31. package/src/archiver/archiver.ts +62 -808
  32. package/src/archiver/archiver_store_updates.ts +321 -0
  33. package/src/archiver/index.ts +2 -1
  34. package/src/archiver/kv_archiver_store/block_store.ts +1 -1
  35. package/src/archiver/kv_archiver_store/contract_class_store.ts +1 -1
  36. package/src/archiver/kv_archiver_store/contract_instance_store.ts +1 -1
  37. package/src/archiver/kv_archiver_store/kv_archiver_store.ts +170 -8
  38. package/src/archiver/l1/data_retrieval.ts +51 -68
  39. package/src/archiver/test/fake_l1_state.ts +561 -0
  40. package/dest/archiver/archiver_store.d.ts +0 -315
  41. package/dest/archiver/archiver_store.d.ts.map +0 -1
  42. package/dest/archiver/archiver_store.js +0 -4
  43. package/dest/archiver/archiver_store_test_suite.d.ts +0 -8
  44. package/dest/archiver/archiver_store_test_suite.d.ts.map +0 -1
  45. package/dest/archiver/archiver_store_test_suite.js +0 -2770
  46. package/src/archiver/archiver_store.ts +0 -380
  47. package/src/archiver/archiver_store_test_suite.ts +0 -2842
@@ -0,0 +1,561 @@
1
+ import type { BlobClientInterface } from '@aztec/blob-client/client';
2
+ import { type Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/blob-lib';
3
+ import type { CheckpointProposedLog, InboxContract, MessageSentLog, RollupContract } from '@aztec/ethereum/contracts';
4
+ import { MULTI_CALL_3_ADDRESS } from '@aztec/ethereum/contracts';
5
+ import type { ViemPublicClient } from '@aztec/ethereum/types';
6
+ import { type BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
7
+ import { Buffer16, Buffer32 } from '@aztec/foundation/buffer';
8
+ import { Secp256k1Signer } from '@aztec/foundation/crypto/secp256k1-signer';
9
+ import { Fr } from '@aztec/foundation/curves/bn254';
10
+ import { EthAddress } from '@aztec/foundation/eth-address';
11
+ import { createLogger } from '@aztec/foundation/log';
12
+ import { RollupAbi } from '@aztec/l1-artifacts';
13
+ import { CommitteeAttestation, CommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
14
+ import { Checkpoint } from '@aztec/stdlib/checkpoint';
15
+ import { InboxLeaf } from '@aztec/stdlib/messaging';
16
+ import {
17
+ makeAndSignCommitteeAttestationsAndSigners,
18
+ makeCheckpointAttestationFromCheckpoint,
19
+ mockCheckpointAndMessages,
20
+ } from '@aztec/stdlib/testing';
21
+ import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
22
+
23
+ import { type MockProxy, mock } from 'jest-mock-extended';
24
+ import { type FormattedBlock, type Transaction, encodeFunctionData, multicall3Abi, toHex } from 'viem';
25
+
26
+ import { updateRollingHash } from '../structs/inbox_message.js';
27
+
28
+ /** Configuration for the fake L1 state. */
29
+ export type FakeL1StateConfig = {
30
+ /** Genesis archive root. */
31
+ genesisArchiveRoot: Fr;
32
+ /** L1 start block number. */
33
+ l1StartBlock: bigint;
34
+ /** L1 genesis time in seconds. */
35
+ l1GenesisTime: bigint;
36
+ /** Ethereum slot duration in seconds. */
37
+ ethereumSlotDuration: number;
38
+ /** Rollup address for mock contracts. */
39
+ rollupAddress: EthAddress;
40
+ /** Inbox address for mock contracts. */
41
+ inboxAddress: EthAddress;
42
+ };
43
+
44
+ /** Options for adding a checkpoint. */
45
+ type AddCheckpointOptions = {
46
+ /** L1 block number where checkpoint was proposed. */
47
+ l1BlockNumber: bigint;
48
+ /** Number of L2 blocks in the checkpoint. Default: 1 */
49
+ numBlocks?: number;
50
+ /** Number of transactions per block. Default: 4 */
51
+ txsPerBlock?: number;
52
+ /** Max number of effects per tx (for generating large blobs). Default: undefined */
53
+ maxEffects?: number;
54
+ /** Signers for attestations. Default: none */
55
+ signers?: Secp256k1Signer[];
56
+ /** Override slot number. */
57
+ slotNumber?: SlotNumber;
58
+ /** Override previous archive. */
59
+ previousArchive?: AppendOnlyTreeSnapshot;
60
+ /** Timestamp for the checkpoint. */
61
+ timestamp?: bigint;
62
+ /** Number of L1-to-L2 messages. Default: 3 */
63
+ numL1ToL2Messages?: number;
64
+ /** L1 block number where messages were sent. Default: l1BlockNumber - 3 */
65
+ messagesL1BlockNumber?: bigint;
66
+ };
67
+
68
+ /** Result from adding a checkpoint. */
69
+ type AddCheckpointResult = {
70
+ /** The checkpoint that was created. */
71
+ checkpoint: Checkpoint;
72
+ /** The L1-to-L2 messages for this checkpoint. */
73
+ messages: Fr[];
74
+ };
75
+
76
+ /** Data stored for a checkpoint. */
77
+ type CheckpointData = {
78
+ checkpointNumber: CheckpointNumber;
79
+ checkpoint: Checkpoint;
80
+ l1BlockNumber: bigint;
81
+ tx: Transaction;
82
+ blobHashes: `0x${string}`[];
83
+ blobs: Blob[];
84
+ signers: Secp256k1Signer[];
85
+ /** If true, archiveAt will ignore it */
86
+ pruned?: boolean;
87
+ };
88
+
89
+ /** Data stored for a message. */
90
+ type MessageData = {
91
+ l1BlockNumber: bigint;
92
+ checkpointNumber: CheckpointNumber;
93
+ index: bigint;
94
+ leaf: Fr;
95
+ rollingHash: Buffer16;
96
+ };
97
+
98
+ /**
99
+ * Stateful fake for L1 data used by the archiver.
100
+ *
101
+ * This class simulates the L1 blockchain state that the archiver reads from:
102
+ * - RollupContract: status(), archiveAt(), getVersion(), getTargetCommitteeSize(), CheckpointProposed events
103
+ * - InboxContract: getState(), MessageSent events
104
+ * - PublicClient: getBlockNumber(), getBlock(), getTransaction()
105
+ * - BlobClient: getBlobSidecar()
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * const fake = new FakeL1State(config);
110
+ *
111
+ * // Add checkpoint (creates messages automatically)
112
+ * const { checkpoint, messages } = await fake.addCheckpoint(CheckpointNumber(1), { l1BlockNumber: 101n });
113
+ * fake.setL1BlockNumber(105n);
114
+ *
115
+ * // Status auto-updated
116
+ * expect(fake.getRollupStatus().pendingCheckpointNumber).toEqual(CheckpointNumber(1));
117
+ * ```
118
+ */
119
+ export class FakeL1State {
120
+ private readonly log = createLogger('archiver:test:fake-l1');
121
+ private l1BlockNumber: bigint;
122
+ private checkpoints: CheckpointData[] = [];
123
+ private messages: MessageData[] = [];
124
+ private messagesRollingHash: Buffer16 = Buffer16.ZERO;
125
+ private lastArchive: AppendOnlyTreeSnapshot;
126
+ private provenCheckpointNumber: CheckpointNumber = CheckpointNumber(0);
127
+ private targetCommitteeSize: number = 0;
128
+ private version: bigint = 1n;
129
+
130
+ // Computed from checkpoints based on L1 block visibility
131
+ private pendingCheckpointNumber: CheckpointNumber = CheckpointNumber(0);
132
+
133
+ constructor(private readonly config: FakeL1StateConfig) {
134
+ this.l1BlockNumber = config.l1StartBlock;
135
+ this.lastArchive = new AppendOnlyTreeSnapshot(config.genesisArchiveRoot, 1);
136
+ }
137
+
138
+ /**
139
+ * Adds messages for a checkpoint. Returns the message leaves.
140
+ * Auto-updates rolling hash.
141
+ *
142
+ * Note: For most use cases, use `addCheckpoint` which creates both checkpoint and messages.
143
+ * Use this method only when you need to add messages without creating a checkpoint (e.g., for reorg tests).
144
+ */
145
+ addMessages(checkpointNumber: CheckpointNumber, l1BlockNumber: bigint, messageLeaves: Fr[]): void {
146
+ messageLeaves.forEach((leaf, i) => {
147
+ const index = InboxLeaf.smallestIndexForCheckpoint(checkpointNumber) + BigInt(i);
148
+ this.messagesRollingHash = updateRollingHash(this.messagesRollingHash, leaf);
149
+
150
+ this.messages.push({
151
+ l1BlockNumber,
152
+ checkpointNumber,
153
+ index,
154
+ leaf,
155
+ rollingHash: this.messagesRollingHash,
156
+ });
157
+ });
158
+ }
159
+
160
+ /**
161
+ * Creates and adds a checkpoint with its L1-to-L2 messages.
162
+ * Returns both the checkpoint and the message leaves.
163
+ * Auto-chains from lastArchive, auto-updates pending status if L1 block >= checkpoint's L1 block.
164
+ */
165
+ async addCheckpoint(checkpointNumber: CheckpointNumber, options: AddCheckpointOptions): Promise<AddCheckpointResult> {
166
+ this.log.warn(`Adding checkpoint ${checkpointNumber}`);
167
+
168
+ const {
169
+ l1BlockNumber,
170
+ numBlocks = 1,
171
+ txsPerBlock = 4,
172
+ maxEffects,
173
+ signers = [],
174
+ slotNumber,
175
+ previousArchive = this.lastArchive,
176
+ timestamp,
177
+ numL1ToL2Messages = 3,
178
+ messagesL1BlockNumber = l1BlockNumber - 3n,
179
+ } = options;
180
+
181
+ // Create the checkpoint using the stdlib helper
182
+ // Only pass slotNumber and timestamp if they're defined to avoid overwriting defaults
183
+ const { checkpoint, messages, lastArchive } = await mockCheckpointAndMessages(checkpointNumber, {
184
+ startBlockNumber: this.getNextBlockNumber(checkpointNumber),
185
+ numBlocks,
186
+ numTxsPerBlock: txsPerBlock,
187
+ numL1ToL2Messages,
188
+ previousArchive,
189
+ ...(slotNumber !== undefined ? { slotNumber } : {}),
190
+ ...(timestamp !== undefined ? { timestamp } : {}),
191
+ ...(maxEffects !== undefined ? { maxEffects } : {}),
192
+ });
193
+
194
+ // Store the messages internally so they match the checkpoint's inHash
195
+ this.addMessages(checkpointNumber, messagesL1BlockNumber, messages);
196
+
197
+ // Create the transaction and blobs
198
+ const tx = this.makeRollupTx(checkpoint, signers);
199
+ const blobHashes = this.makeVersionedBlobHashes(checkpoint);
200
+ const blobs = this.makeBlobsFromCheckpoint(checkpoint);
201
+
202
+ // Store the checkpoint data
203
+ this.checkpoints.push({
204
+ checkpointNumber,
205
+ checkpoint,
206
+ l1BlockNumber,
207
+ tx,
208
+ blobHashes,
209
+ blobs,
210
+ signers,
211
+ });
212
+
213
+ // Update last archive for auto-chaining
214
+ this.lastArchive = lastArchive ?? checkpoint.blocks.at(-1)!.archive;
215
+
216
+ // Auto-update pending checkpoint number
217
+ this.updatePendingCheckpointNumber();
218
+
219
+ return { checkpoint, messages };
220
+ }
221
+
222
+ /**
223
+ * Sets the current L1 block number.
224
+ * Auto-updates pending checkpoint number based on visible checkpoints.
225
+ */
226
+ setL1BlockNumber(blockNumber: bigint): void {
227
+ this.l1BlockNumber = blockNumber;
228
+ this.updatePendingCheckpointNumber();
229
+ }
230
+
231
+ /** Marks a checkpoint as proven. Updates provenCheckpointNumber. */
232
+ markCheckpointAsProven(checkpointNumber: CheckpointNumber): void {
233
+ this.provenCheckpointNumber = checkpointNumber;
234
+ }
235
+
236
+ /** Sets the target committee size for attestation validation. */
237
+ setTargetCommitteeSize(size: number): void {
238
+ this.targetCommitteeSize = size;
239
+ }
240
+
241
+ /**
242
+ * Removes all entries for a checkpoint number (simulates L1 reorg or prune).
243
+ * Note: Does NOT remove messages for this checkpoint (use numL1ToL2Messages: 0 when re-adding).
244
+ * Auto-updates pending status.
245
+ */
246
+ removeCheckpoint(checkpointNumber: CheckpointNumber): void {
247
+ this.checkpoints = this.checkpoints.filter(cpData => cpData.checkpointNumber !== checkpointNumber);
248
+ this.updatePendingCheckpointNumber();
249
+ }
250
+
251
+ /**
252
+ * Removes messages after a given total index (simulates L1 reorg).
253
+ * Auto-updates rolling hash.
254
+ */
255
+ removeMessagesAfter(totalIndex: number): void {
256
+ this.messages = this.messages.slice(0, totalIndex);
257
+ // Recalculate rolling hash
258
+ this.messagesRollingHash = this.messages.reduce((hash, msg) => updateRollingHash(hash, msg.leaf), Buffer16.ZERO);
259
+ }
260
+
261
+ /**
262
+ * Simulates a pruned checkpoint by marking all entries with this number as pruned.
263
+ * The event will still be returned, but archiveAt() will return the previous checkpoint's archive
264
+ * (or genesis if no previous checkpoint), causing a mismatch that the archiver will detect.
265
+ */
266
+ markCheckpointAsPruned(checkpointNumber: CheckpointNumber): void {
267
+ for (const cpData of this.checkpoints) {
268
+ if (cpData.checkpointNumber === checkpointNumber) {
269
+ cpData.pruned = true;
270
+ }
271
+ }
272
+ this.updatePendingCheckpointNumber();
273
+ }
274
+
275
+ /**
276
+ * Moves messages at a given L1 block to a new L1 block.
277
+ * Useful for simulating partial message visibility (messages at higher L1 blocks won't be fetched).
278
+ */
279
+ moveMessagesToL1Block(fromL1Block: bigint, toL1Block: bigint): void {
280
+ this.messages.forEach(msg => {
281
+ if (msg.l1BlockNumber === fromL1Block) {
282
+ msg.l1BlockNumber = toL1Block;
283
+ }
284
+ });
285
+ }
286
+
287
+ /**
288
+ * Moves a specific message (by index in the messages array) to a new L1 block.
289
+ */
290
+ moveMessageAtIndexToL1Block(index: number, toL1Block: bigint): void {
291
+ if (this.messages[index]) {
292
+ this.messages[index].l1BlockNumber = toL1Block;
293
+ }
294
+ }
295
+
296
+ /** Gets current rollup status. */
297
+ getRollupStatus(): {
298
+ provenCheckpointNumber: CheckpointNumber;
299
+ pendingCheckpointNumber: CheckpointNumber;
300
+ provenArchive: Fr;
301
+ pendingArchive: Fr;
302
+ } {
303
+ return {
304
+ provenCheckpointNumber: this.provenCheckpointNumber,
305
+ pendingCheckpointNumber: this.pendingCheckpointNumber,
306
+ provenArchive: this.getArchiveAt(this.provenCheckpointNumber),
307
+ pendingArchive: this.getArchiveAt(this.pendingCheckpointNumber),
308
+ };
309
+ }
310
+
311
+ /** Gets the last archive (for manual chaining if needed). */
312
+ getLastArchive(): AppendOnlyTreeSnapshot {
313
+ return this.lastArchive;
314
+ }
315
+
316
+ /** Gets the latest checkpoint entry by number (returns the last added one). */
317
+ getCheckpoint(checkpointNumber: CheckpointNumber): Checkpoint | undefined {
318
+ return this.checkpoints.findLast(cpData => cpData.checkpointNumber === checkpointNumber)?.checkpoint;
319
+ }
320
+
321
+ /** Gets messages for a checkpoint. */
322
+ getMessages(checkpointNumber: CheckpointNumber): Fr[] {
323
+ return this.messages.filter(m => m.checkpointNumber === checkpointNumber).map(m => m.leaf);
324
+ }
325
+
326
+ /** Gets the blobs for a checkpoint. */
327
+ getCheckpointBlobs(checkpointNumber: CheckpointNumber): Blob[] {
328
+ return this.checkpoints.findLast(cpData => cpData.checkpointNumber === checkpointNumber)?.blobs ?? [];
329
+ }
330
+
331
+ /** Creates mock RollupContract that reads from this fake state. */
332
+ createMockRollupContract(_publicClient: MockProxy<ViemPublicClient>): MockProxy<RollupContract> {
333
+ const mockRollup = mock<RollupContract>();
334
+
335
+ mockRollup.getVersion.mockImplementation(() => Promise.resolve(this.version));
336
+ mockRollup.getTargetCommitteeSize.mockImplementation(() => Promise.resolve(this.targetCommitteeSize));
337
+ mockRollup.archiveAt.mockImplementation((cpNum: CheckpointNumber) => Promise.resolve(this.getArchiveAt(cpNum)));
338
+ mockRollup.status.mockImplementation((localCheckpointNum: CheckpointNumber) => {
339
+ const status = this.getRollupStatus();
340
+ return Promise.resolve({
341
+ provenCheckpointNumber: status.provenCheckpointNumber,
342
+ provenArchive: status.provenArchive,
343
+ pendingCheckpointNumber: status.pendingCheckpointNumber,
344
+ pendingArchive: status.pendingArchive,
345
+ archiveOfMyCheckpoint: this.getArchiveAt(localCheckpointNum),
346
+ });
347
+ });
348
+
349
+ // Mock the wrapper method for fetching checkpoint events
350
+ mockRollup.getCheckpointProposedEvents.mockImplementation((fromBlock: bigint, toBlock: bigint) =>
351
+ Promise.resolve(this.getCheckpointProposedLogs(fromBlock, toBlock)),
352
+ );
353
+
354
+ Object.defineProperty(mockRollup, 'address', { get: () => this.config.rollupAddress.toString() });
355
+
356
+ return mockRollup;
357
+ }
358
+
359
+ /** Creates mock InboxContract that reads from this fake state. */
360
+ createMockInboxContract(_publicClient: MockProxy<ViemPublicClient>): MockProxy<InboxContract> {
361
+ const mockInbox = mock<InboxContract>();
362
+
363
+ mockInbox.getState.mockImplementation(() =>
364
+ Promise.resolve({
365
+ messagesRollingHash: this.messagesRollingHash,
366
+ totalMessagesInserted: BigInt(this.messages.length),
367
+ treeInProgress: 0n,
368
+ }),
369
+ );
370
+
371
+ // Mock the wrapper methods for fetching message events
372
+ mockInbox.getMessageSentEvents.mockImplementation((fromBlock: bigint, toBlock: bigint) =>
373
+ Promise.resolve(this.getMessageSentLogs(fromBlock, toBlock)),
374
+ );
375
+
376
+ mockInbox.getMessageSentEventByHash.mockImplementation((hash: string, fromBlock: bigint, toBlock: bigint) =>
377
+ Promise.resolve(this.getMessageSentLogs(fromBlock, toBlock, hash)),
378
+ );
379
+
380
+ return mockInbox;
381
+ }
382
+
383
+ /** Creates mock PublicClient that reads from this fake state. */
384
+ createMockPublicClient(): MockProxy<ViemPublicClient> {
385
+ const publicClient = mock<ViemPublicClient>();
386
+
387
+ publicClient.getChainId.mockResolvedValue(1);
388
+ publicClient.getBlockNumber.mockImplementation(() => Promise.resolve(this.l1BlockNumber));
389
+
390
+ // Use async function pattern that existing test uses for getBlock
391
+
392
+ publicClient.getBlock.mockImplementation((async (args: { blockNumber?: bigint } = {}) => {
393
+ const blockNum = args.blockNumber ?? (await publicClient.getBlockNumber());
394
+ return {
395
+ number: blockNum,
396
+ timestamp: BigInt(blockNum) * BigInt(this.config.ethereumSlotDuration) + this.config.l1GenesisTime,
397
+ hash: Buffer32.fromBigInt(blockNum).toString(),
398
+ } as FormattedBlock;
399
+ }) as any);
400
+
401
+ // Use the same pattern as existing test for getTransaction
402
+ publicClient.getTransaction.mockImplementation((args: { hash?: `0x${string}` }) =>
403
+ Promise.resolve(
404
+ args.hash ? (this.checkpoints.find(cpData => cpData.tx.hash === args.hash)?.tx as any) : undefined,
405
+ ),
406
+ );
407
+
408
+ return publicClient;
409
+ }
410
+
411
+ /** Creates mock BlobClient that reads from this fake state. */
412
+ createMockBlobClient(): MockProxy<BlobClientInterface> {
413
+ const blobClient = mock<BlobClientInterface>();
414
+
415
+ // The blockId is the transaction's blockHash, which we set to the checkpoint's archive root
416
+ blobClient.getBlobSidecar.mockImplementation((blockId: `0x${string}`) =>
417
+ Promise.resolve(
418
+ this.checkpoints.find(cpData => cpData.checkpoint.archive.root.toString() === blockId)?.blobs ?? [],
419
+ ),
420
+ );
421
+
422
+ blobClient.testSources.mockResolvedValue(undefined);
423
+
424
+ return blobClient;
425
+ }
426
+
427
+ private updatePendingCheckpointNumber(): void {
428
+ this.pendingCheckpointNumber = this.checkpoints
429
+ .filter(cpData => cpData.l1BlockNumber <= this.l1BlockNumber && !cpData.pruned)
430
+ .reduce((max, cpData) => (cpData.checkpointNumber > max ? cpData.checkpointNumber : max), CheckpointNumber(0));
431
+ }
432
+
433
+ private getArchiveAt(checkpointNumber: CheckpointNumber): Fr {
434
+ if (checkpointNumber === 0) {
435
+ return this.config.genesisArchiveRoot;
436
+ }
437
+ // Find the latest non-pruned entry for this checkpoint number
438
+ const entries = this.checkpoints.filter(cpData => cpData.checkpointNumber === checkpointNumber);
439
+ const latestEntry = entries.at(-1);
440
+
441
+ if (!latestEntry || latestEntry.pruned) {
442
+ // For pruned or missing checkpoints, return the previous non-pruned checkpoint's archive
443
+ return this.getArchiveAt(CheckpointNumber(checkpointNumber - 1));
444
+ }
445
+ return latestEntry.checkpoint.archive.root;
446
+ }
447
+
448
+ private getNextBlockNumber(checkpointNumber: CheckpointNumber): BlockNumber {
449
+ const lastBlockNumber = this.checkpoints
450
+ .filter(cpData => cpData.checkpointNumber < checkpointNumber)
451
+ .flatMap(cpData => cpData.checkpoint.blocks.map(b => b.number))
452
+ .reduce((max, num) => Math.max(max, num), 0);
453
+ return (lastBlockNumber + 1) as BlockNumber;
454
+ }
455
+
456
+ private getCheckpointProposedLogs(fromBlock: bigint, toBlock: bigint): CheckpointProposedLog[] {
457
+ return this.checkpoints
458
+ .filter(cpData => cpData.l1BlockNumber >= fromBlock && cpData.l1BlockNumber <= toBlock)
459
+ .map(cpData => ({
460
+ l1BlockNumber: cpData.l1BlockNumber,
461
+ l1BlockHash: Buffer32.fromBigInt(cpData.l1BlockNumber),
462
+ l1TransactionHash: cpData.tx.hash as `0x${string}`,
463
+ args: {
464
+ checkpointNumber: cpData.checkpointNumber,
465
+ archive: cpData.checkpoint.archive.root,
466
+ versionedBlobHashes: cpData.blobHashes.map(h => Buffer.from(h.slice(2), 'hex')),
467
+ // These are intentionally undefined to skip hash validation in the archiver
468
+ // (validation is skipped when these fields are falsy)
469
+ payloadDigest: undefined,
470
+ attestationsHash: undefined,
471
+ },
472
+ }));
473
+ }
474
+
475
+ private getMessageSentLogs(fromBlock?: bigint, toBlock?: bigint, hashFilter?: string): MessageSentLog[] {
476
+ return this.messages
477
+ .filter(
478
+ msg =>
479
+ (!hashFilter || msg.leaf.toString() === hashFilter) &&
480
+ (!fromBlock || msg.l1BlockNumber >= fromBlock) &&
481
+ (!toBlock || msg.l1BlockNumber <= toBlock),
482
+ )
483
+ .map(msg => ({
484
+ l1BlockNumber: msg.l1BlockNumber,
485
+ l1BlockHash: Buffer32.fromBigInt(msg.l1BlockNumber),
486
+ l1TransactionHash: `0x${msg.l1BlockNumber.toString(16)}` as `0x${string}`,
487
+ args: {
488
+ checkpointNumber: msg.checkpointNumber,
489
+ index: msg.index,
490
+ leaf: msg.leaf,
491
+ rollingHash: msg.rollingHash,
492
+ },
493
+ }));
494
+ }
495
+
496
+ private makeRollupTx(checkpoint: Checkpoint, signers: Secp256k1Signer[]): Transaction {
497
+ const attestations = signers
498
+ .map(signer => makeCheckpointAttestationFromCheckpoint(checkpoint, signer))
499
+ .map(attestation => CommitteeAttestation.fromSignature(attestation.signature))
500
+ .map(committeeAttestation => committeeAttestation.toViem());
501
+
502
+ const header = checkpoint.header.toViem();
503
+ const blobInput = getPrefixedEthBlobCommitments(getBlobsPerL1Block(checkpoint.toBlobFields()));
504
+ const archive = toHex(checkpoint.archive.root.toBuffer());
505
+ const attestationsAndSigners = new CommitteeAttestationsAndSigners(
506
+ attestations.map(attestation => CommitteeAttestation.fromViem(attestation)),
507
+ );
508
+
509
+ const attestationsAndSignersSignature = makeAndSignCommitteeAttestationsAndSigners(
510
+ attestationsAndSigners,
511
+ signers[0],
512
+ );
513
+
514
+ const rollupInput = encodeFunctionData({
515
+ abi: RollupAbi,
516
+ functionName: 'propose',
517
+ args: [
518
+ {
519
+ header,
520
+ archive,
521
+ oracleInput: { feeAssetPriceModifier: 0n },
522
+ },
523
+ attestationsAndSigners.getPackedAttestations(),
524
+ attestationsAndSigners.getSigners().map(signer => signer.toString()),
525
+ attestationsAndSignersSignature.toViemSignature(),
526
+ blobInput,
527
+ ],
528
+ });
529
+
530
+ const multiCallInput = encodeFunctionData({
531
+ abi: multicall3Abi,
532
+ functionName: 'aggregate3',
533
+ args: [
534
+ [
535
+ {
536
+ target: this.config.rollupAddress.toString(),
537
+ callData: rollupInput,
538
+ allowFailure: false,
539
+ },
540
+ ],
541
+ ],
542
+ });
543
+
544
+ return {
545
+ input: multiCallInput,
546
+ hash: archive,
547
+ blockHash: archive,
548
+ to: MULTI_CALL_3_ADDRESS as `0x${string}`,
549
+ } as Transaction<bigint, number>;
550
+ }
551
+
552
+ private makeVersionedBlobHashes(checkpoint: Checkpoint): `0x${string}`[] {
553
+ return getBlobsPerL1Block(checkpoint.toBlobFields()).map(
554
+ b => `0x${b.getEthVersionedBlobHash().toString('hex')}` as `0x${string}`,
555
+ );
556
+ }
557
+
558
+ private makeBlobsFromCheckpoint(checkpoint: Checkpoint): Blob[] {
559
+ return getBlobsPerL1Block(checkpoint.toBlobFields());
560
+ }
561
+ }