@aztec/prover-node 5.1.0 → 5.2.0-nightly.20260724
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 +95 -34
- package/dest/actions/rerun-epoch-proving-job.d.ts +11 -2
- package/dest/actions/rerun-epoch-proving-job.d.ts.map +1 -1
- package/dest/actions/rerun-epoch-proving-job.js +83 -41
- package/dest/checkpoint-store.d.ts +9 -2
- package/dest/checkpoint-store.d.ts.map +1 -1
- package/dest/checkpoint-store.js +9 -0
- package/dest/job/checkpoint-prover.d.ts +34 -4
- package/dest/job/checkpoint-prover.d.ts.map +1 -1
- package/dest/job/checkpoint-prover.js +41 -8
- package/dest/job/epoch-session.d.ts +14 -2
- package/dest/job/epoch-session.d.ts.map +1 -1
- package/dest/job/epoch-session.js +24 -2
- package/dest/prover-node.d.ts +34 -7
- package/dest/prover-node.d.ts.map +1 -1
- package/dest/prover-node.js +78 -17
- package/dest/session-manager.d.ts +16 -16
- package/dest/session-manager.d.ts.map +1 -1
- package/dest/session-manager.js +62 -62
- package/package.json +23 -23
- package/src/actions/rerun-epoch-proving-job.ts +124 -65
- package/src/checkpoint-store.ts +20 -2
- package/src/job/checkpoint-prover.ts +61 -7
- package/src/job/epoch-session.ts +26 -2
- package/src/prover-node.ts +94 -19
- package/src/session-manager.ts +70 -66
|
@@ -38,9 +38,25 @@ export type CheckpointProverDeps = {
|
|
|
38
38
|
txGatheringTimeoutMs: number;
|
|
39
39
|
/** Public processor deadline. */
|
|
40
40
|
deadline: Date | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Fired once when the prover's block proofs reject for a genuine (non-cancel) reason — a sub-tree
|
|
43
|
+
* fault or a prune-induced fork fault. Useful for performing post mortem on failures.
|
|
44
|
+
*/
|
|
45
|
+
onFailed?: (prover: CheckpointProver) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Test-only hook: if set, invoked at the start of checkpoint execution instead of proving. Lets e2e
|
|
48
|
+
* tests force a sub-tree failure (it should throw) to exercise the checkpoint failure/upload path.
|
|
49
|
+
*/
|
|
50
|
+
checkpointProveOverride?: () => Promise<never>;
|
|
41
51
|
log: Logger;
|
|
42
52
|
};
|
|
43
53
|
|
|
54
|
+
/** Test-only hooks the store injects into every `CheckpointProver` it constructs. */
|
|
55
|
+
export type CheckpointProverTestHooks = {
|
|
56
|
+
/** If set, invoked at the start of checkpoint execution instead of proving; should throw to fail. */
|
|
57
|
+
checkpointProveOverride?: () => Promise<never>;
|
|
58
|
+
};
|
|
59
|
+
|
|
44
60
|
/** Inputs that fully describe a checkpoint at register time. */
|
|
45
61
|
export type CheckpointProverArgs = {
|
|
46
62
|
checkpoint: Checkpoint;
|
|
@@ -89,9 +105,18 @@ export class CheckpointProver {
|
|
|
89
105
|
/** Resolved by the sub-tree on success, rejected on cancel/failure. */
|
|
90
106
|
private readonly blockProofs: PromiseWithResolvers<SubTreeResult['blockProofOutputs']> = promiseWithResolvers();
|
|
91
107
|
|
|
108
|
+
// Three independent lifecycle facts — deliberately not collapsed into one status enum, because several
|
|
109
|
+
// combinations are legal and relied on: a prover can be `completed` and then `cancelled` (routine
|
|
110
|
+
// teardown of an already-proven checkpoint), or `completed` and then `failed` (block proving was
|
|
111
|
+
// enqueued, but the sub-tree subsequently faulted). Only `failed` + `cancelled` is excluded — a cancel
|
|
112
|
+
// is not a failure (enforced in `failBlockProofs`).
|
|
113
|
+
/** Block-level proving was fully *enqueued* (a progress marker; the sub-tree may still be proving). */
|
|
114
|
+
private completed = false;
|
|
115
|
+
/** Block proofs rejected for a genuine (non-cancel) reason — a sub-tree or prune-induced fork fault. */
|
|
116
|
+
private failed = false;
|
|
117
|
+
/** The prover was torn down (prune / reap / shutdown). */
|
|
92
118
|
private cancelled = false;
|
|
93
119
|
private subTree?: CheckpointSubTreeOrchestrator;
|
|
94
|
-
private completed = false;
|
|
95
120
|
private readonly abortController = new AbortController();
|
|
96
121
|
|
|
97
122
|
/** Tracks the eager gather+execute task so `cancel()` and `whenDone()` can await its unwind. */
|
|
@@ -139,9 +164,14 @@ export class CheckpointProver {
|
|
|
139
164
|
return this.cancelled;
|
|
140
165
|
}
|
|
141
166
|
|
|
142
|
-
/**
|
|
143
|
-
|
|
144
|
-
|
|
167
|
+
/**
|
|
168
|
+
* True once this prover's block proofs have rejected for a genuine (non-cancel) reason — a sub-tree
|
|
169
|
+
* proving fault or a prune-induced world-state fork fault. A failed prover cannot produce its block
|
|
170
|
+
* proofs, so the reconciler must not build (or rebuild) an EpochSession over it; it is cleared only by
|
|
171
|
+
* a prune/re-add replacing it with a fresh prover, or by expiry reaping it.
|
|
172
|
+
*/
|
|
173
|
+
public isFailed(): boolean {
|
|
174
|
+
return this.failed;
|
|
145
175
|
}
|
|
146
176
|
|
|
147
177
|
/** AbortSignal that fires on cancel — for callers that want to wire their own tasks. */
|
|
@@ -179,10 +209,29 @@ export class CheckpointProver {
|
|
|
179
209
|
this.deps.log.error(`Error in CheckpointProver ${this.id}`, err, {
|
|
180
210
|
checkpointNumber: this.checkpoint.number,
|
|
181
211
|
});
|
|
182
|
-
this.
|
|
212
|
+
this.failBlockProofs(err instanceof Error ? err : new Error(String(err)));
|
|
183
213
|
}
|
|
184
214
|
}
|
|
185
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Rejects the block-proof promise and, unless this is a cancellation, records the prover as failed so
|
|
218
|
+
* the reconciler won't build an EpochSession over it. First rejection wins, so a later duplicate reject
|
|
219
|
+
* (e.g. the executeCheckpoint `finally`) is a harmless no-op.
|
|
220
|
+
*/
|
|
221
|
+
private failBlockProofs(err: Error): void {
|
|
222
|
+
if (!this.cancelled && !this.failed) {
|
|
223
|
+
this.failed = true;
|
|
224
|
+
// Notify the owner so it can upload a post-mortem for this checkpoint. Fire-and-forget: the
|
|
225
|
+
// callback must not block the prover's teardown, and a throw in it must not mask the rejection.
|
|
226
|
+
try {
|
|
227
|
+
this.deps.onFailed?.(this);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
this.deps.log.error(`Error in CheckpointProver onFailed callback for ${this.id}`, err);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
this.blockProofs.reject(err);
|
|
233
|
+
}
|
|
234
|
+
|
|
186
235
|
private async gatherTxs(): Promise<Map<string, Tx>> {
|
|
187
236
|
const deadline = new Date(this.deps.dateProvider.now() + this.deps.txGatheringTimeoutMs);
|
|
188
237
|
const txsByBlock = await Promise.all(
|
|
@@ -205,6 +254,11 @@ export class CheckpointProver {
|
|
|
205
254
|
let subTreeStarted = false;
|
|
206
255
|
|
|
207
256
|
try {
|
|
257
|
+
// Test hook: force a sub-tree failure to exercise the checkpoint failure/upload path.
|
|
258
|
+
if (this.deps.checkpointProveOverride) {
|
|
259
|
+
await this.deps.checkpointProveOverride();
|
|
260
|
+
}
|
|
261
|
+
|
|
208
262
|
for (const [hash, tx] of txs) {
|
|
209
263
|
this.txs.set(hash, tx);
|
|
210
264
|
}
|
|
@@ -248,7 +302,7 @@ export class CheckpointProver {
|
|
|
248
302
|
this.deps.metrics.recordCheckpointProving(checkpointTimer.ms());
|
|
249
303
|
this.blockProofs.resolve(result.blockProofOutputs);
|
|
250
304
|
},
|
|
251
|
-
err => this.
|
|
305
|
+
err => this.failBlockProofs(err instanceof Error ? err : new Error(String(err))),
|
|
252
306
|
);
|
|
253
307
|
if (signal.aborted) {
|
|
254
308
|
return;
|
|
@@ -328,7 +382,7 @@ export class CheckpointProver {
|
|
|
328
382
|
if (subTreeStarted) {
|
|
329
383
|
await this.teardownSubTree();
|
|
330
384
|
}
|
|
331
|
-
this.
|
|
385
|
+
this.failBlockProofs(new Error(`Checkpoint ${this.id} did not complete block processing`));
|
|
332
386
|
}
|
|
333
387
|
}
|
|
334
388
|
}
|
package/src/job/epoch-session.ts
CHANGED
|
@@ -95,7 +95,11 @@ export type EpochSessionDeps = {
|
|
|
95
95
|
* initialized → awaiting-checkpoints → awaiting-root → publishing-proof → completed
|
|
96
96
|
*
|
|
97
97
|
* Terminal states map the publishing outcome: `published` → `completed`, `superseded` →
|
|
98
|
-
* `superseded`, `
|
|
98
|
+
* `superseded`, `expired` → `timed-out`, `withdrawn` → `cancelled`. A fault ends the attempt in one
|
|
99
|
+
* of two terminal states depending on its cause: `stopped` if a checkpoint prover under it failed
|
|
100
|
+
* (possibly a prune — the reconciler will rebuild over a fresh prover on re-add), or `failed` if the
|
|
101
|
+
* session's own top-tree/submit work failed while every prover was healthy (a genuine, non-prune
|
|
102
|
+
* failure the reconciler retains and uploads — see `hasFailed()`).
|
|
99
103
|
* Additionally, the session-level deadline fires `cancel('deadline')` and transitions
|
|
100
104
|
* to `timed-out` for the pre-submit window (top-tree proving) — the publishing service
|
|
101
105
|
* handles the post-submit window via the candidate's `deadline`.
|
|
@@ -187,6 +191,17 @@ export class EpochSession implements Traceable {
|
|
|
187
191
|
return EpochProvingJobTerminalState.includes(this.state);
|
|
188
192
|
}
|
|
189
193
|
|
|
194
|
+
/**
|
|
195
|
+
* True if the session ended in its own genuine failure — top-tree proving or L1 submission failed
|
|
196
|
+
* while every checkpoint prover succeeded. Because healthy provers rule out a prune-induced fault,
|
|
197
|
+
* this is a race-free "the epoch could not be proven" signal: the reconciler retains such a (full)
|
|
198
|
+
* session rather than re-proving it, and uploads a post-mortem. A `stopped` session (a checkpoint
|
|
199
|
+
* prover failed under it) is NOT a session failure in this sense.
|
|
200
|
+
*/
|
|
201
|
+
public hasFailed(): boolean {
|
|
202
|
+
return this.state === 'failed';
|
|
203
|
+
}
|
|
204
|
+
|
|
190
205
|
/** First block this session proves. */
|
|
191
206
|
public getStartBlockNumber(): BlockNumber {
|
|
192
207
|
return BlockNumber(this.checkpoints[0].checkpoint.blocks[0].number);
|
|
@@ -213,8 +228,17 @@ export class EpochSession implements Traceable {
|
|
|
213
228
|
uuid: this.uuid,
|
|
214
229
|
...this.spec,
|
|
215
230
|
});
|
|
231
|
+
// Distinguish the two ways an attempt can fault:
|
|
232
|
+
// - a checkpoint prover in the set has failed OR was cancelled (a sub-tree fault, a prune-induced
|
|
233
|
+
// fork fault, or a control-plane cancel that reached this catch before the reconcile marked the
|
|
234
|
+
// session 'cancelled'): end in the non-declaring terminal 'stopped'. This is not the session's own
|
|
235
|
+
// failure and may be a prune, so the reconciler does not upload it; a re-add installs a fresh prover.
|
|
236
|
+
// - no prover failed or was cancelled, yet top-tree proving or L1 submission failed: this is the
|
|
237
|
+
// session's own, genuine failure — and, because every prover is healthy and un-cancelled, it is
|
|
238
|
+
// definitively NOT a prune. End in terminal 'failed' so the reconciler retains it (no pointless
|
|
239
|
+
// re-prove) and uploads a race-free post-mortem.
|
|
216
240
|
if (!this.isTerminal()) {
|
|
217
|
-
this.state = 'failed';
|
|
241
|
+
this.state = this.checkpoints.some(c => c.isFailed() || c.isCancelled()) ? 'stopped' : 'failed';
|
|
218
242
|
}
|
|
219
243
|
} finally {
|
|
220
244
|
clearTimeout(this.deadlineTimeoutHandler);
|
package/src/prover-node.ts
CHANGED
|
@@ -47,7 +47,8 @@ import {
|
|
|
47
47
|
import { uploadEpochProofFailure } from './actions/upload-epoch-proof-failure.js';
|
|
48
48
|
import { CheckpointStore, type RegisterCheckpointData } from './checkpoint-store.js';
|
|
49
49
|
import type { SpecificProverNodeConfig } from './config.js';
|
|
50
|
-
import type {
|
|
50
|
+
import type { CheckpointProver, CheckpointProverTestHooks } from './job/checkpoint-prover.js';
|
|
51
|
+
import type { EpochSessionHooks } from './job/epoch-session.js';
|
|
51
52
|
import { ProverNodeJobMetrics, ProverNodeRewardsMetrics } from './metrics.js';
|
|
52
53
|
import { ProofPublishingService } from './proof-publishing-service.js';
|
|
53
54
|
import type { ProverPublisherFactory } from './prover-publisher-factory.js';
|
|
@@ -168,6 +169,9 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
|
|
|
168
169
|
metrics: this.jobMetrics,
|
|
169
170
|
txGatheringTimeoutMs: this.config.txGatheringTimeoutMs,
|
|
170
171
|
deadline: undefined,
|
|
172
|
+
// A checkpoint prover that fails (a sub-tree fault or a prune-induced fork fault) uploads a
|
|
173
|
+
// post-mortem for its own checkpoint, independently of any session. Fire-and-forget.
|
|
174
|
+
onFailed: prover => void this.tryUploadCheckpointFailure(prover),
|
|
171
175
|
},
|
|
172
176
|
this.log.getBindings(),
|
|
173
177
|
);
|
|
@@ -239,11 +243,9 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
|
|
|
239
243
|
break;
|
|
240
244
|
}
|
|
241
245
|
}
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
//
|
|
245
|
-
// failure above propagates to the L2BlockStream (which logs and stops this poll pass) and
|
|
246
|
-
// skips this update, so the event is re-emitted on the next poll rather than skipped (A-1041).
|
|
246
|
+
// Advance the local tips store only after the proving-side handling (registration / prune) has
|
|
247
|
+
// succeeded. Any failure above propagates to the L2BlockStream (which logs and stops this poll
|
|
248
|
+
// pass) and skips this update, so the event is re-emitted on the next poll rather than skipped
|
|
247
249
|
await this.tipsStore.handleBlockStreamEvent(event);
|
|
248
250
|
}
|
|
249
251
|
|
|
@@ -458,8 +460,8 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
|
|
|
458
460
|
}
|
|
459
461
|
|
|
460
462
|
/**
|
|
461
|
-
* Releases chonk-cache entries for every block in the supplied epoch (best-effort) and
|
|
462
|
-
*
|
|
463
|
+
* Releases chonk-cache entries for every block in the supplied epoch (best-effort) and reaps every
|
|
464
|
+
* CheckpointProver in the store whose epoch is at or below it.
|
|
463
465
|
*/
|
|
464
466
|
private async expireEpoch(epoch: EpochNumber): Promise<void> {
|
|
465
467
|
try {
|
|
@@ -515,8 +517,9 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
|
|
|
515
517
|
});
|
|
516
518
|
this.blockStream.start();
|
|
517
519
|
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
+
// The periodic ticker is the sole driver of the expiry sweep: it fires every poll interval whether
|
|
521
|
+
// or not block-stream events arrive, and RunningPromise never overlaps its own runs, so the sweep's
|
|
522
|
+
// `lastExpiredEpoch` high-water mark advances — and each epoch's post-mortem uploads — exactly once.
|
|
520
523
|
this.expiryTicker = new RunningPromise(
|
|
521
524
|
() => this.checkEpochExpiry(),
|
|
522
525
|
this.log,
|
|
@@ -559,9 +562,10 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
|
|
|
559
562
|
}
|
|
560
563
|
|
|
561
564
|
/**
|
|
562
|
-
* Constructs the session manager. Extracted so subclasses (test harness) can swap
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
+
* Constructs the session manager. Extracted so subclasses (test harness) can swap the
|
|
566
|
+
* implementation. Wired to upload a post-mortem when a full session ends in its own genuine failure
|
|
567
|
+
* (`EpochSession.hasFailed()` — top-tree/submit failed with every prover healthy, so definitively not
|
|
568
|
+
* a prune). A `stopped` session (a prover under it failed) is not uploaded; it recovers on re-add.
|
|
565
569
|
*/
|
|
566
570
|
protected createSessionManager(publishingService: ProofPublishingService): SessionManager {
|
|
567
571
|
return new SessionManager({
|
|
@@ -578,7 +582,7 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
|
|
|
578
582
|
finalizationDelayMs: this.config.proverNodeEpochProvingDelayMs,
|
|
579
583
|
},
|
|
580
584
|
onSessionFailed: async session => {
|
|
581
|
-
await this.
|
|
585
|
+
await this.tryUploadEpochFailure(session.getId(), session.getCheckpoints());
|
|
582
586
|
},
|
|
583
587
|
bindings: this.log.getBindings(),
|
|
584
588
|
});
|
|
@@ -596,15 +600,32 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
|
|
|
596
600
|
this.sessionManager.setSessionHooks(hooks);
|
|
597
601
|
}
|
|
598
602
|
|
|
599
|
-
/**
|
|
600
|
-
|
|
601
|
-
|
|
603
|
+
/**
|
|
604
|
+
* Installs checkpoint-prover test hooks (e.g. forcing a sub-tree failure) applied to every
|
|
605
|
+
* CheckpointProver constructed after this call. For the e2e harness only.
|
|
606
|
+
*/
|
|
607
|
+
public setCheckpointHooks(hooks: CheckpointProverTestHooks): void {
|
|
608
|
+
this.checkpointStore.setTestHooks(hooks);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Uploads a post-mortem snapshot for an epoch whose full session failed to prove, built from that
|
|
613
|
+
* session's checkpoint provers. Fired from the session manager's `onSessionFailed` callback (a
|
|
614
|
+
* genuine, race-free failure). Exposed as a method so tests can spy on it. No-ops if no failed-epoch
|
|
615
|
+
* store is configured or the checkpoint set is empty.
|
|
616
|
+
*/
|
|
617
|
+
public async tryUploadEpochFailure(
|
|
618
|
+
id: string,
|
|
619
|
+
checkpoints: readonly CheckpointProver[],
|
|
620
|
+
): Promise<string | undefined> {
|
|
621
|
+
if (!this.config.proverNodeFailedEpochStore || checkpoints.length === 0) {
|
|
602
622
|
return undefined;
|
|
603
623
|
}
|
|
604
|
-
const data = SessionManager.
|
|
624
|
+
const data = SessionManager.buildProvingData(checkpoints);
|
|
605
625
|
return await uploadEpochProofFailure(
|
|
606
626
|
this.config.proverNodeFailedEpochStore,
|
|
607
|
-
session.
|
|
627
|
+
// The session's own id; `uploadEpochProofFailure` already prefixes the path with the epoch number.
|
|
628
|
+
id,
|
|
608
629
|
data,
|
|
609
630
|
this.l2BlockSource as Archiver,
|
|
610
631
|
this.worldState,
|
|
@@ -613,8 +634,62 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
|
|
|
613
634
|
);
|
|
614
635
|
}
|
|
615
636
|
|
|
637
|
+
/**
|
|
638
|
+
* Uploads a post-mortem for a single failed checkpoint prover, built from just that checkpoint's
|
|
639
|
+
* proving data. Fired (fire-and-forget) from the store's `onFailed` callback for any non-cancel
|
|
640
|
+
* block-proof failure — a genuine sub-tree fault or a prune-induced fork fault alike. No-ops if no
|
|
641
|
+
* failed-epoch store is configured, or if the checkpoint is no longer canonical (a prune left nothing
|
|
642
|
+
* to diagnose). Swallows its own errors so a fire-and-forget caller can't leak.
|
|
643
|
+
*/
|
|
644
|
+
public async tryUploadCheckpointFailure(prover: CheckpointProver): Promise<string | undefined> {
|
|
645
|
+
if (!this.config.proverNodeFailedEpochStore) {
|
|
646
|
+
return undefined;
|
|
647
|
+
}
|
|
648
|
+
try {
|
|
649
|
+
// A prune-induced fork fault and a genuine sub-tree failure are indistinguishable at the moment the
|
|
650
|
+
// prover rejects (no control-plane cancel has landed yet). But the archiver is the authoritative
|
|
651
|
+
// committed chain: if this checkpoint was pruned out, its last block is no longer canonical there.
|
|
652
|
+
// Only upload for a checkpoint that still exists on-chain — a prune leaves nothing to diagnose, and
|
|
653
|
+
// the snapshot (full world-state + archiver) is expensive to produce and store.
|
|
654
|
+
if (!(await this.isCheckpointCanonical(prover.checkpoint))) {
|
|
655
|
+
this.log.debug(`Skipping checkpoint-failure upload for ${prover.id}: no longer canonical (pruned)`, {
|
|
656
|
+
checkpointNumber: prover.checkpoint.number,
|
|
657
|
+
});
|
|
658
|
+
return undefined;
|
|
659
|
+
}
|
|
660
|
+
const data = SessionManager.buildProvingData([prover]);
|
|
661
|
+
return await uploadEpochProofFailure(
|
|
662
|
+
this.config.proverNodeFailedEpochStore,
|
|
663
|
+
// The prover's content-addressed id; the epoch number is already in the upload path.
|
|
664
|
+
prover.id,
|
|
665
|
+
data,
|
|
666
|
+
this.l2BlockSource as Archiver,
|
|
667
|
+
this.worldState,
|
|
668
|
+
assertRequired(pick(this.config, 'l1ChainId', 'rollupVersion', 'dataDirectory')),
|
|
669
|
+
this.log,
|
|
670
|
+
);
|
|
671
|
+
} catch (err) {
|
|
672
|
+
this.log.error(`Error uploading checkpoint failure for ${prover.id}`, err);
|
|
673
|
+
return undefined;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
616
677
|
// ---------------- helpers ----------------
|
|
617
678
|
|
|
679
|
+
/**
|
|
680
|
+
* True if the checkpoint still exists on the canonical chain: the archiver holds a block at its last
|
|
681
|
+
* block's height whose archive root matches. A prune (fork fault) leaves the block missing or replaced,
|
|
682
|
+
* so this returns false. Protected for direct unit-test access.
|
|
683
|
+
*/
|
|
684
|
+
protected async isCheckpointCanonical(checkpoint: Checkpoint): Promise<boolean> {
|
|
685
|
+
const lastBlock = checkpoint.blocks.at(-1);
|
|
686
|
+
if (!lastBlock) {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
const onChain = await this.l2BlockSource.getBlock({ number: lastBlock.number });
|
|
690
|
+
return !!onChain && onChain.archive.root.equals(checkpoint.archive.root);
|
|
691
|
+
}
|
|
692
|
+
|
|
618
693
|
@memoize
|
|
619
694
|
private getL1Constants(): Promise<L1RollupConstants> {
|
|
620
695
|
return this.l2BlockSource.getL1Constants();
|
package/src/session-manager.ts
CHANGED
|
@@ -60,8 +60,9 @@ export type SessionManagerDeps = {
|
|
|
60
60
|
dateProvider: DateProvider;
|
|
61
61
|
config: SessionManagerConfig;
|
|
62
62
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
63
|
+
* Fired once when a full session ends in its own genuine failure (`EpochSession.hasFailed()` — top-tree
|
|
64
|
+
* or submit failed with every prover healthy). The owner uploads a post-mortem here. Not fired for a
|
|
65
|
+
* `stopped` session (a prover under it failed — possibly a prune), which is recovered on re-add instead.
|
|
65
66
|
*/
|
|
66
67
|
onSessionFailed?: (session: EpochSession) => Promise<void>;
|
|
67
68
|
bindings?: LoggerBindings;
|
|
@@ -88,15 +89,6 @@ export class SessionManager {
|
|
|
88
89
|
private readonly reconcileQueue = new SerialQueue();
|
|
89
90
|
/** Cached L1 constants, populated on first read. */
|
|
90
91
|
private cachedL1Constants: L1RollupConstants | undefined;
|
|
91
|
-
/**
|
|
92
|
-
* Highest epoch for which the periodic tick has successfully created a full session.
|
|
93
|
-
* Monotonic high-water mark: once the tick observes a session for epoch X, it stops
|
|
94
|
-
* trying to open one — even if that session subsequently fails (only a new checkpoint
|
|
95
|
-
* event reopens it). Crucially, the mark only advances when a session actually exists
|
|
96
|
-
* post-open, so transient blockers (atMaxSessionLimit, archiver still indexing) leave
|
|
97
|
-
* the mark in place and the next tick retries.
|
|
98
|
-
*/
|
|
99
|
-
private lastTickEpoch: EpochNumber | undefined;
|
|
100
92
|
/** Test-only hooks applied to every session this manager constructs. */
|
|
101
93
|
private sessionHooks: EpochSessionHooks | undefined;
|
|
102
94
|
/** Periodic tick that nudges reconcile to pick up newly-complete epochs. Started by `start()`. */
|
|
@@ -248,17 +240,6 @@ export class SessionManager {
|
|
|
248
240
|
await this.openFullSessionIfReady(epoch);
|
|
249
241
|
}
|
|
250
242
|
|
|
251
|
-
// Advance the tick high-water mark only once a session actually exists for the epoch.
|
|
252
|
-
// `openFullSessionIfReady` can early-return without creating one (atMaxSessionLimit,
|
|
253
|
-
// archiver still indexing, etc.); in those cases we want the next tick to try again
|
|
254
|
-
// rather than skip the epoch forever.
|
|
255
|
-
if (trigger.kind === 'tick' && implicatedEpochs.length === 1) {
|
|
256
|
-
const epoch = implicatedEpochs[0];
|
|
257
|
-
if (this.fullSessions.has(epoch)) {
|
|
258
|
-
this.lastTickEpoch = epoch;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
243
|
if (trigger.kind === 'start-proof') {
|
|
263
244
|
this.openPartialSession(trigger.spec);
|
|
264
245
|
}
|
|
@@ -266,15 +247,30 @@ export class SessionManager {
|
|
|
266
247
|
|
|
267
248
|
private recreateInvalidSessions(): void {
|
|
268
249
|
for (const [key, session] of Array.from(this.fullSessions.entries())) {
|
|
250
|
+
const canonical = this.checkpointsForSpec(session.getSpec());
|
|
251
|
+
const contentChanged = !this.checkpointsMatch(session.getCheckpoints(), canonical);
|
|
252
|
+
|
|
269
253
|
if (session.isTerminal()) {
|
|
254
|
+
// A full session that failed on its own account is retained as a "do not re-prove" marker while
|
|
255
|
+
// its content is unchanged — this is what stops the tick re-proving a deterministically-failing
|
|
256
|
+
// epoch. When the content changes (a re-add), it is replaced so the epoch retries over the new
|
|
257
|
+
// provers. Any other terminal full session is simply dropped.
|
|
258
|
+
if (session.hasFailed() && !contentChanged) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
270
261
|
this.fullSessions.delete(key);
|
|
262
|
+
if (contentChanged && this.canBuildOver(canonical)) {
|
|
263
|
+
const newSession = this.constructSession(session.getSpec(), canonical);
|
|
264
|
+
this.fullSessions.set(key, newSession);
|
|
265
|
+
void this.runSession(newSession);
|
|
266
|
+
}
|
|
271
267
|
continue;
|
|
272
268
|
}
|
|
273
|
-
|
|
274
|
-
if (
|
|
269
|
+
|
|
270
|
+
if (contentChanged) {
|
|
275
271
|
this.fireAndForgetCancel(session, 'canonical content changed');
|
|
276
272
|
this.fullSessions.delete(key);
|
|
277
|
-
if (canonical
|
|
273
|
+
if (this.canBuildOver(canonical)) {
|
|
278
274
|
const newSession = this.constructSession(session.getSpec(), canonical);
|
|
279
275
|
this.fullSessions.set(key, newSession);
|
|
280
276
|
void this.runSession(newSession);
|
|
@@ -290,7 +286,7 @@ export class SessionManager {
|
|
|
290
286
|
if (!this.checkpointsMatch(session.getCheckpoints(), canonical)) {
|
|
291
287
|
this.fireAndForgetCancel(session, 'canonical content changed');
|
|
292
288
|
this.partialSessions.delete(key);
|
|
293
|
-
if (canonical
|
|
289
|
+
if (this.canBuildOver(canonical)) {
|
|
294
290
|
const newSession = this.constructSession(session.getSpec(), canonical);
|
|
295
291
|
this.partialSessions.set(key, newSession);
|
|
296
292
|
void this.runSession(newSession);
|
|
@@ -299,9 +295,15 @@ export class SessionManager {
|
|
|
299
295
|
}
|
|
300
296
|
}
|
|
301
297
|
|
|
298
|
+
/** A session may be built over a checkpoint set only when it is non-empty and contains no failed prover. */
|
|
299
|
+
private canBuildOver(canonical: readonly CheckpointProver[]): boolean {
|
|
300
|
+
return canonical.length > 0 && !this.hasFailedProver(canonical);
|
|
301
|
+
}
|
|
302
|
+
|
|
302
303
|
private async openFullSessionIfReady(epoch: EpochNumber): Promise<void> {
|
|
303
|
-
//
|
|
304
|
-
//
|
|
304
|
+
// A session present here already covers the epoch: either live, or a retained genuinely-failed
|
|
305
|
+
// session kept by `recreateInvalidSessions` as a "do not re-prove" marker. Either way, don't open
|
|
306
|
+
// another — the retained-failed one is replaced only when its canonical content changes.
|
|
305
307
|
if (this.fullSessions.has(epoch)) {
|
|
306
308
|
return;
|
|
307
309
|
}
|
|
@@ -326,6 +328,13 @@ export class SessionManager {
|
|
|
326
328
|
});
|
|
327
329
|
return;
|
|
328
330
|
}
|
|
331
|
+
if (this.hasFailedProver(canonical)) {
|
|
332
|
+
// A checkpoint prover in the set has failed (a sub-tree fault or a prune-induced fork fault), so a
|
|
333
|
+
// session over it would fail immediately. Don't re-create it every tick — it recovers when a
|
|
334
|
+
// prune/re-add replaces the failed prover with a fresh one, or fails for good at expiry.
|
|
335
|
+
this.log.debug(`Skipping full-session open for epoch ${epoch}: a checkpoint prover has failed`, { epoch });
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
329
338
|
const spec: SessionSpec = { kind: 'full', epochNumber: epoch, fromSlot, toSlot };
|
|
330
339
|
const session = this.constructSession(spec, canonical);
|
|
331
340
|
this.fullSessions.set(epoch, session);
|
|
@@ -334,7 +343,7 @@ export class SessionManager {
|
|
|
334
343
|
|
|
335
344
|
private openPartialSession(spec: SessionSpec): void {
|
|
336
345
|
const canonical = this.deps.checkpointStore.listInSlotRange(spec.fromSlot, spec.toSlot);
|
|
337
|
-
if (canonical.length === 0) {
|
|
346
|
+
if (canonical.length === 0 || this.hasFailedProver(canonical)) {
|
|
338
347
|
return;
|
|
339
348
|
}
|
|
340
349
|
// Reuse a live partial session for this epoch whose checkpoint set already matches the
|
|
@@ -406,33 +415,30 @@ export class SessionManager {
|
|
|
406
415
|
}
|
|
407
416
|
const state = await session.start();
|
|
408
417
|
this.log.info(`Session ${session.getId()} exited with state ${state}`);
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
this.log.info(`Skipping failure upload for session ${session.getId()}: canonical content changed`, {
|
|
417
|
-
...session.getSpec(),
|
|
418
|
-
});
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
418
|
+
|
|
419
|
+
// A full session that failed on its own account (top-tree/submit failed with every prover healthy)
|
|
420
|
+
// is a genuine, race-free failure: upload its post-mortem once. `recreateInvalidSessions` retains
|
|
421
|
+
// the terminal session so this fires exactly once (it is never re-run over the same content). A
|
|
422
|
+
// `stopped` session (a prover under it failed) is not uploaded — it may be a prune, and recovers on
|
|
423
|
+
// re-add.
|
|
424
|
+
if (session.getKind() === 'full' && session.hasFailed() && this.deps.onSessionFailed) {
|
|
421
425
|
try {
|
|
422
426
|
await this.deps.onSessionFailed(session);
|
|
423
427
|
} catch (err) {
|
|
424
|
-
this.log.error(`Error in onSessionFailed callback for ${session.
|
|
428
|
+
this.log.error(`Error in onSessionFailed callback for epoch ${session.getEpochNumber()}`, err);
|
|
425
429
|
}
|
|
426
430
|
}
|
|
427
431
|
}
|
|
428
432
|
|
|
429
433
|
/**
|
|
430
|
-
* Builds the EpochProvingJobData snapshot for failure upload
|
|
431
|
-
*
|
|
432
|
-
* partial state is still useful for post-mortem analysis.
|
|
434
|
+
* Builds the EpochProvingJobData snapshot for a post-mortem failure upload from a set of
|
|
435
|
+
* checkpoint provers. Includes every checkpoint regardless of whether sub-tree proving
|
|
436
|
+
* completed — partial state is still useful for post-mortem analysis.
|
|
433
437
|
*/
|
|
434
|
-
public static
|
|
435
|
-
|
|
438
|
+
public static buildProvingData(checkpoints: readonly CheckpointProver[]): EpochProvingJobData {
|
|
439
|
+
if (checkpoints.length === 0) {
|
|
440
|
+
throw new Error('Cannot build proving data from an empty checkpoint set');
|
|
441
|
+
}
|
|
436
442
|
const txs = new Map();
|
|
437
443
|
const l1ToL2Messages: Record<number, Fr[]> = {};
|
|
438
444
|
for (const c of checkpoints) {
|
|
@@ -442,7 +448,7 @@ export class SessionManager {
|
|
|
442
448
|
l1ToL2Messages[c.checkpoint.number] = c.l1ToL2Messages;
|
|
443
449
|
}
|
|
444
450
|
return {
|
|
445
|
-
epochNumber:
|
|
451
|
+
epochNumber: checkpoints[0].epochNumber,
|
|
446
452
|
checkpoints: checkpoints.map(c => c.checkpoint),
|
|
447
453
|
txs,
|
|
448
454
|
l1ToL2Messages,
|
|
@@ -465,20 +471,12 @@ export class SessionManager {
|
|
|
465
471
|
/**
|
|
466
472
|
* Maps a reconcile trigger to the epochs whose full session should be (re)opened.
|
|
467
473
|
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
* - `checkpoint` and `prune` are deliberately NOT gated. They only fire when the epoch's canonical
|
|
475
|
-
* content actually changes — a checkpoint arrives, or a reorg prunes/replaces one — which is
|
|
476
|
-
* exactly when re-attempting is correct.
|
|
477
|
-
*
|
|
478
|
-
* A genuine proving failure produces no content change, hence no checkpoint/prune event, so only
|
|
479
|
-
* the gated tick could reopen it — and it won't. A prune + re-add fires ungated events, so the
|
|
480
|
-
* epoch is reopened through this path (and `openFullSessionIfReady` rebuilds over the fresh
|
|
481
|
-
* provers). See the "onTick does not retry ... but recovers ... re-added" test.
|
|
474
|
+
* The periodic `tick` returns the next unproven epoch every time; it does not track prior attempts.
|
|
475
|
+
* `openFullSessionIfReady` is what keeps this from re-proving a doomed epoch: it refuses to build a
|
|
476
|
+
* session when any checkpoint prover in the set has failed, so a stuck epoch is cheaply skipped each
|
|
477
|
+
* tick rather than re-proved. `checkpoint` and `prune` fire when an epoch's canonical content changes
|
|
478
|
+
* (a checkpoint arrives, or a reorg prunes/replaces one) — which is what installs a fresh prover in
|
|
479
|
+
* place of a failed one, letting the next open succeed and recovering a pruned-then-re-added epoch.
|
|
482
480
|
*/
|
|
483
481
|
private async epochsForTrigger(trigger: ReconcileTrigger): Promise<EpochNumber[]> {
|
|
484
482
|
switch (trigger.kind) {
|
|
@@ -488,10 +486,7 @@ export class SessionManager {
|
|
|
488
486
|
return trigger.affectedEpochs;
|
|
489
487
|
case 'tick': {
|
|
490
488
|
const epoch = await this.nextUnprovenEpoch();
|
|
491
|
-
|
|
492
|
-
return [];
|
|
493
|
-
}
|
|
494
|
-
return [epoch];
|
|
489
|
+
return epoch === undefined ? [] : [epoch];
|
|
495
490
|
}
|
|
496
491
|
case 'start-proof':
|
|
497
492
|
return [];
|
|
@@ -518,6 +513,15 @@ export class SessionManager {
|
|
|
518
513
|
return this.deps.checkpointStore.listInSlotRange(spec.fromSlot, spec.toSlot);
|
|
519
514
|
}
|
|
520
515
|
|
|
516
|
+
/**
|
|
517
|
+
* True if any prover in the set has failed. The epoch cannot be proven over a failed prover (it can
|
|
518
|
+
* never produce its block proofs), so a session must not be built or rebuilt over it until a prune/re-add
|
|
519
|
+
* has replaced it with a fresh prover.
|
|
520
|
+
*/
|
|
521
|
+
private hasFailedProver(checkpoints: readonly CheckpointProver[]): boolean {
|
|
522
|
+
return checkpoints.some(c => c.isFailed());
|
|
523
|
+
}
|
|
524
|
+
|
|
521
525
|
private fireAndForgetCancel(session: EpochSession, reason: string): void {
|
|
522
526
|
void session.cancel(reason).catch(err => this.log.warn(`Error cancelling session ${session.getId()}`, err));
|
|
523
527
|
}
|