@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
|
@@ -8,17 +8,19 @@ import { asyncPool } from '@aztec/foundation/async-pool';
|
|
|
8
8
|
import { maxBigint } from '@aztec/foundation/bigint';
|
|
9
9
|
import { BlockNumber, CheckpointNumber, EpochNumber } from '@aztec/foundation/branded-types';
|
|
10
10
|
import { Buffer16, Buffer32 } from '@aztec/foundation/buffer';
|
|
11
|
-
import { partition, pick } from '@aztec/foundation/collection';
|
|
11
|
+
import { compactArray, partition, pick } from '@aztec/foundation/collection';
|
|
12
12
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
13
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
13
14
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
14
15
|
import { retryTimes } from '@aztec/foundation/retry';
|
|
15
16
|
import { count } from '@aztec/foundation/string';
|
|
16
17
|
import { DateProvider, Timer, elapsed } from '@aztec/foundation/timer';
|
|
17
18
|
import { isDefined, isErrorClass } from '@aztec/foundation/types';
|
|
18
19
|
import { type ArchiverEmitter, L2BlockSourceEvents, type ValidateCheckpointResult } from '@aztec/stdlib/block';
|
|
19
|
-
import { Checkpoint, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
20
|
+
import { Checkpoint, type CheckpointData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
20
21
|
import { type L1RollupConstants, getEpochAtSlot, getSlotAtNextL1Block } from '@aztec/stdlib/epoch-helpers';
|
|
21
22
|
import { computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
23
|
+
import type { CoordinationSignatureContext } from '@aztec/stdlib/p2p';
|
|
22
24
|
import { type Traceable, type Tracer, execInSpan, trackSpan } from '@aztec/telemetry-client';
|
|
23
25
|
|
|
24
26
|
import { InitialCheckpointNumberNotSequentialError } from '../errors.js';
|
|
@@ -30,7 +32,8 @@ import {
|
|
|
30
32
|
retrieveL1ToL2Messages,
|
|
31
33
|
retrievedToPublishedCheckpoint,
|
|
32
34
|
} from '../l1/data_retrieval.js';
|
|
33
|
-
import type {
|
|
35
|
+
import type { RejectedCheckpoint } from '../store/block_store.js';
|
|
36
|
+
import { type ArchiverDataStores, getArchiverSynchPoint } from '../store/data_stores.js';
|
|
34
37
|
import type { L2TipsCache } from '../store/l2_tips_cache.js';
|
|
35
38
|
import { MessageStoreError } from '../store/message_store.js';
|
|
36
39
|
import type { InboxMessage } from '../structs/inbox_message.js';
|
|
@@ -44,13 +47,15 @@ type RollupStatus = {
|
|
|
44
47
|
pendingCheckpointNumber: CheckpointNumber;
|
|
45
48
|
pendingArchive: string;
|
|
46
49
|
validationResult: ValidateCheckpointResult | undefined;
|
|
50
|
+
/** Last valid checkpoint observed on L1 and synced on this iteration */
|
|
47
51
|
lastRetrievedCheckpoint?: PublishedCheckpoint;
|
|
48
|
-
|
|
52
|
+
/** Last checkpoint observed on L1 across both valid and rejected entries on this iteration */
|
|
53
|
+
lastSeenCheckpoint?: PublishedCheckpoint;
|
|
49
54
|
};
|
|
50
55
|
|
|
51
56
|
/**
|
|
52
57
|
* Handles L1 synchronization for the archiver.
|
|
53
|
-
* Responsible for fetching checkpoints, L1
|
|
58
|
+
* Responsible for fetching checkpoints, L1 to L2 messages, and handling L1 reorgs.
|
|
54
59
|
*/
|
|
55
60
|
export class ArchiverL1Synchronizer implements Traceable {
|
|
56
61
|
private l1BlockNumber: bigint | undefined;
|
|
@@ -65,7 +70,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
65
70
|
private readonly debugClient: ViemPublicDebugClient,
|
|
66
71
|
private readonly rollup: RollupContract,
|
|
67
72
|
private readonly inbox: InboxContract,
|
|
68
|
-
private readonly
|
|
73
|
+
private readonly stores: ArchiverDataStores,
|
|
69
74
|
private config: {
|
|
70
75
|
batchSize: number;
|
|
71
76
|
skipValidateCheckpointAttestations?: boolean;
|
|
@@ -85,7 +90,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
85
90
|
l2TipsCache?: L2TipsCache,
|
|
86
91
|
private readonly log: Logger = createLogger('archiver:l1-sync'),
|
|
87
92
|
) {
|
|
88
|
-
this.updater = new ArchiverDataStoreUpdater(this.
|
|
93
|
+
this.updater = new ArchiverDataStoreUpdater(this.stores, l2TipsCache, {
|
|
89
94
|
rollupManaLimit: l1Constants.rollupManaLimit,
|
|
90
95
|
});
|
|
91
96
|
this.tracer = tracer;
|
|
@@ -111,6 +116,13 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
111
116
|
return this.l1Timestamp;
|
|
112
117
|
}
|
|
113
118
|
|
|
119
|
+
private getSignatureContext(): CoordinationSignatureContext {
|
|
120
|
+
return {
|
|
121
|
+
chainId: this.publicClient.chain.id,
|
|
122
|
+
rollupAddress: EthAddress.fromString(this.rollup.address),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
114
126
|
/** Checks that the ethereum node we are connected to has a latest timestamp no more than the allowed drift. Throw if not. */
|
|
115
127
|
public async testEthereumNodeSynced(): Promise<void> {
|
|
116
128
|
const maxAllowedDelay = this.config.maxAllowedEthClientDriftSeconds;
|
|
@@ -143,24 +155,36 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
143
155
|
return;
|
|
144
156
|
}
|
|
145
157
|
|
|
146
|
-
//
|
|
158
|
+
// Log at error if the latest L1 block timestamp is too old
|
|
147
159
|
const maxAllowedDelay = this.config.maxAllowedEthClientDriftSeconds;
|
|
148
160
|
const now = this.dateProvider.nowInSeconds();
|
|
149
161
|
if (maxAllowedDelay > 0 && Number(currentL1Timestamp) <= now - maxAllowedDelay) {
|
|
150
|
-
this.log.
|
|
162
|
+
this.log.error(
|
|
151
163
|
`Latest L1 block ${currentL1BlockNumber} timestamp ${currentL1Timestamp} is too old. Make sure your Ethereum node is synced.`,
|
|
152
164
|
{ currentL1BlockNumber, currentL1Timestamp, now, maxAllowedDelay },
|
|
153
165
|
);
|
|
154
166
|
}
|
|
155
167
|
|
|
168
|
+
// Query finalized block on L1
|
|
169
|
+
const rawFinalizedL1Block = await getFinalizedL1Block(this.publicClient);
|
|
170
|
+
const finalizedL1Block: L1BlockId | undefined = rawFinalizedL1Block && {
|
|
171
|
+
l1BlockNumber: rawFinalizedL1Block.number,
|
|
172
|
+
l1BlockHash: Buffer32.fromString(rawFinalizedL1Block.hash),
|
|
173
|
+
};
|
|
174
|
+
|
|
156
175
|
// Load sync point for blocks defaulting to start block
|
|
157
|
-
const { blocksSynchedTo = this.l1Constants.l1StartBlock } = await this.
|
|
158
|
-
this.log.debug(`Starting new archiver sync iteration`, { blocksSynchedTo, currentL1BlockData });
|
|
176
|
+
const { blocksSynchedTo = this.l1Constants.l1StartBlock } = await getArchiverSynchPoint(this.stores);
|
|
177
|
+
this.log.debug(`Starting new archiver sync iteration`, { blocksSynchedTo, currentL1BlockData, finalizedL1Block });
|
|
159
178
|
|
|
160
179
|
// Sync L1 to L2 messages. We retry this a few times since there are error conditions that reset the sync point, requiring a new iteration.
|
|
161
180
|
// Note that we cannot just wait for the l1 synchronizer to loop again, since the synchronizer would report as synced up to the current L1
|
|
162
181
|
// block, when that wouldn't be the case, since L1 to L2 messages would need another iteration.
|
|
163
|
-
await retryTimes(
|
|
182
|
+
await retryTimes(
|
|
183
|
+
() => this.handleL1ToL2Messages(currentL1BlockData, finalizedL1Block),
|
|
184
|
+
'Handling L1 to L2 messages',
|
|
185
|
+
3,
|
|
186
|
+
0.1,
|
|
187
|
+
);
|
|
164
188
|
|
|
165
189
|
if (currentL1BlockNumber > blocksSynchedTo) {
|
|
166
190
|
// First we retrieve new checkpoints and L2 blocks and store them in the DB. This will also update the
|
|
@@ -181,18 +205,10 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
181
205
|
currentL1Timestamp,
|
|
182
206
|
);
|
|
183
207
|
|
|
184
|
-
// If the last checkpoint we processed had an invalid attestation, we manually advance the L1 syncpoint
|
|
185
|
-
// past it, since otherwise we'll keep downloading it and reprocessing it on every iteration until
|
|
186
|
-
// we get a valid checkpoint to advance the syncpoint.
|
|
187
|
-
if (!rollupStatus.validationResult?.valid && rollupStatus.lastL1BlockWithCheckpoint !== undefined) {
|
|
188
|
-
await this.store.setCheckpointSynchedL1BlockNumber(rollupStatus.lastL1BlockWithCheckpoint);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
208
|
// And lastly we check if we are missing any checkpoints behind us due to a possible L1 reorg.
|
|
192
209
|
// We only do this if rollup cant prune on the next submission. Otherwise we will end up
|
|
193
|
-
// re-syncing the checkpoints we have just unwound above.
|
|
194
|
-
|
|
195
|
-
if (!rollupCanPrune && rollupStatus.validationResult?.valid) {
|
|
210
|
+
// re-syncing the checkpoints we have just unwound above.
|
|
211
|
+
if (!rollupCanPrune) {
|
|
196
212
|
await this.checkForNewCheckpointsBeforeL1SyncPoint(rollupStatus, blocksSynchedTo, currentL1BlockNumber);
|
|
197
213
|
}
|
|
198
214
|
|
|
@@ -200,7 +216,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
200
216
|
}
|
|
201
217
|
|
|
202
218
|
// Update the finalized L2 checkpoint based on L1 finality.
|
|
203
|
-
await this.updateFinalizedCheckpoint();
|
|
219
|
+
await this.updateFinalizedCheckpoint(finalizedL1Block);
|
|
204
220
|
|
|
205
221
|
// After syncing has completed, update the current l1 block number and timestamp,
|
|
206
222
|
// otherwise we risk announcing to the world that we've synced to a given point,
|
|
@@ -217,19 +233,18 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
217
233
|
});
|
|
218
234
|
}
|
|
219
235
|
|
|
220
|
-
/**
|
|
221
|
-
private async updateFinalizedCheckpoint(): Promise<void> {
|
|
236
|
+
/** Updates the finalized checkpoint using the pre-fetched finalized L1 block from the current sync iteration. */
|
|
237
|
+
private async updateFinalizedCheckpoint(finalizedL1Block: L1BlockId | undefined): Promise<void> {
|
|
222
238
|
try {
|
|
223
|
-
const finalizedL1Block = await getFinalizedL1Block(this.publicClient);
|
|
224
239
|
if (!finalizedL1Block) {
|
|
225
240
|
this.log.trace(`Skipping finalized checkpoint update: L1 has no finalized block yet.`);
|
|
226
241
|
return;
|
|
227
242
|
}
|
|
228
|
-
const finalizedL1BlockNumber = finalizedL1Block.
|
|
243
|
+
const finalizedL1BlockNumber = finalizedL1Block.l1BlockNumber;
|
|
229
244
|
const finalizedCheckpointNumber = await this.rollup.getProvenCheckpointNumber({
|
|
230
245
|
blockNumber: finalizedL1BlockNumber,
|
|
231
246
|
});
|
|
232
|
-
const localFinalizedCheckpointNumber = await this.
|
|
247
|
+
const localFinalizedCheckpointNumber = await this.stores.blocks.getFinalizedCheckpointNumber();
|
|
233
248
|
if (localFinalizedCheckpointNumber !== finalizedCheckpointNumber) {
|
|
234
249
|
await this.updater.setFinalizedCheckpointNumber(finalizedCheckpointNumber);
|
|
235
250
|
this.log.info(`Updated finalized chain to checkpoint ${finalizedCheckpointNumber}`, {
|
|
@@ -248,8 +263,8 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
248
263
|
/** Prune all proposed local blocks that should have been checkpointed by now. */
|
|
249
264
|
private async pruneUncheckpointedBlocks(currentL1Timestamp: bigint) {
|
|
250
265
|
const [lastCheckpointedBlockNumber, lastProposedBlockNumber] = await Promise.all([
|
|
251
|
-
this.
|
|
252
|
-
this.
|
|
266
|
+
this.stores.blocks.getCheckpointedL2BlockNumber(),
|
|
267
|
+
this.stores.blocks.getLatestL2BlockNumber(),
|
|
253
268
|
]);
|
|
254
269
|
|
|
255
270
|
// If there are no uncheckpointed blocks, we got nothing to do
|
|
@@ -263,8 +278,10 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
263
278
|
const firstUncheckpointedBlockNumber = BlockNumber(lastCheckpointedBlockNumber + 1);
|
|
264
279
|
|
|
265
280
|
// What's the slot of the first uncheckpointed block?
|
|
266
|
-
const
|
|
267
|
-
|
|
281
|
+
const firstUncheckpointedBlockData = await this.stores.blocks.getBlockData({
|
|
282
|
+
number: firstUncheckpointedBlockNumber,
|
|
283
|
+
});
|
|
284
|
+
const firstUncheckpointedBlockSlot = firstUncheckpointedBlockData?.header.getSlot();
|
|
268
285
|
|
|
269
286
|
if (firstUncheckpointedBlockSlot === undefined || firstUncheckpointedBlockSlot >= slotAtNextL1Block) {
|
|
270
287
|
return;
|
|
@@ -274,11 +291,10 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
274
291
|
// This also clears any proposed checkpoint whose blocks are being pruned.
|
|
275
292
|
this.log.warn(
|
|
276
293
|
`Pruning blocks after block ${lastCheckpointedBlockNumber} due to slot ${firstUncheckpointedBlockSlot} not being checkpointed`,
|
|
277
|
-
{ firstUncheckpointedBlockHeader:
|
|
294
|
+
{ firstUncheckpointedBlockHeader: firstUncheckpointedBlockData?.header.toInspect(), slotAtNextL1Block },
|
|
278
295
|
);
|
|
279
296
|
|
|
280
297
|
const prunedBlocks = await this.updater.removeUncheckpointedBlocksAfter(lastCheckpointedBlockNumber);
|
|
281
|
-
|
|
282
298
|
if (prunedBlocks.length > 0) {
|
|
283
299
|
this.events.emit(L2BlockSourceEvents.L2PruneUncheckpointed, {
|
|
284
300
|
type: L2BlockSourceEvents.L2PruneUncheckpointed,
|
|
@@ -310,7 +326,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
310
326
|
currentL1Timestamp: bigint,
|
|
311
327
|
): Promise<{ rollupCanPrune: boolean }> {
|
|
312
328
|
const rollupCanPrune = await this.canPrune(currentL1BlockNumber, currentL1Timestamp);
|
|
313
|
-
const localPendingCheckpointNumber = await this.
|
|
329
|
+
const localPendingCheckpointNumber = await this.stores.blocks.getLatestCheckpointNumber();
|
|
314
330
|
const canPrune = localPendingCheckpointNumber > provenCheckpointNumber && rollupCanPrune;
|
|
315
331
|
|
|
316
332
|
if (canPrune) {
|
|
@@ -331,12 +347,12 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
331
347
|
// promises when the gap between local pending and proven checkpoint numbers is large.
|
|
332
348
|
const BATCH_SIZE = 10;
|
|
333
349
|
const indices = Array.from({ length: checkpointsToUnwind }, (_, i) => CheckpointNumber(i + pruneFrom));
|
|
334
|
-
const checkpoints = (
|
|
335
|
-
|
|
336
|
-
);
|
|
350
|
+
const checkpoints = (
|
|
351
|
+
await asyncPool(BATCH_SIZE, indices, idx => this.stores.blocks.getCheckpointData(idx))
|
|
352
|
+
).filter(isDefined);
|
|
337
353
|
const newBlocks = (
|
|
338
354
|
await asyncPool(BATCH_SIZE, checkpoints, cp =>
|
|
339
|
-
this.
|
|
355
|
+
this.stores.blocks.getBlocksForCheckpoint(CheckpointNumber(cp.checkpointNumber)),
|
|
340
356
|
)
|
|
341
357
|
)
|
|
342
358
|
.filter(isDefined)
|
|
@@ -356,12 +372,12 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
356
372
|
this.log.warn(
|
|
357
373
|
`Removed ${count(checkpointsToUnwind, 'checkpoint')} after checkpoint ${provenCheckpointNumber} ` +
|
|
358
374
|
`due to predicted reorg at L1 block ${currentL1BlockNumber}. ` +
|
|
359
|
-
`Updated latest checkpoint is ${await this.
|
|
375
|
+
`Updated latest checkpoint is ${await this.stores.blocks.getLatestCheckpointNumber()}.`,
|
|
360
376
|
);
|
|
361
377
|
this.instrumentation.processPrune(timer.ms());
|
|
362
378
|
// TODO(palla/reorg): Do we need to set the block synched L1 block number here?
|
|
363
379
|
// Seems like the next iteration should handle this.
|
|
364
|
-
// await this.
|
|
380
|
+
// await this.stores.blocks.setSynchedL1BlockNumber(currentL1BlockNumber);
|
|
365
381
|
}
|
|
366
382
|
|
|
367
383
|
return { rollupCanPrune };
|
|
@@ -378,14 +394,17 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
378
394
|
}
|
|
379
395
|
|
|
380
396
|
@trackSpan('Archiver.handleL1ToL2Messages')
|
|
381
|
-
private async handleL1ToL2Messages(
|
|
397
|
+
private async handleL1ToL2Messages(
|
|
398
|
+
currentL1Block: L1BlockId,
|
|
399
|
+
finalizedL1Block: L1BlockId | undefined,
|
|
400
|
+
): Promise<boolean> {
|
|
382
401
|
// Load the syncpoint, which may have been updated in a previous iteration
|
|
383
402
|
const {
|
|
384
403
|
messagesSynchedTo = {
|
|
385
404
|
l1BlockNumber: this.l1Constants.l1StartBlock,
|
|
386
405
|
l1BlockHash: this.l1Constants.l1StartBlockHash,
|
|
387
406
|
},
|
|
388
|
-
} = await this.
|
|
407
|
+
} = await getArchiverSynchPoint(this.stores);
|
|
389
408
|
|
|
390
409
|
// Nothing to do if L1 block number has not moved forward
|
|
391
410
|
const currentL1BlockNumber = currentL1Block.l1BlockNumber;
|
|
@@ -395,10 +414,14 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
395
414
|
|
|
396
415
|
// Compare local message store state with the remote. If they match, we just advance the match pointer.
|
|
397
416
|
const remoteMessagesState = await this.inbox.getState({ blockNumber: currentL1BlockNumber });
|
|
398
|
-
const localLastMessage = await this.
|
|
417
|
+
const localLastMessage = await this.stores.messages.getLastMessage();
|
|
399
418
|
if (await this.localStateMatches(localLastMessage, remoteMessagesState)) {
|
|
400
419
|
this.log.trace(`Local L1 to L2 messages are already in sync with remote at L1 block ${currentL1BlockNumber}`);
|
|
401
|
-
await this.
|
|
420
|
+
await this.stores.messages.setMessageSyncState(
|
|
421
|
+
currentL1Block,
|
|
422
|
+
remoteMessagesState.treeInProgress,
|
|
423
|
+
finalizedL1Block,
|
|
424
|
+
);
|
|
402
425
|
return true;
|
|
403
426
|
}
|
|
404
427
|
|
|
@@ -423,7 +446,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
423
446
|
// Note that, if there are no new messages to insert, but there was an L1 reorg that pruned out last messages,
|
|
424
447
|
// we'd notice by comparing our local state with the remote one again, and seeing they don't match even after
|
|
425
448
|
// our sync attempt. In this case, we also rollback our syncpoint, and trigger a retry.
|
|
426
|
-
const localLastMessageAfterSync = await this.
|
|
449
|
+
const localLastMessageAfterSync = await this.stores.messages.getLastMessage();
|
|
427
450
|
if (!(await this.localStateMatches(localLastMessageAfterSync, remoteMessagesState))) {
|
|
428
451
|
this.log.warn(
|
|
429
452
|
`Local L1 to L2 messages state does not match remote after sync attempt. Rolling back syncpoint to retry.`,
|
|
@@ -434,13 +457,17 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
434
457
|
}
|
|
435
458
|
|
|
436
459
|
// Advance the syncpoint after a successful sync
|
|
437
|
-
await this.
|
|
460
|
+
await this.stores.messages.setMessageSyncState(
|
|
461
|
+
currentL1Block,
|
|
462
|
+
remoteMessagesState.treeInProgress,
|
|
463
|
+
finalizedL1Block,
|
|
464
|
+
);
|
|
438
465
|
return true;
|
|
439
466
|
}
|
|
440
467
|
|
|
441
468
|
/** Checks if the local rolling hash and message count matches the remote state */
|
|
442
469
|
private async localStateMatches(localLastMessage: InboxMessage | undefined, remoteState: InboxContractState) {
|
|
443
|
-
const localMessageCount = await this.
|
|
470
|
+
const localMessageCount = await this.stores.messages.getTotalL1ToL2MessageCount();
|
|
444
471
|
this.log.trace(`Comparing local and remote inbox state`, { localMessageCount, localLastMessage, remoteState });
|
|
445
472
|
|
|
446
473
|
return (
|
|
@@ -462,7 +489,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
462
489
|
this.log.trace(`Retrieving L1 to L2 messages in L1 blocks ${searchStartBlock}-${searchEndBlock}`);
|
|
463
490
|
const messages = await retrieveL1ToL2Messages(this.inbox, searchStartBlock, searchEndBlock);
|
|
464
491
|
const timer = new Timer();
|
|
465
|
-
await this.
|
|
492
|
+
await this.stores.messages.addL1ToL2Messages(messages);
|
|
466
493
|
const perMsg = timer.ms() / messages.length;
|
|
467
494
|
this.instrumentation.processNewMessages(messages.length, perMsg);
|
|
468
495
|
for (const msg of messages) {
|
|
@@ -487,13 +514,16 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
487
514
|
private async rollbackL1ToL2Messages(remoteMessagesState: InboxContractState): Promise<L1BlockId> {
|
|
488
515
|
const { treeInProgress: remoteTreeInProgress, messagesRollingHash: remoteRollingHash } = remoteMessagesState;
|
|
489
516
|
|
|
517
|
+
const messagesFinalizedL1Block = await this.stores.messages.getMessagesFinalizedL1Block();
|
|
518
|
+
const finalizedL1BlockNumber = messagesFinalizedL1Block?.l1BlockNumber;
|
|
519
|
+
|
|
490
520
|
// Slowly go back through our messages until we find the last common message. We could query the logs in
|
|
491
521
|
// batch as an optimization, but the depth of the reorg should not be deep, and this is a very rare case,
|
|
492
522
|
// so it's fine to query one log at a time.
|
|
493
523
|
let commonMsg: undefined | InboxMessage;
|
|
494
524
|
let messagesToDelete = 0;
|
|
495
525
|
this.log.verbose(`Searching most recent common L1 to L2 message`);
|
|
496
|
-
for await (const localMsg of this.
|
|
526
|
+
for await (const localMsg of this.stores.messages.iterateL1ToL2Messages({ reverse: true })) {
|
|
497
527
|
const logCtx = { remoteMsg: undefined as InboxMessage | undefined, localMsg, remoteMessagesState };
|
|
498
528
|
|
|
499
529
|
// First check if the local message rolling hash matches the current rolling hash of the inbox contract,
|
|
@@ -508,6 +538,13 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
508
538
|
break;
|
|
509
539
|
}
|
|
510
540
|
|
|
541
|
+
// Messages at or below the finalized L1 block cannot have been reorged — accept as common without querying L1.
|
|
542
|
+
if (finalizedL1BlockNumber !== undefined && localMsg.l1BlockNumber <= finalizedL1BlockNumber) {
|
|
543
|
+
this.log.info(`Found common L1 to L2 message at finalized L1 block ${localMsg.l1BlockNumber}`, logCtx);
|
|
544
|
+
commonMsg = localMsg;
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
|
|
511
548
|
// If there's no match with the current remote state, check if the message exists on the inbox contract at all
|
|
512
549
|
// by looking at the inbox events. If the L1 reorg *added* new messages in addition to deleting existing ones,
|
|
513
550
|
// then the current remote state's rolling hash will not match anything we have locally, so we need to check existence
|
|
@@ -536,17 +573,29 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
536
573
|
if (messagesToDelete > 0) {
|
|
537
574
|
const lastGoodIndex = commonMsg?.index;
|
|
538
575
|
this.log.warn(`Rolling back all local L1 to L2 messages after index ${lastGoodIndex ?? 'initial'}`);
|
|
539
|
-
await this.
|
|
576
|
+
await this.stores.messages.removeL1ToL2Messages(lastGoodIndex !== undefined ? lastGoodIndex + 1n : 0n);
|
|
540
577
|
}
|
|
541
578
|
|
|
542
579
|
// Update the syncpoint so the loop below reprocesses the changed messages. We go to the block before
|
|
543
580
|
// the last common one, so we force reprocessing it, in case new messages were added on that same L1 block
|
|
544
|
-
// after the last common message.
|
|
545
|
-
|
|
546
|
-
const
|
|
581
|
+
// after the last common message. Cap at the finalized L1 block: messages at or below finalized cannot
|
|
582
|
+
// have been reorged, so there is no need to walk back any further than that.
|
|
583
|
+
const syncPointL1BlockNumber = maxBigint(
|
|
584
|
+
...compactArray([
|
|
585
|
+
commonMsg ? commonMsg.l1BlockNumber - 1n : undefined,
|
|
586
|
+
finalizedL1BlockNumber,
|
|
587
|
+
this.l1Constants.l1StartBlock,
|
|
588
|
+
]),
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
const syncPointL1BlockHash =
|
|
592
|
+
syncPointL1BlockNumber === finalizedL1BlockNumber
|
|
593
|
+
? messagesFinalizedL1Block!.l1BlockHash
|
|
594
|
+
: await this.getL1BlockHash(syncPointL1BlockNumber);
|
|
595
|
+
|
|
547
596
|
const messagesSyncPoint = { l1BlockNumber: syncPointL1BlockNumber, l1BlockHash: syncPointL1BlockHash };
|
|
548
|
-
await this.
|
|
549
|
-
this.log.verbose(`Updated messages syncpoint to L1 block ${
|
|
597
|
+
await this.stores.messages.setMessageSyncState(messagesSyncPoint, remoteTreeInProgress);
|
|
598
|
+
this.log.verbose(`Updated messages syncpoint to L1 block ${messagesSyncPoint.l1BlockNumber}`, {
|
|
550
599
|
...messagesSyncPoint,
|
|
551
600
|
remoteTreeInProgress,
|
|
552
601
|
});
|
|
@@ -567,9 +616,9 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
567
616
|
currentL1BlockNumber: bigint,
|
|
568
617
|
initialSyncComplete: boolean,
|
|
569
618
|
): Promise<RollupStatus> {
|
|
570
|
-
const localPendingCheckpointNumber = await this.
|
|
619
|
+
const localPendingCheckpointNumber = await this.stores.blocks.getLatestCheckpointNumber();
|
|
571
620
|
const initialValidationResult: ValidateCheckpointResult | undefined =
|
|
572
|
-
await this.
|
|
621
|
+
await this.stores.blocks.getPendingChainValidationStatus();
|
|
573
622
|
const {
|
|
574
623
|
provenCheckpointNumber,
|
|
575
624
|
provenArchive,
|
|
@@ -599,7 +648,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
599
648
|
// we need to set it to zero. This is an edge case because we dont have a checkpoint zero (initial checkpoint is one),
|
|
600
649
|
// so localCheckpointForDestinationProvenCheckpointNumber would not be found below.
|
|
601
650
|
if (provenCheckpointNumber === 0) {
|
|
602
|
-
const localProvenCheckpointNumber = await this.
|
|
651
|
+
const localProvenCheckpointNumber = await this.stores.blocks.getProvenCheckpointNumber();
|
|
603
652
|
if (localProvenCheckpointNumber !== provenCheckpointNumber) {
|
|
604
653
|
await this.updater.setProvenCheckpointNumber(provenCheckpointNumber);
|
|
605
654
|
this.log.info(`Rolled back proven chain to checkpoint ${provenCheckpointNumber}`, { provenCheckpointNumber });
|
|
@@ -607,11 +656,11 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
607
656
|
}
|
|
608
657
|
|
|
609
658
|
const localCheckpointForDestinationProvenCheckpointNumber =
|
|
610
|
-
await this.
|
|
659
|
+
await this.stores.blocks.getCheckpointData(provenCheckpointNumber);
|
|
611
660
|
|
|
612
661
|
// Sanity check. I've hit what seems to be a state where the proven checkpoint is set to a value greater than the latest
|
|
613
662
|
// synched checkpoint when requesting L2Tips from the archiver. This is the only place where the proven checkpoint is set.
|
|
614
|
-
const synched = await this.
|
|
663
|
+
const synched = await this.stores.blocks.getLatestCheckpointNumber();
|
|
615
664
|
if (
|
|
616
665
|
localCheckpointForDestinationProvenCheckpointNumber &&
|
|
617
666
|
synched < localCheckpointForDestinationProvenCheckpointNumber.checkpointNumber
|
|
@@ -631,7 +680,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
631
680
|
localCheckpointForDestinationProvenCheckpointNumber &&
|
|
632
681
|
provenArchive.equals(localCheckpointForDestinationProvenCheckpointNumber.archive.root)
|
|
633
682
|
) {
|
|
634
|
-
const localProvenCheckpointNumber = await this.
|
|
683
|
+
const localProvenCheckpointNumber = await this.stores.blocks.getProvenCheckpointNumber();
|
|
635
684
|
if (localProvenCheckpointNumber !== provenCheckpointNumber) {
|
|
636
685
|
await this.updater.setProvenCheckpointNumber(provenCheckpointNumber);
|
|
637
686
|
this.log.info(`Updated proven chain to checkpoint ${provenCheckpointNumber}`, { provenCheckpointNumber });
|
|
@@ -659,7 +708,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
659
708
|
// If we have 0 checkpoints locally and there are no checkpoints onchain there is nothing to do.
|
|
660
709
|
const noCheckpoints = localPendingCheckpointNumber === 0 && pendingCheckpointNumber === 0;
|
|
661
710
|
if (noCheckpoints) {
|
|
662
|
-
await this.
|
|
711
|
+
await this.stores.blocks.setSynchedL1BlockNumber(currentL1BlockNumber);
|
|
663
712
|
this.log.debug(
|
|
664
713
|
`No checkpoints to retrieve from ${blocksSynchedTo + 1n} to ${currentL1BlockNumber}, no checkpoints on chain`,
|
|
665
714
|
);
|
|
@@ -671,7 +720,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
671
720
|
// Related to the L2 reorgs of the pending chain. We are only interested in actually addressing a reorg if there
|
|
672
721
|
// are any state that could be impacted by it. If we have no checkpoints, there is no impact.
|
|
673
722
|
if (localPendingCheckpointNumber > 0) {
|
|
674
|
-
const localPendingCheckpoint = await this.
|
|
723
|
+
const localPendingCheckpoint = await this.stores.blocks.getCheckpointData(localPendingCheckpointNumber);
|
|
675
724
|
if (localPendingCheckpoint === undefined) {
|
|
676
725
|
throw new Error(`Missing checkpoint ${localPendingCheckpointNumber}`);
|
|
677
726
|
}
|
|
@@ -686,7 +735,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
686
735
|
// However, in the re-org scenario, our L1 node is temporarily lying to us and we end up potentially missing checkpoints.
|
|
687
736
|
// We must only set this block number based on actually retrieved logs.
|
|
688
737
|
// TODO(#8621): Tackle this properly when we handle L1 Re-orgs.
|
|
689
|
-
// await this.
|
|
738
|
+
// await this.stores.blocks.setSynchedL1BlockNumber(currentL1BlockNumber);
|
|
690
739
|
this.log.debug(`No checkpoints to retrieve from ${blocksSynchedTo + 1n} to ${currentL1BlockNumber}`);
|
|
691
740
|
return rollupStatus;
|
|
692
741
|
}
|
|
@@ -706,7 +755,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
706
755
|
|
|
707
756
|
let tipAfterUnwind = localPendingCheckpointNumber;
|
|
708
757
|
while (true) {
|
|
709
|
-
const candidateCheckpoint = await this.
|
|
758
|
+
const candidateCheckpoint = await this.stores.blocks.getCheckpointData(tipAfterUnwind);
|
|
710
759
|
if (candidateCheckpoint === undefined) {
|
|
711
760
|
break;
|
|
712
761
|
}
|
|
@@ -731,7 +780,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
731
780
|
this.log.warn(
|
|
732
781
|
`Removed ${count(checkpointsToRemove, 'checkpoint')} after checkpoint ${tipAfterUnwind} ` +
|
|
733
782
|
`due to mismatched checkpoint hashes at L1 block ${currentL1BlockNumber}. ` +
|
|
734
|
-
`Updated L2 latest checkpoint is ${await this.
|
|
783
|
+
`Updated L2 latest checkpoint is ${await this.stores.blocks.getLatestCheckpointNumber()}.`,
|
|
735
784
|
);
|
|
736
785
|
}
|
|
737
786
|
}
|
|
@@ -741,7 +790,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
741
790
|
let searchStartBlock: bigint = blocksSynchedTo;
|
|
742
791
|
let searchEndBlock: bigint = blocksSynchedTo;
|
|
743
792
|
let lastRetrievedCheckpoint: PublishedCheckpoint | undefined;
|
|
744
|
-
let
|
|
793
|
+
let lastSeenCheckpoint: PublishedCheckpoint | undefined;
|
|
745
794
|
|
|
746
795
|
do {
|
|
747
796
|
[searchStartBlock, searchEndBlock] = this.nextRange(searchEndBlock, currentL1BlockNumber);
|
|
@@ -777,11 +826,14 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
777
826
|
},
|
|
778
827
|
);
|
|
779
828
|
|
|
780
|
-
// Check if the last checkpoint matches
|
|
781
|
-
// We only check the last one
|
|
782
|
-
//
|
|
829
|
+
// Check if the last checkpoint matches a local pending entry (so we can skip blob fetch).
|
|
830
|
+
// We only check the last one; if it matches, the blob fetch is skipped for that entry.
|
|
831
|
+
// TODO(palla/pipelining): We may have more than a single checkpoint to promote
|
|
783
832
|
const lastCalldataCheckpoint = calldataCheckpoints[calldataCheckpoints.length - 1];
|
|
784
|
-
const
|
|
833
|
+
const promoteResult = await this.tryBuildPublishedCheckpointFromProposed(lastCalldataCheckpoint);
|
|
834
|
+
const checkpointToPromote = promoteResult && !('diverged' in promoteResult) ? promoteResult : undefined;
|
|
835
|
+
const evictProposedFrom =
|
|
836
|
+
promoteResult && 'diverged' in promoteResult ? promoteResult.fromCheckpointNumber : undefined;
|
|
785
837
|
|
|
786
838
|
// Then fetch blobs in parallel and build the full published checkpoints
|
|
787
839
|
const toFetchBlobs = checkpointToPromote ? calldataCheckpoints.slice(0, -1) : calldataCheckpoints;
|
|
@@ -807,21 +859,42 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
807
859
|
|
|
808
860
|
// Now loop through all checkpoints and validate their attestations
|
|
809
861
|
for (const published of publishedCheckpoints) {
|
|
862
|
+
// Check the attestations uploaded by the publisher to L1 are correct
|
|
863
|
+
// Rollup contract does not validate attestations to save on gas, so this
|
|
864
|
+
// falls on the nodes to verify offchain and skip those checkpoints.
|
|
810
865
|
const validationResult = this.config.skipValidateCheckpointAttestations
|
|
811
866
|
? { valid: true as const }
|
|
812
|
-
: await validateCheckpointAttestations(
|
|
867
|
+
: await validateCheckpointAttestations(
|
|
868
|
+
published,
|
|
869
|
+
this.epochCache,
|
|
870
|
+
this.l1Constants,
|
|
871
|
+
this.getSignatureContext(),
|
|
872
|
+
this.log,
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
// Also skip the checkpoint if it builds on a previously-rejected ancestor. Without
|
|
876
|
+
// this, addCheckpoints would throw InitialCheckpointNumberNotSequentialError when the
|
|
877
|
+
// ancestor was skipped earlier (e.g. due to invalid attestations), the catch handler
|
|
878
|
+
// would roll back the L1 sync point, and the next iteration would re-fetch and re-throw.
|
|
879
|
+
const rejectedAncestor = await this.stores.blocks.getRejectedCheckpointByArchiveRoot(
|
|
880
|
+
published.checkpoint.header.lastArchiveRoot,
|
|
881
|
+
);
|
|
813
882
|
|
|
814
|
-
//
|
|
883
|
+
// Update the validation result if it has changed, so we can keep track of the first invalid checkpoint
|
|
815
884
|
// in case there is a sequence of more than one invalid checkpoint, as we need to invalidate the first one.
|
|
816
885
|
// There is an exception though: if a checkpoint is invalidated and replaced with another invalid checkpoint,
|
|
817
886
|
// we need to update the validation result, since we need to be able to invalidate the new one.
|
|
818
887
|
// See test 'chain progresses if an invalid checkpoint is invalidated with an invalid one' for more info.
|
|
819
|
-
if
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
888
|
+
// Do not update the validation result if there is a rejected ancestor, since in that case we want to keep the
|
|
889
|
+
// original invalidation, as the new checkpoint is extending from a previous invalid one.
|
|
890
|
+
const validStatusChanged = rollupStatus.validationResult?.valid !== validationResult.valid;
|
|
891
|
+
const invalidStatusWithSameCheckpointNumber =
|
|
892
|
+
!validationResult.valid &&
|
|
893
|
+
rollupStatus.validationResult &&
|
|
894
|
+
!rollupStatus.validationResult.valid &&
|
|
895
|
+
rollupStatus.validationResult.checkpoint.checkpointNumber === validationResult.checkpoint.checkpointNumber;
|
|
896
|
+
|
|
897
|
+
if (!rejectedAncestor && (validStatusChanged || invalidStatusWithSameCheckpointNumber)) {
|
|
825
898
|
rollupStatus.validationResult = validationResult;
|
|
826
899
|
}
|
|
827
900
|
|
|
@@ -838,16 +911,62 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
838
911
|
validationResult,
|
|
839
912
|
});
|
|
840
913
|
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
//
|
|
914
|
+
// Persist a rejected-ancestor entry so any later checkpoint that builds on this one
|
|
915
|
+
// is detected and skipped (rather than tripping the addCheckpoints consecutive-number
|
|
916
|
+
// check and causing the sync point to roll back in a loop).
|
|
917
|
+
await this.stores.blocks.addRejectedCheckpoint({
|
|
918
|
+
checkpointNumber: published.checkpoint.number,
|
|
919
|
+
archiveRoot: published.checkpoint.archive.root,
|
|
920
|
+
parentArchiveRoot: published.checkpoint.header.lastArchiveRoot,
|
|
921
|
+
slotNumber: published.checkpoint.header.slotNumber,
|
|
922
|
+
l1: published.l1,
|
|
923
|
+
reason: 'invalid-attestations' as const,
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (rejectedAncestor) {
|
|
930
|
+
const descendantInfo = published.checkpoint.toCheckpointInfo();
|
|
931
|
+
this.log.warn(
|
|
932
|
+
`Skipping checkpoint ${published.checkpoint.number} as it is a descendant of ` +
|
|
933
|
+
`rejected checkpoint ${rejectedAncestor.checkpointNumber} (${rejectedAncestor.reason})`,
|
|
934
|
+
{
|
|
935
|
+
checkpointNumber: published.checkpoint.number,
|
|
936
|
+
checkpointHash: published.checkpoint.hash(),
|
|
937
|
+
l1BlockNumber: published.l1.blockNumber,
|
|
938
|
+
l1BlockHash: published.l1.blockHash,
|
|
939
|
+
ancestorCheckpointNumber: rejectedAncestor.checkpointNumber,
|
|
940
|
+
ancestorArchiveRoot: rejectedAncestor.archiveRoot.toString(),
|
|
941
|
+
ancestorReason: rejectedAncestor.reason,
|
|
942
|
+
},
|
|
943
|
+
);
|
|
944
|
+
|
|
945
|
+
this.events.emit(L2BlockSourceEvents.DescendentOfInvalidAttestationsCheckpointDetected, {
|
|
946
|
+
type: L2BlockSourceEvents.DescendentOfInvalidAttestationsCheckpointDetected,
|
|
947
|
+
checkpoint: descendantInfo,
|
|
948
|
+
ancestorArchiveRoot: rejectedAncestor.archiveRoot,
|
|
949
|
+
ancestorCheckpointNumber: rejectedAncestor.checkpointNumber,
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
// Persist this chainpoint as rejected as well, so we can construct a chain of
|
|
953
|
+
// skipped checkpoints starting from the first one with invalid attestations.
|
|
954
|
+
await this.stores.blocks.addRejectedCheckpoint({
|
|
955
|
+
checkpointNumber: published.checkpoint.number,
|
|
956
|
+
archiveRoot: published.checkpoint.archive.root,
|
|
957
|
+
parentArchiveRoot: published.checkpoint.header.lastArchiveRoot,
|
|
958
|
+
slotNumber: published.checkpoint.header.slotNumber,
|
|
959
|
+
l1: published.l1,
|
|
960
|
+
reason: 'descends-from-invalid-attestations' as const,
|
|
961
|
+
});
|
|
962
|
+
|
|
844
963
|
continue;
|
|
845
964
|
}
|
|
846
965
|
|
|
847
966
|
// Check the inHash of the checkpoint against the l1->l2 messages.
|
|
848
967
|
// The messages should've been synced up to the currentL1BlockNumber and must be available for the published
|
|
849
968
|
// checkpoints we just retrieved.
|
|
850
|
-
const l1ToL2Messages = await this.
|
|
969
|
+
const l1ToL2Messages = await this.stores.messages.getL1ToL2Messages(published.checkpoint.number);
|
|
851
970
|
const computedInHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
852
971
|
const publishedInHash = published.checkpoint.header.inHash;
|
|
853
972
|
if (!computedInHash.equals(publishedInHash)) {
|
|
@@ -904,14 +1023,15 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
904
1023
|
attestations: lastCalldataCheckpoint.attestations,
|
|
905
1024
|
checkpoint: maybeValidCheckpointToPromote,
|
|
906
1025
|
},
|
|
1026
|
+
evictProposedFrom,
|
|
907
1027
|
),
|
|
908
1028
|
),
|
|
909
1029
|
);
|
|
910
1030
|
|
|
911
|
-
if (
|
|
1031
|
+
if (validCheckpoints.length > 0) {
|
|
912
1032
|
this.instrumentation.processNewCheckpointedBlocks(
|
|
913
|
-
processDuration /
|
|
914
|
-
|
|
1033
|
+
processDuration / validCheckpoints.length,
|
|
1034
|
+
validCheckpoints.flatMap(c => c.checkpoint.blocks),
|
|
915
1035
|
);
|
|
916
1036
|
}
|
|
917
1037
|
|
|
@@ -938,14 +1058,23 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
938
1058
|
if (err instanceof InitialCheckpointNumberNotSequentialError) {
|
|
939
1059
|
const { previousCheckpointNumber, newCheckpointNumber } = err;
|
|
940
1060
|
const previousCheckpoint = previousCheckpointNumber
|
|
941
|
-
? await this.
|
|
1061
|
+
? await this.stores.blocks.getCheckpointData(CheckpointNumber(previousCheckpointNumber))
|
|
942
1062
|
: undefined;
|
|
943
|
-
const
|
|
944
|
-
|
|
1063
|
+
const lastFinalizedCheckpoint = await this.stores.blocks.getCheckpointData(
|
|
1064
|
+
await this.stores.blocks.getFinalizedCheckpointNumber(),
|
|
1065
|
+
);
|
|
1066
|
+
const updatedL1SyncPoint =
|
|
1067
|
+
previousCheckpoint?.l1.blockNumber ??
|
|
1068
|
+
lastFinalizedCheckpoint?.l1.blockNumber ??
|
|
1069
|
+
this.l1Constants.l1StartBlock;
|
|
1070
|
+
await this.stores.blocks.setSynchedL1BlockNumber(updatedL1SyncPoint);
|
|
945
1071
|
this.log.warn(
|
|
946
1072
|
`Attempting to insert checkpoint ${newCheckpointNumber} with previous block ${previousCheckpointNumber}. Rolling back L1 sync point to ${updatedL1SyncPoint} to try and fetch the missing blocks.`,
|
|
947
1073
|
{
|
|
948
1074
|
previousCheckpointNumber,
|
|
1075
|
+
previousCheckpoint: previousCheckpoint?.header.toInspect(),
|
|
1076
|
+
lastFinalizedCheckpoint: lastFinalizedCheckpoint?.header.toInspect(),
|
|
1077
|
+
l1StartBlock: this.l1Constants.l1StartBlock,
|
|
949
1078
|
newCheckpointNumber,
|
|
950
1079
|
updatedL1SyncPoint,
|
|
951
1080
|
},
|
|
@@ -966,26 +1095,33 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
966
1095
|
});
|
|
967
1096
|
}
|
|
968
1097
|
lastRetrievedCheckpoint = validCheckpoints.at(-1) ?? lastRetrievedCheckpoint;
|
|
969
|
-
|
|
1098
|
+
lastSeenCheckpoint = publishedCheckpoints.at(-1) ?? lastSeenCheckpoint;
|
|
970
1099
|
} while (searchEndBlock < currentL1BlockNumber);
|
|
971
1100
|
|
|
972
1101
|
// Important that we update AFTER inserting the blocks.
|
|
973
1102
|
await updateProvenCheckpoint();
|
|
974
1103
|
|
|
975
|
-
return { ...rollupStatus, lastRetrievedCheckpoint,
|
|
1104
|
+
return { ...rollupStatus, lastRetrievedCheckpoint, lastSeenCheckpoint };
|
|
976
1105
|
}
|
|
977
1106
|
|
|
978
|
-
/**
|
|
1107
|
+
/**
|
|
1108
|
+
* Checks if a specific checkpoint matches a local pending entry, and if so, loads local data to build
|
|
1109
|
+
* a synthetic published checkpoint (skipping blob fetch).
|
|
1110
|
+
*
|
|
1111
|
+
* Returns { diverged: true, fromCheckpointNumber } when the L1 checkpoint does NOT match local pending
|
|
1112
|
+
* data for that number, so the caller can evict the entire pending suffix >= fromCheckpointNumber
|
|
1113
|
+
* (those entries chain off the now-invalid local state) within the same addCheckpoints transaction.
|
|
1114
|
+
*/
|
|
979
1115
|
private async tryBuildPublishedCheckpointFromProposed(
|
|
980
1116
|
calldataCheckpoint: RetrievedCheckpointFromCalldata | undefined,
|
|
981
|
-
): Promise<PublishedCheckpoint | undefined> {
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
) {
|
|
1117
|
+
): Promise<PublishedCheckpoint | { diverged: true; fromCheckpointNumber: CheckpointNumber } | undefined> {
|
|
1118
|
+
if (this.config.skipPromoteProposedCheckpointDuringL1Sync || !calldataCheckpoint) {
|
|
1119
|
+
return undefined;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Look up the specific pending entry for the checkpoint being mined, not just the tip
|
|
1123
|
+
const proposed = await this.stores.blocks.getProposedCheckpointByNumber(calldataCheckpoint.checkpointNumber);
|
|
1124
|
+
if (!proposed) {
|
|
989
1125
|
return undefined;
|
|
990
1126
|
}
|
|
991
1127
|
|
|
@@ -1004,7 +1140,21 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
1004
1140
|
calldataArchiveRoot: calldataCheckpoint.archiveRoot.toString(),
|
|
1005
1141
|
},
|
|
1006
1142
|
);
|
|
1007
|
-
|
|
1143
|
+
// Both the locally-proposed checkpoint and the L1-confirmed one are signed by the
|
|
1144
|
+
// slot proposer; emit a divergence event so the slasher can attribute equivocation.
|
|
1145
|
+
// Only emit when the slots match — uncheckpointed entries are pruned above so this
|
|
1146
|
+
// should always hold, but guard defensively to avoid mis-attributing a slash.
|
|
1147
|
+
if (proposed.header.slotNumber === calldataCheckpoint.header.slotNumber) {
|
|
1148
|
+
this.events.emit(L2BlockSourceEvents.CheckpointEquivocationDetected, {
|
|
1149
|
+
type: L2BlockSourceEvents.CheckpointEquivocationDetected,
|
|
1150
|
+
slotNumber: calldataCheckpoint.header.slotNumber,
|
|
1151
|
+
checkpointNumber: calldataCheckpoint.checkpointNumber,
|
|
1152
|
+
l1ArchiveRoot: calldataCheckpoint.archiveRoot,
|
|
1153
|
+
proposedArchiveRoot: proposed.archive.root,
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
// Return a divergence signal so the caller can evict pending >= this number
|
|
1157
|
+
return { diverged: true, fromCheckpointNumber: proposed.checkpointNumber };
|
|
1008
1158
|
}
|
|
1009
1159
|
|
|
1010
1160
|
this.log.debug(
|
|
@@ -1012,7 +1162,10 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
1012
1162
|
{ proposedHeader: proposed.header.toInspect(), proposedArchiveRoot: proposed.archive.root.toString() },
|
|
1013
1163
|
);
|
|
1014
1164
|
|
|
1015
|
-
const blocks = await this.
|
|
1165
|
+
const blocks = await this.stores.blocks.getBlocks({
|
|
1166
|
+
from: BlockNumber(proposed.startBlock),
|
|
1167
|
+
limit: proposed.blockCount,
|
|
1168
|
+
});
|
|
1016
1169
|
if (blocks.length !== proposed.blockCount) {
|
|
1017
1170
|
this.log.warn(
|
|
1018
1171
|
`Local proposed checkpoint ${proposed.checkpointNumber} has wrong block count (expected ${proposed.blockCount} blocks starting at ${proposed.startBlock} but got ${blocks.length})`,
|
|
@@ -1048,39 +1201,44 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
1048
1201
|
blocksSynchedTo: bigint,
|
|
1049
1202
|
currentL1BlockNumber: bigint,
|
|
1050
1203
|
): Promise<void> {
|
|
1051
|
-
const {
|
|
1052
|
-
// Compare the last checkpoint we have (either retrieved in this round or loaded from store)
|
|
1053
|
-
// rollup contract told us was the latest one (pinned at the currentL1BlockNumber).
|
|
1204
|
+
const { lastSeenCheckpoint, pendingCheckpointNumber } = status;
|
|
1205
|
+
// Compare the last checkpoint (valid or not) we have (either retrieved in this round or loaded from store)
|
|
1206
|
+
// with what the rollup contract told us was the latest one (pinned at the currentL1BlockNumber).
|
|
1054
1207
|
const latestLocalCheckpointNumber =
|
|
1055
|
-
|
|
1208
|
+
lastSeenCheckpoint?.checkpoint.number ??
|
|
1209
|
+
CheckpointNumber.max(
|
|
1210
|
+
await this.stores.blocks.getLatestCheckpointNumber(),
|
|
1211
|
+
await this.stores.blocks.getLatestRejectedCheckpointNumber(),
|
|
1212
|
+
) ??
|
|
1213
|
+
CheckpointNumber.ZERO;
|
|
1214
|
+
|
|
1056
1215
|
if (latestLocalCheckpointNumber < pendingCheckpointNumber) {
|
|
1057
1216
|
// Here we have consumed all logs until the `currentL1Block` we pinned at the beginning of the archiver loop,
|
|
1058
1217
|
// but still haven't reached the pending checkpoint according to the call to the rollup contract.
|
|
1059
1218
|
// We suspect an L1 reorg that added checkpoints *behind* us. If that is the case, it must have happened between
|
|
1060
1219
|
// the last checkpoint we saw and the current one, so we reset the last synched L1 block number. In the edge case
|
|
1061
1220
|
// we don't have one, we go back 2 L1 epochs, which is the deepest possible reorg (assuming Casper is working).
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
}
|
|
1221
|
+
const latestLocalCheckpoint: PublishedCheckpoint | CheckpointData | RejectedCheckpoint | undefined =
|
|
1222
|
+
lastSeenCheckpoint ??
|
|
1223
|
+
(await this.stores.blocks.getCheckpointData(latestLocalCheckpointNumber)) ??
|
|
1224
|
+
(await this.stores.blocks.getRejectedCheckpointByNumber(latestLocalCheckpointNumber));
|
|
1225
|
+
|
|
1226
|
+
const targetL1BlockNumber =
|
|
1227
|
+
latestLocalCheckpoint?.l1.blockNumber ??
|
|
1228
|
+
maxBigint(currentL1BlockNumber - 64n, this.l1Constants.l1StartBlock, 0n);
|
|
1229
|
+
|
|
1072
1230
|
this.log.warn(
|
|
1073
1231
|
`Failed to reach checkpoint ${pendingCheckpointNumber} at ${currentL1BlockNumber} (latest is ${latestLocalCheckpointNumber}). ` +
|
|
1074
1232
|
`Rolling back last synched L1 block number to ${targetL1BlockNumber}.`,
|
|
1075
1233
|
{
|
|
1076
1234
|
latestLocalCheckpointNumber,
|
|
1077
|
-
|
|
1235
|
+
latestLocalCheckpointL1: latestLocalCheckpoint?.l1,
|
|
1078
1236
|
blocksSynchedTo,
|
|
1079
1237
|
currentL1BlockNumber,
|
|
1080
1238
|
...status,
|
|
1081
1239
|
},
|
|
1082
1240
|
);
|
|
1083
|
-
await this.
|
|
1241
|
+
await this.stores.blocks.setSynchedL1BlockNumber(targetL1BlockNumber);
|
|
1084
1242
|
} else {
|
|
1085
1243
|
this.log.trace(`No new checkpoints behind L1 sync point to retrieve.`, {
|
|
1086
1244
|
latestLocalCheckpointNumber,
|
|
@@ -1090,7 +1248,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
1090
1248
|
}
|
|
1091
1249
|
|
|
1092
1250
|
private async getCheckpointHeader(number: CheckpointNumber) {
|
|
1093
|
-
const checkpoint = await this.
|
|
1251
|
+
const checkpoint = await this.stores.blocks.getCheckpointData(number);
|
|
1094
1252
|
if (!checkpoint) {
|
|
1095
1253
|
return undefined;
|
|
1096
1254
|
}
|