@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
@@ -24,14 +24,6 @@ import { EpochSession, specKey } from './job/epoch-session.js';
24
24
  * freshly-constructed session.
25
25
  */ reconcileQueue;
26
26
  /** Cached L1 constants, populated on first read. */ cachedL1Constants;
27
- /**
28
- * Highest epoch for which the periodic tick has successfully created a full session.
29
- * Monotonic high-water mark: once the tick observes a session for epoch X, it stops
30
- * trying to open one — even if that session subsequently fails (only a new checkpoint
31
- * event reopens it). Crucially, the mark only advances when a session actually exists
32
- * post-open, so transient blockers (atMaxSessionLimit, archiver still indexing) leave
33
- * the mark in place and the next tick retries.
34
- */ lastTickEpoch;
35
27
  /** Test-only hooks applied to every session this manager constructs. */ sessionHooks;
36
28
  /** Periodic tick that nudges reconcile to pick up newly-complete epochs. Started by `start()`. */ epochTicker;
37
29
  constructor(deps){
@@ -150,7 +142,11 @@ import { EpochSession, specKey } from './job/epoch-session.js';
150
142
  await this.epochTicker?.stop();
151
143
  await this.reconcileQueue.cancel();
152
144
  const sessions = this.allSessions();
153
- await Promise.allSettled(sessions.map((s)=>s.cancel('prover-node stopping')));
145
+ // A clean shutdown is just a restart, so preserve the in-flight broker jobs (abortJobs: false)
146
+ // for the restarted node to reuse rather than re-proving the epoch from scratch.
147
+ await Promise.allSettled(sessions.map((s)=>s.cancel('prover-node stopping', {
148
+ abortJobs: false
149
+ })));
154
150
  }
155
151
  // ---------------- reconcile ----------------
156
152
  scheduleReconcile(trigger) {
@@ -165,31 +161,34 @@ import { EpochSession, specKey } from './job/epoch-session.js';
165
161
  for (const epoch of implicatedEpochs){
166
162
  await this.openFullSessionIfReady(epoch);
167
163
  }
168
- // Advance the tick high-water mark only once a session actually exists for the epoch.
169
- // `openFullSessionIfReady` can early-return without creating one (atMaxSessionLimit,
170
- // archiver still indexing, etc.); in those cases we want the next tick to try again
171
- // rather than skip the epoch forever.
172
- if (trigger.kind === 'tick' && implicatedEpochs.length === 1) {
173
- const epoch = implicatedEpochs[0];
174
- if (this.fullSessions.has(epoch)) {
175
- this.lastTickEpoch = epoch;
176
- }
177
- }
178
164
  if (trigger.kind === 'start-proof') {
179
165
  this.openPartialSession(trigger.spec);
180
166
  }
181
167
  }
182
168
  recreateInvalidSessions() {
183
169
  for (const [key, session] of Array.from(this.fullSessions.entries())){
170
+ const canonical = this.checkpointsForSpec(session.getSpec());
171
+ const contentChanged = !this.checkpointsMatch(session.getCheckpoints(), canonical);
184
172
  if (session.isTerminal()) {
173
+ // A full session that failed on its own account is retained as a "do not re-prove" marker while
174
+ // its content is unchanged — this is what stops the tick re-proving a deterministically-failing
175
+ // epoch. When the content changes (a re-add), it is replaced so the epoch retries over the new
176
+ // provers. Any other terminal full session is simply dropped.
177
+ if (session.hasFailed() && !contentChanged) {
178
+ continue;
179
+ }
185
180
  this.fullSessions.delete(key);
181
+ if (contentChanged && this.canBuildOver(canonical)) {
182
+ const newSession = this.constructSession(session.getSpec(), canonical);
183
+ this.fullSessions.set(key, newSession);
184
+ void this.runSession(newSession);
185
+ }
186
186
  continue;
187
187
  }
188
- const canonical = this.checkpointsForSpec(session.getSpec());
189
- if (!this.checkpointsMatch(session.getCheckpoints(), canonical)) {
188
+ if (contentChanged) {
190
189
  this.fireAndForgetCancel(session, 'canonical content changed');
191
190
  this.fullSessions.delete(key);
192
- if (canonical.length > 0) {
191
+ if (this.canBuildOver(canonical)) {
193
192
  const newSession = this.constructSession(session.getSpec(), canonical);
194
193
  this.fullSessions.set(key, newSession);
195
194
  void this.runSession(newSession);
@@ -205,7 +204,7 @@ import { EpochSession, specKey } from './job/epoch-session.js';
205
204
  if (!this.checkpointsMatch(session.getCheckpoints(), canonical)) {
206
205
  this.fireAndForgetCancel(session, 'canonical content changed');
207
206
  this.partialSessions.delete(key);
208
- if (canonical.length > 0) {
207
+ if (this.canBuildOver(canonical)) {
209
208
  const newSession = this.constructSession(session.getSpec(), canonical);
210
209
  this.partialSessions.set(key, newSession);
211
210
  void this.runSession(newSession);
@@ -213,9 +212,13 @@ import { EpochSession, specKey } from './job/epoch-session.js';
213
212
  }
214
213
  }
215
214
  }
215
+ /** A session may be built over a checkpoint set only when it is non-empty and contains no failed prover. */ canBuildOver(canonical) {
216
+ return canonical.length > 0 && !this.hasFailedProver(canonical);
217
+ }
216
218
  async openFullSessionIfReady(epoch) {
217
- // `recreateInvalidSessions` runs at the top of every reconcile and deletes terminal sessions
218
- // before this is called, so a session present here is live and already covers the epoch.
219
+ // A session present here already covers the epoch: either live, or a retained genuinely-failed
220
+ // session kept by `recreateInvalidSessions` as a "do not re-prove" marker. Either way, don't open
221
+ // another — the retained-failed one is replaced only when its canonical content changes.
219
222
  if (this.fullSessions.has(epoch)) {
220
223
  return;
221
224
  }
@@ -242,6 +245,15 @@ import { EpochSession, specKey } from './job/epoch-session.js';
242
245
  });
243
246
  return;
244
247
  }
248
+ if (this.hasFailedProver(canonical)) {
249
+ // A checkpoint prover in the set has failed (a sub-tree fault or a prune-induced fork fault), so a
250
+ // session over it would fail immediately. Don't re-create it every tick — it recovers when a
251
+ // prune/re-add replaces the failed prover with a fresh one, or fails for good at expiry.
252
+ this.log.debug(`Skipping full-session open for epoch ${epoch}: a checkpoint prover has failed`, {
253
+ epoch
254
+ });
255
+ return;
256
+ }
245
257
  const spec = {
246
258
  kind: 'full',
247
259
  epochNumber: epoch,
@@ -254,7 +266,7 @@ import { EpochSession, specKey } from './job/epoch-session.js';
254
266
  }
255
267
  openPartialSession(spec) {
256
268
  const canonical = this.deps.checkpointStore.listInSlotRange(spec.fromSlot, spec.toSlot);
257
- if (canonical.length === 0) {
269
+ if (canonical.length === 0 || this.hasFailedProver(canonical)) {
258
270
  return;
259
271
  }
260
272
  // Reuse a live partial session for this epoch whose checkpoint set already matches the
@@ -312,31 +324,27 @@ import { EpochSession, specKey } from './job/epoch-session.js';
312
324
  }
313
325
  const state = await session.start();
314
326
  this.log.info(`Session ${session.getId()} exited with state ${state}`);
315
- if (state === 'failed' && this.deps.onSessionFailed) {
316
- // Best-effort suppression of the spurious post-mortem upload a prune produces: if the session's
317
- // checkpoints no longer match the store's current set, the failure was caused by the content
318
- // changing under it, not a genuine proving fault, so skip the upload. This is inherently racy —
319
- // the store lags the world-state unwind, so a fault observed before the prune is reconciled here
320
- // still uploads. The epoch is recovered regardless by recreating the session on re-add.
321
- if (!this.checkpointsMatch(session.getCheckpoints(), this.checkpointsForSpec(session.getSpec()))) {
322
- this.log.info(`Skipping failure upload for session ${session.getId()}: canonical content changed`, {
323
- ...session.getSpec()
324
- });
325
- return;
326
- }
327
+ // A full session that failed on its own account (top-tree/submit failed with every prover healthy)
328
+ // is a genuine, race-free failure: upload its post-mortem once. `recreateInvalidSessions` retains
329
+ // the terminal session so this fires exactly once (it is never re-run over the same content). A
330
+ // `stopped` session (a prover under it failed) is not uploaded it may be a prune, and recovers on
331
+ // re-add.
332
+ if (session.getKind() === 'full' && session.hasFailed() && this.deps.onSessionFailed) {
327
333
  try {
328
334
  await this.deps.onSessionFailed(session);
329
335
  } catch (err) {
330
- this.log.error(`Error in onSessionFailed callback for ${session.getSpec().epochNumber}`, err);
336
+ this.log.error(`Error in onSessionFailed callback for epoch ${session.getEpochNumber()}`, err);
331
337
  }
332
338
  }
333
339
  }
334
340
  /**
335
- * Builds the EpochProvingJobData snapshot for failure upload. Includes every checkpoint
336
- * referenced by the session, regardless of whether sub-tree proving completed —
337
- * partial state is still useful for post-mortem analysis.
338
- */ static buildSessionProvingData(session) {
339
- const checkpoints = session.getCheckpoints();
341
+ * Builds the EpochProvingJobData snapshot for a post-mortem failure upload from a set of
342
+ * checkpoint provers. Includes every checkpoint regardless of whether sub-tree proving
343
+ * completed — partial state is still useful for post-mortem analysis.
344
+ */ static buildProvingData(checkpoints) {
345
+ if (checkpoints.length === 0) {
346
+ throw new Error('Cannot build proving data from an empty checkpoint set');
347
+ }
340
348
  const txs = new Map();
341
349
  const l1ToL2Messages = {};
342
350
  for (const c of checkpoints){
@@ -346,7 +354,7 @@ import { EpochSession, specKey } from './job/epoch-session.js';
346
354
  l1ToL2Messages[c.checkpoint.number] = c.l1ToL2Messages;
347
355
  }
348
356
  return {
349
- epochNumber: session.getSpec().epochNumber,
357
+ epochNumber: checkpoints[0].epochNumber,
350
358
  checkpoints: checkpoints.map((c)=>c.checkpoint),
351
359
  txs,
352
360
  l1ToL2Messages,
@@ -366,20 +374,12 @@ import { EpochSession, specKey } from './job/epoch-session.js';
366
374
  /**
367
375
  * Maps a reconcile trigger to the epochs whose full session should be (re)opened.
368
376
  *
369
- * This is where the "don't retry a genuinely-failed epoch, but do recover a pruned one" invariant
370
- * lives enforced by which triggers are gated by `lastTickEpoch`:
371
- *
372
- * - The periodic `tick` IS gated: once a tick has opened a session for an epoch, `lastTickEpoch`
373
- * advances to it and later ticks skip it (`epoch <= lastTickEpoch`). So a failed attempt is never
374
- * resubmitted on a loop by the tick.
375
- * - `checkpoint` and `prune` are deliberately NOT gated. They only fire when the epoch's canonical
376
- * content actually changes — a checkpoint arrives, or a reorg prunes/replaces one — which is
377
- * exactly when re-attempting is correct.
378
- *
379
- * A genuine proving failure produces no content change, hence no checkpoint/prune event, so only
380
- * the gated tick could reopen it — and it won't. A prune + re-add fires ungated events, so the
381
- * epoch is reopened through this path (and `openFullSessionIfReady` rebuilds over the fresh
382
- * provers). See the "onTick does not retry ... but recovers ... re-added" test.
377
+ * The periodic `tick` returns the next unproven epoch every time; it does not track prior attempts.
378
+ * `openFullSessionIfReady` is what keeps this from re-proving a doomed epoch: it refuses to build a
379
+ * session when any checkpoint prover in the set has failed, so a stuck epoch is cheaply skipped each
380
+ * tick rather than re-proved. `checkpoint` and `prune` fire when an epoch's canonical content changes
381
+ * (a checkpoint arrives, or a reorg prunes/replaces one) which is what installs a fresh prover in
382
+ * place of a failed one, letting the next open succeed and recovering a pruned-then-re-added epoch.
383
383
  */ async epochsForTrigger(trigger) {
384
384
  switch(trigger.kind){
385
385
  case 'checkpoint':
@@ -391,10 +391,7 @@ import { EpochSession, specKey } from './job/epoch-session.js';
391
391
  case 'tick':
392
392
  {
393
393
  const epoch = await this.nextUnprovenEpoch();
394
- if (epoch === undefined || this.lastTickEpoch !== undefined && epoch <= this.lastTickEpoch) {
395
- return [];
396
- }
397
- return [
394
+ return epoch === undefined ? [] : [
398
395
  epoch
399
396
  ];
400
397
  }
@@ -423,6 +420,13 @@ import { EpochSession, specKey } from './job/epoch-session.js';
423
420
  checkpointsForSpec(spec) {
424
421
  return this.deps.checkpointStore.listInSlotRange(spec.fromSlot, spec.toSlot);
425
422
  }
423
+ /**
424
+ * True if any prover in the set has failed. The epoch cannot be proven over a failed prover (it can
425
+ * never produce its block proofs), so a session must not be built or rebuilt over it until a prune/re-add
426
+ * has replaced it with a fresh prover.
427
+ */ hasFailedProver(checkpoints) {
428
+ return checkpoints.some((c)=>c.isFailed());
429
+ }
426
430
  fireAndForgetCancel(session, reason) {
427
431
  void session.cancel(reason).catch((err)=>this.log.warn(`Error cancelling session ${session.getId()}`, err));
428
432
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/prover-node",
3
- "version": "0.0.1-commit.3100065",
3
+ "version": "0.0.1-commit.321f6a9",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dest/index.js",
@@ -56,28 +56,29 @@
56
56
  ]
57
57
  },
58
58
  "dependencies": {
59
- "@aztec/archiver": "0.0.1-commit.3100065",
60
- "@aztec/bb-prover": "0.0.1-commit.3100065",
61
- "@aztec/blob-client": "0.0.1-commit.3100065",
62
- "@aztec/blob-lib": "0.0.1-commit.3100065",
63
- "@aztec/constants": "0.0.1-commit.3100065",
64
- "@aztec/epoch-cache": "0.0.1-commit.3100065",
65
- "@aztec/ethereum": "0.0.1-commit.3100065",
66
- "@aztec/foundation": "0.0.1-commit.3100065",
67
- "@aztec/kv-store": "0.0.1-commit.3100065",
68
- "@aztec/l1-artifacts": "0.0.1-commit.3100065",
69
- "@aztec/native": "0.0.1-commit.3100065",
70
- "@aztec/node-keystore": "0.0.1-commit.3100065",
71
- "@aztec/node-lib": "0.0.1-commit.3100065",
72
- "@aztec/noir-protocol-circuits-types": "0.0.1-commit.3100065",
73
- "@aztec/p2p": "0.0.1-commit.3100065",
74
- "@aztec/protocol-contracts": "0.0.1-commit.3100065",
75
- "@aztec/prover-client": "0.0.1-commit.3100065",
76
- "@aztec/sequencer-client": "0.0.1-commit.3100065",
77
- "@aztec/simulator": "0.0.1-commit.3100065",
78
- "@aztec/stdlib": "0.0.1-commit.3100065",
79
- "@aztec/telemetry-client": "0.0.1-commit.3100065",
80
- "@aztec/world-state": "0.0.1-commit.3100065",
59
+ "@aztec/archiver": "0.0.1-commit.321f6a9",
60
+ "@aztec/bb-prover": "0.0.1-commit.321f6a9",
61
+ "@aztec/bb.js": "0.0.1-commit.321f6a9",
62
+ "@aztec/blob-client": "0.0.1-commit.321f6a9",
63
+ "@aztec/blob-lib": "0.0.1-commit.321f6a9",
64
+ "@aztec/constants": "0.0.1-commit.321f6a9",
65
+ "@aztec/epoch-cache": "0.0.1-commit.321f6a9",
66
+ "@aztec/ethereum": "0.0.1-commit.321f6a9",
67
+ "@aztec/foundation": "0.0.1-commit.321f6a9",
68
+ "@aztec/kv-store": "0.0.1-commit.321f6a9",
69
+ "@aztec/l1-artifacts": "0.0.1-commit.321f6a9",
70
+ "@aztec/native": "0.0.1-commit.321f6a9",
71
+ "@aztec/node-keystore": "0.0.1-commit.321f6a9",
72
+ "@aztec/node-lib": "0.0.1-commit.321f6a9",
73
+ "@aztec/noir-protocol-circuits-types": "0.0.1-commit.321f6a9",
74
+ "@aztec/p2p": "0.0.1-commit.321f6a9",
75
+ "@aztec/protocol-contracts": "0.0.1-commit.321f6a9",
76
+ "@aztec/prover-client": "0.0.1-commit.321f6a9",
77
+ "@aztec/sequencer-client": "0.0.1-commit.321f6a9",
78
+ "@aztec/simulator": "0.0.1-commit.321f6a9",
79
+ "@aztec/stdlib": "0.0.1-commit.321f6a9",
80
+ "@aztec/telemetry-client": "0.0.1-commit.321f6a9",
81
+ "@aztec/world-state": "0.0.1-commit.321f6a9",
81
82
  "source-map-support": "^0.5.21",
82
83
  "tslib": "^2.4.0",
83
84
  "viem": "npm:@aztec/viem@2.38.2"
@@ -7,7 +7,7 @@ import { type ProverClientConfig, createProverClient } from '@aztec/prover-clien
7
7
  import { ProverBrokerConfig, createAndStartProvingBroker } from '@aztec/prover-client/broker';
8
8
  import { getLastSiblingPath } from '@aztec/prover-client/helpers';
9
9
  import { ChonkCache } from '@aztec/prover-client/orchestrator';
10
- import { PublicProcessorFactory } from '@aztec/simulator/server';
10
+ import { AvmSimulatorPool, PublicProcessorFactory } from '@aztec/simulator/server';
11
11
  import type { L2Block } from '@aztec/stdlib/block';
12
12
  import { getEpochAtSlot, getSlotRangeForEpoch } from '@aztec/stdlib/epoch-helpers';
13
13
  import type { ITxProvider } from '@aztec/stdlib/interfaces/server';
@@ -21,35 +21,30 @@ import { createWorldState } from '@aztec/world-state';
21
21
  import { readFileSync } from 'fs';
22
22
 
23
23
  import { CheckpointProver } from '../job/checkpoint-prover.js';
24
- import { deserializeEpochProvingJobData } from '../job/epoch-proving-job-data.js';
24
+ import { type EpochProvingJobData, deserializeEpochProvingJobData } from '../job/epoch-proving-job-data.js';
25
25
  import { EpochSession, type SessionSpec } from '../job/epoch-session.js';
26
26
  import { ProverNodeJobMetrics } from '../metrics.js';
27
27
 
28
+ type RerunConfig = DataStoreConfig &
29
+ ProverBrokerConfig &
30
+ ProverClientConfig &
31
+ Pick<L1ContractsConfig, 'aztecEpochDuration'>;
32
+
28
33
  /**
29
34
  * Given a local folder where `downloadEpochProvingJob` was called, creates a new archiver and world state
30
35
  * using the state snapshots, and creates a new epoch proving session to prove the downloaded proving job.
31
36
  * Proving is done with a local proving broker and agents as specified by the config.
32
37
  */
33
- export async function rerunEpochProvingJob(
34
- localPath: string,
35
- log: Logger,
36
- config: DataStoreConfig & ProverBrokerConfig & ProverClientConfig & Pick<L1ContractsConfig, 'aztecEpochDuration'>,
37
- genesis?: GenesisData,
38
- ) {
39
- const jobData = deserializeEpochProvingJobData(readFileSync(localPath));
40
- log.info(`Loaded proving job data for epoch ${jobData.epochNumber}`);
38
+ export async function rerunEpochProvingJob(localPath: string, log: Logger, config: RerunConfig, genesis?: GenesisData) {
39
+ await using ctx = await createRerunContext(localPath, log, config, genesis);
40
+ const { jobData, prover, metrics } = ctx;
41
41
 
42
- const telemetry = getTelemetryClient();
43
- const metrics = new ProverNodeJobMetrics(telemetry.getMeter('prover-job'), telemetry.getTracer('prover-job'));
44
- const worldState = await createWorldState(config, genesis);
45
- const initialBlockHash = await worldState.getInitialHeader().hash();
46
- const archiver = await createArchiverStore(config, initialBlockHash);
47
- const publicProcessorFactory = new PublicProcessorFactory(
48
- createContractDataSource(archiver),
49
- undefined,
50
- undefined,
51
- log.getBindings(),
52
- );
42
+ log.info(`Rerunning epoch proving for epoch ${jobData.epochNumber}`);
43
+
44
+ const provers: CheckpointProver[] = [];
45
+ for (let i = 0; i < jobData.checkpoints.length; i++) {
46
+ provers.push(await buildCheckpointProver(ctx, i, log));
47
+ }
53
48
 
54
49
  // Local rerun never publishes — stub the service so submit() always resolves 'published'
55
50
  // and withdraw is a no-op.
@@ -57,51 +52,6 @@ export async function rerunEpochProvingJob(
57
52
  submit: () => Promise.resolve('published' as const),
58
53
  withdraw: () => {},
59
54
  };
60
- const broker = await createAndStartProvingBroker(config, telemetry);
61
- const prover = await createProverClient(config, worldState, broker, telemetry);
62
- const chonkCache = new ChonkCache(log.getBindings());
63
-
64
- const txProvider = makeReplayingTxProvider(jobData.txs);
65
-
66
- log.info(`Rerunning epoch proving for epoch ${jobData.epochNumber}`);
67
-
68
- const provers: CheckpointProver[] = [];
69
- for (let i = 0; i < jobData.checkpoints.length; i++) {
70
- const checkpoint = jobData.checkpoints[i];
71
- const previousBlockHeader =
72
- i === 0 ? jobData.previousBlockHeader : jobData.checkpoints[i - 1].blocks.at(-1)!.header;
73
- const l1ToL2Messages = jobData.l1ToL2Messages[checkpoint.number] ?? [];
74
- const previousArchiveSiblingPath = await getLastSiblingPath(
75
- MerkleTreeId.ARCHIVE,
76
- worldState.getSnapshot(BlockNumber(checkpoint.blocks[0].number - 1)),
77
- );
78
- const attestations = checkpoint.number === jobData.checkpoints.at(-1)!.number ? jobData.attestations : [];
79
- provers.push(
80
- new CheckpointProver(
81
- {
82
- checkpoint,
83
- epochNumber: jobData.epochNumber,
84
- attestations,
85
- previousBlockHeader,
86
- l1ToL2Messages,
87
- previousArchiveSiblingPath,
88
- },
89
- {
90
- proverFactory: prover,
91
- chonkCache,
92
- publicProcessorFactory,
93
- dbProvider: worldState,
94
- txProvider,
95
- dateProvider: new DateProvider(),
96
- proverId: prover.getProverId(),
97
- metrics,
98
- txGatheringTimeoutMs: 120_000,
99
- deadline: undefined,
100
- log,
101
- },
102
- ),
103
- );
104
- }
105
55
 
106
56
  const l1Constants = { epochDuration: config.aztecEpochDuration };
107
57
  const [fromSlot, toSlot] = getSlotRangeForEpoch(jobData.epochNumber, l1Constants);
@@ -125,6 +75,129 @@ export async function rerunEpochProvingJob(
125
75
  return finalState;
126
76
  }
127
77
 
78
+ /**
79
+ * Re-proves a single downloaded checkpoint proving job (as uploaded by a `CheckpointProver` failure).
80
+ * Reconstructs just that checkpoint's sub-tree prover from the snapshot and awaits its block proofs — no
81
+ * epoch top-tree, no L1 submission — so a checkpoint-level failure can be reproduced offline in isolation.
82
+ * Returns the block-rollup proof outputs on success; throws if the checkpoint fails to prove again.
83
+ */
84
+ export async function rerunCheckpointProvingJob(
85
+ localPath: string,
86
+ log: Logger,
87
+ config: RerunConfig,
88
+ genesis?: GenesisData,
89
+ ) {
90
+ await using ctx = await createRerunContext(localPath, log, config, genesis);
91
+ const { jobData } = ctx;
92
+ const checkpointNumber = jobData.checkpoints[0].number;
93
+
94
+ log.info(`Rerunning checkpoint proving for checkpoint ${checkpointNumber} (epoch ${jobData.epochNumber})`);
95
+
96
+ const prover = await buildCheckpointProver(ctx, 0, log);
97
+ try {
98
+ const blockProofs = await prover.whenBlockProofsReady();
99
+ log.info(`Completed proving for checkpoint ${checkpointNumber} with ${blockProofs.length} block proof(s)`);
100
+ return blockProofs;
101
+ } finally {
102
+ prover.cancel({ routine: true });
103
+ await prover.whenDone();
104
+ }
105
+ }
106
+
107
+ /** Everything a rerun needs, reconstructed from the downloaded snapshot + job data. */
108
+ type RerunContext = {
109
+ jobData: EpochProvingJobData;
110
+ metrics: ProverNodeJobMetrics;
111
+ worldState: Awaited<ReturnType<typeof createWorldState>>;
112
+ publicProcessorFactory: PublicProcessorFactory;
113
+ prover: Awaited<ReturnType<typeof createProverClient>>;
114
+ chonkCache: ChonkCache;
115
+ txProvider: ITxProvider;
116
+ } & AsyncDisposable;
117
+
118
+ /**
119
+ * Rebuilds the offline proving environment from a downloaded job: world state + archiver from the
120
+ * snapshots, a local proving broker + client, a chonk cache, and a tx provider that replays the job's txs.
121
+ */
122
+ async function createRerunContext(
123
+ localPath: string,
124
+ log: Logger,
125
+ config: RerunConfig,
126
+ genesis?: GenesisData,
127
+ ): Promise<RerunContext> {
128
+ const jobData = deserializeEpochProvingJobData(readFileSync(localPath));
129
+ log.info(`Loaded proving job data for epoch ${jobData.epochNumber}`);
130
+
131
+ const telemetry = getTelemetryClient();
132
+ const metrics = new ProverNodeJobMetrics(telemetry.getMeter('prover-job'), telemetry.getTracer('prover-job'));
133
+ const worldState = await createWorldState(config, genesis);
134
+ const initialBlockHash = await worldState.getInitialHeader().hash();
135
+ const archiver = await createArchiverStore(config, initialBlockHash);
136
+ const avmSimulator = await AvmSimulatorPool.spawn({ wsdbIpcPath: worldState.getIpcPath() });
137
+ const publicProcessorFactory = new PublicProcessorFactory(
138
+ createContractDataSource(archiver),
139
+ avmSimulator,
140
+ undefined,
141
+ undefined,
142
+ log.getBindings(),
143
+ );
144
+ const broker = await createAndStartProvingBroker(config, telemetry);
145
+ const prover = await createProverClient(config, worldState, broker, telemetry);
146
+ const chonkCache = new ChonkCache(log.getBindings());
147
+ const txProvider = makeReplayingTxProvider(jobData.txs);
148
+
149
+ return {
150
+ jobData,
151
+ metrics,
152
+ worldState,
153
+ publicProcessorFactory,
154
+ prover,
155
+ chonkCache,
156
+ txProvider,
157
+ async [Symbol.asyncDispose]() {
158
+ await avmSimulator[Symbol.asyncDispose]();
159
+ await worldState[Symbol.asyncDispose]();
160
+ },
161
+ };
162
+ }
163
+
164
+ /** Reconstructs the `CheckpointProver` for the checkpoint at `index` in the job, ready to prove. */
165
+ async function buildCheckpointProver(ctx: RerunContext, index: number, log: Logger): Promise<CheckpointProver> {
166
+ const { jobData, worldState, prover, chonkCache, publicProcessorFactory, txProvider, metrics } = ctx;
167
+ const checkpoint = jobData.checkpoints[index];
168
+ const previousBlockHeader =
169
+ index === 0 ? jobData.previousBlockHeader : jobData.checkpoints[index - 1].blocks.at(-1)!.header;
170
+ const l1ToL2Messages = jobData.l1ToL2Messages[checkpoint.number] ?? [];
171
+ const previousArchiveSiblingPath = await getLastSiblingPath(
172
+ MerkleTreeId.ARCHIVE,
173
+ worldState.getSnapshot(BlockNumber(checkpoint.blocks[0].number - 1)),
174
+ );
175
+ const attestations = checkpoint.number === jobData.checkpoints.at(-1)!.number ? jobData.attestations : [];
176
+ return new CheckpointProver(
177
+ {
178
+ checkpoint,
179
+ epochNumber: jobData.epochNumber,
180
+ attestations,
181
+ previousBlockHeader,
182
+ l1ToL2Messages,
183
+ previousArchiveSiblingPath,
184
+ },
185
+ {
186
+ proverFactory: prover,
187
+ chonkCache,
188
+ publicProcessorFactory,
189
+ dbProvider: worldState,
190
+ txProvider,
191
+ dateProvider: new DateProvider(),
192
+ proverId: prover.getProverId(),
193
+ metrics,
194
+ txGatheringTimeoutMs: 120_000,
195
+ deadline: undefined,
196
+ log,
197
+ },
198
+ );
199
+ }
200
+
128
201
  /** Build a synthetic ITxProvider that returns the supplied txs map by lookup. */
129
202
  function makeReplayingTxProvider(txs: Map<string, Tx>): ITxProvider {
130
203
  const lookup = (hashes: TxHash[]) => {
@@ -4,7 +4,12 @@ import type { L2BlockSource } from '@aztec/stdlib/block';
4
4
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
5
5
  import { type L1RollupConstants, getEpochAtSlot, getSlotRangeForEpoch } from '@aztec/stdlib/epoch-helpers';
6
6
 
7
- import { CheckpointProver, type CheckpointProverArgs, type CheckpointProverDeps } from './job/checkpoint-prover.js';
7
+ import {
8
+ CheckpointProver,
9
+ type CheckpointProverArgs,
10
+ type CheckpointProverDeps,
11
+ type CheckpointProverTestHooks,
12
+ } from './job/checkpoint-prover.js';
8
13
 
9
14
  /** Register-time data needed to construct a `CheckpointProver` (everything except the checkpoint + epoch). */
10
15
  export type RegisterCheckpointData = Omit<CheckpointProverArgs, 'checkpoint' | 'epochNumber'>;
@@ -35,6 +40,8 @@ export class CheckpointStore {
35
40
  */
36
41
  private readonly pendingTeardowns = new Map<number, Promise<void>>();
37
42
  private nextTeardownId = 0;
43
+ /** Test-only hooks injected into every prover this store constructs. */
44
+ private testHooks: CheckpointProverTestHooks = {};
38
45
  private readonly log: Logger;
39
46
 
40
47
  constructor(
@@ -100,11 +107,22 @@ export class CheckpointStore {
100
107
  }
101
108
  }
102
109
 
103
- const prover = this.proverFactoryFn({ ...data, checkpoint, epochNumber }, { ...this.proverDeps, log: this.log });
110
+ const prover = this.proverFactoryFn(
111
+ { ...data, checkpoint, epochNumber },
112
+ { ...this.proverDeps, checkpointProveOverride: this.testHooks.checkpointProveOverride, log: this.log },
113
+ );
104
114
  this.provers.set(id, prover);
105
115
  return prover;
106
116
  }
107
117
 
118
+ /**
119
+ * Installs test-only hooks applied to every prover constructed from now on. Used by the e2e harness to
120
+ * force a checkpoint sub-tree failure without monkey-patching the prover factory.
121
+ */
122
+ public setTestHooks(hooks: CheckpointProverTestHooks): void {
123
+ this.testHooks = hooks;
124
+ }
125
+
108
126
  /**
109
127
  * Cancels and removes every prover that holds a block above the prune target. A checkpoint is orphaned by a prune to
110
128
  * block `targetBlockNumber` iff its last block sits above the target — including a checkpoint whose range straddles
package/src/config.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  numberConfigHelper,
7
7
  pickConfigMappings,
8
8
  } from '@aztec/foundation/config';
9
+ import { EthAddress } from '@aztec/foundation/eth-address';
9
10
  import { type KeyStoreConfig, keyStoreConfigMappings } from '@aztec/node-keystore/config';
10
11
  import { ethPrivateKeySchema } from '@aztec/node-keystore/schemas';
11
12
  import type { KeyStore } from '@aztec/node-keystore/types';
@@ -44,6 +45,7 @@ export type SpecificProverNodeConfig = {
44
45
  txGatheringIntervalMs: number;
45
46
  txGatheringBatchSize: number;
46
47
  txGatheringMaxParallelRequestsPerNode: number;
48
+ proofSubmissionTargetAddress?: EthAddress;
47
49
  };
48
50
 
49
51
  export const specificProverNodeConfigMappings: ConfigMappingsType<SpecificProverNodeConfig> = {
@@ -97,6 +99,14 @@ export const specificProverNodeConfigMappings: ConfigMappingsType<SpecificProver
97
99
  description: 'Whether the prover node skips publishing proofs to L1',
98
100
  ...booleanConfigHelper(false),
99
101
  },
102
+ proofSubmissionTargetAddress: {
103
+ env: 'PROVER_NODE_PROOF_SUBMISSION_TARGET_ADDRESS',
104
+ description:
105
+ 'Optional L1 address the submitEpochRootProof tx is sent to. Must expose the identical submitEpochRootProof ABI ' +
106
+ 'and forward to the rollup. Defaults to the rollup address.',
107
+ parseEnv: (val: string) => EthAddress.fromString(val),
108
+ defaultValue: undefined,
109
+ },
100
110
  };
101
111
 
102
112
  export const proverNodeConfigMappings: ConfigMappingsType<ProverNodeConfig> = {
package/src/factory.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  type ProverTxSenderConfig,
20
20
  getPublisherConfigFromProverConfig,
21
21
  } from '@aztec/sequencer-client';
22
+ import type { AvmSimulator } from '@aztec/simulator/server';
22
23
  import type {
23
24
  ITxProvider,
24
25
  ProverConfig,
@@ -47,6 +48,8 @@ export type ProverNodeDeps = {
47
48
  epochCache: EpochCacheInterface;
48
49
  blobClient: BlobClientInterface;
49
50
  keyStoreManager?: KeystoreManager;
51
+ /** AVM execution backend (simulator pool + CDB server) for public simulation. */
52
+ avmSimulator: AvmSimulator;
50
53
  };
51
54
 
52
55
  /** Creates a new prover node subsystem given a config and dependencies */
@@ -135,6 +138,7 @@ export async function createProverNode(
135
138
  deps.publisherFactory ??
136
139
  new ProverPublisherFactory(config, {
137
140
  rollupContract,
141
+ proofSubmissionTarget: config.proofSubmissionTargetAddress,
138
142
  publisherManager: new PublisherManager(l1TxUtils, getPublisherConfigFromProverConfig(config), {
139
143
  bindings: log.getBindings(),
140
144
  funder: funderL1TxUtils,
@@ -179,6 +183,7 @@ export async function createProverNode(
179
183
  p2pClient,
180
184
  rollupContract,
181
185
  l1Metrics,
186
+ deps.avmSimulator,
182
187
  proverNodeConfig,
183
188
  telemetry,
184
189
  delayer,