@aztec/prover-node 0.0.1-commit.3100065 → 0.0.1-commit.321f6a9

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.
Files changed (46) hide show
  1. package/README.md +95 -34
  2. package/dest/actions/rerun-epoch-proving-job.d.ts +11 -2
  3. package/dest/actions/rerun-epoch-proving-job.d.ts.map +1 -1
  4. package/dest/actions/rerun-epoch-proving-job.js +195 -55
  5. package/dest/checkpoint-store.d.ts +9 -2
  6. package/dest/checkpoint-store.d.ts.map +1 -1
  7. package/dest/checkpoint-store.js +9 -0
  8. package/dest/config.d.ts +3 -1
  9. package/dest/config.d.ts.map +1 -1
  10. package/dest/config.js +7 -0
  11. package/dest/factory.d.ts +4 -1
  12. package/dest/factory.d.ts.map +1 -1
  13. package/dest/factory.js +2 -1
  14. package/dest/job/checkpoint-prover.d.ts +34 -4
  15. package/dest/job/checkpoint-prover.d.ts.map +1 -1
  16. package/dest/job/checkpoint-prover.js +41 -8
  17. package/dest/job/epoch-session.d.ts +17 -3
  18. package/dest/job/epoch-session.d.ts.map +1 -1
  19. package/dest/job/epoch-session.js +28 -4
  20. package/dest/job/top-tree-job.d.ts +2 -2
  21. package/dest/job/top-tree-job.d.ts.map +1 -1
  22. package/dest/job/top-tree-job.js +4 -4
  23. package/dest/prover-node-publisher.d.ts +4 -1
  24. package/dest/prover-node-publisher.d.ts.map +1 -1
  25. package/dest/prover-node-publisher.js +5 -3
  26. package/dest/prover-node.d.ts +37 -8
  27. package/dest/prover-node.d.ts.map +1 -1
  28. package/dest/prover-node.js +82 -19
  29. package/dest/prover-publisher-factory.d.ts +3 -1
  30. package/dest/prover-publisher-factory.d.ts.map +1 -1
  31. package/dest/prover-publisher-factory.js +1 -0
  32. package/dest/session-manager.d.ts +16 -16
  33. package/dest/session-manager.d.ts.map +1 -1
  34. package/dest/session-manager.js +67 -63
  35. package/package.json +24 -23
  36. package/src/actions/rerun-epoch-proving-job.ts +139 -66
  37. package/src/checkpoint-store.ts +20 -2
  38. package/src/config.ts +10 -0
  39. package/src/factory.ts +5 -0
  40. package/src/job/checkpoint-prover.ts +61 -7
  41. package/src/job/epoch-session.ts +30 -4
  42. package/src/job/top-tree-job.ts +4 -4
  43. package/src/prover-node-publisher.ts +7 -3
  44. package/src/prover-node.ts +97 -20
  45. package/src/prover-publisher-factory.ts +3 -0
  46. package/src/session-manager.ts +73 -67
@@ -60,8 +60,9 @@ export type SessionManagerDeps = {
60
60
  dateProvider: DateProvider;
61
61
  config: SessionManagerConfig;
62
62
  /**
63
- * Optional callback fired when a session terminates with `failed`. The session manager
64
- * doesn't own the failure-upload action; it just notifies the owner.
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()`. */
@@ -227,7 +219,9 @@ export class SessionManager {
227
219
  await this.epochTicker?.stop();
228
220
  await this.reconcileQueue.cancel();
229
221
  const sessions = this.allSessions();
230
- await Promise.allSettled(sessions.map(s => s.cancel('prover-node stopping')));
222
+ // A clean shutdown is just a restart, so preserve the in-flight broker jobs (abortJobs: false)
223
+ // for the restarted node to reuse rather than re-proving the epoch from scratch.
224
+ await Promise.allSettled(sessions.map(s => s.cancel('prover-node stopping', { abortJobs: false })));
231
225
  }
232
226
 
233
227
  // ---------------- reconcile ----------------
@@ -246,17 +240,6 @@ export class SessionManager {
246
240
  await this.openFullSessionIfReady(epoch);
247
241
  }
248
242
 
249
- // Advance the tick high-water mark only once a session actually exists for the epoch.
250
- // `openFullSessionIfReady` can early-return without creating one (atMaxSessionLimit,
251
- // archiver still indexing, etc.); in those cases we want the next tick to try again
252
- // rather than skip the epoch forever.
253
- if (trigger.kind === 'tick' && implicatedEpochs.length === 1) {
254
- const epoch = implicatedEpochs[0];
255
- if (this.fullSessions.has(epoch)) {
256
- this.lastTickEpoch = epoch;
257
- }
258
- }
259
-
260
243
  if (trigger.kind === 'start-proof') {
261
244
  this.openPartialSession(trigger.spec);
262
245
  }
@@ -264,15 +247,30 @@ export class SessionManager {
264
247
 
265
248
  private recreateInvalidSessions(): void {
266
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
+
267
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
+ }
268
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
+ }
269
267
  continue;
270
268
  }
271
- const canonical = this.checkpointsForSpec(session.getSpec());
272
- if (!this.checkpointsMatch(session.getCheckpoints(), canonical)) {
269
+
270
+ if (contentChanged) {
273
271
  this.fireAndForgetCancel(session, 'canonical content changed');
274
272
  this.fullSessions.delete(key);
275
- if (canonical.length > 0) {
273
+ if (this.canBuildOver(canonical)) {
276
274
  const newSession = this.constructSession(session.getSpec(), canonical);
277
275
  this.fullSessions.set(key, newSession);
278
276
  void this.runSession(newSession);
@@ -288,7 +286,7 @@ export class SessionManager {
288
286
  if (!this.checkpointsMatch(session.getCheckpoints(), canonical)) {
289
287
  this.fireAndForgetCancel(session, 'canonical content changed');
290
288
  this.partialSessions.delete(key);
291
- if (canonical.length > 0) {
289
+ if (this.canBuildOver(canonical)) {
292
290
  const newSession = this.constructSession(session.getSpec(), canonical);
293
291
  this.partialSessions.set(key, newSession);
294
292
  void this.runSession(newSession);
@@ -297,9 +295,15 @@ export class SessionManager {
297
295
  }
298
296
  }
299
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
+
300
303
  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.
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.
303
307
  if (this.fullSessions.has(epoch)) {
304
308
  return;
305
309
  }
@@ -324,6 +328,13 @@ export class SessionManager {
324
328
  });
325
329
  return;
326
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
+ }
327
338
  const spec: SessionSpec = { kind: 'full', epochNumber: epoch, fromSlot, toSlot };
328
339
  const session = this.constructSession(spec, canonical);
329
340
  this.fullSessions.set(epoch, session);
@@ -332,7 +343,7 @@ export class SessionManager {
332
343
 
333
344
  private openPartialSession(spec: SessionSpec): void {
334
345
  const canonical = this.deps.checkpointStore.listInSlotRange(spec.fromSlot, spec.toSlot);
335
- if (canonical.length === 0) {
346
+ if (canonical.length === 0 || this.hasFailedProver(canonical)) {
336
347
  return;
337
348
  }
338
349
  // Reuse a live partial session for this epoch whose checkpoint set already matches the
@@ -404,33 +415,30 @@ export class SessionManager {
404
415
  }
405
416
  const state = await session.start();
406
417
  this.log.info(`Session ${session.getId()} exited with state ${state}`);
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
- }
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) {
419
425
  try {
420
426
  await this.deps.onSessionFailed(session);
421
427
  } catch (err) {
422
- this.log.error(`Error in onSessionFailed callback for ${session.getSpec().epochNumber}`, err);
428
+ this.log.error(`Error in onSessionFailed callback for epoch ${session.getEpochNumber()}`, err);
423
429
  }
424
430
  }
425
431
  }
426
432
 
427
433
  /**
428
- * Builds the EpochProvingJobData snapshot for failure upload. Includes every checkpoint
429
- * referenced by the session, regardless of whether sub-tree proving completed —
430
- * 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.
431
437
  */
432
- public static buildSessionProvingData(session: EpochSession): EpochProvingJobData {
433
- const checkpoints = session.getCheckpoints();
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
+ }
434
442
  const txs = new Map();
435
443
  const l1ToL2Messages: Record<number, Fr[]> = {};
436
444
  for (const c of checkpoints) {
@@ -440,7 +448,7 @@ export class SessionManager {
440
448
  l1ToL2Messages[c.checkpoint.number] = c.l1ToL2Messages;
441
449
  }
442
450
  return {
443
- epochNumber: session.getSpec().epochNumber,
451
+ epochNumber: checkpoints[0].epochNumber,
444
452
  checkpoints: checkpoints.map(c => c.checkpoint),
445
453
  txs,
446
454
  l1ToL2Messages,
@@ -463,20 +471,12 @@ export class SessionManager {
463
471
  /**
464
472
  * Maps a reconcile trigger to the epochs whose full session should be (re)opened.
465
473
  *
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.
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.
480
480
  */
481
481
  private async epochsForTrigger(trigger: ReconcileTrigger): Promise<EpochNumber[]> {
482
482
  switch (trigger.kind) {
@@ -486,10 +486,7 @@ export class SessionManager {
486
486
  return trigger.affectedEpochs;
487
487
  case 'tick': {
488
488
  const epoch = await this.nextUnprovenEpoch();
489
- if (epoch === undefined || (this.lastTickEpoch !== undefined && epoch <= this.lastTickEpoch)) {
490
- return [];
491
- }
492
- return [epoch];
489
+ return epoch === undefined ? [] : [epoch];
493
490
  }
494
491
  case 'start-proof':
495
492
  return [];
@@ -516,6 +513,15 @@ export class SessionManager {
516
513
  return this.deps.checkpointStore.listInSlotRange(spec.fromSlot, spec.toSlot);
517
514
  }
518
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
+
519
525
  private fireAndForgetCancel(session: EpochSession, reason: string): void {
520
526
  void session.cancel(reason).catch(err => this.log.warn(`Error cancelling session ${session.getId()}`, err));
521
527
  }