@aztec/archiver 0.0.1-commit.b2a5d0dd1 → 0.0.1-commit.b3d3157a
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 +7 -5
- package/dest/archiver.d.ts +49 -14
- package/dest/archiver.d.ts.map +1 -1
- package/dest/archiver.js +216 -68
- package/dest/config.d.ts +4 -3
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +11 -16
- package/dest/errors.d.ts +19 -13
- package/dest/errors.d.ts.map +1 -1
- package/dest/errors.js +32 -16
- package/dest/factory.d.ts +12 -6
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +31 -21
- package/dest/index.d.ts +9 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +8 -1
- package/dest/l1/calldata_retriever.d.ts +2 -1
- package/dest/l1/calldata_retriever.d.ts.map +1 -1
- package/dest/l1/calldata_retriever.js +13 -4
- package/dest/l1/trace_tx.d.ts +12 -66
- package/dest/l1/trace_tx.d.ts.map +1 -1
- package/dest/modules/contract_data_source_adapter.d.ts +25 -0
- package/dest/modules/contract_data_source_adapter.d.ts.map +1 -0
- package/dest/modules/contract_data_source_adapter.js +40 -0
- package/dest/modules/data_source_base.d.ts +71 -49
- package/dest/modules/data_source_base.d.ts.map +1 -1
- package/dest/modules/data_source_base.js +269 -143
- package/dest/modules/data_store_updater.d.ts +18 -6
- package/dest/modules/data_store_updater.d.ts.map +1 -1
- package/dest/modules/data_store_updater.js +108 -59
- package/dest/modules/instrumentation.js +3 -1
- package/dest/modules/l1_synchronizer.d.ts +6 -5
- package/dest/modules/l1_synchronizer.d.ts.map +1 -1
- package/dest/modules/l1_synchronizer.js +203 -100
- package/dest/modules/outbox_trees_resolver.d.ts +62 -0
- package/dest/modules/outbox_trees_resolver.d.ts.map +1 -0
- package/dest/modules/outbox_trees_resolver.js +162 -0
- package/dest/modules/validation.d.ts +4 -3
- package/dest/modules/validation.d.ts.map +1 -1
- package/dest/modules/validation.js +4 -4
- package/dest/store/block_store.d.ts +155 -98
- package/dest/store/block_store.d.ts.map +1 -1
- package/dest/store/block_store.js +473 -265
- package/dest/store/contract_class_store.d.ts +17 -3
- package/dest/store/contract_class_store.d.ts.map +1 -1
- package/dest/store/contract_class_store.js +17 -1
- package/dest/store/contract_instance_store.d.ts +28 -1
- package/dest/store/contract_instance_store.d.ts.map +1 -1
- package/dest/store/contract_instance_store.js +31 -0
- package/dest/store/data_stores.d.ts +68 -0
- package/dest/store/data_stores.d.ts.map +1 -0
- package/dest/store/data_stores.js +54 -0
- package/dest/store/function_names_cache.d.ts +17 -0
- package/dest/store/function_names_cache.d.ts.map +1 -0
- package/dest/store/function_names_cache.js +30 -0
- package/dest/store/l2_tips_cache.d.ts +13 -8
- package/dest/store/l2_tips_cache.d.ts.map +1 -1
- package/dest/store/l2_tips_cache.js +13 -96
- package/dest/store/log_store.d.ts +42 -40
- package/dest/store/log_store.d.ts.map +1 -1
- package/dest/store/log_store.js +257 -478
- package/dest/store/log_store_codec.d.ts +70 -0
- package/dest/store/log_store_codec.d.ts.map +1 -0
- package/dest/store/log_store_codec.js +101 -0
- package/dest/store/message_store.d.ts +9 -3
- package/dest/store/message_store.d.ts.map +1 -1
- package/dest/store/message_store.js +31 -1
- package/dest/test/fake_l1_state.d.ts +2 -1
- package/dest/test/fake_l1_state.d.ts.map +1 -1
- package/dest/test/fake_l1_state.js +25 -8
- package/dest/test/mock_l2_block_source.d.ts +47 -51
- package/dest/test/mock_l2_block_source.d.ts.map +1 -1
- package/dest/test/mock_l2_block_source.js +223 -185
- package/dest/test/noop_l1_archiver.d.ts +10 -6
- package/dest/test/noop_l1_archiver.d.ts.map +1 -1
- package/dest/test/noop_l1_archiver.js +19 -8
- package/package.json +14 -14
- package/src/archiver.ts +265 -72
- package/src/config.ts +12 -19
- package/src/errors.ts +46 -23
- package/src/factory.ts +38 -20
- package/src/index.ts +16 -1
- package/src/l1/calldata_retriever.ts +14 -4
- package/src/modules/contract_data_source_adapter.ts +55 -0
- package/src/modules/data_source_base.ts +335 -187
- package/src/modules/data_store_updater.ts +118 -61
- package/src/modules/instrumentation.ts +1 -1
- package/src/modules/l1_synchronizer.ts +278 -120
- package/src/modules/outbox_trees_resolver.ts +199 -0
- package/src/modules/validation.ts +8 -7
- package/src/store/block_store.ts +566 -323
- package/src/store/contract_class_store.ts +28 -2
- package/src/store/contract_instance_store.ts +43 -0
- package/src/store/data_stores.ts +104 -0
- package/src/store/function_names_cache.ts +37 -0
- package/src/store/l2_tips_cache.ts +16 -115
- package/src/store/log_store.ts +296 -650
- package/src/store/log_store_codec.ts +132 -0
- package/src/store/message_store.ts +35 -2
- package/src/test/fake_l1_state.ts +24 -14
- package/src/test/mock_l2_block_source.ts +271 -239
- package/src/test/noop_l1_archiver.ts +39 -8
- package/dest/store/kv_archiver_store.d.ts +0 -383
- package/dest/store/kv_archiver_store.d.ts.map +0 -1
- package/dest/store/kv_archiver_store.js +0 -501
- package/src/store/kv_archiver_store.ts +0 -728
package/src/store/block_store.ts
CHANGED
|
@@ -12,9 +12,11 @@ import {
|
|
|
12
12
|
type BlockData,
|
|
13
13
|
BlockHash,
|
|
14
14
|
Body,
|
|
15
|
-
CheckpointedL2Block,
|
|
16
15
|
CommitteeAttestation,
|
|
16
|
+
GENESIS_CHECKPOINT_HEADER_HASH,
|
|
17
17
|
L2Block,
|
|
18
|
+
type L2TipId,
|
|
19
|
+
type L2Tips,
|
|
18
20
|
type ValidateCheckpointResult,
|
|
19
21
|
deserializeValidateCheckpointResult,
|
|
20
22
|
serializeValidateCheckpointResult,
|
|
@@ -28,7 +30,6 @@ import {
|
|
|
28
30
|
type ProposedCheckpointInput,
|
|
29
31
|
PublishedCheckpoint,
|
|
30
32
|
} from '@aztec/stdlib/checkpoint';
|
|
31
|
-
import { type L1RollupConstants, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
32
33
|
import { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
33
34
|
import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
|
|
34
35
|
import {
|
|
@@ -36,8 +37,6 @@ import {
|
|
|
36
37
|
type IndexedTxEffect,
|
|
37
38
|
TxEffect,
|
|
38
39
|
TxHash,
|
|
39
|
-
TxReceipt,
|
|
40
|
-
TxStatus,
|
|
41
40
|
deserializeIndexedTxEffect,
|
|
42
41
|
serializeIndexedTxEffect,
|
|
43
42
|
} from '@aztec/stdlib/tx';
|
|
@@ -45,6 +44,7 @@ import {
|
|
|
45
44
|
import {
|
|
46
45
|
BlockAlreadyCheckpointedError,
|
|
47
46
|
BlockArchiveNotConsistentError,
|
|
47
|
+
BlockCheckpointNumberNotSequentialError,
|
|
48
48
|
BlockIndexNotSequentialError,
|
|
49
49
|
BlockNotFoundError,
|
|
50
50
|
BlockNumberNotSequentialError,
|
|
@@ -56,10 +56,9 @@ import {
|
|
|
56
56
|
ProposedCheckpointArchiveRootMismatchError,
|
|
57
57
|
ProposedCheckpointNotSequentialError,
|
|
58
58
|
ProposedCheckpointPromotionNotSequentialError,
|
|
59
|
-
ProposedCheckpointStaleError,
|
|
60
59
|
} from '../errors.js';
|
|
61
60
|
|
|
62
|
-
export {
|
|
61
|
+
export type { TxEffect, TxHash, TxReceipt } from '@aztec/stdlib/tx';
|
|
63
62
|
|
|
64
63
|
type BlockIndexValue = [blockNumber: number, index: number];
|
|
65
64
|
|
|
@@ -71,6 +70,41 @@ type BlockStorage = {
|
|
|
71
70
|
indexWithinCheckpoint: number;
|
|
72
71
|
};
|
|
73
72
|
|
|
73
|
+
/** Reason a checkpoint was rejected during sync. */
|
|
74
|
+
export type RejectedCheckpointReason = 'invalid-attestations' | 'descends-from-invalid-attestations';
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* A checkpoint observed on L1 that the archiver decided not to ingest, recorded so that
|
|
78
|
+
* any descendant that builds on top of it can also be skipped (rather than throwing
|
|
79
|
+
* `InitialCheckpointNumberNotSequentialError` and looping). An entry is dropped via
|
|
80
|
+
* {@link BlockStore.removeRejectedCheckpointByArchiveRoot} once a checkpoint with the same
|
|
81
|
+
* archive root is later ingested as valid (e.g. it gathered enough attestations), which
|
|
82
|
+
* re-enables its descendants.
|
|
83
|
+
*/
|
|
84
|
+
export type RejectedCheckpoint = {
|
|
85
|
+
/** Checkpoint number this entry represents. */
|
|
86
|
+
checkpointNumber: CheckpointNumber;
|
|
87
|
+
/** Archive root produced by this rejected checkpoint (matched against descendants' `lastArchiveRoot`). */
|
|
88
|
+
archiveRoot: Fr;
|
|
89
|
+
/** `lastArchiveRoot` from this checkpoint's header (the ancestor it built on). */
|
|
90
|
+
parentArchiveRoot: Fr;
|
|
91
|
+
/** Slot number of the rejected checkpoint. */
|
|
92
|
+
slotNumber: SlotNumber;
|
|
93
|
+
/** L1 publication data for the rejected checkpoint (block number, hash, timestamp). */
|
|
94
|
+
l1: L1PublishedData;
|
|
95
|
+
/** Why the entry was recorded. */
|
|
96
|
+
reason: RejectedCheckpointReason;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
type RejectedCheckpointStorage = {
|
|
100
|
+
checkpointNumber: number;
|
|
101
|
+
archiveRoot: Buffer;
|
|
102
|
+
parentArchiveRoot: Buffer;
|
|
103
|
+
slotNumber: number;
|
|
104
|
+
l1: Buffer;
|
|
105
|
+
reason: RejectedCheckpointReason;
|
|
106
|
+
};
|
|
107
|
+
|
|
74
108
|
/** Checkpoint Storage shared between Checkpoints + Proposed Checkpoints */
|
|
75
109
|
type CommonCheckpointStorage = {
|
|
76
110
|
header: Buffer;
|
|
@@ -84,6 +118,7 @@ type CommonCheckpointStorage = {
|
|
|
84
118
|
type CheckpointStorage = CommonCheckpointStorage & {
|
|
85
119
|
l1: Buffer;
|
|
86
120
|
attestations: Buffer[];
|
|
121
|
+
feeAssetPriceModifier: string;
|
|
87
122
|
};
|
|
88
123
|
|
|
89
124
|
/** Storage format for a proposed checkpoint (attested but not yet L1-confirmed). */
|
|
@@ -94,6 +129,19 @@ type ProposedCheckpointStorage = CommonCheckpointStorage & {
|
|
|
94
129
|
|
|
95
130
|
export type RemoveCheckpointsResult = { blocksRemoved: L2Block[] | undefined };
|
|
96
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Single-block lookup with the chain-tip `tag` variant of {@link BlockQuery} already resolved
|
|
134
|
+
* to a concrete block number. The `tag` branch is unrepresentable here so storage code does
|
|
135
|
+
* not need to handle it at runtime.
|
|
136
|
+
*/
|
|
137
|
+
export type ResolvedBlockQuery = { number: BlockNumber } | { hash: BlockHash } | { archive: Fr };
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Range lookup with the `epoch` variant of {@link BlocksQuery} already resolved to a
|
|
141
|
+
* `{ from, limit }` pair. Storage code never needs to map epoch numbers to block ranges.
|
|
142
|
+
*/
|
|
143
|
+
export type ResolvedBlocksQuery = { from: BlockNumber; limit: number; onlyCheckpointed?: boolean };
|
|
144
|
+
|
|
97
145
|
/**
|
|
98
146
|
* LMDB-based block storage for the archiver.
|
|
99
147
|
*/
|
|
@@ -101,7 +149,10 @@ export class BlockStore {
|
|
|
101
149
|
/** Map block number to block data */
|
|
102
150
|
#blocks: AztecAsyncMap<number, BlockStorage>;
|
|
103
151
|
|
|
104
|
-
/** Map checkpoint number
|
|
152
|
+
/** Map keyed by checkpoint number holding proposed (locally-validated, not yet L1-confirmed) checkpoints. */
|
|
153
|
+
#proposedCheckpoints: AztecAsyncMap<number, ProposedCheckpointStorage>;
|
|
154
|
+
|
|
155
|
+
/** Map checkpoint number to checkpoint data for mined checkpoints only */
|
|
105
156
|
#checkpoints: AztecAsyncMap<number, CheckpointStorage>;
|
|
106
157
|
|
|
107
158
|
/** Map slot number to checkpoint number, for looking up checkpoints by slot range. */
|
|
@@ -134,8 +185,11 @@ export class BlockStore {
|
|
|
134
185
|
/** Index mapping block archive to block number */
|
|
135
186
|
#blockArchiveIndex: AztecAsyncMap<string, number>;
|
|
136
187
|
|
|
137
|
-
/**
|
|
138
|
-
#
|
|
188
|
+
/** Map rejected checkpoints (due to invalid attestations) by archive root */
|
|
189
|
+
#rejectedCheckpoints: AztecAsyncMap<string, RejectedCheckpointStorage>;
|
|
190
|
+
|
|
191
|
+
/** Index mapping a rejected checkpoint's number to its archive root, so the latest can be read in reverse order */
|
|
192
|
+
#rejectedCheckpointsByNumber: AztecAsyncMap<number, string>;
|
|
139
193
|
|
|
140
194
|
#log = createLogger('archiver:block_store');
|
|
141
195
|
|
|
@@ -152,7 +206,9 @@ export class BlockStore {
|
|
|
152
206
|
this.#pendingChainValidationStatus = db.openSingleton('archiver_pending_chain_validation_status');
|
|
153
207
|
this.#checkpoints = db.openMap('archiver_checkpoints');
|
|
154
208
|
this.#slotToCheckpoint = db.openMap('archiver_slot_to_checkpoint');
|
|
155
|
-
this.#
|
|
209
|
+
this.#proposedCheckpoints = db.openMap('archiver_proposed_checkpoints');
|
|
210
|
+
this.#rejectedCheckpoints = db.openMap('archiver_rejected_checkpoints');
|
|
211
|
+
this.#rejectedCheckpointsByNumber = db.openMap('archiver_rejected_checkpoints_by_number');
|
|
156
212
|
}
|
|
157
213
|
|
|
158
214
|
/**
|
|
@@ -188,14 +244,13 @@ export class BlockStore {
|
|
|
188
244
|
|
|
189
245
|
// Extract the latest block and checkpoint numbers
|
|
190
246
|
const previousBlockNumber = await this.getLatestL2BlockNumber();
|
|
191
|
-
const
|
|
192
|
-
const previousCheckpointNumber = await this.getLatestCheckpointNumber();
|
|
247
|
+
const latestCheckpointNumber = await this.getLatestCheckpointNumber();
|
|
193
248
|
|
|
194
249
|
// Verify we're not overwriting checkpointed blocks
|
|
195
250
|
const lastCheckpointedBlockNumber = await this.getCheckpointedL2BlockNumber();
|
|
196
251
|
if (!opts.force && blockNumber <= lastCheckpointedBlockNumber) {
|
|
197
252
|
// Check if the proposed block matches the already-checkpointed one
|
|
198
|
-
const existingBlock = await this.
|
|
253
|
+
const existingBlock = await this.getBlockData({ number: BlockNumber(blockNumber) });
|
|
199
254
|
if (existingBlock && existingBlock.archive.root.equals(block.archive.root)) {
|
|
200
255
|
throw new BlockAlreadyCheckpointedError(blockNumber);
|
|
201
256
|
}
|
|
@@ -207,23 +262,18 @@ export class BlockStore {
|
|
|
207
262
|
throw new BlockNumberNotSequentialError(blockNumber, previousBlockNumber);
|
|
208
263
|
}
|
|
209
264
|
|
|
210
|
-
//
|
|
211
|
-
//
|
|
265
|
+
// Accept the block if either the confirmed checkpoint or a pending checkpoint matches
|
|
266
|
+
// the expected predecessor. We look for a pending entry at exactly blockCheckpointNumber - 1.
|
|
212
267
|
const expectedCheckpointNumber = blockCheckpointNumber - 1;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const [reported, source]: [CheckpointNumber, 'confirmed' | 'proposed'] =
|
|
219
|
-
proposedCheckpointNumber > previousCheckpointNumber
|
|
220
|
-
? [proposedCheckpointNumber, 'proposed']
|
|
221
|
-
: [previousCheckpointNumber, 'confirmed'];
|
|
222
|
-
throw new CheckpointNumberNotSequentialError(blockCheckpointNumber, reported, source);
|
|
268
|
+
const hasPendingAtExpected = await this.#proposedCheckpoints.hasAsync(expectedCheckpointNumber);
|
|
269
|
+
if (!opts.force && latestCheckpointNumber !== expectedCheckpointNumber && !hasPendingAtExpected) {
|
|
270
|
+
const [latestPendingKey] = await toArray(this.#proposedCheckpoints.keysAsync({ reverse: true, limit: 1 }));
|
|
271
|
+
const previous = CheckpointNumber(Math.max(latestCheckpointNumber, latestPendingKey ?? 0));
|
|
272
|
+
throw new BlockCheckpointNumberNotSequentialError(BlockNumber(blockNumber), blockCheckpointNumber, previous);
|
|
223
273
|
}
|
|
224
274
|
|
|
225
275
|
// Extract the previous block if there is one and see if it is for the same checkpoint or not
|
|
226
|
-
const previousBlockResult = await this.
|
|
276
|
+
const previousBlockResult = await this.getBlockData({ number: previousBlockNumber });
|
|
227
277
|
|
|
228
278
|
let expectedBlockIndex = 0;
|
|
229
279
|
let previousBlockIndex: number | undefined = undefined;
|
|
@@ -236,7 +286,7 @@ export class BlockStore {
|
|
|
236
286
|
if (!previousBlockResult.archive.root.equals(blockLastArchive)) {
|
|
237
287
|
throw new BlockArchiveNotConsistentError(
|
|
238
288
|
blockNumber,
|
|
239
|
-
previousBlockResult.
|
|
289
|
+
previousBlockResult.header.globalVariables.blockNumber,
|
|
240
290
|
blockLastArchive,
|
|
241
291
|
previousBlockResult.archive.root,
|
|
242
292
|
);
|
|
@@ -322,17 +372,21 @@ export class BlockStore {
|
|
|
322
372
|
checkpointNumber: checkpoint.checkpoint.number,
|
|
323
373
|
startBlock: checkpoint.checkpoint.blocks[0].number,
|
|
324
374
|
blockCount: checkpoint.checkpoint.blocks.length,
|
|
375
|
+
feeAssetPriceModifier: checkpoint.checkpoint.feeAssetPriceModifier.toString(),
|
|
325
376
|
});
|
|
326
377
|
|
|
327
378
|
// Update slot-to-checkpoint index
|
|
328
379
|
await this.#slotToCheckpoint.set(checkpoint.checkpoint.header.slotNumber, checkpoint.checkpoint.number);
|
|
329
|
-
}
|
|
330
380
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
await this.clearProposedCheckpointIfSuperseded(lastConfirmedCheckpointNumber);
|
|
381
|
+
// Remove proposed checkpoint if it exists, since L1 is authoritative
|
|
382
|
+
await this.#proposedCheckpoints.delete(checkpoint.checkpoint.number);
|
|
334
383
|
|
|
335
|
-
|
|
384
|
+
// Drop any rejected entry for this archive root: a checkpoint that was previously rejected
|
|
385
|
+
// (e.g. invalid attestations) is now being ingested as valid, so its descendants are allowed.
|
|
386
|
+
await this.removeRejectedCheckpointByArchiveRoot(checkpoint.checkpoint.archive.root);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
await this.advanceSynchedL1BlockNumber(checkpoints[checkpoints.length - 1].l1.blockNumber);
|
|
336
390
|
return true;
|
|
337
391
|
});
|
|
338
392
|
}
|
|
@@ -374,9 +428,10 @@ export class BlockStore {
|
|
|
374
428
|
checkpointNumber: incoming.checkpoint.number,
|
|
375
429
|
startBlock: incoming.checkpoint.blocks[0].number,
|
|
376
430
|
blockCount: incoming.checkpoint.blocks.length,
|
|
431
|
+
feeAssetPriceModifier: incoming.checkpoint.feeAssetPriceModifier.toString(),
|
|
377
432
|
});
|
|
378
433
|
// Update the sync point to reflect the new L1 block
|
|
379
|
-
await this
|
|
434
|
+
await this.advanceSynchedL1BlockNumber(incoming.l1.blockNumber);
|
|
380
435
|
}
|
|
381
436
|
return checkpoints.slice(i);
|
|
382
437
|
}
|
|
@@ -391,24 +446,27 @@ export class BlockStore {
|
|
|
391
446
|
return undefined;
|
|
392
447
|
}
|
|
393
448
|
|
|
394
|
-
|
|
395
|
-
|
|
449
|
+
// Check across both proposed and mined checkpoints
|
|
450
|
+
const predecessor =
|
|
451
|
+
(await this.getProposedCheckpointByNumber(previousCheckpointNumber)) ??
|
|
452
|
+
(await this.getCheckpointData(previousCheckpointNumber));
|
|
453
|
+
|
|
454
|
+
if (!predecessor) {
|
|
396
455
|
throw new CheckpointNotFoundError(previousCheckpointNumber);
|
|
397
456
|
}
|
|
398
457
|
|
|
399
|
-
const previousBlockNumber = BlockNumber(
|
|
400
|
-
const previousBlock = await this.getBlock(previousBlockNumber);
|
|
458
|
+
const previousBlockNumber = BlockNumber(predecessor.startBlock + predecessor.blockCount - 1);
|
|
459
|
+
const previousBlock = await this.getBlock({ number: previousBlockNumber });
|
|
401
460
|
if (previousBlock === undefined) {
|
|
402
461
|
throw new BlockNotFoundError(previousBlockNumber);
|
|
403
462
|
}
|
|
404
|
-
|
|
405
463
|
return previousBlock;
|
|
406
464
|
}
|
|
407
465
|
|
|
408
466
|
/**
|
|
409
467
|
* Validates that blocks are sequential, have correct indexes, and chain via archive roots.
|
|
410
468
|
* This is the same validation used for both confirmed checkpoints (addCheckpoints) and
|
|
411
|
-
* proposed checkpoints (
|
|
469
|
+
* proposed checkpoints (addProposedCheckpoint).
|
|
412
470
|
*/
|
|
413
471
|
private validateCheckpointBlocks(blocks: L2Block[], previousBlock: L2Block | undefined): void {
|
|
414
472
|
for (const block of blocks) {
|
|
@@ -460,6 +518,7 @@ export class BlockStore {
|
|
|
460
518
|
l2BlockNumber: block.number,
|
|
461
519
|
l2BlockHash: blockHash,
|
|
462
520
|
txIndexInBlock: i,
|
|
521
|
+
slotNumber: block.header.globalVariables.slotNumber,
|
|
463
522
|
};
|
|
464
523
|
await this.#txEffects.set(txEffect.data.txHash.toString(), serializeIndexedTxEffect(txEffect));
|
|
465
524
|
}
|
|
@@ -535,11 +594,8 @@ export class BlockStore {
|
|
|
535
594
|
this.#log.debug(`Removed checkpoint ${c}`);
|
|
536
595
|
}
|
|
537
596
|
|
|
538
|
-
//
|
|
539
|
-
|
|
540
|
-
if (proposedCheckpointNumber > checkpointNumber) {
|
|
541
|
-
await this.#proposedCheckpoint.delete();
|
|
542
|
-
}
|
|
597
|
+
// Evict all pending checkpoints > checkpointNumber (their base chain no longer exists)
|
|
598
|
+
await this.evictProposedCheckpointsFrom(CheckpointNumber(checkpointNumber + 1));
|
|
543
599
|
|
|
544
600
|
return { blocksRemoved };
|
|
545
601
|
});
|
|
@@ -580,6 +636,22 @@ export class BlockStore {
|
|
|
580
636
|
return result;
|
|
581
637
|
}
|
|
582
638
|
|
|
639
|
+
/**
|
|
640
|
+
* Returns the checkpoint numbers for all checkpoints whose slot falls within the given range (inclusive).
|
|
641
|
+
* Lighter than {@link getCheckpointDataForSlotRange} when callers only need to identify which
|
|
642
|
+
* checkpoints fall in the range and will fetch full data for at most a few of them.
|
|
643
|
+
*/
|
|
644
|
+
async getCheckpointNumbersForSlotRange(startSlot: SlotNumber, endSlot: SlotNumber): Promise<CheckpointNumber[]> {
|
|
645
|
+
const result: CheckpointNumber[] = [];
|
|
646
|
+
for await (const [, checkpointNumber] of this.#slotToCheckpoint.entriesAsync({
|
|
647
|
+
start: startSlot,
|
|
648
|
+
end: endSlot + 1,
|
|
649
|
+
})) {
|
|
650
|
+
result.push(CheckpointNumber(checkpointNumber));
|
|
651
|
+
}
|
|
652
|
+
return result;
|
|
653
|
+
}
|
|
654
|
+
|
|
583
655
|
private checkpointDataFromCheckpointStorage(checkpointStorage: CheckpointStorage): CheckpointData {
|
|
584
656
|
return {
|
|
585
657
|
header: CheckpointHeader.fromBuffer(checkpointStorage.header),
|
|
@@ -588,6 +660,7 @@ export class BlockStore {
|
|
|
588
660
|
checkpointNumber: CheckpointNumber(checkpointStorage.checkpointNumber),
|
|
589
661
|
startBlock: BlockNumber(checkpointStorage.startBlock),
|
|
590
662
|
blockCount: checkpointStorage.blockCount,
|
|
663
|
+
feeAssetPriceModifier: BigInt(checkpointStorage.feeAssetPriceModifier),
|
|
591
664
|
l1: L1PublishedData.fromBuffer(checkpointStorage.l1),
|
|
592
665
|
attestations: checkpointStorage.attestations.map(buf => CommitteeAttestation.fromBuffer(buf)),
|
|
593
666
|
};
|
|
@@ -650,7 +723,7 @@ export class BlockStore {
|
|
|
650
723
|
|
|
651
724
|
// Iterate from blockNumber + 1 to latestBlockNumber
|
|
652
725
|
for (let bn = blockNumber + 1; bn <= latestBlockNumber; bn++) {
|
|
653
|
-
const block = await this.getBlock(BlockNumber(bn));
|
|
726
|
+
const block = await this.getBlock({ number: BlockNumber(bn) });
|
|
654
727
|
|
|
655
728
|
if (block === undefined) {
|
|
656
729
|
this.#log.warn(`Cannot remove block ${bn} from the store since we don't have it`);
|
|
@@ -688,28 +761,34 @@ export class BlockStore {
|
|
|
688
761
|
}
|
|
689
762
|
|
|
690
763
|
async hasProposedCheckpoint(): Promise<boolean> {
|
|
691
|
-
const
|
|
692
|
-
return
|
|
764
|
+
const [key] = await toArray(this.#proposedCheckpoints.keysAsync({ limit: 1 }));
|
|
765
|
+
return key !== undefined;
|
|
693
766
|
}
|
|
694
767
|
|
|
695
|
-
/** Deletes
|
|
696
|
-
async
|
|
697
|
-
await this.#
|
|
768
|
+
/** Deletes all pending proposed checkpoints from storage. */
|
|
769
|
+
async deleteProposedCheckpoints(): Promise<void> {
|
|
770
|
+
for await (const key of this.#proposedCheckpoints.keysAsync()) {
|
|
771
|
+
await this.#proposedCheckpoints.delete(key);
|
|
772
|
+
}
|
|
698
773
|
}
|
|
699
774
|
|
|
700
775
|
/**
|
|
701
|
-
* Promotes
|
|
702
|
-
* This persists the checkpoint to the store,
|
|
703
|
-
*
|
|
704
|
-
* @param
|
|
776
|
+
* Promotes a specific pending checkpoint to a confirmed checkpoint entry.
|
|
777
|
+
* This persists the checkpoint to the store, removes only that pending entry, and updates the L1 sync point.
|
|
778
|
+
* Remaining pending entries (e.g. N+1, N+2) are left intact — they chain off the just-promoted one.
|
|
779
|
+
* @param checkpointNumber - The checkpoint number to promote.
|
|
780
|
+
* @param l1 - L1 published data for the checkpoint.
|
|
781
|
+
* @param attestations - Committee attestations.
|
|
782
|
+
* @param expectedArchiveRoot - Archive root guard against races.
|
|
705
783
|
*/
|
|
706
784
|
async promoteProposedToCheckpointed(
|
|
785
|
+
checkpointNumber: CheckpointNumber,
|
|
707
786
|
l1: L1PublishedData,
|
|
708
787
|
attestations: CommitteeAttestation[],
|
|
709
788
|
expectedArchiveRoot: Fr,
|
|
710
789
|
): Promise<void> {
|
|
711
790
|
return await this.db.transactionAsync(async () => {
|
|
712
|
-
const proposed = await this.
|
|
791
|
+
const proposed = await this.getProposedCheckpointByNumber(checkpointNumber);
|
|
713
792
|
if (!proposed) {
|
|
714
793
|
throw new NoProposedCheckpointToPromoteError();
|
|
715
794
|
}
|
|
@@ -733,48 +812,86 @@ export class BlockStore {
|
|
|
733
812
|
checkpointNumber: proposed.checkpointNumber,
|
|
734
813
|
startBlock: proposed.startBlock,
|
|
735
814
|
blockCount: proposed.blockCount,
|
|
815
|
+
feeAssetPriceModifier: proposed.feeAssetPriceModifier.toString(),
|
|
736
816
|
});
|
|
737
817
|
|
|
738
818
|
// Update the slot-to-checkpoint index
|
|
739
819
|
await this.#slotToCheckpoint.set(proposed.header.slotNumber, proposed.checkpointNumber);
|
|
740
820
|
|
|
741
|
-
//
|
|
742
|
-
await this.#
|
|
821
|
+
// Remove only this pending entry — remaining entries N+1, N+2, ... stay valid
|
|
822
|
+
await this.#proposedCheckpoints.delete(proposed.checkpointNumber);
|
|
823
|
+
|
|
824
|
+
// Drop any rejected entry for this archive root: a checkpoint that was previously rejected
|
|
825
|
+
// (e.g. invalid attestations) is now being promoted as valid, so its descendants are allowed.
|
|
826
|
+
await this.removeRejectedCheckpointByArchiveRoot(proposed.archive.root);
|
|
743
827
|
|
|
744
828
|
// Update the last synced L1 block
|
|
745
|
-
await this
|
|
829
|
+
await this.advanceSynchedL1BlockNumber(l1.blockNumber);
|
|
746
830
|
});
|
|
747
831
|
}
|
|
748
832
|
|
|
749
|
-
/**
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
833
|
+
/**
|
|
834
|
+
* Returns the latest pending checkpoint (highest-numbered entry), or undefined if none.
|
|
835
|
+
* No fallback to confirmed.
|
|
836
|
+
*/
|
|
837
|
+
async getLastProposedCheckpoint(): Promise<ProposedCheckpointData | undefined> {
|
|
838
|
+
const [key] = await toArray(this.#proposedCheckpoints.keysAsync({ reverse: true, limit: 1 }));
|
|
839
|
+
if (key === undefined) {
|
|
840
|
+
return undefined;
|
|
754
841
|
}
|
|
842
|
+
const stored = await this.#proposedCheckpoints.getAsync(key);
|
|
843
|
+
return stored ? this.convertToProposedCheckpointData(stored) : undefined;
|
|
755
844
|
}
|
|
756
845
|
|
|
757
|
-
/** Returns the
|
|
758
|
-
async
|
|
759
|
-
const stored = await this.#
|
|
760
|
-
|
|
761
|
-
|
|
846
|
+
/** Returns the pending checkpoint for a specific checkpoint number, or undefined if not found. */
|
|
847
|
+
async getProposedCheckpointByNumber(n: CheckpointNumber): Promise<ProposedCheckpointData | undefined> {
|
|
848
|
+
const stored = await this.#proposedCheckpoints.getAsync(n);
|
|
849
|
+
return stored ? this.convertToProposedCheckpointData(stored) : undefined;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Returns the pending checkpoint whose header slot matches the given slot, or undefined if not found.
|
|
854
|
+
* Iterates `#proposedCheckpoints` rather than reading an index because the map carries 0–1 entries
|
|
855
|
+
* in normal operation (bounded by the proposer pipelining window). Returns the first match.
|
|
856
|
+
*/
|
|
857
|
+
async getProposedCheckpointBySlot(slot: SlotNumber): Promise<ProposedCheckpointData | undefined> {
|
|
858
|
+
for await (const [, stored] of this.#proposedCheckpoints.entriesAsync()) {
|
|
859
|
+
const header = CheckpointHeader.fromBuffer(stored.header);
|
|
860
|
+
if (header.slotNumber === slot) {
|
|
861
|
+
return this.convertToProposedCheckpointData(stored);
|
|
862
|
+
}
|
|
762
863
|
}
|
|
763
|
-
return
|
|
864
|
+
return undefined;
|
|
764
865
|
}
|
|
765
866
|
|
|
766
867
|
/**
|
|
767
|
-
*
|
|
768
|
-
* -
|
|
868
|
+
* Evicts all pending checkpoints with checkpoint number >= fromNumber.
|
|
869
|
+
* Used for divergent-mined-checkpoint cleanup: when L1 mines checkpoint N with a different archive,
|
|
870
|
+
* all pending >= N must be evicted since they chain off the now-invalid pending N.
|
|
871
|
+
*/
|
|
872
|
+
async evictProposedCheckpointsFrom(fromNumber: CheckpointNumber): Promise<void> {
|
|
873
|
+
const keysToDelete: number[] = [];
|
|
874
|
+
for await (const key of this.#proposedCheckpoints.keysAsync()) {
|
|
875
|
+
if (key >= fromNumber) {
|
|
876
|
+
keysToDelete.push(key);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
for (const key of keysToDelete) {
|
|
880
|
+
await this.#proposedCheckpoints.delete(key);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Gets the checkpoint at the proposed tip:
|
|
886
|
+
* - latest pending checkpoint if any exist
|
|
769
887
|
* - fallsback to latest confirmed checkpoint otherwise
|
|
770
|
-
* @returns CommonCheckpointData
|
|
771
888
|
*/
|
|
772
|
-
async
|
|
773
|
-
const
|
|
774
|
-
if (!
|
|
889
|
+
async getLastCheckpoint(): Promise<CommonCheckpointData | undefined> {
|
|
890
|
+
const latest = await this.getLastProposedCheckpoint();
|
|
891
|
+
if (!latest) {
|
|
775
892
|
return this.getCheckpointData(await this.getLatestCheckpointNumber());
|
|
776
893
|
}
|
|
777
|
-
return
|
|
894
|
+
return latest;
|
|
778
895
|
}
|
|
779
896
|
|
|
780
897
|
private convertToProposedCheckpointData(stored: ProposedCheckpointStorage): ProposedCheckpointData {
|
|
@@ -795,7 +912,7 @@ export class BlockStore {
|
|
|
795
912
|
* @returns CheckpointNumber
|
|
796
913
|
*/
|
|
797
914
|
async getProposedCheckpointNumber(): Promise<CheckpointNumber> {
|
|
798
|
-
const proposed = await this.
|
|
915
|
+
const proposed = await this.getLastCheckpoint();
|
|
799
916
|
if (!proposed) {
|
|
800
917
|
return await this.getLatestCheckpointNumber();
|
|
801
918
|
}
|
|
@@ -807,165 +924,40 @@ export class BlockStore {
|
|
|
807
924
|
* @returns BlockNumber
|
|
808
925
|
*/
|
|
809
926
|
async getProposedCheckpointL2BlockNumber(): Promise<BlockNumber> {
|
|
810
|
-
const proposed = await this.
|
|
927
|
+
const proposed = await this.getLastCheckpoint();
|
|
811
928
|
if (!proposed) {
|
|
812
929
|
return await this.getCheckpointedL2BlockNumber();
|
|
813
930
|
}
|
|
814
931
|
return BlockNumber(proposed.startBlock + proposed.blockCount - 1);
|
|
815
932
|
}
|
|
816
933
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
}
|
|
822
|
-
const checkpoint = await this.#checkpoints.getAsync(blockStorage.checkpointNumber);
|
|
823
|
-
if (!checkpoint) {
|
|
824
|
-
return undefined;
|
|
825
|
-
}
|
|
826
|
-
const block = await this.getBlockFromBlockStorage(number, blockStorage);
|
|
827
|
-
if (!block) {
|
|
828
|
-
return undefined;
|
|
829
|
-
}
|
|
830
|
-
return new CheckpointedL2Block(
|
|
831
|
-
CheckpointNumber(checkpoint.checkpointNumber),
|
|
832
|
-
block,
|
|
833
|
-
L1PublishedData.fromBuffer(checkpoint.l1),
|
|
834
|
-
checkpoint.attestations.map(buf => CommitteeAttestation.fromBuffer(buf)),
|
|
835
|
-
);
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
/**
|
|
839
|
-
* Gets up to `limit` amount of Checkpointed L2 blocks starting from `from`.
|
|
840
|
-
* @param start - Number of the first block to return (inclusive).
|
|
841
|
-
* @param limit - The number of blocks to return.
|
|
842
|
-
* @returns The requested L2 blocks
|
|
843
|
-
*/
|
|
844
|
-
async *getCheckpointedBlocks(start: BlockNumber, limit: number): AsyncIterableIterator<CheckpointedL2Block> {
|
|
845
|
-
const checkpointCache = new Map<CheckpointNumber, CheckpointStorage>();
|
|
846
|
-
for await (const [blockNumber, blockStorage] of this.getBlockStorages(start, limit)) {
|
|
847
|
-
const block = await this.getBlockFromBlockStorage(blockNumber, blockStorage);
|
|
848
|
-
if (block) {
|
|
849
|
-
const checkpoint =
|
|
850
|
-
checkpointCache.get(CheckpointNumber(blockStorage.checkpointNumber)) ??
|
|
851
|
-
(await this.#checkpoints.getAsync(blockStorage.checkpointNumber));
|
|
852
|
-
if (checkpoint) {
|
|
853
|
-
checkpointCache.set(CheckpointNumber(blockStorage.checkpointNumber), checkpoint);
|
|
854
|
-
const checkpointedBlock = new CheckpointedL2Block(
|
|
855
|
-
CheckpointNumber(checkpoint.checkpointNumber),
|
|
856
|
-
block,
|
|
857
|
-
L1PublishedData.fromBuffer(checkpoint.l1),
|
|
858
|
-
checkpoint.attestations.map(buf => CommitteeAttestation.fromBuffer(buf)),
|
|
859
|
-
);
|
|
860
|
-
yield checkpointedBlock;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
}
|
|
934
|
+
/** Returns the checkpoint number that contains the given slot (or undefined if not found). */
|
|
935
|
+
async getCheckpointNumberBySlot(slot: SlotNumber): Promise<CheckpointNumber | undefined> {
|
|
936
|
+
const checkpointNumber = await this.#slotToCheckpoint.getAsync(slot);
|
|
937
|
+
return checkpointNumber === undefined ? undefined : CheckpointNumber(checkpointNumber);
|
|
864
938
|
}
|
|
865
939
|
|
|
866
|
-
|
|
867
|
-
|
|
940
|
+
/** Gets a single L2 block matching the given resolved query. */
|
|
941
|
+
async getBlock(query: ResolvedBlockQuery): Promise<L2Block | undefined> {
|
|
942
|
+
const blockNumber = await this.getBlockNumber(query);
|
|
868
943
|
if (blockNumber === undefined) {
|
|
869
944
|
return undefined;
|
|
870
945
|
}
|
|
871
|
-
return this.getCheckpointedBlock(BlockNumber(blockNumber));
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
async getCheckpointedBlockByArchive(archive: Fr): Promise<CheckpointedL2Block | undefined> {
|
|
875
|
-
const blockNumber = await this.#blockArchiveIndex.getAsync(archive.toString());
|
|
876
|
-
if (blockNumber === undefined) {
|
|
877
|
-
return undefined;
|
|
878
|
-
}
|
|
879
|
-
return this.getCheckpointedBlock(BlockNumber(blockNumber));
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
/**
|
|
883
|
-
* Gets up to `limit` amount of L2 blocks starting from `from`.
|
|
884
|
-
* @param start - Number of the first block to return (inclusive).
|
|
885
|
-
* @param limit - The number of blocks to return.
|
|
886
|
-
* @returns The requested L2 blocks
|
|
887
|
-
*/
|
|
888
|
-
async *getBlocks(start: BlockNumber, limit: number): AsyncIterableIterator<L2Block> {
|
|
889
|
-
for await (const [blockNumber, blockStorage] of this.getBlockStorages(start, limit)) {
|
|
890
|
-
const block = await this.getBlockFromBlockStorage(blockNumber, blockStorage);
|
|
891
|
-
if (block) {
|
|
892
|
-
yield block;
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
/**
|
|
898
|
-
* Gets block metadata (without tx data) by block number.
|
|
899
|
-
* @param blockNumber - The number of the block to return.
|
|
900
|
-
* @returns The requested block data.
|
|
901
|
-
*/
|
|
902
|
-
async getBlockData(blockNumber: BlockNumber): Promise<BlockData | undefined> {
|
|
903
946
|
const blockStorage = await this.#blocks.getAsync(blockNumber);
|
|
904
|
-
if (!blockStorage
|
|
905
|
-
return undefined;
|
|
906
|
-
}
|
|
907
|
-
return this.getBlockDataFromBlockStorage(blockStorage);
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
/**
|
|
911
|
-
* Gets block metadata (without tx data) by archive root.
|
|
912
|
-
* @param archive - The archive root of the block to return.
|
|
913
|
-
* @returns The requested block data.
|
|
914
|
-
*/
|
|
915
|
-
async getBlockDataByArchive(archive: Fr): Promise<BlockData | undefined> {
|
|
916
|
-
const blockNumber = await this.#blockArchiveIndex.getAsync(archive.toString());
|
|
917
|
-
if (blockNumber === undefined) {
|
|
947
|
+
if (!blockStorage) {
|
|
918
948
|
return undefined;
|
|
919
949
|
}
|
|
920
|
-
return this.getBlockData(BlockNumber(blockNumber));
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
/**
|
|
924
|
-
* Gets an L2 block.
|
|
925
|
-
* @param blockNumber - The number of the block to return.
|
|
926
|
-
* @returns The requested L2 block.
|
|
927
|
-
*/
|
|
928
|
-
async getBlock(blockNumber: BlockNumber): Promise<L2Block | undefined> {
|
|
929
|
-
const blockStorage = await this.#blocks.getAsync(blockNumber);
|
|
930
|
-
if (!blockStorage || !blockStorage.header) {
|
|
931
|
-
return Promise.resolve(undefined);
|
|
932
|
-
}
|
|
933
950
|
return this.getBlockFromBlockStorage(blockNumber, blockStorage);
|
|
934
951
|
}
|
|
935
952
|
|
|
936
|
-
/**
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
* @returns The requested L2 block.
|
|
940
|
-
*/
|
|
941
|
-
async getBlockByHash(blockHash: BlockHash): Promise<L2Block | undefined> {
|
|
942
|
-
const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString());
|
|
943
|
-
if (blockNumber === undefined) {
|
|
944
|
-
return undefined;
|
|
945
|
-
}
|
|
946
|
-
return this.getBlock(BlockNumber(blockNumber));
|
|
953
|
+
/** Gets a collection of L2 blocks for a resolved range. */
|
|
954
|
+
getBlocks(query: ResolvedBlocksQuery): Promise<L2Block[]> {
|
|
955
|
+
return toArray(this.iterateBlocks(query));
|
|
947
956
|
}
|
|
948
957
|
|
|
949
|
-
/**
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
* @returns The requested L2 block.
|
|
953
|
-
*/
|
|
954
|
-
async getBlockByArchive(archive: Fr): Promise<L2Block | undefined> {
|
|
955
|
-
const blockNumber = await this.#blockArchiveIndex.getAsync(archive.toString());
|
|
956
|
-
if (blockNumber === undefined) {
|
|
957
|
-
return undefined;
|
|
958
|
-
}
|
|
959
|
-
return this.getBlock(BlockNumber(blockNumber));
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
/**
|
|
963
|
-
* Gets a block header by its hash.
|
|
964
|
-
* @param blockHash - The hash of the block to return.
|
|
965
|
-
* @returns The requested block header.
|
|
966
|
-
*/
|
|
967
|
-
async getBlockHeaderByHash(blockHash: BlockHash): Promise<BlockHeader | undefined> {
|
|
968
|
-
const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString());
|
|
958
|
+
/** Gets single block metadata matching the given resolved query. */
|
|
959
|
+
async getBlockData(query: ResolvedBlockQuery): Promise<BlockData | undefined> {
|
|
960
|
+
const blockNumber = await this.getBlockNumber(query);
|
|
969
961
|
if (blockNumber === undefined) {
|
|
970
962
|
return undefined;
|
|
971
963
|
}
|
|
@@ -973,41 +965,36 @@ export class BlockStore {
|
|
|
973
965
|
if (!blockStorage || !blockStorage.header) {
|
|
974
966
|
return undefined;
|
|
975
967
|
}
|
|
976
|
-
return
|
|
968
|
+
return this.getBlockDataFromBlockStorage(blockStorage);
|
|
977
969
|
}
|
|
978
970
|
|
|
979
|
-
/**
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
971
|
+
/** Gets a collection of block metadata entries for a resolved range. */
|
|
972
|
+
getBlocksData(query: ResolvedBlocksQuery): Promise<BlockData[]> {
|
|
973
|
+
return toArray(this.iterateBlocksData(query));
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/** Async iterator over L2 blocks for a resolved range. */
|
|
977
|
+
private async *iterateBlocks(query: ResolvedBlocksQuery): AsyncIterableIterator<L2Block> {
|
|
978
|
+
const cap = query.onlyCheckpointed ? await this.getCheckpointedL2BlockNumber() : undefined;
|
|
979
|
+
for await (const [blockNumber, blockStorage] of this.getBlockStorages(query.from, query.limit)) {
|
|
980
|
+
if (cap !== undefined && blockNumber > cap) {
|
|
981
|
+
break;
|
|
982
|
+
}
|
|
983
|
+
const block = await this.getBlockFromBlockStorage(blockNumber, blockStorage);
|
|
984
|
+
if (block) {
|
|
985
|
+
yield block;
|
|
986
|
+
}
|
|
992
987
|
}
|
|
993
|
-
return BlockHeader.fromBuffer(blockStorage.header);
|
|
994
988
|
}
|
|
995
989
|
|
|
996
|
-
/**
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
async *getBlockHeaders(start: BlockNumber, limit: number): AsyncIterableIterator<BlockHeader> {
|
|
1003
|
-
for await (const [blockNumber, blockStorage] of this.getBlockStorages(start, limit)) {
|
|
1004
|
-
const header = BlockHeader.fromBuffer(blockStorage.header);
|
|
1005
|
-
if (header.getBlockNumber() !== blockNumber) {
|
|
1006
|
-
throw new Error(
|
|
1007
|
-
`Block number mismatch when retrieving block header from archive (expected ${blockNumber} but got ${header.getBlockNumber()})`,
|
|
1008
|
-
);
|
|
990
|
+
/** Async iterator over block metadata for a resolved range. */
|
|
991
|
+
private async *iterateBlocksData(query: ResolvedBlocksQuery): AsyncIterableIterator<BlockData> {
|
|
992
|
+
const cap = query.onlyCheckpointed ? await this.getCheckpointedL2BlockNumber() : undefined;
|
|
993
|
+
for await (const [blockNumber, blockStorage] of this.getBlockStorages(query.from, query.limit)) {
|
|
994
|
+
if (cap !== undefined && blockNumber > cap) {
|
|
995
|
+
break;
|
|
1009
996
|
}
|
|
1010
|
-
yield
|
|
997
|
+
yield this.getBlockDataFromBlockStorage(blockStorage);
|
|
1011
998
|
}
|
|
1012
999
|
}
|
|
1013
1000
|
|
|
@@ -1024,6 +1011,24 @@ export class BlockStore {
|
|
|
1024
1011
|
}
|
|
1025
1012
|
}
|
|
1026
1013
|
|
|
1014
|
+
/** Resolves a ResolvedBlockQuery discriminant to a block number, or undefined if not found. */
|
|
1015
|
+
async getBlockNumber(query: ResolvedBlockQuery): Promise<BlockNumber | undefined> {
|
|
1016
|
+
let blockNumber: BlockNumber | undefined;
|
|
1017
|
+
if ('number' in query) {
|
|
1018
|
+
blockNumber = query.number;
|
|
1019
|
+
} else if ('hash' in query) {
|
|
1020
|
+
const n = await this.#blockHashIndex.getAsync(query.hash.toString());
|
|
1021
|
+
blockNumber = n !== undefined ? BlockNumber(n) : undefined;
|
|
1022
|
+
} else {
|
|
1023
|
+
const n = await this.#blockArchiveIndex.getAsync(query.archive.toString());
|
|
1024
|
+
blockNumber = n !== undefined ? BlockNumber(n) : undefined;
|
|
1025
|
+
}
|
|
1026
|
+
if (blockNumber === undefined) {
|
|
1027
|
+
return undefined;
|
|
1028
|
+
}
|
|
1029
|
+
return blockNumber;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1027
1032
|
private getBlockDataFromBlockStorage(blockStorage: BlockStorage): BlockData {
|
|
1028
1033
|
return {
|
|
1029
1034
|
header: BlockHeader.fromBuffer(blockStorage.header),
|
|
@@ -1085,56 +1090,6 @@ export class BlockStore {
|
|
|
1085
1090
|
return deserializeIndexedTxEffect(buffer);
|
|
1086
1091
|
}
|
|
1087
1092
|
|
|
1088
|
-
/**
|
|
1089
|
-
* Gets a receipt of a settled tx.
|
|
1090
|
-
* @param txHash - The hash of a tx we try to get the receipt for.
|
|
1091
|
-
* @returns The requested tx receipt (or undefined if not found).
|
|
1092
|
-
*/
|
|
1093
|
-
async getSettledTxReceipt(
|
|
1094
|
-
txHash: TxHash,
|
|
1095
|
-
l1Constants?: Pick<L1RollupConstants, 'epochDuration'>,
|
|
1096
|
-
): Promise<TxReceipt | undefined> {
|
|
1097
|
-
const txEffect = await this.getTxEffect(txHash);
|
|
1098
|
-
if (!txEffect) {
|
|
1099
|
-
return undefined;
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
const blockNumber = BlockNumber(txEffect.l2BlockNumber);
|
|
1103
|
-
|
|
1104
|
-
// Use existing archiver methods to determine finalization level
|
|
1105
|
-
const [provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber, blockData] = await Promise.all([
|
|
1106
|
-
this.getProvenBlockNumber(),
|
|
1107
|
-
this.getCheckpointedL2BlockNumber(),
|
|
1108
|
-
this.getFinalizedL2BlockNumber(),
|
|
1109
|
-
this.getBlockData(blockNumber),
|
|
1110
|
-
]);
|
|
1111
|
-
|
|
1112
|
-
let status: TxStatus;
|
|
1113
|
-
if (blockNumber <= finalizedBlockNumber) {
|
|
1114
|
-
status = TxStatus.FINALIZED;
|
|
1115
|
-
} else if (blockNumber <= provenBlockNumber) {
|
|
1116
|
-
status = TxStatus.PROVEN;
|
|
1117
|
-
} else if (blockNumber <= checkpointedBlockNumber) {
|
|
1118
|
-
status = TxStatus.CHECKPOINTED;
|
|
1119
|
-
} else {
|
|
1120
|
-
status = TxStatus.PROPOSED;
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
const epochNumber =
|
|
1124
|
-
blockData && l1Constants ? getEpochAtSlot(blockData.header.globalVariables.slotNumber, l1Constants) : undefined;
|
|
1125
|
-
|
|
1126
|
-
return new TxReceipt(
|
|
1127
|
-
txHash,
|
|
1128
|
-
status,
|
|
1129
|
-
TxReceipt.executionResultFromRevertCode(txEffect.data.revertCode),
|
|
1130
|
-
undefined,
|
|
1131
|
-
txEffect.data.transactionFee.toBigInt(),
|
|
1132
|
-
txEffect.l2BlockHash,
|
|
1133
|
-
blockNumber,
|
|
1134
|
-
epochNumber,
|
|
1135
|
-
);
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
1093
|
/**
|
|
1139
1094
|
* Looks up which block included the requested tx effect.
|
|
1140
1095
|
* @param txHash - The txHash of the tx.
|
|
@@ -1145,10 +1100,45 @@ export class BlockStore {
|
|
|
1145
1100
|
if (!txEffect) {
|
|
1146
1101
|
return undefined;
|
|
1147
1102
|
}
|
|
1148
|
-
|
|
1103
|
+
// Read only the IndexedTxEffect header (`blockHash(32) + l2BlockNumber(4) + txIndexInBlock(4)`); the
|
|
1104
|
+
// large tail (the full TxEffect with logs etc.) is irrelevant here.
|
|
1105
|
+
const view = Buffer.from(txEffect.buffer, txEffect.byteOffset, txEffect.byteLength);
|
|
1106
|
+
const l2BlockNumber = view.readUInt32BE(32);
|
|
1107
|
+
const txIndexInBlock = view.readUInt32BE(36);
|
|
1149
1108
|
return [l2BlockNumber, txIndexInBlock];
|
|
1150
1109
|
}
|
|
1151
1110
|
|
|
1111
|
+
/**
|
|
1112
|
+
* Batched, partial deserializer that fetches `noteHashes` and `nullifiers` (all of them) for the given
|
|
1113
|
+
* txs. For each input txHash, returns a `[noteHashes, nullifiers]` tuple. Returns `[[], []]` for any
|
|
1114
|
+
* unknown txHash. Preserves input order. Used by the log read path when `includeEffects` is set to
|
|
1115
|
+
* attach effect data on demand without paying for a full {@link TxEffect} deserialization.
|
|
1116
|
+
*
|
|
1117
|
+
* The on-disk `IndexedTxEffect` layout starts with a fixed-length header
|
|
1118
|
+
* (`blockHash(32) + l2BlockNumber(4) + txIndexInBlock(4) + slotNumber(4) + revertCode(1) + txHash(32) +
|
|
1119
|
+
* transactionFee(32)` = 109 bytes), followed by `noteHashes` and `nullifiers` (both u8-length-prefixed `Fr`
|
|
1120
|
+
* vectors). We
|
|
1121
|
+
* skip the header, then read the two vectors, and stop — the large tail (`l2ToL1Msgs`,
|
|
1122
|
+
* `publicDataWrites`, `privateLogs`, `publicLogs`, `contractClassLogs`) is never touched.
|
|
1123
|
+
*/
|
|
1124
|
+
public getNoteHashesAndNullifiers(txHashes: TxHash[]): Promise<[Fr[], Fr[]][]> {
|
|
1125
|
+
return Promise.all(
|
|
1126
|
+
txHashes.map(async (txHash): Promise<[Fr[], Fr[]]> => {
|
|
1127
|
+
const buffer = await this.#txEffects.getAsync(txHash.toString());
|
|
1128
|
+
if (!buffer) {
|
|
1129
|
+
return [[], []];
|
|
1130
|
+
}
|
|
1131
|
+
const reader = BufferReader.asReader(buffer);
|
|
1132
|
+
// Skip the fixed-length header: blockHash + l2BlockNumber + txIndexInBlock + slotNumber + revertCode +
|
|
1133
|
+
// txHash + transactionFee.
|
|
1134
|
+
reader.readBytes(32 + 4 + 4 + 4 + 1 + 32 + 32);
|
|
1135
|
+
const noteHashes = reader.readVectorUint8Prefix(Fr);
|
|
1136
|
+
const nullifiers = reader.readVectorUint8Prefix(Fr);
|
|
1137
|
+
return [noteHashes, nullifiers];
|
|
1138
|
+
}),
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1152
1142
|
/**
|
|
1153
1143
|
* Looks up which block deployed a particular contract.
|
|
1154
1144
|
* @param contractAddress - The address of the contract to look up.
|
|
@@ -1176,6 +1166,174 @@ export class BlockStore {
|
|
|
1176
1166
|
return typeof lastBlockNumber === 'number' ? BlockNumber(lastBlockNumber) : BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
|
|
1177
1167
|
}
|
|
1178
1168
|
|
|
1169
|
+
/**
|
|
1170
|
+
* Resolves all five L2 chain tips (proposed, proposedCheckpoint, checkpointed, proven, finalized)
|
|
1171
|
+
* in a single read-only transaction so the snapshot is internally consistent. Each underlying
|
|
1172
|
+
* record is read at most once: latest block, latest confirmed checkpoint, and latest pending
|
|
1173
|
+
* checkpoint are each loaded directly (no separate "find the number, then look up data" hop),
|
|
1174
|
+
* the proven/finalized checkpoint singletons are read once and their storage entries are
|
|
1175
|
+
* reused if they coincide with the latest checkpoint, and per-tip block hashes are deduped
|
|
1176
|
+
* when two tips land on the same block (e.g. finalized == proven, or proposedCheckpoint falls
|
|
1177
|
+
* back to checkpointed when no pending checkpoint exists).
|
|
1178
|
+
*
|
|
1179
|
+
* The result is guaranteed to satisfy `finalized <= proven <= checkpointed <= proposed` (by
|
|
1180
|
+
* block number). Genesis is represented by `(INITIAL_L2_BLOCK_NUM - 1)` and the supplied
|
|
1181
|
+
* `genesisBlockHash`, paired with the synthetic genesis checkpoint id.
|
|
1182
|
+
*
|
|
1183
|
+
* @param genesisBlockHash - Block hash to report for the synthetic pre-initial block (used when
|
|
1184
|
+
* a tip is still at genesis).
|
|
1185
|
+
*/
|
|
1186
|
+
async getL2TipsData(genesisBlockHash: BlockHash): Promise<L2Tips> {
|
|
1187
|
+
return await this.db.transactionAsync(async () => {
|
|
1188
|
+
// Define genesis tips
|
|
1189
|
+
const genesisBlockNumber = BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
|
|
1190
|
+
const genesisCheckpointNumber = CheckpointNumber(INITIAL_CHECKPOINT_NUMBER - 1);
|
|
1191
|
+
const genesisBlockId = { number: genesisBlockNumber, hash: genesisBlockHash.toString() };
|
|
1192
|
+
const genesisCheckpointId = {
|
|
1193
|
+
number: genesisCheckpointNumber,
|
|
1194
|
+
hash: GENESIS_CHECKPOINT_HEADER_HASH.toString(),
|
|
1195
|
+
};
|
|
1196
|
+
const genesisTip: L2TipId = { block: genesisBlockId, checkpoint: genesisCheckpointId };
|
|
1197
|
+
|
|
1198
|
+
// Load latest block and checkpoint entries
|
|
1199
|
+
const [latestBlockEntry] = await toArray(this.#blocks.entriesAsync({ reverse: true, limit: 1 }));
|
|
1200
|
+
const [proposedCheckpointEntry] = await toArray(
|
|
1201
|
+
this.#proposedCheckpoints.entriesAsync({ reverse: true, limit: 1 }),
|
|
1202
|
+
);
|
|
1203
|
+
const [latestCheckpointEntry] = await toArray(this.#checkpoints.entriesAsync({ reverse: true, limit: 1 }));
|
|
1204
|
+
const latestCheckpointNumber = latestCheckpointEntry
|
|
1205
|
+
? CheckpointNumber(latestCheckpointEntry[0])
|
|
1206
|
+
: genesisCheckpointNumber;
|
|
1207
|
+
|
|
1208
|
+
// Load proven and finalized checkpoint number pointers
|
|
1209
|
+
const [provenRaw, finalizedRaw] = await Promise.all([
|
|
1210
|
+
this.#lastProvenCheckpoint.getAsync(),
|
|
1211
|
+
this.#lastFinalizedCheckpoint.getAsync(),
|
|
1212
|
+
]);
|
|
1213
|
+
|
|
1214
|
+
// Clamp to enforce finalized <= proven <= checkpointed.
|
|
1215
|
+
const provenCheckpointNumber = CheckpointNumber(Math.min(provenRaw ?? 0, latestCheckpointNumber));
|
|
1216
|
+
const finalizedCheckpointNumber = CheckpointNumber(Math.min(finalizedRaw ?? 0, provenCheckpointNumber));
|
|
1217
|
+
|
|
1218
|
+
// Avoid loading the same checkpoint more than once
|
|
1219
|
+
const checkpointStorageCache = new Map<CheckpointNumber, CheckpointStorage>();
|
|
1220
|
+
if (latestCheckpointEntry) {
|
|
1221
|
+
checkpointStorageCache.set(CheckpointNumber(latestCheckpointEntry[0]), latestCheckpointEntry[1]);
|
|
1222
|
+
}
|
|
1223
|
+
const loadCheckpointStorage = async (n: CheckpointNumber): Promise<CheckpointStorage | undefined> => {
|
|
1224
|
+
if (n === 0) {
|
|
1225
|
+
return undefined;
|
|
1226
|
+
}
|
|
1227
|
+
if (!checkpointStorageCache.has(n)) {
|
|
1228
|
+
const checkpointStorage = await this.#checkpoints.getAsync(n);
|
|
1229
|
+
if (!checkpointStorage) {
|
|
1230
|
+
throw new CheckpointNotFoundError(n);
|
|
1231
|
+
}
|
|
1232
|
+
checkpointStorageCache.set(n, checkpointStorage);
|
|
1233
|
+
}
|
|
1234
|
+
return checkpointStorageCache.get(n)!;
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
// Load proven and finalized checkpoint storage entries
|
|
1238
|
+
const provenCheckpoint = await loadCheckpointStorage(provenCheckpointNumber);
|
|
1239
|
+
const finalizedCheckpoint = await loadCheckpointStorage(finalizedCheckpointNumber);
|
|
1240
|
+
|
|
1241
|
+
// Avoid loading the same block hash multiple times when tips land on the same block
|
|
1242
|
+
const blockHashCache = new Map<number, string>();
|
|
1243
|
+
blockHashCache.set(genesisBlockNumber, genesisBlockHash.toString());
|
|
1244
|
+
if (latestBlockEntry) {
|
|
1245
|
+
blockHashCache.set(latestBlockEntry[0], BlockHash.fromBuffer(latestBlockEntry[1].blockHash).toString());
|
|
1246
|
+
}
|
|
1247
|
+
const loadBlockHash = async (n: BlockNumber): Promise<string> => {
|
|
1248
|
+
if (!blockHashCache.has(n)) {
|
|
1249
|
+
const blockStorage = await this.#blocks.getAsync(n);
|
|
1250
|
+
if (!blockStorage) {
|
|
1251
|
+
throw new BlockNotFoundError(n);
|
|
1252
|
+
}
|
|
1253
|
+
const blockHash = BlockHash.fromBuffer(blockStorage.blockHash).toString();
|
|
1254
|
+
blockHashCache.set(n, blockHash);
|
|
1255
|
+
}
|
|
1256
|
+
return blockHashCache.get(n)!;
|
|
1257
|
+
};
|
|
1258
|
+
|
|
1259
|
+
// Build proposed chain tip (this one has block only, no checkpoint)
|
|
1260
|
+
const proposedBlockId =
|
|
1261
|
+
latestBlockEntry === undefined
|
|
1262
|
+
? genesisBlockId
|
|
1263
|
+
: {
|
|
1264
|
+
number: BlockNumber(latestBlockEntry[0]),
|
|
1265
|
+
hash: BlockHash.fromBuffer(latestBlockEntry[1].blockHash).toString(),
|
|
1266
|
+
};
|
|
1267
|
+
|
|
1268
|
+
// Build other tips from checkpoint data, reading corresponding block data from the cache
|
|
1269
|
+
const buildTipFromCheckpoint = async (
|
|
1270
|
+
stored: ProposedCheckpointStorage | CheckpointStorage | undefined,
|
|
1271
|
+
): Promise<L2TipId> => {
|
|
1272
|
+
if (!stored) {
|
|
1273
|
+
return genesisTip;
|
|
1274
|
+
}
|
|
1275
|
+
const blockNumber = BlockNumber(stored.startBlock + stored.blockCount - 1);
|
|
1276
|
+
const blockHash = await loadBlockHash(blockNumber);
|
|
1277
|
+
const header = CheckpointHeader.fromBuffer(stored.header);
|
|
1278
|
+
return {
|
|
1279
|
+
block: { number: blockNumber, hash: blockHash },
|
|
1280
|
+
checkpoint: { number: CheckpointNumber(stored.checkpointNumber), hash: header.hash().toString() },
|
|
1281
|
+
};
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
const checkpointedTip = await buildTipFromCheckpoint(latestCheckpointEntry?.[1]);
|
|
1285
|
+
const provenTip = await buildTipFromCheckpoint(provenCheckpoint);
|
|
1286
|
+
const finalizedTip = await buildTipFromCheckpoint(finalizedCheckpoint);
|
|
1287
|
+
|
|
1288
|
+
// Proposed checkpoint falls back to the checkpoint tip if it's not set. And if local storage is
|
|
1289
|
+
// inconsistent and the proposed checkpoint is behind the checkpointed tip, we patch that and
|
|
1290
|
+
// report the checkpointed tip as the proposed checkpoint to maintain the invariant.
|
|
1291
|
+
const proposedCheckpointTip =
|
|
1292
|
+
proposedCheckpointEntry === undefined || proposedCheckpointEntry[0] <= latestCheckpointNumber
|
|
1293
|
+
? checkpointedTip
|
|
1294
|
+
: await buildTipFromCheckpoint(proposedCheckpointEntry[1]);
|
|
1295
|
+
|
|
1296
|
+
// A checkpointed block past the latest stored block would mean a checkpoint
|
|
1297
|
+
// references blocks that aren't in blocks.
|
|
1298
|
+
if (proposedBlockId.number < checkpointedTip.block.number) {
|
|
1299
|
+
throw new Error(
|
|
1300
|
+
`Inconsistent block store: latest block ${proposedBlockId.number} is behind checkpointed block ${checkpointedTip.block.number}`,
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// Assert that checkpoint numbers are increasing
|
|
1305
|
+
if (
|
|
1306
|
+
finalizedTip.checkpoint.number > provenTip.checkpoint.number ||
|
|
1307
|
+
provenTip.checkpoint.number > checkpointedTip.checkpoint.number ||
|
|
1308
|
+
checkpointedTip.checkpoint.number > proposedCheckpointTip.checkpoint.number
|
|
1309
|
+
) {
|
|
1310
|
+
throw new Error(
|
|
1311
|
+
`Inconsistent checkpoint numbers in chain tips: finalized=${finalizedTip.checkpoint.number} proven=${provenTip.checkpoint.number} checkpointed=${checkpointedTip.checkpoint.number} proposed=${proposedCheckpointTip.checkpoint.number}`,
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// Assert block numbers are increasing
|
|
1316
|
+
if (
|
|
1317
|
+
finalizedTip.block.number > provenTip.block.number ||
|
|
1318
|
+
provenTip.block.number > checkpointedTip.block.number ||
|
|
1319
|
+
checkpointedTip.block.number > proposedCheckpointTip.block.number ||
|
|
1320
|
+
proposedCheckpointTip.block.number > proposedBlockId.number
|
|
1321
|
+
) {
|
|
1322
|
+
throw new Error(
|
|
1323
|
+
`Inconsistent block numbers in chain tips: finalized=${finalizedTip.block.number} proven=${provenTip.block.number} checkpointed=${checkpointedTip.block.number} proposedCheckpoint=${proposedCheckpointTip.block.number} proposed=${proposedBlockId.number}`,
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
return {
|
|
1328
|
+
proposed: proposedBlockId,
|
|
1329
|
+
proposedCheckpoint: proposedCheckpointTip,
|
|
1330
|
+
checkpointed: checkpointedTip,
|
|
1331
|
+
proven: provenTip,
|
|
1332
|
+
finalized: finalizedTip,
|
|
1333
|
+
};
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1179
1337
|
/**
|
|
1180
1338
|
* Gets the most recent L1 block processed.
|
|
1181
1339
|
* @returns The L1 block that published the latest L2 block
|
|
@@ -1188,24 +1346,29 @@ export class BlockStore {
|
|
|
1188
1346
|
return this.#lastSynchedL1Block.set(l1BlockNumber);
|
|
1189
1347
|
}
|
|
1190
1348
|
|
|
1191
|
-
/**
|
|
1192
|
-
*
|
|
1193
|
-
|
|
1349
|
+
/**
|
|
1350
|
+
* Adds a proposed checkpoint to the pending queue.
|
|
1351
|
+
* Accepts proposed.checkpointNumber === latestTip + 1, where latestTip is the highest of
|
|
1352
|
+
* confirmed and the highest pending checkpoint number.
|
|
1353
|
+
* Computes archive and checkpointOutHash from the stored blocks.
|
|
1354
|
+
*/
|
|
1355
|
+
async addProposedCheckpoint(proposed: ProposedCheckpointInput) {
|
|
1194
1356
|
return await this.db.transactionAsync(async () => {
|
|
1195
|
-
const current = await this.getProposedCheckpointNumber();
|
|
1196
|
-
if (proposed.checkpointNumber <= current) {
|
|
1197
|
-
throw new ProposedCheckpointStaleError(proposed.checkpointNumber, current);
|
|
1198
|
-
}
|
|
1199
1357
|
const confirmed = await this.getLatestCheckpointNumber();
|
|
1200
|
-
|
|
1201
|
-
|
|
1358
|
+
const [latestPendingKey] = await toArray(this.#proposedCheckpoints.keysAsync({ reverse: true, limit: 1 }));
|
|
1359
|
+
const latestTip = CheckpointNumber(
|
|
1360
|
+
latestPendingKey !== undefined ? Math.max(latestPendingKey, confirmed) : confirmed,
|
|
1361
|
+
);
|
|
1362
|
+
|
|
1363
|
+
if (proposed.checkpointNumber !== latestTip + 1) {
|
|
1364
|
+
throw new ProposedCheckpointNotSequentialError(proposed.checkpointNumber, latestTip);
|
|
1202
1365
|
}
|
|
1203
1366
|
|
|
1204
|
-
// Ensure the
|
|
1367
|
+
// Ensure the predecessor block (from pending or confirmed chain) exists
|
|
1205
1368
|
const previousBlock = await this.getPreviousCheckpointBlock(proposed.checkpointNumber);
|
|
1206
1369
|
const blocks: L2Block[] = [];
|
|
1207
1370
|
for (let i = 0; i < proposed.blockCount; i++) {
|
|
1208
|
-
const block = await this.getBlock(BlockNumber(proposed.startBlock + i));
|
|
1371
|
+
const block = await this.getBlock({ number: BlockNumber(proposed.startBlock + i) });
|
|
1209
1372
|
if (!block) {
|
|
1210
1373
|
throw new BlockNotFoundError(proposed.startBlock + i);
|
|
1211
1374
|
}
|
|
@@ -1216,7 +1379,7 @@ export class BlockStore {
|
|
|
1216
1379
|
const archive = blocks[blocks.length - 1].archive;
|
|
1217
1380
|
const checkpointOutHash = Checkpoint.getCheckpointOutHash(blocks);
|
|
1218
1381
|
|
|
1219
|
-
await this.#
|
|
1382
|
+
await this.#proposedCheckpoints.set(proposed.checkpointNumber, {
|
|
1220
1383
|
header: proposed.header.toBuffer(),
|
|
1221
1384
|
archive: archive.toBuffer(),
|
|
1222
1385
|
checkpointOutHash: checkpointOutHash.toBuffer(),
|
|
@@ -1230,13 +1393,15 @@ export class BlockStore {
|
|
|
1230
1393
|
}
|
|
1231
1394
|
|
|
1232
1395
|
async getProvenCheckpointNumber(): Promise<CheckpointNumber> {
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1396
|
+
return await this.db.transactionAsync(async () => {
|
|
1397
|
+
const [latestCheckpointNumber, provenCheckpointNumber] = await Promise.all([
|
|
1398
|
+
this.getLatestCheckpointNumber(),
|
|
1399
|
+
this.#lastProvenCheckpoint.getAsync(),
|
|
1400
|
+
]);
|
|
1401
|
+
return (provenCheckpointNumber ?? 0) > latestCheckpointNumber
|
|
1402
|
+
? latestCheckpointNumber
|
|
1403
|
+
: CheckpointNumber(provenCheckpointNumber ?? 0);
|
|
1404
|
+
});
|
|
1240
1405
|
}
|
|
1241
1406
|
|
|
1242
1407
|
async setProvenCheckpointNumber(checkpointNumber: CheckpointNumber) {
|
|
@@ -1245,13 +1410,15 @@ export class BlockStore {
|
|
|
1245
1410
|
}
|
|
1246
1411
|
|
|
1247
1412
|
async getFinalizedCheckpointNumber(): Promise<CheckpointNumber> {
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1413
|
+
return await this.db.transactionAsync(async () => {
|
|
1414
|
+
const [provenCheckpointNumber, finalizedCheckpointNumber] = await Promise.all([
|
|
1415
|
+
this.getProvenCheckpointNumber(),
|
|
1416
|
+
this.#lastFinalizedCheckpoint.getAsync(),
|
|
1417
|
+
]);
|
|
1418
|
+
return (finalizedCheckpointNumber ?? 0) > provenCheckpointNumber
|
|
1419
|
+
? provenCheckpointNumber
|
|
1420
|
+
: CheckpointNumber(finalizedCheckpointNumber ?? 0);
|
|
1421
|
+
});
|
|
1255
1422
|
}
|
|
1256
1423
|
|
|
1257
1424
|
setFinalizedCheckpointNumber(checkpointNumber: CheckpointNumber) {
|
|
@@ -1294,4 +1461,80 @@ export class BlockStore {
|
|
|
1294
1461
|
await this.#pendingChainValidationStatus.delete();
|
|
1295
1462
|
}
|
|
1296
1463
|
}
|
|
1464
|
+
|
|
1465
|
+
/** Records a rejected-checkpoint entry, keyed by its own archive root. */
|
|
1466
|
+
async addRejectedCheckpoint(entry: RejectedCheckpoint): Promise<void> {
|
|
1467
|
+
const archiveRootHex = entry.archiveRoot.toString();
|
|
1468
|
+
await this.#rejectedCheckpoints.set(archiveRootHex, {
|
|
1469
|
+
checkpointNumber: entry.checkpointNumber,
|
|
1470
|
+
archiveRoot: entry.archiveRoot.toBuffer(),
|
|
1471
|
+
parentArchiveRoot: entry.parentArchiveRoot.toBuffer(),
|
|
1472
|
+
slotNumber: entry.slotNumber,
|
|
1473
|
+
l1: entry.l1.toBuffer(),
|
|
1474
|
+
reason: entry.reason,
|
|
1475
|
+
});
|
|
1476
|
+
await this.#rejectedCheckpointsByNumber.set(entry.checkpointNumber, archiveRootHex);
|
|
1477
|
+
await this.advanceSynchedL1BlockNumber(entry.l1.blockNumber);
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
/** Returns the rejected-checkpoint entry with the given archive root, or undefined if not present. */
|
|
1481
|
+
async getRejectedCheckpointByArchiveRoot(archiveRoot: Fr): Promise<RejectedCheckpoint | undefined> {
|
|
1482
|
+
const stored = await this.#rejectedCheckpoints.getAsync(archiveRoot.toString());
|
|
1483
|
+
return stored ? this.rejectedCheckpointFromStorage(stored) : undefined;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
/** Returns the rejected-checkpoint entry recorded for the given checkpoint number, or undefined if none. */
|
|
1487
|
+
async getRejectedCheckpointByNumber(checkpointNumber: CheckpointNumber): Promise<RejectedCheckpoint | undefined> {
|
|
1488
|
+
const archiveRootHex = await this.#rejectedCheckpointsByNumber.getAsync(checkpointNumber);
|
|
1489
|
+
if (archiveRootHex === undefined) {
|
|
1490
|
+
return undefined;
|
|
1491
|
+
}
|
|
1492
|
+
const stored = await this.#rejectedCheckpoints.getAsync(archiveRootHex);
|
|
1493
|
+
return stored ? this.rejectedCheckpointFromStorage(stored) : undefined;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/** Returns the highest checkpoint number recorded across all rejected entries, or `INITIAL_CHECKPOINT_NUMBER - 1` if none. */
|
|
1497
|
+
async getLatestRejectedCheckpointNumber(): Promise<CheckpointNumber> {
|
|
1498
|
+
const [latest] = await toArray(this.#rejectedCheckpointsByNumber.keysAsync({ reverse: true, limit: 1 }));
|
|
1499
|
+
return CheckpointNumber(latest ?? INITIAL_CHECKPOINT_NUMBER - 1);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
/** Removes a rejected-checkpoint entry by its archive root (used when an entry no longer matches L1). */
|
|
1503
|
+
async removeRejectedCheckpointByArchiveRoot(archiveRoot: Fr): Promise<void> {
|
|
1504
|
+
const archiveRootHex = archiveRoot.toString();
|
|
1505
|
+
const stored = await this.#rejectedCheckpoints.getAsync(archiveRootHex);
|
|
1506
|
+
await this.#rejectedCheckpoints.delete(archiveRootHex);
|
|
1507
|
+
if (stored) {
|
|
1508
|
+
// Only clear the by-number index if it still points at this archive root, so a distinct
|
|
1509
|
+
// entry that shares the checkpoint number (e.g. an L1 reorg replacement) is not dropped.
|
|
1510
|
+
const indexed = await this.#rejectedCheckpointsByNumber.getAsync(stored.checkpointNumber);
|
|
1511
|
+
if (indexed === archiveRootHex) {
|
|
1512
|
+
await this.#rejectedCheckpointsByNumber.delete(stored.checkpointNumber);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
/**
|
|
1518
|
+
* Advances the stored last-synched L1 block number to `l1BlockNumber` only if it is strictly
|
|
1519
|
+
* greater than the current value. Use this whenever ingesting checkpoint-shaped data so the
|
|
1520
|
+
* sync pointer never walks backwards on out-of-order writes (e.g. an invalid checkpoint
|
|
1521
|
+
* advance followed by a valid-checkpoint commit landing at an earlier L1 block).
|
|
1522
|
+
*/
|
|
1523
|
+
private async advanceSynchedL1BlockNumber(l1BlockNumber: bigint): Promise<void> {
|
|
1524
|
+
const current = await this.#lastSynchedL1Block.getAsync();
|
|
1525
|
+
if (current === undefined || l1BlockNumber > current) {
|
|
1526
|
+
await this.#lastSynchedL1Block.set(l1BlockNumber);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
private rejectedCheckpointFromStorage(stored: RejectedCheckpointStorage): RejectedCheckpoint {
|
|
1531
|
+
return {
|
|
1532
|
+
checkpointNumber: CheckpointNumber(stored.checkpointNumber),
|
|
1533
|
+
archiveRoot: Fr.fromBuffer(stored.archiveRoot),
|
|
1534
|
+
parentArchiveRoot: Fr.fromBuffer(stored.parentArchiveRoot),
|
|
1535
|
+
slotNumber: SlotNumber(stored.slotNumber),
|
|
1536
|
+
l1: L1PublishedData.fromBuffer(stored.l1),
|
|
1537
|
+
reason: stored.reason,
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1297
1540
|
}
|