@aztec/prover-node 6.0.0-nightly.20260723 → 6.0.0-nightly.20260725

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 CHANGED
@@ -35,7 +35,6 @@ flowchart TB
35
35
  SessionManager --> EpochTicker[(periodic tick)]
36
36
  SessionManager --> FullSessions[(fullSessions)]
37
37
  SessionManager --> PartialSessions[(partialSessions)]
38
- CheckpointStore --> SlotWatcher
39
38
  FullSessions -.referenced checkpoints.-> CheckpointStore
40
39
  PartialSessions -.referenced checkpoints.-> CheckpointStore
41
40
  FullSessions --> TopTreeJob
@@ -49,23 +48,30 @@ The prover-node splits responsibility between four classes:
49
48
 
50
49
  - **`ProverNode`** — owns the long-lived collections, wires the L2BlockStream, and
51
50
  translates each chain event into a single method call on the `SessionManager` or
52
- `ProofPublishingService`. It also performs the per-event side effects that don't
53
- belong on an `EpochSession` (registering new checkpoints with the store, sweeping
54
- expired epochs out of the cache and the store, etc.) and runs the failure-upload
55
- action when an `EpochSession` exits with `failed`.
51
+ `ProofPublishingService`. It also performs side effects that don't belong on an
52
+ `EpochSession`: registering new checkpoints with the store per event, and sweeping expired
53
+ epochs out of the cache and the store on a periodic ticker (see below). It uploads post-mortem
54
+ snapshots on failure at two levels: per checkpoint when a `CheckpointProver` fails (its `onFailed`
55
+ callback), and per session when a full session fails on its own account (`onSessionFailed` /
56
+ `EpochSession.hasFailed()`).
56
57
  - **`CheckpointStore`** — a registry of `CheckpointProver` instances keyed by
57
58
  `(checkpointNumber, slot, archiveRoot)`. Each `CheckpointProver` runs its own sub-tree pipeline
58
59
  (tx gather → block processing → block-rollup proofs), starting eagerly the moment a
59
- checkpoint is registered. The store is the single source of canonical-vs-pruned
60
- checkpoint content that `EpochSession`s query when assembling their subsets.
60
+ checkpoint is registered. The store holds the canonical checkpoint content that
61
+ `EpochSession`s query when assembling their subsets; a prune cancels and removes the affected
62
+ provers, so every prover in the store is canonical.
61
63
  - **`SessionManager`** — owns every live `EpochSession`, the serial reconcile queue,
62
64
  the periodic tick, and all `EpochSession` lifecycle decisions. `ProverNode` calls into it
63
65
  via `onCheckpointAdded`, `onPrune`, and `startProof`. Every trigger it receives is
64
66
  translated into a `reconcile(trigger)` call, a single idempotent function that walks
65
67
  all `EpochSession`s, cancels any whose canonical content has shifted, re-creates them with
66
68
  the new content, and opens fresh full `EpochSession`s for any epoch that has become provable.
67
- Reconcile runs on a `SerialQueue` (from `@aztec/foundation/queue`), so two concurrent
68
- triggers can never interleave on an `await` and race on the `EpochSession` maps.
69
+ It never builds a session over a **failed** `CheckpointProver` (one whose block proofs rejected —
70
+ a sub-tree fault or a prune-induced fork fault): a session over it could only fail, so the epoch is
71
+ cheaply skipped until a prune/re-add installs a fresh prover in its place, at which point a rebuilt
72
+ session reuses every already-completed sub-proof from the content-addressed broker. Reconcile runs on
73
+ a `SerialQueue` (from `@aztec/foundation/queue`), so two concurrent triggers can never interleave on an
74
+ `await` and race on the `EpochSession` maps.
69
75
  - **`ProofPublishingService`** — central owner of L1 proof submission. `EpochSession`s hand
70
76
  their top-tree proofs to the service as `PublishCandidate`s; the service serialises
71
77
  one publish at a time against a freshly-created `ProverNodePublisher`, gates eligibility
@@ -81,43 +87,38 @@ A `CheckpointProver` is content-addressed by `(checkpoint.number, slot, archiveR
81
87
  where `archiveRoot` is the checkpoint's own archive root (its post-state). Keying on the
82
88
  post-state makes the identity precise: two checkpoints are "the same" iff they produce
83
89
  the same archive — so a reorg branch, or a replacement built on the same predecessor but
84
- with different content, yields a different archive root and a distinct `CheckpointProver`, while an
85
- identical re-add collapses to the same `CheckpointProver` and reuses its in-flight work.
90
+ with different content, yields a different archive root and a distinct `CheckpointProver`. A prune
91
+ cancels and removes a prover — its sub-tree work forks world-state per block and cannot survive the
92
+ prune — so a re-add, even of identical content, constructs a fresh `CheckpointProver` (block-rollup
93
+ jobs already completed in the proving broker are still reused, as they are content-addressed).
86
94
 
87
95
  ```mermaid
88
96
  stateDiagram-v2
89
97
  [*] --> Created
90
98
  Created --> Proving: gather + execute
91
99
  Proving --> Proven: sub-tree resolves blockProofs
92
- Proving --> Cancelled: cancel()
100
+ Proving --> Cancelled: cancel() (prune / shutdown / epoch-session error)
101
+ Proven --> Cancelled: cancel() (prune / reap)
93
102
  Proven --> Reaped: reapExpired(epoch)
94
103
  Cancelled --> [*]
95
-
96
- state "Pruned (side)" as Pruned
97
- Proving --> Pruned: markPruned()
98
- Pruned --> Proving: markCanonical()
99
- Proven --> Pruned: markPruned()
100
- Pruned --> Reaped: SlotWatcher (slot < syncedSlot)
101
104
  ```
102
105
 
103
- The **`Pruned`** state is a side flag, not a place in the main lifecycle: sub-tree
104
- proving keeps running underneath, so a brief reorg that prunes and immediately
105
- re-adds the same checkpoint avoids any re-proving. The flag only gates *eligibility*
106
- to be included in an `EpochSession` `EpochSession`s ask the store for *canonical* (non-pruned)
107
- checkpoints when assembling their subsets.
108
-
109
- ### Reaping rules
110
-
111
- - **Pruned**: the `SlotWatcher` (a `RunningPromise` polling
112
- `l2BlockSource.getSyncedL2SlotNumber`) reaps a pruned `CheckpointProver` when the chain's
113
- synced slot has moved past the `CheckpointProver`'s slot. Once the chain is past that slot,
114
- a re-add with the same content is impossible.
115
- - **Canonical**: `CheckpointStore.reapExpired(expiredEpoch)` drops any canonical
116
- `CheckpointProver` whose epoch is at or below the supplied expired epoch. Once an epoch's
117
- proof-submission window has closed, its proof can no longer be accepted on L1,
118
- so the `CheckpointProver` is no longer needed.
119
- - **Cancelled**: removed immediately by whichever path called `cancel()` (store
120
- shutdown, prune past-slot, `EpochSession` error).
106
+ A prune **cancels and removes** the affected `CheckpointProver`s from the store: a prover's
107
+ sub-tree work forks world-state per block, and an L1 prune of a base block faults those reads,
108
+ so there is nothing to preserve across a reorg. A re-add even of identical content — therefore
109
+ constructs a fresh `CheckpointProver`. `EpochSession`s ask the store for the checkpoints in a slot
110
+ range; every prover in the store is canonical.
111
+
112
+ ### Removal rules
113
+
114
+ - **Pruned**: `CheckpointStore.cancelAndRemoveAboveBlock(target)` cancels and removes every
115
+ `CheckpointProver` whose last block is above the prune target, the moment a `chain-pruned`
116
+ event arrives.
117
+ - **Expired**: `CheckpointStore.reapExpired(expiredEpoch)` drops any `CheckpointProver` whose
118
+ epoch is at or below the supplied expired epoch. Once an epoch's proof-submission window has
119
+ closed, its proof can no longer be accepted on L1, so the `CheckpointProver` is no longer needed.
120
+ - **Shutdown**: `CheckpointStore.stop()` cancels every remaining prover and awaits its teardown
121
+ (including teardowns still in flight for provers already removed by a prune or reap).
121
122
 
122
123
  ### Eager tx gathering
123
124
 
@@ -155,9 +156,9 @@ stateDiagram-v2
155
156
  awaiting_root --> publishing_proof: epoch proof ready, submit to L1
156
157
  publishing_proof --> completed: publish succeeds
157
158
  publishing_proof --> superseded: longer same-epoch candidate wins
158
- publishing_proof --> failed: L1 submission errored
159
- awaiting_checkpoints --> failed: top-tree prove errored
160
- awaiting_root --> failed: top-tree prove errored
159
+ publishing_proof --> failed: L1 submission errored (provers healthy)
160
+ awaiting_checkpoints --> stopped: a checkpoint prover failed
161
+ awaiting_root --> failed: top-tree prove errored (provers healthy)
161
162
  initialized --> timed_out: deadline
162
163
  awaiting_checkpoints --> timed_out: deadline (EpochSession or candidate)
163
164
  awaiting_root --> timed_out: deadline (EpochSession or candidate)
@@ -167,9 +168,22 @@ stateDiagram-v2
167
168
  superseded --> [*]
168
169
  cancelled --> [*]
169
170
  timed_out --> [*]
171
+ stopped --> [*]
170
172
  failed --> [*]
171
173
  ```
172
174
 
175
+ A fault settles the `EpochSession` in one of two terminal states:
176
+
177
+ - **`stopped`** — a `CheckpointProver` under the session failed (its block proofs rejected). This may
178
+ be a prune-induced fork fault, so it is *not* a verdict on the epoch and is *not* uploaded. The
179
+ reconciler drops the session and, guarded by `isFailed()`, does not rebuild over the failed prover;
180
+ a prune/re-add installs a fresh prover and the epoch is retried.
181
+ - **`failed`** — the session's own work (top-tree prove or L1 submit) failed while *every* checkpoint
182
+ prover succeeded. Because healthy provers rule out a prune, this is a genuine, race-free failure
183
+ (`EpochSession.hasFailed()`). The reconciler retains such a full session (so the tick does not
184
+ re-prove a deterministically-failing epoch) and uploads a post-mortem exactly once. No prune/fault
185
+ classification is needed — the two states carry it.
186
+
173
187
  The non-terminal states track the window between `start()` and the L1 submission:
174
188
 
175
189
  - `awaiting-checkpoints` — a `TopTreeJob` is awaiting each checkpoint's sub-tree result
@@ -288,8 +302,8 @@ sequenceDiagram
288
302
  alt content key new
289
303
  CS->>CP: new CheckpointProver(args)
290
304
  CP->>CP: eager gather + sub-tree start
291
- else content key matches
292
- CS->>CP: markCanonical()
305
+ else content key already live
306
+ CS->>CP: reuse existing prover
293
307
  end
294
308
  PN->>SM: onCheckpointAdded(epoch)
295
309
  SM->>SM: queue reconcile({kind:'checkpoint', epoch})
@@ -306,9 +320,9 @@ sequenceDiagram
306
320
  participant CS as CheckpointStore
307
321
  participant SM as SessionManager
308
322
 
309
- L2->>PN: chain-pruned{checkpoint}
310
- PN->>CS: markPrunedAfter(checkpoint.number)
311
- CS->>CS: flip every CheckpointProver above threshold to pruned (sub-tree keeps running)
323
+ L2->>PN: chain-pruned{block}
324
+ PN->>CS: cancelAndRemoveAboveBlock(prunedToBlock)
325
+ CS->>CS: cancel + remove every CheckpointProver whose last block is above the target
312
326
  PN->>SM: onPrune(affectedEpochs)
313
327
  SM->>SM: queue reconcile({kind:'prune', affectedEpochs})
314
328
  SM->>SM: walk EpochSessions, cancel-and-recreate those with shifted content
@@ -328,73 +342,84 @@ sequenceDiagram
328
342
  PS->>PS: drain reads proven afresh, re-checks eligibility
329
343
  ```
330
344
 
331
- ### Per-event expiry sweep
345
+ ### Periodic expiry sweep
332
346
 
333
347
  ```mermaid
334
348
  sequenceDiagram
335
- participant L2 as L2BlockStream
349
+ participant T as expiryTicker (RunningPromise)
336
350
  participant PN as ProverNode
351
+ participant L2 as L2BlockStream
337
352
  participant CC as ChonkCache
338
353
  participant CS as CheckpointStore
339
354
 
340
- L2->>PN: any event
355
+ T->>PN: checkEpochExpiry() (every poll interval)
341
356
  PN->>L2: getSyncedL2SlotNumber()
342
357
  PN->>PN: latestEpoch = getEpochAtSlot(latestSlot)
343
358
  PN->>PN: newlyExpiredUpTo = latestEpoch - (proofSubmissionEpochs + 1)
344
359
  loop for each newly-expired epoch
345
- PN->>L2: getCheckpointsData({epoch}) + getBlocks(...)
360
+ PN->>L2: getBlocks({epoch, onlyCheckpointed})
346
361
  PN->>CC: releaseForBlocks(blocks)
347
362
  PN->>CS: reapExpired(epoch)
348
363
  end
349
364
  ```
350
365
 
351
- Expiry runs at the end of every `handleBlockStreamEvent` call (not on any specific
352
- event type). An epoch `E` is expired once the chain reaches the start of epoch
353
- `E + proofSubmissionEpochs + 1` — the deadline beyond which an L1 submission for
354
- `E` would be rejected. A monotonic high-water mark (`lastExpiredEpoch`) makes the
355
- sweep cheap: it advances per event and never revisits an epoch. It is seeded at
356
- `start()` from the last fully-proven epoch (computed in `computeStartupState`),
357
- so on a restart we never re-sweep epochs that already reached L1.
366
+ The sweep is driven solely by the `expiryTicker` (a `RunningPromise` armed in `start()`), which
367
+ calls `checkEpochExpiry()` every poll interval whether or not block-stream events arrive. It is
368
+ deliberately **not** run from `handleBlockStreamEvent`expiry is a background sweep keyed off the
369
+ archiver's synced slot, not part of event processing — and because a `RunningPromise` never overlaps
370
+ its own runs, no two sweeps can race. An epoch `E` is expired once the chain reaches the start of epoch
371
+ `E + proofSubmissionEpochs + 1` the deadline beyond which an L1 submission for `E` would be rejected.
372
+ Expiry only releases the chonk cache and reaps `E`'s provers.
373
+ A monotonic high-water mark (`lastExpiredEpoch`) makes the sweep cheap: it only
374
+ advances after a sweep completes and never revisits an epoch. It is seeded at `start()` from the last
375
+ fully-proven epoch (`resolveLastFullyProvenEpoch`), so on a restart we never re-sweep epochs that already
376
+ reached L1.
358
377
 
359
378
  ### Periodic tick
360
379
 
361
380
  `SessionManager.start()` arms a `RunningPromise` that fires
362
381
  `reconcile({ kind: 'tick' })` every `tickIntervalMs`. The tick picks up epochs that
363
382
  became complete by time alone (no fresh checkpoint event) and advances to the
364
- next unproven epoch once the previous one lands on L1. A monotonic high-water
365
- mark (`lastTickEpoch`) prevents the tick from re-opening an epoch whose `EpochSession`
366
- already terminated; the mark advances only after an `EpochSession` actually exists for
367
- the epoch, so transient blockers (max-pending-jobs reached, archiver still
368
- indexing) leave the mark in place and the next tick retries.
383
+ next unproven epoch once the previous one lands on L1. The tick is not gated by any per-epoch
384
+ bookkeeping; two things keep it from re-proving a doomed epoch:
385
+
386
+ - `openFullSessionIfReady` refuses to build a session when any `CheckpointProver` in the set has
387
+ **failed** (`isFailed()`). So a prover fault (possibly a prune) skips the epoch cheaply each tick
388
+ no session, no proving — until a prune/re-add installs a fresh prover.
389
+ - A session that failed on its own account (`hasFailed()` — top-tree/submit failed with every prover
390
+ healthy) is **retained** by `recreateInvalidSessions` rather than deleted, so the tick's
391
+ `fullSessions.has(epoch)` check skips it. It is replaced only when its canonical content changes (a
392
+ re-add), so a deterministically-failing epoch is not re-proved every tick.
393
+
394
+ Transient blockers (max-pending-jobs reached, archiver still indexing) create no session and no failed
395
+ prover, so the next tick simply tries again.
369
396
 
370
397
  ## Walkthroughs
371
398
 
372
399
  ### checkpoint-added → prune → checkpoint-added (reorg resilience)
373
400
 
374
- State: epoch N has checkpoints c1..c4 all canonical (slots s1..s4). `fullSessions[N]`
375
- holds `EpochSession` **A** with spec `{kind:'full', N, fromSlot:s1, toSlot:s4}`, referencing
376
- checkpoints `[c1, c2, c3, c4]`.
401
+ State: epoch N has checkpoints c1..c4 (slots s1..s4). `fullSessions[N]` holds `EpochSession`
402
+ **A** with spec `{kind:'full', N, fromSlot:s1, toSlot:s4}`, referencing `[c1, c2, c3, c4]`.
377
403
 
378
- 1. **chain-pruned arrives, target c3.** Store flips c4 to pruned. Reconcile fires:
379
- for `EpochSession` A, canonical content for `(s1, s4)` is now `[c1, c2, c3]` (c4 pruned).
380
- The frozen set `[c1, c2, c3, c4]` no longer matches `A.cancel('canonical content
381
- changed')`. Epoch N still complete on L1 → reconcile constructs `EpochSession` **B** with
382
- the same spec `{full, N, s1, s4}` but checkpoints `[c1, c2, c3]`.
404
+ 1. **chain-pruned arrives, target c3.** The store cancels and removes c4 (its last block is
405
+ above the prune target). Reconcile fires: `EpochSession` A's frozen set `[c1, c2, c3, c4]`
406
+ includes the now-cancelled c4, so it no longer matches the store's `[c1, c2, c3]` →
407
+ `A.cancel('canonical content changed')`. Epoch N still complete on L1 → reconcile constructs
408
+ `EpochSession` **B** with the same spec `{full, N, s1, s4}` but checkpoints `[c1, c2, c3]`.
383
409
 
384
410
  2. **`EpochSession` B starts top-tree proving over [c1, c2, c3].**
385
411
 
386
- 3. **chain-checkpointed arrives, target c4_re (same content key as old c4).** The
387
- store finds the existing `CheckpointProver` at `(c4.number, s4, c4.archive.root)`
388
- and calls `markCanonical()`. The sub-tree work that never stopped is visible to
389
- `EpochSession`s again. (A re-add with *different* content would have a different archive
390
- root and so get a fresh `CheckpointProver` instead.)
412
+ 3. **chain-checkpointed arrives, target c4_re (same content key as old c4).** The old c4
413
+ prover was removed on the prune, so the store constructs a **fresh** `CheckpointProver` for
414
+ c4_re and starts its sub-tree work from scratch. (Its block-rollup jobs are content-addressed
415
+ in the proving broker, so any the old c4 already completed are reused rather than re-proved.)
391
416
 
392
- 4. **Reconcile fires.** `EpochSession` B's canonical content for `(s1, s4)` is now `[c1, c2,
393
- c3, c4]`, doesn't match its frozen `[c1, c2, c3]` → `B.cancel(...)`. Construct
394
- `EpochSession` **C** with same spec but checkpoints `[c1, c2, c3, c4]`.
417
+ 4. **Reconcile fires.** `EpochSession` B's content for `(s1, s4)` is now `[c1, c2, c3, c4_re]`,
418
+ doesn't match its frozen `[c1, c2, c3]` → `B.cancel(...)`. Construct `EpochSession` **C** with
419
+ the same spec but checkpoints `[c1, c2, c3, c4_re]`.
395
420
 
396
- 5. **`EpochSession` C reuses the long-lived c1..c4 `CheckpointProver` instances.** Sub-tree
397
- work may already be complete; only the top-tree is recomputed. The chonk cache
421
+ 5. **`EpochSession` C reuses the long-lived c1..c3 `CheckpointProver` instances and the fresh
422
+ c4_re.** Only c4_re's witnesses are regenerated; the top-tree is recomputed. The chonk cache
398
423
  survived the reorg because no epoch in this range has expired yet.
399
424
 
400
425
 
@@ -466,23 +491,37 @@ Keying by tx hash makes the cache survive any reorg up to finality; releasing on
466
491
  finality means we don't grow the cache indefinitely while still keeping every
467
492
  reorg-relevant proof.
468
493
 
469
- ### Why does the slot watcher only reap pruned `CheckpointProver`s?
494
+ ### Why is a pruned `CheckpointProver` removed rather than kept for a possible re-add?
495
+
496
+ A prover's sub-tree work forks world-state per block, and an L1 prune of a base block faults
497
+ those in-flight reads — so the work cannot survive the prune, and there is nothing to preserve
498
+ by keeping the prover around. Removing it on prune and rebuilding on re-add is therefore both
499
+ correct and simpler; the expensive block-rollup proofs are still reused when content re-appears,
500
+ because the proving broker is content-addressed independently of the prover's lifetime.
501
+
502
+ ### How is a genuine failure told apart from a prune, without a racy classification?
470
503
 
471
- Canonical `CheckpointProver`s can't be reaped on a slot heuristic they're still part of the
472
- proven-chain story. Pruned `CheckpointProver`s, on the other hand, are only kept around in
473
- case the chain re-adds the same content; once the synced slot has moved past, that
474
- re-add is impossible, and the `CheckpointProver` can go. Finality is the right signal for
475
- canonical reaping, because finality is the only state that rules out future reorgs.
504
+ Knowledge of an L1 reorg reaches the prover-node on two causally unordered channels: the control
505
+ plane (`chain-pruned` `onPrune`) and the data plane (world-state unwinds, and an in-flight fork
506
+ read faults inside a `CheckpointProver` mid-proof). If the `EpochSession` reacted to a fault by asking
507
+ "was this a prune or a genuine failure?", every answer would sample control-plane state that lags the
508
+ data-plane unwind racy by construction. We remove the question by pushing the fact down to where it
509
+ is unambiguous: a `CheckpointProver` records `isFailed()` when *its* block proofs reject. The session
510
+ then reads a clean signal: if any prover failed, the fault might be a prune (`stopped`) — don't upload,
511
+ don't rebuild over it, recover on re-add; if *no* prover failed yet the session's own top-tree/submit
512
+ work failed, that is definitively **not** a prune (`failed`) — a genuine, race-free failure that is
513
+ retained (so a deterministic failure isn't re-proved every tick) and uploaded once. No control-plane
514
+ sampling, no timing race — the two prover-level and session-level facts carry the whole decision.
476
515
 
477
516
  ## Configuration
478
517
 
479
518
  | Env var | Description |
480
519
  |---|---|
481
- | `PROVER_NODE_POLLING_INTERVAL_MS` | Polling interval for the L2BlockStream, the checkpoint-store slot watcher, and the SessionManager periodic tick. Default 1000 ms. |
520
+ | `PROVER_NODE_POLLING_INTERVAL_MS` | Polling interval for the L2BlockStream and the SessionManager periodic tick. Default 1000 ms. |
482
521
  | `PROVER_NODE_MAX_PENDING_JOBS` | Cap on the number of non-terminal `EpochSession`s (full + partial). When at limit, reconcile defers opening new full `EpochSession`s; explicit `startProof` calls throw. |
483
522
  | `PROVER_NODE_EPOCH_PROVING_DELAY_MS` | Optional sleep at the start of each `EpochSession`, before the TopTreeJob is constructed. Used in tests to give late events time to land. |
484
523
  | `TX_GATHERING_TIMEOUT_MS` | Per-block tx gather deadline used by each `CheckpointProver`. |
485
- | `PROVER_NODE_FAILED_EPOCH_STORE` | If set, failed `EpochSession`s upload their proving data (every `CheckpointProver`'s txs + register-time data, regardless of sub-tree completion) to this file store. |
524
+ | `PROVER_NODE_FAILED_EPOCH_STORE` | If set, a full session that fails on its own account (top-tree/submit, provers healthy) uploads its proving data (every `CheckpointProver`'s txs + register-time data, regardless of sub-tree completion) to this file store. |
486
525
  | `PROVER_NODE_DISABLE_PROOF_PUBLISH` | If true, the publishing service runs `analyzeEpochProofSubmission` (estimates L1 fees) instead of actually submitting. |
487
526
 
488
527
  ## Failure handling and observability
@@ -500,14 +539,29 @@ Loggers:
500
539
  - `prover-node:checkpoint-prover` — sub-tree pipeline (gather, block processing).
501
540
  - `prover-client:chonk-cache` — chonk-verifier cache enqueue / release events.
502
541
 
503
- On `failed` exit, `SessionManager.runSession` invokes the `onSessionFailed` callback
504
- the manager was constructed with. `ProverNode` wires this to `tryUploadSessionFailure`,
505
- which calls `SessionManager.buildSessionProvingData(session)` to walk every `CheckpointProver`
506
- referenced by the `EpochSession` and assemble an `EpochProvingJobData` snapshot including
507
- every `CheckpointProver`'s txs and register-time data even if its sub-tree never reached
508
- `isCompleted()`. This snapshot is what `uploadEpochProofFailure` ships to the
509
- configured file store along with a world-state + archiver backup, so the failure
510
- can be reproduced offline via `rerunEpochProvingJob`.
542
+ Failed proving data is uploaded to the configured file store (`PROVER_NODE_FAILED_EPOCH_STORE`) at two
543
+ levels, and re-proved offline by the matching downloader/rerunner: an epoch job via
544
+ `downloadEpochProvingJob` + `rerunEpochProvingJob`, and a single checkpoint via the same download plus
545
+ `rerunCheckpointProvingJob` (which rebuilds just that checkpoint's sub-tree prover and awaits its block
546
+ proofs no epoch top-tree or L1 submit). Both upload levels build an `EpochProvingJobData` snapshot with
547
+ `SessionManager.buildProvingData(...)` every `CheckpointProver`'s txs and register-time data, even if
548
+ its sub-tree never reached `isCompleted()` and ship it via `uploadEpochProofFailure` alongside a
549
+ world-state + archiver backup:
550
+
551
+ - **Per checkpoint.** When a `CheckpointProver`'s block proofs reject for a non-cancel reason (a sub-tree
552
+ fault or a prune-induced fork fault), it fires its `onFailed` callback and `ProverNode` uploads that one
553
+ checkpoint via `tryUploadCheckpointFailure(prover)`. This fires for prune-induced faults too — that is
554
+ intentional; a prune-caused checkpoint snapshot is harmless, and *not* trying to tell prune from genuine
555
+ failure is exactly what keeps this race-free. A cancelled prover (control-plane prune / shutdown) is not
556
+ a failure and does not upload.
557
+ - **Per session.** When a full session ends in its own genuine failure (`EpochSession.hasFailed()` —
558
+ top-tree/submit failed with every prover healthy), the session manager fires `onSessionFailed` and
559
+ `ProverNode` uploads all the session's checkpoints via `tryUploadEpochFailure(...)`. This is race-free
560
+ (healthy provers rule out a prune) and fires once, since the failed session is retained and never re-run.
561
+
562
+ A `stopped` session (a prover under it failed) does not upload at the session level — its failed checkpoint
563
+ already uploaded itself. Each backup is a full world-state + archiver snapshot, so a prune that faults
564
+ several in-flight provers produces several uploads; this is accepted for the diagnostic value.
511
565
 
512
566
  Metrics emitted by `EpochSession`s:
513
567
 
@@ -4,10 +4,19 @@ import { type ProverClientConfig } from '@aztec/prover-client';
4
4
  import { ProverBrokerConfig } from '@aztec/prover-client/broker';
5
5
  import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
6
6
  import type { GenesisData } from '@aztec/stdlib/world-state';
7
+ type RerunConfig = DataStoreConfig & ProverBrokerConfig & ProverClientConfig & Pick<L1ContractsConfig, 'aztecEpochDuration'>;
7
8
  /**
8
9
  * Given a local folder where `downloadEpochProvingJob` was called, creates a new archiver and world state
9
10
  * using the state snapshots, and creates a new epoch proving session to prove the downloaded proving job.
10
11
  * Proving is done with a local proving broker and agents as specified by the config.
11
12
  */
12
- export declare function rerunEpochProvingJob(localPath: string, log: Logger, config: DataStoreConfig & ProverBrokerConfig & ProverClientConfig & Pick<L1ContractsConfig, 'aztecEpochDuration'>, genesis?: GenesisData): Promise<"awaiting-checkpoints" | "awaiting-predecessor" | "awaiting-root" | "cancelled" | "completed" | "failed" | "initialized" | "publishing-proof" | "stopped" | "superseded" | "timed-out">;
13
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVydW4tZXBvY2gtcHJvdmluZy1qb2IuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9hY3Rpb25zL3JlcnVuLWVwb2NoLXByb3Zpbmctam9iLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLE9BQU8sS0FBSyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFFaEUsT0FBTyxLQUFLLEVBQUUsTUFBTSxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFFcEQsT0FBTyxFQUFFLEtBQUssa0JBQWtCLEVBQXNCLE1BQU0sc0JBQXNCLENBQUM7QUFDbkYsT0FBTyxFQUFFLGtCQUFrQixFQUErQixNQUFNLDZCQUE2QixDQUFDO0FBTzlGLE9BQU8sS0FBSyxFQUFFLGVBQWUsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBRzlELE9BQU8sS0FBSyxFQUFFLFdBQVcsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBVzdEOzs7O0dBSUc7QUFDSCx3QkFBc0Isb0JBQW9CLENBQ3hDLFNBQVMsRUFBRSxNQUFNLEVBQ2pCLEdBQUcsRUFBRSxNQUFNLEVBQ1gsTUFBTSxFQUFFLGVBQWUsR0FBRyxrQkFBa0IsR0FBRyxrQkFBa0IsR0FBRyxJQUFJLENBQUMsaUJBQWlCLEVBQUUsb0JBQW9CLENBQUMsRUFDakgsT0FBTyxDQUFDLEVBQUUsV0FBVyxtTUEyRnRCIn0=
13
+ export declare function rerunEpochProvingJob(localPath: string, log: Logger, config: RerunConfig, genesis?: GenesisData): Promise<"awaiting-checkpoints" | "awaiting-predecessor" | "awaiting-root" | "cancelled" | "completed" | "failed" | "initialized" | "publishing-proof" | "stopped" | "superseded" | "timed-out">;
14
+ /**
15
+ * Re-proves a single downloaded checkpoint proving job (as uploaded by a `CheckpointProver` failure).
16
+ * Reconstructs just that checkpoint's sub-tree prover from the snapshot and awaits its block proofs — no
17
+ * epoch top-tree, no L1 submission — so a checkpoint-level failure can be reproduced offline in isolation.
18
+ * Returns the block-rollup proof outputs on success; throws if the checkpoint fails to prove again.
19
+ */
20
+ export declare function rerunCheckpointProvingJob(localPath: string, log: Logger, config: RerunConfig, genesis?: GenesisData): Promise<import("@aztec/stdlib/interfaces/server").PublicInputsAndRecursiveProof<import("@aztec/stdlib/rollup").BlockRollupPublicInputs, 480>[]>;
21
+ export {};
22
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVydW4tZXBvY2gtcHJvdmluZy1qb2IuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9hY3Rpb25zL3JlcnVuLWVwb2NoLXByb3Zpbmctam9iLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLE9BQU8sS0FBSyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFFaEUsT0FBTyxLQUFLLEVBQUUsTUFBTSxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFFcEQsT0FBTyxFQUFFLEtBQUssa0JBQWtCLEVBQXNCLE1BQU0sc0JBQXNCLENBQUM7QUFDbkYsT0FBTyxFQUFFLGtCQUFrQixFQUErQixNQUFNLDZCQUE2QixDQUFDO0FBTzlGLE9BQU8sS0FBSyxFQUFFLGVBQWUsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBRzlELE9BQU8sS0FBSyxFQUFFLFdBQVcsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBVzdELEtBQUssV0FBVyxHQUFHLGVBQWUsR0FDaEMsa0JBQWtCLEdBQ2xCLGtCQUFrQixHQUNsQixJQUFJLENBQUMsaUJBQWlCLEVBQUUsb0JBQW9CLENBQUMsQ0FBQztBQUVoRDs7OztHQUlHO0FBQ0gsd0JBQXNCLG9CQUFvQixDQUFDLFNBQVMsRUFBRSxNQUFNLEVBQUUsR0FBRyxFQUFFLE1BQU0sRUFBRSxNQUFNLEVBQUUsV0FBVyxFQUFFLE9BQU8sQ0FBQyxFQUFFLFdBQVcsbU1Bc0NwSDtBQUVEOzs7OztHQUtHO0FBQ0gsd0JBQXNCLHlCQUF5QixDQUM3QyxTQUFTLEVBQUUsTUFBTSxFQUNqQixHQUFHLEVBQUUsTUFBTSxFQUNYLE1BQU0sRUFBRSxXQUFXLEVBQ25CLE9BQU8sQ0FBQyxFQUFFLFdBQVcsbUpBaUJ0QiJ9
@@ -1 +1 @@
1
- {"version":3,"file":"rerun-epoch-proving-job.d.ts","sourceRoot":"","sources":["../../src/actions/rerun-epoch-proving-job.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAEpD,OAAO,EAAE,KAAK,kBAAkB,EAAsB,MAAM,sBAAsB,CAAC;AACnF,OAAO,EAAE,kBAAkB,EAA+B,MAAM,6BAA6B,CAAC;AAO9F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAG9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAW7D;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,eAAe,GAAG,kBAAkB,GAAG,kBAAkB,GAAG,IAAI,CAAC,iBAAiB,EAAE,oBAAoB,CAAC,EACjH,OAAO,CAAC,EAAE,WAAW,mMA2FtB"}
1
+ {"version":3,"file":"rerun-epoch-proving-job.d.ts","sourceRoot":"","sources":["../../src/actions/rerun-epoch-proving-job.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAEpD,OAAO,EAAE,KAAK,kBAAkB,EAAsB,MAAM,sBAAsB,CAAC;AACnF,OAAO,EAAE,kBAAkB,EAA+B,MAAM,6BAA6B,CAAC;AAO9F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAG9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAW7D,KAAK,WAAW,GAAG,eAAe,GAChC,kBAAkB,GAClB,kBAAkB,GAClB,IAAI,CAAC,iBAAiB,EAAE,oBAAoB,CAAC,CAAC;AAEhD;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,WAAW,mMAsCpH;AAED;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAC7C,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE,WAAW,mJAiBtB"}
@@ -91,56 +91,19 @@ import { ProverNodeJobMetrics } from '../metrics.js';
91
91
  hasError: false
92
92
  };
93
93
  try {
94
- const jobData = deserializeEpochProvingJobData(readFileSync(localPath));
95
- log.info(`Loaded proving job data for epoch ${jobData.epochNumber}`);
96
- const telemetry = getTelemetryClient();
97
- const metrics = new ProverNodeJobMetrics(telemetry.getMeter('prover-job'), telemetry.getTracer('prover-job'));
98
- const worldState = _ts_add_disposable_resource(env, await createWorldState(config, genesis), true);
99
- const initialBlockHash = await worldState.getInitialHeader().hash();
100
- const archiver = await createArchiverStore(config, initialBlockHash);
101
- const avmSimulator = _ts_add_disposable_resource(env, await AvmSimulatorPool.spawn({
102
- wsdbIpcPath: worldState.getIpcPath()
103
- }), true);
104
- const publicProcessorFactory = new PublicProcessorFactory(createContractDataSource(archiver), avmSimulator, undefined, undefined, log.getBindings());
94
+ const ctx = _ts_add_disposable_resource(env, await createRerunContext(localPath, log, config, genesis), true);
95
+ const { jobData, prover, metrics } = ctx;
96
+ log.info(`Rerunning epoch proving for epoch ${jobData.epochNumber}`);
97
+ const provers = [];
98
+ for(let i = 0; i < jobData.checkpoints.length; i++){
99
+ provers.push(await buildCheckpointProver(ctx, i, log));
100
+ }
105
101
  // Local rerun never publishes — stub the service so submit() always resolves 'published'
106
102
  // and withdraw is a no-op.
107
103
  const publishingService = {
108
104
  submit: ()=>Promise.resolve('published'),
109
105
  withdraw: ()=>{}
110
106
  };
111
- const broker = await createAndStartProvingBroker(config, telemetry);
112
- const prover = await createProverClient(config, worldState, broker, telemetry);
113
- const chonkCache = new ChonkCache(log.getBindings());
114
- const txProvider = makeReplayingTxProvider(jobData.txs);
115
- log.info(`Rerunning epoch proving for epoch ${jobData.epochNumber}`);
116
- const provers = [];
117
- for(let i = 0; i < jobData.checkpoints.length; i++){
118
- const checkpoint = jobData.checkpoints[i];
119
- const previousBlockHeader = i === 0 ? jobData.previousBlockHeader : jobData.checkpoints[i - 1].blocks.at(-1).header;
120
- const l1ToL2Messages = jobData.l1ToL2Messages[checkpoint.number] ?? [];
121
- const previousArchiveSiblingPath = await getLastSiblingPath(MerkleTreeId.ARCHIVE, worldState.getSnapshot(BlockNumber(checkpoint.blocks[0].number - 1)));
122
- const attestations = checkpoint.number === jobData.checkpoints.at(-1).number ? jobData.attestations : [];
123
- provers.push(new CheckpointProver({
124
- checkpoint,
125
- epochNumber: jobData.epochNumber,
126
- attestations,
127
- previousBlockHeader,
128
- l1ToL2Messages,
129
- previousArchiveSiblingPath
130
- }, {
131
- proverFactory: prover,
132
- chonkCache,
133
- publicProcessorFactory,
134
- dbProvider: worldState,
135
- txProvider,
136
- dateProvider: new DateProvider(),
137
- proverId: prover.getProverId(),
138
- metrics,
139
- txGatheringTimeoutMs: 120_000,
140
- deadline: undefined,
141
- log
142
- }));
143
- }
144
107
  const l1Constants = {
145
108
  epochDuration: config.aztecEpochDuration
146
109
  };
@@ -174,6 +137,102 @@ import { ProverNodeJobMetrics } from '../metrics.js';
174
137
  if (result) await result;
175
138
  }
176
139
  }
140
+ /**
141
+ * Re-proves a single downloaded checkpoint proving job (as uploaded by a `CheckpointProver` failure).
142
+ * Reconstructs just that checkpoint's sub-tree prover from the snapshot and awaits its block proofs — no
143
+ * epoch top-tree, no L1 submission — so a checkpoint-level failure can be reproduced offline in isolation.
144
+ * Returns the block-rollup proof outputs on success; throws if the checkpoint fails to prove again.
145
+ */ export async function rerunCheckpointProvingJob(localPath, log, config, genesis) {
146
+ const env = {
147
+ stack: [],
148
+ error: void 0,
149
+ hasError: false
150
+ };
151
+ try {
152
+ const ctx = _ts_add_disposable_resource(env, await createRerunContext(localPath, log, config, genesis), true);
153
+ const { jobData } = ctx;
154
+ const checkpointNumber = jobData.checkpoints[0].number;
155
+ log.info(`Rerunning checkpoint proving for checkpoint ${checkpointNumber} (epoch ${jobData.epochNumber})`);
156
+ const prover = await buildCheckpointProver(ctx, 0, log);
157
+ try {
158
+ const blockProofs = await prover.whenBlockProofsReady();
159
+ log.info(`Completed proving for checkpoint ${checkpointNumber} with ${blockProofs.length} block proof(s)`);
160
+ return blockProofs;
161
+ } finally{
162
+ prover.cancel({
163
+ routine: true
164
+ });
165
+ await prover.whenDone();
166
+ }
167
+ } catch (e) {
168
+ env.error = e;
169
+ env.hasError = true;
170
+ } finally{
171
+ const result = _ts_dispose_resources(env);
172
+ if (result) await result;
173
+ }
174
+ }
175
+ /**
176
+ * Rebuilds the offline proving environment from a downloaded job: world state + archiver from the
177
+ * snapshots, a local proving broker + client, a chonk cache, and a tx provider that replays the job's txs.
178
+ */ async function createRerunContext(localPath, log, config, genesis) {
179
+ const jobData = deserializeEpochProvingJobData(readFileSync(localPath));
180
+ log.info(`Loaded proving job data for epoch ${jobData.epochNumber}`);
181
+ const telemetry = getTelemetryClient();
182
+ const metrics = new ProverNodeJobMetrics(telemetry.getMeter('prover-job'), telemetry.getTracer('prover-job'));
183
+ const worldState = await createWorldState(config, genesis);
184
+ const initialBlockHash = await worldState.getInitialHeader().hash();
185
+ const archiver = await createArchiverStore(config, initialBlockHash);
186
+ const avmSimulator = await AvmSimulatorPool.spawn({
187
+ wsdbIpcPath: worldState.getIpcPath()
188
+ });
189
+ const publicProcessorFactory = new PublicProcessorFactory(createContractDataSource(archiver), avmSimulator, undefined, undefined, log.getBindings());
190
+ const broker = await createAndStartProvingBroker(config, telemetry);
191
+ const prover = await createProverClient(config, worldState, broker, telemetry);
192
+ const chonkCache = new ChonkCache(log.getBindings());
193
+ const txProvider = makeReplayingTxProvider(jobData.txs);
194
+ return {
195
+ jobData,
196
+ metrics,
197
+ worldState,
198
+ publicProcessorFactory,
199
+ prover,
200
+ chonkCache,
201
+ txProvider,
202
+ async [Symbol.asyncDispose] () {
203
+ await avmSimulator[Symbol.asyncDispose]();
204
+ await worldState[Symbol.asyncDispose]();
205
+ }
206
+ };
207
+ }
208
+ /** Reconstructs the `CheckpointProver` for the checkpoint at `index` in the job, ready to prove. */ async function buildCheckpointProver(ctx, index, log) {
209
+ const { jobData, worldState, prover, chonkCache, publicProcessorFactory, txProvider, metrics } = ctx;
210
+ const checkpoint = jobData.checkpoints[index];
211
+ const previousBlockHeader = index === 0 ? jobData.previousBlockHeader : jobData.checkpoints[index - 1].blocks.at(-1).header;
212
+ const l1ToL2Messages = jobData.l1ToL2Messages[checkpoint.number] ?? [];
213
+ const previousArchiveSiblingPath = await getLastSiblingPath(MerkleTreeId.ARCHIVE, worldState.getSnapshot(BlockNumber(checkpoint.blocks[0].number - 1)));
214
+ const attestations = checkpoint.number === jobData.checkpoints.at(-1).number ? jobData.attestations : [];
215
+ return new CheckpointProver({
216
+ checkpoint,
217
+ epochNumber: jobData.epochNumber,
218
+ attestations,
219
+ previousBlockHeader,
220
+ l1ToL2Messages,
221
+ previousArchiveSiblingPath
222
+ }, {
223
+ proverFactory: prover,
224
+ chonkCache,
225
+ publicProcessorFactory,
226
+ dbProvider: worldState,
227
+ txProvider,
228
+ dateProvider: new DateProvider(),
229
+ proverId: prover.getProverId(),
230
+ metrics,
231
+ txGatheringTimeoutMs: 120_000,
232
+ deadline: undefined,
233
+ log
234
+ });
235
+ }
177
236
  /** Build a synthetic ITxProvider that returns the supplied txs map by lookup. */ function makeReplayingTxProvider(txs) {
178
237
  const lookup = (hashes)=>{
179
238
  const found = [];