@aztec/archiver 0.0.1-commit.d431d1c → 0.0.1-commit.d939eb5aa
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.
- package/README.md +21 -6
- package/dest/archiver.d.ts +17 -12
- package/dest/archiver.d.ts.map +1 -1
- package/dest/archiver.js +108 -127
- package/dest/config.d.ts +3 -3
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +3 -2
- package/dest/errors.d.ts +39 -10
- package/dest/errors.d.ts.map +1 -1
- package/dest/errors.js +52 -15
- package/dest/factory.d.ts +4 -2
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +29 -23
- package/dest/index.d.ts +2 -1
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -0
- package/dest/l1/bin/retrieve-calldata.js +35 -32
- package/dest/l1/calldata_retriever.d.ts +73 -50
- package/dest/l1/calldata_retriever.d.ts.map +1 -1
- package/dest/l1/calldata_retriever.js +191 -259
- package/dest/l1/data_retrieval.d.ts +11 -11
- package/dest/l1/data_retrieval.d.ts.map +1 -1
- package/dest/l1/data_retrieval.js +38 -37
- package/dest/l1/spire_proposer.d.ts +5 -5
- package/dest/l1/spire_proposer.d.ts.map +1 -1
- package/dest/l1/spire_proposer.js +9 -17
- package/dest/l1/validate_trace.d.ts +6 -3
- package/dest/l1/validate_trace.d.ts.map +1 -1
- package/dest/l1/validate_trace.js +13 -9
- package/dest/modules/data_source_base.d.ts +29 -23
- package/dest/modules/data_source_base.d.ts.map +1 -1
- package/dest/modules/data_source_base.js +55 -124
- package/dest/modules/data_store_updater.d.ts +43 -26
- package/dest/modules/data_store_updater.d.ts.map +1 -1
- package/dest/modules/data_store_updater.js +158 -129
- package/dest/modules/instrumentation.d.ts +19 -4
- package/dest/modules/instrumentation.d.ts.map +1 -1
- package/dest/modules/instrumentation.js +53 -18
- package/dest/modules/l1_synchronizer.d.ts +7 -9
- package/dest/modules/l1_synchronizer.d.ts.map +1 -1
- package/dest/modules/l1_synchronizer.js +186 -145
- package/dest/modules/validation.d.ts +1 -1
- package/dest/modules/validation.d.ts.map +1 -1
- package/dest/modules/validation.js +2 -2
- package/dest/store/block_store.d.ts +86 -34
- package/dest/store/block_store.d.ts.map +1 -1
- package/dest/store/block_store.js +414 -152
- package/dest/store/contract_class_store.d.ts +2 -3
- package/dest/store/contract_class_store.d.ts.map +1 -1
- package/dest/store/contract_class_store.js +16 -72
- package/dest/store/contract_instance_store.d.ts +1 -1
- package/dest/store/contract_instance_store.d.ts.map +1 -1
- package/dest/store/contract_instance_store.js +6 -2
- package/dest/store/kv_archiver_store.d.ts +80 -39
- package/dest/store/kv_archiver_store.d.ts.map +1 -1
- package/dest/store/kv_archiver_store.js +86 -35
- package/dest/store/l2_tips_cache.d.ts +20 -0
- package/dest/store/l2_tips_cache.d.ts.map +1 -0
- package/dest/store/l2_tips_cache.js +109 -0
- package/dest/store/log_store.d.ts +9 -6
- package/dest/store/log_store.d.ts.map +1 -1
- package/dest/store/log_store.js +151 -56
- package/dest/store/message_store.d.ts +5 -1
- package/dest/store/message_store.d.ts.map +1 -1
- package/dest/store/message_store.js +21 -9
- package/dest/test/fake_l1_state.d.ts +24 -4
- package/dest/test/fake_l1_state.d.ts.map +1 -1
- package/dest/test/fake_l1_state.js +133 -26
- package/dest/test/index.js +3 -1
- package/dest/test/mock_archiver.d.ts +1 -1
- package/dest/test/mock_archiver.d.ts.map +1 -1
- package/dest/test/mock_archiver.js +3 -2
- package/dest/test/mock_l1_to_l2_message_source.d.ts +1 -1
- package/dest/test/mock_l1_to_l2_message_source.d.ts.map +1 -1
- package/dest/test/mock_l1_to_l2_message_source.js +2 -1
- package/dest/test/mock_l2_block_source.d.ts +44 -23
- package/dest/test/mock_l2_block_source.d.ts.map +1 -1
- package/dest/test/mock_l2_block_source.js +185 -115
- package/dest/test/mock_structs.d.ts +6 -2
- package/dest/test/mock_structs.d.ts.map +1 -1
- package/dest/test/mock_structs.js +24 -10
- package/dest/test/noop_l1_archiver.d.ts +26 -0
- package/dest/test/noop_l1_archiver.d.ts.map +1 -0
- package/dest/test/noop_l1_archiver.js +71 -0
- package/package.json +14 -13
- package/src/archiver.ts +144 -159
- package/src/config.ts +9 -2
- package/src/errors.ts +82 -26
- package/src/factory.ts +46 -22
- package/src/index.ts +1 -0
- package/src/l1/README.md +25 -68
- package/src/l1/bin/retrieve-calldata.ts +45 -33
- package/src/l1/calldata_retriever.ts +250 -379
- package/src/l1/data_retrieval.ts +35 -41
- package/src/l1/spire_proposer.ts +7 -15
- package/src/l1/validate_trace.ts +24 -6
- package/src/modules/data_source_base.ts +98 -169
- package/src/modules/data_store_updater.ts +178 -160
- package/src/modules/instrumentation.ts +64 -20
- package/src/modules/l1_synchronizer.ts +212 -182
- package/src/modules/validation.ts +2 -2
- package/src/store/block_store.ts +533 -207
- package/src/store/contract_class_store.ts +16 -110
- package/src/store/contract_instance_store.ts +8 -5
- package/src/store/kv_archiver_store.ts +141 -59
- package/src/store/l2_tips_cache.ts +134 -0
- package/src/store/log_store.ts +232 -74
- package/src/store/message_store.ts +27 -10
- package/src/structs/inbox_message.ts +1 -1
- package/src/test/fake_l1_state.ts +180 -32
- package/src/test/index.ts +3 -0
- package/src/test/mock_archiver.ts +3 -2
- package/src/test/mock_l1_to_l2_message_source.ts +1 -0
- package/src/test/mock_l2_block_source.ts +247 -130
- package/src/test/mock_structs.ts +45 -15
- package/src/test/noop_l1_archiver.ts +114 -0
package/src/store/block_store.ts
CHANGED
|
@@ -9,16 +9,26 @@ import { isDefined } from '@aztec/foundation/types';
|
|
|
9
9
|
import type { AztecAsyncKVStore, AztecAsyncMap, AztecAsyncSingleton, Range } from '@aztec/kv-store';
|
|
10
10
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
11
11
|
import {
|
|
12
|
+
type BlockData,
|
|
13
|
+
BlockHash,
|
|
12
14
|
Body,
|
|
13
15
|
CheckpointedL2Block,
|
|
14
16
|
CommitteeAttestation,
|
|
15
|
-
|
|
16
|
-
L2BlockNew,
|
|
17
|
+
L2Block,
|
|
17
18
|
type ValidateCheckpointResult,
|
|
18
19
|
deserializeValidateCheckpointResult,
|
|
19
20
|
serializeValidateCheckpointResult,
|
|
20
21
|
} from '@aztec/stdlib/block';
|
|
21
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
Checkpoint,
|
|
24
|
+
type CheckpointData,
|
|
25
|
+
type CommonCheckpointData,
|
|
26
|
+
L1PublishedData,
|
|
27
|
+
type ProposedCheckpointData,
|
|
28
|
+
type ProposedCheckpointInput,
|
|
29
|
+
PublishedCheckpoint,
|
|
30
|
+
} from '@aztec/stdlib/checkpoint';
|
|
31
|
+
import { type L1RollupConstants, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
22
32
|
import { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
23
33
|
import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
|
|
24
34
|
import {
|
|
@@ -27,20 +37,23 @@ import {
|
|
|
27
37
|
TxEffect,
|
|
28
38
|
TxHash,
|
|
29
39
|
TxReceipt,
|
|
40
|
+
TxStatus,
|
|
30
41
|
deserializeIndexedTxEffect,
|
|
31
42
|
serializeIndexedTxEffect,
|
|
32
43
|
} from '@aztec/stdlib/tx';
|
|
33
44
|
|
|
34
45
|
import {
|
|
46
|
+
BlockAlreadyCheckpointedError,
|
|
35
47
|
BlockArchiveNotConsistentError,
|
|
36
48
|
BlockIndexNotSequentialError,
|
|
37
49
|
BlockNotFoundError,
|
|
38
50
|
BlockNumberNotSequentialError,
|
|
51
|
+
CannotOverwriteCheckpointedBlockError,
|
|
39
52
|
CheckpointNotFoundError,
|
|
40
|
-
CheckpointNumberNotConsistentError,
|
|
41
53
|
CheckpointNumberNotSequentialError,
|
|
42
|
-
InitialBlockNumberNotSequentialError,
|
|
43
54
|
InitialCheckpointNumberNotSequentialError,
|
|
55
|
+
ProposedCheckpointNotSequentialError,
|
|
56
|
+
ProposedCheckpointStaleError,
|
|
44
57
|
} from '../errors.js';
|
|
45
58
|
|
|
46
59
|
export { TxReceipt, type TxEffect, type TxHash } from '@aztec/stdlib/tx';
|
|
@@ -55,26 +68,29 @@ type BlockStorage = {
|
|
|
55
68
|
indexWithinCheckpoint: number;
|
|
56
69
|
};
|
|
57
70
|
|
|
58
|
-
|
|
71
|
+
/** Checkpoint Storage shared between Checkpoints + Proposed Checkpoints */
|
|
72
|
+
type CommonCheckpointStorage = {
|
|
59
73
|
header: Buffer;
|
|
60
74
|
archive: Buffer;
|
|
75
|
+
checkpointOutHash: Buffer;
|
|
61
76
|
checkpointNumber: number;
|
|
62
77
|
startBlock: number;
|
|
63
|
-
|
|
78
|
+
blockCount: number;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type CheckpointStorage = CommonCheckpointStorage & {
|
|
64
82
|
l1: Buffer;
|
|
65
83
|
attestations: Buffer[];
|
|
66
84
|
};
|
|
67
85
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
startBlock: number;
|
|
73
|
-
numBlocks: number;
|
|
74
|
-
l1: L1PublishedData;
|
|
75
|
-
attestations: Buffer[];
|
|
86
|
+
/** Storage format for a proposed checkpoint (attested but not yet L1-confirmed). */
|
|
87
|
+
type ProposedCheckpointStorage = CommonCheckpointStorage & {
|
|
88
|
+
totalManaUsed: string;
|
|
89
|
+
feeAssetPriceModifier: string;
|
|
76
90
|
};
|
|
77
91
|
|
|
92
|
+
export type RemoveCheckpointsResult = { blocksRemoved: L2Block[] | undefined };
|
|
93
|
+
|
|
78
94
|
/**
|
|
79
95
|
* LMDB-based block storage for the archiver.
|
|
80
96
|
*/
|
|
@@ -85,6 +101,9 @@ export class BlockStore {
|
|
|
85
101
|
/** Map checkpoint number to checkpoint data */
|
|
86
102
|
#checkpoints: AztecAsyncMap<number, CheckpointStorage>;
|
|
87
103
|
|
|
104
|
+
/** Map slot number to checkpoint number, for looking up checkpoints by slot range. */
|
|
105
|
+
#slotToCheckpoint: AztecAsyncMap<number, number>;
|
|
106
|
+
|
|
88
107
|
/** Map block hash to list of tx hashes */
|
|
89
108
|
#blockTxs: AztecAsyncMap<string, Buffer>;
|
|
90
109
|
|
|
@@ -97,6 +116,9 @@ export class BlockStore {
|
|
|
97
116
|
/** Stores last proven checkpoint */
|
|
98
117
|
#lastProvenCheckpoint: AztecAsyncSingleton<number>;
|
|
99
118
|
|
|
119
|
+
/** Stores last finalized checkpoint (proven at or before the finalized L1 block) */
|
|
120
|
+
#lastFinalizedCheckpoint: AztecAsyncSingleton<number>;
|
|
121
|
+
|
|
100
122
|
/** Stores the pending chain validation status */
|
|
101
123
|
#pendingChainValidationStatus: AztecAsyncSingleton<Buffer>;
|
|
102
124
|
|
|
@@ -109,6 +131,9 @@ export class BlockStore {
|
|
|
109
131
|
/** Index mapping block archive to block number */
|
|
110
132
|
#blockArchiveIndex: AztecAsyncMap<string, number>;
|
|
111
133
|
|
|
134
|
+
/** Singleton: assumes max 1-deep pipeline. For deeper pipelining, replace with a map keyed by checkpoint number. */
|
|
135
|
+
#proposedCheckpoint: AztecAsyncSingleton<ProposedCheckpointStorage>;
|
|
136
|
+
|
|
112
137
|
#log = createLogger('archiver:block_store');
|
|
113
138
|
|
|
114
139
|
constructor(private db: AztecAsyncKVStore) {
|
|
@@ -120,99 +145,114 @@ export class BlockStore {
|
|
|
120
145
|
this.#blockArchiveIndex = db.openMap('archiver_block_archive_index');
|
|
121
146
|
this.#lastSynchedL1Block = db.openSingleton('archiver_last_synched_l1_block');
|
|
122
147
|
this.#lastProvenCheckpoint = db.openSingleton('archiver_last_proven_l2_checkpoint');
|
|
148
|
+
this.#lastFinalizedCheckpoint = db.openSingleton('archiver_last_finalized_l2_checkpoint');
|
|
123
149
|
this.#pendingChainValidationStatus = db.openSingleton('archiver_pending_chain_validation_status');
|
|
124
150
|
this.#checkpoints = db.openMap('archiver_checkpoints');
|
|
151
|
+
this.#slotToCheckpoint = db.openMap('archiver_slot_to_checkpoint');
|
|
152
|
+
this.#proposedCheckpoint = db.openSingleton('proposed_checkpoint_data');
|
|
125
153
|
}
|
|
126
154
|
|
|
127
155
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* @returns
|
|
156
|
+
* Returns the finalized L2 block number. An L2 block is finalized when it was proven
|
|
157
|
+
* in an L1 block that has itself been finalized on Ethereum.
|
|
158
|
+
* @returns The finalized block number.
|
|
131
159
|
*/
|
|
132
|
-
async
|
|
133
|
-
|
|
134
|
-
|
|
160
|
+
async getFinalizedL2BlockNumber(): Promise<BlockNumber> {
|
|
161
|
+
const finalizedCheckpointNumber = await this.getFinalizedCheckpointNumber();
|
|
162
|
+
if (finalizedCheckpointNumber === INITIAL_CHECKPOINT_NUMBER - 1) {
|
|
163
|
+
return BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
|
|
164
|
+
}
|
|
165
|
+
const checkpointStorage = await this.#checkpoints.getAsync(finalizedCheckpointNumber);
|
|
166
|
+
if (!checkpointStorage) {
|
|
167
|
+
throw new CheckpointNotFoundError(finalizedCheckpointNumber);
|
|
135
168
|
}
|
|
169
|
+
return BlockNumber(checkpointStorage.startBlock + checkpointStorage.blockCount - 1);
|
|
170
|
+
}
|
|
136
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Append a new proposed block to the store.
|
|
174
|
+
* This is an uncheckpointed block that has been proposed by the sequencer but not yet included in a checkpoint on L1.
|
|
175
|
+
* For checkpointed blocks (already published to L1), use addCheckpoints() instead.
|
|
176
|
+
* @param block - The proposed L2 block to be added to the store.
|
|
177
|
+
* @returns True if the operation is successful.
|
|
178
|
+
*/
|
|
179
|
+
async addProposedBlock(block: L2Block, opts: { force?: boolean } = {}): Promise<boolean> {
|
|
137
180
|
return await this.db.transactionAsync(async () => {
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
const firstBlockLastArchive = blocks[0].header.lastArchive.root;
|
|
181
|
+
const blockNumber = block.number;
|
|
182
|
+
const blockCheckpointNumber = block.checkpointNumber;
|
|
183
|
+
const blockIndex = block.indexWithinCheckpoint;
|
|
184
|
+
const blockLastArchive = block.header.lastArchive.root;
|
|
143
185
|
|
|
144
186
|
// Extract the latest block and checkpoint numbers
|
|
145
|
-
const previousBlockNumber = await this.
|
|
187
|
+
const previousBlockNumber = await this.getLatestL2BlockNumber();
|
|
188
|
+
const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
|
|
146
189
|
const previousCheckpointNumber = await this.getLatestCheckpointNumber();
|
|
147
190
|
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
191
|
+
// Verify we're not overwriting checkpointed blocks
|
|
192
|
+
const lastCheckpointedBlockNumber = await this.getCheckpointedL2BlockNumber();
|
|
193
|
+
if (!opts.force && blockNumber <= lastCheckpointedBlockNumber) {
|
|
194
|
+
// Check if the proposed block matches the already-checkpointed one
|
|
195
|
+
const existingBlock = await this.getBlock(BlockNumber(blockNumber));
|
|
196
|
+
if (existingBlock && existingBlock.archive.root.equals(block.archive.root)) {
|
|
197
|
+
throw new BlockAlreadyCheckpointedError(blockNumber);
|
|
198
|
+
}
|
|
199
|
+
throw new CannotOverwriteCheckpointedBlockError(blockNumber, lastCheckpointedBlockNumber);
|
|
151
200
|
}
|
|
152
201
|
|
|
153
|
-
//
|
|
154
|
-
if (!opts.force &&
|
|
155
|
-
throw new
|
|
202
|
+
// Check that the block number is the expected one
|
|
203
|
+
if (!opts.force && previousBlockNumber !== blockNumber - 1) {
|
|
204
|
+
throw new BlockNumberNotSequentialError(blockNumber, previousBlockNumber);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// The same check as above but for checkpoints. Accept the block if either the confirmed
|
|
208
|
+
// checkpoint or the pending (locally validated but not yet confirmed) checkpoint matches.
|
|
209
|
+
const expectedCheckpointNumber = blockCheckpointNumber - 1;
|
|
210
|
+
if (
|
|
211
|
+
!opts.force &&
|
|
212
|
+
previousCheckpointNumber !== expectedCheckpointNumber &&
|
|
213
|
+
proposedCheckpointNumber !== expectedCheckpointNumber
|
|
214
|
+
) {
|
|
215
|
+
const [reported, source]: [CheckpointNumber, 'confirmed' | 'proposed'] =
|
|
216
|
+
proposedCheckpointNumber > previousCheckpointNumber
|
|
217
|
+
? [proposedCheckpointNumber, 'proposed']
|
|
218
|
+
: [previousCheckpointNumber, 'confirmed'];
|
|
219
|
+
throw new CheckpointNumberNotSequentialError(blockCheckpointNumber, reported, source);
|
|
156
220
|
}
|
|
157
221
|
|
|
158
222
|
// Extract the previous block if there is one and see if it is for the same checkpoint or not
|
|
159
223
|
const previousBlockResult = await this.getBlock(previousBlockNumber);
|
|
160
224
|
|
|
161
|
-
let
|
|
225
|
+
let expectedBlockIndex = 0;
|
|
162
226
|
let previousBlockIndex: number | undefined = undefined;
|
|
163
227
|
if (previousBlockResult !== undefined) {
|
|
164
|
-
if (previousBlockResult.checkpointNumber ===
|
|
228
|
+
if (previousBlockResult.checkpointNumber === blockCheckpointNumber) {
|
|
165
229
|
// The previous block is for the same checkpoint, therefore our index should follow it
|
|
166
230
|
previousBlockIndex = previousBlockResult.indexWithinCheckpoint;
|
|
167
|
-
|
|
231
|
+
expectedBlockIndex = previousBlockIndex + 1;
|
|
168
232
|
}
|
|
169
|
-
if (!previousBlockResult.archive.root.equals(
|
|
233
|
+
if (!previousBlockResult.archive.root.equals(blockLastArchive)) {
|
|
170
234
|
throw new BlockArchiveNotConsistentError(
|
|
171
|
-
|
|
235
|
+
blockNumber,
|
|
172
236
|
previousBlockResult.number,
|
|
173
|
-
|
|
237
|
+
blockLastArchive,
|
|
174
238
|
previousBlockResult.archive.root,
|
|
175
239
|
);
|
|
176
240
|
}
|
|
177
241
|
}
|
|
178
242
|
|
|
179
|
-
// Now check that the
|
|
180
|
-
if (!opts.force &&
|
|
181
|
-
throw new BlockIndexNotSequentialError(
|
|
243
|
+
// Now check that the block has the expected index value
|
|
244
|
+
if (!opts.force && expectedBlockIndex !== blockIndex) {
|
|
245
|
+
throw new BlockIndexNotSequentialError(blockIndex, previousBlockIndex);
|
|
182
246
|
}
|
|
183
247
|
|
|
184
|
-
|
|
185
|
-
let previousBlock: L2BlockNew | undefined = undefined;
|
|
186
|
-
for (const block of blocks) {
|
|
187
|
-
if (!opts.force && previousBlock) {
|
|
188
|
-
if (previousBlock.number + 1 !== block.number) {
|
|
189
|
-
throw new BlockNumberNotSequentialError(block.number, previousBlock.number);
|
|
190
|
-
}
|
|
191
|
-
if (previousBlock.indexWithinCheckpoint + 1 !== block.indexWithinCheckpoint) {
|
|
192
|
-
throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint);
|
|
193
|
-
}
|
|
194
|
-
if (!previousBlock.archive.root.equals(block.header.lastArchive.root)) {
|
|
195
|
-
throw new BlockArchiveNotConsistentError(
|
|
196
|
-
block.number,
|
|
197
|
-
previousBlock.number,
|
|
198
|
-
block.header.lastArchive.root,
|
|
199
|
-
previousBlock.archive.root,
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
if (!opts.force && firstBlockCheckpointNumber !== block.checkpointNumber) {
|
|
204
|
-
throw new CheckpointNumberNotConsistentError(block.checkpointNumber, firstBlockCheckpointNumber);
|
|
205
|
-
}
|
|
206
|
-
previousBlock = block;
|
|
207
|
-
await this.addBlockToDatabase(block, block.checkpointNumber, block.indexWithinCheckpoint);
|
|
208
|
-
}
|
|
248
|
+
await this.addBlockToDatabase(block, block.checkpointNumber, block.indexWithinCheckpoint);
|
|
209
249
|
|
|
210
250
|
return true;
|
|
211
251
|
});
|
|
212
252
|
}
|
|
213
253
|
|
|
214
254
|
/**
|
|
215
|
-
* Append new
|
|
255
|
+
* Append new checkpoints to the store's list.
|
|
216
256
|
* @param checkpoints - The L2 checkpoints to be added to the store.
|
|
217
257
|
* @returns True if the operation is successful.
|
|
218
258
|
*/
|
|
@@ -222,37 +262,29 @@ export class BlockStore {
|
|
|
222
262
|
}
|
|
223
263
|
|
|
224
264
|
return await this.db.transactionAsync(async () => {
|
|
225
|
-
// Check that the checkpoint immediately before the first block to be added is present in the store.
|
|
226
265
|
const firstCheckpointNumber = checkpoints[0].checkpoint.number;
|
|
227
266
|
const previousCheckpointNumber = await this.getLatestCheckpointNumber();
|
|
228
267
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
// There should be a previous checkpoint
|
|
237
|
-
previousCheckpointData = await this.getCheckpointData(previousCheckpointNumber);
|
|
238
|
-
if (previousCheckpointData === undefined) {
|
|
239
|
-
throw new CheckpointNotFoundError(previousCheckpointNumber);
|
|
268
|
+
// Handle already-stored checkpoints at the start of the batch.
|
|
269
|
+
// This can happen after an L1 reorg re-includes a checkpoint in a different L1 block.
|
|
270
|
+
// We accept them if archives match (same content) and update their L1 metadata.
|
|
271
|
+
if (!opts.force && firstCheckpointNumber <= previousCheckpointNumber) {
|
|
272
|
+
checkpoints = await this.skipOrUpdateAlreadyStoredCheckpoints(checkpoints, previousCheckpointNumber);
|
|
273
|
+
if (checkpoints.length === 0) {
|
|
274
|
+
return true;
|
|
240
275
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
// If we have a previous checkpoint then we need to get the previous block number
|
|
247
|
-
if (previousCheckpointData !== undefined) {
|
|
248
|
-
previousBlockNumber = BlockNumber(previousCheckpointData.startBlock + previousCheckpointData.numBlocks - 1);
|
|
249
|
-
previousBlock = await this.getBlock(previousBlockNumber);
|
|
250
|
-
if (previousBlock === undefined) {
|
|
251
|
-
// We should be able to get the required previous block
|
|
252
|
-
throw new BlockNotFoundError(previousBlockNumber);
|
|
276
|
+
// Re-check sequentiality after skipping
|
|
277
|
+
const newFirstNumber = checkpoints[0].checkpoint.number;
|
|
278
|
+
if (previousCheckpointNumber !== newFirstNumber - 1) {
|
|
279
|
+
throw new InitialCheckpointNumberNotSequentialError(newFirstNumber, previousCheckpointNumber);
|
|
253
280
|
}
|
|
281
|
+
} else if (previousCheckpointNumber !== firstCheckpointNumber - 1 && !opts.force) {
|
|
282
|
+
throw new InitialCheckpointNumberNotSequentialError(firstCheckpointNumber, previousCheckpointNumber);
|
|
254
283
|
}
|
|
255
284
|
|
|
285
|
+
// Get the last block of the previous checkpoint for archive chaining
|
|
286
|
+
let previousBlock = await this.getPreviousCheckpointBlock(checkpoints[0].checkpoint.number);
|
|
287
|
+
|
|
256
288
|
// Iterate over checkpoints array and insert them, checking that the block numbers are sequential.
|
|
257
289
|
let previousCheckpoint: PublishedCheckpoint | undefined = undefined;
|
|
258
290
|
for (const checkpoint of checkpoints) {
|
|
@@ -268,62 +300,148 @@ export class BlockStore {
|
|
|
268
300
|
}
|
|
269
301
|
previousCheckpoint = checkpoint;
|
|
270
302
|
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
const block = checkpoint.checkpoint.blocks[i];
|
|
274
|
-
if (previousBlock) {
|
|
275
|
-
// The blocks should have a sequential block number
|
|
276
|
-
if (previousBlock.number !== block.number - 1) {
|
|
277
|
-
throw new BlockNumberNotSequentialError(block.number, previousBlock.number);
|
|
278
|
-
}
|
|
279
|
-
// If the blocks are for the same checkpoint then they should have sequential indexes
|
|
280
|
-
if (
|
|
281
|
-
previousBlock.checkpointNumber === block.checkpointNumber &&
|
|
282
|
-
previousBlock.indexWithinCheckpoint !== block.indexWithinCheckpoint - 1
|
|
283
|
-
) {
|
|
284
|
-
throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint);
|
|
285
|
-
}
|
|
286
|
-
if (!previousBlock.archive.root.equals(block.header.lastArchive.root)) {
|
|
287
|
-
throw new BlockArchiveNotConsistentError(
|
|
288
|
-
block.number,
|
|
289
|
-
previousBlock.number,
|
|
290
|
-
block.header.lastArchive.root,
|
|
291
|
-
previousBlock.archive.root,
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
} else {
|
|
295
|
-
// No previous block, must be block 1 at checkpoint index 0
|
|
296
|
-
if (block.indexWithinCheckpoint !== 0) {
|
|
297
|
-
throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, undefined);
|
|
298
|
-
}
|
|
299
|
-
if (block.number !== INITIAL_L2_BLOCK_NUM) {
|
|
300
|
-
throw new BlockNumberNotSequentialError(block.number, undefined);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
+
// Validate block sequencing, indexes, and archive chaining
|
|
304
|
+
this.validateCheckpointBlocks(checkpoint.checkpoint.blocks, previousBlock);
|
|
303
305
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
+
// Store every block in the database (may already exist, but L1 data is authoritative)
|
|
307
|
+
for (let i = 0; i < checkpoint.checkpoint.blocks.length; i++) {
|
|
308
|
+
await this.addBlockToDatabase(checkpoint.checkpoint.blocks[i], checkpoint.checkpoint.number, i);
|
|
306
309
|
}
|
|
310
|
+
previousBlock = checkpoint.checkpoint.blocks.at(-1);
|
|
307
311
|
|
|
308
312
|
// Store the checkpoint in the database
|
|
309
313
|
await this.#checkpoints.set(checkpoint.checkpoint.number, {
|
|
310
314
|
header: checkpoint.checkpoint.header.toBuffer(),
|
|
311
315
|
archive: checkpoint.checkpoint.archive.toBuffer(),
|
|
316
|
+
checkpointOutHash: checkpoint.checkpoint.getCheckpointOutHash().toBuffer(),
|
|
312
317
|
l1: checkpoint.l1.toBuffer(),
|
|
313
318
|
attestations: checkpoint.attestations.map(attestation => attestation.toBuffer()),
|
|
314
319
|
checkpointNumber: checkpoint.checkpoint.number,
|
|
315
320
|
startBlock: checkpoint.checkpoint.blocks[0].number,
|
|
316
|
-
|
|
321
|
+
blockCount: checkpoint.checkpoint.blocks.length,
|
|
317
322
|
});
|
|
323
|
+
|
|
324
|
+
// Update slot-to-checkpoint index
|
|
325
|
+
await this.#slotToCheckpoint.set(checkpoint.checkpoint.header.slotNumber, checkpoint.checkpoint.number);
|
|
318
326
|
}
|
|
319
327
|
|
|
328
|
+
// Clear the proposed checkpoint if any of the confirmed checkpoints match or supersede it
|
|
329
|
+
const lastConfirmedCheckpointNumber = checkpoints[checkpoints.length - 1].checkpoint.number;
|
|
330
|
+
await this.clearProposedCheckpointIfSuperseded(lastConfirmedCheckpointNumber);
|
|
331
|
+
|
|
320
332
|
await this.#lastSynchedL1Block.set(checkpoints[checkpoints.length - 1].l1.blockNumber);
|
|
321
333
|
return true;
|
|
322
334
|
});
|
|
323
335
|
}
|
|
324
336
|
|
|
325
|
-
|
|
326
|
-
|
|
337
|
+
/**
|
|
338
|
+
* Handles checkpoints at the start of a batch that are already stored (e.g. due to L1 reorg).
|
|
339
|
+
* Verifies the archive root matches, updates L1 metadata, and returns only the new checkpoints.
|
|
340
|
+
*/
|
|
341
|
+
private async skipOrUpdateAlreadyStoredCheckpoints(
|
|
342
|
+
checkpoints: PublishedCheckpoint[],
|
|
343
|
+
latestStored: CheckpointNumber,
|
|
344
|
+
): Promise<PublishedCheckpoint[]> {
|
|
345
|
+
let i = 0;
|
|
346
|
+
for (; i < checkpoints.length && checkpoints[i].checkpoint.number <= latestStored; i++) {
|
|
347
|
+
const incoming = checkpoints[i];
|
|
348
|
+
const stored = await this.getCheckpointData(incoming.checkpoint.number);
|
|
349
|
+
if (!stored) {
|
|
350
|
+
// Should not happen if latestStored is correct, but be safe
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
// Verify the checkpoint content matches (archive root)
|
|
354
|
+
if (!stored.archive.root.equals(incoming.checkpoint.archive.root)) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
`Checkpoint ${incoming.checkpoint.number} already exists in store but with a different archive root. ` +
|
|
357
|
+
`Stored: ${stored.archive.root}, incoming: ${incoming.checkpoint.archive.root}`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
// Update L1 metadata and attestations for the already-stored checkpoint
|
|
361
|
+
this.#log.warn(
|
|
362
|
+
`Checkpoint ${incoming.checkpoint.number} already stored, updating L1 info ` +
|
|
363
|
+
`(L1 block ${stored.l1.blockNumber} -> ${incoming.l1.blockNumber})`,
|
|
364
|
+
);
|
|
365
|
+
await this.#checkpoints.set(incoming.checkpoint.number, {
|
|
366
|
+
header: incoming.checkpoint.header.toBuffer(),
|
|
367
|
+
archive: incoming.checkpoint.archive.toBuffer(),
|
|
368
|
+
checkpointOutHash: incoming.checkpoint.getCheckpointOutHash().toBuffer(),
|
|
369
|
+
l1: incoming.l1.toBuffer(),
|
|
370
|
+
attestations: incoming.attestations.map(a => a.toBuffer()),
|
|
371
|
+
checkpointNumber: incoming.checkpoint.number,
|
|
372
|
+
startBlock: incoming.checkpoint.blocks[0].number,
|
|
373
|
+
blockCount: incoming.checkpoint.blocks.length,
|
|
374
|
+
});
|
|
375
|
+
// Update the sync point to reflect the new L1 block
|
|
376
|
+
await this.#lastSynchedL1Block.set(incoming.l1.blockNumber);
|
|
377
|
+
}
|
|
378
|
+
return checkpoints.slice(i);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Gets the last block of the checkpoint before the given one.
|
|
383
|
+
* Returns undefined if there is no previous checkpoint (i.e. genesis).
|
|
384
|
+
*/
|
|
385
|
+
private async getPreviousCheckpointBlock(checkpointNumber: CheckpointNumber): Promise<L2Block | undefined> {
|
|
386
|
+
const previousCheckpointNumber = CheckpointNumber(checkpointNumber - 1);
|
|
387
|
+
if (previousCheckpointNumber === INITIAL_CHECKPOINT_NUMBER - 1) {
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const previousCheckpointData = await this.getCheckpointData(previousCheckpointNumber);
|
|
392
|
+
if (previousCheckpointData === undefined) {
|
|
393
|
+
throw new CheckpointNotFoundError(previousCheckpointNumber);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const previousBlockNumber = BlockNumber(previousCheckpointData.startBlock + previousCheckpointData.blockCount - 1);
|
|
397
|
+
const previousBlock = await this.getBlock(previousBlockNumber);
|
|
398
|
+
if (previousBlock === undefined) {
|
|
399
|
+
throw new BlockNotFoundError(previousBlockNumber);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return previousBlock;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Validates that blocks are sequential, have correct indexes, and chain via archive roots.
|
|
407
|
+
* This is the same validation used for both confirmed checkpoints (addCheckpoints) and
|
|
408
|
+
* proposed checkpoints (setProposedCheckpoint).
|
|
409
|
+
*/
|
|
410
|
+
private validateCheckpointBlocks(blocks: L2Block[], previousBlock: L2Block | undefined): void {
|
|
411
|
+
for (const block of blocks) {
|
|
412
|
+
if (previousBlock) {
|
|
413
|
+
if (previousBlock.number !== block.number - 1) {
|
|
414
|
+
throw new BlockNumberNotSequentialError(block.number, previousBlock.number);
|
|
415
|
+
}
|
|
416
|
+
if (previousBlock.checkpointNumber === block.checkpointNumber) {
|
|
417
|
+
if (previousBlock.indexWithinCheckpoint !== block.indexWithinCheckpoint - 1) {
|
|
418
|
+
throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint);
|
|
419
|
+
}
|
|
420
|
+
} else if (block.indexWithinCheckpoint !== 0) {
|
|
421
|
+
throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint);
|
|
422
|
+
}
|
|
423
|
+
if (!previousBlock.archive.root.equals(block.header.lastArchive.root)) {
|
|
424
|
+
throw new BlockArchiveNotConsistentError(
|
|
425
|
+
block.number,
|
|
426
|
+
previousBlock.number,
|
|
427
|
+
block.header.lastArchive.root,
|
|
428
|
+
previousBlock.archive.root,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
} else {
|
|
432
|
+
if (block.indexWithinCheckpoint !== 0) {
|
|
433
|
+
throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, undefined);
|
|
434
|
+
}
|
|
435
|
+
if (block.number !== INITIAL_L2_BLOCK_NUM) {
|
|
436
|
+
throw new BlockNumberNotSequentialError(block.number, undefined);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
previousBlock = block;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private async addBlockToDatabase(block: L2Block, checkpointNumber: number, indexWithinCheckpoint: number) {
|
|
444
|
+
const blockHash = await block.hash();
|
|
327
445
|
|
|
328
446
|
await this.#blocks.set(block.number, {
|
|
329
447
|
header: block.header.toBuffer(),
|
|
@@ -351,7 +469,7 @@ export class BlockStore {
|
|
|
351
469
|
}
|
|
352
470
|
|
|
353
471
|
/** Deletes a block and all associated data (tx effects, indices). */
|
|
354
|
-
private async deleteBlock(block:
|
|
472
|
+
private async deleteBlock(block: L2Block): Promise<void> {
|
|
355
473
|
// Delete the block from the main blocks map
|
|
356
474
|
await this.#blocks.delete(block.number);
|
|
357
475
|
|
|
@@ -368,51 +486,59 @@ export class BlockStore {
|
|
|
368
486
|
}
|
|
369
487
|
|
|
370
488
|
/**
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
* @param checkpointsToUnwind - The number of checkpoints we are to unwind
|
|
375
|
-
* @returns True if the operation is successful
|
|
489
|
+
* Removes all checkpoints with checkpoint number > checkpointNumber.
|
|
490
|
+
* Also removes ALL blocks (both checkpointed and uncheckpointed) after the last block of the given checkpoint.
|
|
491
|
+
* @param checkpointNumber - Remove all checkpoints strictly after this one.
|
|
376
492
|
*/
|
|
377
|
-
async
|
|
493
|
+
async removeCheckpointsAfter(checkpointNumber: CheckpointNumber): Promise<RemoveCheckpointsResult> {
|
|
378
494
|
return await this.db.transactionAsync(async () => {
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
495
|
+
const latestCheckpointNumber = await this.getLatestCheckpointNumber();
|
|
496
|
+
|
|
497
|
+
if (checkpointNumber >= latestCheckpointNumber) {
|
|
498
|
+
this.#log.warn(`No checkpoints to remove after ${checkpointNumber} (latest is ${latestCheckpointNumber})`);
|
|
499
|
+
return { blocksRemoved: undefined };
|
|
382
500
|
}
|
|
383
501
|
|
|
502
|
+
// If the proven checkpoint is beyond the target, update it
|
|
384
503
|
const proven = await this.getProvenCheckpointNumber();
|
|
385
|
-
if (
|
|
386
|
-
|
|
504
|
+
if (proven > checkpointNumber) {
|
|
505
|
+
this.#log.warn(`Updating proven checkpoint ${proven} to last valid checkpoint ${checkpointNumber}`);
|
|
506
|
+
await this.setProvenCheckpointNumber(checkpointNumber);
|
|
387
507
|
}
|
|
388
508
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
509
|
+
// Find the last block number to keep (last block of the given checkpoint, or 0 if no checkpoint)
|
|
510
|
+
let lastBlockToKeep: BlockNumber;
|
|
511
|
+
if (checkpointNumber <= 0) {
|
|
512
|
+
lastBlockToKeep = BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
|
|
513
|
+
} else {
|
|
514
|
+
const targetCheckpoint = await this.#checkpoints.getAsync(checkpointNumber);
|
|
515
|
+
if (!targetCheckpoint) {
|
|
516
|
+
throw new Error(`Target checkpoint ${checkpointNumber} not found in store`);
|
|
396
517
|
}
|
|
397
|
-
|
|
398
|
-
|
|
518
|
+
lastBlockToKeep = BlockNumber(targetCheckpoint.startBlock + targetCheckpoint.blockCount - 1);
|
|
519
|
+
}
|
|
399
520
|
|
|
400
|
-
|
|
401
|
-
|
|
521
|
+
// Remove all blocks after lastBlockToKeep (both checkpointed and uncheckpointed)
|
|
522
|
+
const blocksRemoved = await this.removeBlocksAfter(lastBlockToKeep);
|
|
402
523
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
await this.
|
|
409
|
-
this.#log.debug(
|
|
410
|
-
`Unwound block ${blockNumber} ${(await block.hash()).toString()} for checkpoint ${checkpointNumber}`,
|
|
411
|
-
);
|
|
524
|
+
// Remove all checkpoints after the target
|
|
525
|
+
for (let c = latestCheckpointNumber; c > checkpointNumber; c = CheckpointNumber(c - 1)) {
|
|
526
|
+
const checkpointStorage = await this.#checkpoints.getAsync(c);
|
|
527
|
+
if (checkpointStorage) {
|
|
528
|
+
const slotNumber = CheckpointHeader.fromBuffer(checkpointStorage.header).slotNumber;
|
|
529
|
+
await this.#slotToCheckpoint.delete(slotNumber);
|
|
412
530
|
}
|
|
531
|
+
await this.#checkpoints.delete(c);
|
|
532
|
+
this.#log.debug(`Removed checkpoint ${c}`);
|
|
413
533
|
}
|
|
414
534
|
|
|
415
|
-
|
|
535
|
+
// Clear any proposed checkpoint that was orphaned by the removal (its base chain no longer exists)
|
|
536
|
+
const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
|
|
537
|
+
if (proposedCheckpointNumber > checkpointNumber) {
|
|
538
|
+
await this.#proposedCheckpoint.delete();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return { blocksRemoved };
|
|
416
542
|
});
|
|
417
543
|
}
|
|
418
544
|
|
|
@@ -436,20 +562,35 @@ export class BlockStore {
|
|
|
436
562
|
return checkpoints;
|
|
437
563
|
}
|
|
438
564
|
|
|
439
|
-
|
|
440
|
-
|
|
565
|
+
/** Returns checkpoint data for all checkpoints whose slot falls within the given range (inclusive). */
|
|
566
|
+
async getCheckpointDataForSlotRange(startSlot: SlotNumber, endSlot: SlotNumber): Promise<CheckpointData[]> {
|
|
567
|
+
const result: CheckpointData[] = [];
|
|
568
|
+
for await (const [, checkpointNumber] of this.#slotToCheckpoint.entriesAsync({
|
|
569
|
+
start: startSlot,
|
|
570
|
+
end: endSlot + 1,
|
|
571
|
+
})) {
|
|
572
|
+
const checkpointStorage = await this.#checkpoints.getAsync(checkpointNumber);
|
|
573
|
+
if (checkpointStorage) {
|
|
574
|
+
result.push(this.checkpointDataFromCheckpointStorage(checkpointStorage));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return result;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
private checkpointDataFromCheckpointStorage(checkpointStorage: CheckpointStorage): CheckpointData {
|
|
581
|
+
return {
|
|
441
582
|
header: CheckpointHeader.fromBuffer(checkpointStorage.header),
|
|
442
583
|
archive: AppendOnlyTreeSnapshot.fromBuffer(checkpointStorage.archive),
|
|
584
|
+
checkpointOutHash: Fr.fromBuffer(checkpointStorage.checkpointOutHash),
|
|
443
585
|
checkpointNumber: CheckpointNumber(checkpointStorage.checkpointNumber),
|
|
444
|
-
startBlock: checkpointStorage.startBlock,
|
|
445
|
-
|
|
586
|
+
startBlock: BlockNumber(checkpointStorage.startBlock),
|
|
587
|
+
blockCount: checkpointStorage.blockCount,
|
|
446
588
|
l1: L1PublishedData.fromBuffer(checkpointStorage.l1),
|
|
447
|
-
attestations: checkpointStorage.attestations,
|
|
589
|
+
attestations: checkpointStorage.attestations.map(buf => CommitteeAttestation.fromBuffer(buf)),
|
|
448
590
|
};
|
|
449
|
-
return data;
|
|
450
591
|
}
|
|
451
592
|
|
|
452
|
-
async getBlocksForCheckpoint(checkpointNumber: CheckpointNumber): Promise<
|
|
593
|
+
async getBlocksForCheckpoint(checkpointNumber: CheckpointNumber): Promise<L2Block[] | undefined> {
|
|
453
594
|
const checkpoint = await this.#checkpoints.getAsync(checkpointNumber);
|
|
454
595
|
if (!checkpoint) {
|
|
455
596
|
return undefined;
|
|
@@ -458,7 +599,7 @@ export class BlockStore {
|
|
|
458
599
|
const blocksForCheckpoint = await toArray(
|
|
459
600
|
this.#blocks.entriesAsync({
|
|
460
601
|
start: checkpoint.startBlock,
|
|
461
|
-
end: checkpoint.startBlock + checkpoint.
|
|
602
|
+
end: checkpoint.startBlock + checkpoint.blockCount,
|
|
462
603
|
}),
|
|
463
604
|
);
|
|
464
605
|
|
|
@@ -472,8 +613,8 @@ export class BlockStore {
|
|
|
472
613
|
* @param slotNumber - The slot number to search for.
|
|
473
614
|
* @returns All blocks with the given slot number, in ascending block number order.
|
|
474
615
|
*/
|
|
475
|
-
async getBlocksForSlot(slotNumber: SlotNumber): Promise<
|
|
476
|
-
const blocks:
|
|
616
|
+
async getBlocksForSlot(slotNumber: SlotNumber): Promise<L2Block[]> {
|
|
617
|
+
const blocks: L2Block[] = [];
|
|
477
618
|
|
|
478
619
|
// Iterate backwards through all blocks and filter by slot number
|
|
479
620
|
// This is more efficient since we usually query for the most recent slot
|
|
@@ -493,15 +634,16 @@ export class BlockStore {
|
|
|
493
634
|
|
|
494
635
|
/**
|
|
495
636
|
* Removes all blocks with block number > blockNumber.
|
|
637
|
+
* Does not remove any associated checkpoints.
|
|
496
638
|
* @param blockNumber - The block number to remove after.
|
|
497
639
|
* @returns The removed blocks (for event emission).
|
|
498
640
|
*/
|
|
499
|
-
async
|
|
641
|
+
async removeBlocksAfter(blockNumber: BlockNumber): Promise<L2Block[]> {
|
|
500
642
|
return await this.db.transactionAsync(async () => {
|
|
501
|
-
const removedBlocks:
|
|
643
|
+
const removedBlocks: L2Block[] = [];
|
|
502
644
|
|
|
503
645
|
// Get the latest block number to determine the range
|
|
504
|
-
const latestBlockNumber = await this.
|
|
646
|
+
const latestBlockNumber = await this.getLatestL2BlockNumber();
|
|
505
647
|
|
|
506
648
|
// Iterate from blockNumber + 1 to latestBlockNumber
|
|
507
649
|
for (let bn = blockNumber + 1; bn <= latestBlockNumber; bn++) {
|
|
@@ -530,17 +672,10 @@ export class BlockStore {
|
|
|
530
672
|
if (!checkpointStorage) {
|
|
531
673
|
throw new CheckpointNotFoundError(provenCheckpointNumber);
|
|
532
674
|
} else {
|
|
533
|
-
return BlockNumber(checkpointStorage.startBlock + checkpointStorage.
|
|
675
|
+
return BlockNumber(checkpointStorage.startBlock + checkpointStorage.blockCount - 1);
|
|
534
676
|
}
|
|
535
677
|
}
|
|
536
678
|
|
|
537
|
-
async getLatestBlockNumber(): Promise<BlockNumber> {
|
|
538
|
-
const [latestBlocknumber] = await toArray(this.#blocks.keysAsync({ reverse: true, limit: 1 }));
|
|
539
|
-
return typeof latestBlocknumber === 'number'
|
|
540
|
-
? BlockNumber(latestBlocknumber)
|
|
541
|
-
: BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
|
|
542
|
-
}
|
|
543
|
-
|
|
544
679
|
async getLatestCheckpointNumber(): Promise<CheckpointNumber> {
|
|
545
680
|
const [latestCheckpointNumber] = await toArray(this.#checkpoints.keysAsync({ reverse: true, limit: 1 }));
|
|
546
681
|
if (latestCheckpointNumber === undefined) {
|
|
@@ -549,6 +684,84 @@ export class BlockStore {
|
|
|
549
684
|
return CheckpointNumber(latestCheckpointNumber);
|
|
550
685
|
}
|
|
551
686
|
|
|
687
|
+
async hasProposedCheckpoint(): Promise<boolean> {
|
|
688
|
+
const proposed = await this.#proposedCheckpoint.getAsync();
|
|
689
|
+
return proposed !== undefined;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/** Deletes the proposed checkpoint from storage. */
|
|
693
|
+
async deleteProposedCheckpoint(): Promise<void> {
|
|
694
|
+
await this.#proposedCheckpoint.delete();
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** Clears the proposed checkpoint if the given confirmed checkpoint number supersedes it. */
|
|
698
|
+
async clearProposedCheckpointIfSuperseded(confirmedCheckpointNumber: CheckpointNumber): Promise<void> {
|
|
699
|
+
const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
|
|
700
|
+
if (proposedCheckpointNumber <= confirmedCheckpointNumber) {
|
|
701
|
+
await this.#proposedCheckpoint.delete();
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/** Returns the proposed checkpoint data, or undefined if no proposed checkpoint exists. No fallback to confirmed. */
|
|
706
|
+
async getProposedCheckpointOnly(): Promise<ProposedCheckpointData | undefined> {
|
|
707
|
+
const stored = await this.#proposedCheckpoint.getAsync();
|
|
708
|
+
if (!stored) {
|
|
709
|
+
return undefined;
|
|
710
|
+
}
|
|
711
|
+
return this.convertToProposedCheckpointData(stored);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Gets the checkpoint at the proposed tip
|
|
716
|
+
* - pending checkpoint if it exists
|
|
717
|
+
* - fallsback to latest confirmed checkpoint otherwise
|
|
718
|
+
* @returns CommonCheckpointData
|
|
719
|
+
*/
|
|
720
|
+
async getProposedCheckpoint(): Promise<CommonCheckpointData | undefined> {
|
|
721
|
+
const stored = await this.#proposedCheckpoint.getAsync();
|
|
722
|
+
if (!stored) {
|
|
723
|
+
return this.getCheckpointData(await this.getLatestCheckpointNumber());
|
|
724
|
+
}
|
|
725
|
+
return this.convertToProposedCheckpointData(stored);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
private convertToProposedCheckpointData(stored: ProposedCheckpointStorage): ProposedCheckpointData {
|
|
729
|
+
return {
|
|
730
|
+
checkpointNumber: CheckpointNumber(stored.checkpointNumber),
|
|
731
|
+
header: CheckpointHeader.fromBuffer(stored.header),
|
|
732
|
+
archive: AppendOnlyTreeSnapshot.fromBuffer(stored.archive),
|
|
733
|
+
checkpointOutHash: Fr.fromBuffer(stored.checkpointOutHash),
|
|
734
|
+
startBlock: BlockNumber(stored.startBlock),
|
|
735
|
+
blockCount: stored.blockCount,
|
|
736
|
+
totalManaUsed: BigInt(stored.totalManaUsed),
|
|
737
|
+
feeAssetPriceModifier: BigInt(stored.feeAssetPriceModifier),
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Attempts to get the proposedCheckpoint's number, if there is not one, then fallback to the latest confirmed checkpoint number.
|
|
743
|
+
* @returns CheckpointNumber
|
|
744
|
+
*/
|
|
745
|
+
async getProposedCheckpointNumber(): Promise<CheckpointNumber> {
|
|
746
|
+
const proposed = await this.getProposedCheckpoint();
|
|
747
|
+
if (!proposed) {
|
|
748
|
+
return await this.getLatestCheckpointNumber();
|
|
749
|
+
}
|
|
750
|
+
return CheckpointNumber(proposed.checkpointNumber);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Attempts to get the proposedCheckpoint's block number, if there is not one, then fallback to the checkpointed block number
|
|
755
|
+
* @returns BlockNumber
|
|
756
|
+
*/
|
|
757
|
+
async getProposedCheckpointL2BlockNumber(): Promise<BlockNumber> {
|
|
758
|
+
const proposed = await this.getProposedCheckpoint();
|
|
759
|
+
if (!proposed) {
|
|
760
|
+
return await this.getCheckpointedL2BlockNumber();
|
|
761
|
+
}
|
|
762
|
+
return BlockNumber(proposed.startBlock + proposed.blockCount - 1);
|
|
763
|
+
}
|
|
764
|
+
|
|
552
765
|
async getCheckpointedBlock(number: BlockNumber): Promise<CheckpointedL2Block | undefined> {
|
|
553
766
|
const blockStorage = await this.#blocks.getAsync(number);
|
|
554
767
|
if (!blockStorage) {
|
|
@@ -598,7 +811,7 @@ export class BlockStore {
|
|
|
598
811
|
}
|
|
599
812
|
}
|
|
600
813
|
|
|
601
|
-
async getCheckpointedBlockByHash(blockHash:
|
|
814
|
+
async getCheckpointedBlockByHash(blockHash: BlockHash): Promise<CheckpointedL2Block | undefined> {
|
|
602
815
|
const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString());
|
|
603
816
|
if (blockNumber === undefined) {
|
|
604
817
|
return undefined;
|
|
@@ -620,7 +833,7 @@ export class BlockStore {
|
|
|
620
833
|
* @param limit - The number of blocks to return.
|
|
621
834
|
* @returns The requested L2 blocks
|
|
622
835
|
*/
|
|
623
|
-
async *getBlocks(start: BlockNumber, limit: number): AsyncIterableIterator<
|
|
836
|
+
async *getBlocks(start: BlockNumber, limit: number): AsyncIterableIterator<L2Block> {
|
|
624
837
|
for await (const [blockNumber, blockStorage] of this.getBlockStorages(start, limit)) {
|
|
625
838
|
const block = await this.getBlockFromBlockStorage(blockNumber, blockStorage);
|
|
626
839
|
if (block) {
|
|
@@ -629,12 +842,38 @@ export class BlockStore {
|
|
|
629
842
|
}
|
|
630
843
|
}
|
|
631
844
|
|
|
845
|
+
/**
|
|
846
|
+
* Gets block metadata (without tx data) by block number.
|
|
847
|
+
* @param blockNumber - The number of the block to return.
|
|
848
|
+
* @returns The requested block data.
|
|
849
|
+
*/
|
|
850
|
+
async getBlockData(blockNumber: BlockNumber): Promise<BlockData | undefined> {
|
|
851
|
+
const blockStorage = await this.#blocks.getAsync(blockNumber);
|
|
852
|
+
if (!blockStorage || !blockStorage.header) {
|
|
853
|
+
return undefined;
|
|
854
|
+
}
|
|
855
|
+
return this.getBlockDataFromBlockStorage(blockStorage);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Gets block metadata (without tx data) by archive root.
|
|
860
|
+
* @param archive - The archive root of the block to return.
|
|
861
|
+
* @returns The requested block data.
|
|
862
|
+
*/
|
|
863
|
+
async getBlockDataByArchive(archive: Fr): Promise<BlockData | undefined> {
|
|
864
|
+
const blockNumber = await this.#blockArchiveIndex.getAsync(archive.toString());
|
|
865
|
+
if (blockNumber === undefined) {
|
|
866
|
+
return undefined;
|
|
867
|
+
}
|
|
868
|
+
return this.getBlockData(BlockNumber(blockNumber));
|
|
869
|
+
}
|
|
870
|
+
|
|
632
871
|
/**
|
|
633
872
|
* Gets an L2 block.
|
|
634
873
|
* @param blockNumber - The number of the block to return.
|
|
635
874
|
* @returns The requested L2 block.
|
|
636
875
|
*/
|
|
637
|
-
async getBlock(blockNumber: BlockNumber): Promise<
|
|
876
|
+
async getBlock(blockNumber: BlockNumber): Promise<L2Block | undefined> {
|
|
638
877
|
const blockStorage = await this.#blocks.getAsync(blockNumber);
|
|
639
878
|
if (!blockStorage || !blockStorage.header) {
|
|
640
879
|
return Promise.resolve(undefined);
|
|
@@ -647,7 +886,7 @@ export class BlockStore {
|
|
|
647
886
|
* @param blockHash - The hash of the block to return.
|
|
648
887
|
* @returns The requested L2 block.
|
|
649
888
|
*/
|
|
650
|
-
async getBlockByHash(blockHash:
|
|
889
|
+
async getBlockByHash(blockHash: BlockHash): Promise<L2Block | undefined> {
|
|
651
890
|
const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString());
|
|
652
891
|
if (blockNumber === undefined) {
|
|
653
892
|
return undefined;
|
|
@@ -660,7 +899,7 @@ export class BlockStore {
|
|
|
660
899
|
* @param archive - The archive root of the block to return.
|
|
661
900
|
* @returns The requested L2 block.
|
|
662
901
|
*/
|
|
663
|
-
async getBlockByArchive(archive: Fr): Promise<
|
|
902
|
+
async getBlockByArchive(archive: Fr): Promise<L2Block | undefined> {
|
|
664
903
|
const blockNumber = await this.#blockArchiveIndex.getAsync(archive.toString());
|
|
665
904
|
if (blockNumber === undefined) {
|
|
666
905
|
return undefined;
|
|
@@ -673,7 +912,7 @@ export class BlockStore {
|
|
|
673
912
|
* @param blockHash - The hash of the block to return.
|
|
674
913
|
* @returns The requested block header.
|
|
675
914
|
*/
|
|
676
|
-
async getBlockHeaderByHash(blockHash:
|
|
915
|
+
async getBlockHeaderByHash(blockHash: BlockHash): Promise<BlockHeader | undefined> {
|
|
677
916
|
const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString());
|
|
678
917
|
if (blockNumber === undefined) {
|
|
679
918
|
return undefined;
|
|
@@ -733,15 +972,24 @@ export class BlockStore {
|
|
|
733
972
|
}
|
|
734
973
|
}
|
|
735
974
|
|
|
975
|
+
private getBlockDataFromBlockStorage(blockStorage: BlockStorage): BlockData {
|
|
976
|
+
return {
|
|
977
|
+
header: BlockHeader.fromBuffer(blockStorage.header),
|
|
978
|
+
archive: AppendOnlyTreeSnapshot.fromBuffer(blockStorage.archive),
|
|
979
|
+
blockHash: BlockHash.fromBuffer(blockStorage.blockHash),
|
|
980
|
+
checkpointNumber: CheckpointNumber(blockStorage.checkpointNumber),
|
|
981
|
+
indexWithinCheckpoint: IndexWithinCheckpoint(blockStorage.indexWithinCheckpoint),
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
|
|
736
985
|
private async getBlockFromBlockStorage(
|
|
737
986
|
blockNumber: number,
|
|
738
987
|
blockStorage: BlockStorage,
|
|
739
|
-
): Promise<
|
|
740
|
-
const header =
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
const blockHashString = bufferToHex(blockHash);
|
|
988
|
+
): Promise<L2Block | undefined> {
|
|
989
|
+
const { header, archive, blockHash, checkpointNumber, indexWithinCheckpoint } =
|
|
990
|
+
this.getBlockDataFromBlockStorage(blockStorage);
|
|
991
|
+
header.setHash(blockHash);
|
|
992
|
+
const blockHashString = bufferToHex(blockStorage.blockHash);
|
|
745
993
|
const blockTxsBuffer = await this.#blockTxs.getAsync(blockHashString);
|
|
746
994
|
if (blockTxsBuffer === undefined) {
|
|
747
995
|
this.#log.warn(`Could not find body for block ${header.globalVariables.blockNumber} ${blockHash}`);
|
|
@@ -760,13 +1008,7 @@ export class BlockStore {
|
|
|
760
1008
|
txEffects.push(deserializeIndexedTxEffect(txEffect).data);
|
|
761
1009
|
}
|
|
762
1010
|
const body = new Body(txEffects);
|
|
763
|
-
const block = new
|
|
764
|
-
archive,
|
|
765
|
-
header,
|
|
766
|
-
body,
|
|
767
|
-
CheckpointNumber(blockStorage.checkpointNumber!),
|
|
768
|
-
IndexWithinCheckpoint(blockStorage.indexWithinCheckpoint),
|
|
769
|
-
);
|
|
1011
|
+
const block = new L2Block(archive, header, body, checkpointNumber, indexWithinCheckpoint);
|
|
770
1012
|
|
|
771
1013
|
if (block.number !== blockNumber) {
|
|
772
1014
|
throw new Error(
|
|
@@ -796,19 +1038,48 @@ export class BlockStore {
|
|
|
796
1038
|
* @param txHash - The hash of a tx we try to get the receipt for.
|
|
797
1039
|
* @returns The requested tx receipt (or undefined if not found).
|
|
798
1040
|
*/
|
|
799
|
-
async getSettledTxReceipt(
|
|
1041
|
+
async getSettledTxReceipt(
|
|
1042
|
+
txHash: TxHash,
|
|
1043
|
+
l1Constants?: Pick<L1RollupConstants, 'epochDuration'>,
|
|
1044
|
+
): Promise<TxReceipt | undefined> {
|
|
800
1045
|
const txEffect = await this.getTxEffect(txHash);
|
|
801
1046
|
if (!txEffect) {
|
|
802
1047
|
return undefined;
|
|
803
1048
|
}
|
|
804
1049
|
|
|
1050
|
+
const blockNumber = BlockNumber(txEffect.l2BlockNumber);
|
|
1051
|
+
|
|
1052
|
+
// Use existing archiver methods to determine finalization level
|
|
1053
|
+
const [provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber, blockData] = await Promise.all([
|
|
1054
|
+
this.getProvenBlockNumber(),
|
|
1055
|
+
this.getCheckpointedL2BlockNumber(),
|
|
1056
|
+
this.getFinalizedL2BlockNumber(),
|
|
1057
|
+
this.getBlockData(blockNumber),
|
|
1058
|
+
]);
|
|
1059
|
+
|
|
1060
|
+
let status: TxStatus;
|
|
1061
|
+
if (blockNumber <= finalizedBlockNumber) {
|
|
1062
|
+
status = TxStatus.FINALIZED;
|
|
1063
|
+
} else if (blockNumber <= provenBlockNumber) {
|
|
1064
|
+
status = TxStatus.PROVEN;
|
|
1065
|
+
} else if (blockNumber <= checkpointedBlockNumber) {
|
|
1066
|
+
status = TxStatus.CHECKPOINTED;
|
|
1067
|
+
} else {
|
|
1068
|
+
status = TxStatus.PROPOSED;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const epochNumber =
|
|
1072
|
+
blockData && l1Constants ? getEpochAtSlot(blockData.header.globalVariables.slotNumber, l1Constants) : undefined;
|
|
1073
|
+
|
|
805
1074
|
return new TxReceipt(
|
|
806
1075
|
txHash,
|
|
807
|
-
|
|
808
|
-
|
|
1076
|
+
status,
|
|
1077
|
+
TxReceipt.executionResultFromRevertCode(txEffect.data.revertCode),
|
|
1078
|
+
undefined,
|
|
809
1079
|
txEffect.data.transactionFee.toBigInt(),
|
|
810
1080
|
txEffect.l2BlockHash,
|
|
811
|
-
|
|
1081
|
+
blockNumber,
|
|
1082
|
+
epochNumber,
|
|
812
1083
|
);
|
|
813
1084
|
}
|
|
814
1085
|
|
|
@@ -845,7 +1116,7 @@ export class BlockStore {
|
|
|
845
1116
|
if (!checkpoint) {
|
|
846
1117
|
return BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
|
|
847
1118
|
}
|
|
848
|
-
return BlockNumber(checkpoint.startBlock + checkpoint.
|
|
1119
|
+
return BlockNumber(checkpoint.startBlock + checkpoint.blockCount - 1);
|
|
849
1120
|
}
|
|
850
1121
|
|
|
851
1122
|
async getLatestL2BlockNumber(): Promise<BlockNumber> {
|
|
@@ -865,6 +1136,47 @@ export class BlockStore {
|
|
|
865
1136
|
return this.#lastSynchedL1Block.set(l1BlockNumber);
|
|
866
1137
|
}
|
|
867
1138
|
|
|
1139
|
+
/** Sets the proposed checkpoint (not yet L1-confirmed). Only accepts confirmed + 1.
|
|
1140
|
+
* Computes archive and checkpointOutHash from the stored blocks. */
|
|
1141
|
+
async setProposedCheckpoint(proposed: ProposedCheckpointInput) {
|
|
1142
|
+
return await this.db.transactionAsync(async () => {
|
|
1143
|
+
const current = await this.getProposedCheckpointNumber();
|
|
1144
|
+
if (proposed.checkpointNumber <= current) {
|
|
1145
|
+
throw new ProposedCheckpointStaleError(proposed.checkpointNumber, current);
|
|
1146
|
+
}
|
|
1147
|
+
const confirmed = await this.getLatestCheckpointNumber();
|
|
1148
|
+
if (proposed.checkpointNumber !== confirmed + 1) {
|
|
1149
|
+
throw new ProposedCheckpointNotSequentialError(proposed.checkpointNumber, confirmed);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// Ensure the previous checkpoint + blocks exist
|
|
1153
|
+
const previousBlock = await this.getPreviousCheckpointBlock(proposed.checkpointNumber);
|
|
1154
|
+
const blocks: L2Block[] = [];
|
|
1155
|
+
for (let i = 0; i < proposed.blockCount; i++) {
|
|
1156
|
+
const block = await this.getBlock(BlockNumber(proposed.startBlock + i));
|
|
1157
|
+
if (!block) {
|
|
1158
|
+
throw new BlockNotFoundError(proposed.startBlock + i);
|
|
1159
|
+
}
|
|
1160
|
+
blocks.push(block);
|
|
1161
|
+
}
|
|
1162
|
+
this.validateCheckpointBlocks(blocks, previousBlock);
|
|
1163
|
+
|
|
1164
|
+
const archive = blocks[blocks.length - 1].archive;
|
|
1165
|
+
const checkpointOutHash = Checkpoint.getCheckpointOutHash(blocks);
|
|
1166
|
+
|
|
1167
|
+
await this.#proposedCheckpoint.set({
|
|
1168
|
+
header: proposed.header.toBuffer(),
|
|
1169
|
+
archive: archive.toBuffer(),
|
|
1170
|
+
checkpointOutHash: checkpointOutHash.toBuffer(),
|
|
1171
|
+
checkpointNumber: proposed.checkpointNumber,
|
|
1172
|
+
startBlock: proposed.startBlock,
|
|
1173
|
+
blockCount: proposed.blockCount,
|
|
1174
|
+
totalManaUsed: proposed.totalManaUsed.toString(),
|
|
1175
|
+
feeAssetPriceModifier: proposed.feeAssetPriceModifier.toString(),
|
|
1176
|
+
});
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
|
|
868
1180
|
async getProvenCheckpointNumber(): Promise<CheckpointNumber> {
|
|
869
1181
|
const [latestCheckpointNumber, provenCheckpointNumber] = await Promise.all([
|
|
870
1182
|
this.getLatestCheckpointNumber(),
|
|
@@ -880,6 +1192,20 @@ export class BlockStore {
|
|
|
880
1192
|
return result;
|
|
881
1193
|
}
|
|
882
1194
|
|
|
1195
|
+
async getFinalizedCheckpointNumber(): Promise<CheckpointNumber> {
|
|
1196
|
+
const [latestCheckpointNumber, finalizedCheckpointNumber] = await Promise.all([
|
|
1197
|
+
this.getLatestCheckpointNumber(),
|
|
1198
|
+
this.#lastFinalizedCheckpoint.getAsync(),
|
|
1199
|
+
]);
|
|
1200
|
+
return (finalizedCheckpointNumber ?? 0) > latestCheckpointNumber
|
|
1201
|
+
? latestCheckpointNumber
|
|
1202
|
+
: CheckpointNumber(finalizedCheckpointNumber ?? 0);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
setFinalizedCheckpointNumber(checkpointNumber: CheckpointNumber) {
|
|
1206
|
+
return this.#lastFinalizedCheckpoint.set(checkpointNumber);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
883
1209
|
#computeBlockRange(start: BlockNumber, limit: number): Required<Pick<Range<number>, 'start' | 'limit'>> {
|
|
884
1210
|
if (limit < 1) {
|
|
885
1211
|
throw new Error(`Invalid limit: ${limit}`);
|