@aztec/archiver 0.0.1-commit.b1c78909e → 0.0.1-commit.b2a5d0dd1
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 +12 -6
- package/dest/archiver.d.ts +7 -5
- package/dest/archiver.d.ts.map +1 -1
- package/dest/archiver.js +39 -15
- package/dest/config.d.ts +5 -3
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +15 -3
- package/dest/errors.d.ts +44 -2
- package/dest/errors.d.ts.map +1 -1
- package/dest/errors.js +58 -2
- package/dest/factory.d.ts +2 -2
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +18 -14
- package/dest/index.d.ts +3 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +2 -1
- package/dest/l1/calldata_retriever.d.ts +1 -1
- package/dest/l1/calldata_retriever.d.ts.map +1 -1
- package/dest/l1/calldata_retriever.js +2 -1
- package/dest/l1/data_retrieval.d.ts +19 -10
- package/dest/l1/data_retrieval.d.ts.map +1 -1
- package/dest/l1/data_retrieval.js +25 -32
- package/dest/l1/validate_historical_logs.d.ts +23 -0
- package/dest/l1/validate_historical_logs.d.ts.map +1 -0
- package/dest/l1/validate_historical_logs.js +108 -0
- package/dest/modules/data_source_base.d.ts +6 -4
- package/dest/modules/data_source_base.d.ts.map +1 -1
- package/dest/modules/data_source_base.js +11 -5
- package/dest/modules/data_store_updater.d.ts +15 -10
- package/dest/modules/data_store_updater.d.ts.map +1 -1
- package/dest/modules/data_store_updater.js +69 -68
- package/dest/modules/instrumentation.d.ts +7 -2
- package/dest/modules/instrumentation.d.ts.map +1 -1
- package/dest/modules/instrumentation.js +22 -6
- package/dest/modules/l1_synchronizer.d.ts +6 -2
- package/dest/modules/l1_synchronizer.d.ts.map +1 -1
- package/dest/modules/l1_synchronizer.js +244 -146
- 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 +49 -6
- package/dest/store/block_store.d.ts.map +1 -1
- package/dest/store/block_store.js +277 -67
- 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 +7 -67
- 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 +39 -18
- package/dest/store/kv_archiver_store.d.ts.map +1 -1
- package/dest/store/kv_archiver_store.js +45 -20
- package/dest/store/l2_tips_cache.d.ts +2 -1
- package/dest/store/l2_tips_cache.d.ts.map +1 -1
- package/dest/store/l2_tips_cache.js +27 -7
- package/dest/store/log_store.d.ts +6 -3
- package/dest/store/log_store.d.ts.map +1 -1
- package/dest/store/log_store.js +95 -20
- 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 +20 -8
- package/dest/test/fake_l1_state.d.ts +14 -3
- package/dest/test/fake_l1_state.d.ts.map +1 -1
- package/dest/test/fake_l1_state.js +63 -10
- 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 +7 -2
- package/dest/test/mock_l2_block_source.d.ts.map +1 -1
- package/dest/test/mock_l2_block_source.js +30 -5
- package/dest/test/noop_l1_archiver.d.ts +1 -1
- package/dest/test/noop_l1_archiver.d.ts.map +1 -1
- package/dest/test/noop_l1_archiver.js +4 -2
- package/package.json +13 -13
- package/src/archiver.ts +57 -18
- package/src/config.ts +22 -2
- package/src/errors.ts +94 -2
- package/src/factory.ts +18 -10
- package/src/index.ts +2 -1
- package/src/l1/calldata_retriever.ts +2 -1
- package/src/l1/data_retrieval.ts +36 -45
- package/src/l1/validate_historical_logs.ts +140 -0
- package/src/modules/data_source_base.ts +24 -5
- package/src/modules/data_store_updater.ts +94 -97
- package/src/modules/instrumentation.ts +27 -7
- package/src/modules/l1_synchronizer.ts +314 -177
- package/src/modules/validation.ts +2 -2
- package/src/store/block_store.ts +364 -76
- package/src/store/contract_class_store.ts +8 -106
- package/src/store/contract_instance_store.ts +8 -5
- package/src/store/kv_archiver_store.ts +77 -34
- package/src/store/l2_tips_cache.ts +58 -13
- package/src/store/log_store.ts +128 -32
- package/src/store/message_store.ts +26 -9
- package/src/structs/inbox_message.ts +1 -1
- package/src/test/fake_l1_state.ts +87 -15
- package/src/test/mock_l1_to_l2_message_source.ts +1 -0
- package/src/test/mock_l2_block_source.ts +44 -3
- package/src/test/noop_l1_archiver.ts +3 -1
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
getAttestationInfoFromPayload,
|
|
10
10
|
} from '@aztec/stdlib/block';
|
|
11
11
|
import type { PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
12
|
-
import { type L1RollupConstants, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
12
|
+
import { type L1RollupConstants, computeQuorum, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
13
13
|
import { ConsensusPayload } from '@aztec/stdlib/p2p';
|
|
14
14
|
|
|
15
15
|
export type { ValidateCheckpointResult };
|
|
@@ -66,7 +66,7 @@ export async function validateCheckpointAttestations(
|
|
|
66
66
|
return { valid: true };
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
const requiredAttestationCount =
|
|
69
|
+
const requiredAttestationCount = computeQuorum(committee.length);
|
|
70
70
|
|
|
71
71
|
const failedValidationResult = <TReason extends ValidateCheckpointNegativeResult['reason']>(reason: TReason) => ({
|
|
72
72
|
valid: false as const,
|
package/src/store/block_store.ts
CHANGED
|
@@ -19,7 +19,16 @@ import {
|
|
|
19
19
|
deserializeValidateCheckpointResult,
|
|
20
20
|
serializeValidateCheckpointResult,
|
|
21
21
|
} from '@aztec/stdlib/block';
|
|
22
|
-
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';
|
|
23
32
|
import { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
24
33
|
import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
|
|
25
34
|
import {
|
|
@@ -43,6 +52,11 @@ import {
|
|
|
43
52
|
CheckpointNotFoundError,
|
|
44
53
|
CheckpointNumberNotSequentialError,
|
|
45
54
|
InitialCheckpointNumberNotSequentialError,
|
|
55
|
+
NoProposedCheckpointToPromoteError,
|
|
56
|
+
ProposedCheckpointArchiveRootMismatchError,
|
|
57
|
+
ProposedCheckpointNotSequentialError,
|
|
58
|
+
ProposedCheckpointPromotionNotSequentialError,
|
|
59
|
+
ProposedCheckpointStaleError,
|
|
46
60
|
} from '../errors.js';
|
|
47
61
|
|
|
48
62
|
export { TxReceipt, type TxEffect, type TxHash } from '@aztec/stdlib/tx';
|
|
@@ -57,17 +71,27 @@ type BlockStorage = {
|
|
|
57
71
|
indexWithinCheckpoint: number;
|
|
58
72
|
};
|
|
59
73
|
|
|
60
|
-
|
|
74
|
+
/** Checkpoint Storage shared between Checkpoints + Proposed Checkpoints */
|
|
75
|
+
type CommonCheckpointStorage = {
|
|
61
76
|
header: Buffer;
|
|
62
77
|
archive: Buffer;
|
|
63
78
|
checkpointOutHash: Buffer;
|
|
64
79
|
checkpointNumber: number;
|
|
65
80
|
startBlock: number;
|
|
66
81
|
blockCount: number;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
type CheckpointStorage = CommonCheckpointStorage & {
|
|
67
85
|
l1: Buffer;
|
|
68
86
|
attestations: Buffer[];
|
|
69
87
|
};
|
|
70
88
|
|
|
89
|
+
/** Storage format for a proposed checkpoint (attested but not yet L1-confirmed). */
|
|
90
|
+
type ProposedCheckpointStorage = CommonCheckpointStorage & {
|
|
91
|
+
totalManaUsed: string;
|
|
92
|
+
feeAssetPriceModifier: string;
|
|
93
|
+
};
|
|
94
|
+
|
|
71
95
|
export type RemoveCheckpointsResult = { blocksRemoved: L2Block[] | undefined };
|
|
72
96
|
|
|
73
97
|
/**
|
|
@@ -110,6 +134,9 @@ export class BlockStore {
|
|
|
110
134
|
/** Index mapping block archive to block number */
|
|
111
135
|
#blockArchiveIndex: AztecAsyncMap<string, number>;
|
|
112
136
|
|
|
137
|
+
/** Singleton: assumes max 1-deep pipeline. For deeper pipelining, replace with a map keyed by checkpoint number. */
|
|
138
|
+
#proposedCheckpoint: AztecAsyncSingleton<ProposedCheckpointStorage>;
|
|
139
|
+
|
|
113
140
|
#log = createLogger('archiver:block_store');
|
|
114
141
|
|
|
115
142
|
constructor(private db: AztecAsyncKVStore) {
|
|
@@ -125,6 +152,7 @@ export class BlockStore {
|
|
|
125
152
|
this.#pendingChainValidationStatus = db.openSingleton('archiver_pending_chain_validation_status');
|
|
126
153
|
this.#checkpoints = db.openMap('archiver_checkpoints');
|
|
127
154
|
this.#slotToCheckpoint = db.openMap('archiver_slot_to_checkpoint');
|
|
155
|
+
this.#proposedCheckpoint = db.openSingleton('proposed_checkpoint_data');
|
|
128
156
|
}
|
|
129
157
|
|
|
130
158
|
/**
|
|
@@ -159,7 +187,8 @@ export class BlockStore {
|
|
|
159
187
|
const blockLastArchive = block.header.lastArchive.root;
|
|
160
188
|
|
|
161
189
|
// Extract the latest block and checkpoint numbers
|
|
162
|
-
const previousBlockNumber = await this.
|
|
190
|
+
const previousBlockNumber = await this.getLatestL2BlockNumber();
|
|
191
|
+
const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
|
|
163
192
|
const previousCheckpointNumber = await this.getLatestCheckpointNumber();
|
|
164
193
|
|
|
165
194
|
// Verify we're not overwriting checkpointed blocks
|
|
@@ -178,9 +207,19 @@ export class BlockStore {
|
|
|
178
207
|
throw new BlockNumberNotSequentialError(blockNumber, previousBlockNumber);
|
|
179
208
|
}
|
|
180
209
|
|
|
181
|
-
// The same check as above but for checkpoints
|
|
182
|
-
|
|
183
|
-
|
|
210
|
+
// The same check as above but for checkpoints. Accept the block if either the confirmed
|
|
211
|
+
// checkpoint or the pending (locally validated but not yet confirmed) checkpoint matches.
|
|
212
|
+
const expectedCheckpointNumber = blockCheckpointNumber - 1;
|
|
213
|
+
if (
|
|
214
|
+
!opts.force &&
|
|
215
|
+
previousCheckpointNumber !== expectedCheckpointNumber &&
|
|
216
|
+
proposedCheckpointNumber !== expectedCheckpointNumber
|
|
217
|
+
) {
|
|
218
|
+
const [reported, source]: [CheckpointNumber, 'confirmed' | 'proposed'] =
|
|
219
|
+
proposedCheckpointNumber > previousCheckpointNumber
|
|
220
|
+
? [proposedCheckpointNumber, 'proposed']
|
|
221
|
+
: [previousCheckpointNumber, 'confirmed'];
|
|
222
|
+
throw new CheckpointNumberNotSequentialError(blockCheckpointNumber, reported, source);
|
|
184
223
|
}
|
|
185
224
|
|
|
186
225
|
// Extract the previous block if there is one and see if it is for the same checkpoint or not
|
|
@@ -216,7 +255,7 @@ export class BlockStore {
|
|
|
216
255
|
}
|
|
217
256
|
|
|
218
257
|
/**
|
|
219
|
-
* Append new
|
|
258
|
+
* Append new checkpoints to the store's list.
|
|
220
259
|
* @param checkpoints - The L2 checkpoints to be added to the store.
|
|
221
260
|
* @returns True if the operation is successful.
|
|
222
261
|
*/
|
|
@@ -226,37 +265,29 @@ export class BlockStore {
|
|
|
226
265
|
}
|
|
227
266
|
|
|
228
267
|
return await this.db.transactionAsync(async () => {
|
|
229
|
-
// Check that the checkpoint immediately before the first block to be added is present in the store.
|
|
230
268
|
const firstCheckpointNumber = checkpoints[0].checkpoint.number;
|
|
231
269
|
const previousCheckpointNumber = await this.getLatestCheckpointNumber();
|
|
232
270
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
// There should be a previous checkpoint
|
|
241
|
-
previousCheckpointData = await this.getCheckpointData(previousCheckpointNumber);
|
|
242
|
-
if (previousCheckpointData === undefined) {
|
|
243
|
-
throw new CheckpointNotFoundError(previousCheckpointNumber);
|
|
271
|
+
// Handle already-stored checkpoints at the start of the batch.
|
|
272
|
+
// This can happen after an L1 reorg re-includes a checkpoint in a different L1 block.
|
|
273
|
+
// We accept them if archives match (same content) and update their L1 metadata.
|
|
274
|
+
if (!opts.force && firstCheckpointNumber <= previousCheckpointNumber) {
|
|
275
|
+
checkpoints = await this.skipOrUpdateAlreadyStoredCheckpoints(checkpoints, previousCheckpointNumber);
|
|
276
|
+
if (checkpoints.length === 0) {
|
|
277
|
+
return true;
|
|
244
278
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// If we have a previous checkpoint then we need to get the previous block number
|
|
251
|
-
if (previousCheckpointData !== undefined) {
|
|
252
|
-
previousBlockNumber = BlockNumber(previousCheckpointData.startBlock + previousCheckpointData.blockCount - 1);
|
|
253
|
-
previousBlock = await this.getBlock(previousBlockNumber);
|
|
254
|
-
if (previousBlock === undefined) {
|
|
255
|
-
// We should be able to get the required previous block
|
|
256
|
-
throw new BlockNotFoundError(previousBlockNumber);
|
|
279
|
+
// Re-check sequentiality after skipping
|
|
280
|
+
const newFirstNumber = checkpoints[0].checkpoint.number;
|
|
281
|
+
if (previousCheckpointNumber !== newFirstNumber - 1) {
|
|
282
|
+
throw new InitialCheckpointNumberNotSequentialError(newFirstNumber, previousCheckpointNumber);
|
|
257
283
|
}
|
|
284
|
+
} else if (previousCheckpointNumber !== firstCheckpointNumber - 1 && !opts.force) {
|
|
285
|
+
throw new InitialCheckpointNumberNotSequentialError(firstCheckpointNumber, previousCheckpointNumber);
|
|
258
286
|
}
|
|
259
287
|
|
|
288
|
+
// Get the last block of the previous checkpoint for archive chaining
|
|
289
|
+
let previousBlock = await this.getPreviousCheckpointBlock(checkpoints[0].checkpoint.number);
|
|
290
|
+
|
|
260
291
|
// Iterate over checkpoints array and insert them, checking that the block numbers are sequential.
|
|
261
292
|
let previousCheckpoint: PublishedCheckpoint | undefined = undefined;
|
|
262
293
|
for (const checkpoint of checkpoints) {
|
|
@@ -272,42 +303,14 @@ export class BlockStore {
|
|
|
272
303
|
}
|
|
273
304
|
previousCheckpoint = checkpoint;
|
|
274
305
|
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
const block = checkpoint.checkpoint.blocks[i];
|
|
278
|
-
if (previousBlock) {
|
|
279
|
-
// The blocks should have a sequential block number
|
|
280
|
-
if (previousBlock.number !== block.number - 1) {
|
|
281
|
-
throw new BlockNumberNotSequentialError(block.number, previousBlock.number);
|
|
282
|
-
}
|
|
283
|
-
// If the blocks are for the same checkpoint then they should have sequential indexes
|
|
284
|
-
if (
|
|
285
|
-
previousBlock.checkpointNumber === block.checkpointNumber &&
|
|
286
|
-
previousBlock.indexWithinCheckpoint !== block.indexWithinCheckpoint - 1
|
|
287
|
-
) {
|
|
288
|
-
throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint);
|
|
289
|
-
}
|
|
290
|
-
if (!previousBlock.archive.root.equals(block.header.lastArchive.root)) {
|
|
291
|
-
throw new BlockArchiveNotConsistentError(
|
|
292
|
-
block.number,
|
|
293
|
-
previousBlock.number,
|
|
294
|
-
block.header.lastArchive.root,
|
|
295
|
-
previousBlock.archive.root,
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
} else {
|
|
299
|
-
// No previous block, must be block 1 at checkpoint index 0
|
|
300
|
-
if (block.indexWithinCheckpoint !== 0) {
|
|
301
|
-
throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, undefined);
|
|
302
|
-
}
|
|
303
|
-
if (block.number !== INITIAL_L2_BLOCK_NUM) {
|
|
304
|
-
throw new BlockNumberNotSequentialError(block.number, undefined);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
306
|
+
// Validate block sequencing, indexes, and archive chaining
|
|
307
|
+
this.validateCheckpointBlocks(checkpoint.checkpoint.blocks, previousBlock);
|
|
307
308
|
|
|
308
|
-
|
|
309
|
-
|
|
309
|
+
// Store every block in the database (may already exist, but L1 data is authoritative)
|
|
310
|
+
for (let i = 0; i < checkpoint.checkpoint.blocks.length; i++) {
|
|
311
|
+
await this.addBlockToDatabase(checkpoint.checkpoint.blocks[i], checkpoint.checkpoint.number, i);
|
|
310
312
|
}
|
|
313
|
+
previousBlock = checkpoint.checkpoint.blocks.at(-1);
|
|
311
314
|
|
|
312
315
|
// Store the checkpoint in the database
|
|
313
316
|
await this.#checkpoints.set(checkpoint.checkpoint.number, {
|
|
@@ -325,11 +328,121 @@ export class BlockStore {
|
|
|
325
328
|
await this.#slotToCheckpoint.set(checkpoint.checkpoint.header.slotNumber, checkpoint.checkpoint.number);
|
|
326
329
|
}
|
|
327
330
|
|
|
331
|
+
// Clear the proposed checkpoint if any of the confirmed checkpoints match or supersede it
|
|
332
|
+
const lastConfirmedCheckpointNumber = checkpoints[checkpoints.length - 1].checkpoint.number;
|
|
333
|
+
await this.clearProposedCheckpointIfSuperseded(lastConfirmedCheckpointNumber);
|
|
334
|
+
|
|
328
335
|
await this.#lastSynchedL1Block.set(checkpoints[checkpoints.length - 1].l1.blockNumber);
|
|
329
336
|
return true;
|
|
330
337
|
});
|
|
331
338
|
}
|
|
332
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Handles checkpoints at the start of a batch that are already stored (e.g. due to L1 reorg).
|
|
342
|
+
* Verifies the archive root matches, updates L1 metadata, and returns only the new checkpoints.
|
|
343
|
+
*/
|
|
344
|
+
private async skipOrUpdateAlreadyStoredCheckpoints(
|
|
345
|
+
checkpoints: PublishedCheckpoint[],
|
|
346
|
+
latestStored: CheckpointNumber,
|
|
347
|
+
): Promise<PublishedCheckpoint[]> {
|
|
348
|
+
let i = 0;
|
|
349
|
+
for (; i < checkpoints.length && checkpoints[i].checkpoint.number <= latestStored; i++) {
|
|
350
|
+
const incoming = checkpoints[i];
|
|
351
|
+
const stored = await this.getCheckpointData(incoming.checkpoint.number);
|
|
352
|
+
if (!stored) {
|
|
353
|
+
// Should not happen if latestStored is correct, but be safe
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
// Verify the checkpoint content matches (archive root)
|
|
357
|
+
if (!stored.archive.root.equals(incoming.checkpoint.archive.root)) {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`Checkpoint ${incoming.checkpoint.number} already exists in store but with a different archive root. ` +
|
|
360
|
+
`Stored: ${stored.archive.root}, incoming: ${incoming.checkpoint.archive.root}`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
// Update L1 metadata and attestations for the already-stored checkpoint
|
|
364
|
+
this.#log.warn(
|
|
365
|
+
`Checkpoint ${incoming.checkpoint.number} already stored, updating L1 info ` +
|
|
366
|
+
`(L1 block ${stored.l1.blockNumber} -> ${incoming.l1.blockNumber})`,
|
|
367
|
+
);
|
|
368
|
+
await this.#checkpoints.set(incoming.checkpoint.number, {
|
|
369
|
+
header: incoming.checkpoint.header.toBuffer(),
|
|
370
|
+
archive: incoming.checkpoint.archive.toBuffer(),
|
|
371
|
+
checkpointOutHash: incoming.checkpoint.getCheckpointOutHash().toBuffer(),
|
|
372
|
+
l1: incoming.l1.toBuffer(),
|
|
373
|
+
attestations: incoming.attestations.map(a => a.toBuffer()),
|
|
374
|
+
checkpointNumber: incoming.checkpoint.number,
|
|
375
|
+
startBlock: incoming.checkpoint.blocks[0].number,
|
|
376
|
+
blockCount: incoming.checkpoint.blocks.length,
|
|
377
|
+
});
|
|
378
|
+
// Update the sync point to reflect the new L1 block
|
|
379
|
+
await this.#lastSynchedL1Block.set(incoming.l1.blockNumber);
|
|
380
|
+
}
|
|
381
|
+
return checkpoints.slice(i);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Gets the last block of the checkpoint before the given one.
|
|
386
|
+
* Returns undefined if there is no previous checkpoint (i.e. genesis).
|
|
387
|
+
*/
|
|
388
|
+
private async getPreviousCheckpointBlock(checkpointNumber: CheckpointNumber): Promise<L2Block | undefined> {
|
|
389
|
+
const previousCheckpointNumber = CheckpointNumber(checkpointNumber - 1);
|
|
390
|
+
if (previousCheckpointNumber === INITIAL_CHECKPOINT_NUMBER - 1) {
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const previousCheckpointData = await this.getCheckpointData(previousCheckpointNumber);
|
|
395
|
+
if (previousCheckpointData === undefined) {
|
|
396
|
+
throw new CheckpointNotFoundError(previousCheckpointNumber);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const previousBlockNumber = BlockNumber(previousCheckpointData.startBlock + previousCheckpointData.blockCount - 1);
|
|
400
|
+
const previousBlock = await this.getBlock(previousBlockNumber);
|
|
401
|
+
if (previousBlock === undefined) {
|
|
402
|
+
throw new BlockNotFoundError(previousBlockNumber);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return previousBlock;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Validates that blocks are sequential, have correct indexes, and chain via archive roots.
|
|
410
|
+
* This is the same validation used for both confirmed checkpoints (addCheckpoints) and
|
|
411
|
+
* proposed checkpoints (setProposedCheckpoint).
|
|
412
|
+
*/
|
|
413
|
+
private validateCheckpointBlocks(blocks: L2Block[], previousBlock: L2Block | undefined): void {
|
|
414
|
+
for (const block of blocks) {
|
|
415
|
+
if (previousBlock) {
|
|
416
|
+
if (previousBlock.number !== block.number - 1) {
|
|
417
|
+
throw new BlockNumberNotSequentialError(block.number, previousBlock.number);
|
|
418
|
+
}
|
|
419
|
+
if (previousBlock.checkpointNumber === block.checkpointNumber) {
|
|
420
|
+
if (previousBlock.indexWithinCheckpoint !== block.indexWithinCheckpoint - 1) {
|
|
421
|
+
throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint);
|
|
422
|
+
}
|
|
423
|
+
} else if (block.indexWithinCheckpoint !== 0) {
|
|
424
|
+
throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint);
|
|
425
|
+
}
|
|
426
|
+
if (!previousBlock.archive.root.equals(block.header.lastArchive.root)) {
|
|
427
|
+
throw new BlockArchiveNotConsistentError(
|
|
428
|
+
block.number,
|
|
429
|
+
previousBlock.number,
|
|
430
|
+
block.header.lastArchive.root,
|
|
431
|
+
previousBlock.archive.root,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
if (block.indexWithinCheckpoint !== 0) {
|
|
436
|
+
throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, undefined);
|
|
437
|
+
}
|
|
438
|
+
if (block.number !== INITIAL_L2_BLOCK_NUM) {
|
|
439
|
+
throw new BlockNumberNotSequentialError(block.number, undefined);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
previousBlock = block;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
333
446
|
private async addBlockToDatabase(block: L2Block, checkpointNumber: number, indexWithinCheckpoint: number) {
|
|
334
447
|
const blockHash = await block.hash();
|
|
335
448
|
|
|
@@ -422,6 +535,12 @@ export class BlockStore {
|
|
|
422
535
|
this.#log.debug(`Removed checkpoint ${c}`);
|
|
423
536
|
}
|
|
424
537
|
|
|
538
|
+
// Clear any proposed checkpoint that was orphaned by the removal (its base chain no longer exists)
|
|
539
|
+
const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
|
|
540
|
+
if (proposedCheckpointNumber > checkpointNumber) {
|
|
541
|
+
await this.#proposedCheckpoint.delete();
|
|
542
|
+
}
|
|
543
|
+
|
|
425
544
|
return { blocksRemoved };
|
|
426
545
|
});
|
|
427
546
|
}
|
|
@@ -527,7 +646,7 @@ export class BlockStore {
|
|
|
527
646
|
const removedBlocks: L2Block[] = [];
|
|
528
647
|
|
|
529
648
|
// Get the latest block number to determine the range
|
|
530
|
-
const latestBlockNumber = await this.
|
|
649
|
+
const latestBlockNumber = await this.getLatestL2BlockNumber();
|
|
531
650
|
|
|
532
651
|
// Iterate from blockNumber + 1 to latestBlockNumber
|
|
533
652
|
for (let bn = blockNumber + 1; bn <= latestBlockNumber; bn++) {
|
|
@@ -560,13 +679,6 @@ export class BlockStore {
|
|
|
560
679
|
}
|
|
561
680
|
}
|
|
562
681
|
|
|
563
|
-
async getLatestBlockNumber(): Promise<BlockNumber> {
|
|
564
|
-
const [latestBlocknumber] = await toArray(this.#blocks.keysAsync({ reverse: true, limit: 1 }));
|
|
565
|
-
return typeof latestBlocknumber === 'number'
|
|
566
|
-
? BlockNumber(latestBlocknumber)
|
|
567
|
-
: BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
682
|
async getLatestCheckpointNumber(): Promise<CheckpointNumber> {
|
|
571
683
|
const [latestCheckpointNumber] = await toArray(this.#checkpoints.keysAsync({ reverse: true, limit: 1 }));
|
|
572
684
|
if (latestCheckpointNumber === undefined) {
|
|
@@ -575,6 +687,133 @@ export class BlockStore {
|
|
|
575
687
|
return CheckpointNumber(latestCheckpointNumber);
|
|
576
688
|
}
|
|
577
689
|
|
|
690
|
+
async hasProposedCheckpoint(): Promise<boolean> {
|
|
691
|
+
const proposed = await this.#proposedCheckpoint.getAsync();
|
|
692
|
+
return proposed !== undefined;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/** Deletes the proposed checkpoint from storage. */
|
|
696
|
+
async deleteProposedCheckpoint(): Promise<void> {
|
|
697
|
+
await this.#proposedCheckpoint.delete();
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Promotes the proposed checkpoint singleton to a confirmed checkpoint entry.
|
|
702
|
+
* This persists the checkpoint to the store, clears the proposed singleton, and updates the L1 sync point.
|
|
703
|
+
* Should only be called after the checkpoint has been validated.
|
|
704
|
+
* @param expectedArchiveRoot - The archive root to match against the proposed checkpoint, to guard against races.
|
|
705
|
+
*/
|
|
706
|
+
async promoteProposedToCheckpointed(
|
|
707
|
+
l1: L1PublishedData,
|
|
708
|
+
attestations: CommitteeAttestation[],
|
|
709
|
+
expectedArchiveRoot: Fr,
|
|
710
|
+
): Promise<void> {
|
|
711
|
+
return await this.db.transactionAsync(async () => {
|
|
712
|
+
const proposed = await this.getProposedCheckpointOnly();
|
|
713
|
+
if (!proposed) {
|
|
714
|
+
throw new NoProposedCheckpointToPromoteError();
|
|
715
|
+
}
|
|
716
|
+
if (!proposed.archive.root.equals(expectedArchiveRoot)) {
|
|
717
|
+
throw new ProposedCheckpointArchiveRootMismatchError(expectedArchiveRoot, proposed.archive.root);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Verify sequentiality: promoted checkpoint must follow the latest confirmed one
|
|
721
|
+
const latestCheckpointNumber = await this.getLatestCheckpointNumber();
|
|
722
|
+
if (latestCheckpointNumber !== proposed.checkpointNumber - 1) {
|
|
723
|
+
throw new ProposedCheckpointPromotionNotSequentialError(proposed.checkpointNumber, latestCheckpointNumber);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Write the checkpoint entry
|
|
727
|
+
await this.#checkpoints.set(proposed.checkpointNumber, {
|
|
728
|
+
header: proposed.header.toBuffer(),
|
|
729
|
+
archive: proposed.archive.toBuffer(),
|
|
730
|
+
checkpointOutHash: proposed.checkpointOutHash.toBuffer(),
|
|
731
|
+
l1: l1.toBuffer(),
|
|
732
|
+
attestations: attestations.map(attestation => attestation.toBuffer()),
|
|
733
|
+
checkpointNumber: proposed.checkpointNumber,
|
|
734
|
+
startBlock: proposed.startBlock,
|
|
735
|
+
blockCount: proposed.blockCount,
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
// Update the slot-to-checkpoint index
|
|
739
|
+
await this.#slotToCheckpoint.set(proposed.header.slotNumber, proposed.checkpointNumber);
|
|
740
|
+
|
|
741
|
+
// Clear the proposed checkpoint singleton
|
|
742
|
+
await this.#proposedCheckpoint.delete();
|
|
743
|
+
|
|
744
|
+
// Update the last synced L1 block
|
|
745
|
+
await this.#lastSynchedL1Block.set(l1.blockNumber);
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/** Clears the proposed checkpoint if the given confirmed checkpoint number supersedes it. */
|
|
750
|
+
async clearProposedCheckpointIfSuperseded(confirmedCheckpointNumber: CheckpointNumber): Promise<void> {
|
|
751
|
+
const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
|
|
752
|
+
if (proposedCheckpointNumber <= confirmedCheckpointNumber) {
|
|
753
|
+
await this.#proposedCheckpoint.delete();
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** Returns the proposed checkpoint data, or undefined if no proposed checkpoint exists. No fallback to confirmed. */
|
|
758
|
+
async getProposedCheckpointOnly(): Promise<ProposedCheckpointData | undefined> {
|
|
759
|
+
const stored = await this.#proposedCheckpoint.getAsync();
|
|
760
|
+
if (!stored) {
|
|
761
|
+
return undefined;
|
|
762
|
+
}
|
|
763
|
+
return this.convertToProposedCheckpointData(stored);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Gets the checkpoint at the proposed tip
|
|
768
|
+
* - pending checkpoint if it exists
|
|
769
|
+
* - fallsback to latest confirmed checkpoint otherwise
|
|
770
|
+
* @returns CommonCheckpointData
|
|
771
|
+
*/
|
|
772
|
+
async getProposedCheckpoint(): Promise<CommonCheckpointData | undefined> {
|
|
773
|
+
const stored = await this.#proposedCheckpoint.getAsync();
|
|
774
|
+
if (!stored) {
|
|
775
|
+
return this.getCheckpointData(await this.getLatestCheckpointNumber());
|
|
776
|
+
}
|
|
777
|
+
return this.convertToProposedCheckpointData(stored);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
private convertToProposedCheckpointData(stored: ProposedCheckpointStorage): ProposedCheckpointData {
|
|
781
|
+
return {
|
|
782
|
+
checkpointNumber: CheckpointNumber(stored.checkpointNumber),
|
|
783
|
+
header: CheckpointHeader.fromBuffer(stored.header),
|
|
784
|
+
archive: AppendOnlyTreeSnapshot.fromBuffer(stored.archive),
|
|
785
|
+
checkpointOutHash: Fr.fromBuffer(stored.checkpointOutHash),
|
|
786
|
+
startBlock: BlockNumber(stored.startBlock),
|
|
787
|
+
blockCount: stored.blockCount,
|
|
788
|
+
totalManaUsed: BigInt(stored.totalManaUsed),
|
|
789
|
+
feeAssetPriceModifier: BigInt(stored.feeAssetPriceModifier),
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Attempts to get the proposedCheckpoint's number, if there is not one, then fallback to the latest confirmed checkpoint number.
|
|
795
|
+
* @returns CheckpointNumber
|
|
796
|
+
*/
|
|
797
|
+
async getProposedCheckpointNumber(): Promise<CheckpointNumber> {
|
|
798
|
+
const proposed = await this.getProposedCheckpoint();
|
|
799
|
+
if (!proposed) {
|
|
800
|
+
return await this.getLatestCheckpointNumber();
|
|
801
|
+
}
|
|
802
|
+
return CheckpointNumber(proposed.checkpointNumber);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Attempts to get the proposedCheckpoint's block number, if there is not one, then fallback to the checkpointed block number
|
|
807
|
+
* @returns BlockNumber
|
|
808
|
+
*/
|
|
809
|
+
async getProposedCheckpointL2BlockNumber(): Promise<BlockNumber> {
|
|
810
|
+
const proposed = await this.getProposedCheckpoint();
|
|
811
|
+
if (!proposed) {
|
|
812
|
+
return await this.getCheckpointedL2BlockNumber();
|
|
813
|
+
}
|
|
814
|
+
return BlockNumber(proposed.startBlock + proposed.blockCount - 1);
|
|
815
|
+
}
|
|
816
|
+
|
|
578
817
|
async getCheckpointedBlock(number: BlockNumber): Promise<CheckpointedL2Block | undefined> {
|
|
579
818
|
const blockStorage = await this.#blocks.getAsync(number);
|
|
580
819
|
if (!blockStorage) {
|
|
@@ -789,7 +1028,7 @@ export class BlockStore {
|
|
|
789
1028
|
return {
|
|
790
1029
|
header: BlockHeader.fromBuffer(blockStorage.header),
|
|
791
1030
|
archive: AppendOnlyTreeSnapshot.fromBuffer(blockStorage.archive),
|
|
792
|
-
blockHash:
|
|
1031
|
+
blockHash: BlockHash.fromBuffer(blockStorage.blockHash),
|
|
793
1032
|
checkpointNumber: CheckpointNumber(blockStorage.checkpointNumber),
|
|
794
1033
|
indexWithinCheckpoint: IndexWithinCheckpoint(blockStorage.indexWithinCheckpoint),
|
|
795
1034
|
};
|
|
@@ -851,7 +1090,10 @@ export class BlockStore {
|
|
|
851
1090
|
* @param txHash - The hash of a tx we try to get the receipt for.
|
|
852
1091
|
* @returns The requested tx receipt (or undefined if not found).
|
|
853
1092
|
*/
|
|
854
|
-
async getSettledTxReceipt(
|
|
1093
|
+
async getSettledTxReceipt(
|
|
1094
|
+
txHash: TxHash,
|
|
1095
|
+
l1Constants?: Pick<L1RollupConstants, 'epochDuration'>,
|
|
1096
|
+
): Promise<TxReceipt | undefined> {
|
|
855
1097
|
const txEffect = await this.getTxEffect(txHash);
|
|
856
1098
|
if (!txEffect) {
|
|
857
1099
|
return undefined;
|
|
@@ -860,10 +1102,11 @@ export class BlockStore {
|
|
|
860
1102
|
const blockNumber = BlockNumber(txEffect.l2BlockNumber);
|
|
861
1103
|
|
|
862
1104
|
// Use existing archiver methods to determine finalization level
|
|
863
|
-
const [provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber] = await Promise.all([
|
|
1105
|
+
const [provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber, blockData] = await Promise.all([
|
|
864
1106
|
this.getProvenBlockNumber(),
|
|
865
1107
|
this.getCheckpointedL2BlockNumber(),
|
|
866
1108
|
this.getFinalizedL2BlockNumber(),
|
|
1109
|
+
this.getBlockData(blockNumber),
|
|
867
1110
|
]);
|
|
868
1111
|
|
|
869
1112
|
let status: TxStatus;
|
|
@@ -877,6 +1120,9 @@ export class BlockStore {
|
|
|
877
1120
|
status = TxStatus.PROPOSED;
|
|
878
1121
|
}
|
|
879
1122
|
|
|
1123
|
+
const epochNumber =
|
|
1124
|
+
blockData && l1Constants ? getEpochAtSlot(blockData.header.globalVariables.slotNumber, l1Constants) : undefined;
|
|
1125
|
+
|
|
880
1126
|
return new TxReceipt(
|
|
881
1127
|
txHash,
|
|
882
1128
|
status,
|
|
@@ -885,6 +1131,7 @@ export class BlockStore {
|
|
|
885
1131
|
txEffect.data.transactionFee.toBigInt(),
|
|
886
1132
|
txEffect.l2BlockHash,
|
|
887
1133
|
blockNumber,
|
|
1134
|
+
epochNumber,
|
|
888
1135
|
);
|
|
889
1136
|
}
|
|
890
1137
|
|
|
@@ -941,6 +1188,47 @@ export class BlockStore {
|
|
|
941
1188
|
return this.#lastSynchedL1Block.set(l1BlockNumber);
|
|
942
1189
|
}
|
|
943
1190
|
|
|
1191
|
+
/** Sets the proposed checkpoint (not yet L1-confirmed). Only accepts confirmed + 1.
|
|
1192
|
+
* Computes archive and checkpointOutHash from the stored blocks. */
|
|
1193
|
+
async setProposedCheckpoint(proposed: ProposedCheckpointInput) {
|
|
1194
|
+
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
|
+
const confirmed = await this.getLatestCheckpointNumber();
|
|
1200
|
+
if (proposed.checkpointNumber !== confirmed + 1) {
|
|
1201
|
+
throw new ProposedCheckpointNotSequentialError(proposed.checkpointNumber, confirmed);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// Ensure the previous checkpoint + blocks exist
|
|
1205
|
+
const previousBlock = await this.getPreviousCheckpointBlock(proposed.checkpointNumber);
|
|
1206
|
+
const blocks: L2Block[] = [];
|
|
1207
|
+
for (let i = 0; i < proposed.blockCount; i++) {
|
|
1208
|
+
const block = await this.getBlock(BlockNumber(proposed.startBlock + i));
|
|
1209
|
+
if (!block) {
|
|
1210
|
+
throw new BlockNotFoundError(proposed.startBlock + i);
|
|
1211
|
+
}
|
|
1212
|
+
blocks.push(block);
|
|
1213
|
+
}
|
|
1214
|
+
this.validateCheckpointBlocks(blocks, previousBlock);
|
|
1215
|
+
|
|
1216
|
+
const archive = blocks[blocks.length - 1].archive;
|
|
1217
|
+
const checkpointOutHash = Checkpoint.getCheckpointOutHash(blocks);
|
|
1218
|
+
|
|
1219
|
+
await this.#proposedCheckpoint.set({
|
|
1220
|
+
header: proposed.header.toBuffer(),
|
|
1221
|
+
archive: archive.toBuffer(),
|
|
1222
|
+
checkpointOutHash: checkpointOutHash.toBuffer(),
|
|
1223
|
+
checkpointNumber: proposed.checkpointNumber,
|
|
1224
|
+
startBlock: proposed.startBlock,
|
|
1225
|
+
blockCount: proposed.blockCount,
|
|
1226
|
+
totalManaUsed: proposed.totalManaUsed.toString(),
|
|
1227
|
+
feeAssetPriceModifier: proposed.feeAssetPriceModifier.toString(),
|
|
1228
|
+
});
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
|
|
944
1232
|
async getProvenCheckpointNumber(): Promise<CheckpointNumber> {
|
|
945
1233
|
const [latestCheckpointNumber, provenCheckpointNumber] = await Promise.all([
|
|
946
1234
|
this.getLatestCheckpointNumber(),
|