@aztec/prover-node 0.0.1-commit.3100065 → 0.0.1-commit.330febf

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 +47 -6
  15. package/dest/job/checkpoint-prover.d.ts.map +1 -1
  16. package/dest/job/checkpoint-prover.js +94 -19
  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 +75 -65
  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 +113 -17
  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 +78 -69
@@ -18,7 +18,7 @@ import type { Checkpoint } from '@aztec/stdlib/checkpoint';
18
18
  import type { ForkMerkleTreeOperations, ITxProvider } from '@aztec/stdlib/interfaces/server';
19
19
  import { CheckpointConstantData } from '@aztec/stdlib/rollup';
20
20
  import { MerkleTreeId } from '@aztec/stdlib/trees';
21
- import type { BlockHeader, ProcessedTx, Tx } from '@aztec/stdlib/tx';
21
+ import type { BlockHeader, ProcessedTx, Tx, TxHash } from '@aztec/stdlib/tx';
22
22
 
23
23
  import type { ProverNodeJobMetrics } from '../metrics.js';
24
24
 
@@ -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;
@@ -83,21 +99,29 @@ export class CheckpointProver {
83
99
  readonly l1ToL2Messages: Fr[];
84
100
  readonly previousArchiveSiblingPath: Tuple<Fr, typeof ARCHIVE_HEIGHT>;
85
101
 
86
- /** Per-prover tx map — populated by the internal gather. Empty until then. */
87
- readonly txs: Map<string, Tx> = new Map();
88
-
89
102
  /** Resolved by the sub-tree on success, rejected on cancel/failure. */
90
103
  private readonly blockProofs: PromiseWithResolvers<SubTreeResult['blockProofOutputs']> = promiseWithResolvers();
91
104
 
105
+ // Three independent lifecycle facts — deliberately not collapsed into one status enum, because several
106
+ // combinations are legal and relied on: a prover can be `completed` and then `cancelled` (routine
107
+ // teardown of an already-proven checkpoint), or `completed` and then `failed` (block proving was
108
+ // enqueued, but the sub-tree subsequently faulted). Only `failed` + `cancelled` is excluded — a cancel
109
+ // is not a failure (enforced in `failBlockProofs`).
110
+ /** Block-level proving was fully *enqueued* (a progress marker; the sub-tree may still be proving). */
111
+ private completed = false;
112
+ /** Block proofs rejected for a genuine (non-cancel) reason — a sub-tree or prune-induced fork fault. */
113
+ private failed = false;
114
+ /** The prover was torn down (prune / reap / shutdown). */
92
115
  private cancelled = false;
93
116
  private subTree?: CheckpointSubTreeOrchestrator;
94
- private completed = false;
95
117
  private readonly abortController = new AbortController();
96
118
 
97
119
  /** Tracks the eager gather+execute task so `cancel()` and `whenDone()` can await its unwind. */
98
120
  private readonly runPromise: Promise<void>;
99
121
  /** Tracks the cancel-driven teardown so `whenDone()` can await it. */
100
122
  private cancelPromise?: Promise<void>;
123
+ /** Tracks the success-driven sub-tree teardown (once block proofs are captured) so `whenDone()` can await it. */
124
+ private teardownPromise?: Promise<void>;
101
125
 
102
126
  constructor(
103
127
  args: CheckpointProverArgs,
@@ -139,9 +163,14 @@ export class CheckpointProver {
139
163
  return this.cancelled;
140
164
  }
141
165
 
142
- /** True once block-level proving has been fully *enqueued* (sub-tree completion may still be pending). */
143
- public isCompleted(): boolean {
144
- return this.completed;
166
+ /**
167
+ * True once this prover's block proofs have rejected for a genuine (non-cancel) reason — a sub-tree
168
+ * proving fault or a prune-induced world-state fork fault. A failed prover cannot produce its block
169
+ * proofs, so the reconciler must not build (or rebuild) an EpochSession over it; it is cleared only by
170
+ * a prune/re-add replacing it with a fresh prover, or by expiry reaping it.
171
+ */
172
+ public isFailed(): boolean {
173
+ return this.failed;
145
174
  }
146
175
 
147
176
  /** AbortSignal that fires on cancel — for callers that want to wire their own tasks. */
@@ -157,9 +186,19 @@ export class CheckpointProver {
157
186
  /** Resolves when all in-flight work for this prover has fully unwound. */
158
187
  public async whenDone(): Promise<void> {
159
188
  await this.runPromise.catch(() => {});
189
+ // `runPromise` resolves once block-level proving is *enqueued*, but the sub-tree's proofs (and the
190
+ // success-driven teardown they trigger) land later, on the `getSubTreeResult()` callback. Awaiting
191
+ // `blockProofs` here bridges that gap: on success the callback resolves `blockProofs` and then
192
+ // synchronously sets `teardownPromise` before this await resumes, so the teardown is observable
193
+ // below; on failure/cancel `blockProofs` rejects and teardown is driven by the `finally`/cancel
194
+ // paths already awaited via `runPromise`/`cancelPromise`.
195
+ await this.blockProofs.promise.catch(() => {});
160
196
  if (this.cancelPromise) {
161
197
  await this.cancelPromise;
162
198
  }
199
+ if (this.teardownPromise) {
200
+ await this.teardownPromise;
201
+ }
163
202
  }
164
203
 
165
204
  private async gatherAndExecute(): Promise<void> {
@@ -179,24 +218,68 @@ export class CheckpointProver {
179
218
  this.deps.log.error(`Error in CheckpointProver ${this.id}`, err, {
180
219
  checkpointNumber: this.checkpoint.number,
181
220
  });
182
- this.blockProofs.reject(err instanceof Error ? err : new Error(String(err)));
221
+ this.failBlockProofs(err instanceof Error ? err : new Error(String(err)));
183
222
  }
184
223
  }
185
224
 
186
- private async gatherTxs(): Promise<Map<string, Tx>> {
225
+ /**
226
+ * Rejects the block-proof promise and, unless this is a cancellation, records the prover as failed so
227
+ * the reconciler won't build an EpochSession over it. First rejection wins, so a later duplicate reject
228
+ * (e.g. the executeCheckpoint `finally`) is a harmless no-op.
229
+ */
230
+ private failBlockProofs(err: Error): void {
231
+ if (!this.cancelled && !this.failed) {
232
+ this.failed = true;
233
+ // Notify the owner so it can upload a post-mortem for this checkpoint. Fire-and-forget: the
234
+ // callback must not block the prover's teardown, and a throw in it must not mask the rejection.
235
+ try {
236
+ this.deps.onFailed?.(this);
237
+ } catch (err) {
238
+ this.deps.log.error(`Error in CheckpointProver onFailed callback for ${this.id}`, err);
239
+ }
240
+ }
241
+ this.blockProofs.reject(err);
242
+ }
243
+
244
+ /** Fetches every tx in this checkpoint from the tx pool (by hash, via the block tx effects). */
245
+ private async fetchTxs(): Promise<{ txs: Map<string, Tx>; missingTxs: TxHash[] }> {
187
246
  const deadline = new Date(this.deps.dateProvider.now() + this.deps.txGatheringTimeoutMs);
188
247
  const txsByBlock = await Promise.all(
189
248
  this.checkpoint.blocks.map(block => this.deps.txProvider.getTxsForBlock(block, { deadline })),
190
249
  );
191
- const txs = txsByBlock.map(({ txs }) => txs).flat();
192
- const missingTxs = txsByBlock.map(({ missingTxs }) => missingTxs).flat();
250
+ const txs = txsByBlock.flatMap(({ txs }) => txs);
251
+ const missingTxs = txsByBlock.flatMap(({ missingTxs }) => missingTxs);
252
+ return { txs: new Map<string, Tx>(txs.map(tx => [tx.getTxHash().toString(), tx])), missingTxs };
253
+ }
193
254
 
255
+ private async gatherTxs(): Promise<Map<string, Tx>> {
256
+ const { txs, missingTxs } = await this.fetchTxs();
194
257
  if (missingTxs.length > 0) {
195
258
  throw new Error(
196
259
  `Txs not found for checkpoint ${this.checkpoint.number}: ${missingTxs.map(hash => hash.toString()).join(', ')}`,
197
260
  );
198
261
  }
199
- return new Map<string, Tx>(txs.map(tx => [tx.getTxHash().toString(), tx]));
262
+ return txs;
263
+ }
264
+
265
+ /**
266
+ * Re-fetches this checkpoint's txs from the tx pool for a post-mortem failure upload.
267
+ *
268
+ * The prover does not cache txs on the heap — they are consumed during proving and dropped — so the
269
+ * durable pool (which keeps a mined tx until L1 finality, with A-1274's retention margin covering a
270
+ * lagging prover) is the source of truth, read back by hash here. Best-effort: any tx the pool can
271
+ * no longer supply is logged and omitted rather than failing the upload, since a partial post-mortem
272
+ * snapshot is still useful for diagnosis.
273
+ */
274
+ public async getTxsForUpload(): Promise<Map<string, Tx>> {
275
+ const { txs, missingTxs } = await this.fetchTxs();
276
+ if (missingTxs.length > 0) {
277
+ this.deps.log.warn(
278
+ `Missing ${missingTxs.length} tx(s) re-fetching checkpoint ${this.checkpoint.number} for failure upload`,
279
+ { checkpointNumber: this.checkpoint.number, missingTxCount: missingTxs.length },
280
+ );
281
+ }
282
+ return txs;
200
283
  }
201
284
 
202
285
  private async executeCheckpoint(txs: Map<string, Tx>): Promise<void> {
@@ -205,10 +288,15 @@ export class CheckpointProver {
205
288
  let subTreeStarted = false;
206
289
 
207
290
  try {
208
- for (const [hash, tx] of txs) {
209
- this.txs.set(hash, tx);
291
+ // Test hook: force a sub-tree failure to exercise the checkpoint failure/upload path.
292
+ if (this.deps.checkpointProveOverride) {
293
+ await this.deps.checkpointProveOverride();
210
294
  }
211
295
 
296
+ // The gathered txs are consumed locally below (public processing + sub-tree) and then dropped.
297
+ // They are deliberately not retained on the instance: the tx pool is the durable source and
298
+ // `getTxsForUpload` re-fetches them by hash if a post-mortem upload needs them.
299
+
212
300
  const { chainId, version } = this.checkpoint.blocks[0].header.globalVariables;
213
301
  const checkpointConstants = CheckpointConstantData.from({
214
302
  chainId,
@@ -247,8 +335,16 @@ export class CheckpointProver {
247
335
  // Spans processing + proving (from executeCheckpoint start, after tx gathering) to proofs ready.
248
336
  this.deps.metrics.recordCheckpointProving(checkpointTimer.ms());
249
337
  this.blockProofs.resolve(result.blockProofOutputs);
338
+ // Release the sub-tree orchestrator now that its output is captured. The block-proof outputs
339
+ // survive via the resolved promise; everything else the sub-tree held — per-tx AVM inputs, and
340
+ // the base/merge/parity proof trees — is dead once proving completes, yet the prover is retained
341
+ // for the whole proof-submission window. Dropping it here is what stops that retention from
342
+ // accumulating across every proven checkpoint. Post-completion consumers (the top-tree job, a
343
+ // rebuilt EpochSession, failure upload) read only `whenBlockProofsReady()` and this prover's own
344
+ // fields (`checkpoint`, `txs`, headers, sibling paths), never the sub-tree.
345
+ this.teardownPromise = this.teardownSubTree();
250
346
  },
251
- err => this.blockProofs.reject(err),
347
+ err => this.failBlockProofs(err instanceof Error ? err : new Error(String(err))),
252
348
  );
253
349
  if (signal.aborted) {
254
350
  return;
@@ -328,7 +424,7 @@ export class CheckpointProver {
328
424
  if (subTreeStarted) {
329
425
  await this.teardownSubTree();
330
426
  }
331
- this.blockProofs.reject(new Error(`Checkpoint ${this.id} did not complete block processing`));
427
+ this.failBlockProofs(new Error(`Checkpoint ${this.id} did not complete block processing`));
332
428
  }
333
429
  }
334
430
  }
@@ -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`, `failed` → `failed`, `expired` → `timed-out`, `withdrawn` → `cancelled`.
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);
@@ -228,7 +252,7 @@ export class EpochSession implements Traceable {
228
252
  * Cancels the session. Idempotent. Withdraws any submitted candidate from the
229
253
  * publishing service so the in-flight publisher (if any) is interrupted.
230
254
  */
231
- public async cancel(reason = 'cancelled'): Promise<void> {
255
+ public async cancel(reason = 'cancelled', { abortJobs = true }: { abortJobs?: boolean } = {}): Promise<void> {
232
256
  if (this.isTerminal()) {
233
257
  return;
234
258
  }
@@ -247,7 +271,9 @@ export class EpochSession implements Traceable {
247
271
  if (this.topTreeJob && !this.topTreeJob.isCancelled()) {
248
272
  const job = this.topTreeJob;
249
273
  this.topTreeJob = undefined;
250
- job.cancel();
274
+ // On a clean shutdown we leave the in-flight broker jobs alone so a restart can reuse them;
275
+ // other cancellations (reorg, supersede, deadline) abort them since their inputs are stale.
276
+ job.cancel(abortJobs);
251
277
  this.pendingTopTreeCleanups.push(job);
252
278
  }
253
279
  await this.teardownTopTreeIfNeeded();
@@ -128,7 +128,7 @@ export class TopTreeJob {
128
128
  * via `whenDone()` — the parent collects the cancelled job and awaits all
129
129
  * pending top-tree teardowns at the end of the epoch.
130
130
  */
131
- public cancel(): void {
131
+ public cancel(abortJobs = true): void {
132
132
  if (this.cancelled) {
133
133
  return;
134
134
  }
@@ -146,7 +146,7 @@ export class TopTreeJob {
146
146
  // Fire and forget: parent awaits the cancel-driven teardown via whenDone(); the
147
147
  // chained .catch swallows rejections so the unawaited promise doesn't surface
148
148
  // as an unhandled rejection.
149
- this.cancelPromise = this.runCancel().catch(() => {});
149
+ this.cancelPromise = this.runCancel(abortJobs).catch(() => {});
150
150
  }
151
151
 
152
152
  /** Resolves once the cancel-driven teardown of the underlying orchestrator has unwound. */
@@ -156,9 +156,9 @@ export class TopTreeJob {
156
156
  }
157
157
  }
158
158
 
159
- private async runCancel(): Promise<void> {
159
+ private async runCancel(abortJobs: boolean): Promise<void> {
160
160
  try {
161
- this.topTree.cancel({ abortJobs: true });
161
+ this.topTree.cancel({ abortJobs });
162
162
  } catch (err) {
163
163
  this.deps.log.error('Error cancelling top tree', err);
164
164
  }
@@ -40,6 +40,8 @@ export class ProverNodePublisher {
40
40
 
41
41
  protected rollupContract: RollupContract;
42
42
 
43
+ protected proofSubmissionTarget: Hex;
44
+
43
45
  public readonly l1TxUtils: L1TxUtils;
44
46
 
45
47
  constructor(
@@ -47,6 +49,7 @@ export class ProverNodePublisher {
47
49
  deps: {
48
50
  rollupContract: RollupContract;
49
51
  l1TxUtils: L1TxUtils;
52
+ proofSubmissionTarget?: EthAddress;
50
53
  telemetry?: TelemetryClient;
51
54
  },
52
55
  bindings?: LoggerBindings,
@@ -57,6 +60,7 @@ export class ProverNodePublisher {
57
60
  this.log = createLogger('prover-node:l1-tx-publisher', bindings);
58
61
 
59
62
  this.rollupContract = deps.rollupContract;
63
+ this.proofSubmissionTarget = deps.proofSubmissionTarget?.toString() ?? deps.rollupContract.address;
60
64
  this.l1TxUtils = deps.l1TxUtils;
61
65
  }
62
66
 
@@ -217,7 +221,7 @@ export class ProverNodePublisher {
217
221
  const senderAddress = this.l1TxUtils.getSenderAddress();
218
222
 
219
223
  const [gasLimit, gasPrice, latestBlock] = await Promise.all([
220
- this.l1TxUtils.estimateGas(senderAddress.toString() as `0x${string}`, { to: this.rollupContract.address, data }),
224
+ this.l1TxUtils.estimateGas(senderAddress.toString() as `0x${string}`, { to: this.proofSubmissionTarget, data }),
221
225
  this.l1TxUtils.getGasPrice(),
222
226
  this.l1TxUtils.client.getBlock({ blockTag: 'latest' }),
223
227
  ]);
@@ -288,7 +292,7 @@ export class ProverNodePublisher {
288
292
  });
289
293
  try {
290
294
  const { receipt } = await this.l1TxUtils.sendAndMonitorTransaction(
291
- { to: this.rollupContract.address, data },
295
+ { to: this.proofSubmissionTarget, data },
292
296
  { txTimeoutAt: args.deadline },
293
297
  );
294
298
  if (receipt.status !== 'success') {
@@ -298,7 +302,7 @@ export class ProverNodePublisher {
298
302
  args: [...txArgs],
299
303
  functionName: 'submitEpochRootProof',
300
304
  abi: RollupAbi,
301
- address: this.rollupContract.address,
305
+ address: this.proofSubmissionTarget,
302
306
  },
303
307
  /*blobInputs*/ undefined,
304
308
  /*stateOverride*/ [],
@@ -10,7 +10,7 @@ import { DateProvider, executeTimeout } from '@aztec/foundation/timer';
10
10
  import type { EpochProverFactory } from '@aztec/prover-client';
11
11
  import { getLastSiblingPath } from '@aztec/prover-client/helpers';
12
12
  import { ChonkCache } from '@aztec/prover-client/orchestrator';
13
- import { PublicProcessorFactory } from '@aztec/simulator/server';
13
+ import { type AvmSimulator, PublicProcessorFactory } from '@aztec/simulator/server';
14
14
  import {
15
15
  EventDrivenL2BlockStream,
16
16
  type L2BlockId,
@@ -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 { EpochSession, EpochSessionHooks } from './job/epoch-session.js';
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';
@@ -121,6 +122,7 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
121
122
  protected readonly p2pClient: { getTxProvider(): ITxProvider } & Partial<Service>,
122
123
  protected readonly rollupContract: RollupContract,
123
124
  protected readonly l1Metrics: L1Metrics,
125
+ private readonly avmSimulator: AvmSimulator,
124
126
  config: Partial<ProverNodeOptions> = {},
125
127
  protected readonly telemetryClient: TelemetryClient = getTelemetryClient(),
126
128
  private delayer?: Delayer,
@@ -157,6 +159,7 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
157
159
  chonkCache: this.chonkCache,
158
160
  publicProcessorFactory: new PublicProcessorFactory(
159
161
  this.contractDataSource,
162
+ this.avmSimulator,
160
163
  this.dateProvider,
161
164
  this.telemetryClient,
162
165
  this.log.getBindings(),
@@ -168,6 +171,9 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
168
171
  metrics: this.jobMetrics,
169
172
  txGatheringTimeoutMs: this.config.txGatheringTimeoutMs,
170
173
  deadline: undefined,
174
+ // A checkpoint prover that fails (a sub-tree fault or a prune-induced fork fault) uploads a
175
+ // post-mortem for its own checkpoint, independently of any session. Fire-and-forget.
176
+ onFailed: prover => void this.tryUploadCheckpointFailure(prover),
171
177
  },
172
178
  this.log.getBindings(),
173
179
  );
@@ -239,11 +245,9 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
239
245
  break;
240
246
  }
241
247
  }
242
- // Expiry is driven by the archiver's latest synced L2 slot
243
- await this.checkEpochExpiry();
244
- // Advance the local tips store only after the proving-side handling has succeeded. Any
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).
248
+ // Advance the local tips store only after the proving-side handling (registration / prune) has
249
+ // succeeded. Any failure above propagates to the L2BlockStream (which logs and stops this poll
250
+ // pass) and skips this update, so the event is re-emitted on the next poll rather than skipped
247
251
  await this.tipsStore.handleBlockStreamEvent(event);
248
252
  }
249
253
 
@@ -458,8 +462,8 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
458
462
  }
459
463
 
460
464
  /**
461
- * Releases chonk-cache entries for every block in the supplied epoch (best-effort) and
462
- * reaps every CheckpointProver in the store whose epoch number matches.
465
+ * Releases chonk-cache entries for every block in the supplied epoch (best-effort) and reaps every
466
+ * CheckpointProver in the store whose epoch is at or below it.
463
467
  */
464
468
  private async expireEpoch(epoch: EpochNumber): Promise<void> {
465
469
  try {
@@ -515,8 +519,9 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
515
519
  });
516
520
  this.blockStream.start();
517
521
 
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.
522
+ // The periodic ticker is the sole driver of the expiry sweep: it fires every poll interval whether
523
+ // or not block-stream events arrive, and RunningPromise never overlaps its own runs, so the sweep's
524
+ // `lastExpiredEpoch` high-water mark advances — and each epoch's post-mortem uploads — exactly once.
520
525
  this.expiryTicker = new RunningPromise(
521
526
  () => this.checkEpochExpiry(),
522
527
  this.log,
@@ -559,9 +564,10 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
559
564
  }
560
565
 
561
566
  /**
562
- * Constructs the session manager. Extracted so subclasses (test harness) can swap
563
- * the implementation. Wired to `tryUploadSessionFailure` so failed sessions get
564
- * their proving data uploaded.
567
+ * Constructs the session manager. Extracted so subclasses (test harness) can swap the
568
+ * implementation. Wired to upload a post-mortem when a full session ends in its own genuine failure
569
+ * (`EpochSession.hasFailed()` top-tree/submit failed with every prover healthy, so definitively not
570
+ * a prune). A `stopped` session (a prover under it failed) is not uploaded; it recovers on re-add.
565
571
  */
566
572
  protected createSessionManager(publishingService: ProofPublishingService): SessionManager {
567
573
  return new SessionManager({
@@ -578,7 +584,7 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
578
584
  finalizationDelayMs: this.config.proverNodeEpochProvingDelayMs,
579
585
  },
580
586
  onSessionFailed: async session => {
581
- await this.tryUploadSessionFailure(session);
587
+ await this.tryUploadEpochFailure(session.getId(), session.getCheckpoints());
582
588
  },
583
589
  bindings: this.log.getBindings(),
584
590
  });
@@ -596,15 +602,32 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
596
602
  this.sessionManager.setSessionHooks(hooks);
597
603
  }
598
604
 
599
- /** Uploads failure snapshots when sessions exit with `failed`. Exposed as a method so tests can spy on it. */
600
- public async tryUploadSessionFailure(session: EpochSession): Promise<string | undefined> {
601
- if (!this.config.proverNodeFailedEpochStore) {
605
+ /**
606
+ * Installs checkpoint-prover test hooks (e.g. forcing a sub-tree failure) applied to every
607
+ * CheckpointProver constructed after this call. For the e2e harness only.
608
+ */
609
+ public setCheckpointHooks(hooks: CheckpointProverTestHooks): void {
610
+ this.checkpointStore.setTestHooks(hooks);
611
+ }
612
+
613
+ /**
614
+ * Uploads a post-mortem snapshot for an epoch whose full session failed to prove, built from that
615
+ * session's checkpoint provers. Fired from the session manager's `onSessionFailed` callback (a
616
+ * genuine, race-free failure). Exposed as a method so tests can spy on it. No-ops if no failed-epoch
617
+ * store is configured or the checkpoint set is empty.
618
+ */
619
+ public async tryUploadEpochFailure(
620
+ id: string,
621
+ checkpoints: readonly CheckpointProver[],
622
+ ): Promise<string | undefined> {
623
+ if (!this.config.proverNodeFailedEpochStore || checkpoints.length === 0) {
602
624
  return undefined;
603
625
  }
604
- const data = SessionManager.buildSessionProvingData(session);
626
+ const data = await SessionManager.buildProvingData(checkpoints);
605
627
  return await uploadEpochProofFailure(
606
628
  this.config.proverNodeFailedEpochStore,
607
- session.getId(),
629
+ // The session's own id; `uploadEpochProofFailure` already prefixes the path with the epoch number.
630
+ id,
608
631
  data,
609
632
  this.l2BlockSource as Archiver,
610
633
  this.worldState,
@@ -613,8 +636,62 @@ export class ProverNode implements L2BlockStreamEventHandler, ProverNodeApi, Tra
613
636
  );
614
637
  }
615
638
 
639
+ /**
640
+ * Uploads a post-mortem for a single failed checkpoint prover, built from just that checkpoint's
641
+ * proving data. Fired (fire-and-forget) from the store's `onFailed` callback for any non-cancel
642
+ * block-proof failure — a genuine sub-tree fault or a prune-induced fork fault alike. No-ops if no
643
+ * failed-epoch store is configured, or if the checkpoint is no longer canonical (a prune left nothing
644
+ * to diagnose). Swallows its own errors so a fire-and-forget caller can't leak.
645
+ */
646
+ public async tryUploadCheckpointFailure(prover: CheckpointProver): Promise<string | undefined> {
647
+ if (!this.config.proverNodeFailedEpochStore) {
648
+ return undefined;
649
+ }
650
+ try {
651
+ // A prune-induced fork fault and a genuine sub-tree failure are indistinguishable at the moment the
652
+ // prover rejects (no control-plane cancel has landed yet). But the archiver is the authoritative
653
+ // committed chain: if this checkpoint was pruned out, its last block is no longer canonical there.
654
+ // Only upload for a checkpoint that still exists on-chain — a prune leaves nothing to diagnose, and
655
+ // the snapshot (full world-state + archiver) is expensive to produce and store.
656
+ if (!(await this.isCheckpointCanonical(prover.checkpoint))) {
657
+ this.log.debug(`Skipping checkpoint-failure upload for ${prover.id}: no longer canonical (pruned)`, {
658
+ checkpointNumber: prover.checkpoint.number,
659
+ });
660
+ return undefined;
661
+ }
662
+ const data = await SessionManager.buildProvingData([prover]);
663
+ return await uploadEpochProofFailure(
664
+ this.config.proverNodeFailedEpochStore,
665
+ // The prover's content-addressed id; the epoch number is already in the upload path.
666
+ prover.id,
667
+ data,
668
+ this.l2BlockSource as Archiver,
669
+ this.worldState,
670
+ assertRequired(pick(this.config, 'l1ChainId', 'rollupVersion', 'dataDirectory')),
671
+ this.log,
672
+ );
673
+ } catch (err) {
674
+ this.log.error(`Error uploading checkpoint failure for ${prover.id}`, err);
675
+ return undefined;
676
+ }
677
+ }
678
+
616
679
  // ---------------- helpers ----------------
617
680
 
681
+ /**
682
+ * True if the checkpoint still exists on the canonical chain: the archiver holds a block at its last
683
+ * block's height whose archive root matches. A prune (fork fault) leaves the block missing or replaced,
684
+ * so this returns false. Protected for direct unit-test access.
685
+ */
686
+ protected async isCheckpointCanonical(checkpoint: Checkpoint): Promise<boolean> {
687
+ const lastBlock = checkpoint.blocks.at(-1);
688
+ if (!lastBlock) {
689
+ return false;
690
+ }
691
+ const onChain = await this.l2BlockSource.getBlock({ number: lastBlock.number });
692
+ return !!onChain && onChain.archive.root.equals(checkpoint.archive.root);
693
+ }
694
+
618
695
  @memoize
619
696
  private getL1Constants(): Promise<L1RollupConstants> {
620
697
  return this.l2BlockSource.getL1Constants();
@@ -1,6 +1,7 @@
1
1
  import type { RollupContract } from '@aztec/ethereum/contracts';
2
2
  import type { L1TxUtils } from '@aztec/ethereum/l1-tx-utils';
3
3
  import type { PublisherManager } from '@aztec/ethereum/publisher-manager';
4
+ import type { EthAddress } from '@aztec/foundation/eth-address';
4
5
  import type { LoggerBindings } from '@aztec/foundation/log';
5
6
  import type { ProverPublisherConfig, ProverTxSenderConfig } from '@aztec/sequencer-client';
6
7
  import type { TelemetryClient } from '@aztec/telemetry-client';
@@ -13,6 +14,7 @@ export class ProverPublisherFactory {
13
14
  private deps: {
14
15
  rollupContract: RollupContract;
15
16
  publisherManager: PublisherManager<L1TxUtils>;
17
+ proofSubmissionTarget?: EthAddress;
16
18
  telemetry?: TelemetryClient;
17
19
  },
18
20
  private bindings?: LoggerBindings,
@@ -37,6 +39,7 @@ export class ProverPublisherFactory {
37
39
  {
38
40
  rollupContract: this.deps.rollupContract,
39
41
  l1TxUtils: l1Publisher,
42
+ proofSubmissionTarget: this.deps.proofSubmissionTarget,
40
43
  telemetry: this.deps.telemetry,
41
44
  },
42
45
  this.bindings,