@aztec/prover-node 5.0.0-rc.1 → 5.0.0
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 +73 -68
- package/dest/actions/rerun-epoch-proving-job.d.ts +2 -2
- package/dest/actions/rerun-epoch-proving-job.d.ts.map +1 -1
- package/dest/checkpoint-store.d.ts +42 -37
- package/dest/checkpoint-store.d.ts.map +1 -1
- package/dest/checkpoint-store.js +65 -77
- package/dest/job/checkpoint-prover.d.ts +6 -16
- package/dest/job/checkpoint-prover.d.ts.map +1 -1
- package/dest/job/checkpoint-prover.js +15 -35
- package/dest/job/epoch-session.d.ts +2 -2
- package/dest/job/epoch-session.d.ts.map +1 -1
- package/dest/job/epoch-session.js +20 -9
- package/dest/metrics.d.ts +7 -4
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +14 -6
- package/dest/proof-publishing-service.d.ts +4 -2
- package/dest/proof-publishing-service.d.ts.map +1 -1
- package/dest/proof-publishing-service.js +1 -0
- package/dest/prover-node-publisher.d.ts +5 -5
- package/dest/prover-node-publisher.d.ts.map +1 -1
- package/dest/prover-node-publisher.js +2 -3
- package/dest/prover-node.d.ts +25 -15
- package/dest/prover-node.d.ts.map +1 -1
- package/dest/prover-node.js +152 -51
- package/dest/session-manager.d.ts +2 -2
- package/dest/session-manager.d.ts.map +1 -1
- package/dest/session-manager.js +38 -8
- package/package.json +23 -23
- package/src/checkpoint-store.ts +66 -85
- package/src/job/checkpoint-prover.ts +17 -40
- package/src/job/epoch-session.ts +21 -9
- package/src/metrics.ts +15 -12
- package/src/proof-publishing-service.ts +4 -1
- package/src/prover-node-publisher.ts +11 -10
- package/src/prover-node.ts +170 -60
- package/src/session-manager.ts +38 -7
package/src/prover-node.ts
CHANGED
|
@@ -5,14 +5,16 @@ import { BlockNumber, CheckpointNumber, EpochNumber } from '@aztec/foundation/br
|
|
|
5
5
|
import { assertRequired, compact, pick } from '@aztec/foundation/collection';
|
|
6
6
|
import { memoize } from '@aztec/foundation/decorators';
|
|
7
7
|
import { createLogger } from '@aztec/foundation/log';
|
|
8
|
+
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
8
9
|
import { DateProvider, executeTimeout } from '@aztec/foundation/timer';
|
|
9
10
|
import type { EpochProverFactory } from '@aztec/prover-client';
|
|
10
11
|
import { getLastSiblingPath } from '@aztec/prover-client/helpers';
|
|
11
12
|
import { ChonkCache } from '@aztec/prover-client/orchestrator';
|
|
12
13
|
import { PublicProcessorFactory } from '@aztec/simulator/server';
|
|
13
14
|
import {
|
|
15
|
+
EventDrivenL2BlockStream,
|
|
16
|
+
type L2BlockId,
|
|
14
17
|
type L2BlockSource,
|
|
15
|
-
L2BlockStream,
|
|
16
18
|
type L2BlockStreamEvent,
|
|
17
19
|
type L2BlockStreamEventHandler,
|
|
18
20
|
L2TipsMemoryStore,
|
|
@@ -28,7 +30,6 @@ import {
|
|
|
28
30
|
type ITxProvider,
|
|
29
31
|
type ProverNodeApi,
|
|
30
32
|
type Service,
|
|
31
|
-
type WorldStateSyncStatus,
|
|
32
33
|
type WorldStateSynchronizer,
|
|
33
34
|
tryStop,
|
|
34
35
|
} from '@aztec/stdlib/interfaces/server';
|
|
@@ -86,7 +87,7 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
|
|
|
86
87
|
/** In-memory store for the L2BlockStream's local data provider. */
|
|
87
88
|
private tipsStore: L2TipsMemoryStore;
|
|
88
89
|
/** Block stream for checkpoint and reorg detection. */
|
|
89
|
-
private blockStream:
|
|
90
|
+
private blockStream: EventDrivenL2BlockStream | undefined;
|
|
90
91
|
/**
|
|
91
92
|
* Highest epoch whose proof-submission window has passed. Monotonic high-water mark.
|
|
92
93
|
* Seeded from the last fully-proven epoch at start(); advanced on every block-stream
|
|
@@ -95,6 +96,17 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
|
|
|
95
96
|
*/
|
|
96
97
|
protected lastExpiredEpoch: EpochNumber | undefined;
|
|
97
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Highest checkpoint number whose proving-side handling has completed (or that was legitimately skipped).
|
|
101
|
+
* The catch-up loop walks from here to each `chain-checkpointed` tip event. Seeded at start() from the last
|
|
102
|
+
* checkpoint of the last fully-proven epoch (or 0), so a restart reprocesses the partially-proven epoch rather
|
|
103
|
+
* than trusting a checkpointed tip that may sit ahead of unproven checkpoints. Clamped down on a prune.
|
|
104
|
+
*/
|
|
105
|
+
protected lastProcessedCheckpoint: CheckpointNumber = CheckpointNumber.ZERO;
|
|
106
|
+
|
|
107
|
+
/** Periodic tick that runs the epoch-expiry sweep during idle periods when no block-stream events arrive. */
|
|
108
|
+
private expiryTicker: RunningPromise | undefined;
|
|
109
|
+
|
|
98
110
|
public readonly tracer: Tracer;
|
|
99
111
|
|
|
100
112
|
protected publishingService: ProofPublishingService | undefined;
|
|
@@ -157,7 +169,6 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
|
|
|
157
169
|
txGatheringTimeoutMs: this.config.txGatheringTimeoutMs,
|
|
158
170
|
deadline: undefined,
|
|
159
171
|
},
|
|
160
|
-
{ slotWatcherPollIntervalMs: this.config.proverNodePollingIntervalMs },
|
|
161
172
|
this.log.getBindings(),
|
|
162
173
|
);
|
|
163
174
|
}
|
|
@@ -198,17 +209,6 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
|
|
|
198
209
|
return this.sessionManager;
|
|
199
210
|
}
|
|
200
211
|
|
|
201
|
-
/** Returns world state status. */
|
|
202
|
-
public async getWorldStateSyncStatus(): Promise<WorldStateSyncStatus> {
|
|
203
|
-
const { syncSummary } = await this.worldState.status();
|
|
204
|
-
return syncSummary;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/** Returns archiver status. */
|
|
208
|
-
public getL2Tips() {
|
|
209
|
-
return this.l2BlockSource.getL2Tips();
|
|
210
|
-
}
|
|
211
|
-
|
|
212
212
|
/** Returns the underlying prover instance. */
|
|
213
213
|
public getProver() {
|
|
214
214
|
return this.prover;
|
|
@@ -219,17 +219,25 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
|
|
|
219
219
|
public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise<void> {
|
|
220
220
|
switch (event.type) {
|
|
221
221
|
case 'chain-checkpointed':
|
|
222
|
-
await this.
|
|
222
|
+
await this.processCheckpointJump(event.checkpoint.number);
|
|
223
223
|
break;
|
|
224
224
|
case 'chain-pruned':
|
|
225
|
-
await this.handlePruneEvent(event.
|
|
225
|
+
await this.handlePruneEvent(event.block);
|
|
226
226
|
break;
|
|
227
227
|
case 'chain-proven':
|
|
228
228
|
this.publishingService?.onChainProven(BlockNumber(event.block.number));
|
|
229
229
|
break;
|
|
230
|
+
// The proposed tip drives only the tips store's walk-back history (recorded below); the prover-node
|
|
231
|
+
// tracks checkpoints, not proposed blocks. `blocks-added` is never emitted in tips-only mode, and
|
|
232
|
+
// `chain-finalized` carries nothing the prover-node acts on.
|
|
233
|
+
case 'chain-proposed':
|
|
230
234
|
case 'chain-finalized':
|
|
231
235
|
case 'blocks-added':
|
|
232
236
|
break;
|
|
237
|
+
default: {
|
|
238
|
+
const _: never = event;
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
233
241
|
}
|
|
234
242
|
// Expiry is driven by the archiver's latest synced L2 slot
|
|
235
243
|
await this.checkEpochExpiry();
|
|
@@ -239,34 +247,92 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
|
|
|
239
247
|
await this.tipsStore.handleBlockStreamEvent(event);
|
|
240
248
|
}
|
|
241
249
|
|
|
242
|
-
/**
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
250
|
+
/**
|
|
251
|
+
* Walks every checkpoint between the local cursor and the newly-reported checkpointed tip, registering
|
|
252
|
+
* each one that belongs to an epoch that can still be proven. The block stream now delivers a single thin
|
|
253
|
+
* `chain-checkpointed` tip event per pass rather than one fat event per checkpoint, so this drives the
|
|
254
|
+
* catch-up itself: light metadata first (`getCheckpointsData`) to decide relevance per epoch, then a heavy
|
|
255
|
+
* `getCheckpoints` fetch only for checkpoints in provable epochs.
|
|
256
|
+
*
|
|
257
|
+
* The cursor advances one checkpoint at a time and only after that checkpoint's proving-side handling has
|
|
258
|
+
* fully succeeded, preserving the A-1041 at-least-once semantics: a mid-jump failure leaves the cursor
|
|
259
|
+
* behind so the next pass retries from the first checkpoint that did not complete.
|
|
260
|
+
*/
|
|
261
|
+
private async processCheckpointJump(targetCheckpoint: CheckpointNumber): Promise<void> {
|
|
262
|
+
if (targetCheckpoint <= this.lastProcessedCheckpoint) {
|
|
251
263
|
return;
|
|
252
264
|
}
|
|
265
|
+
const l1Constants = await this.getL1Constants();
|
|
253
266
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
267
|
+
// Cap the catch-up at the `(proofSubmissionEpochs + 1) * epochDuration` most recent checkpoints.
|
|
268
|
+
// When the cursor is much further behind (e.g. resyncing after a long time offline), fetching the whole gap could
|
|
269
|
+
// load thousands of checkpoints we cannot act on: anything older than the last two epochs is already past
|
|
270
|
+
// its proof-submission window, so we skip it and jump the cursor forward to the start of the capped range.
|
|
271
|
+
const maxCheckpoints = (l1Constants.proofSubmissionEpochs + 1) * l1Constants.epochDuration;
|
|
272
|
+
let from = CheckpointNumber(this.lastProcessedCheckpoint + 1);
|
|
273
|
+
if (Number(targetCheckpoint - from) + 1 > maxCheckpoints) {
|
|
274
|
+
const cappedFrom = CheckpointNumber(targetCheckpoint - maxCheckpoints + 1);
|
|
275
|
+
this.log.warn(`Skipping unprovable checkpoints during catch-up; the prover node is far behind`, {
|
|
276
|
+
from,
|
|
277
|
+
cappedFrom,
|
|
278
|
+
targetCheckpoint,
|
|
279
|
+
maxCheckpoints,
|
|
280
|
+
});
|
|
281
|
+
// Advance the cursor past the skipped checkpoints so they are never retried.
|
|
282
|
+
this.lastProcessedCheckpoint = CheckpointNumber(cappedFrom - 1);
|
|
283
|
+
from = cappedFrom;
|
|
259
284
|
}
|
|
285
|
+
const limit = Number(targetCheckpoint - from) + 1;
|
|
286
|
+
const metadatas = await this.l2BlockSource.getCheckpointsData({ from, limit });
|
|
287
|
+
|
|
288
|
+
// Per-epoch relevance is cached so a multi-checkpoint epoch resolves it once. Skipping is whole-epoch
|
|
289
|
+
// only: the SessionManager requires an epoch's checkpoints fully covered before it opens a session, so we
|
|
290
|
+
// never drop an individual checkpoint inside an epoch we will prove.
|
|
291
|
+
const epochSkippable = new Map<EpochNumber, boolean>();
|
|
292
|
+
for (const metadata of metadatas) {
|
|
293
|
+
const epochNumber = getEpochAtSlot(metadata.header.slotNumber, l1Constants);
|
|
294
|
+
let skippable = epochSkippable.get(epochNumber);
|
|
295
|
+
if (skippable === undefined) {
|
|
296
|
+
skippable =
|
|
297
|
+
(await this.isEpochFullyProven(epochNumber, l1Constants)) ||
|
|
298
|
+
(await this.isEpochPastProofSubmissionWindow(epochNumber, l1Constants));
|
|
299
|
+
epochSkippable.set(epochNumber, skippable);
|
|
300
|
+
}
|
|
301
|
+
if (skippable) {
|
|
302
|
+
this.log.debug(`Skipping checkpoint ${metadata.checkpointNumber} for unprovable epoch ${epochNumber}`);
|
|
303
|
+
} else {
|
|
304
|
+
await this.registerCheckpoint(metadata.checkpointNumber, epochNumber);
|
|
305
|
+
}
|
|
306
|
+
// Advance only after the checkpoint's handling succeeded (or it was legitimately skipped). registerCheckpoint
|
|
307
|
+
// throws on failure, which leaves the cursor here for the next pass to retry (A-1041).
|
|
308
|
+
this.lastProcessedCheckpoint = metadata.checkpointNumber;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
260
311
|
|
|
312
|
+
/** Heavy-fetch a single checkpoint, register it with the store, and notify the session manager. */
|
|
313
|
+
private async registerCheckpoint(checkpointNumber: CheckpointNumber, epochNumber: EpochNumber): Promise<void> {
|
|
314
|
+
const published = await this.l2BlockSource.getCheckpoint({ number: checkpointNumber });
|
|
315
|
+
if (!published) {
|
|
316
|
+
throw new Error(`Checkpoint ${checkpointNumber} not found in block source during catch-up`);
|
|
317
|
+
}
|
|
318
|
+
const checkpoint = published.checkpoint;
|
|
261
319
|
this.log.info(`New checkpoint ${checkpoint.number} for epoch ${epochNumber}`, {
|
|
262
320
|
checkpointNumber: checkpoint.number,
|
|
263
321
|
epochNumber,
|
|
264
|
-
slotNumber,
|
|
322
|
+
slotNumber: checkpoint.header.slotNumber,
|
|
265
323
|
});
|
|
266
324
|
|
|
267
|
-
const registerData = await this.collectRegisterData(checkpoint,
|
|
325
|
+
const registerData = await this.collectRegisterData(checkpoint, published.attestations);
|
|
268
326
|
await this.checkpointStore.addOrUpdate(checkpoint, registerData);
|
|
269
327
|
await this.sessionManager?.onCheckpointAdded(epochNumber);
|
|
328
|
+
|
|
329
|
+
// Tips-only mode delivers no blocks, so record one witness per checkpointed block: a reorg into the checkpoint's
|
|
330
|
+
// range then prunes at the true divergence instead of the nearest sparse tip anchor.
|
|
331
|
+
await this.tipsStore.recordBlockHashes(
|
|
332
|
+
await Promise.all(
|
|
333
|
+
checkpoint.blocks.map(async block => ({ number: block.number, hash: (await block.header.hash()).toString() })),
|
|
334
|
+
),
|
|
335
|
+
);
|
|
270
336
|
}
|
|
271
337
|
|
|
272
338
|
/**
|
|
@@ -295,10 +361,41 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
|
|
|
295
361
|
};
|
|
296
362
|
}
|
|
297
363
|
|
|
298
|
-
/**
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
364
|
+
/**
|
|
365
|
+
* Marks every prover orphaned by the prune as pruned, clamps the catch-up cursor below the prune target's
|
|
366
|
+
* checkpoint, and notifies the session manager. Keyed off the prune target block (the highest surviving block)
|
|
367
|
+
* rather than the source's checkpointed tip, which can sit above the target after a re-checkpoint and would leave
|
|
368
|
+
* orphaned provers canonical. Throws (rather than warning) if the cursor floor cannot be resolved, so the pass
|
|
369
|
+
* fails and the prune is retried next iteration.
|
|
370
|
+
*/
|
|
371
|
+
private async handlePruneEvent(prunedToBlock: L2BlockId) {
|
|
372
|
+
this.log.warn(`Chain pruned to block ${prunedToBlock.number}`, { prunedToBlock });
|
|
373
|
+
|
|
374
|
+
// Resolve the cursor floor BEFORE removing provers: cancelAndRemoveAboveBlock returns only the provers it removed,
|
|
375
|
+
// so a throw after removing would leave a retry pass with nothing to act on. Resolving first means a throw leaves
|
|
376
|
+
// everything untouched and the next pass retries the whole handler (the tips cursor only advances on success).
|
|
377
|
+
let cursorFloor: CheckpointNumber;
|
|
378
|
+
if (prunedToBlock.number === 0) {
|
|
379
|
+
cursorFloor = CheckpointNumber.ZERO;
|
|
380
|
+
} else {
|
|
381
|
+
const targetData = await this.l2BlockSource.getBlockData({ number: prunedToBlock.number });
|
|
382
|
+
if (targetData === undefined) {
|
|
383
|
+
throw new Error(
|
|
384
|
+
`No block data found for prune target block ${prunedToBlock.number}; cannot clamp checkpoint cursor`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
// Clamp to `cpAtTarget - 1`: a mid-checkpoint target leaves that checkpoint partially orphaned and it must be
|
|
388
|
+
// reprocessed. Over-clamping merely re-registers a checkpoint (at-least-once by design — A-1041); under-clamping
|
|
389
|
+
// would permanently skip a rebuilt same-number checkpoint.
|
|
390
|
+
cursorFloor = CheckpointNumber(Math.max(0, Number(targetData.checkpointNumber) - 1));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const affected = this.checkpointStore.cancelAndRemoveAboveBlock(prunedToBlock.number);
|
|
394
|
+
|
|
395
|
+
if (this.lastProcessedCheckpoint > cursorFloor) {
|
|
396
|
+
this.lastProcessedCheckpoint = cursorFloor;
|
|
397
|
+
}
|
|
398
|
+
|
|
302
399
|
if (affected.length === 0) {
|
|
303
400
|
return;
|
|
304
401
|
}
|
|
@@ -409,14 +506,24 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
|
|
|
409
506
|
// Now that the store + manager exist, arm the live-state observable gauges.
|
|
410
507
|
this.jobMetrics.observeState(this.checkpointStore, this.sessionManager);
|
|
411
508
|
|
|
412
|
-
const {
|
|
509
|
+
const { lastFullyProvenEpoch } = await this.resolveLastFullyProvenEpoch();
|
|
413
510
|
this.lastExpiredEpoch = lastFullyProvenEpoch;
|
|
414
|
-
this.
|
|
511
|
+
this.lastProcessedCheckpoint = await this.computeStartingCheckpoint(lastFullyProvenEpoch);
|
|
512
|
+
this.blockStream = new EventDrivenL2BlockStream(this.l2BlockSource, this.tipsStore, this, this.log, {
|
|
415
513
|
pollIntervalMS: this.config.proverNodePollingIntervalMs,
|
|
416
|
-
|
|
514
|
+
tipsOnly: true,
|
|
417
515
|
});
|
|
418
516
|
this.blockStream.start();
|
|
419
517
|
|
|
518
|
+
// With thin once-per-pass tip events, the expiry sweep no longer fires once per checkpoint; drive it
|
|
519
|
+
// from a periodic tick so epochs still expire during idle/no-event periods.
|
|
520
|
+
this.expiryTicker = new RunningPromise(
|
|
521
|
+
() => this.checkEpochExpiry(),
|
|
522
|
+
this.log,
|
|
523
|
+
this.config.proverNodePollingIntervalMs,
|
|
524
|
+
);
|
|
525
|
+
this.expiryTicker.start();
|
|
526
|
+
|
|
420
527
|
await this.rewardsMetrics.start();
|
|
421
528
|
this.l1Metrics.start();
|
|
422
529
|
this.log.info(`Started Prover Node with prover id ${this.prover.getProverId().toString()}`, this.config);
|
|
@@ -426,6 +533,7 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
|
|
|
426
533
|
this.log.info('Stopping ProverNode');
|
|
427
534
|
this.jobMetrics.stopObservingState();
|
|
428
535
|
await this.blockStream?.stop();
|
|
536
|
+
await this.expiryTicker?.stop();
|
|
429
537
|
if (this.sessionManager) {
|
|
430
538
|
await this.sessionManager.stop();
|
|
431
539
|
}
|
|
@@ -552,38 +660,40 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
|
|
|
552
660
|
}
|
|
553
661
|
|
|
554
662
|
/**
|
|
555
|
-
* Resolves the
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
* `provenEpoch - 1`, or `undefined` if no block is proven yet.
|
|
663
|
+
* Resolves the last fully-proven epoch from L1 proven state, used to seed the catch-up cursor (via
|
|
664
|
+
* `computeStartingCheckpoint`) and `lastExpiredEpoch`. The fully-proven epoch is `provenEpoch` when the
|
|
665
|
+
* proven tip is the last block of its epoch, otherwise `provenEpoch - 1`, or `undefined` if no block is
|
|
666
|
+
* proven yet (so a restart reprocesses the partially-proven epoch rather than trusting a stale tip).
|
|
560
667
|
*/
|
|
561
|
-
protected async
|
|
562
|
-
startingBlock: BlockNumber;
|
|
563
|
-
lastFullyProvenEpoch: EpochNumber | undefined;
|
|
564
|
-
}> {
|
|
668
|
+
protected async resolveLastFullyProvenEpoch(): Promise<{ lastFullyProvenEpoch: EpochNumber | undefined }> {
|
|
565
669
|
const provenBlockNumber = await this.l2BlockSource.getBlockNumber({ tag: 'proven' });
|
|
566
670
|
if (!provenBlockNumber || provenBlockNumber <= 0) {
|
|
567
|
-
return {
|
|
671
|
+
return { lastFullyProvenEpoch: undefined };
|
|
568
672
|
}
|
|
569
673
|
const l1Constants = await this.getL1Constants();
|
|
570
674
|
const provenHeader = (await this.l2BlockSource.getBlockData({ number: BlockNumber(provenBlockNumber) }))?.header;
|
|
571
675
|
if (!provenHeader) {
|
|
572
|
-
return {
|
|
676
|
+
return { lastFullyProvenEpoch: undefined };
|
|
573
677
|
}
|
|
574
678
|
const provenEpoch = getEpochAtSlot(provenHeader.getSlot(), l1Constants);
|
|
575
679
|
if (await this.isProvenBlockLastOfItsEpoch(BlockNumber(provenBlockNumber), provenEpoch, l1Constants)) {
|
|
576
|
-
return {
|
|
680
|
+
return { lastFullyProvenEpoch: provenEpoch };
|
|
577
681
|
}
|
|
578
|
-
const epochCheckpoints = await this.l2BlockSource.getCheckpointsData({ epoch: provenEpoch });
|
|
579
|
-
const firstBlockOfEpoch =
|
|
580
|
-
epochCheckpoints.length > 0 ? epochCheckpoints[0].startBlock : BlockNumber(provenBlockNumber);
|
|
581
|
-
this.log.info(
|
|
582
|
-
`Starting L2BlockStream at block ${firstBlockOfEpoch} (start of partially-proven epoch ${provenEpoch})`,
|
|
583
|
-
{ provenBlockNumber, provenEpoch, firstBlockOfEpoch },
|
|
584
|
-
);
|
|
585
682
|
const lastFullyProvenEpoch = provenEpoch > 0 ? EpochNumber(provenEpoch - 1) : undefined;
|
|
586
|
-
return {
|
|
683
|
+
return { lastFullyProvenEpoch };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Resolves the catch-up cursor seed: the last checkpoint of the last fully-proven epoch, or 0 if none. Seeding
|
|
688
|
+
* from a checkpoint (rather than a checkpointed tip) guarantees a restart reprocesses every checkpoint of the
|
|
689
|
+
* partially-proven epoch, since the checkpointed tip can sit ahead of the last fully-proven checkpoint.
|
|
690
|
+
*/
|
|
691
|
+
protected async computeStartingCheckpoint(lastFullyProvenEpoch: EpochNumber | undefined): Promise<CheckpointNumber> {
|
|
692
|
+
if (lastFullyProvenEpoch === undefined) {
|
|
693
|
+
return CheckpointNumber.ZERO;
|
|
694
|
+
}
|
|
695
|
+
const checkpoints = await this.l2BlockSource.getCheckpointsData({ epoch: lastFullyProvenEpoch });
|
|
696
|
+
return checkpoints.at(-1)?.checkpointNumber ?? CheckpointNumber.ZERO;
|
|
587
697
|
}
|
|
588
698
|
|
|
589
699
|
private async gatherPreviousBlockHeader(previousBlockNumber: number) {
|
package/src/session-manager.ts
CHANGED
|
@@ -185,7 +185,7 @@ export class SessionManager {
|
|
|
185
185
|
* slot. Dedupes against any existing session covering the same range, returning its id.
|
|
186
186
|
*/
|
|
187
187
|
public async startProof(epoch: EpochNumber): Promise<string> {
|
|
188
|
-
const canonical = await this.deps.checkpointStore.
|
|
188
|
+
const canonical = await this.deps.checkpointStore.listForEpoch(epoch);
|
|
189
189
|
if (canonical.length === 0) {
|
|
190
190
|
throw new EmptyEpochError(epoch);
|
|
191
191
|
}
|
|
@@ -268,7 +268,7 @@ export class SessionManager {
|
|
|
268
268
|
this.fullSessions.delete(key);
|
|
269
269
|
continue;
|
|
270
270
|
}
|
|
271
|
-
const canonical = this.
|
|
271
|
+
const canonical = this.checkpointsForSpec(session.getSpec());
|
|
272
272
|
if (!this.checkpointsMatch(session.getCheckpoints(), canonical)) {
|
|
273
273
|
this.fireAndForgetCancel(session, 'canonical content changed');
|
|
274
274
|
this.fullSessions.delete(key);
|
|
@@ -284,7 +284,7 @@ export class SessionManager {
|
|
|
284
284
|
this.partialSessions.delete(key);
|
|
285
285
|
continue;
|
|
286
286
|
}
|
|
287
|
-
const canonical = this.
|
|
287
|
+
const canonical = this.checkpointsForSpec(session.getSpec());
|
|
288
288
|
if (!this.checkpointsMatch(session.getCheckpoints(), canonical)) {
|
|
289
289
|
this.fireAndForgetCancel(session, 'canonical content changed');
|
|
290
290
|
this.partialSessions.delete(key);
|
|
@@ -298,6 +298,8 @@ export class SessionManager {
|
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
private async openFullSessionIfReady(epoch: EpochNumber): Promise<void> {
|
|
301
|
+
// `recreateInvalidSessions` runs at the top of every reconcile and deletes terminal sessions
|
|
302
|
+
// before this is called, so a session present here is live and already covers the epoch.
|
|
301
303
|
if (this.fullSessions.has(epoch)) {
|
|
302
304
|
return;
|
|
303
305
|
}
|
|
@@ -314,7 +316,7 @@ export class SessionManager {
|
|
|
314
316
|
return;
|
|
315
317
|
}
|
|
316
318
|
const [fromSlot, toSlot] = getSlotRangeForEpoch(epoch, l1Constants);
|
|
317
|
-
const canonical = this.deps.checkpointStore.
|
|
319
|
+
const canonical = this.deps.checkpointStore.listInSlotRange(fromSlot, toSlot);
|
|
318
320
|
if (!this.archiverFullyCovered(archiverCps, canonical)) {
|
|
319
321
|
this.log.debug(`Skipping full-session open for epoch ${epoch}: archiver checkpoints not all in store`, {
|
|
320
322
|
archiverCount: archiverCps.length,
|
|
@@ -329,7 +331,7 @@ export class SessionManager {
|
|
|
329
331
|
}
|
|
330
332
|
|
|
331
333
|
private openPartialSession(spec: SessionSpec): void {
|
|
332
|
-
const canonical = this.deps.checkpointStore.
|
|
334
|
+
const canonical = this.deps.checkpointStore.listInSlotRange(spec.fromSlot, spec.toSlot);
|
|
333
335
|
if (canonical.length === 0) {
|
|
334
336
|
return;
|
|
335
337
|
}
|
|
@@ -403,6 +405,17 @@ export class SessionManager {
|
|
|
403
405
|
const state = await session.start();
|
|
404
406
|
this.log.info(`Session ${session.getId()} exited with state ${state}`);
|
|
405
407
|
if (state === 'failed' && this.deps.onSessionFailed) {
|
|
408
|
+
// Best-effort suppression of the spurious post-mortem upload a prune produces: if the session's
|
|
409
|
+
// checkpoints no longer match the store's current set, the failure was caused by the content
|
|
410
|
+
// changing under it, not a genuine proving fault, so skip the upload. This is inherently racy —
|
|
411
|
+
// the store lags the world-state unwind, so a fault observed before the prune is reconciled here
|
|
412
|
+
// still uploads. The epoch is recovered regardless by recreating the session on re-add.
|
|
413
|
+
if (!this.checkpointsMatch(session.getCheckpoints(), this.checkpointsForSpec(session.getSpec()))) {
|
|
414
|
+
this.log.info(`Skipping failure upload for session ${session.getId()}: canonical content changed`, {
|
|
415
|
+
...session.getSpec(),
|
|
416
|
+
});
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
406
419
|
try {
|
|
407
420
|
await this.deps.onSessionFailed(session);
|
|
408
421
|
} catch (err) {
|
|
@@ -447,6 +460,24 @@ export class SessionManager {
|
|
|
447
460
|
return live >= max;
|
|
448
461
|
}
|
|
449
462
|
|
|
463
|
+
/**
|
|
464
|
+
* Maps a reconcile trigger to the epochs whose full session should be (re)opened.
|
|
465
|
+
*
|
|
466
|
+
* This is where the "don't retry a genuinely-failed epoch, but do recover a pruned one" invariant
|
|
467
|
+
* lives — enforced by which triggers are gated by `lastTickEpoch`:
|
|
468
|
+
*
|
|
469
|
+
* - The periodic `tick` IS gated: once a tick has opened a session for an epoch, `lastTickEpoch`
|
|
470
|
+
* advances to it and later ticks skip it (`epoch <= lastTickEpoch`). So a failed attempt is never
|
|
471
|
+
* resubmitted on a loop by the tick.
|
|
472
|
+
* - `checkpoint` and `prune` are deliberately NOT gated. They only fire when the epoch's canonical
|
|
473
|
+
* content actually changes — a checkpoint arrives, or a reorg prunes/replaces one — which is
|
|
474
|
+
* exactly when re-attempting is correct.
|
|
475
|
+
*
|
|
476
|
+
* A genuine proving failure produces no content change, hence no checkpoint/prune event, so only
|
|
477
|
+
* the gated tick could reopen it — and it won't. A prune + re-add fires ungated events, so the
|
|
478
|
+
* epoch is reopened through this path (and `openFullSessionIfReady` rebuilds over the fresh
|
|
479
|
+
* provers). See the "onTick does not retry ... but recovers ... re-added" test.
|
|
480
|
+
*/
|
|
450
481
|
private async epochsForTrigger(trigger: ReconcileTrigger): Promise<EpochNumber[]> {
|
|
451
482
|
switch (trigger.kind) {
|
|
452
483
|
case 'checkpoint':
|
|
@@ -481,8 +512,8 @@ export class SessionManager {
|
|
|
481
512
|
return getEpochAtSlot(header.getSlot(), await this.getL1Constants());
|
|
482
513
|
}
|
|
483
514
|
|
|
484
|
-
private
|
|
485
|
-
return this.deps.checkpointStore.
|
|
515
|
+
private checkpointsForSpec(spec: SessionSpec): CheckpointProver[] {
|
|
516
|
+
return this.deps.checkpointStore.listInSlotRange(spec.fromSlot, spec.toSlot);
|
|
486
517
|
}
|
|
487
518
|
|
|
488
519
|
private fireAndForgetCancel(session: EpochSession, reason: string): void {
|