You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(prover-node): rebuild pruned checkpoint provers and recover the epoch
A CheckpointProver's sub-tree work forks world-state per block. When an L1 reorg
prunes a base block, those fork reads fault and the prover permanently rejects
its one-shot blockProofs promise. The store previously kept the prover alive
(markPruned) so a re-add of identical content could reuse its in-flight work,
but a prune that removes the checkpoint also breaks the fork reads, so the reuse
only ever handed back a poisoned prover: a re-add or a full EpochSession recreate
re-referenced the same rejected blockProofs and failed immediately, and the node
silently abandoned the epoch until a process restart.
Drop the reuse machinery — a pruned prover cannot survive:
- CheckpointStore.cancelAndRemoveAboveBlock cancels and removes orphaned provers
(replacing markPrunedAboveBlock); a re-add builds a fresh prover.
- Remove the pruned flag / markPruned / markCanonical from CheckpointProver and
the SlotWatcher that only reaped lingering pruned provers.
- stop() awaits teardowns still in flight for removed provers, not just live ones.
- Drop the now-redundant "canonical" method naming: listCanonical* -> list*,
canonicalCheckpointsForSpec -> checkpointsForSpec.
Recover the epoch by rebuilding on re-add rather than racing the failure. A
prune unwinds world-state before the prover-node cancels the session, so a
prover mid-fork faults while live and the session goes terminal `failed`; that
race cannot be won from the session side, so make the terminal state harmless:
- SessionManager.openFullSessionIfReady rebuilds over current canonical content;
recreateInvalidSessions deletes terminal sessions before it runs.
- runSession skips the failure-upload when the session's checkpoints no longer
match the store (best-effort; inherently races the prune, as the store lags the
world-state unwind).
Cancel the pruned prover's in-flight work promptly: thread the abort signal into
PublicProcessor.process so a cancel stops the current block's public execution
immediately instead of running it to completion.
The expensive proving is unaffected: broker proofs are content-addressed and
survive the prune (cancelJobsOnStop defaults to false), so an identical re-add
reuses them; only witness generation, which cannot survive a prune, is redone.
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]`.
369
+
State: epoch N has checkpoints c1..c4 (slots s1..s4). `fullSessions[N]` holds `EpochSession`
370
+
**A** with spec `{kind:'full', N, fromSlot:s1, toSlot:s4}`, referencing `[c1, c2, c3, c4]`.
377
371
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]`.
372
+
1.**chain-pruned arrives, target c3.**The store cancels and removes c4 (its last block is
373
+
above the prune target). Reconcile fires: `EpochSession` A's frozen set `[c1, c2, c3, c4]`
374
+
includes the now-cancelled c4, so it no longer matches the store's `[c1, c2, c3]` →
375
+
`A.cancel('canonical content changed')`. Epoch N still complete on L1 → reconcile constructs
376
+
`EpochSession`**B** with the same spec `{full, N, s1, s4}` but checkpoints `[c1, c2, c3]`.
383
377
384
378
2.**`EpochSession` B starts top-tree proving over [c1, c2, c3].**
385
379
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.)
380
+
3.**chain-checkpointed arrives, target c4_re (same content key as old c4).** The old c4
381
+
prover was removed on the prune, so the store constructs a **fresh**`CheckpointProver` for
382
+
c4_re and starts its sub-tree work from scratch. (Its block-rollup jobs are content-addressed
383
+
in the proving broker, so any the old c4 already completed are reused rather than re-proved.)
391
384
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]`.
385
+
4.**Reconcile fires.**`EpochSession` B's content for `(s1, s4)` is now `[c1, c2, c3, c4_re]`,
386
+
doesn't match its frozen `[c1, c2, c3]` → `B.cancel(...)`. Construct`EpochSession`**C** with
387
+
the same spec but checkpoints `[c1, c2, c3, c4_re]`.
395
388
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
389
+
5.**`EpochSession` C reuses the long-lived c1..c3`CheckpointProver` instances and the fresh
390
+
c4_re.** Only c4_re's witnesses are regenerated; the top-tree is recomputed. The chonk cache
398
391
survived the reorg because no epoch in this range has expired yet.
399
392
400
393
@@ -466,19 +459,19 @@ Keying by tx hash makes the cache survive any reorg up to finality; releasing on
466
459
finality means we don't grow the cache indefinitely while still keeping every
467
460
reorg-relevant proof.
468
461
469
-
### Why does the slot watcher only reap pruned `CheckpointProver`s?
462
+
### Why is a pruned `CheckpointProver` removed rather than kept for a possible re-add?
470
463
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.
464
+
A prover's sub-tree work forks world-state per block, and an L1 prune of a base block faults
465
+
those in-flight reads — so the work cannot survive the prune, and there is nothing to preserve
466
+
by keeping the prover around. Removing it on prune and rebuilding on re-add is therefore both
467
+
correct and simpler; the expensive block-rollup proofs are still reused when content re-appears,
468
+
because the proving broker is content-addressed independently of the prover's lifetime.
476
469
477
470
## Configuration
478
471
479
472
| Env var | Description |
480
473
|---|---|
481
-
|`PROVER_NODE_POLLING_INTERVAL_MS`| Polling interval for the L2BlockStream, the checkpoint-store slot watcher, and the SessionManager periodic tick. Default 1000 ms. |
474
+
|`PROVER_NODE_POLLING_INTERVAL_MS`| Polling interval for the L2BlockStream and the SessionManager periodic tick. Default 1000 ms. |
482
475
|`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
476
|`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
477
|`TX_GATHERING_TIMEOUT_MS`| Per-block tx gather deadline used by each `CheckpointProver`. |
0 commit comments