@aztec/archiver 0.0.0-test.1 → 0.0.1-commit.03f7ef2

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 (151) hide show
  1. package/README.md +27 -6
  2. package/dest/archiver/archiver.d.ts +201 -94
  3. package/dest/archiver/archiver.d.ts.map +1 -1
  4. package/dest/archiver/archiver.js +1141 -396
  5. package/dest/archiver/archiver_store.d.ts +171 -83
  6. package/dest/archiver/archiver_store.d.ts.map +1 -1
  7. package/dest/archiver/archiver_store_test_suite.d.ts +1 -1
  8. package/dest/archiver/archiver_store_test_suite.d.ts.map +1 -1
  9. package/dest/archiver/archiver_store_test_suite.js +2389 -393
  10. package/dest/archiver/config.d.ts +7 -22
  11. package/dest/archiver/config.d.ts.map +1 -1
  12. package/dest/archiver/config.js +30 -14
  13. package/dest/archiver/errors.d.ts +33 -1
  14. package/dest/archiver/errors.d.ts.map +1 -1
  15. package/dest/archiver/errors.js +49 -0
  16. package/dest/archiver/index.d.ts +3 -4
  17. package/dest/archiver/index.d.ts.map +1 -1
  18. package/dest/archiver/index.js +1 -2
  19. package/dest/archiver/instrumentation.d.ts +14 -6
  20. package/dest/archiver/instrumentation.d.ts.map +1 -1
  21. package/dest/archiver/instrumentation.js +69 -17
  22. package/dest/archiver/kv_archiver_store/block_store.d.ts +91 -21
  23. package/dest/archiver/kv_archiver_store/block_store.d.ts.map +1 -1
  24. package/dest/archiver/kv_archiver_store/block_store.js +476 -86
  25. package/dest/archiver/kv_archiver_store/contract_class_store.d.ts +4 -4
  26. package/dest/archiver/kv_archiver_store/contract_class_store.d.ts.map +1 -1
  27. package/dest/archiver/kv_archiver_store/contract_class_store.js +13 -19
  28. package/dest/archiver/kv_archiver_store/contract_instance_store.d.ts +12 -9
  29. package/dest/archiver/kv_archiver_store/contract_instance_store.d.ts.map +1 -1
  30. package/dest/archiver/kv_archiver_store/contract_instance_store.js +30 -16
  31. package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts +80 -75
  32. package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts.map +1 -1
  33. package/dest/archiver/kv_archiver_store/kv_archiver_store.js +142 -83
  34. package/dest/archiver/kv_archiver_store/log_store.d.ts +12 -16
  35. package/dest/archiver/kv_archiver_store/log_store.d.ts.map +1 -1
  36. package/dest/archiver/kv_archiver_store/log_store.js +153 -113
  37. package/dest/archiver/kv_archiver_store/message_store.d.ts +25 -18
  38. package/dest/archiver/kv_archiver_store/message_store.d.ts.map +1 -1
  39. package/dest/archiver/kv_archiver_store/message_store.js +152 -49
  40. package/dest/archiver/l1/bin/retrieve-calldata.d.ts +3 -0
  41. package/dest/archiver/l1/bin/retrieve-calldata.d.ts.map +1 -0
  42. package/dest/archiver/l1/bin/retrieve-calldata.js +149 -0
  43. package/dest/archiver/l1/calldata_retriever.d.ts +112 -0
  44. package/dest/archiver/l1/calldata_retriever.d.ts.map +1 -0
  45. package/dest/archiver/l1/calldata_retriever.js +471 -0
  46. package/dest/archiver/l1/data_retrieval.d.ts +90 -0
  47. package/dest/archiver/l1/data_retrieval.d.ts.map +1 -0
  48. package/dest/archiver/l1/data_retrieval.js +331 -0
  49. package/dest/archiver/l1/debug_tx.d.ts +19 -0
  50. package/dest/archiver/l1/debug_tx.d.ts.map +1 -0
  51. package/dest/archiver/l1/debug_tx.js +73 -0
  52. package/dest/archiver/l1/spire_proposer.d.ts +70 -0
  53. package/dest/archiver/l1/spire_proposer.d.ts.map +1 -0
  54. package/dest/archiver/l1/spire_proposer.js +157 -0
  55. package/dest/archiver/l1/trace_tx.d.ts +97 -0
  56. package/dest/archiver/l1/trace_tx.d.ts.map +1 -0
  57. package/dest/archiver/l1/trace_tx.js +91 -0
  58. package/dest/archiver/l1/types.d.ts +12 -0
  59. package/dest/archiver/l1/types.d.ts.map +1 -0
  60. package/dest/archiver/l1/types.js +3 -0
  61. package/dest/archiver/l1/validate_trace.d.ts +29 -0
  62. package/dest/archiver/l1/validate_trace.d.ts.map +1 -0
  63. package/dest/archiver/l1/validate_trace.js +150 -0
  64. package/dest/archiver/structs/data_retrieval.d.ts +1 -1
  65. package/dest/archiver/structs/inbox_message.d.ts +15 -0
  66. package/dest/archiver/structs/inbox_message.d.ts.map +1 -0
  67. package/dest/archiver/structs/inbox_message.js +39 -0
  68. package/dest/archiver/structs/published.d.ts +2 -11
  69. package/dest/archiver/structs/published.d.ts.map +1 -1
  70. package/dest/archiver/structs/published.js +1 -1
  71. package/dest/archiver/validation.d.ts +17 -0
  72. package/dest/archiver/validation.d.ts.map +1 -0
  73. package/dest/archiver/validation.js +98 -0
  74. package/dest/factory.d.ts +9 -14
  75. package/dest/factory.d.ts.map +1 -1
  76. package/dest/factory.js +22 -52
  77. package/dest/index.d.ts +2 -2
  78. package/dest/index.d.ts.map +1 -1
  79. package/dest/index.js +1 -1
  80. package/dest/rpc/index.d.ts +2 -3
  81. package/dest/rpc/index.d.ts.map +1 -1
  82. package/dest/rpc/index.js +1 -4
  83. package/dest/test/index.d.ts +1 -1
  84. package/dest/test/mock_archiver.d.ts +16 -8
  85. package/dest/test/mock_archiver.d.ts.map +1 -1
  86. package/dest/test/mock_archiver.js +19 -14
  87. package/dest/test/mock_l1_to_l2_message_source.d.ts +9 -6
  88. package/dest/test/mock_l1_to_l2_message_source.d.ts.map +1 -1
  89. package/dest/test/mock_l1_to_l2_message_source.js +21 -7
  90. package/dest/test/mock_l2_block_source.d.ts +52 -13
  91. package/dest/test/mock_l2_block_source.d.ts.map +1 -1
  92. package/dest/test/mock_l2_block_source.js +140 -15
  93. package/dest/test/mock_structs.d.ts +10 -0
  94. package/dest/test/mock_structs.d.ts.map +1 -0
  95. package/dest/test/mock_structs.js +38 -0
  96. package/package.json +29 -30
  97. package/src/archiver/archiver.ts +1477 -501
  98. package/src/archiver/archiver_store.ts +197 -88
  99. package/src/archiver/archiver_store_test_suite.ts +2403 -350
  100. package/src/archiver/config.ts +38 -46
  101. package/src/archiver/errors.ts +85 -0
  102. package/src/archiver/index.ts +2 -3
  103. package/src/archiver/instrumentation.ts +91 -22
  104. package/src/archiver/kv_archiver_store/block_store.ts +640 -101
  105. package/src/archiver/kv_archiver_store/contract_class_store.ts +14 -24
  106. package/src/archiver/kv_archiver_store/contract_instance_store.ts +36 -28
  107. package/src/archiver/kv_archiver_store/kv_archiver_store.ts +193 -113
  108. package/src/archiver/kv_archiver_store/log_store.ts +205 -127
  109. package/src/archiver/kv_archiver_store/message_store.ts +213 -54
  110. package/src/archiver/l1/README.md +98 -0
  111. package/src/archiver/l1/bin/retrieve-calldata.ts +182 -0
  112. package/src/archiver/l1/calldata_retriever.ts +641 -0
  113. package/src/archiver/l1/data_retrieval.ts +512 -0
  114. package/src/archiver/l1/debug_tx.ts +99 -0
  115. package/src/archiver/l1/spire_proposer.ts +160 -0
  116. package/src/archiver/l1/trace_tx.ts +128 -0
  117. package/src/archiver/l1/types.ts +13 -0
  118. package/src/archiver/l1/validate_trace.ts +211 -0
  119. package/src/archiver/structs/inbox_message.ts +41 -0
  120. package/src/archiver/structs/published.ts +1 -11
  121. package/src/archiver/validation.ts +124 -0
  122. package/src/factory.ts +28 -69
  123. package/src/index.ts +1 -1
  124. package/src/rpc/index.ts +1 -5
  125. package/src/test/fixtures/debug_traceTransaction-multicall3.json +88 -0
  126. package/src/test/fixtures/debug_traceTransaction-multiplePropose.json +153 -0
  127. package/src/test/fixtures/debug_traceTransaction-proxied.json +122 -0
  128. package/src/test/fixtures/trace_transaction-multicall3.json +65 -0
  129. package/src/test/fixtures/trace_transaction-multiplePropose.json +319 -0
  130. package/src/test/fixtures/trace_transaction-proxied.json +128 -0
  131. package/src/test/fixtures/trace_transaction-randomRevert.json +216 -0
  132. package/src/test/mock_archiver.ts +22 -16
  133. package/src/test/mock_l1_to_l2_message_source.ts +20 -8
  134. package/src/test/mock_l2_block_source.ts +186 -21
  135. package/src/test/mock_structs.ts +50 -0
  136. package/dest/archiver/data_retrieval.d.ts +0 -74
  137. package/dest/archiver/data_retrieval.d.ts.map +0 -1
  138. package/dest/archiver/data_retrieval.js +0 -283
  139. package/dest/archiver/kv_archiver_store/nullifier_store.d.ts +0 -12
  140. package/dest/archiver/kv_archiver_store/nullifier_store.d.ts.map +0 -1
  141. package/dest/archiver/kv_archiver_store/nullifier_store.js +0 -73
  142. package/dest/archiver/memory_archiver_store/l1_to_l2_message_store.d.ts +0 -23
  143. package/dest/archiver/memory_archiver_store/l1_to_l2_message_store.d.ts.map +0 -1
  144. package/dest/archiver/memory_archiver_store/l1_to_l2_message_store.js +0 -49
  145. package/dest/archiver/memory_archiver_store/memory_archiver_store.d.ts +0 -175
  146. package/dest/archiver/memory_archiver_store/memory_archiver_store.d.ts.map +0 -1
  147. package/dest/archiver/memory_archiver_store/memory_archiver_store.js +0 -636
  148. package/src/archiver/data_retrieval.ts +0 -422
  149. package/src/archiver/kv_archiver_store/nullifier_store.ts +0 -97
  150. package/src/archiver/memory_archiver_store/l1_to_l2_message_store.ts +0 -61
  151. package/src/archiver/memory_archiver_store/memory_archiver_store.ts +0 -801
@@ -1,33 +1,60 @@
1
1
  import {
2
+ INITIAL_CHECKPOINT_NUMBER,
2
3
  INITIAL_L2_BLOCK_NUM,
3
- L1_TO_L2_MSG_SUBTREE_HEIGHT,
4
- MAX_NULLIFIERS_PER_TX,
4
+ MAX_NOTE_HASHES_PER_TX,
5
+ NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP,
5
6
  PRIVATE_LOG_SIZE_IN_FIELDS,
6
- PUBLIC_LOG_DATA_SIZE_IN_FIELDS,
7
7
  } from '@aztec/constants';
8
+ import { makeTuple } from '@aztec/foundation/array';
9
+ import { BlockNumber, CheckpointNumber, EpochNumber } from '@aztec/foundation/branded-types';
10
+ import { Buffer16, Buffer32 } from '@aztec/foundation/buffer';
8
11
  import { times, timesParallel } from '@aztec/foundation/collection';
9
- import { randomInt } from '@aztec/foundation/crypto';
10
- import { Fr } from '@aztec/foundation/fields';
12
+ import { randomInt } from '@aztec/foundation/crypto/random';
13
+ import { Fr } from '@aztec/foundation/curves/bn254';
14
+ import { toArray } from '@aztec/foundation/iterable';
15
+ import { sleep } from '@aztec/foundation/sleep';
11
16
  import { AztecAddress } from '@aztec/stdlib/aztec-address';
12
- import { L2Block, wrapInBlock } from '@aztec/stdlib/block';
17
+ import {
18
+ CheckpointedL2Block,
19
+ CommitteeAttestation,
20
+ EthAddress,
21
+ L2BlockHash,
22
+ L2BlockNew,
23
+ type ValidateBlockResult,
24
+ randomBlockInfo,
25
+ } from '@aztec/stdlib/block';
26
+ import { Checkpoint, L1PublishedData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
13
27
  import {
14
28
  type ContractClassPublic,
15
29
  type ContractInstanceWithAddress,
16
30
  SerializableContractInstance,
17
31
  computePublicBytecodeCommitment,
18
32
  } from '@aztec/stdlib/contract';
19
- import { LogId, PrivateLog, PublicLog } from '@aztec/stdlib/logs';
33
+ import { ContractClassLog, LogId, PrivateLog, PublicLog, SiloedTag, Tag } from '@aztec/stdlib/logs';
20
34
  import { InboxLeaf } from '@aztec/stdlib/messaging';
35
+ import { CheckpointHeader } from '@aztec/stdlib/rollup';
21
36
  import {
22
37
  makeContractClassPublic,
23
38
  makeExecutablePrivateFunctionWithMembershipProof,
24
- makeUnconstrainedFunctionWithMembershipProof,
39
+ makeUtilityFunctionWithMembershipProof,
25
40
  } from '@aztec/stdlib/testing';
26
41
  import '@aztec/stdlib/testing/jest';
27
- import { TxEffect, TxHash } from '@aztec/stdlib/tx';
42
+ import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
43
+ import { type IndexedTxEffect, PartialStateReference, StateReference, TxEffect, TxHash } from '@aztec/stdlib/tx';
28
44
 
45
+ import { makeInboxMessage, makeInboxMessages } from '../test/mock_structs.js';
29
46
  import type { ArchiverDataStore, ArchiverL1SynchPoint } from './archiver_store.js';
30
- import type { L1Published } from './structs/published.js';
47
+ import {
48
+ BlockArchiveNotConsistentError,
49
+ BlockIndexNotSequentialError,
50
+ BlockNumberNotSequentialError,
51
+ CheckpointNumberNotConsistentError,
52
+ CheckpointNumberNotSequentialError,
53
+ InitialBlockNumberNotSequentialError,
54
+ InitialCheckpointNumberNotSequentialError,
55
+ } from './errors.js';
56
+ import { MessageStoreError } from './kv_archiver_store/message_store.js';
57
+ import type { InboxMessage } from './structs/inbox_message.js';
31
58
 
32
59
  /**
33
60
  * @param testName - The name of the test suite.
@@ -39,99 +66,1610 @@ export function describeArchiverDataStore(
39
66
  ) {
40
67
  describe(testName, () => {
41
68
  let store: ArchiverDataStore;
42
- let blocks: L1Published<L2Block>[];
43
- const blockTests: [number, number, () => L1Published<L2Block>[]][] = [
44
- [1, 1, () => blocks.slice(0, 1)],
45
- [10, 1, () => blocks.slice(9, 10)],
46
- [1, 10, () => blocks.slice(0, 10)],
47
- [2, 5, () => blocks.slice(1, 6)],
48
- [5, 2, () => blocks.slice(4, 6)],
69
+ let publishedCheckpoints: PublishedCheckpoint[];
70
+
71
+ const blockNumberTests: [number, () => L2BlockNew][] = [
72
+ [1, () => publishedCheckpoints[0].checkpoint.blocks[0]],
73
+ [10, () => publishedCheckpoints[9].checkpoint.blocks[0]],
74
+ [5, () => publishedCheckpoints[4].checkpoint.blocks[0]],
49
75
  ];
50
76
 
51
- const makeL1Published = (block: L2Block, l1BlockNumber: number): L1Published<L2Block> => ({
52
- data: block,
53
- l1: {
54
- blockNumber: BigInt(l1BlockNumber),
55
- blockHash: `0x${l1BlockNumber}`,
56
- timestamp: BigInt(l1BlockNumber * 1000),
57
- },
58
- });
77
+ const makeBlockHash = (blockNumber: number) => `0x${blockNumber.toString(16).padStart(64, '0')}`;
78
+
79
+ // Create a state reference with properly calculated noteHashTree.nextAvailableLeafIndex
80
+ // This is needed because the log store calculates dataStartIndexForBlock as:
81
+ // noteHashTree.nextAvailableLeafIndex - txEffects.length * MAX_NOTE_HASHES_PER_TX
82
+ // If nextAvailableLeafIndex is too small (random values 0-1000), this becomes negative
83
+ const makeStateForBlock = (blockNumber: number, txsPerBlock: number): StateReference => {
84
+ // Ensure nextAvailableLeafIndex is large enough for all blocks up to this point
85
+ const noteHashIndex = blockNumber * txsPerBlock * MAX_NOTE_HASHES_PER_TX;
86
+ return new StateReference(
87
+ AppendOnlyTreeSnapshot.random(),
88
+ new PartialStateReference(
89
+ new AppendOnlyTreeSnapshot(Fr.random(), noteHashIndex),
90
+ AppendOnlyTreeSnapshot.random(),
91
+ AppendOnlyTreeSnapshot.random(),
92
+ ),
93
+ );
94
+ };
95
+
96
+ const makePublishedCheckpoint = (checkpoint: Checkpoint, l1BlockNumber: number): PublishedCheckpoint => {
97
+ return new PublishedCheckpoint(
98
+ checkpoint,
99
+ new L1PublishedData(BigInt(l1BlockNumber), BigInt(l1BlockNumber * 1000), makeBlockHash(l1BlockNumber)),
100
+ times(3, CommitteeAttestation.random),
101
+ );
102
+ };
103
+
104
+ const expectCheckpointedBlockEquals = (
105
+ actual: CheckpointedL2Block,
106
+ expectedBlock: L2BlockNew,
107
+ expectedCheckpoint: PublishedCheckpoint,
108
+ ) => {
109
+ expect(actual.l1).toEqual(expectedCheckpoint.l1);
110
+ expect(actual.block.header.equals(expectedBlock.header)).toBe(true);
111
+ expect(actual.checkpointNumber).toEqual(expectedCheckpoint.checkpoint.number);
112
+ expect(actual.attestations.every((a, i) => a.equals(expectedCheckpoint.attestations[i]))).toBe(true);
113
+ };
59
114
 
60
115
  beforeEach(async () => {
61
116
  store = await getStore();
62
- blocks = await timesParallel(10, async i => makeL1Published(await L2Block.random(i + 1), i + 10));
117
+ // Create checkpoints sequentially to ensure archive roots are chained properly.
118
+ // Each block's header.lastArchive must equal the previous block's archive.
119
+ publishedCheckpoints = [];
120
+ const txsPerBlock = 4;
121
+ for (let i = 0; i < 10; i++) {
122
+ const blockNumber = i + 1;
123
+ const previousArchive = i > 0 ? publishedCheckpoints[i - 1].checkpoint.blocks[0].archive : undefined;
124
+ const checkpoint = await Checkpoint.random(CheckpointNumber(i + 1), {
125
+ numBlocks: 1,
126
+ startBlockNumber: blockNumber,
127
+ previousArchive,
128
+ txsPerBlock,
129
+ state: makeStateForBlock(blockNumber, txsPerBlock),
130
+ // Ensure each tx has public logs for getPublicLogs tests
131
+ txOptions: { numPublicCallsPerTx: 2, numPublicLogsPerCall: 2 },
132
+ });
133
+ publishedCheckpoints.push(makePublishedCheckpoint(checkpoint, i + 10));
134
+ }
63
135
  });
64
136
 
65
- describe('addBlocks', () => {
66
- it('returns success when adding blocks', async () => {
67
- await expect(store.addBlocks(blocks)).resolves.toBe(true);
137
+ describe('addCheckpoints', () => {
138
+ it('returns success when adding checkpoints', async () => {
139
+ await expect(store.addCheckpoints(publishedCheckpoints)).resolves.toBe(true);
140
+ });
141
+
142
+ it('throws on duplicate checkpoints', async () => {
143
+ await store.addCheckpoints(publishedCheckpoints);
144
+ await expect(store.addCheckpoints(publishedCheckpoints)).rejects.toThrow(
145
+ InitialCheckpointNumberNotSequentialError,
146
+ );
147
+ });
148
+
149
+ it('throws an error if the previous block does not exist in the store', async () => {
150
+ const checkpoint = await Checkpoint.random(CheckpointNumber(2), { numBlocks: 1, startBlockNumber: 2 });
151
+ const block = makePublishedCheckpoint(checkpoint, 2);
152
+ await expect(store.addCheckpoints([block])).rejects.toThrow(InitialCheckpointNumberNotSequentialError);
153
+ await expect(store.getCheckpointedBlock(1)).resolves.toBeUndefined();
154
+ });
155
+
156
+ it('throws an error if there is a gap in the blocks being added', async () => {
157
+ const checkpoint1 = await Checkpoint.random(CheckpointNumber(1), { numBlocks: 1, startBlockNumber: 1 });
158
+ const checkpoint3 = await Checkpoint.random(CheckpointNumber(3), { numBlocks: 1, startBlockNumber: 3 });
159
+ const checkpoints = [makePublishedCheckpoint(checkpoint1, 1), makePublishedCheckpoint(checkpoint3, 3)];
160
+ await expect(store.addCheckpoints(checkpoints)).rejects.toThrow(CheckpointNumberNotSequentialError);
161
+ await expect(store.getCheckpointedBlock(1)).resolves.toBeUndefined();
68
162
  });
69
163
 
70
- it('allows duplicate blocks', async () => {
71
- await store.addBlocks(blocks);
72
- await expect(store.addBlocks(blocks)).resolves.toBe(true);
164
+ it('throws an error if blocks within a checkpoint are not sequential', async () => {
165
+ // Create a checkpoint with non-sequential block numbers (block 1 and block 3, skipping block 2)
166
+ const block1 = await L2BlockNew.random(BlockNumber(1), { checkpointNumber: CheckpointNumber(1) });
167
+ const block3 = await L2BlockNew.random(BlockNumber(3), { checkpointNumber: CheckpointNumber(1) });
168
+
169
+ const checkpoint = new Checkpoint(
170
+ AppendOnlyTreeSnapshot.random(),
171
+ CheckpointHeader.random(),
172
+ [block1, block3],
173
+ CheckpointNumber(1),
174
+ );
175
+ const publishedCheckpoint = makePublishedCheckpoint(checkpoint, 10);
176
+
177
+ await expect(store.addCheckpoints([publishedCheckpoint])).rejects.toThrow(BlockNumberNotSequentialError);
178
+ await expect(store.getCheckpointedBlock(1)).resolves.toBeUndefined();
179
+ });
180
+
181
+ it('throws an error if blocks within a checkpoint do not have sequential indexes', async () => {
182
+ // Create a checkpoint with non-sequential indexes
183
+ const block1 = await L2BlockNew.random(BlockNumber(1), {
184
+ checkpointNumber: CheckpointNumber(1),
185
+ indexWithinCheckpoint: 0,
186
+ });
187
+ const block3 = await L2BlockNew.random(BlockNumber(2), {
188
+ checkpointNumber: CheckpointNumber(1),
189
+ indexWithinCheckpoint: 2,
190
+ });
191
+
192
+ const checkpoint = new Checkpoint(
193
+ AppendOnlyTreeSnapshot.random(),
194
+ CheckpointHeader.random(),
195
+ [block1, block3],
196
+ CheckpointNumber(1),
197
+ );
198
+ const publishedCheckpoint = makePublishedCheckpoint(checkpoint, 10);
199
+
200
+ await expect(store.addCheckpoints([publishedCheckpoint])).rejects.toThrow(BlockIndexNotSequentialError);
201
+ await expect(store.getCheckpointedBlock(1)).resolves.toBeUndefined();
202
+ });
203
+
204
+ it('throws an error if blocks within a checkpoint do not start from index 0', async () => {
205
+ // Create a checkpoint with non-sequential indexes
206
+ const block1 = await L2BlockNew.random(BlockNumber(1), {
207
+ checkpointNumber: CheckpointNumber(1),
208
+ indexWithinCheckpoint: 1,
209
+ });
210
+ const block3 = await L2BlockNew.random(BlockNumber(2), {
211
+ checkpointNumber: CheckpointNumber(1),
212
+ indexWithinCheckpoint: 2,
213
+ });
214
+
215
+ const checkpoint = new Checkpoint(
216
+ AppendOnlyTreeSnapshot.random(),
217
+ CheckpointHeader.random(),
218
+ [block1, block3],
219
+ CheckpointNumber(1),
220
+ );
221
+ const publishedCheckpoint = makePublishedCheckpoint(checkpoint, 10);
222
+
223
+ await expect(store.addCheckpoints([publishedCheckpoint])).rejects.toThrow(BlockIndexNotSequentialError);
224
+ await expect(store.getCheckpointedBlock(1)).resolves.toBeUndefined();
225
+ });
226
+
227
+ it('throws an error if block has invalid checkpoint index', async () => {
228
+ // Create a block wit an invalid checkpoint index
229
+ const block1 = await L2BlockNew.random(BlockNumber(1), {
230
+ checkpointNumber: CheckpointNumber(1),
231
+ indexWithinCheckpoint: -1,
232
+ });
233
+
234
+ const checkpoint = new Checkpoint(
235
+ AppendOnlyTreeSnapshot.random(),
236
+ CheckpointHeader.random(),
237
+ [block1],
238
+ CheckpointNumber(1),
239
+ );
240
+ const publishedCheckpoint = makePublishedCheckpoint(checkpoint, 10);
241
+
242
+ await expect(store.addCheckpoints([publishedCheckpoint])).rejects.toThrow(BlockIndexNotSequentialError);
243
+ await expect(store.getCheckpointedBlock(1)).resolves.toBeUndefined();
244
+ });
245
+
246
+ it('throws an error if checkpoint has invalid initial number', async () => {
247
+ const block1 = await L2BlockNew.random(BlockNumber(1), {
248
+ checkpointNumber: CheckpointNumber(2),
249
+ indexWithinCheckpoint: 0,
250
+ });
251
+
252
+ const checkpoint = new Checkpoint(
253
+ AppendOnlyTreeSnapshot.random(),
254
+ CheckpointHeader.random(),
255
+ [block1],
256
+ CheckpointNumber(2),
257
+ );
258
+ const publishedCheckpoint = makePublishedCheckpoint(checkpoint, 10);
259
+
260
+ await expect(store.addCheckpoints([publishedCheckpoint])).rejects.toThrow(
261
+ InitialCheckpointNumberNotSequentialError,
262
+ );
263
+ });
264
+
265
+ it('allows the correct initial checkpoint', async () => {
266
+ const block1 = await L2BlockNew.random(BlockNumber(1), {
267
+ checkpointNumber: CheckpointNumber(1),
268
+ indexWithinCheckpoint: 0,
269
+ });
270
+
271
+ const checkpoint = new Checkpoint(
272
+ AppendOnlyTreeSnapshot.random(),
273
+ CheckpointHeader.random(),
274
+ [block1],
275
+ CheckpointNumber(1),
276
+ );
277
+ const publishedCheckpoint = makePublishedCheckpoint(checkpoint, 10);
278
+
279
+ await expect(store.addCheckpoints([publishedCheckpoint])).resolves.toBe(true);
280
+ });
281
+
282
+ it('throws on duplicate initial checkpoint', async () => {
283
+ const block1 = await L2BlockNew.random(BlockNumber(1), {
284
+ checkpointNumber: CheckpointNumber(1),
285
+ indexWithinCheckpoint: 0,
286
+ });
287
+
288
+ const block2 = await L2BlockNew.random(BlockNumber(1), {
289
+ checkpointNumber: CheckpointNumber(1),
290
+ indexWithinCheckpoint: 0,
291
+ });
292
+
293
+ const checkpoint = new Checkpoint(
294
+ AppendOnlyTreeSnapshot.random(),
295
+ CheckpointHeader.random(),
296
+ [block1],
297
+ CheckpointNumber(1),
298
+ );
299
+ const publishedCheckpoint = makePublishedCheckpoint(checkpoint, 10);
300
+
301
+ const checkpoint2 = new Checkpoint(
302
+ AppendOnlyTreeSnapshot.random(),
303
+ CheckpointHeader.random(),
304
+ [block2],
305
+ CheckpointNumber(1),
306
+ );
307
+ const publishedCheckpoint2 = makePublishedCheckpoint(checkpoint2, 10);
308
+
309
+ await expect(store.addCheckpoints([publishedCheckpoint])).resolves.toBe(true);
310
+ await expect(store.addCheckpoints([publishedCheckpoint2])).rejects.toThrow(
311
+ InitialCheckpointNumberNotSequentialError,
312
+ );
313
+ });
314
+ });
315
+
316
+ describe('unwindcheckpoints', () => {
317
+ it('unwinding checkpoints will remove checkpoints from the chain', async () => {
318
+ await store.addCheckpoints(publishedCheckpoints);
319
+ const checkpointNumber = await store.getSynchedCheckpointNumber();
320
+ const lastCheckpoint = publishedCheckpoints.at(-1)!;
321
+ const lastBlockNumber = lastCheckpoint.checkpoint.blocks[0].number;
322
+
323
+ // Verify block exists before unwinding
324
+ const retrievedBlock = await store.getCheckpointedBlock(lastBlockNumber);
325
+ expect(retrievedBlock).toBeDefined();
326
+ expect(retrievedBlock!.block.header.equals(lastCheckpoint.checkpoint.blocks[0].header)).toBe(true);
327
+ expect(retrievedBlock!.checkpointNumber).toEqual(checkpointNumber);
328
+
329
+ await store.unwindCheckpoints(checkpointNumber, 1);
330
+
331
+ expect(await store.getSynchedCheckpointNumber()).toBe(checkpointNumber - 1);
332
+ await expect(store.getCheckpointedBlock(lastBlockNumber)).resolves.toBeUndefined();
333
+ });
334
+
335
+ it('can unwind multiple empty blocks', async () => {
336
+ // Create checkpoints sequentially to chain archive roots
337
+ const emptyCheckpoints: PublishedCheckpoint[] = [];
338
+ for (let i = 0; i < 10; i++) {
339
+ const previousArchive = i > 0 ? emptyCheckpoints[i - 1].checkpoint.blocks[0].archive : undefined;
340
+ const checkpoint = await Checkpoint.random(CheckpointNumber(i + 1), {
341
+ numBlocks: 1,
342
+ startBlockNumber: i + 1,
343
+ txsPerBlock: 0,
344
+ previousArchive,
345
+ });
346
+ emptyCheckpoints.push(makePublishedCheckpoint(checkpoint, i + 10));
347
+ }
348
+ await store.addCheckpoints(emptyCheckpoints);
349
+ expect(await store.getSynchedCheckpointNumber()).toBe(10);
350
+
351
+ await store.unwindCheckpoints(CheckpointNumber(10), 3);
352
+ expect(await store.getSynchedCheckpointNumber()).toBe(7);
353
+ expect((await store.getRangeOfCheckpoints(CheckpointNumber(1), 10)).map(b => b.checkpointNumber)).toEqual([
354
+ 1, 2, 3, 4, 5, 6, 7,
355
+ ]);
356
+ });
357
+
358
+ it('refuses to unwind checkpoints if the tip is not the last checkpoint', async () => {
359
+ await store.addCheckpoints(publishedCheckpoints);
360
+ await expect(store.unwindCheckpoints(CheckpointNumber(5), 1)).rejects.toThrow(
361
+ /can only unwind checkpoints from the tip/i,
362
+ );
363
+ });
364
+
365
+ it('unwound blocks and headers cannot be retrieved by hash or archive', async () => {
366
+ await store.addCheckpoints(publishedCheckpoints);
367
+ const lastCheckpoint = publishedCheckpoints[publishedCheckpoints.length - 1];
368
+ const lastBlock = lastCheckpoint.checkpoint.blocks[0];
369
+ const blockHash = await lastBlock.header.hash();
370
+ const archive = lastBlock.archive.root;
371
+
372
+ // Verify block and header exist before unwinding
373
+ const retrievedByHash = await store.getCheckpointedBlockByHash(blockHash);
374
+ expect(retrievedByHash).toBeDefined();
375
+ expect(retrievedByHash!.block.header.equals(lastBlock.header)).toBe(true);
376
+
377
+ const retrievedByArchive = await store.getCheckpointedBlockByArchive(archive);
378
+ expect(retrievedByArchive).toBeDefined();
379
+ expect(retrievedByArchive!.block.header.equals(lastBlock.header)).toBe(true);
380
+
381
+ const headerByHash = await store.getBlockHeaderByHash(blockHash);
382
+ expect(headerByHash).toBeDefined();
383
+ expect(headerByHash!.equals(lastBlock.header)).toBe(true);
384
+
385
+ const headerByArchive = await store.getBlockHeaderByArchive(archive);
386
+ expect(headerByArchive).toBeDefined();
387
+ expect(headerByArchive!.equals(lastBlock.header)).toBe(true);
388
+
389
+ // Unwind the checkpoint
390
+ await store.unwindCheckpoints(lastCheckpoint.checkpoint.number, 1);
391
+
392
+ // Verify neither block nor header can be retrieved after unwinding
393
+ expect(await store.getCheckpointedBlockByHash(blockHash)).toBeUndefined();
394
+ expect(await store.getCheckpointedBlockByArchive(archive)).toBeUndefined();
395
+ expect(await store.getBlockHeaderByHash(blockHash)).toBeUndefined();
396
+ expect(await store.getBlockHeaderByArchive(archive)).toBeUndefined();
73
397
  });
74
398
  });
75
399
 
76
- describe('unwindBlocks', () => {
77
- it('unwinding blocks will remove blocks from the chain', async () => {
78
- await store.addBlocks(blocks);
79
- const blockNumber = await store.getSynchedL2BlockNumber();
400
+ describe('multi-block checkpoints', () => {
401
+ it('block number increases correctly when adding checkpoints with multiple blocks', async () => {
402
+ // Create 3 checkpoints: first with 2 blocks, second with 3 blocks, third with 1 block
403
+ // Total blocks: 6, spanning block numbers 1-6
404
+ // Chain archive roots across checkpoints
405
+ const checkpoint1Cp = await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 });
406
+ const checkpoint1 = makePublishedCheckpoint(checkpoint1Cp, 10);
407
+
408
+ const previousArchive1 = checkpoint1Cp.blocks.at(-1)!.archive;
409
+ const checkpoint2Cp = await Checkpoint.random(CheckpointNumber(2), {
410
+ numBlocks: 3,
411
+ startBlockNumber: 3,
412
+ previousArchive: previousArchive1,
413
+ });
414
+ const checkpoint2 = makePublishedCheckpoint(checkpoint2Cp, 11);
415
+
416
+ const previousArchive2 = checkpoint2Cp.blocks.at(-1)!.archive;
417
+ const checkpoint3Cp = await Checkpoint.random(CheckpointNumber(3), {
418
+ numBlocks: 1,
419
+ startBlockNumber: 6,
420
+ previousArchive: previousArchive2,
421
+ });
422
+ const checkpoint3 = makePublishedCheckpoint(checkpoint3Cp, 12);
423
+
424
+ await store.addCheckpoints([checkpoint1, checkpoint2, checkpoint3]);
425
+
426
+ // Checkpoint number should be 3 (the last checkpoint number)
427
+ expect(await store.getSynchedCheckpointNumber()).toBe(3);
428
+ // Block number should be 6 (the last block number across all checkpoints)
429
+ expect(await store.getLatestBlockNumber()).toBe(6);
430
+ });
431
+
432
+ it('block number decreases correctly when unwinding checkpoints with multiple blocks', async () => {
433
+ // Create 3 checkpoints with varying block counts, chaining archive roots
434
+ const checkpoint1Cp = await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 });
435
+ const checkpoint1 = makePublishedCheckpoint(checkpoint1Cp, 10);
436
+
437
+ const previousArchive1 = checkpoint1Cp.blocks.at(-1)!.archive;
438
+ const checkpoint2Cp = await Checkpoint.random(CheckpointNumber(2), {
439
+ numBlocks: 3,
440
+ startBlockNumber: 3,
441
+ previousArchive: previousArchive1,
442
+ });
443
+ const checkpoint2 = makePublishedCheckpoint(checkpoint2Cp, 11);
444
+
445
+ const previousArchive2 = checkpoint2Cp.blocks.at(-1)!.archive;
446
+ const checkpoint3Cp = await Checkpoint.random(CheckpointNumber(3), {
447
+ numBlocks: 2,
448
+ startBlockNumber: 6,
449
+ previousArchive: previousArchive2,
450
+ });
451
+ const checkpoint3 = makePublishedCheckpoint(checkpoint3Cp, 12);
452
+
453
+ await store.addCheckpoints([checkpoint1, checkpoint2, checkpoint3]);
454
+
455
+ expect(await store.getSynchedCheckpointNumber()).toBe(3);
456
+ expect(await store.getLatestBlockNumber()).toBe(7);
457
+
458
+ // Unwind the last checkpoint (which has 2 blocks)
459
+ await store.unwindCheckpoints(CheckpointNumber(3), 1);
460
+
461
+ expect(await store.getSynchedCheckpointNumber()).toBe(2);
462
+ expect(await store.getLatestBlockNumber()).toBe(5);
463
+
464
+ // Unwind another checkpoint (which has 3 blocks)
465
+ await store.unwindCheckpoints(CheckpointNumber(2), 1);
466
+
467
+ expect(await store.getSynchedCheckpointNumber()).toBe(1);
468
+ expect(await store.getLatestBlockNumber()).toBe(2);
469
+ });
470
+
471
+ it('unwinding multiple checkpoints with multiple blocks in one go', async () => {
472
+ // Create 4 checkpoints with varying block counts, chaining archive roots
473
+ // Checkpoint 1: blocks 1-2 (2 blocks)
474
+ // Checkpoint 2: blocks 3-5 (3 blocks)
475
+ // Checkpoint 3: blocks 6-7 (2 blocks)
476
+ // Checkpoint 4: blocks 8-10 (3 blocks)
477
+ // Total: 10 blocks across 4 checkpoints
478
+ const checkpoint1Cp = await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 });
479
+ const checkpoint1 = makePublishedCheckpoint(checkpoint1Cp, 10);
480
+
481
+ const previousArchive1 = checkpoint1Cp.blocks.at(-1)!.archive;
482
+ const checkpoint2Cp = await Checkpoint.random(CheckpointNumber(2), {
483
+ numBlocks: 3,
484
+ startBlockNumber: 3,
485
+ previousArchive: previousArchive1,
486
+ });
487
+ const checkpoint2 = makePublishedCheckpoint(checkpoint2Cp, 11);
488
+
489
+ const previousArchive2 = checkpoint2Cp.blocks.at(-1)!.archive;
490
+ const checkpoint3Cp = await Checkpoint.random(CheckpointNumber(3), {
491
+ numBlocks: 2,
492
+ startBlockNumber: 6,
493
+ previousArchive: previousArchive2,
494
+ });
495
+ const checkpoint3 = makePublishedCheckpoint(checkpoint3Cp, 12);
496
+
497
+ const previousArchive3 = checkpoint3Cp.blocks.at(-1)!.archive;
498
+ const checkpoint4Cp = await Checkpoint.random(CheckpointNumber(4), {
499
+ numBlocks: 3,
500
+ startBlockNumber: 8,
501
+ previousArchive: previousArchive3,
502
+ });
503
+ const checkpoint4 = makePublishedCheckpoint(checkpoint4Cp, 13);
504
+
505
+ await store.addCheckpoints([checkpoint1, checkpoint2, checkpoint3, checkpoint4]);
506
+
507
+ expect(await store.getSynchedCheckpointNumber()).toBe(4);
508
+ expect(await store.getLatestBlockNumber()).toBe(10);
509
+
510
+ // Unwind 2 checkpoints at once (checkpoints 3 and 4, which together have 5 blocks)
511
+ await store.unwindCheckpoints(CheckpointNumber(4), 2);
512
+
513
+ expect(await store.getSynchedCheckpointNumber()).toBe(2);
514
+ expect(await store.getLatestBlockNumber()).toBe(5);
515
+
516
+ // Verify blocks 1-5 still exist (from checkpoints 1 and 2)
517
+ for (let blockNumber = 1; blockNumber <= 5; blockNumber++) {
518
+ expect(await store.getCheckpointedBlock(blockNumber)).toBeDefined();
519
+ }
520
+
521
+ // Verify blocks 6-10 are gone (from checkpoints 3 and 4)
522
+ for (let blockNumber = 6; blockNumber <= 10; blockNumber++) {
523
+ expect(await store.getCheckpointedBlock(blockNumber)).toBeUndefined();
524
+ }
525
+
526
+ // Unwind remaining 2 checkpoints at once (checkpoints 1 and 2, which together have 5 blocks)
527
+ await store.unwindCheckpoints(CheckpointNumber(2), 2);
528
+
529
+ expect(await store.getSynchedCheckpointNumber()).toBe(0);
530
+ expect(await store.getLatestBlockNumber()).toBe(0);
531
+
532
+ // Verify all blocks are gone
533
+ for (let blockNumber = 1; blockNumber <= 10; blockNumber++) {
534
+ expect(await store.getCheckpointedBlock(blockNumber)).toBeUndefined();
535
+ }
536
+ });
537
+
538
+ it('getCheckpointedBlock returns correct checkpoint info for blocks within multi-block checkpoints', async () => {
539
+ // Create checkpoints with chained archive roots
540
+ // Create a checkpoint with 3 blocks
541
+ const checkpoint1Cp = await Checkpoint.random(CheckpointNumber(1), { numBlocks: 3, startBlockNumber: 1 });
542
+ const checkpoint1 = makePublishedCheckpoint(checkpoint1Cp, 10);
543
+
544
+ // Create another checkpoint with 2 blocks
545
+ const previousArchive1 = checkpoint1Cp.blocks.at(-1)!.archive;
546
+ const checkpoint2Cp = await Checkpoint.random(CheckpointNumber(2), {
547
+ numBlocks: 2,
548
+ startBlockNumber: 4,
549
+ previousArchive: previousArchive1,
550
+ });
551
+ const checkpoint2 = makePublishedCheckpoint(checkpoint2Cp, 11);
552
+
553
+ await store.addCheckpoints([checkpoint1, checkpoint2]);
554
+
555
+ // Check blocks from the first checkpoint (blocks 1, 2, 3)
556
+ for (let i = 0; i < 3; i++) {
557
+ const blockNumber = i + 1;
558
+ const retrievedBlock = await store.getCheckpointedBlock(blockNumber);
559
+
560
+ expect(retrievedBlock).toBeDefined();
561
+ expect(retrievedBlock!.checkpointNumber).toBe(1);
562
+ expect(retrievedBlock!.block.number).toBe(blockNumber);
563
+ expect(retrievedBlock!.l1).toEqual(checkpoint1.l1);
564
+ expect(retrievedBlock!.attestations.every((a, j) => a.equals(checkpoint1.attestations[j]))).toBe(true);
565
+ }
566
+
567
+ // Check blocks from the second checkpoint (blocks 4, 5)
568
+ for (let i = 0; i < 2; i++) {
569
+ const blockNumber = i + 4;
570
+ const retrievedBlock = await store.getCheckpointedBlock(blockNumber);
571
+
572
+ expect(retrievedBlock).toBeDefined();
573
+ expect(retrievedBlock!.checkpointNumber).toBe(2);
574
+ expect(retrievedBlock!.block.number).toBe(blockNumber);
575
+ expect(retrievedBlock!.l1).toEqual(checkpoint2.l1);
576
+ expect(retrievedBlock!.attestations.every((a, j) => a.equals(checkpoint2.attestations[j]))).toBe(true);
577
+ }
578
+ });
579
+
580
+ it('getCheckpointedBlockByHash returns correct checkpoint info for blocks within multi-block checkpoints', async () => {
581
+ const checkpoint = makePublishedCheckpoint(
582
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 3, startBlockNumber: 1 }),
583
+ 10,
584
+ );
585
+
586
+ await store.addCheckpoints([checkpoint]);
587
+
588
+ // Check each block by its hash
589
+ for (let i = 0; i < checkpoint.checkpoint.blocks.length; i++) {
590
+ const block = checkpoint.checkpoint.blocks[i];
591
+ const blockHash = await block.header.hash();
592
+ const retrievedBlock = await store.getCheckpointedBlockByHash(blockHash);
593
+
594
+ expect(retrievedBlock).toBeDefined();
595
+ expect(retrievedBlock!.checkpointNumber).toBe(1);
596
+ expect(retrievedBlock!.block.number).toBe(i + 1);
597
+ expect(retrievedBlock!.l1).toEqual(checkpoint.l1);
598
+ }
599
+ });
600
+
601
+ it('getCheckpointedBlockByArchive returns correct checkpoint info for blocks within multi-block checkpoints', async () => {
602
+ const checkpoint = makePublishedCheckpoint(
603
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 3, startBlockNumber: 1 }),
604
+ 10,
605
+ );
606
+
607
+ await store.addCheckpoints([checkpoint]);
608
+
609
+ // Check each block by its archive root
610
+ for (let i = 0; i < checkpoint.checkpoint.blocks.length; i++) {
611
+ const block = checkpoint.checkpoint.blocks[i];
612
+ const archive = block.archive.root;
613
+ const retrievedBlock = await store.getCheckpointedBlockByArchive(archive);
614
+
615
+ expect(retrievedBlock).toBeDefined();
616
+ expect(retrievedBlock!.checkpointNumber).toBe(1);
617
+ expect(retrievedBlock!.block.number).toBe(i + 1);
618
+ expect(retrievedBlock!.l1).toEqual(checkpoint.l1);
619
+ }
620
+ });
621
+
622
+ it('unwinding a multi-block checkpoint removes all its blocks', async () => {
623
+ const checkpoint = makePublishedCheckpoint(
624
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 3, startBlockNumber: 1 }),
625
+ 10,
626
+ );
627
+
628
+ await store.addCheckpoints([checkpoint]);
629
+
630
+ // Verify all 3 blocks exist
631
+ for (let blockNumber = 1; blockNumber <= 3; blockNumber++) {
632
+ expect(await store.getCheckpointedBlock(blockNumber)).toBeDefined();
633
+ }
634
+
635
+ // Unwind the checkpoint
636
+ await store.unwindCheckpoints(CheckpointNumber(1), 1);
637
+
638
+ // Verify all 3 blocks are removed
639
+ for (let blockNumber = 1; blockNumber <= 3; blockNumber++) {
640
+ expect(await store.getCheckpointedBlock(blockNumber)).toBeUndefined();
641
+ }
642
+
643
+ expect(await store.getSynchedCheckpointNumber()).toBe(0);
644
+ expect(await store.getLatestBlockNumber()).toBe(0);
645
+ });
646
+ });
647
+
648
+ describe('uncheckpointed blocks', () => {
649
+ it('can add blocks independently before a checkpoint arrives', async () => {
650
+ // First, establish some checkpointed blocks (checkpoint 1 with blocks 1-3)
651
+ const checkpoint1 = makePublishedCheckpoint(
652
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 3, startBlockNumber: 1 }),
653
+ 10,
654
+ );
655
+ await store.addCheckpoints([checkpoint1]);
656
+
657
+ expect(await store.getSynchedCheckpointNumber()).toBe(1);
658
+ expect(await store.getLatestBlockNumber()).toBe(3);
659
+
660
+ // Now add blocks 4, 5, 6 independently (without a checkpoint) for upcoming checkpoint 2
661
+ // Chain archive roots from the last block of checkpoint 1
662
+ const lastBlockArchive = checkpoint1.checkpoint.blocks.at(-1)!.archive;
663
+ const block4 = await L2BlockNew.random(BlockNumber(4), {
664
+ checkpointNumber: CheckpointNumber(2),
665
+ indexWithinCheckpoint: 0,
666
+ lastArchive: lastBlockArchive,
667
+ });
668
+ const block5 = await L2BlockNew.random(BlockNumber(5), {
669
+ checkpointNumber: CheckpointNumber(2),
670
+ indexWithinCheckpoint: 1,
671
+ lastArchive: block4.archive,
672
+ });
673
+ const block6 = await L2BlockNew.random(BlockNumber(6), {
674
+ checkpointNumber: CheckpointNumber(2),
675
+ indexWithinCheckpoint: 2,
676
+ lastArchive: block5.archive,
677
+ });
678
+
679
+ await store.addBlocks([block4, block5, block6]);
680
+
681
+ // Checkpoint number should still be 1 (no new checkpoint added)
682
+ expect(await store.getSynchedCheckpointNumber()).toBe(1);
683
+ // But latest block number should be 6
684
+ expect(await store.getLatestBlockNumber()).toBe(6);
685
+ });
686
+
687
+ it('getBlock retrieves uncheckpointed blocks', async () => {
688
+ // First, establish some checkpointed blocks
689
+ const checkpoint1 = makePublishedCheckpoint(
690
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 }),
691
+ 10,
692
+ );
693
+ await store.addCheckpoints([checkpoint1]);
694
+
695
+ // Add uncheckpointed blocks for upcoming checkpoint 2, chaining archive roots
696
+ const lastBlockArchive = checkpoint1.checkpoint.blocks.at(-1)!.archive;
697
+ const block3 = await L2BlockNew.random(BlockNumber(3), {
698
+ checkpointNumber: CheckpointNumber(2),
699
+ indexWithinCheckpoint: 0,
700
+ lastArchive: lastBlockArchive,
701
+ });
702
+ const block4 = await L2BlockNew.random(BlockNumber(4), {
703
+ checkpointNumber: CheckpointNumber(2),
704
+ indexWithinCheckpoint: 1,
705
+ lastArchive: block3.archive,
706
+ });
707
+ await store.addBlocks([block3, block4]);
708
+
709
+ // getBlock should work for both checkpointed and uncheckpointed blocks
710
+ expect((await store.getBlock(1))?.number).toBe(1);
711
+ expect((await store.getBlock(2))?.number).toBe(2);
712
+ expect((await store.getBlock(3))?.equals(block3)).toBe(true);
713
+ expect((await store.getBlock(4))?.equals(block4)).toBe(true);
714
+ expect(await store.getBlock(5)).toBeUndefined();
715
+
716
+ const block5 = await L2BlockNew.random(BlockNumber(5), {
717
+ checkpointNumber: CheckpointNumber(2),
718
+ indexWithinCheckpoint: 2,
719
+ lastArchive: block4.archive,
720
+ });
721
+ await store.addBlocks([block5]);
722
+
723
+ // Verify the uncheckpointed blocks have correct data
724
+ const retrieved3 = await store.getBlock(3);
725
+ expect(retrieved3!.number).toBe(3);
726
+ expect(retrieved3!.equals(block3)).toBe(true);
727
+ const retrieved4 = await store.getBlock(4);
728
+ expect(retrieved4!.number).toBe(4);
729
+ expect(retrieved4!.equals(block4)).toBe(true);
730
+ const retrieved5 = await store.getBlock(5);
731
+ expect(retrieved5!.number).toBe(5);
732
+ expect(retrieved5!.equals(block5)).toBe(true);
733
+ });
734
+
735
+ it('getBlockByHash retrieves uncheckpointed blocks', async () => {
736
+ // Add uncheckpointed blocks (no checkpoints at all) for initial checkpoint 1, chaining archive roots
737
+ const block1 = await L2BlockNew.random(BlockNumber(1), {
738
+ checkpointNumber: CheckpointNumber(1),
739
+ indexWithinCheckpoint: 0,
740
+ });
741
+ const block2 = await L2BlockNew.random(BlockNumber(2), {
742
+ checkpointNumber: CheckpointNumber(1),
743
+ indexWithinCheckpoint: 1,
744
+ lastArchive: block1.archive,
745
+ });
746
+ await store.addBlocks([block1, block2]);
747
+
748
+ // getBlockByHash should work for uncheckpointed blocks
749
+ const hash1 = await block1.header.hash();
750
+ const hash2 = await block2.header.hash();
751
+
752
+ const retrieved1 = await store.getBlockByHash(hash1);
753
+ expect(retrieved1!.equals(block1)).toBe(true);
754
+
755
+ const retrieved2 = await store.getBlockByHash(hash2);
756
+ expect(retrieved2!.equals(block2)).toBe(true);
757
+ });
758
+
759
+ it('getBlockByArchive retrieves uncheckpointed blocks', async () => {
760
+ // Add uncheckpointed blocks for initial checkpoint 1, chaining archive roots
761
+ const block1 = await L2BlockNew.random(BlockNumber(1), {
762
+ checkpointNumber: CheckpointNumber(1),
763
+ indexWithinCheckpoint: 0,
764
+ });
765
+ const block2 = await L2BlockNew.random(BlockNumber(2), {
766
+ checkpointNumber: CheckpointNumber(1),
767
+ indexWithinCheckpoint: 1,
768
+ lastArchive: block1.archive,
769
+ });
770
+ await store.addBlocks([block1, block2]);
771
+
772
+ // getBlockByArchive should work for uncheckpointed blocks
773
+ const archive1 = block1.archive.root;
774
+ const archive2 = block2.archive.root;
775
+
776
+ const retrieved1 = await store.getBlockByArchive(archive1);
777
+ expect(retrieved1!.equals(block1)).toBe(true);
778
+
779
+ const retrieved2 = await store.getBlockByArchive(archive2);
780
+ expect(retrieved2!.equals(block2)).toBe(true);
781
+ });
782
+
783
+ it('getCheckpointedBlock returns undefined for uncheckpointed blocks', async () => {
784
+ // Add a checkpoint with blocks 1-2
785
+ const checkpoint1 = makePublishedCheckpoint(
786
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 }),
787
+ 10,
788
+ );
789
+ await store.addCheckpoints([checkpoint1]);
790
+
791
+ // Add uncheckpointed blocks 3-4 for upcoming checkpoint 2, chaining archive roots
792
+ const lastBlockArchive = checkpoint1.checkpoint.blocks.at(-1)!.archive;
793
+ const block3 = await L2BlockNew.random(BlockNumber(3), {
794
+ checkpointNumber: CheckpointNumber(2),
795
+ indexWithinCheckpoint: 0,
796
+ lastArchive: lastBlockArchive,
797
+ });
798
+ const block4 = await L2BlockNew.random(BlockNumber(4), {
799
+ checkpointNumber: CheckpointNumber(2),
800
+ indexWithinCheckpoint: 1,
801
+ lastArchive: block3.archive,
802
+ });
803
+ await store.addBlocks([block3, block4]);
804
+
805
+ // getCheckpointedBlock should work for checkpointed blocks
806
+ expect((await store.getCheckpointedBlock(1))?.block.number).toBe(1);
807
+ expect((await store.getCheckpointedBlock(2))?.block.number).toBe(2);
808
+
809
+ // getCheckpointedBlock should return undefined for uncheckpointed blocks
810
+ expect(await store.getCheckpointedBlock(3)).toBeUndefined();
811
+ expect(await store.getCheckpointedBlock(4)).toBeUndefined();
812
+
813
+ // But getBlock should work for all blocks
814
+ expect((await store.getBlock(3))?.equals(block3)).toBe(true);
815
+ expect((await store.getBlock(4))?.equals(block4)).toBe(true);
816
+ });
817
+
818
+ it('getCheckpointedBlockByHash returns undefined for uncheckpointed blocks', async () => {
819
+ // Add uncheckpointed blocks for initial checkpoint 1
820
+ const block1 = await L2BlockNew.random(BlockNumber(1), {
821
+ checkpointNumber: CheckpointNumber(1),
822
+ indexWithinCheckpoint: 0,
823
+ });
824
+ await store.addBlocks([block1]);
825
+
826
+ const hash = await block1.header.hash();
827
+
828
+ // getCheckpointedBlockByHash should return undefined
829
+ expect(await store.getCheckpointedBlockByHash(hash)).toBeUndefined();
830
+
831
+ // But getBlockByHash should work
832
+ expect((await store.getBlockByHash(hash))?.equals(block1)).toBe(true);
833
+ });
834
+
835
+ it('getCheckpointedBlockByArchive returns undefined for uncheckpointed blocks', async () => {
836
+ // Add uncheckpointed blocks for initial checkpoint 1
837
+ const block1 = await L2BlockNew.random(BlockNumber(1), {
838
+ checkpointNumber: CheckpointNumber(1),
839
+ indexWithinCheckpoint: 0,
840
+ });
841
+ await store.addBlocks([block1]);
842
+
843
+ const archive = block1.archive.root;
844
+
845
+ // getCheckpointedBlockByArchive should return undefined
846
+ expect(await store.getCheckpointedBlockByArchive(archive)).toBeUndefined();
847
+
848
+ // But getBlockByArchive should work
849
+ expect((await store.getBlockByArchive(archive))?.equals(block1)).toBe(true);
850
+ });
851
+
852
+ it('checkpoint adopts previously added uncheckpointed blocks', async () => {
853
+ // Add blocks 1-3 without a checkpoint (for initial checkpoint 1), chaining archive roots
854
+ const block1 = await L2BlockNew.random(BlockNumber(1), {
855
+ checkpointNumber: CheckpointNumber(1),
856
+ indexWithinCheckpoint: 0,
857
+ });
858
+ const block2 = await L2BlockNew.random(BlockNumber(2), {
859
+ checkpointNumber: CheckpointNumber(1),
860
+ indexWithinCheckpoint: 1,
861
+ lastArchive: block1.archive,
862
+ });
863
+ const block3 = await L2BlockNew.random(BlockNumber(3), {
864
+ checkpointNumber: CheckpointNumber(1),
865
+ indexWithinCheckpoint: 2,
866
+ lastArchive: block2.archive,
867
+ });
868
+ await store.addBlocks([block1, block2, block3]);
80
869
 
81
- expect(await store.getBlocks(blockNumber, 1)).toEqual([blocks[blocks.length - 1]]);
870
+ expect(await store.getSynchedCheckpointNumber()).toBe(0);
871
+ expect(await store.getLatestBlockNumber()).toBe(3);
82
872
 
83
- await store.unwindBlocks(blockNumber, 1);
873
+ // getCheckpointedBlock should return undefined for all
874
+ expect(await store.getCheckpointedBlock(1)).toBeUndefined();
875
+ expect(await store.getCheckpointedBlock(2)).toBeUndefined();
876
+ expect(await store.getCheckpointedBlock(3)).toBeUndefined();
84
877
 
85
- expect(await store.getSynchedL2BlockNumber()).toBe(blockNumber - 1);
86
- expect(await store.getBlocks(blockNumber, 1)).toEqual([]);
878
+ // Now add a checkpoint that covers blocks 1-3
879
+ const checkpoint1 = makePublishedCheckpoint(
880
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 3, startBlockNumber: 1 }),
881
+ 10,
882
+ );
883
+ await store.addCheckpoints([checkpoint1]);
884
+
885
+ expect(await store.getSynchedCheckpointNumber()).toBe(1);
886
+ expect(await store.getLatestBlockNumber()).toBe(3);
887
+
888
+ // Now getCheckpointedBlock should work for all blocks
889
+ const checkpointed1 = await store.getCheckpointedBlock(1);
890
+ expect(checkpointed1).toBeDefined();
891
+ expect(checkpointed1!.checkpointNumber).toBe(1);
892
+ expect(checkpointed1!.l1).toEqual(checkpoint1.l1);
893
+
894
+ const checkpointed2 = await store.getCheckpointedBlock(2);
895
+ expect(checkpointed2).toBeDefined();
896
+ expect(checkpointed2!.checkpointNumber).toBe(1);
897
+
898
+ const checkpointed3 = await store.getCheckpointedBlock(3);
899
+ expect(checkpointed3).toBeDefined();
900
+ expect(checkpointed3!.checkpointNumber).toBe(1);
901
+ });
902
+
903
+ it('can add more uncheckpointed blocks after a checkpoint and then checkpoint them', async () => {
904
+ // Start with checkpoint 1 covering blocks 1-2
905
+ const checkpoint1 = makePublishedCheckpoint(
906
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 }),
907
+ 10,
908
+ );
909
+ await store.addCheckpoints([checkpoint1]);
910
+
911
+ // Add uncheckpointed blocks 3-5 for the upcoming checkpoint 2, chaining archive roots
912
+ const lastBlockArchive = checkpoint1.checkpoint.blocks.at(-1)!.archive;
913
+ const block3 = await L2BlockNew.random(BlockNumber(3), {
914
+ checkpointNumber: CheckpointNumber(2),
915
+ indexWithinCheckpoint: 0,
916
+ lastArchive: lastBlockArchive,
917
+ });
918
+ const block4 = await L2BlockNew.random(BlockNumber(4), {
919
+ checkpointNumber: CheckpointNumber(2),
920
+ indexWithinCheckpoint: 1,
921
+ lastArchive: block3.archive,
922
+ });
923
+ const block5 = await L2BlockNew.random(BlockNumber(5), {
924
+ checkpointNumber: CheckpointNumber(2),
925
+ indexWithinCheckpoint: 2,
926
+ lastArchive: block4.archive,
927
+ });
928
+ await store.addBlocks([block3, block4, block5]);
929
+
930
+ expect(await store.getSynchedCheckpointNumber()).toBe(1);
931
+ expect(await store.getLatestBlockNumber()).toBe(5);
932
+
933
+ // Blocks 3-5 are not checkpointed yet
934
+ expect(await store.getCheckpointedBlock(3)).toBeUndefined();
935
+ expect(await store.getCheckpointedBlock(4)).toBeUndefined();
936
+ expect(await store.getCheckpointedBlock(5)).toBeUndefined();
937
+
938
+ // Add checkpoint 2 covering blocks 3-5, chaining from checkpoint1
939
+ const checkpoint2 = makePublishedCheckpoint(
940
+ await Checkpoint.random(CheckpointNumber(2), {
941
+ numBlocks: 3,
942
+ startBlockNumber: 3,
943
+ previousArchive: lastBlockArchive,
944
+ }),
945
+ 11,
946
+ );
947
+ await store.addCheckpoints([checkpoint2]);
948
+
949
+ expect(await store.getSynchedCheckpointNumber()).toBe(2);
950
+ expect(await store.getLatestBlockNumber()).toBe(5);
951
+
952
+ // Now blocks 3-5 should be checkpointed with checkpoint 2's info
953
+ const checkpointed3 = await store.getCheckpointedBlock(3);
954
+ expect(checkpointed3).toBeDefined();
955
+ expect(checkpointed3!.checkpointNumber).toBe(2);
956
+ expect(checkpointed3!.l1).toEqual(checkpoint2.l1);
957
+
958
+ const checkpointed4 = await store.getCheckpointedBlock(4);
959
+ expect(checkpointed4).toBeDefined();
960
+ expect(checkpointed4!.checkpointNumber).toBe(2);
961
+
962
+ const checkpointed5 = await store.getCheckpointedBlock(5);
963
+ expect(checkpointed5).toBeDefined();
964
+ expect(checkpointed5!.checkpointNumber).toBe(2);
965
+ });
966
+
967
+ it('getBlocks retrieves both checkpointed and uncheckpointed blocks', async () => {
968
+ // Add checkpoint with blocks 1-2
969
+ const checkpoint1 = makePublishedCheckpoint(
970
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 }),
971
+ 10,
972
+ );
973
+ await store.addCheckpoints([checkpoint1]);
974
+
975
+ // Add uncheckpointed blocks 3-4 for the upcoming checkpoint 2, chaining archive roots
976
+ const lastBlockArchive = checkpoint1.checkpoint.blocks.at(-1)!.archive;
977
+ const block3 = await L2BlockNew.random(BlockNumber(3), {
978
+ checkpointNumber: CheckpointNumber(2),
979
+ indexWithinCheckpoint: 0,
980
+ lastArchive: lastBlockArchive,
981
+ });
982
+ const block4 = await L2BlockNew.random(BlockNumber(4), {
983
+ checkpointNumber: CheckpointNumber(2),
984
+ indexWithinCheckpoint: 1,
985
+ lastArchive: block3.archive,
986
+ });
987
+ await store.addBlocks([block3, block4]);
988
+
989
+ // getBlocks should retrieve all blocks
990
+ const allBlocks = await store.getBlocks(1, 10);
991
+ expect(allBlocks.length).toBe(4);
992
+ expect(allBlocks.map(b => b.number)).toEqual([1, 2, 3, 4]);
993
+ });
994
+ });
995
+
996
+ describe('addBlocks validation', () => {
997
+ it('throws if blocks have different checkpoint numbers', async () => {
998
+ // First, establish checkpoint 1 with blocks 1-2
999
+ const checkpoint1 = makePublishedCheckpoint(
1000
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 }),
1001
+ 10,
1002
+ );
1003
+ await store.addCheckpoints([checkpoint1]);
1004
+
1005
+ // Try to add blocks 3 and 4 with different checkpoint numbers
1006
+ // Chain archives correctly to test the checkpoint number validation
1007
+ const lastBlockArchive = checkpoint1.checkpoint.blocks.at(-1)!.archive;
1008
+ const block3 = await L2BlockNew.random(BlockNumber(3), {
1009
+ checkpointNumber: CheckpointNumber(2),
1010
+ indexWithinCheckpoint: 0,
1011
+ lastArchive: lastBlockArchive,
1012
+ });
1013
+ const block4 = await L2BlockNew.random(BlockNumber(4), {
1014
+ checkpointNumber: CheckpointNumber(3),
1015
+ indexWithinCheckpoint: 1,
1016
+ lastArchive: block3.archive,
1017
+ });
1018
+
1019
+ await expect(store.addBlocks([block3, block4])).rejects.toThrow(CheckpointNumberNotConsistentError);
1020
+ });
1021
+
1022
+ it('throws if checkpoint number is not the current checkpoint', async () => {
1023
+ // First, establish checkpoint 1 with blocks 1-2
1024
+ const checkpoint1 = makePublishedCheckpoint(
1025
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 }),
1026
+ 10,
1027
+ );
1028
+ await store.addCheckpoints([checkpoint1]);
1029
+
1030
+ // Try to add blocks for checkpoint 3 (skipping checkpoint 2)
1031
+ const block3 = await L2BlockNew.random(BlockNumber(3), {
1032
+ checkpointNumber: CheckpointNumber(3),
1033
+ indexWithinCheckpoint: 0,
1034
+ });
1035
+ const block4 = await L2BlockNew.random(BlockNumber(4), {
1036
+ checkpointNumber: CheckpointNumber(3),
1037
+ indexWithinCheckpoint: 1,
1038
+ });
1039
+
1040
+ await expect(store.addBlocks([block3, block4])).rejects.toThrow(InitialCheckpointNumberNotSequentialError);
1041
+ });
1042
+
1043
+ it('allows blocks with the same checkpoint number for the current checkpoint', async () => {
1044
+ // First, establish checkpoint 1 with blocks 1-2
1045
+ const checkpoint1 = makePublishedCheckpoint(
1046
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 }),
1047
+ 10,
1048
+ );
1049
+ await store.addCheckpoints([checkpoint1]);
1050
+
1051
+ // Add blocks 3 and 4 with consistent checkpoint number (2), chaining archive roots
1052
+ const lastBlockArchive = checkpoint1.checkpoint.blocks.at(-1)!.archive;
1053
+ const block3 = await L2BlockNew.random(BlockNumber(3), {
1054
+ checkpointNumber: CheckpointNumber(2),
1055
+ indexWithinCheckpoint: 0,
1056
+ lastArchive: lastBlockArchive,
1057
+ });
1058
+ const block4 = await L2BlockNew.random(BlockNumber(4), {
1059
+ checkpointNumber: CheckpointNumber(2),
1060
+ indexWithinCheckpoint: 1,
1061
+ lastArchive: block3.archive,
1062
+ });
1063
+
1064
+ await expect(store.addBlocks([block3, block4])).resolves.toBe(true);
1065
+
1066
+ // Verify blocks were added
1067
+ expect((await store.getBlock(3))?.equals(block3)).toBe(true);
1068
+ expect((await store.getBlock(4))?.equals(block4)).toBe(true);
1069
+ });
1070
+
1071
+ it('allows blocks for the initial checkpoint when store is empty', async () => {
1072
+ // Add blocks for the initial checkpoint (1), chaining archive roots
1073
+ const block1 = await L2BlockNew.random(BlockNumber(1), {
1074
+ checkpointNumber: CheckpointNumber(1),
1075
+ indexWithinCheckpoint: 0,
1076
+ });
1077
+ const block2 = await L2BlockNew.random(BlockNumber(2), {
1078
+ checkpointNumber: CheckpointNumber(1),
1079
+ indexWithinCheckpoint: 1,
1080
+ lastArchive: block1.archive,
1081
+ });
1082
+
1083
+ await expect(store.addBlocks([block1, block2])).resolves.toBe(true);
1084
+
1085
+ // Verify blocks were added
1086
+ expect((await store.getBlock(1))?.equals(block1)).toBe(true);
1087
+ expect((await store.getBlock(2))?.equals(block2)).toBe(true);
1088
+ expect(await store.getLatestBlockNumber()).toBe(2);
1089
+ });
1090
+
1091
+ it('throws if initial block is duplicated across calls', async () => {
1092
+ // Add blocks for the initial checkpoint (1)
1093
+ const block1 = await L2BlockNew.random(BlockNumber(1), {
1094
+ checkpointNumber: CheckpointNumber(1),
1095
+ indexWithinCheckpoint: 0,
1096
+ });
1097
+ const block2 = await L2BlockNew.random(BlockNumber(1), {
1098
+ checkpointNumber: CheckpointNumber(1),
1099
+ indexWithinCheckpoint: 0,
1100
+ });
1101
+
1102
+ await expect(store.addBlocks([block1])).resolves.toBe(true);
1103
+ await expect(store.addBlocks([block2])).rejects.toThrow(InitialBlockNumberNotSequentialError);
1104
+ });
1105
+
1106
+ it('throws if first block has wrong checkpoint number when store is empty', async () => {
1107
+ // Try to add blocks for checkpoint 2 when store is empty (should start at 1)
1108
+ const block1 = await L2BlockNew.random(BlockNumber(1), {
1109
+ checkpointNumber: CheckpointNumber(2),
1110
+ indexWithinCheckpoint: 0,
1111
+ });
1112
+ const block2 = await L2BlockNew.random(BlockNumber(2), {
1113
+ checkpointNumber: CheckpointNumber(2),
1114
+ indexWithinCheckpoint: 1,
1115
+ });
1116
+
1117
+ await expect(store.addBlocks([block1, block2])).rejects.toThrow(InitialCheckpointNumberNotSequentialError);
1118
+ });
1119
+
1120
+ it('allows adding more blocks to the same checkpoint in separate calls', async () => {
1121
+ // First, establish checkpoint 1 with blocks 1-2
1122
+ const checkpoint1 = makePublishedCheckpoint(
1123
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 }),
1124
+ 10,
1125
+ );
1126
+ await store.addCheckpoints([checkpoint1]);
1127
+
1128
+ // Add block 3 for checkpoint 2, chaining archive roots
1129
+ const lastBlockArchive = checkpoint1.checkpoint.blocks.at(-1)!.archive;
1130
+ const block3 = await L2BlockNew.random(BlockNumber(3), {
1131
+ checkpointNumber: CheckpointNumber(2),
1132
+ indexWithinCheckpoint: 0,
1133
+ lastArchive: lastBlockArchive,
1134
+ });
1135
+ await expect(store.addBlocks([block3])).resolves.toBe(true);
1136
+
1137
+ // Add block 4 for the same checkpoint 2 in a separate call
1138
+ const block4 = await L2BlockNew.random(BlockNumber(4), {
1139
+ checkpointNumber: CheckpointNumber(2),
1140
+ indexWithinCheckpoint: 1,
1141
+ lastArchive: block3.archive,
1142
+ });
1143
+ await expect(store.addBlocks([block4])).resolves.toBe(true);
1144
+
1145
+ expect(await store.getLatestBlockNumber()).toBe(4);
1146
+ });
1147
+
1148
+ it('throws if adding blocks in separate calls with non-consecutive indexes', async () => {
1149
+ // First, establish checkpoint 1 with blocks 1-2
1150
+ const checkpoint1 = makePublishedCheckpoint(
1151
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 }),
1152
+ 10,
1153
+ );
1154
+ await store.addCheckpoints([checkpoint1]);
1155
+
1156
+ // Add block 3 for checkpoint 2, chaining archive roots
1157
+ const lastBlockArchive = checkpoint1.checkpoint.blocks.at(-1)!.archive;
1158
+ const block3 = await L2BlockNew.random(BlockNumber(3), {
1159
+ checkpointNumber: CheckpointNumber(2),
1160
+ indexWithinCheckpoint: 0,
1161
+ lastArchive: lastBlockArchive,
1162
+ });
1163
+ await expect(store.addBlocks([block3])).resolves.toBe(true);
1164
+
1165
+ // Add block 4 for the same checkpoint 2 in a separate call but with a missing index
1166
+ const block4 = await L2BlockNew.random(BlockNumber(4), {
1167
+ checkpointNumber: CheckpointNumber(2),
1168
+ indexWithinCheckpoint: 2,
1169
+ lastArchive: block3.archive,
1170
+ });
1171
+ await expect(store.addBlocks([block4])).rejects.toThrow(BlockIndexNotSequentialError);
1172
+
1173
+ expect(await store.getLatestBlockNumber()).toBe(3);
1174
+ });
1175
+
1176
+ it('throws if second batch of blocks has different checkpoint number than first batch', async () => {
1177
+ // First, establish checkpoint 1 with blocks 1-2
1178
+ const checkpoint1 = makePublishedCheckpoint(
1179
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 }),
1180
+ 10,
1181
+ );
1182
+ await store.addCheckpoints([checkpoint1]);
1183
+
1184
+ // Add block 3 for checkpoint 2, chaining archive roots
1185
+ const lastBlockArchive = checkpoint1.checkpoint.blocks.at(-1)!.archive;
1186
+ const block3 = await L2BlockNew.random(BlockNumber(3), {
1187
+ checkpointNumber: CheckpointNumber(2),
1188
+ indexWithinCheckpoint: 0,
1189
+ lastArchive: lastBlockArchive,
1190
+ });
1191
+ await store.addBlocks([block3]);
1192
+
1193
+ // Try to add block 4 for checkpoint 3 (should fail because current checkpoint is still 2)
1194
+ const block4 = await L2BlockNew.random(BlockNumber(4), {
1195
+ checkpointNumber: CheckpointNumber(3),
1196
+ indexWithinCheckpoint: 0,
1197
+ lastArchive: block3.archive,
1198
+ });
1199
+ await expect(store.addBlocks([block4])).rejects.toThrow(InitialCheckpointNumberNotSequentialError);
1200
+ });
1201
+
1202
+ it('force option bypasses checkpoint number validation', async () => {
1203
+ // First, establish checkpoint 1 with blocks 1-2
1204
+ const checkpoint1 = makePublishedCheckpoint(
1205
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 }),
1206
+ 10,
1207
+ );
1208
+ await store.addCheckpoints([checkpoint1]);
1209
+
1210
+ // Add blocks with different checkpoint numbers using force option, chaining archive roots
1211
+ const lastBlockArchive = checkpoint1.checkpoint.blocks.at(-1)!.archive;
1212
+ const block3 = await L2BlockNew.random(BlockNumber(3), {
1213
+ checkpointNumber: CheckpointNumber(2),
1214
+ indexWithinCheckpoint: 0,
1215
+ lastArchive: lastBlockArchive,
1216
+ });
1217
+ const block4 = await L2BlockNew.random(BlockNumber(4), {
1218
+ checkpointNumber: CheckpointNumber(5),
1219
+ indexWithinCheckpoint: 0,
1220
+ lastArchive: block3.archive,
1221
+ });
1222
+
1223
+ await expect(store.addBlocks([block3, block4], { force: true })).resolves.toBe(true);
1224
+ });
1225
+
1226
+ it('force option bypasses blockindex number validation', async () => {
1227
+ // First, establish checkpoint 1 with blocks 1-2
1228
+ const checkpoint1 = makePublishedCheckpoint(
1229
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 }),
1230
+ 10,
1231
+ );
1232
+ await store.addCheckpoints([checkpoint1]);
1233
+
1234
+ // Add blocks with different checkpoint numbers using force option, chaining archive roots
1235
+ const lastBlockArchive = checkpoint1.checkpoint.blocks.at(-1)!.archive;
1236
+ const block3 = await L2BlockNew.random(BlockNumber(3), {
1237
+ checkpointNumber: CheckpointNumber(2),
1238
+ indexWithinCheckpoint: 0,
1239
+ lastArchive: lastBlockArchive,
1240
+ });
1241
+ const block4 = await L2BlockNew.random(BlockNumber(4), {
1242
+ checkpointNumber: CheckpointNumber(2),
1243
+ indexWithinCheckpoint: 2,
1244
+ lastArchive: block3.archive,
1245
+ });
1246
+
1247
+ await expect(store.addBlocks([block3, block4], { force: true })).resolves.toBe(true);
1248
+ });
1249
+
1250
+ it('throws if adding blocks with non-consecutive archives', async () => {
1251
+ // First, establish checkpoint 1 with blocks 1-2
1252
+ const checkpoint1 = makePublishedCheckpoint(
1253
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 }),
1254
+ 10,
1255
+ );
1256
+ await store.addCheckpoints([checkpoint1]);
1257
+
1258
+ // Add block 3 for checkpoint 2 with incorrect archive
1259
+ const block3 = await L2BlockNew.random(BlockNumber(3), {
1260
+ checkpointNumber: CheckpointNumber(2),
1261
+ indexWithinCheckpoint: 0,
1262
+ });
1263
+ await expect(store.addBlocks([block3])).rejects.toThrow(BlockArchiveNotConsistentError);
1264
+
1265
+ expect(await store.getLatestBlockNumber()).toBe(2);
1266
+ });
1267
+
1268
+ it('throws if adding blocks with non-consecutive archives across calls', async () => {
1269
+ // First, establish checkpoint 1 with blocks 1-2
1270
+ const checkpoint1 = makePublishedCheckpoint(
1271
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 }),
1272
+ 10,
1273
+ );
1274
+ await store.addCheckpoints([checkpoint1]);
1275
+
1276
+ // Add block 3 for checkpoint 2 with correct archive
1277
+ const lastBlockArchive = checkpoint1.checkpoint.blocks.at(-1)!.archive;
1278
+ const block3 = await L2BlockNew.random(BlockNumber(3), {
1279
+ checkpointNumber: CheckpointNumber(2),
1280
+ indexWithinCheckpoint: 0,
1281
+ lastArchive: lastBlockArchive,
1282
+ });
1283
+ await expect(store.addBlocks([block3])).resolves.toBe(true);
1284
+
1285
+ // Add block 4 with incorrect archive (should fail)
1286
+ const block4 = await L2BlockNew.random(BlockNumber(4), {
1287
+ checkpointNumber: CheckpointNumber(2),
1288
+ indexWithinCheckpoint: 1,
1289
+ lastArchive: AppendOnlyTreeSnapshot.random(),
1290
+ });
1291
+ await expect(store.addBlocks([block4])).rejects.toThrow(BlockArchiveNotConsistentError);
1292
+
1293
+ expect(await store.getLatestBlockNumber()).toBe(3);
1294
+ });
1295
+ });
1296
+
1297
+ describe('getBlocksForCheckpoint', () => {
1298
+ it('returns blocks for a single-block checkpoint', async () => {
1299
+ const checkpoint = makePublishedCheckpoint(
1300
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 1, startBlockNumber: 1 }),
1301
+ 10,
1302
+ );
1303
+ await store.addCheckpoints([checkpoint]);
1304
+
1305
+ const blocks = await store.getBlocksForCheckpoint(CheckpointNumber(1));
1306
+ expect(blocks).toBeDefined();
1307
+ expect(blocks!.length).toBe(1);
1308
+ expect(blocks![0].number).toBe(1);
1309
+ });
1310
+
1311
+ it('returns all blocks for a multi-block checkpoint', async () => {
1312
+ const checkpoint = makePublishedCheckpoint(
1313
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 4, startBlockNumber: 1 }),
1314
+ 10,
1315
+ );
1316
+ await store.addCheckpoints([checkpoint]);
1317
+
1318
+ const blocks = await store.getBlocksForCheckpoint(CheckpointNumber(1));
1319
+ expect(blocks).toBeDefined();
1320
+ expect(blocks!.length).toBe(4);
1321
+ expect(blocks!.map(b => b.number)).toEqual([1, 2, 3, 4]);
1322
+ });
1323
+
1324
+ it('returns correct blocks for different checkpoints', async () => {
1325
+ // Create checkpoints with chained archive roots
1326
+ // Checkpoint 1: blocks 1-2
1327
+ const checkpoint1Cp = await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 });
1328
+ const checkpoint1 = makePublishedCheckpoint(checkpoint1Cp, 10);
1329
+
1330
+ // Checkpoint 2: blocks 3-5
1331
+ const previousArchive1 = checkpoint1Cp.blocks.at(-1)!.archive;
1332
+ const checkpoint2Cp = await Checkpoint.random(CheckpointNumber(2), {
1333
+ numBlocks: 3,
1334
+ startBlockNumber: 3,
1335
+ previousArchive: previousArchive1,
1336
+ });
1337
+ const checkpoint2 = makePublishedCheckpoint(checkpoint2Cp, 11);
1338
+
1339
+ // Checkpoint 3: blocks 6-7
1340
+ const previousArchive2 = checkpoint2Cp.blocks.at(-1)!.archive;
1341
+ const checkpoint3Cp = await Checkpoint.random(CheckpointNumber(3), {
1342
+ numBlocks: 2,
1343
+ startBlockNumber: 6,
1344
+ previousArchive: previousArchive2,
1345
+ });
1346
+ const checkpoint3 = makePublishedCheckpoint(checkpoint3Cp, 12);
1347
+
1348
+ await store.addCheckpoints([checkpoint1, checkpoint2, checkpoint3]);
1349
+
1350
+ const blocks1 = await store.getBlocksForCheckpoint(CheckpointNumber(1));
1351
+ expect(blocks1).toBeDefined();
1352
+ expect(blocks1!.map(b => b.number)).toEqual([1, 2]);
1353
+
1354
+ const blocks2 = await store.getBlocksForCheckpoint(CheckpointNumber(2));
1355
+ expect(blocks2).toBeDefined();
1356
+ expect(blocks2!.map(b => b.number)).toEqual([3, 4, 5]);
1357
+
1358
+ const blocks3 = await store.getBlocksForCheckpoint(CheckpointNumber(3));
1359
+ expect(blocks3).toBeDefined();
1360
+ expect(blocks3!.map(b => b.number)).toEqual([6, 7]);
1361
+ });
1362
+
1363
+ it('returns undefined for non-existent checkpoint', async () => {
1364
+ const checkpoint = makePublishedCheckpoint(
1365
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 }),
1366
+ 10,
1367
+ );
1368
+ await store.addCheckpoints([checkpoint]);
1369
+
1370
+ const blocks = await store.getBlocksForCheckpoint(CheckpointNumber(5));
1371
+ expect(blocks).toBeUndefined();
1372
+ });
1373
+
1374
+ it('returns undefined when no checkpoints exist', async () => {
1375
+ const blocks = await store.getBlocksForCheckpoint(CheckpointNumber(1));
1376
+ expect(blocks).toBeUndefined();
1377
+ });
1378
+ });
1379
+
1380
+ describe('getRangeOfCheckpoints', () => {
1381
+ it('returns empty array when no checkpoints exist', async () => {
1382
+ const checkpoints = await store.getRangeOfCheckpoints(CheckpointNumber(1), 10);
1383
+ expect(checkpoints).toEqual([]);
1384
+ });
1385
+
1386
+ it('returns single checkpoint', async () => {
1387
+ const checkpoint = makePublishedCheckpoint(
1388
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 }),
1389
+ 10,
1390
+ );
1391
+ await store.addCheckpoints([checkpoint]);
1392
+
1393
+ const checkpoints = await store.getRangeOfCheckpoints(CheckpointNumber(1), 10);
1394
+ expect(checkpoints.length).toBe(1);
1395
+ expect(checkpoints[0].checkpointNumber).toBe(1);
1396
+ expect(checkpoints[0].startBlock).toBe(1);
1397
+ expect(checkpoints[0].numBlocks).toBe(2);
1398
+ });
1399
+
1400
+ it('returns multiple checkpoints in order', async () => {
1401
+ // Create checkpoints with chained archive roots
1402
+ const checkpoint1Cp = await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 });
1403
+ const checkpoint1 = makePublishedCheckpoint(checkpoint1Cp, 10);
1404
+
1405
+ const previousArchive1 = checkpoint1Cp.blocks.at(-1)!.archive;
1406
+ const checkpoint2Cp = await Checkpoint.random(CheckpointNumber(2), {
1407
+ numBlocks: 3,
1408
+ startBlockNumber: 3,
1409
+ previousArchive: previousArchive1,
1410
+ });
1411
+ const checkpoint2 = makePublishedCheckpoint(checkpoint2Cp, 11);
1412
+
1413
+ const previousArchive2 = checkpoint2Cp.blocks.at(-1)!.archive;
1414
+ const checkpoint3Cp = await Checkpoint.random(CheckpointNumber(3), {
1415
+ numBlocks: 1,
1416
+ startBlockNumber: 6,
1417
+ previousArchive: previousArchive2,
1418
+ });
1419
+ const checkpoint3 = makePublishedCheckpoint(checkpoint3Cp, 12);
1420
+
1421
+ await store.addCheckpoints([checkpoint1, checkpoint2, checkpoint3]);
1422
+
1423
+ const checkpoints = await store.getRangeOfCheckpoints(CheckpointNumber(1), 10);
1424
+ expect(checkpoints.length).toBe(3);
1425
+ expect(checkpoints.map(c => c.checkpointNumber)).toEqual([1, 2, 3]);
1426
+ expect(checkpoints.map(c => c.startBlock)).toEqual([1, 3, 6]);
1427
+ expect(checkpoints.map(c => c.numBlocks)).toEqual([2, 3, 1]);
1428
+ });
1429
+
1430
+ it('respects the from parameter', async () => {
1431
+ // Create checkpoints with chained archive roots
1432
+ const checkpoint1Cp = await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 });
1433
+ const checkpoint1 = makePublishedCheckpoint(checkpoint1Cp, 10);
1434
+
1435
+ const previousArchive1 = checkpoint1Cp.blocks.at(-1)!.archive;
1436
+ const checkpoint2Cp = await Checkpoint.random(CheckpointNumber(2), {
1437
+ numBlocks: 2,
1438
+ startBlockNumber: 3,
1439
+ previousArchive: previousArchive1,
1440
+ });
1441
+ const checkpoint2 = makePublishedCheckpoint(checkpoint2Cp, 11);
1442
+
1443
+ const previousArchive2 = checkpoint2Cp.blocks.at(-1)!.archive;
1444
+ const checkpoint3Cp = await Checkpoint.random(CheckpointNumber(3), {
1445
+ numBlocks: 2,
1446
+ startBlockNumber: 5,
1447
+ previousArchive: previousArchive2,
1448
+ });
1449
+ const checkpoint3 = makePublishedCheckpoint(checkpoint3Cp, 12);
1450
+
1451
+ await store.addCheckpoints([checkpoint1, checkpoint2, checkpoint3]);
1452
+
1453
+ // Start from checkpoint 2
1454
+ const checkpoints = await store.getRangeOfCheckpoints(CheckpointNumber(2), 10);
1455
+ expect(checkpoints.length).toBe(2);
1456
+ expect(checkpoints.map(c => c.checkpointNumber)).toEqual([2, 3]);
1457
+ });
1458
+
1459
+ it('respects the limit parameter', async () => {
1460
+ // Create checkpoints with chained archive roots
1461
+ const checkpoint1Cp = await Checkpoint.random(CheckpointNumber(1), { numBlocks: 1, startBlockNumber: 1 });
1462
+ const checkpoint1 = makePublishedCheckpoint(checkpoint1Cp, 10);
1463
+
1464
+ const previousArchive1 = checkpoint1Cp.blocks.at(-1)!.archive;
1465
+ const checkpoint2Cp = await Checkpoint.random(CheckpointNumber(2), {
1466
+ numBlocks: 1,
1467
+ startBlockNumber: 2,
1468
+ previousArchive: previousArchive1,
1469
+ });
1470
+ const checkpoint2 = makePublishedCheckpoint(checkpoint2Cp, 11);
1471
+
1472
+ const previousArchive2 = checkpoint2Cp.blocks.at(-1)!.archive;
1473
+ const checkpoint3Cp = await Checkpoint.random(CheckpointNumber(3), {
1474
+ numBlocks: 1,
1475
+ startBlockNumber: 3,
1476
+ previousArchive: previousArchive2,
1477
+ });
1478
+ const checkpoint3 = makePublishedCheckpoint(checkpoint3Cp, 12);
1479
+
1480
+ const previousArchive3 = checkpoint3Cp.blocks.at(-1)!.archive;
1481
+ const checkpoint4Cp = await Checkpoint.random(CheckpointNumber(4), {
1482
+ numBlocks: 1,
1483
+ startBlockNumber: 4,
1484
+ previousArchive: previousArchive3,
1485
+ });
1486
+ const checkpoint4 = makePublishedCheckpoint(checkpoint4Cp, 13);
1487
+
1488
+ await store.addCheckpoints([checkpoint1, checkpoint2, checkpoint3, checkpoint4]);
1489
+
1490
+ // Only get 2 checkpoints
1491
+ const checkpoints = await store.getRangeOfCheckpoints(CheckpointNumber(1), 2);
1492
+ expect(checkpoints.length).toBe(2);
1493
+ expect(checkpoints.map(c => c.checkpointNumber)).toEqual([1, 2]);
1494
+ });
1495
+
1496
+ it('returns correct checkpoint data including L1 info', async () => {
1497
+ const checkpoint = makePublishedCheckpoint(
1498
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 3, startBlockNumber: 1 }),
1499
+ 42,
1500
+ );
1501
+ await store.addCheckpoints([checkpoint]);
1502
+
1503
+ const checkpoints = await store.getRangeOfCheckpoints(CheckpointNumber(1), 1);
1504
+ expect(checkpoints.length).toBe(1);
1505
+
1506
+ const data = checkpoints[0];
1507
+ expect(data.checkpointNumber).toBe(1);
1508
+ expect(data.startBlock).toBe(1);
1509
+ expect(data.numBlocks).toBe(3);
1510
+ expect(data.l1.blockNumber).toBe(42n);
1511
+ expect(data.header.equals(checkpoint.checkpoint.header)).toBe(true);
1512
+ expect(data.archive.equals(checkpoint.checkpoint.archive)).toBe(true);
1513
+ });
1514
+
1515
+ it('returns empty array when from is beyond available checkpoints', async () => {
1516
+ const checkpoint = makePublishedCheckpoint(
1517
+ await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 }),
1518
+ 10,
1519
+ );
1520
+ await store.addCheckpoints([checkpoint]);
1521
+
1522
+ const checkpoints = await store.getRangeOfCheckpoints(CheckpointNumber(5), 10);
1523
+ expect(checkpoints).toEqual([]);
1524
+ });
1525
+
1526
+ it('works correctly after unwinding checkpoints', async () => {
1527
+ // Create checkpoints with chained archive roots
1528
+ const checkpoint1Cp = await Checkpoint.random(CheckpointNumber(1), { numBlocks: 2, startBlockNumber: 1 });
1529
+ const checkpoint1 = makePublishedCheckpoint(checkpoint1Cp, 10);
1530
+
1531
+ const previousArchive1 = checkpoint1Cp.blocks.at(-1)!.archive;
1532
+ const checkpoint2Cp = await Checkpoint.random(CheckpointNumber(2), {
1533
+ numBlocks: 2,
1534
+ startBlockNumber: 3,
1535
+ previousArchive: previousArchive1,
1536
+ });
1537
+ const checkpoint2 = makePublishedCheckpoint(checkpoint2Cp, 11);
1538
+
1539
+ const previousArchive2 = checkpoint2Cp.blocks.at(-1)!.archive;
1540
+ const checkpoint3Cp = await Checkpoint.random(CheckpointNumber(3), {
1541
+ numBlocks: 2,
1542
+ startBlockNumber: 5,
1543
+ previousArchive: previousArchive2,
1544
+ });
1545
+ const checkpoint3 = makePublishedCheckpoint(checkpoint3Cp, 12);
1546
+
1547
+ await store.addCheckpoints([checkpoint1, checkpoint2, checkpoint3]);
1548
+
1549
+ // Unwind checkpoint 3
1550
+ await store.unwindCheckpoints(CheckpointNumber(3), 1);
1551
+
1552
+ const checkpoints = await store.getRangeOfCheckpoints(CheckpointNumber(1), 10);
1553
+ expect(checkpoints.length).toBe(2);
1554
+ expect(checkpoints.map(c => c.checkpointNumber)).toEqual([1, 2]);
1555
+ });
1556
+ });
1557
+
1558
+ describe('getCheckpointedBlock', () => {
1559
+ beforeEach(async () => {
1560
+ await store.addCheckpoints(publishedCheckpoints);
1561
+ });
1562
+
1563
+ it.each(blockNumberTests)('retrieves previously stored block %i', async (blockNumber, getExpectedBlock) => {
1564
+ const retrievedBlock = await store.getCheckpointedBlock(blockNumber);
1565
+ const expectedBlock = getExpectedBlock();
1566
+ const expectedCheckpoint = publishedCheckpoints[blockNumber - 1];
1567
+
1568
+ expect(retrievedBlock).toBeDefined();
1569
+ expectCheckpointedBlockEquals(retrievedBlock!, expectedBlock, expectedCheckpoint);
1570
+ });
1571
+
1572
+ it('returns undefined if block is not found', async () => {
1573
+ await expect(store.getCheckpointedBlock(12)).resolves.toBeUndefined();
1574
+ });
1575
+
1576
+ it('returns undefined for block number 0', async () => {
1577
+ await expect(store.getCheckpointedBlock(0)).resolves.toBeUndefined();
1578
+ });
1579
+ });
1580
+
1581
+ describe('getCheckpointedBlockByHash', () => {
1582
+ beforeEach(async () => {
1583
+ await store.addCheckpoints(publishedCheckpoints);
1584
+ });
1585
+
1586
+ it('retrieves a block by its hash', async () => {
1587
+ const expectedCheckpoint = publishedCheckpoints[5];
1588
+ const expectedBlock = expectedCheckpoint.checkpoint.blocks[0];
1589
+ const blockHash = await expectedBlock.header.hash();
1590
+ const retrievedBlock = await store.getCheckpointedBlockByHash(blockHash);
1591
+
1592
+ expect(retrievedBlock).toBeDefined();
1593
+ expectCheckpointedBlockEquals(retrievedBlock!, expectedBlock, expectedCheckpoint);
1594
+ });
1595
+
1596
+ it('returns undefined for non-existent block hash', async () => {
1597
+ const nonExistentHash = Fr.random();
1598
+ await expect(store.getCheckpointedBlockByHash(nonExistentHash)).resolves.toBeUndefined();
1599
+ });
1600
+ });
1601
+
1602
+ describe('getCheckpointedBlockByArchive', () => {
1603
+ beforeEach(async () => {
1604
+ await store.addCheckpoints(publishedCheckpoints);
87
1605
  });
88
1606
 
89
- it('can unwind multiple empty blocks', async () => {
90
- const emptyBlocks = await timesParallel(10, async i => makeL1Published(await L2Block.random(i + 1, 0), i + 10));
91
- await store.addBlocks(emptyBlocks);
92
- expect(await store.getSynchedL2BlockNumber()).toBe(10);
1607
+ it('retrieves a block by its archive root', async () => {
1608
+ const expectedCheckpoint = publishedCheckpoints[3];
1609
+ const expectedBlock = expectedCheckpoint.checkpoint.blocks[0];
1610
+ const archive = expectedBlock.archive.root;
1611
+ const retrievedBlock = await store.getCheckpointedBlockByArchive(archive);
93
1612
 
94
- await store.unwindBlocks(10, 3);
95
- expect(await store.getSynchedL2BlockNumber()).toBe(7);
96
- expect((await store.getBlocks(1, 10)).map(b => b.data.number)).toEqual([1, 2, 3, 4, 5, 6, 7]);
1613
+ expect(retrievedBlock).toBeDefined();
1614
+ expectCheckpointedBlockEquals(retrievedBlock!, expectedBlock, expectedCheckpoint);
97
1615
  });
98
1616
 
99
- it('refuses to unwind blocks if the tip is not the last block', async () => {
100
- await store.addBlocks(blocks);
101
- await expect(store.unwindBlocks(5, 1)).rejects.toThrow(/can only unwind blocks from the tip/i);
1617
+ it('returns undefined for non-existent archive root', async () => {
1618
+ const nonExistentArchive = Fr.random();
1619
+ await expect(store.getCheckpointedBlockByArchive(nonExistentArchive)).resolves.toBeUndefined();
102
1620
  });
103
1621
  });
104
1622
 
105
- describe('getBlocks', () => {
1623
+ describe('getBlockHeaderByHash', () => {
106
1624
  beforeEach(async () => {
107
- await store.addBlocks(blocks);
1625
+ await store.addCheckpoints(publishedCheckpoints);
1626
+ });
1627
+
1628
+ it('retrieves a block header by its hash', async () => {
1629
+ const expectedBlock = publishedCheckpoints[7].checkpoint.blocks[0];
1630
+ const blockHash = await expectedBlock.header.hash();
1631
+ const retrievedHeader = await store.getBlockHeaderByHash(blockHash);
1632
+
1633
+ expect(retrievedHeader).toBeDefined();
1634
+ expect(retrievedHeader!.equals(expectedBlock.header)).toBe(true);
108
1635
  });
109
1636
 
110
- it.each(blockTests)('retrieves previously stored blocks', async (start, limit, getExpectedBlocks) => {
111
- await expect(store.getBlocks(start, limit)).resolves.toEqual(getExpectedBlocks());
1637
+ it('returns undefined for non-existent block hash', async () => {
1638
+ const nonExistentHash = Fr.random();
1639
+ await expect(store.getBlockHeaderByHash(nonExistentHash)).resolves.toBeUndefined();
112
1640
  });
1641
+ });
113
1642
 
114
- it('returns an empty array if no blocks are found', async () => {
115
- await expect(store.getBlocks(12, 1)).resolves.toEqual([]);
1643
+ describe('getBlockHeaderByArchive', () => {
1644
+ beforeEach(async () => {
1645
+ await store.addCheckpoints(publishedCheckpoints);
116
1646
  });
117
1647
 
118
- it('throws an error if limit is invalid', async () => {
119
- await expect(store.getBlocks(1, 0)).rejects.toThrow('Invalid limit: 0');
1648
+ it('retrieves a block header by its archive root', async () => {
1649
+ const expectedBlock = publishedCheckpoints[2].checkpoint.blocks[0];
1650
+ const archive = expectedBlock.archive.root;
1651
+ const retrievedHeader = await store.getBlockHeaderByArchive(archive);
1652
+
1653
+ expect(retrievedHeader).toBeDefined();
1654
+ expect(retrievedHeader!.equals(expectedBlock.header)).toBe(true);
120
1655
  });
121
1656
 
122
- it('throws an error if `from` it is out of range', async () => {
123
- await expect(store.getBlocks(INITIAL_L2_BLOCK_NUM - 100, 1)).rejects.toThrow('Invalid start: -99');
1657
+ it('returns undefined for non-existent archive root', async () => {
1658
+ const nonExistentArchive = Fr.random();
1659
+ await expect(store.getBlockHeaderByArchive(nonExistentArchive)).resolves.toBeUndefined();
124
1660
  });
125
1661
  });
126
1662
 
127
- describe('getSyncedL2BlockNumber', () => {
128
- it('returns the block number before INITIAL_L2_BLOCK_NUM if no blocks have been added', async () => {
129
- await expect(store.getSynchedL2BlockNumber()).resolves.toEqual(INITIAL_L2_BLOCK_NUM - 1);
1663
+ describe('getSynchedCheckpointNumber', () => {
1664
+ it('returns the checkpoint number before INITIAL_CHECKPOINT_NUMBER if no checkpoints have been added', async () => {
1665
+ await expect(store.getSynchedCheckpointNumber()).resolves.toEqual(INITIAL_CHECKPOINT_NUMBER - 1);
130
1666
  });
131
1667
 
132
- it("returns the most recently added block's number", async () => {
133
- await store.addBlocks(blocks);
134
- await expect(store.getSynchedL2BlockNumber()).resolves.toEqual(blocks.at(-1)!.data.number);
1668
+ it('returns the most recently added checkpoint number', async () => {
1669
+ await store.addCheckpoints(publishedCheckpoints);
1670
+ await expect(store.getSynchedCheckpointNumber()).resolves.toEqual(
1671
+ publishedCheckpoints.at(-1)!.checkpoint.number,
1672
+ );
135
1673
  });
136
1674
  });
137
1675
 
@@ -144,7 +1682,7 @@ export function describeArchiverDataStore(
144
1682
  });
145
1683
 
146
1684
  it('returns the L1 block number in which the most recent L2 block was published', async () => {
147
- await store.addBlocks(blocks);
1685
+ await store.addCheckpoints(publishedCheckpoints);
148
1686
  await expect(store.getSynchPoint()).resolves.toEqual({
149
1687
  blocksSynchedTo: 19n,
150
1688
  messagesSynchedTo: undefined,
@@ -152,71 +1690,75 @@ export function describeArchiverDataStore(
152
1690
  });
153
1691
 
154
1692
  it('returns the L1 block number that most recently added messages from inbox', async () => {
155
- await store.addL1ToL2Messages({
156
- lastProcessedL1BlockNumber: 1n,
157
- retrievedData: [new InboxLeaf(1n, Fr.ZERO)],
158
- });
1693
+ const l1BlockHash = Buffer32.random();
1694
+ const l1BlockNumber = 10n;
1695
+ await store.setMessageSynchedL1Block({ l1BlockNumber: 5n, l1BlockHash: Buffer32.random() });
1696
+ await store.addL1ToL2Messages([makeInboxMessage(Buffer16.ZERO, { l1BlockNumber, l1BlockHash })]);
1697
+ await expect(store.getSynchPoint()).resolves.toEqual({
1698
+ blocksSynchedTo: undefined,
1699
+ messagesSynchedTo: { l1BlockHash, l1BlockNumber },
1700
+ } satisfies ArchiverL1SynchPoint);
1701
+ });
1702
+
1703
+ it('returns the latest syncpoint if latest message is behind', async () => {
1704
+ const l1BlockHash = Buffer32.random();
1705
+ const l1BlockNumber = 10n;
1706
+ await store.setMessageSynchedL1Block({ l1BlockNumber, l1BlockHash });
1707
+ const msg = makeInboxMessage(Buffer16.ZERO, { l1BlockNumber: 5n, l1BlockHash: Buffer32.random() });
1708
+ await store.addL1ToL2Messages([msg]);
159
1709
  await expect(store.getSynchPoint()).resolves.toEqual({
160
1710
  blocksSynchedTo: undefined,
161
- messagesSynchedTo: 1n,
1711
+ messagesSynchedTo: { l1BlockHash, l1BlockNumber },
162
1712
  } satisfies ArchiverL1SynchPoint);
163
1713
  });
164
1714
  });
165
1715
 
166
1716
  describe('addLogs', () => {
167
1717
  it('adds private & public logs', async () => {
168
- const block = blocks[0].data;
169
- await expect(store.addLogs([block])).resolves.toEqual(true);
1718
+ const checkpoint = publishedCheckpoints[0];
1719
+ await store.addCheckpoints([checkpoint]);
1720
+ await expect(store.addLogs(checkpoint.checkpoint.blocks)).resolves.toEqual(true);
170
1721
  });
171
1722
  });
172
1723
 
173
- describe('deleteLogs', () => {
174
- it('deletes private & public logs', async () => {
175
- const block = blocks[0].data;
176
- await store.addBlocks([blocks[0]]);
177
- await expect(store.addLogs([block])).resolves.toEqual(true);
178
-
179
- expect((await store.getPrivateLogs(1, 1)).length).toEqual(
180
- block.body.txEffects.map(txEffect => txEffect.privateLogs).flat().length,
181
- );
182
- expect((await store.getPublicLogs({ fromBlock: 1 })).logs.length).toEqual(
183
- block.body.txEffects.map(txEffect => txEffect.publicLogs).flat().length,
184
- );
185
-
186
- // This one is a pain for memory as we would never want to just delete memory in the middle.
187
- await store.deleteLogs([block]);
1724
+ it('deleteLogs', async () => {
1725
+ const block = publishedCheckpoints[0].checkpoint.blocks[0];
1726
+ await store.addBlocks([block]);
1727
+ await expect(store.addLogs([block])).resolves.toEqual(true);
188
1728
 
189
- expect((await store.getPrivateLogs(1, 1)).length).toEqual(0);
190
- expect((await store.getPublicLogs({ fromBlock: 1 })).logs.length).toEqual(0);
191
- });
192
- });
1729
+ expect((await store.getPublicLogs({ fromBlock: BlockNumber(1) })).logs.length).toEqual(
1730
+ block.body.txEffects.map(txEffect => txEffect.publicLogs).flat().length,
1731
+ );
193
1732
 
194
- describe('getPrivateLogs', () => {
195
- it('gets added private logs', async () => {
196
- const block = blocks[0].data;
197
- await store.addBlocks([blocks[0]]);
198
- await store.addLogs([block]);
1733
+ // This one is a pain for memory as we would never want to just delete memory in the middle.
1734
+ await store.deleteLogs([block]);
199
1735
 
200
- const privateLogs = await store.getPrivateLogs(1, 1);
201
- expect(privateLogs).toEqual(block.body.txEffects.map(txEffect => txEffect.privateLogs).flat());
202
- });
1736
+ expect((await store.getPublicLogs({ fromBlock: BlockNumber(1) })).logs.length).toEqual(0);
203
1737
  });
204
1738
 
205
1739
  describe('getTxEffect', () => {
1740
+ const getBlock = (i: number) => publishedCheckpoints[i].checkpoint.blocks[0];
1741
+
206
1742
  beforeEach(async () => {
207
- await store.addLogs(blocks.map(b => b.data));
208
- await store.addBlocks(blocks);
1743
+ await store.addLogs(publishedCheckpoints.flatMap(x => x.checkpoint.blocks));
1744
+ await store.addCheckpoints(publishedCheckpoints);
209
1745
  });
210
1746
 
211
1747
  it.each([
212
- () => wrapInBlock(blocks[0].data.body.txEffects[0], blocks[0].data),
213
- () => wrapInBlock(blocks[9].data.body.txEffects[3], blocks[9].data),
214
- () => wrapInBlock(blocks[3].data.body.txEffects[1], blocks[3].data),
215
- () => wrapInBlock(blocks[5].data.body.txEffects[2], blocks[5].data),
216
- () => wrapInBlock(blocks[1].data.body.txEffects[0], blocks[1].data),
1748
+ () => ({ data: getBlock(0).body.txEffects[0], block: getBlock(0), txIndexInBlock: 0 }),
1749
+ () => ({ data: getBlock(9).body.txEffects[3], block: getBlock(9), txIndexInBlock: 3 }),
1750
+ () => ({ data: getBlock(3).body.txEffects[1], block: getBlock(3), txIndexInBlock: 1 }),
1751
+ () => ({ data: getBlock(5).body.txEffects[2], block: getBlock(5), txIndexInBlock: 2 }),
1752
+ () => ({ data: getBlock(1).body.txEffects[0], block: getBlock(1), txIndexInBlock: 0 }),
217
1753
  ])('retrieves a previously stored transaction', async getExpectedTx => {
218
- const expectedTx = await getExpectedTx();
219
- const actualTx = await store.getTxEffect(expectedTx.data.txHash);
1754
+ const { data, block, txIndexInBlock } = getExpectedTx();
1755
+ const expectedTx: IndexedTxEffect = {
1756
+ data,
1757
+ l2BlockNumber: block.number,
1758
+ l2BlockHash: L2BlockHash.fromField(await block.header.hash()),
1759
+ txIndexInBlock,
1760
+ };
1761
+ const actualTx = await store.getTxEffect(data.txHash);
220
1762
  expect(actualTx).toEqual(expectedTx);
221
1763
  });
222
1764
 
@@ -225,61 +1767,250 @@ export function describeArchiverDataStore(
225
1767
  });
226
1768
 
227
1769
  it.each([
228
- () => wrapInBlock(blocks[0].data.body.txEffects[0], blocks[0].data),
229
- () => wrapInBlock(blocks[9].data.body.txEffects[3], blocks[9].data),
230
- () => wrapInBlock(blocks[3].data.body.txEffects[1], blocks[3].data),
231
- () => wrapInBlock(blocks[5].data.body.txEffects[2], blocks[5].data),
232
- () => wrapInBlock(blocks[1].data.body.txEffects[0], blocks[1].data),
233
- ])('tries to retrieves a previously stored transaction after deleted', async getExpectedTx => {
234
- await store.unwindBlocks(blocks.length, blocks.length);
235
-
236
- const expectedTx = await getExpectedTx();
237
- const actualTx = await store.getTxEffect(expectedTx.data.txHash);
1770
+ () => getBlock(0).body.txEffects[0],
1771
+ () => getBlock(9).body.txEffects[3],
1772
+ () => getBlock(3).body.txEffects[1],
1773
+ () => getBlock(5).body.txEffects[2],
1774
+ () => getBlock(1).body.txEffects[0],
1775
+ ])('tries to retrieves a previously stored transaction after deleted', async getTxEffect => {
1776
+ await store.unwindCheckpoints(CheckpointNumber(publishedCheckpoints.length), publishedCheckpoints.length);
1777
+
1778
+ const txEffect = getTxEffect();
1779
+ const actualTx = await store.getTxEffect(txEffect.txHash);
238
1780
  expect(actualTx).toEqual(undefined);
239
1781
  });
240
1782
 
241
1783
  it('returns undefined if tx is not found', async () => {
242
1784
  await expect(store.getTxEffect(TxHash.random())).resolves.toBeUndefined();
243
1785
  });
1786
+
1787
+ it('does not fail if the block is unwound while requesting a tx', async () => {
1788
+ const txEffect = getBlock(1).body.txEffects[0];
1789
+ let done = false;
1790
+ void (async () => {
1791
+ while (!done) {
1792
+ void store.getTxEffect(txEffect.txHash);
1793
+ await sleep(1);
1794
+ }
1795
+ })();
1796
+ await store.unwindCheckpoints(CheckpointNumber(publishedCheckpoints.length), publishedCheckpoints.length);
1797
+ done = true;
1798
+ expect(await store.getTxEffect(txEffect.txHash)).toEqual(undefined);
1799
+ });
244
1800
  });
245
1801
 
246
1802
  describe('L1 to L2 Messages', () => {
247
- const l2BlockNumber = 13n;
248
- const l1ToL2MessageSubtreeSize = 2 ** L1_TO_L2_MSG_SUBTREE_HEIGHT;
1803
+ const initialCheckpointNumber = CheckpointNumber(13);
1804
+
1805
+ const checkMessages = async (msgs: InboxMessage[]) => {
1806
+ expect(await store.getLastL1ToL2Message()).toEqual(msgs.at(-1));
1807
+ expect(await toArray(store.iterateL1ToL2Messages())).toEqual(msgs);
1808
+ expect(await store.getTotalL1ToL2MessageCount()).toEqual(BigInt(msgs.length));
1809
+ };
1810
+
1811
+ const makeInboxMessagesWithFullBlocks = (
1812
+ blockCount: number,
1813
+ opts: { initialCheckpointNumber?: CheckpointNumber } = {},
1814
+ ) =>
1815
+ makeInboxMessages(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP * blockCount, {
1816
+ overrideFn: (msg, i) => {
1817
+ const checkpointNumber = CheckpointNumber(
1818
+ (opts.initialCheckpointNumber ?? initialCheckpointNumber) +
1819
+ Math.floor(i / NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP),
1820
+ );
1821
+ const index =
1822
+ InboxLeaf.smallestIndexForCheckpoint(checkpointNumber) + BigInt(i % NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP);
1823
+ return { ...msg, checkpointNumber, index };
1824
+ },
1825
+ });
1826
+
1827
+ it('stores first message ever', async () => {
1828
+ const msg = makeInboxMessage(Buffer16.ZERO, { index: 0n, checkpointNumber: CheckpointNumber(1) });
1829
+ await store.addL1ToL2Messages([msg]);
1830
+
1831
+ await checkMessages([msg]);
1832
+ expect(await store.getL1ToL2Messages(CheckpointNumber(1))).toEqual([msg.leaf]);
1833
+ });
1834
+
1835
+ it('stores single message', async () => {
1836
+ const msg = makeInboxMessage(Buffer16.ZERO, { checkpointNumber: CheckpointNumber(2) });
1837
+ await store.addL1ToL2Messages([msg]);
1838
+
1839
+ await checkMessages([msg]);
1840
+ expect(await store.getL1ToL2Messages(CheckpointNumber(2))).toEqual([msg.leaf]);
1841
+ });
1842
+
1843
+ it('stores and returns messages across different blocks', async () => {
1844
+ const msgs = makeInboxMessages(5, { initialCheckpointNumber });
1845
+ await store.addL1ToL2Messages(msgs);
1846
+
1847
+ await checkMessages(msgs);
1848
+ expect(await store.getL1ToL2Messages(CheckpointNumber(initialCheckpointNumber + 2))).toEqual(
1849
+ [msgs[2]].map(m => m.leaf),
1850
+ );
1851
+ });
1852
+
1853
+ it('stores the same messages again', async () => {
1854
+ const msgs = makeInboxMessages(5, { initialCheckpointNumber });
1855
+ await store.addL1ToL2Messages(msgs);
1856
+ await store.addL1ToL2Messages(msgs.slice(2));
1857
+
1858
+ await checkMessages(msgs);
1859
+ });
1860
+
1861
+ it('stores and returns messages across different blocks with gaps', async () => {
1862
+ const msgs1 = makeInboxMessages(3, { initialCheckpointNumber: CheckpointNumber(1) });
1863
+ const msgs2 = makeInboxMessages(3, {
1864
+ initialCheckpointNumber: CheckpointNumber(20),
1865
+ initialHash: msgs1.at(-1)!.rollingHash,
1866
+ });
1867
+
1868
+ await store.addL1ToL2Messages(msgs1);
1869
+ await store.addL1ToL2Messages(msgs2);
1870
+
1871
+ await checkMessages([...msgs1, ...msgs2]);
1872
+
1873
+ expect(await store.getL1ToL2Messages(CheckpointNumber(1))).toEqual([msgs1[0].leaf]);
1874
+ expect(await store.getL1ToL2Messages(CheckpointNumber(4))).toEqual([]);
1875
+ expect(await store.getL1ToL2Messages(CheckpointNumber(20))).toEqual([msgs2[0].leaf]);
1876
+ expect(await store.getL1ToL2Messages(CheckpointNumber(24))).toEqual([]);
1877
+ });
1878
+
1879
+ it('stores and returns messages with block numbers larger than a byte', async () => {
1880
+ const msgs = makeInboxMessages(5, { initialCheckpointNumber: CheckpointNumber(1000) });
1881
+ await store.addL1ToL2Messages(msgs);
1882
+
1883
+ await checkMessages(msgs);
1884
+ expect(await store.getL1ToL2Messages(CheckpointNumber(1002))).toEqual([msgs[2]].map(m => m.leaf));
1885
+ });
1886
+
1887
+ it('stores and returns multiple messages per block', async () => {
1888
+ const msgs = makeInboxMessagesWithFullBlocks(4);
1889
+ await store.addL1ToL2Messages(msgs);
1890
+
1891
+ await checkMessages(msgs);
1892
+ const blockMessages = await store.getL1ToL2Messages(CheckpointNumber(initialCheckpointNumber + 1));
1893
+ expect(blockMessages).toHaveLength(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP);
1894
+ expect(blockMessages).toEqual(
1895
+ msgs.slice(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP * 2).map(m => m.leaf),
1896
+ );
1897
+ });
249
1898
 
250
- const generateBlockMessages = (blockNumber: bigint, numMessages: number) =>
251
- Array.from(
252
- { length: numMessages },
253
- (_, i) => new InboxLeaf(InboxLeaf.smallestIndexFromL2Block(blockNumber) + BigInt(i), Fr.random()),
1899
+ it('stores messages in multiple operations', async () => {
1900
+ const msgs = makeInboxMessages(20, { initialCheckpointNumber });
1901
+ await store.addL1ToL2Messages(msgs.slice(0, 10));
1902
+ await store.addL1ToL2Messages(msgs.slice(10, 20));
1903
+
1904
+ expect(await store.getL1ToL2Messages(CheckpointNumber(initialCheckpointNumber + 2))).toEqual(
1905
+ [msgs[2]].map(m => m.leaf),
1906
+ );
1907
+ expect(await store.getL1ToL2Messages(CheckpointNumber(initialCheckpointNumber + 12))).toEqual(
1908
+ [msgs[12]].map(m => m.leaf),
254
1909
  );
1910
+ await checkMessages(msgs);
1911
+ });
1912
+
1913
+ it('iterates over messages from start index', async () => {
1914
+ const msgs = makeInboxMessages(10, { initialCheckpointNumber });
1915
+ await store.addL1ToL2Messages(msgs);
1916
+
1917
+ const iterated = await toArray(store.iterateL1ToL2Messages({ start: msgs[3].index }));
1918
+ expect(iterated).toEqual(msgs.slice(3));
1919
+ });
1920
+
1921
+ it('iterates over messages in reverse', async () => {
1922
+ const msgs = makeInboxMessages(10, { initialCheckpointNumber });
1923
+ await store.addL1ToL2Messages(msgs);
1924
+ initialCheckpointNumber;
1925
+
1926
+ const iterated = await toArray(store.iterateL1ToL2Messages({ reverse: true, end: msgs[3].index }));
1927
+ expect(iterated).toEqual(msgs.slice(0, 4).reverse());
1928
+ });
1929
+
1930
+ it('throws if messages are added out of order', async () => {
1931
+ const msgs = makeInboxMessages(5, { overrideFn: (msg, i) => ({ ...msg, index: BigInt(10 - i) }) });
1932
+ await expect(store.addL1ToL2Messages(msgs)).rejects.toThrow(MessageStoreError);
1933
+ });
1934
+
1935
+ it('throws if block number for the first message is out of order', async () => {
1936
+ const msgs = makeInboxMessages(4, { initialCheckpointNumber });
1937
+ msgs[2].checkpointNumber = CheckpointNumber(initialCheckpointNumber - 1);
1938
+ await store.addL1ToL2Messages(msgs.slice(0, 2));
1939
+ await expect(store.addL1ToL2Messages(msgs.slice(2, 4))).rejects.toThrow(MessageStoreError);
1940
+ });
1941
+
1942
+ it('throws if rolling hash is not correct', async () => {
1943
+ const msgs = makeInboxMessages(5);
1944
+ msgs[1].rollingHash = Buffer16.random();
1945
+ await expect(store.addL1ToL2Messages(msgs)).rejects.toThrow(MessageStoreError);
1946
+ });
1947
+
1948
+ it('throws if rolling hash for first message is not correct', async () => {
1949
+ const msgs = makeInboxMessages(4);
1950
+ msgs[2].rollingHash = Buffer16.random();
1951
+ await store.addL1ToL2Messages(msgs.slice(0, CheckpointNumber(2)));
1952
+ await expect(store.addL1ToL2Messages(msgs.slice(2, 4))).rejects.toThrow(MessageStoreError);
1953
+ });
1954
+
1955
+ it('throws if index is not in the correct range', async () => {
1956
+ const msgs = makeInboxMessages(5, { initialCheckpointNumber });
1957
+ msgs.at(-1)!.index += 100n;
1958
+ await expect(store.addL1ToL2Messages(msgs)).rejects.toThrow(MessageStoreError);
1959
+ });
1960
+
1961
+ it('throws if first index in block has gaps', async () => {
1962
+ const msgs = makeInboxMessages(4, { initialCheckpointNumber });
1963
+ msgs[2].index++;
1964
+ await expect(store.addL1ToL2Messages(msgs)).rejects.toThrow(MessageStoreError);
1965
+ });
1966
+
1967
+ it('throws if index does not follow previous one', async () => {
1968
+ const msgs = makeInboxMessages(2, {
1969
+ initialCheckpointNumber,
1970
+ overrideFn: (msg, i) => ({
1971
+ ...msg,
1972
+ checkpointNumber: CheckpointNumber(2),
1973
+ index: BigInt(i + NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP * 2),
1974
+ }),
1975
+ });
1976
+ msgs[1].index++;
1977
+ await expect(store.addL1ToL2Messages(msgs)).rejects.toThrow(MessageStoreError);
1978
+ });
255
1979
 
256
- it('returns messages in correct order', async () => {
257
- const msgs = generateBlockMessages(l2BlockNumber, l1ToL2MessageSubtreeSize);
258
- const shuffledMessages = msgs.slice().sort(() => randomInt(1) - 0.5);
259
- await store.addL1ToL2Messages({ lastProcessedL1BlockNumber: 100n, retrievedData: shuffledMessages });
260
- const retrievedMessages = await store.getL1ToL2Messages(l2BlockNumber);
1980
+ it('removes messages up to the given block number', async () => {
1981
+ const msgs = makeInboxMessagesWithFullBlocks(4, { initialCheckpointNumber: CheckpointNumber(1) });
261
1982
 
262
- const expectedLeavesOrder = msgs.map(msg => msg.leaf);
263
- expect(expectedLeavesOrder).toEqual(retrievedMessages);
1983
+ await store.addL1ToL2Messages(msgs);
1984
+ await checkMessages(msgs);
1985
+
1986
+ expect(await store.getL1ToL2Messages(CheckpointNumber(1))).toHaveLength(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP);
1987
+ expect(await store.getL1ToL2Messages(CheckpointNumber(2))).toHaveLength(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP);
1988
+ expect(await store.getL1ToL2Messages(CheckpointNumber(3))).toHaveLength(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP);
1989
+ expect(await store.getL1ToL2Messages(CheckpointNumber(4))).toHaveLength(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP);
1990
+
1991
+ await store.rollbackL1ToL2MessagesToCheckpoint(CheckpointNumber(2));
1992
+
1993
+ expect(await store.getL1ToL2Messages(CheckpointNumber(1))).toHaveLength(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP);
1994
+ expect(await store.getL1ToL2Messages(CheckpointNumber(2))).toHaveLength(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP);
1995
+ expect(await store.getL1ToL2Messages(CheckpointNumber(3))).toHaveLength(0);
1996
+ expect(await store.getL1ToL2Messages(CheckpointNumber(4))).toHaveLength(0);
1997
+
1998
+ await checkMessages(msgs.slice(0, NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP * 2));
264
1999
  });
265
2000
 
266
- it('throws if it is impossible to sequence messages correctly', async () => {
267
- const msgs = generateBlockMessages(l2BlockNumber, l1ToL2MessageSubtreeSize - 1);
268
- // We replace a message with index 4 with a message with index at the end of the tree
269
- // --> with that there will be a gap and it will be impossible to sequence the
270
- // end of tree = start of next tree/block - 1
271
- msgs[4] = new InboxLeaf(InboxLeaf.smallestIndexFromL2Block(l2BlockNumber + 1n) - 1n, Fr.random());
2001
+ it('removes messages starting with the given index', async () => {
2002
+ const msgs = makeInboxMessagesWithFullBlocks(4, { initialCheckpointNumber: CheckpointNumber(1) });
2003
+ await store.addL1ToL2Messages(msgs);
272
2004
 
273
- await store.addL1ToL2Messages({ lastProcessedL1BlockNumber: 100n, retrievedData: msgs });
274
- await expect(async () => {
275
- await store.getL1ToL2Messages(l2BlockNumber);
276
- }).rejects.toThrow(`L1 to L2 message gap found in block ${l2BlockNumber}`);
2005
+ await store.removeL1ToL2Messages(msgs[13].index);
2006
+ await checkMessages(msgs.slice(0, 13));
277
2007
  });
278
2008
  });
279
2009
 
280
2010
  describe('contractInstances', () => {
281
2011
  let contractInstance: ContractInstanceWithAddress;
282
2012
  const blockNum = 10;
2013
+ const timestamp = 3600n;
283
2014
 
284
2015
  beforeEach(async () => {
285
2016
  const classId = Fr.random();
@@ -288,20 +2019,115 @@ export function describeArchiverDataStore(
288
2019
  originalContractClassId: classId,
289
2020
  });
290
2021
  contractInstance = { ...randomInstance, address: await AztecAddress.random() };
291
- await store.addContractInstances([contractInstance], blockNum);
2022
+ await store.addContractInstances([contractInstance], BlockNumber(blockNum));
292
2023
  });
293
2024
 
294
2025
  it('returns previously stored contract instances', async () => {
295
- await expect(store.getContractInstance(contractInstance.address)).resolves.toMatchObject(contractInstance);
2026
+ await expect(store.getContractInstance(contractInstance.address, timestamp)).resolves.toMatchObject(
2027
+ contractInstance,
2028
+ );
296
2029
  });
297
2030
 
298
2031
  it('returns undefined if contract instance is not found', async () => {
299
- await expect(store.getContractInstance(await AztecAddress.random())).resolves.toBeUndefined();
2032
+ await expect(store.getContractInstance(await AztecAddress.random(), timestamp)).resolves.toBeUndefined();
300
2033
  });
301
2034
 
302
2035
  it('returns undefined if previously stored contract instances was deleted', async () => {
303
- await store.deleteContractInstances([contractInstance], blockNum);
304
- await expect(store.getContractInstance(contractInstance.address)).resolves.toBeUndefined();
2036
+ await store.deleteContractInstances([contractInstance], BlockNumber(blockNum));
2037
+ await expect(store.getContractInstance(contractInstance.address, timestamp)).resolves.toBeUndefined();
2038
+ });
2039
+ });
2040
+
2041
+ describe('contractInstanceUpdates', () => {
2042
+ let contractInstance: ContractInstanceWithAddress;
2043
+ let classId: Fr;
2044
+ let nextClassId: Fr;
2045
+ const timestampOfChange = 3600n;
2046
+
2047
+ beforeEach(async () => {
2048
+ classId = Fr.random();
2049
+ nextClassId = Fr.random();
2050
+ const randomInstance = await SerializableContractInstance.random({
2051
+ currentContractClassId: classId,
2052
+ originalContractClassId: classId,
2053
+ });
2054
+ contractInstance = { ...randomInstance, address: await AztecAddress.random() };
2055
+ await store.addContractInstances([contractInstance], BlockNumber(1));
2056
+ await store.addContractInstanceUpdates(
2057
+ [
2058
+ {
2059
+ prevContractClassId: classId,
2060
+ newContractClassId: nextClassId,
2061
+ timestampOfChange,
2062
+ address: contractInstance.address,
2063
+ },
2064
+ ],
2065
+ timestampOfChange - 1n,
2066
+ );
2067
+ });
2068
+
2069
+ it('gets the correct current class id for a contract not updated yet', async () => {
2070
+ const fetchedInstance = await store.getContractInstance(contractInstance.address, timestampOfChange - 1n);
2071
+ expect(fetchedInstance?.originalContractClassId).toEqual(classId);
2072
+ expect(fetchedInstance?.currentContractClassId).toEqual(classId);
2073
+ });
2074
+
2075
+ it('gets the correct current class id for a contract that has just been updated', async () => {
2076
+ const fetchedInstance = await store.getContractInstance(contractInstance.address, timestampOfChange);
2077
+ expect(fetchedInstance?.originalContractClassId).toEqual(classId);
2078
+ expect(fetchedInstance?.currentContractClassId).toEqual(nextClassId);
2079
+ });
2080
+
2081
+ it('gets the correct current class id for a contract that was updated in the past', async () => {
2082
+ const fetchedInstance = await store.getContractInstance(contractInstance.address, timestampOfChange + 1n);
2083
+ expect(fetchedInstance?.originalContractClassId).toEqual(classId);
2084
+ expect(fetchedInstance?.currentContractClassId).toEqual(nextClassId);
2085
+ });
2086
+
2087
+ it('ignores updates for the wrong contract', async () => {
2088
+ const otherClassId = Fr.random();
2089
+ const randomInstance = await SerializableContractInstance.random({
2090
+ currentContractClassId: otherClassId,
2091
+ originalContractClassId: otherClassId,
2092
+ });
2093
+ const otherContractInstance = {
2094
+ ...randomInstance,
2095
+ address: await AztecAddress.random(),
2096
+ };
2097
+ await store.addContractInstances([otherContractInstance], BlockNumber(1));
2098
+
2099
+ const fetchedInstance = await store.getContractInstance(otherContractInstance.address, timestampOfChange + 1n);
2100
+ expect(fetchedInstance?.originalContractClassId).toEqual(otherClassId);
2101
+ expect(fetchedInstance?.currentContractClassId).toEqual(otherClassId);
2102
+ });
2103
+
2104
+ it('bounds its search to the right contract if more than than one update exists', async () => {
2105
+ const otherClassId = Fr.random();
2106
+ const otherNextClassId = Fr.random();
2107
+ const randomInstance = await SerializableContractInstance.random({
2108
+ currentContractClassId: otherClassId,
2109
+ originalContractClassId: otherNextClassId,
2110
+ });
2111
+ const otherContractInstance = {
2112
+ ...randomInstance,
2113
+ address: await AztecAddress.random(),
2114
+ };
2115
+ await store.addContractInstances([otherContractInstance], BlockNumber(1));
2116
+ await store.addContractInstanceUpdates(
2117
+ [
2118
+ {
2119
+ prevContractClassId: otherClassId,
2120
+ newContractClassId: otherNextClassId,
2121
+ timestampOfChange,
2122
+ address: otherContractInstance.address,
2123
+ },
2124
+ ],
2125
+ timestampOfChange - 1n,
2126
+ );
2127
+
2128
+ const fetchedInstance = await store.getContractInstance(contractInstance.address, timestampOfChange + 1n);
2129
+ expect(fetchedInstance?.originalContractClassId).toEqual(classId);
2130
+ expect(fetchedInstance?.currentContractClassId).toEqual(nextClassId);
305
2131
  });
306
2132
  });
307
2133
 
@@ -314,7 +2140,7 @@ export function describeArchiverDataStore(
314
2140
  await store.addContractClasses(
315
2141
  [contractClass],
316
2142
  [await computePublicBytecodeCommitment(contractClass.packedBytecode)],
317
- blockNum,
2143
+ BlockNumber(blockNum),
318
2144
  );
319
2145
  });
320
2146
 
@@ -323,7 +2149,7 @@ export function describeArchiverDataStore(
323
2149
  });
324
2150
 
325
2151
  it('returns undefined if the initial deployed contract class was deleted', async () => {
326
- await store.deleteContractClasses([contractClass], blockNum);
2152
+ await store.deleteContractClasses([contractClass], BlockNumber(blockNum));
327
2153
  await expect(store.getContractClass(contractClass.id)).resolves.toBeUndefined();
328
2154
  });
329
2155
 
@@ -331,9 +2157,9 @@ export function describeArchiverDataStore(
331
2157
  await store.addContractClasses(
332
2158
  [contractClass],
333
2159
  [await computePublicBytecodeCommitment(contractClass.packedBytecode)],
334
- blockNum + 1,
2160
+ BlockNumber(blockNum + 1),
335
2161
  );
336
- await store.deleteContractClasses([contractClass], blockNum + 1);
2162
+ await store.deleteContractClasses([contractClass], BlockNumber(blockNum + 1));
337
2163
  await expect(store.getContractClass(contractClass.id)).resolves.toMatchObject(contractClass);
338
2164
  });
339
2165
 
@@ -356,132 +2182,259 @@ export function describeArchiverDataStore(
356
2182
  expect(stored?.privateFunctions).toEqual(fns);
357
2183
  });
358
2184
 
359
- it('adds new unconstrained functions', async () => {
360
- const fns = times(3, makeUnconstrainedFunctionWithMembershipProof);
2185
+ it('adds new utility functions', async () => {
2186
+ const fns = times(3, makeUtilityFunctionWithMembershipProof);
361
2187
  await store.addFunctions(contractClass.id, [], fns);
362
2188
  const stored = await store.getContractClass(contractClass.id);
363
- expect(stored?.unconstrainedFunctions).toEqual(fns);
2189
+ expect(stored?.utilityFunctions).toEqual(fns);
364
2190
  });
365
2191
 
366
- it('does not duplicate unconstrained functions', async () => {
367
- const fns = times(3, makeUnconstrainedFunctionWithMembershipProof);
2192
+ it('does not duplicate utility functions', async () => {
2193
+ const fns = times(3, makeUtilityFunctionWithMembershipProof);
368
2194
  await store.addFunctions(contractClass.id, [], fns.slice(0, 1));
369
2195
  await store.addFunctions(contractClass.id, [], fns);
370
2196
  const stored = await store.getContractClass(contractClass.id);
371
- expect(stored?.unconstrainedFunctions).toEqual(fns);
2197
+ expect(stored?.utilityFunctions).toEqual(fns);
372
2198
  });
373
2199
  });
374
2200
 
375
- describe('getLogsByTags', () => {
376
- const numBlocks = 3;
2201
+ describe('getPrivateLogsByTags', () => {
2202
+ const numBlocksForLogs = 3;
377
2203
  const numTxsPerBlock = 4;
378
2204
  const numPrivateLogsPerTx = 3;
379
- const numPublicLogsPerTx = 2;
380
-
381
- let blocks: L1Published<L2Block>[];
382
2205
 
383
- const makeTag = (blockNumber: number, txIndex: number, logIndex: number, isPublic = false) =>
384
- new Fr((blockNumber * 100 + txIndex * 10 + logIndex) * (isPublic ? 123 : 1));
2206
+ let logsCheckpoints: PublishedCheckpoint[];
385
2207
 
386
- // See parseLogFromPublic
387
- // Search the codebase for "disgusting encoding" to see other hardcoded instances of this encoding, that you might need to change if you ever find yourself here.
388
- const makeLengthsField = (publicValuesLen: number, privateValuesLen: number) => {
389
- const buf = Buffer.alloc(32);
390
- buf.writeUint16BE(publicValuesLen, 27);
391
- buf.writeUint16BE(privateValuesLen, 30);
392
- return Fr.fromBuffer(buf);
393
- };
2208
+ const makePrivateLogTag = (blockNumber: number, txIndex: number, logIndex: number): SiloedTag =>
2209
+ new SiloedTag(
2210
+ blockNumber === 1 && txIndex === 0 && logIndex === 0
2211
+ ? Fr.ZERO // Shared tag
2212
+ : new Fr(blockNumber * 100 + txIndex * 10 + logIndex),
2213
+ );
394
2214
 
395
- const makePrivateLog = (tag: Fr) =>
396
- PrivateLog.fromFields([tag, ...times(PRIVATE_LOG_SIZE_IN_FIELDS - 1, i => new Fr(tag.toNumber() + i))]);
397
-
398
- // The tag lives in field 1, not 0, of a public log
399
- // See extractTaggedLogsFromPublic and noir-projects/aztec-nr/aztec/src/macros/notes/mod.nr -> emit_log
400
- const makePublicLog = (tag: Fr) =>
401
- PublicLog.fromFields([
402
- AztecAddress.fromNumber(1).toField(), // log address
403
- makeLengthsField(2, PUBLIC_LOG_DATA_SIZE_IN_FIELDS - 3), // field 0
404
- tag, // field 1
405
- ...times(PUBLIC_LOG_DATA_SIZE_IN_FIELDS - 1, i => new Fr(tag.toNumber() + i)), // fields 2 to end
406
- ]);
2215
+ const makePrivateLog = (tag: SiloedTag) =>
2216
+ PrivateLog.from({
2217
+ fields: makeTuple(PRIVATE_LOG_SIZE_IN_FIELDS, i =>
2218
+ !i ? tag.value : new Fr(tag.value.toBigInt() + BigInt(i)),
2219
+ ),
2220
+ emittedLength: PRIVATE_LOG_SIZE_IN_FIELDS,
2221
+ });
407
2222
 
408
2223
  const mockPrivateLogs = (blockNumber: number, txIndex: number) => {
409
2224
  return times(numPrivateLogsPerTx, (logIndex: number) => {
410
- const tag = makeTag(blockNumber, txIndex, logIndex);
2225
+ const tag = makePrivateLogTag(blockNumber, txIndex, logIndex);
411
2226
  return makePrivateLog(tag);
412
2227
  });
413
2228
  };
414
2229
 
415
- const mockPublicLogs = (blockNumber: number, txIndex: number) => {
416
- return times(numPublicLogsPerTx, (logIndex: number) => {
417
- const tag = makeTag(blockNumber, txIndex, logIndex, /* isPublic */ true);
418
- return makePublicLog(tag);
2230
+ const mockCheckpointWithLogs = async (
2231
+ blockNumber: number,
2232
+ previousArchive?: AppendOnlyTreeSnapshot,
2233
+ ): Promise<PublishedCheckpoint> => {
2234
+ const block = await L2BlockNew.random(BlockNumber(blockNumber), {
2235
+ checkpointNumber: CheckpointNumber(blockNumber),
2236
+ indexWithinCheckpoint: 0,
2237
+ state: makeStateForBlock(blockNumber, numTxsPerBlock),
2238
+ ...(previousArchive ? { lastArchive: previousArchive } : {}),
419
2239
  });
420
- };
421
-
422
- const mockBlockWithLogs = async (blockNumber: number): Promise<L1Published<L2Block>> => {
423
- const block = await L2Block.random(blockNumber);
424
- block.header.globalVariables.blockNumber = new Fr(blockNumber);
2240
+ block.header.globalVariables.blockNumber = BlockNumber(blockNumber);
425
2241
 
426
2242
  block.body.txEffects = await timesParallel(numTxsPerBlock, async (txIndex: number) => {
427
2243
  const txEffect = await TxEffect.random();
428
2244
  txEffect.privateLogs = mockPrivateLogs(blockNumber, txIndex);
429
- txEffect.publicLogs = mockPublicLogs(blockNumber, txIndex);
2245
+ txEffect.publicLogs = []; // No public logs needed for private log tests
430
2246
  return txEffect;
431
2247
  });
432
2248
 
433
- return {
434
- data: block,
435
- l1: { blockNumber: BigInt(blockNumber), blockHash: `0x${blockNumber}`, timestamp: BigInt(blockNumber) },
436
- };
2249
+ const checkpoint = new Checkpoint(
2250
+ AppendOnlyTreeSnapshot.random(),
2251
+ CheckpointHeader.random(),
2252
+ [block],
2253
+ CheckpointNumber(blockNumber),
2254
+ );
2255
+ return makePublishedCheckpoint(checkpoint, blockNumber);
437
2256
  };
438
2257
 
439
2258
  beforeEach(async () => {
440
- blocks = await timesParallel(numBlocks, (index: number) => mockBlockWithLogs(index));
2259
+ // Create checkpoints sequentially to chain archive roots
2260
+ logsCheckpoints = [];
2261
+ for (let i = 0; i < numBlocksForLogs; i++) {
2262
+ const previousArchive = i > 0 ? logsCheckpoints[i - 1].checkpoint.blocks[0].archive : undefined;
2263
+ logsCheckpoints.push(await mockCheckpointWithLogs(i + 1, previousArchive));
2264
+ }
441
2265
 
442
- await store.addBlocks(blocks);
443
- await store.addLogs(blocks.map(b => b.data));
2266
+ await store.addCheckpoints(logsCheckpoints);
2267
+ await store.addLogs(logsCheckpoints.flatMap(p => p.checkpoint.blocks));
444
2268
  });
445
2269
 
446
2270
  it('is possible to batch request private logs via tags', async () => {
447
- const tags = [makeTag(1, 1, 2), makeTag(0, 2, 0)];
2271
+ const tags = [makePrivateLogTag(2, 1, 2), makePrivateLogTag(1, 2, 0)];
448
2272
 
449
- const logsByTags = await store.getLogsByTags(tags);
2273
+ const logsByTags = await store.getPrivateLogsByTags(tags);
450
2274
 
451
2275
  expect(logsByTags).toEqual([
2276
+ [
2277
+ expect.objectContaining({
2278
+ blockNumber: 2,
2279
+ blockHash: L2BlockHash.fromField(await logsCheckpoints[2 - 1].checkpoint.blocks[0].header.hash()),
2280
+ log: makePrivateLog(tags[0]),
2281
+ isFromPublic: false,
2282
+ }),
2283
+ ],
452
2284
  [
453
2285
  expect.objectContaining({
454
2286
  blockNumber: 1,
455
- logData: makePrivateLog(tags[0]).toBuffer(),
2287
+ blockHash: L2BlockHash.fromField(await logsCheckpoints[1 - 1].checkpoint.blocks[0].header.hash()),
2288
+ log: makePrivateLog(tags[1]),
456
2289
  isFromPublic: false,
457
2290
  }),
458
2291
  ],
2292
+ ]);
2293
+ });
2294
+
2295
+ it('is possible to batch request logs that have the same tag but different content', async () => {
2296
+ const tags = [makePrivateLogTag(1, 2, 1)];
2297
+
2298
+ // Create a checkpoint containing logs that have the same tag as the checkpoints before.
2299
+ // Chain from the last checkpoint's archive
2300
+ const newBlockNumber = numBlocksForLogs + 1;
2301
+ const previousArchive = logsCheckpoints[logsCheckpoints.length - 1].checkpoint.blocks[0].archive;
2302
+ const newCheckpoint = await mockCheckpointWithLogs(newBlockNumber, previousArchive);
2303
+ const newLog = newCheckpoint.checkpoint.blocks[0].body.txEffects[1].privateLogs[1];
2304
+ newLog.fields[0] = tags[0].value;
2305
+ newCheckpoint.checkpoint.blocks[0].body.txEffects[1].privateLogs[1] = newLog;
2306
+ await store.addCheckpoints([newCheckpoint]);
2307
+ await store.addLogs([newCheckpoint.checkpoint.blocks[0]]);
2308
+
2309
+ const logsByTags = await store.getPrivateLogsByTags(tags);
2310
+
2311
+ expect(logsByTags).toEqual([
459
2312
  [
460
2313
  expect.objectContaining({
461
- blockNumber: 0,
462
- logData: makePrivateLog(tags[1]).toBuffer(),
2314
+ blockNumber: 1,
2315
+ blockHash: L2BlockHash.fromField(await logsCheckpoints[1 - 1].checkpoint.blocks[0].header.hash()),
2316
+ log: makePrivateLog(tags[0]),
2317
+ isFromPublic: false,
2318
+ }),
2319
+ expect.objectContaining({
2320
+ blockNumber: newBlockNumber,
2321
+ blockHash: L2BlockHash.fromField(await newCheckpoint.checkpoint.blocks[0].header.hash()),
2322
+ log: newLog,
463
2323
  isFromPublic: false,
464
2324
  }),
465
2325
  ],
466
2326
  ]);
467
2327
  });
468
2328
 
469
- it('is possible to batch request all logs (private and public) via tags', async () => {
470
- // Tag(0, 0, 0) is shared with the first private log and the first public log.
471
- const tags = [makeTag(0, 0, 0)];
2329
+ it('is possible to request logs for non-existing tags and determine their position', async () => {
2330
+ const tags = [makePrivateLogTag(99, 88, 77), makePrivateLogTag(1, 1, 1)];
472
2331
 
473
- const logsByTags = await store.getLogsByTags(tags);
2332
+ const logsByTags = await store.getPrivateLogsByTags(tags);
474
2333
 
475
2334
  expect(logsByTags).toEqual([
2335
+ [
2336
+ // No logs for the first tag.
2337
+ ],
476
2338
  [
477
2339
  expect.objectContaining({
478
- blockNumber: 0,
479
- logData: makePrivateLog(tags[0]).toBuffer(),
2340
+ blockNumber: 1,
2341
+ blockHash: L2BlockHash.fromField(await logsCheckpoints[1 - 1].checkpoint.blocks[0].header.hash()),
2342
+ log: makePrivateLog(tags[1]),
480
2343
  isFromPublic: false,
481
2344
  }),
2345
+ ],
2346
+ ]);
2347
+ });
2348
+ });
2349
+
2350
+ describe('getPublicLogsByTagsFromContract', () => {
2351
+ const numBlocksForLogs = 3;
2352
+ const numTxsPerBlock = 4;
2353
+ const numPublicLogsPerTx = 2;
2354
+ const contractAddress = AztecAddress.fromNumber(543254);
2355
+
2356
+ let logsCheckpoints: PublishedCheckpoint[];
2357
+
2358
+ const makePublicLogTag = (blockNumber: number, txIndex: number, logIndex: number): Tag =>
2359
+ new Tag(
2360
+ blockNumber === 1 && txIndex === 0 && logIndex === 0
2361
+ ? Fr.ZERO // Shared tag
2362
+ : new Fr((blockNumber * 100 + txIndex * 10 + logIndex) * 123),
2363
+ );
2364
+
2365
+ const makePublicLog = (tag: Tag) =>
2366
+ PublicLog.from({
2367
+ contractAddress: contractAddress,
2368
+ // Arbitrary length
2369
+ fields: new Array(10).fill(null).map((_, i) => (!i ? tag.value : new Fr(tag.value.toBigInt() + BigInt(i)))),
2370
+ });
2371
+
2372
+ const mockPublicLogs = (blockNumber: number, txIndex: number) => {
2373
+ return times(numPublicLogsPerTx, (logIndex: number) => {
2374
+ const tag = makePublicLogTag(blockNumber, txIndex, logIndex);
2375
+ return makePublicLog(tag);
2376
+ });
2377
+ };
2378
+
2379
+ const mockCheckpointWithLogs = async (
2380
+ blockNumber: number,
2381
+ previousArchive?: AppendOnlyTreeSnapshot,
2382
+ ): Promise<PublishedCheckpoint> => {
2383
+ const block = await L2BlockNew.random(BlockNumber(blockNumber), {
2384
+ checkpointNumber: CheckpointNumber(blockNumber),
2385
+ indexWithinCheckpoint: 0,
2386
+ state: makeStateForBlock(blockNumber, numTxsPerBlock),
2387
+ ...(previousArchive ? { lastArchive: previousArchive } : {}),
2388
+ });
2389
+ block.header.globalVariables.blockNumber = BlockNumber(blockNumber);
2390
+
2391
+ block.body.txEffects = await timesParallel(numTxsPerBlock, async (txIndex: number) => {
2392
+ const txEffect = await TxEffect.random();
2393
+ txEffect.privateLogs = []; // No private logs needed for public log tests
2394
+ txEffect.publicLogs = mockPublicLogs(blockNumber, txIndex);
2395
+ return txEffect;
2396
+ });
2397
+
2398
+ const checkpoint = new Checkpoint(
2399
+ AppendOnlyTreeSnapshot.random(),
2400
+ CheckpointHeader.random(),
2401
+ [block],
2402
+ CheckpointNumber(blockNumber),
2403
+ );
2404
+ return makePublishedCheckpoint(checkpoint, blockNumber);
2405
+ };
2406
+
2407
+ beforeEach(async () => {
2408
+ // Create checkpoints sequentially to chain archive roots
2409
+ logsCheckpoints = [];
2410
+ for (let i = 0; i < numBlocksForLogs; i++) {
2411
+ const previousArchive = i > 0 ? logsCheckpoints[i - 1].checkpoint.blocks[0].archive : undefined;
2412
+ logsCheckpoints.push(await mockCheckpointWithLogs(i + 1, previousArchive));
2413
+ }
2414
+
2415
+ await store.addCheckpoints(logsCheckpoints);
2416
+ await store.addLogs(logsCheckpoints.flatMap(p => p.checkpoint.blocks));
2417
+ });
2418
+
2419
+ it('is possible to batch request public logs via tags', async () => {
2420
+ const tags = [makePublicLogTag(2, 1, 1), makePublicLogTag(1, 2, 0)];
2421
+
2422
+ const logsByTags = await store.getPublicLogsByTagsFromContract(contractAddress, tags);
2423
+
2424
+ expect(logsByTags).toEqual([
2425
+ [
2426
+ expect.objectContaining({
2427
+ blockNumber: 2,
2428
+ blockHash: L2BlockHash.fromField(await logsCheckpoints[2 - 1].checkpoint.blocks[0].header.hash()),
2429
+ log: makePublicLog(tags[0]),
2430
+ isFromPublic: true,
2431
+ }),
2432
+ ],
2433
+ [
482
2434
  expect.objectContaining({
483
- blockNumber: 0,
484
- logData: makePublicLog(tags[0]).toBuffer(),
2435
+ blockNumber: 1,
2436
+ blockHash: L2BlockHash.fromField(await logsCheckpoints[1 - 1].checkpoint.blocks[0].header.hash()),
2437
+ log: makePublicLog(tags[1]),
485
2438
  isFromPublic: true,
486
2439
  }),
487
2440
  ],
@@ -489,39 +2442,43 @@ export function describeArchiverDataStore(
489
2442
  });
490
2443
 
491
2444
  it('is possible to batch request logs that have the same tag but different content', async () => {
492
- const tags = [makeTag(1, 2, 1)];
2445
+ const tags = [makePublicLogTag(1, 2, 1)];
493
2446
 
494
- // Create a block containing logs that have the same tag as the blocks before.
495
- const newBlockNumber = numBlocks;
496
- const newBlock = await mockBlockWithLogs(newBlockNumber);
497
- const newLog = newBlock.data.body.txEffects[1].privateLogs[1];
498
- newLog.fields[0] = tags[0];
499
- newBlock.data.body.txEffects[1].privateLogs[1] = newLog;
500
- await store.addBlocks([newBlock]);
501
- await store.addLogs([newBlock.data]);
2447
+ // Create a checkpoint containing logs that have the same tag as the checkpoints before.
2448
+ // Chain from the last checkpoint's archive
2449
+ const newBlockNumber = numBlocksForLogs + 1;
2450
+ const previousArchive = logsCheckpoints[logsCheckpoints.length - 1].checkpoint.blocks[0].archive;
2451
+ const newCheckpoint = await mockCheckpointWithLogs(newBlockNumber, previousArchive);
2452
+ const newLog = newCheckpoint.checkpoint.blocks[0].body.txEffects[1].publicLogs[1];
2453
+ newLog.fields[0] = tags[0].value;
2454
+ newCheckpoint.checkpoint.blocks[0].body.txEffects[1].publicLogs[1] = newLog;
2455
+ await store.addCheckpoints([newCheckpoint]);
2456
+ await store.addLogs([newCheckpoint.checkpoint.blocks[0]]);
502
2457
 
503
- const logsByTags = await store.getLogsByTags(tags);
2458
+ const logsByTags = await store.getPublicLogsByTagsFromContract(contractAddress, tags);
504
2459
 
505
2460
  expect(logsByTags).toEqual([
506
2461
  [
507
2462
  expect.objectContaining({
508
2463
  blockNumber: 1,
509
- logData: makePrivateLog(tags[0]).toBuffer(),
510
- isFromPublic: false,
2464
+ blockHash: L2BlockHash.fromField(await logsCheckpoints[1 - 1].checkpoint.blocks[0].header.hash()),
2465
+ log: makePublicLog(tags[0]),
2466
+ isFromPublic: true,
511
2467
  }),
512
2468
  expect.objectContaining({
513
2469
  blockNumber: newBlockNumber,
514
- logData: newLog.toBuffer(),
515
- isFromPublic: false,
2470
+ blockHash: L2BlockHash.fromField(await newCheckpoint.checkpoint.blocks[0].header.hash()),
2471
+ log: newLog,
2472
+ isFromPublic: true,
516
2473
  }),
517
2474
  ],
518
2475
  ]);
519
2476
  });
520
2477
 
521
2478
  it('is possible to request logs for non-existing tags and determine their position', async () => {
522
- const tags = [makeTag(99, 88, 77), makeTag(1, 1, 1)];
2479
+ const tags = [makePublicLogTag(99, 88, 77), makePublicLogTag(1, 1, 0)];
523
2480
 
524
- const logsByTags = await store.getLogsByTags(tags);
2481
+ const logsByTags = await store.getPublicLogsByTagsFromContract(contractAddress, tags);
525
2482
 
526
2483
  expect(logsByTags).toEqual([
527
2484
  [
@@ -530,70 +2487,43 @@ export function describeArchiverDataStore(
530
2487
  [
531
2488
  expect.objectContaining({
532
2489
  blockNumber: 1,
533
- logData: makePrivateLog(tags[1]).toBuffer(),
534
- isFromPublic: false,
2490
+ blockHash: L2BlockHash.fromField(await logsCheckpoints[1 - 1].checkpoint.blocks[0].header.hash()),
2491
+ log: makePublicLog(tags[1]),
2492
+ isFromPublic: true,
535
2493
  }),
536
2494
  ],
537
2495
  ]);
538
2496
  });
539
-
540
- it('is not possible to add public logs by tag if they are invalid', async () => {
541
- const tag = makeTag(99, 88, 77);
542
- const invalidLogs = [
543
- PublicLog.fromFields([
544
- AztecAddress.fromNumber(1).toField(),
545
- makeLengthsField(2, 3), // This field claims we have 5 items, but we actually have more
546
- tag,
547
- ...times(PUBLIC_LOG_DATA_SIZE_IN_FIELDS - 1, i => new Fr(tag.toNumber() + i)),
548
- ]),
549
- PublicLog.fromFields([
550
- AztecAddress.fromNumber(1).toField(),
551
- makeLengthsField(2, PUBLIC_LOG_DATA_SIZE_IN_FIELDS), // This field claims we have more than the max items
552
- tag,
553
- ...times(PUBLIC_LOG_DATA_SIZE_IN_FIELDS - 1, i => new Fr(tag.toNumber() + i)),
554
- ]),
555
- ];
556
-
557
- // Create a block containing these invalid logs
558
- const newBlockNumber = numBlocks;
559
- const newBlock = await mockBlockWithLogs(newBlockNumber);
560
- newBlock.data.body.txEffects[0].publicLogs = invalidLogs;
561
- await store.addBlocks([newBlock]);
562
- await store.addLogs([newBlock.data]);
563
-
564
- const logsByTags = await store.getLogsByTags([tag]);
565
-
566
- // Neither of the logs should have been added:
567
- expect(logsByTags).toEqual([[]]);
568
- });
569
2497
  });
570
2498
 
571
2499
  describe('getPublicLogs', () => {
572
- const txsPerBlock = 4;
573
- const numPublicFunctionCalls = 3;
574
- const numPublicLogs = 2;
575
- const numBlocks = 10;
576
- let blocks: L1Published<L2Block>[];
2500
+ const numBlocksForPublicLogs = 10;
577
2501
 
578
- beforeEach(async () => {
579
- blocks = await timesParallel(numBlocks, async (index: number) => ({
580
- data: await L2Block.random(index + 1, txsPerBlock, numPublicFunctionCalls, numPublicLogs),
581
- l1: { blockNumber: BigInt(index), blockHash: `0x${index}`, timestamp: BigInt(index) },
582
- }));
2502
+ // Helper to get total public logs per tx from a block
2503
+ const getPublicLogsPerTx = (block: L2BlockNew, txIndex: number) =>
2504
+ block.body.txEffects[txIndex].publicLogs.length;
2505
+
2506
+ // Helper to get number of txs in a block
2507
+ const getTxsPerBlock = (block: L2BlockNew) => block.body.txEffects.length;
583
2508
 
584
- await store.addBlocks(blocks);
585
- await store.addLogs(blocks.map(b => b.data));
2509
+ beforeEach(async () => {
2510
+ // Use the outer publishedCheckpoints for log tests
2511
+ for (let i = 0; i < numBlocksForPublicLogs; i++) {
2512
+ await store.addCheckpoints([publishedCheckpoints[i]]);
2513
+ await store.addLogs(publishedCheckpoints[i].checkpoint.blocks);
2514
+ }
586
2515
  });
587
2516
 
588
2517
  it('no logs returned if deleted ("txHash" filter param is respected variant)', async () => {
589
2518
  // get random tx
590
- const targetBlockIndex = randomInt(numBlocks);
591
- const targetTxIndex = randomInt(txsPerBlock);
592
- const targetTxHash = blocks[targetBlockIndex].data.body.txEffects[targetTxIndex].txHash;
2519
+ const targetBlockIndex = randomInt(numBlocksForPublicLogs);
2520
+ const targetBlock = publishedCheckpoints[targetBlockIndex].checkpoint.blocks[0];
2521
+ const targetTxIndex = randomInt(getTxsPerBlock(targetBlock));
2522
+ const targetTxHash = targetBlock.body.txEffects[targetTxIndex].txHash;
593
2523
 
594
2524
  await Promise.all([
595
- store.unwindBlocks(blocks.length, blocks.length),
596
- store.deleteLogs(blocks.map(b => b.data)),
2525
+ store.unwindCheckpoints(CheckpointNumber(numBlocksForPublicLogs), numBlocksForPublicLogs),
2526
+ store.deleteLogs(publishedCheckpoints.slice(0, numBlocksForPublicLogs).flatMap(b => b.checkpoint.blocks)),
597
2527
  ]);
598
2528
 
599
2529
  const response = await store.getPublicLogs({ txHash: targetTxHash });
@@ -605,16 +2535,17 @@ export function describeArchiverDataStore(
605
2535
 
606
2536
  it('"txHash" filter param is respected', async () => {
607
2537
  // get random tx
608
- const targetBlockIndex = randomInt(numBlocks);
609
- const targetTxIndex = randomInt(txsPerBlock);
610
- const targetTxHash = blocks[targetBlockIndex].data.body.txEffects[targetTxIndex].txHash;
2538
+ const targetBlockIndex = randomInt(numBlocksForPublicLogs);
2539
+ const targetBlock = publishedCheckpoints[targetBlockIndex].checkpoint.blocks[0];
2540
+ const targetTxIndex = randomInt(getTxsPerBlock(targetBlock));
2541
+ const targetTxHash = targetBlock.body.txEffects[targetTxIndex].txHash;
611
2542
 
612
2543
  const response = await store.getPublicLogs({ txHash: targetTxHash });
613
2544
  const logs = response.logs;
614
2545
 
615
2546
  expect(response.maxLogsHit).toBeFalsy();
616
2547
 
617
- const expectedNumLogs = numPublicFunctionCalls * numPublicLogs;
2548
+ const expectedNumLogs = getPublicLogsPerTx(targetBlock, targetTxIndex);
618
2549
  expect(logs.length).toEqual(expectedNumLogs);
619
2550
 
620
2551
  const targeBlockNumber = targetBlockIndex + INITIAL_L2_BLOCK_NUM;
@@ -624,6 +2555,17 @@ export function describeArchiverDataStore(
624
2555
  }
625
2556
  });
626
2557
 
2558
+ it('returns block hash on public log ids', async () => {
2559
+ const targetBlock = publishedCheckpoints[0].checkpoint.blocks[0];
2560
+ const expectedBlockHash = L2BlockHash.fromField(await targetBlock.header.hash());
2561
+
2562
+ const logs = (await store.getPublicLogs({ fromBlock: targetBlock.number, toBlock: targetBlock.number + 1 }))
2563
+ .logs;
2564
+
2565
+ expect(logs.length).toBeGreaterThan(0);
2566
+ expect(logs.every(log => log.id.blockHash.equals(expectedBlockHash))).toBe(true);
2567
+ });
2568
+
627
2569
  it('"fromBlock" and "toBlock" filter params are respected', async () => {
628
2570
  // Set "fromBlock" and "toBlock"
629
2571
  const fromBlock = 3;
@@ -634,7 +2576,12 @@ export function describeArchiverDataStore(
634
2576
 
635
2577
  expect(response.maxLogsHit).toBeFalsy();
636
2578
 
637
- const expectedNumLogs = txsPerBlock * numPublicFunctionCalls * numPublicLogs * (toBlock - fromBlock);
2579
+ // Compute expected logs from the blocks in range
2580
+ let expectedNumLogs = 0;
2581
+ for (let i = fromBlock - 1; i < toBlock - 1; i++) {
2582
+ const block = publishedCheckpoints[i].checkpoint.blocks[0];
2583
+ expectedNumLogs += block.body.txEffects.reduce((sum, tx) => sum + tx.publicLogs.length, 0);
2584
+ }
638
2585
  expect(logs.length).toEqual(expectedNumLogs);
639
2586
 
640
2587
  for (const log of logs) {
@@ -646,11 +2593,12 @@ export function describeArchiverDataStore(
646
2593
 
647
2594
  it('"contractAddress" filter param is respected', async () => {
648
2595
  // Get a random contract address from the logs
649
- const targetBlockIndex = randomInt(numBlocks);
650
- const targetTxIndex = randomInt(txsPerBlock);
651
- const targetLogIndex = randomInt(numPublicLogs * numPublicFunctionCalls);
2596
+ const targetBlockIndex = randomInt(numBlocksForPublicLogs);
2597
+ const targetBlock = publishedCheckpoints[targetBlockIndex].checkpoint.blocks[0];
2598
+ const targetTxIndex = randomInt(getTxsPerBlock(targetBlock));
2599
+ const targetLogIndex = randomInt(getPublicLogsPerTx(targetBlock, targetTxIndex));
652
2600
  const targetContractAddress =
653
- blocks[targetBlockIndex].data.body.txEffects[targetTxIndex].publicLogs[targetLogIndex].contractAddress;
2601
+ targetBlock.body.txEffects[targetTxIndex].publicLogs[targetLogIndex].contractAddress;
654
2602
 
655
2603
  const response = await store.getPublicLogs({ contractAddress: targetContractAddress });
656
2604
 
@@ -663,11 +2611,19 @@ export function describeArchiverDataStore(
663
2611
 
664
2612
  it('"afterLog" filter param is respected', async () => {
665
2613
  // Get a random log as reference
666
- const targetBlockIndex = randomInt(numBlocks);
667
- const targetTxIndex = randomInt(txsPerBlock);
668
- const targetLogIndex = randomInt(numPublicLogs);
669
-
670
- const afterLog = new LogId(targetBlockIndex + INITIAL_L2_BLOCK_NUM, targetTxIndex, targetLogIndex);
2614
+ const targetBlockIndex = randomInt(numBlocksForPublicLogs);
2615
+ const targetBlock = publishedCheckpoints[targetBlockIndex].checkpoint.blocks[0];
2616
+ const targetTxIndex = randomInt(getTxsPerBlock(targetBlock));
2617
+ const numLogsInTx = targetBlock.body.txEffects[targetTxIndex].publicLogs.length;
2618
+ const targetLogIndex = numLogsInTx > 0 ? randomInt(numLogsInTx) : 0;
2619
+ const targetBlockHash = L2BlockHash.fromField(await targetBlock.header.hash());
2620
+
2621
+ const afterLog = new LogId(
2622
+ BlockNumber(targetBlockIndex + INITIAL_L2_BLOCK_NUM),
2623
+ targetBlockHash,
2624
+ targetTxIndex,
2625
+ targetLogIndex,
2626
+ );
671
2627
 
672
2628
  const response = await store.getPublicLogs({ afterLog });
673
2629
  const logs = response.logs;
@@ -689,52 +2645,77 @@ export function describeArchiverDataStore(
689
2645
  it('"txHash" filter param is ignored when "afterLog" is set', async () => {
690
2646
  // Get random txHash
691
2647
  const txHash = TxHash.random();
692
- const afterLog = new LogId(1, 0, 0);
2648
+ const afterLog = new LogId(BlockNumber(1), L2BlockHash.random(), 0, 0);
693
2649
 
694
2650
  const response = await store.getPublicLogs({ txHash, afterLog });
695
2651
  expect(response.logs.length).toBeGreaterThan(1);
696
2652
  });
697
2653
 
698
2654
  it('intersecting works', async () => {
699
- let logs = (await store.getPublicLogs({ fromBlock: -10, toBlock: -5 })).logs;
2655
+ let logs = (await store.getPublicLogs({ fromBlock: -10 as BlockNumber, toBlock: -5 as BlockNumber })).logs;
700
2656
  expect(logs.length).toBe(0);
701
2657
 
702
2658
  // "fromBlock" gets correctly trimmed to range and "toBlock" is exclusive
703
- logs = (await store.getPublicLogs({ fromBlock: -10, toBlock: 5 })).logs;
2659
+ logs = (await store.getPublicLogs({ fromBlock: -10 as BlockNumber, toBlock: BlockNumber(5) })).logs;
704
2660
  let blockNumbers = new Set(logs.map(log => log.id.blockNumber));
705
2661
  expect(blockNumbers).toEqual(new Set([1, 2, 3, 4]));
706
2662
 
707
2663
  // "toBlock" should be exclusive
708
- logs = (await store.getPublicLogs({ fromBlock: 1, toBlock: 1 })).logs;
2664
+ logs = (await store.getPublicLogs({ fromBlock: BlockNumber(1), toBlock: BlockNumber(1) })).logs;
709
2665
  expect(logs.length).toBe(0);
710
2666
 
711
- logs = (await store.getPublicLogs({ fromBlock: 10, toBlock: 5 })).logs;
2667
+ logs = (await store.getPublicLogs({ fromBlock: BlockNumber(10), toBlock: BlockNumber(5) })).logs;
712
2668
  expect(logs.length).toBe(0);
713
2669
 
714
2670
  // both "fromBlock" and "toBlock" get correctly capped to range and logs from all blocks are returned
715
- logs = (await store.getPublicLogs({ fromBlock: -100, toBlock: +100 })).logs;
2671
+ logs = (await store.getPublicLogs({ fromBlock: -100 as BlockNumber, toBlock: +100 })).logs;
716
2672
  blockNumbers = new Set(logs.map(log => log.id.blockNumber));
717
- expect(blockNumbers.size).toBe(numBlocks);
2673
+ expect(blockNumbers.size).toBe(numBlocksForPublicLogs);
718
2674
 
719
2675
  // intersecting with "afterLog" works
720
- logs = (await store.getPublicLogs({ fromBlock: 2, toBlock: 5, afterLog: new LogId(4, 0, 0) })).logs;
2676
+ logs = (
2677
+ await store.getPublicLogs({
2678
+ fromBlock: BlockNumber(2),
2679
+ toBlock: BlockNumber(5),
2680
+ afterLog: new LogId(BlockNumber(4), L2BlockHash.random(), 0, 0),
2681
+ })
2682
+ ).logs;
721
2683
  blockNumbers = new Set(logs.map(log => log.id.blockNumber));
722
2684
  expect(blockNumbers).toEqual(new Set([4]));
723
2685
 
724
- logs = (await store.getPublicLogs({ toBlock: 5, afterLog: new LogId(5, 1, 0) })).logs;
2686
+ logs = (
2687
+ await store.getPublicLogs({
2688
+ toBlock: BlockNumber(5),
2689
+ afterLog: new LogId(BlockNumber(5), L2BlockHash.random(), 1, 0),
2690
+ })
2691
+ ).logs;
725
2692
  expect(logs.length).toBe(0);
726
2693
 
727
- logs = (await store.getPublicLogs({ fromBlock: 2, toBlock: 5, afterLog: new LogId(100, 0, 0) })).logs;
2694
+ logs = (
2695
+ await store.getPublicLogs({
2696
+ fromBlock: BlockNumber(2),
2697
+ toBlock: BlockNumber(5),
2698
+ afterLog: new LogId(BlockNumber(100), L2BlockHash.random(), 0, 0),
2699
+ })
2700
+ ).logs;
728
2701
  expect(logs.length).toBe(0);
729
2702
  });
730
2703
 
731
2704
  it('"txIndex" and "logIndex" are respected when "afterLog.blockNumber" is equal to "fromBlock"', async () => {
732
2705
  // Get a random log as reference
733
- const targetBlockIndex = randomInt(numBlocks);
734
- const targetTxIndex = randomInt(txsPerBlock);
735
- const targetLogIndex = randomInt(numPublicLogs);
736
-
737
- const afterLog = new LogId(targetBlockIndex + INITIAL_L2_BLOCK_NUM, targetTxIndex, targetLogIndex);
2706
+ const targetBlockIndex = randomInt(numBlocksForPublicLogs);
2707
+ const targetBlock = publishedCheckpoints[targetBlockIndex].checkpoint.blocks[0];
2708
+ const targetTxIndex = randomInt(getTxsPerBlock(targetBlock));
2709
+ const numLogsInTx = targetBlock.body.txEffects[targetTxIndex].publicLogs.length;
2710
+ const targetLogIndex = numLogsInTx > 0 ? randomInt(numLogsInTx) : 0;
2711
+ const targetBlockHash = L2BlockHash.fromField(await targetBlock.header.hash());
2712
+
2713
+ const afterLog = new LogId(
2714
+ BlockNumber(targetBlockIndex + INITIAL_L2_BLOCK_NUM),
2715
+ targetBlockHash,
2716
+ targetTxIndex,
2717
+ targetLogIndex,
2718
+ );
738
2719
 
739
2720
  const response = await store.getPublicLogs({ afterLog, fromBlock: afterLog.blockNumber });
740
2721
  const logs = response.logs;
@@ -754,56 +2735,128 @@ export function describeArchiverDataStore(
754
2735
  });
755
2736
  });
756
2737
 
757
- describe('findNullifiersIndexesWithBlock', () => {
758
- let blocks: L2Block[];
759
- const numBlocks = 10;
760
- const nullifiersPerBlock = new Map<number, Fr[]>();
2738
+ describe('getContractClassLogs', () => {
2739
+ let targetBlock: L2BlockNew;
2740
+ let expectedContractClassLog: ContractClassLog;
761
2741
 
762
2742
  beforeEach(async () => {
763
- blocks = await timesParallel(numBlocks, (index: number) => L2Block.random(index + 1, 1));
2743
+ await store.addCheckpoints(publishedCheckpoints);
764
2744
 
765
- blocks.forEach((block, blockIndex) => {
766
- nullifiersPerBlock.set(
767
- blockIndex,
768
- block.body.txEffects.flatMap(txEffect => txEffect.nullifiers),
769
- );
2745
+ targetBlock = publishedCheckpoints[0].checkpoint.blocks[0];
2746
+ expectedContractClassLog = await ContractClassLog.random();
2747
+ targetBlock.body.txEffects.forEach((txEffect, index) => {
2748
+ txEffect.contractClassLogs = index === 0 ? [expectedContractClassLog] : [];
770
2749
  });
771
- });
772
2750
 
773
- it('returns wrapped nullifiers with blocks if they exist', async () => {
774
- await store.addNullifiers(blocks);
775
- const nullifiersToRetrieve = [...nullifiersPerBlock.get(0)!, ...nullifiersPerBlock.get(5)!, Fr.random()];
776
- const blockScopedNullifiers = await store.findNullifiersIndexesWithBlock(10, nullifiersToRetrieve);
2751
+ await store.addLogs([targetBlock]);
2752
+ });
777
2753
 
778
- expect(blockScopedNullifiers).toHaveLength(nullifiersToRetrieve.length);
779
- const [undefinedNullifier] = blockScopedNullifiers.slice(-1);
780
- const realNullifiers = blockScopedNullifiers.slice(0, -1);
781
- realNullifiers.forEach((blockScopedNullifier, index) => {
782
- expect(blockScopedNullifier).not.toBeUndefined();
783
- const { data, l2BlockNumber } = blockScopedNullifier!;
784
- expect(data).toEqual(expect.any(BigInt));
785
- expect(l2BlockNumber).toEqual(index < MAX_NULLIFIERS_PER_TX ? 1 : 6);
2754
+ it('returns block hash on contract class log ids', async () => {
2755
+ const result = await store.getContractClassLogs({
2756
+ fromBlock: targetBlock.number,
2757
+ toBlock: targetBlock.number + 1,
786
2758
  });
787
- expect(undefinedNullifier).toBeUndefined();
2759
+
2760
+ expect(result.maxLogsHit).toBeFalsy();
2761
+ expect(result.logs).toHaveLength(1);
2762
+
2763
+ const [{ id, log }] = result.logs;
2764
+ const expectedBlockHash = L2BlockHash.fromField(await targetBlock.header.hash());
2765
+
2766
+ expect(id.blockHash.equals(expectedBlockHash)).toBe(true);
2767
+ expect(id.blockNumber).toEqual(targetBlock.number);
2768
+ expect(log).toEqual(expectedContractClassLog);
2769
+ });
2770
+ });
2771
+
2772
+ describe('pendingChainValidationStatus', () => {
2773
+ it('should return undefined when no status is set', async () => {
2774
+ const status = await store.getPendingChainValidationStatus();
2775
+ expect(status).toBeUndefined();
788
2776
  });
789
2777
 
790
- it('returns wrapped nullifiers filtering by blockNumber', async () => {
791
- await store.addNullifiers(blocks);
792
- const nullifiersToRetrieve = [...nullifiersPerBlock.get(0)!, ...nullifiersPerBlock.get(5)!];
793
- const blockScopedNullifiers = await store.findNullifiersIndexesWithBlock(5, nullifiersToRetrieve);
2778
+ it('should store and retrieve a valid validation status', async () => {
2779
+ const validStatus: ValidateBlockResult = { valid: true };
794
2780
 
795
- expect(blockScopedNullifiers).toHaveLength(nullifiersToRetrieve.length);
796
- const undefinedNullifiers = blockScopedNullifiers.slice(-MAX_NULLIFIERS_PER_TX);
797
- const realNullifiers = blockScopedNullifiers.slice(0, -MAX_NULLIFIERS_PER_TX);
798
- realNullifiers.forEach(blockScopedNullifier => {
799
- expect(blockScopedNullifier).not.toBeUndefined();
800
- const { data, l2BlockNumber } = blockScopedNullifier!;
801
- expect(data).toEqual(expect.any(BigInt));
802
- expect(l2BlockNumber).toEqual(1);
803
- });
804
- undefinedNullifiers.forEach(undefinedNullifier => {
805
- expect(undefinedNullifier).toBeUndefined();
806
- });
2781
+ await store.setPendingChainValidationStatus(validStatus);
2782
+ const retrievedStatus = await store.getPendingChainValidationStatus();
2783
+
2784
+ expect(retrievedStatus).toEqual(validStatus);
2785
+ });
2786
+
2787
+ it('should store and retrieve an invalid validation status with insufficient attestations', async () => {
2788
+ const invalidStatus: ValidateBlockResult = {
2789
+ valid: false,
2790
+ block: randomBlockInfo(1),
2791
+ committee: [EthAddress.random(), EthAddress.random()],
2792
+ epoch: EpochNumber(123),
2793
+ seed: 456n,
2794
+ attestors: [EthAddress.random()],
2795
+ attestations: [CommitteeAttestation.random()],
2796
+ reason: 'insufficient-attestations',
2797
+ };
2798
+
2799
+ await store.setPendingChainValidationStatus(invalidStatus);
2800
+ const retrievedStatus = await store.getPendingChainValidationStatus();
2801
+
2802
+ expect(retrievedStatus).toEqual(invalidStatus);
2803
+ });
2804
+
2805
+ it('should store and retrieve an invalid validation status with invalid attestation', async () => {
2806
+ const invalidStatus: ValidateBlockResult = {
2807
+ valid: false,
2808
+ block: randomBlockInfo(2),
2809
+ committee: [EthAddress.random()],
2810
+ attestors: [EthAddress.random()],
2811
+ epoch: EpochNumber(789),
2812
+ seed: 101n,
2813
+ attestations: [CommitteeAttestation.random()],
2814
+ reason: 'invalid-attestation',
2815
+ invalidIndex: 5,
2816
+ };
2817
+
2818
+ await store.setPendingChainValidationStatus(invalidStatus);
2819
+ const retrievedStatus = await store.getPendingChainValidationStatus();
2820
+
2821
+ expect(retrievedStatus).toEqual(invalidStatus);
2822
+ });
2823
+
2824
+ it('should overwrite existing status when setting a new one', async () => {
2825
+ const firstStatus: ValidateBlockResult = { valid: true };
2826
+ const secondStatus: ValidateBlockResult = {
2827
+ valid: false,
2828
+ block: randomBlockInfo(3),
2829
+ committee: [EthAddress.random()],
2830
+ epoch: EpochNumber(999),
2831
+ seed: 888n,
2832
+ attestors: [EthAddress.random()],
2833
+ attestations: [CommitteeAttestation.random()],
2834
+ reason: 'insufficient-attestations',
2835
+ };
2836
+
2837
+ await store.setPendingChainValidationStatus(firstStatus);
2838
+ await store.setPendingChainValidationStatus(secondStatus);
2839
+ const retrievedStatus = await store.getPendingChainValidationStatus();
2840
+
2841
+ expect(retrievedStatus).toEqual(secondStatus);
2842
+ });
2843
+
2844
+ it('should handle empty committee and attestations arrays', async () => {
2845
+ const statusWithEmptyArrays: ValidateBlockResult = {
2846
+ valid: false,
2847
+ block: randomBlockInfo(4),
2848
+ committee: [],
2849
+ epoch: EpochNumber(0),
2850
+ seed: 0n,
2851
+ attestors: [],
2852
+ attestations: [],
2853
+ reason: 'insufficient-attestations',
2854
+ };
2855
+
2856
+ await store.setPendingChainValidationStatus(statusWithEmptyArrays);
2857
+ const retrievedStatus = await store.getPendingChainValidationStatus();
2858
+
2859
+ expect(retrievedStatus).toEqual(statusWithEmptyArrays);
807
2860
  });
808
2861
  });
809
2862
  });