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