@aztec/prover-node 5.0.0-private.20260319 → 5.0.0-rc.2

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 (71) hide show
  1. package/README.md +518 -0
  2. package/dest/actions/download-epoch-proving-job.js +1 -1
  3. package/dest/actions/rerun-epoch-proving-job.d.ts +4 -3
  4. package/dest/actions/rerun-epoch-proving-job.d.ts.map +1 -1
  5. package/dest/actions/rerun-epoch-proving-job.js +103 -21
  6. package/dest/bin/run-failed-epoch.js +1 -3
  7. package/dest/checkpoint-store.d.ts +87 -0
  8. package/dest/checkpoint-store.d.ts.map +1 -0
  9. package/dest/checkpoint-store.js +186 -0
  10. package/dest/config.d.ts +1 -1
  11. package/dest/config.d.ts.map +1 -1
  12. package/dest/config.js +1 -1
  13. package/dest/factory.d.ts +1 -1
  14. package/dest/factory.d.ts.map +1 -1
  15. package/dest/factory.js +22 -8
  16. package/dest/index.d.ts +2 -1
  17. package/dest/index.d.ts.map +1 -1
  18. package/dest/index.js +1 -0
  19. package/dest/job/checkpoint-prover.d.ts +134 -0
  20. package/dest/job/checkpoint-prover.d.ts.map +1 -0
  21. package/dest/job/checkpoint-prover.js +353 -0
  22. package/dest/job/epoch-session.d.ts +146 -0
  23. package/dest/job/epoch-session.d.ts.map +1 -0
  24. package/dest/job/epoch-session.js +720 -0
  25. package/dest/job/top-tree-job.d.ts +82 -0
  26. package/dest/job/top-tree-job.d.ts.map +1 -0
  27. package/dest/job/top-tree-job.js +152 -0
  28. package/dest/metrics.d.ts +35 -8
  29. package/dest/metrics.d.ts.map +1 -1
  30. package/dest/metrics.js +86 -14
  31. package/dest/monitors/epoch-monitor.js +6 -2
  32. package/dest/proof-publishing-service.d.ts +161 -0
  33. package/dest/proof-publishing-service.d.ts.map +1 -0
  34. package/dest/proof-publishing-service.js +335 -0
  35. package/dest/prover-node-publisher.d.ts +22 -15
  36. package/dest/prover-node-publisher.d.ts.map +1 -1
  37. package/dest/prover-node-publisher.js +197 -60
  38. package/dest/prover-node.d.ts +105 -67
  39. package/dest/prover-node.d.ts.map +1 -1
  40. package/dest/prover-node.js +482 -224
  41. package/dest/prover-publisher-factory.d.ts +2 -2
  42. package/dest/prover-publisher-factory.d.ts.map +1 -1
  43. package/dest/prover-publisher-factory.js +3 -3
  44. package/dest/session-manager.d.ts +158 -0
  45. package/dest/session-manager.d.ts.map +1 -0
  46. package/dest/session-manager.js +452 -0
  47. package/dest/test/index.d.ts +7 -6
  48. package/dest/test/index.d.ts.map +1 -1
  49. package/package.json +23 -23
  50. package/src/actions/download-epoch-proving-job.ts +1 -1
  51. package/src/actions/rerun-epoch-proving-job.ts +114 -28
  52. package/src/bin/run-failed-epoch.ts +1 -2
  53. package/src/checkpoint-store.ts +218 -0
  54. package/src/config.ts +2 -1
  55. package/src/factory.ts +18 -10
  56. package/src/index.ts +1 -0
  57. package/src/job/checkpoint-prover.ts +468 -0
  58. package/src/job/epoch-session.ts +436 -0
  59. package/src/job/top-tree-job.ts +227 -0
  60. package/src/metrics.ts +102 -23
  61. package/src/monitors/epoch-monitor.ts +2 -2
  62. package/src/proof-publishing-service.ts +427 -0
  63. package/src/prover-node-publisher.ts +231 -77
  64. package/src/prover-node.ts +552 -251
  65. package/src/prover-publisher-factory.ts +3 -3
  66. package/src/session-manager.ts +552 -0
  67. package/src/test/index.ts +6 -6
  68. package/dest/job/epoch-proving-job.d.ts +0 -63
  69. package/dest/job/epoch-proving-job.d.ts.map +0 -1
  70. package/dest/job/epoch-proving-job.js +0 -762
  71. package/src/job/epoch-proving-job.ts +0 -465
package/README.md CHANGED
@@ -1 +1,519 @@
1
1
  # Prover Node
2
+
3
+ The prover node turns sequenced checkpoints into epoch proofs that get submitted to the L1
4
+ rollup contract. It runs alongside an Aztec validator/full-node and consumes the
5
+ canonical chain view those nodes emit, proving epochs **optimistically** — sub-tree
6
+ work begins the moment a checkpoint lands on L1, not when the epoch closes.
7
+
8
+ This document describes the internal architecture: the state held by the prover-node,
9
+ the events that drive it, and the data flow from a fresh `chain-checkpointed` event
10
+ through to a `submitEpochRootProof` on L1.
11
+
12
+ ## Contents
13
+
14
+ 1. [Architecture](#architecture)
15
+ 2. [CheckpointProver lifecycle](#checkpointprover-lifecycle)
16
+ 3. [EpochSession lifecycle](#epochsession-lifecycle)
17
+ 4. [Event flow](#event-flow)
18
+ 5. [Walkthroughs](#walkthroughs)
19
+ 6. [Design rationale](#design-rationale)
20
+ 7. [Configuration](#configuration)
21
+ 8. [Failure handling and observability](#failure-handling-and-observability)
22
+
23
+ ## Architecture
24
+
25
+ ```mermaid
26
+ flowchart TB
27
+ L2BlockStream -->|chain-checkpointed| ProverNode
28
+ L2BlockStream -->|chain-pruned| ProverNode
29
+ L2BlockStream -->|chain-proven| ProverNode
30
+ L2BlockStream -->|any event| ProverNode
31
+ ProverNode --> CheckpointStore
32
+ ProverNode --> ChonkCache
33
+ ProverNode --> SessionManager
34
+ ProverNode --> ProofPublishingService
35
+ SessionManager --> EpochTicker[(periodic tick)]
36
+ SessionManager --> FullSessions[(fullSessions)]
37
+ SessionManager --> PartialSessions[(partialSessions)]
38
+ CheckpointStore --> SlotWatcher
39
+ FullSessions -.referenced checkpoints.-> CheckpointStore
40
+ PartialSessions -.referenced checkpoints.-> CheckpointStore
41
+ FullSessions --> TopTreeJob
42
+ PartialSessions --> TopTreeJob
43
+ TopTreeJob -->|PublishCandidate| ProofPublishingService
44
+ ProofPublishingService -->|fresh per publish| ProverNodePublisher
45
+ ProverNodePublisher --> L1[L1 Rollup]
46
+ ```
47
+
48
+ The prover-node splits responsibility between four classes:
49
+
50
+ - **`ProverNode`** — owns the long-lived collections, wires the L2BlockStream, and
51
+ 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`.
56
+ - **`CheckpointStore`** — a registry of `CheckpointProver` instances keyed by
57
+ `(checkpointNumber, slot, archiveRoot)`. Each `CheckpointProver` runs its own sub-tree pipeline
58
+ (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.
61
+ - **`SessionManager`** — owns every live `EpochSession`, the serial reconcile queue,
62
+ the periodic tick, and all `EpochSession` lifecycle decisions. `ProverNode` calls into it
63
+ via `onCheckpointAdded`, `onPrune`, and `startProof`. Every trigger it receives is
64
+ translated into a `reconcile(trigger)` call, a single idempotent function that walks
65
+ all `EpochSession`s, cancels any whose canonical content has shifted, re-creates them with
66
+ 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
+ - **`ProofPublishingService`** — central owner of L1 proof submission. `EpochSession`s hand
70
+ their top-tree proofs to the service as `PublishCandidate`s; the service serialises
71
+ one publish at a time against a freshly-created `ProverNodePublisher`, gates eligibility
72
+ on the proven block tip, picks the longest candidate per epoch as the winner
73
+ (others resolve `'superseded'`), and enforces a per-candidate `deadline`. It runs its
74
+ own `drain()` on a separate `SerialQueue`: submits, withdrawals, chain-proven advances,
75
+ and per-candidate deadline expiries all enqueue a drain pass, so the eligibility
76
+ re-check and the L1 publish never interleave with each other.
77
+
78
+ ## CheckpointProver lifecycle
79
+
80
+ A `CheckpointProver` is content-addressed by `(checkpoint.number, slot, archiveRoot)`,
81
+ where `archiveRoot` is the checkpoint's own archive root (its post-state). Keying on the
82
+ post-state makes the identity precise: two checkpoints are "the same" iff they produce
83
+ 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.
86
+
87
+ ```mermaid
88
+ stateDiagram-v2
89
+ [*] --> Created
90
+ Created --> Proving: gather + execute
91
+ Proving --> Proven: sub-tree resolves blockProofs
92
+ Proving --> Cancelled: cancel()
93
+ Proven --> Reaped: reapExpired(epoch)
94
+ 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
+ ```
102
+
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).
121
+
122
+ ### Eager tx gathering
123
+
124
+ A `CheckpointProver` starts its tx gather + sub-tree pipeline **in its constructor**.
125
+ The tx provider is injected as a dependency,
126
+ and the `CheckpointProver` pulls its own txs via `txProvider.getTxsForBlock(block)` for each
127
+ block in its checkpoint.
128
+
129
+ This means the moment a checkpoint lands on L1, sub-tree proving is already in flight.
130
+ By the time the epoch closes (and the `EpochSession` is constructed), most or all of the
131
+ block-rollup proofs are already done — the `EpochSession` only has to drive the top tree.
132
+
133
+ ## EpochSession lifecycle
134
+
135
+ An `EpochSession` is identified by a slot-based **spec**:
136
+
137
+ ```ts
138
+ interface SessionSpec {
139
+ kind: 'full' | 'partial';
140
+ epochNumber: EpochNumber;
141
+ fromSlot: SlotNumber;
142
+ toSlot: SlotNumber;
143
+ }
144
+ ```
145
+
146
+ The spec declares *what to prove* (a slot range). The concrete checkpoint set the
147
+ `EpochSession` holds is the *implementation* of the spec — frozen at construction time,
148
+ derived from the canonical content for that slot range.
149
+
150
+ ```mermaid
151
+ stateDiagram-v2
152
+ [*] --> initialized
153
+ initialized --> awaiting_checkpoints: start()
154
+ awaiting_checkpoints --> awaiting_root: sub-tree proofs ready, top-tree prove begins
155
+ awaiting_root --> publishing_proof: epoch proof ready, submit to L1
156
+ publishing_proof --> completed: publish succeeds
157
+ 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
161
+ initialized --> timed_out: deadline
162
+ awaiting_checkpoints --> timed_out: deadline (EpochSession or candidate)
163
+ awaiting_root --> timed_out: deadline (EpochSession or candidate)
164
+ awaiting_checkpoints --> cancelled: cancel()
165
+ awaiting_root --> cancelled: cancel()
166
+ completed --> [*]
167
+ superseded --> [*]
168
+ cancelled --> [*]
169
+ timed_out --> [*]
170
+ failed --> [*]
171
+ ```
172
+
173
+ The non-terminal states track the window between `start()` and the L1 submission:
174
+
175
+ - `awaiting-checkpoints` — a `TopTreeJob` is awaiting each checkpoint's sub-tree result
176
+ (`CheckpointProver.whenBlockProofsReady`) before the top-tree prove can begin.
177
+ - `awaiting-root` — the sub-tree proofs are ready and the top-tree (root) prove is running,
178
+ assembling the epoch proof (set via the `TopTreeJob` `beforeProve` hook).
179
+ - `publishing-proof` — the epoch proof is being submitted to L1 via `ProofPublishingService`.
180
+
181
+ `cancel()` and the deadline can fire during any of these pre-submit phases; a terminal state
182
+ set that way wins over the phase transitions (which are guarded by `isTerminal()`).
183
+
184
+ The `EpochSession` does three sequential things: (1) run a `TopTreeJob` over the frozen
185
+ checkpoint subset, (2) hand the resulting proof to `ProofPublishingService` as a
186
+ `PublishCandidate`, (3) translate the service's outcome into a terminal state.
187
+ Predecessor gating, same-epoch dedup, deadline enforcement, and the L1 tx are all
188
+ the `ProofPublishingService`'s concern; the `EpochSession` is just the producer of one
189
+ candidate and the observer of its outcome.
190
+
191
+ Outcome → state mapping:
192
+
193
+ | `PublishOutcome` | `EpochSession` state |
194
+ |---|---|
195
+ | `published` | `completed` |
196
+ | `superseded` | `superseded` |
197
+ | `failed` | `failed` |
198
+ | `expired` | `timed-out` |
199
+ | `withdrawn` | `cancelled` |
200
+
201
+ There is a single deadline — the proof submission window — that applies across both
202
+ proving and publishing. Before submission, the `EpochSession` arms its own timer against
203
+ it: if proving doesn't finish in time, the `EpochSession` enters `timed-out` via
204
+ `cancel('deadline')`. After submission, the publishing service enforces the same deadline
205
+ on the candidate. It's the same instant throughout; only which component enforces it
206
+ changes once the candidate has been handed off.
207
+
208
+ ### Full vs partial
209
+
210
+ Every `EpochSession` — full or partial — has `fromSlot = firstSlotOfEpoch(N)`. The L1 rollup
211
+ contract requires every proof to extend from the previous epoch's proven tip, so
212
+ there's no value in starting later than the epoch boundary. The two kinds differ
213
+ only in `toSlot` and in how the publishing service treats their candidate:
214
+
215
+ - **Full** `EpochSession`s are opened by reconcile when the epoch is complete on L1 *and*
216
+ every archiver-reported checkpoint is present in the store. Their `toSlot` is
217
+ the epoch's last slot. The publishing service never auto-supersedes a `full`
218
+ candidate on proven-tip subsumption — the L1 contract records a `(epoch, prover-id)`
219
+ submission for every full-epoch proof, so even after another prover-node has
220
+ landed first, this prover's submission is still worthwhile.
221
+ - **Partial** `EpochSession`s are constructed by an explicit `startProof(epochNumber)` API
222
+ call. Their `toSlot` is the last canonical slot present at request time, which may
223
+ be earlier than the epoch's last slot. Partial candidates are an early-finish
224
+ optimisation: if the proven chain has caught up to or past `endBlock` by the time
225
+ the publishing service picks the winner, the partial resolves `'superseded'`
226
+ without spending L1 gas. Dedup: if the partial's spec collapses to the full's spec
227
+ (canonical content already covers the whole epoch), `startProof` awaits the
228
+ existing full `EpochSession` instead of opening a duplicate.
229
+
230
+ ## ProofPublishingService
231
+
232
+ The service is a single per-prover-node owner of L1 submission. `EpochSession`s call
233
+ `submit(candidate)` and await one of five outcomes:
234
+
235
+ | Outcome | Meaning |
236
+ |---|---|
237
+ | `published` | L1 accepted the proof. |
238
+ | `superseded` | A longer same-epoch candidate won, or (for `partial` candidates) the proven tip has caught up to `endBlock`. |
239
+ | `failed` | L1 submission errored. |
240
+ | `expired` | The candidate's `deadline` elapsed before publishing started. |
241
+ | `withdrawn` | An `EpochSession` called `withdraw(uuid)` on a still-queued candidate. |
242
+
243
+ Key invariants:
244
+
245
+ - **One publish at a time** via a `SerialQueue` drain.
246
+ - **Fresh publisher per publish.** Each drain call constructs a new `ProverNodePublisher`
247
+ via the factory. There is no shared in-memory state across publishes.
248
+ - **Once an L1 publish starts, it runs to completion.** `withdraw` is a queue-only
249
+ operation: it removes a candidate that hasn't started publishing. An in-flight
250
+ candidate is left alone and its outcome reports whatever L1 returned. The
251
+ originating `EpochSession` has already moved to a terminal state via `cancel()` and
252
+ ignores the late outcome.
253
+ - **Drain reads the proven block number afresh** from `l2BlockSource` inside the
254
+ serial queue, so the eligibility
255
+ check is consistent with the publish that follows it on the same drain pass.
256
+ - **Per-candidate `deadline`** arms a `setTimeout` (against the injected `DateProvider`).
257
+ When it fires, a still-queued candidate resolves `'expired'`. An in-flight publish
258
+ is left alone (its outcome reports the natural L1 result).
259
+ - **Transient `publisherFactory.create()` failures are retried.** Instead of resolving
260
+ the candidate as `'failed'`, the service schedules another drain after a 1s backoff
261
+ and leaves the candidate in the queue. The candidate's `deadline` caps the total
262
+ retry window — persistent acquire failure resolves as `'expired'`.
263
+
264
+ ### Eligibility
265
+
266
+ A candidate is eligible to publish when its **predecessor block is proven**
267
+ (`startBlock - 1 <= proven`). Among eligible candidates for the same epoch, the
268
+ one with the **highest `endBlock`** wins; the others resolve `'superseded'`.
269
+ Partial candidates whose `endBlock <= proven` are dropped before this check
270
+ (early-finish optimisation no longer helps); full candidates are never
271
+ auto-superseded on the proven tip.
272
+
273
+ ## Event flow
274
+
275
+ ### chain-checkpointed
276
+
277
+ ```mermaid
278
+ sequenceDiagram
279
+ participant L2 as L2BlockStream
280
+ participant PN as ProverNode
281
+ participant CS as CheckpointStore
282
+ participant CP as CheckpointProver
283
+ participant SM as SessionManager
284
+
285
+ L2->>PN: chain-checkpointed{checkpoint}
286
+ PN->>PN: collectRegisterData (prev-header, l1ToL2 messages, sibling path)
287
+ PN->>CS: addOrUpdate(checkpoint, data)
288
+ alt content key new
289
+ CS->>CP: new CheckpointProver(args)
290
+ CP->>CP: eager gather + sub-tree start
291
+ else content key matches
292
+ CS->>CP: markCanonical()
293
+ end
294
+ PN->>SM: onCheckpointAdded(epoch)
295
+ SM->>SM: queue reconcile({kind:'checkpoint', epoch})
296
+ SM->>SM: walk EpochSessions, recreate invalid
297
+ SM->>SM: open full EpochSession if epoch ready
298
+ ```
299
+
300
+ ### chain-pruned
301
+
302
+ ```mermaid
303
+ sequenceDiagram
304
+ participant L2 as L2BlockStream
305
+ participant PN as ProverNode
306
+ participant CS as CheckpointStore
307
+ participant SM as SessionManager
308
+
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)
312
+ PN->>SM: onPrune(affectedEpochs)
313
+ SM->>SM: queue reconcile({kind:'prune', affectedEpochs})
314
+ SM->>SM: walk EpochSessions, cancel-and-recreate those with shifted content
315
+ ```
316
+
317
+ ### chain-proven
318
+
319
+ ```mermaid
320
+ sequenceDiagram
321
+ participant L2 as L2BlockStream
322
+ participant PN as ProverNode
323
+ participant PS as ProofPublishingService
324
+
325
+ L2->>PN: chain-proven{block}
326
+ PN->>PS: onChainProven(blockNumber)
327
+ PS->>PS: scheduleDrain (wake-up only, no state cached)
328
+ PS->>PS: drain reads proven afresh, re-checks eligibility
329
+ ```
330
+
331
+ ### Per-event expiry sweep
332
+
333
+ ```mermaid
334
+ sequenceDiagram
335
+ participant L2 as L2BlockStream
336
+ participant PN as ProverNode
337
+ participant CC as ChonkCache
338
+ participant CS as CheckpointStore
339
+
340
+ L2->>PN: any event
341
+ PN->>L2: getSyncedL2SlotNumber()
342
+ PN->>PN: latestEpoch = getEpochAtSlot(latestSlot)
343
+ PN->>PN: newlyExpiredUpTo = latestEpoch - (proofSubmissionEpochs + 1)
344
+ loop for each newly-expired epoch
345
+ PN->>L2: getCheckpointsData({epoch}) + getBlocks(...)
346
+ PN->>CC: releaseForBlocks(blocks)
347
+ PN->>CS: reapExpired(epoch)
348
+ end
349
+ ```
350
+
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.
358
+
359
+ ### Periodic tick
360
+
361
+ `SessionManager.start()` arms a `RunningPromise` that fires
362
+ `reconcile({ kind: 'tick' })` every `tickIntervalMs`. The tick picks up epochs that
363
+ 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.
369
+
370
+ ## Walkthroughs
371
+
372
+ ### checkpoint-added → prune → checkpoint-added (reorg resilience)
373
+
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]`.
377
+
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]`.
383
+
384
+ 2. **`EpochSession` B starts top-tree proving over [c1, c2, c3].**
385
+
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.)
391
+
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]`.
395
+
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
398
+ survived the reorg because no epoch in this range has expired yet.
399
+
400
+
401
+ ### Partial request dedups against a running full `EpochSession`
402
+
403
+ The operator calls `startProof(N)` while the full `EpochSession` for epoch N is running with
404
+ c1..c4. Current canonical slot range is `(s1, s4)`, so the partial's computed spec is
405
+ `{partial, N, s1, s4}` — its `fromSlot`/`toSlot` exactly match the running full `EpochSession`'s. `startProof`
406
+ detects this and awaits the existing full instead of opening a duplicate: no partial
407
+ `EpochSession` is created and no second `TopTreeJob` is built. The caller simply blocks on the
408
+ full session's result and the epoch is proven once.
409
+
410
+ ### True partial proof
411
+
412
+ The operator calls `startProof(N)` when only c1, c2 are canonical (epoch incomplete).
413
+ `fromSlot` is the epoch's first slot; `toSlot` is `s2` (the last canonical slot).
414
+ Partial `EpochSession` created with spec `{partial, N, firstSlotOfEpoch(N), s2}` and
415
+ checkpoints `[c1, c2]`.
416
+
417
+ When c3 later arrives in slot s3, the partial is **not** invalidated — c3's slot is
418
+ outside its range. If c2 is then pruned, the partial **is** invalidated (canonical
419
+ content for the same slot range is now just `[c1]`) and recreated with the same
420
+ spec but checkpoints `[c1]`. If c2 re-adds, the partial is invalidated again and
421
+ recreated with `[c1, c2]`.
422
+
423
+ ## Design rationale
424
+
425
+ ### Why slot-based specs (not checkpoint-based)?
426
+
427
+ A spec like "prove checkpoints 7..10" is invalidated by any reorg that renumbers
428
+ those checkpoints. A spec like "prove slots 350..399" survives renumbering — the
429
+ slot range is determined by epoch math and L1 constants, not by which checkpoints
430
+ happen to be canonical at the moment. Reconciliation preserves the slot range
431
+ across cancel-and-recreate cycles.
432
+
433
+ ### Why does every `EpochSession` start at the epoch's first slot?
434
+
435
+ The L1 rollup contract validates that every submitted proof extends from the previous
436
+ proven tip — the `fromCheckpoint` of any submission must be the checkpoint immediately
437
+ after the current L1 proven head. Starting a partial `EpochSession` at a later slot would
438
+ mean the partial's `fromCheckpoint` lies past the proven tip, which the contract
439
+ rejects. Fixing `fromSlot` to `firstSlotOfEpoch(N)` for both kinds means partials and
440
+ fulls always share the same starting point; they differ only in `toSlot` and in the
441
+ submission decision.
442
+
443
+ ### Why does a publishing service own L1 submission instead of the `EpochSession`?
444
+
445
+ Concentrating L1 submission gives us three properties for free that were awkward
446
+ or impossible when each `EpochSession` called the publisher directly:
447
+
448
+ 1. **Atomic same-epoch dedup.** Multiple candidates for the same epoch (full +
449
+ partial, or partial-then-full as canonical content extends) can be in flight
450
+ at once; the service picks the winner under the serial drain so only one L1
451
+ tx is ever sent for the longer candidate.
452
+ 2. **One source of truth for the proven tip.** Reading the proven block number
453
+ inside the drain means the eligibility check and the publish that follows are
454
+ guaranteed to use the same value. `EpochSession`s can't race each other on stale
455
+ reads.
456
+ 3. **Per-candidate deadline and retry.** The service owns expiry timers and the
457
+ `publisherFactory.create()` retry loop. `EpochSession`s don't need to know about
458
+ either — they just await the outcome.
459
+
460
+ ### Why is the chonk cache keyed by tx hash and released on finality?
461
+
462
+ Chonk-verifier proofs are tx-scoped: they prove a transaction's chonk circuit is
463
+ valid, independently of which block or epoch the tx lands in. A tx that gets
464
+ reorged out of one block and re-mined into another should not need to be re-proved.
465
+ Keying by tx hash makes the cache survive any reorg up to finality; releasing on
466
+ finality means we don't grow the cache indefinitely while still keeping every
467
+ reorg-relevant proof.
468
+
469
+ ### Why does the slot watcher only reap pruned `CheckpointProver`s?
470
+
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.
476
+
477
+ ## Configuration
478
+
479
+ | Env var | Description |
480
+ |---|---|
481
+ | `PROVER_NODE_POLLING_INTERVAL_MS` | Polling interval for the L2BlockStream, the checkpoint-store slot watcher, and the SessionManager periodic tick. Default 1000 ms. |
482
+ | `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
+ | `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
+ | `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. |
486
+ | `PROVER_NODE_DISABLE_PROOF_PUBLISH` | If true, the publishing service runs `analyzeEpochProofSubmission` (estimates L1 fees) instead of actually submitting. |
487
+
488
+ ## Failure handling and observability
489
+
490
+ Loggers:
491
+
492
+ - `prover-node` — `ProverNode` itself (event dispatch, lifecycle).
493
+ - `prover-node:session-manager` — reconcile decisions, `EpochSession` opens / drops, tick.
494
+ - `prover-node:epoch-session` — per-`EpochSession` lifecycle (`Created EpochSession`,
495
+ `Top-tree proof ready`, `Submitted proof for epoch N`, etc.).
496
+ - `prover-node:proof-publishing-service` — candidate submit / withdraw / expire,
497
+ drain, publish attempts, transient acquire retries.
498
+ - `prover-node:l1-tx-publisher` — the per-publish `ProverNodePublisher`'s L1 work.
499
+ - `prover-node:checkpoint-store` — content-key collisions, reap decisions.
500
+ - `prover-node:checkpoint-prover` — sub-tree pipeline (gather, block processing).
501
+ - `prover-client:chonk-cache` — chonk-verifier cache enqueue / release events.
502
+
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`.
511
+
512
+ Metrics emitted by `EpochSession`s:
513
+
514
+ - `aztec.prover_node.execution_duration` — wall-clock time from `EpochSession` start to terminal.
515
+ - `aztec.prover_node.job_duration` — same, in seconds.
516
+ - `aztec.prover_node.job_checkpoints` / `_blocks` / `_transactions` — sizes of the
517
+ proven range.
518
+ - `aztec.prover_node.block_processing_duration` /
519
+ `aztec.prover_node.checkpoint_processing_duration` — sub-tree breakdown.
@@ -25,7 +25,7 @@ import { deserializeEpochProvingJobData } from '../job/epoch-proving-job-data.js
25
25
  }, log, {
26
26
  ...config,
27
27
  ...metadata,
28
- snapshotsUrl: location
28
+ fileStore
29
29
  });
30
30
  const dataPath = urlJoin(location, 'data.bin');
31
31
  const localPath = config.jobDataDownloadPath;
@@ -3,10 +3,11 @@ import type { Logger } from '@aztec/foundation/log';
3
3
  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
+ import type { GenesisData } from '@aztec/stdlib/world-state';
6
7
  /**
7
8
  * Given a local folder where `downloadEpochProvingJob` was called, creates a new archiver and world state
8
- * using the state snapshots, and creates a new epoch proving job to prove the downloaded proving job.
9
+ * using the state snapshots, and creates a new epoch proving session to prove the downloaded proving job.
9
10
  * Proving is done with a local proving broker and agents as specified by the config.
10
11
  */
11
- export declare function rerunEpochProvingJob(localPath: string, log: Logger, config: DataStoreConfig & ProverBrokerConfig & ProverClientConfig & Pick<L1ContractsConfig, 'aztecEpochDuration'>): Promise<"awaiting-prover" | "completed" | "failed" | "initialized" | "processing" | "publishing-proof" | "reorg" | "stopped" | "timed-out">;
12
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVydW4tZXBvY2gtcHJvdmluZy1qb2IuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9hY3Rpb25zL3JlcnVuLWVwb2NoLXByb3Zpbmctam9iLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLE9BQU8sS0FBSyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFDaEUsT0FBTyxLQUFLLEVBQUUsTUFBTSxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDcEQsT0FBTyxFQUFFLEtBQUssa0JBQWtCLEVBQXNCLE1BQU0sc0JBQXNCLENBQUM7QUFDbkYsT0FBTyxFQUFFLGtCQUFrQixFQUErQixNQUFNLDZCQUE2QixDQUFDO0FBRTlGLE9BQU8sS0FBSyxFQUFFLGVBQWUsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBVTlEOzs7O0dBSUc7QUFDSCx3QkFBc0Isb0JBQW9CLENBQ3hDLFNBQVMsRUFBRSxNQUFNLEVBQ2pCLEdBQUcsRUFBRSxNQUFNLEVBQ1gsTUFBTSxFQUFFLGVBQWUsR0FBRyxrQkFBa0IsR0FBRyxrQkFBa0IsR0FBRyxJQUFJLENBQUMsaUJBQWlCLEVBQUUsb0JBQW9CLENBQUMsK0lBc0NsSCJ9
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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVydW4tZXBvY2gtcHJvdmluZy1qb2IuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9hY3Rpb25zL3JlcnVuLWVwb2NoLXByb3Zpbmctam9iLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLE9BQU8sS0FBSyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFFaEUsT0FBTyxLQUFLLEVBQUUsTUFBTSxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFFcEQsT0FBTyxFQUFFLEtBQUssa0JBQWtCLEVBQXNCLE1BQU0sc0JBQXNCLENBQUM7QUFDbkYsT0FBTyxFQUFFLGtCQUFrQixFQUErQixNQUFNLDZCQUE2QixDQUFDO0FBTzlGLE9BQU8sS0FBSyxFQUFFLGVBQWUsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBRzlELE9BQU8sS0FBSyxFQUFFLFdBQVcsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBVzdEOzs7O0dBSUc7QUFDSCx3QkFBc0Isb0JBQW9CLENBQ3hDLFNBQVMsRUFBRSxNQUFNLEVBQ2pCLEdBQUcsRUFBRSxNQUFNLEVBQ1gsTUFBTSxFQUFFLGVBQWUsR0FBRyxrQkFBa0IsR0FBRyxrQkFBa0IsR0FBRyxJQUFJLENBQUMsaUJBQWlCLEVBQUUsb0JBQW9CLENBQUMsRUFDakgsT0FBTyxDQUFDLEVBQUUsV0FBVyxtTUF5RnRCIn0=
@@ -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;AAChE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,KAAK,kBAAkB,EAAsB,MAAM,sBAAsB,CAAC;AACnF,OAAO,EAAE,kBAAkB,EAA+B,MAAM,6BAA6B,CAAC;AAE9F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAU9D;;;;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,+IAsClH"}
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,mMAyFtB"}