@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
@@ -370,28 +370,40 @@ function applyDecs2203RFactory() {
370
370
  function _apply_decs_2203_r(targetClass, memberDecs, classDecs, parentClass) {
371
371
  return (_apply_decs_2203_r = applyDecs2203RFactory())(targetClass, memberDecs, classDecs, parentClass);
372
372
  }
373
- var _dec, _dec1, _initProto;
374
- import { BlockNumber } from '@aztec/foundation/branded-types';
375
- import { assertRequired, compact, pick, sum } from '@aztec/foundation/collection';
373
+ var _initProto;
374
+ import { BlockNumber, CheckpointNumber, EpochNumber } from '@aztec/foundation/branded-types';
375
+ import { assertRequired, compact, pick } from '@aztec/foundation/collection';
376
376
  import { memoize } from '@aztec/foundation/decorators';
377
377
  import { createLogger } from '@aztec/foundation/log';
378
- import { DateProvider } from '@aztec/foundation/timer';
378
+ import { RunningPromise } from '@aztec/foundation/running-promise';
379
+ import { DateProvider, executeTimeout } from '@aztec/foundation/timer';
380
+ import { getLastSiblingPath } from '@aztec/prover-client/helpers';
381
+ import { ChonkCache } from '@aztec/prover-client/orchestrator';
379
382
  import { PublicProcessorFactory } from '@aztec/simulator/server';
380
- import { getProofSubmissionDeadlineTimestamp } from '@aztec/stdlib/epoch-helpers';
381
- import { tryStop } from '@aztec/stdlib/interfaces/server';
382
- import { Attributes, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
383
+ import { EventDrivenL2BlockStream, L2TipsMemoryStore } from '@aztec/stdlib/block';
384
+ import { getEpochAtSlot, getProofSubmissionDeadlineEpoch } from '@aztec/stdlib/epoch-helpers';
385
+ import { EpochProvingJobTerminalState, tryStop } from '@aztec/stdlib/interfaces/server';
386
+ import { MerkleTreeId } from '@aztec/stdlib/trees';
387
+ import { getTelemetryClient } from '@aztec/telemetry-client';
383
388
  import { uploadEpochProofFailure } from './actions/upload-epoch-proof-failure.js';
384
- import { EpochProvingJob } from './job/epoch-proving-job.js';
389
+ import { CheckpointStore } from './checkpoint-store.js';
385
390
  import { ProverNodeJobMetrics, ProverNodeRewardsMetrics } from './metrics.js';
386
- _dec = trackSpan('ProverNode.createProvingJob', (epochNumber)=>({
387
- [Attributes.EPOCH_NUMBER]: epochNumber
388
- })), _dec1 = trackSpan('ProverNode.gatherEpochData', (epochNumber)=>({
389
- [Attributes.EPOCH_NUMBER]: epochNumber
390
- }));
391
+ import { ProofPublishingService } from './proof-publishing-service.js';
392
+ import { SessionManager } from './session-manager.js';
391
393
  /**
392
- * An Aztec Prover Node is a standalone process that monitors the unfinalized chain on L1 for unproven epochs,
393
- * fetches their txs from the p2p network or external nodes, re-executes their public functions, creates a rollup
394
- * proof for the epoch, and submits it to L1.
394
+ * Grace period for the proof-publishing service to settle during shutdown. The service waits for
395
+ * any in-flight L1 proof-submission tx to finish; that tx can take a long time to mine, so we cap
396
+ * the wait rather than letting `stop()` hang indefinitely.
397
+ */ const PUBLISHING_SERVICE_STOP_TIMEOUT_MS = 30_000;
398
+ /**
399
+ * An Aztec Prover Node is a standalone process that monitors the chain for new checkpoints,
400
+ * starts proving them optimistically as they arrive, and submits epoch proofs to L1 once
401
+ * complete.
402
+ *
403
+ * The class is intentionally thin: it owns the long-lived collections (`CheckpointStore`,
404
+ * `ChonkCache`, `SessionManager`), the L2BlockStream, and a periodic ticker that nudges the
405
+ * manager to pick up newly-complete epochs. Every session lifecycle decision is delegated to
406
+ * the `SessionManager`. Each chain event is translated here into a single method call on it.
395
407
  */ export class ProverNode {
396
408
  prover;
397
409
  publisherFactory;
@@ -400,40 +412,46 @@ _dec = trackSpan('ProverNode.createProvingJob', (epochNumber)=>({
400
412
  contractDataSource;
401
413
  worldState;
402
414
  p2pClient;
403
- epochsMonitor;
404
415
  rollupContract;
405
416
  l1Metrics;
417
+ avmSimulator;
406
418
  telemetryClient;
407
419
  delayer;
408
420
  dateProvider;
409
421
  static{
410
422
  ({ e: [_initProto] } = _apply_decs_2203_r(this, [
411
- [
412
- _dec,
413
- 2,
414
- "createProvingJob"
415
- ],
416
423
  [
417
424
  memoize,
418
425
  2,
419
426
  "getL1Constants"
420
- ],
421
- [
422
- _dec1,
423
- 2,
424
- "gatherEpochData"
425
427
  ]
426
428
  ], []));
427
429
  }
428
430
  log;
429
- jobs;
431
+ checkpointStore;
432
+ chonkCache;
433
+ sessionManager;
430
434
  config;
431
435
  jobMetrics;
432
436
  rewardsMetrics;
433
- startingProofEpochs;
437
+ /** In-memory store for the L2BlockStream's local data provider. */ tipsStore;
438
+ /** Block stream for checkpoint and reorg detection. */ blockStream;
439
+ /**
440
+ * Highest epoch whose proof-submission window has passed. Monotonic high-water mark.
441
+ * Seeded from the last fully-proven epoch at start(); advanced on every block-stream
442
+ * event by comparing the archiver's latest synced L2 slot against each epoch's
443
+ * submission deadline. Protected so tests can verify the start() seeding.
444
+ */ lastExpiredEpoch;
445
+ /**
446
+ * Highest checkpoint number whose proving-side handling has completed (or that was legitimately skipped).
447
+ * The catch-up loop walks from here to each `chain-checkpointed` tip event. Seeded at start() from the last
448
+ * checkpoint of the last fully-proven epoch (or 0), so a restart reprocesses the partially-proven epoch rather
449
+ * than trusting a checkpointed tip that may sit ahead of unproven checkpoints. Clamped down on a prune.
450
+ */ lastProcessedCheckpoint;
451
+ /** Periodic tick that runs the epoch-expiry sweep during idle periods when no block-stream events arrive. */ expiryTicker;
434
452
  tracer;
435
- publisher;
436
- constructor(prover, publisherFactory, l2BlockSource, l1ToL2MessageSource, contractDataSource, worldState, p2pClient, epochsMonitor, rollupContract, l1Metrics, config = {}, telemetryClient = getTelemetryClient(), delayer, dateProvider = new DateProvider()){
453
+ publishingService;
454
+ constructor(prover, publisherFactory, l2BlockSource, l1ToL2MessageSource, contractDataSource, worldState, p2pClient, rollupContract, l1Metrics, avmSimulator, config = {}, telemetryClient = getTelemetryClient(), delayer, dateProvider = new DateProvider()){
437
455
  this.prover = prover;
438
456
  this.publisherFactory = publisherFactory;
439
457
  this.l2BlockSource = l2BlockSource;
@@ -441,15 +459,14 @@ _dec = trackSpan('ProverNode.createProvingJob', (epochNumber)=>({
441
459
  this.contractDataSource = contractDataSource;
442
460
  this.worldState = worldState;
443
461
  this.p2pClient = p2pClient;
444
- this.epochsMonitor = epochsMonitor;
445
462
  this.rollupContract = rollupContract;
446
463
  this.l1Metrics = l1Metrics;
464
+ this.avmSimulator = avmSimulator;
447
465
  this.telemetryClient = telemetryClient;
448
466
  this.delayer = delayer;
449
467
  this.dateProvider = dateProvider;
450
468
  this.log = (_initProto(this), createLogger('prover-node'));
451
- this.jobs = new Map();
452
- this.startingProofEpochs = new Set();
469
+ this.lastProcessedCheckpoint = CheckpointNumber.ZERO;
453
470
  this.config = {
454
471
  proverNodePollingIntervalMs: 1_000,
455
472
  proverNodeMaxPendingJobs: 100,
@@ -467,6 +484,23 @@ _dec = trackSpan('ProverNode.createProvingJob', (epochNumber)=>({
467
484
  this.tracer = telemetryClient.getTracer('ProverNode');
468
485
  this.jobMetrics = new ProverNodeJobMetrics(meter, telemetryClient.getTracer('EpochProvingJob'));
469
486
  this.rewardsMetrics = new ProverNodeRewardsMetrics(meter, this.prover.getProverId(), rollupContract);
487
+ this.tipsStore = new L2TipsMemoryStore(this.l2BlockSource.getGenesisBlockHash());
488
+ this.chonkCache = new ChonkCache(this.log.getBindings());
489
+ this.checkpointStore = new CheckpointStore(this.l2BlockSource, {
490
+ proverFactory: this.prover,
491
+ chonkCache: this.chonkCache,
492
+ publicProcessorFactory: new PublicProcessorFactory(this.contractDataSource, this.avmSimulator, this.dateProvider, this.telemetryClient, this.log.getBindings()),
493
+ dbProvider: this.worldState,
494
+ txProvider: this.p2pClient.getTxProvider(),
495
+ dateProvider: this.dateProvider,
496
+ proverId: this.prover.getProverId(),
497
+ metrics: this.jobMetrics,
498
+ txGatheringTimeoutMs: this.config.txGatheringTimeoutMs,
499
+ deadline: undefined,
500
+ // A checkpoint prover that fails (a sub-tree fault or a prune-induced fork fault) uploads a
501
+ // post-mortem for its own checkpoint, independently of any session. Fire-and-forget.
502
+ onFailed: (prover)=>void this.tryUploadCheckpointFailure(prover)
503
+ }, this.log.getBindings());
470
504
  }
471
505
  getProverId() {
472
506
  return this.prover.getProverId();
@@ -474,47 +508,320 @@ _dec = trackSpan('ProverNode.createProvingJob', (epochNumber)=>({
474
508
  getP2P() {
475
509
  return this.p2pClient;
476
510
  }
477
- /** Returns the shared tx delayer for prover L1 txs, if enabled. Test-only. */ getDelayer() {
511
+ /** Test-only: the shared L1 tx delayer, if enabled. */ getDelayer() {
478
512
  return this.delayer;
479
513
  }
514
+ /** Observability summary for the ProverNodeApi. */ getJobs() {
515
+ return Promise.resolve(this.sessionManager?.getJobs() ?? []);
516
+ }
517
+ /** Tests inspect this when validating reconcile behaviour. */ getCheckpointStore() {
518
+ return this.checkpointStore;
519
+ }
520
+ /** Tests inspect this to verify chonk-cache release semantics. */ getChonkCache() {
521
+ return this.chonkCache;
522
+ }
523
+ /** Tests inspect this when looking up live sessions. */ getSessionManager() {
524
+ if (!this.sessionManager) {
525
+ throw new Error('SessionManager not yet constructed — start() must be called first.');
526
+ }
527
+ return this.sessionManager;
528
+ }
529
+ /** Returns the underlying prover instance. */ getProver() {
530
+ return this.prover;
531
+ }
532
+ // ---------------- L2BlockStream handler ----------------
533
+ async handleBlockStreamEvent(event) {
534
+ switch(event.type){
535
+ case 'chain-checkpointed':
536
+ await this.processCheckpointJump(event.checkpoint.number);
537
+ break;
538
+ case 'chain-pruned':
539
+ await this.handlePruneEvent(event.block);
540
+ break;
541
+ case 'chain-proven':
542
+ this.publishingService?.onChainProven(BlockNumber(event.block.number));
543
+ break;
544
+ // The proposed tip drives only the tips store's walk-back history (recorded below); the prover-node
545
+ // tracks checkpoints, not proposed blocks. `blocks-added` is never emitted in tips-only mode, and
546
+ // `chain-finalized` carries nothing the prover-node acts on.
547
+ case 'chain-proposed':
548
+ case 'chain-finalized':
549
+ case 'blocks-added':
550
+ break;
551
+ default:
552
+ {
553
+ const _ = event;
554
+ break;
555
+ }
556
+ }
557
+ // Advance the local tips store only after the proving-side handling (registration / prune) has
558
+ // succeeded. Any failure above propagates to the L2BlockStream (which logs and stops this poll
559
+ // pass) and skips this update, so the event is re-emitted on the next poll rather than skipped
560
+ await this.tipsStore.handleBlockStreamEvent(event);
561
+ }
480
562
  /**
481
- * Handles an epoch being completed by starting a proof for it if there are no active jobs for it.
482
- * @param epochNumber - The epoch number that was just completed.
483
- * @returns false if there is an error, true otherwise
484
- */ async handleEpochReadyToProve(epochNumber) {
485
- try {
486
- this.log.debug(`Running jobs as ${epochNumber} is ready to prove`, {
487
- jobs: Array.from(this.jobs.values()).map((job)=>`${job.getEpochNumber()}:${job.getId()}`)
563
+ * Walks every checkpoint between the local cursor and the newly-reported checkpointed tip, registering
564
+ * each one that belongs to an epoch that can still be proven. The block stream now delivers a single thin
565
+ * `chain-checkpointed` tip event per pass rather than one fat event per checkpoint, so this drives the
566
+ * catch-up itself: light metadata first (`getCheckpointsData`) to decide relevance per epoch, then a heavy
567
+ * `getCheckpoints` fetch only for checkpoints in provable epochs.
568
+ *
569
+ * The cursor advances one checkpoint at a time and only after that checkpoint's proving-side handling has
570
+ * fully succeeded, preserving the A-1041 at-least-once semantics: a mid-jump failure leaves the cursor
571
+ * behind so the next pass retries from the first checkpoint that did not complete.
572
+ */ async processCheckpointJump(targetCheckpoint) {
573
+ if (targetCheckpoint <= this.lastProcessedCheckpoint) {
574
+ return;
575
+ }
576
+ const l1Constants = await this.getL1Constants();
577
+ // Cap the catch-up at the `(proofSubmissionEpochs + 1) * epochDuration` most recent checkpoints.
578
+ // When the cursor is much further behind (e.g. resyncing after a long time offline), fetching the whole gap could
579
+ // load thousands of checkpoints we cannot act on: anything older than the last two epochs is already past
580
+ // its proof-submission window, so we skip it and jump the cursor forward to the start of the capped range.
581
+ const maxCheckpoints = (l1Constants.proofSubmissionEpochs + 1) * l1Constants.epochDuration;
582
+ let from = CheckpointNumber(this.lastProcessedCheckpoint + 1);
583
+ if (Number(targetCheckpoint - from) + 1 > maxCheckpoints) {
584
+ const cappedFrom = CheckpointNumber(targetCheckpoint - maxCheckpoints + 1);
585
+ this.log.warn(`Skipping unprovable checkpoints during catch-up; the prover node is far behind`, {
586
+ from,
587
+ cappedFrom,
588
+ targetCheckpoint,
589
+ maxCheckpoints
488
590
  });
489
- return await this.startProofIfNeeded(epochNumber);
490
- } catch (err) {
491
- if (err instanceof EmptyEpochError) {
492
- this.log.info(`Not starting proof for ${epochNumber} since no blocks were found`);
591
+ // Advance the cursor past the skipped checkpoints so they are never retried.
592
+ this.lastProcessedCheckpoint = CheckpointNumber(cappedFrom - 1);
593
+ from = cappedFrom;
594
+ }
595
+ const limit = Number(targetCheckpoint - from) + 1;
596
+ const metadatas = await this.l2BlockSource.getCheckpointsData({
597
+ from,
598
+ limit
599
+ });
600
+ // Per-epoch relevance is cached so a multi-checkpoint epoch resolves it once. Skipping is whole-epoch
601
+ // only: the SessionManager requires an epoch's checkpoints fully covered before it opens a session, so we
602
+ // never drop an individual checkpoint inside an epoch we will prove.
603
+ const epochSkippable = new Map();
604
+ for (const metadata of metadatas){
605
+ const epochNumber = getEpochAtSlot(metadata.header.slotNumber, l1Constants);
606
+ let skippable = epochSkippable.get(epochNumber);
607
+ if (skippable === undefined) {
608
+ skippable = await this.isEpochFullyProven(epochNumber, l1Constants) || await this.isEpochPastProofSubmissionWindow(epochNumber, l1Constants);
609
+ epochSkippable.set(epochNumber, skippable);
610
+ }
611
+ if (skippable) {
612
+ this.log.debug(`Skipping checkpoint ${metadata.checkpointNumber} for unprovable epoch ${epochNumber}`);
493
613
  } else {
494
- this.log.error(`Error handling epoch completed`, err);
614
+ await this.registerCheckpoint(metadata.checkpointNumber, epochNumber);
495
615
  }
616
+ // Advance only after the checkpoint's handling succeeded (or it was legitimately skipped). registerCheckpoint
617
+ // throws on failure, which leaves the cursor here for the next pass to retry (A-1041).
618
+ this.lastProcessedCheckpoint = metadata.checkpointNumber;
619
+ }
620
+ }
621
+ /** Heavy-fetch a single checkpoint, register it with the store, and notify the session manager. */ async registerCheckpoint(checkpointNumber, epochNumber) {
622
+ const published = await this.l2BlockSource.getCheckpoint({
623
+ number: checkpointNumber
624
+ });
625
+ if (!published) {
626
+ throw new Error(`Checkpoint ${checkpointNumber} not found in block source during catch-up`);
627
+ }
628
+ const checkpoint = published.checkpoint;
629
+ this.log.info(`New checkpoint ${checkpoint.number} for epoch ${epochNumber}`, {
630
+ checkpointNumber: checkpoint.number,
631
+ epochNumber,
632
+ slotNumber: checkpoint.header.slotNumber
633
+ });
634
+ const registerData = await this.collectRegisterData(checkpoint, published.attestations);
635
+ await this.checkpointStore.addOrUpdate(checkpoint, registerData);
636
+ await this.sessionManager?.onCheckpointAdded(epochNumber);
637
+ // Tips-only mode delivers no blocks, so record one witness per checkpointed block: a reorg into the checkpoint's
638
+ // range then prunes at the true divergence instead of the nearest sparse tip anchor.
639
+ await this.tipsStore.recordBlockHashes(await Promise.all(checkpoint.blocks.map(async (block)=>({
640
+ number: block.number,
641
+ hash: (await block.header.hash()).toString()
642
+ }))));
643
+ }
644
+ /**
645
+ * Gathers register-time data for a checkpoint: previous block header, L1-to-L2 messages,
646
+ * and the archive sibling path.
647
+ */ async collectRegisterData(checkpoint, attestations) {
648
+ const previousBlockNumber = BlockNumber(checkpoint.blocks[0].number - 1);
649
+ const previousBlockHeader = await this.gatherPreviousBlockHeader(previousBlockNumber);
650
+ const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpoint.number);
651
+ const lastBlock = checkpoint.blocks.at(-1);
652
+ const lastBlockHash = await lastBlock.header.hash();
653
+ await this.worldState.syncImmediate(lastBlock.number, lastBlockHash);
654
+ const previousArchiveSiblingPath = await getLastSiblingPath(MerkleTreeId.ARCHIVE, this.worldState.getSnapshot(previousBlockNumber));
655
+ return {
656
+ attestations,
657
+ previousBlockHeader,
658
+ l1ToL2Messages,
659
+ previousArchiveSiblingPath
660
+ };
661
+ }
662
+ /**
663
+ * Marks every prover orphaned by the prune as pruned, clamps the catch-up cursor below the prune target's
664
+ * checkpoint, and notifies the session manager. Keyed off the prune target block (the highest surviving block)
665
+ * rather than the source's checkpointed tip, which can sit above the target after a re-checkpoint and would leave
666
+ * orphaned provers canonical. Throws (rather than warning) if the cursor floor cannot be resolved, so the pass
667
+ * fails and the prune is retried next iteration.
668
+ */ async handlePruneEvent(prunedToBlock) {
669
+ this.log.warn(`Chain pruned to block ${prunedToBlock.number}`, {
670
+ prunedToBlock
671
+ });
672
+ // Resolve the cursor floor BEFORE removing provers: cancelAndRemoveAboveBlock returns only the provers it removed,
673
+ // so a throw after removing would leave a retry pass with nothing to act on. Resolving first means a throw leaves
674
+ // everything untouched and the next pass retries the whole handler (the tips cursor only advances on success).
675
+ let cursorFloor;
676
+ if (prunedToBlock.number === 0) {
677
+ cursorFloor = CheckpointNumber.ZERO;
678
+ } else {
679
+ const targetData = await this.l2BlockSource.getBlockData({
680
+ number: prunedToBlock.number
681
+ });
682
+ if (targetData === undefined) {
683
+ throw new Error(`No block data found for prune target block ${prunedToBlock.number}; cannot clamp checkpoint cursor`);
684
+ }
685
+ // Clamp to `cpAtTarget - 1`: a mid-checkpoint target leaves that checkpoint partially orphaned and it must be
686
+ // reprocessed. Over-clamping merely re-registers a checkpoint (at-least-once by design — A-1041); under-clamping
687
+ // would permanently skip a rebuilt same-number checkpoint.
688
+ cursorFloor = CheckpointNumber(Math.max(0, Number(targetData.checkpointNumber) - 1));
689
+ }
690
+ const affected = this.checkpointStore.cancelAndRemoveAboveBlock(prunedToBlock.number);
691
+ if (this.lastProcessedCheckpoint > cursorFloor) {
692
+ this.lastProcessedCheckpoint = cursorFloor;
693
+ }
694
+ if (affected.length === 0) {
695
+ return;
696
+ }
697
+ const l1Constants = await this.getL1Constants();
698
+ const affectedEpochs = Array.from(new Set(affected.map((p)=>Number(getEpochAtSlot(p.slotNumber, l1Constants))))).map((n)=>EpochNumber(n));
699
+ // The session manager cancels every affected session, which in turn calls
700
+ // publishingService.withdraw(uuid) for each candidate; no separate notification to the
701
+ // publishing service is needed.
702
+ await this.sessionManager?.onPrune(affectedEpochs);
703
+ }
704
+ /**
705
+ * Returns true once the chain has advanced past the given epoch's proof-submission window.
706
+ * Used to ignore checkpoints whose epoch can no longer be proven in time — chiefly while the
707
+ * archiver replays old blocks after a restart. Compares the archiver's latest synced L2 slot
708
+ * against the epoch's submission-deadline epoch; conservatively returns false if the slot can't
709
+ * be read yet.
710
+ */ async isEpochPastProofSubmissionWindow(epochNumber, l1Constants) {
711
+ const latestSlot = await this.l2BlockSource.getSyncedL2SlotNumber();
712
+ if (latestSlot === undefined) {
496
713
  return false;
497
714
  }
715
+ const latestEpoch = getEpochAtSlot(latestSlot, l1Constants);
716
+ return latestEpoch >= getProofSubmissionDeadlineEpoch(epochNumber, l1Constants);
498
717
  }
499
718
  /**
500
- * Starts the prover node so it periodically checks for unproven epochs in the unfinalized chain from L1 and
501
- * starts proving jobs for them.
502
- */ async start() {
503
- this.epochsMonitor.start(this);
719
+ * Compares the archiver's latest synced L2 slot against `lastExpiredEpoch` and, for each
720
+ * newly-expired epoch, releases the chonk-cache entries for its blocks and reaps any
721
+ * CheckpointProvers in the store. An epoch E is expired once the chain reaches the start
722
+ * of epoch `E + proofSubmissionEpochs + 1`. Silently no-ops if nothing has expired since
723
+ * the last check or the archiver's slot can't be read.
724
+ */ async checkEpochExpiry() {
725
+ const latestSlot = await this.l2BlockSource.getSyncedL2SlotNumber();
726
+ if (latestSlot === undefined) {
727
+ return;
728
+ }
729
+ const l1Constants = await this.getL1Constants();
730
+ const latestEpoch = getEpochAtSlot(latestSlot, l1Constants);
731
+ const offset = l1Constants.proofSubmissionEpochs + 1;
732
+ if (latestEpoch < offset) {
733
+ return;
734
+ }
735
+ const newlyExpiredUpTo = EpochNumber(latestEpoch - offset);
736
+ const from = this.lastExpiredEpoch === undefined ? EpochNumber(0) : EpochNumber(this.lastExpiredEpoch + 1);
737
+ if (newlyExpiredUpTo < from) {
738
+ return;
739
+ }
740
+ for(let e = from; e <= newlyExpiredUpTo; e = EpochNumber(e + 1)){
741
+ await this.expireEpoch(e);
742
+ }
743
+ this.lastExpiredEpoch = newlyExpiredUpTo;
744
+ }
745
+ /**
746
+ * Releases chonk-cache entries for every block in the supplied epoch (best-effort) and reaps every
747
+ * CheckpointProver in the store whose epoch is at or below it.
748
+ */ async expireEpoch(epoch) {
749
+ try {
750
+ const blocks = await this.l2BlockSource.getBlocks({
751
+ epoch,
752
+ onlyCheckpointed: true
753
+ });
754
+ if (blocks.length > 0) {
755
+ this.chonkCache.releaseForBlocks(blocks);
756
+ }
757
+ } catch (err) {
758
+ this.log.warn(`Could not release chonk-cache entries for expired epoch ${epoch}`, err);
759
+ }
760
+ this.checkpointStore.reapExpired(epoch);
761
+ }
762
+ // ---------------- public API ----------------
763
+ /**
764
+ * Schedules proving for the given epoch and returns the job id without waiting for completion.
765
+ */ async startProof(epochNumber) {
766
+ if (!this.sessionManager) {
767
+ throw new Error('ProverNode not started');
768
+ }
769
+ return await this.sessionManager.startProof(epochNumber);
770
+ }
771
+ // ---------------- Service lifecycle ----------------
772
+ async start() {
773
+ await this.checkpointStore.start();
504
774
  await this.publisherFactory.start();
505
- this.publisher = await this.publisherFactory.create();
775
+ this.publishingService = new ProofPublishingService({
776
+ publisherFactory: this.publisherFactory,
777
+ l2BlockSource: this.l2BlockSource,
778
+ dateProvider: this.dateProvider,
779
+ config: {
780
+ skipSubmitProof: !!this.config.proverNodeDisableProofPublish
781
+ },
782
+ bindings: this.log.getBindings()
783
+ });
784
+ this.sessionManager = this.createSessionManager(this.publishingService);
785
+ // SessionManager owns its own periodic tick; start it here so it begins picking up
786
+ // epochs that become complete by time (no fresh checkpoint event) and advances once
787
+ // the previous epoch is proven on L1.
788
+ this.sessionManager.start();
789
+ // Now that the store + manager exist, arm the live-state observable gauges.
790
+ this.jobMetrics.observeState(this.checkpointStore, this.sessionManager);
791
+ const { lastFullyProvenEpoch } = await this.resolveLastFullyProvenEpoch();
792
+ this.lastExpiredEpoch = lastFullyProvenEpoch;
793
+ this.lastProcessedCheckpoint = await this.computeStartingCheckpoint(lastFullyProvenEpoch);
794
+ this.blockStream = new EventDrivenL2BlockStream(this.l2BlockSource, this.tipsStore, this, this.log, {
795
+ pollIntervalMS: this.config.proverNodePollingIntervalMs,
796
+ tipsOnly: true
797
+ });
798
+ this.blockStream.start();
799
+ // The periodic ticker is the sole driver of the expiry sweep: it fires every poll interval whether
800
+ // or not block-stream events arrive, and RunningPromise never overlaps its own runs, so the sweep's
801
+ // `lastExpiredEpoch` high-water mark advances — and each epoch's post-mortem uploads — exactly once.
802
+ this.expiryTicker = new RunningPromise(()=>this.checkEpochExpiry(), this.log, this.config.proverNodePollingIntervalMs);
803
+ this.expiryTicker.start();
506
804
  await this.rewardsMetrics.start();
507
805
  this.l1Metrics.start();
508
806
  this.log.info(`Started Prover Node with prover id ${this.prover.getProverId().toString()}`, this.config);
509
807
  }
510
- /**
511
- * Stops the prover node and all its dependencies.
512
- * Resources not owned by this node (shared with the parent aztec-node) are skipped.
513
- */ async stop() {
808
+ async stop() {
514
809
  this.log.info('Stopping ProverNode');
515
- await this.epochsMonitor.stop();
516
- this.publisher?.interrupt();
517
- await Promise.all(Array.from(this.jobs.values()).map((job)=>job.stop()));
810
+ this.jobMetrics.stopObservingState();
811
+ await this.blockStream?.stop();
812
+ await this.expiryTicker?.stop();
813
+ if (this.sessionManager) {
814
+ await this.sessionManager.stop();
815
+ }
816
+ if (this.publishingService) {
817
+ // Bound the wait: the publishing service blocks until any in-flight L1 proof-submission tx
818
+ // settles, which can outlast a reasonable shutdown window. On timeout we log and move on —
819
+ // the tx may still mine, but shutdown must not hang on it.
820
+ const publishingService = this.publishingService;
821
+ await executeTimeout(()=>publishingService.stop(), PUBLISHING_SERVICE_STOP_TIMEOUT_MS, 'prover-node publishing-service stop').catch((err)=>this.log.warn(`Timed out stopping proof publishing service`, err));
822
+ }
823
+ await this.checkpointStore.stop();
824
+ this.chonkCache.stop();
518
825
  await this.prover.stop();
519
826
  await tryStop(this.publisherFactory);
520
827
  this.rewardsMetrics.stop();
@@ -522,229 +829,200 @@ _dec = trackSpan('ProverNode.createProvingJob', (epochNumber)=>({
522
829
  await this.telemetryClient.stop();
523
830
  this.log.info('Stopped ProverNode');
524
831
  }
525
- /** Returns world state status. */ async getWorldStateSyncStatus() {
526
- const { syncSummary } = await this.worldState.status();
527
- return syncSummary;
528
- }
529
- /** Returns archiver status. */ getL2Tips() {
530
- return this.l2BlockSource.getL2Tips();
531
- }
532
832
  /**
533
- * Starts a proving process and returns immediately.
534
- */ async startProof(epochNumber) {
535
- await this.startProofIfNeeded(epochNumber, {
536
- skipEpochCheck: true
833
+ * Constructs the session manager. Extracted so subclasses (test harness) can swap the
834
+ * implementation. Wired to upload a post-mortem when a full session ends in its own genuine failure
835
+ * (`EpochSession.hasFailed()` — top-tree/submit failed with every prover healthy, so definitively not
836
+ * a prune). A `stopped` session (a prover under it failed) is not uploaded; it recovers on re-add.
837
+ */ createSessionManager(publishingService) {
838
+ return new SessionManager({
839
+ checkpointStore: this.checkpointStore,
840
+ l2BlockSource: this.l2BlockSource,
841
+ proverFactory: this.prover,
842
+ proverId: this.prover.getProverId(),
843
+ publishingService,
844
+ metrics: this.jobMetrics,
845
+ dateProvider: this.dateProvider,
846
+ config: {
847
+ maxPendingJobs: this.config.proverNodeMaxPendingJobs,
848
+ tickIntervalMs: this.config.proverNodePollingIntervalMs,
849
+ finalizationDelayMs: this.config.proverNodeEpochProvingDelayMs
850
+ },
851
+ onSessionFailed: async (session)=>{
852
+ await this.tryUploadEpochFailure(session.getId(), session.getCheckpoints());
853
+ },
854
+ bindings: this.log.getBindings()
537
855
  });
538
856
  }
539
- async startProofIfNeeded(epochNumber, opts = {}) {
540
- if (this.startingProofEpochs.has(epochNumber)) {
541
- this.log.warn(`Not starting proof for ${epochNumber} since a proof is already being started for the epoch`, {
542
- epochNumber
543
- });
544
- return false;
545
- }
546
- this.startingProofEpochs.add(epochNumber);
547
- try {
548
- const activeJobs = await this.activeJobsCoverEpoch(epochNumber);
549
- if (activeJobs.length > 0) {
550
- this.log.warn(`Not starting proof for ${epochNumber} since an active job already covers the epoch`, {
551
- epochNumber,
552
- activeJobs
553
- });
554
- return true;
555
- }
556
- await this.startProofInternal(epochNumber, opts);
557
- return true;
558
- } finally{
559
- this.startingProofEpochs.delete(epochNumber);
857
+ /**
858
+ * Installs session hooks for the e2e harness to interpose around top-tree proving
859
+ * (gate, override, or observe it) without monkey-patching the orchestrator factory.
860
+ * Applies to every session constructed after this call.
861
+ */ setSessionHooks(hooks) {
862
+ if (!this.sessionManager) {
863
+ throw new Error('ProverNode not started; call start() before setting session hooks.');
560
864
  }
865
+ this.sessionManager.setSessionHooks(hooks);
561
866
  }
562
- async startProofInternal(epochNumber, opts = {}) {
563
- const job = await this.createProvingJob(epochNumber, opts);
564
- void this.runJob(job);
867
+ /**
868
+ * Installs checkpoint-prover test hooks (e.g. forcing a sub-tree failure) applied to every
869
+ * CheckpointProver constructed after this call. For the e2e harness only.
870
+ */ setCheckpointHooks(hooks) {
871
+ this.checkpointStore.setTestHooks(hooks);
565
872
  }
566
- async runJob(job) {
567
- const epochNumber = job.getEpochNumber();
568
- const ctx = {
569
- id: job.getId(),
570
- epochNumber,
571
- state: undefined
572
- };
873
+ /**
874
+ * Uploads a post-mortem snapshot for an epoch whose full session failed to prove, built from that
875
+ * session's checkpoint provers. Fired from the session manager's `onSessionFailed` callback (a
876
+ * genuine, race-free failure). Exposed as a method so tests can spy on it. No-ops if no failed-epoch
877
+ * store is configured or the checkpoint set is empty.
878
+ */ async tryUploadEpochFailure(id, checkpoints) {
879
+ if (!this.config.proverNodeFailedEpochStore || checkpoints.length === 0) {
880
+ return undefined;
881
+ }
882
+ const data = SessionManager.buildProvingData(checkpoints);
883
+ return await uploadEpochProofFailure(this.config.proverNodeFailedEpochStore, // The session's own id; `uploadEpochProofFailure` already prefixes the path with the epoch number.
884
+ id, data, this.l2BlockSource, this.worldState, assertRequired(pick(this.config, 'l1ChainId', 'rollupVersion', 'dataDirectory')), this.log);
885
+ }
886
+ /**
887
+ * Uploads a post-mortem for a single failed checkpoint prover, built from just that checkpoint's
888
+ * proving data. Fired (fire-and-forget) from the store's `onFailed` callback for any non-cancel
889
+ * block-proof failure — a genuine sub-tree fault or a prune-induced fork fault alike. No-ops if no
890
+ * failed-epoch store is configured, or if the checkpoint is no longer canonical (a prune left nothing
891
+ * to diagnose). Swallows its own errors so a fire-and-forget caller can't leak.
892
+ */ async tryUploadCheckpointFailure(prover) {
893
+ if (!this.config.proverNodeFailedEpochStore) {
894
+ return undefined;
895
+ }
573
896
  try {
574
- await job.run();
575
- const state = job.getState();
576
- ctx.state = state;
577
- if (state === 'reorg') {
578
- this.log.warn(`Running new job for epoch ${epochNumber} due to reorg`, ctx);
579
- await this.createProvingJob(epochNumber);
580
- } else if (state === 'failed') {
581
- this.log.error(`Job for ${epochNumber} exited with state ${state}`, ctx);
582
- await this.tryUploadEpochFailure(job);
583
- } else {
584
- this.log.verbose(`Job for ${epochNumber} exited with state ${state}`, ctx);
897
+ // A prune-induced fork fault and a genuine sub-tree failure are indistinguishable at the moment the
898
+ // prover rejects (no control-plane cancel has landed yet). But the archiver is the authoritative
899
+ // committed chain: if this checkpoint was pruned out, its last block is no longer canonical there.
900
+ // Only upload for a checkpoint that still exists on-chain — a prune leaves nothing to diagnose, and
901
+ // the snapshot (full world-state + archiver) is expensive to produce and store.
902
+ if (!await this.isCheckpointCanonical(prover.checkpoint)) {
903
+ this.log.debug(`Skipping checkpoint-failure upload for ${prover.id}: no longer canonical (pruned)`, {
904
+ checkpointNumber: prover.checkpoint.number
905
+ });
906
+ return undefined;
585
907
  }
908
+ const data = SessionManager.buildProvingData([
909
+ prover
910
+ ]);
911
+ return await uploadEpochProofFailure(this.config.proverNodeFailedEpochStore, // The prover's content-addressed id; the epoch number is already in the upload path.
912
+ prover.id, data, this.l2BlockSource, this.worldState, assertRequired(pick(this.config, 'l1ChainId', 'rollupVersion', 'dataDirectory')), this.log);
586
913
  } catch (err) {
587
- this.log.error(`Error proving epoch ${epochNumber}`, err, ctx);
588
- } finally{
589
- this.jobs.delete(job.getId());
914
+ this.log.error(`Error uploading checkpoint failure for ${prover.id}`, err);
915
+ return undefined;
590
916
  }
591
917
  }
592
- async tryUploadEpochFailure(job) {
593
- if (this.config.proverNodeFailedEpochStore) {
594
- return await uploadEpochProofFailure(this.config.proverNodeFailedEpochStore, job.getId(), job.getProvingData(), this.l2BlockSource, this.worldState, assertRequired(pick(this.config, 'l1ChainId', 'rollupVersion', 'dataDirectory')), this.log);
918
+ // ---------------- helpers ----------------
919
+ /**
920
+ * True if the checkpoint still exists on the canonical chain: the archiver holds a block at its last
921
+ * block's height whose archive root matches. A prune (fork fault) leaves the block missing or replaced,
922
+ * so this returns false. Protected for direct unit-test access.
923
+ */ async isCheckpointCanonical(checkpoint) {
924
+ const lastBlock = checkpoint.blocks.at(-1);
925
+ if (!lastBlock) {
926
+ return false;
595
927
  }
928
+ const onChain = await this.l2BlockSource.getBlock({
929
+ number: lastBlock.number
930
+ });
931
+ return !!onChain && onChain.archive.root.equals(checkpoint.archive.root);
596
932
  }
597
- /**
598
- * Returns the prover instance.
599
- */ getProver() {
600
- return this.prover;
933
+ getL1Constants() {
934
+ return this.l2BlockSource.getL1Constants();
601
935
  }
602
936
  /**
603
- * Returns an array of jobs being processed.
604
- */ getJobs() {
605
- return Promise.resolve(this.getJobsInternal());
606
- }
607
- getJobsInternal() {
608
- return Array.from(this.jobs.entries()).map(([uuid, job])=>({
609
- uuid,
610
- status: job.getState(),
611
- epochNumber: job.getEpochNumber()
612
- }));
613
- }
614
- async activeJobsCoverEpoch(epochNumber) {
615
- const checkpoints = await this.l2BlockSource.getCheckpointsData({
616
- epoch: epochNumber
937
+ * Returns true if every block in the given epoch is proven on L1. An epoch is only
938
+ * fully proven when its *last* block is proven. Protected for direct unit-test access.
939
+ */ async isEpochFullyProven(epochNumber, l1Constants) {
940
+ const provenBlockNumber = await this.l2BlockSource.getBlockNumber({
941
+ tag: 'proven'
617
942
  });
618
- if (checkpoints.length === 0) {
619
- return [];
620
- }
621
- const firstCheckpoint = checkpoints.at(0).checkpointNumber;
622
- const latestCheckpoint = checkpoints.at(-1).checkpointNumber;
623
- const jobs = [];
624
- for (const job of this.jobs.values()){
625
- if (job.getEpochNumber() !== epochNumber) {
626
- continue;
627
- }
628
- const jobCheckpoints = job.getProvingData().checkpoints;
629
- const checkpointOverlap = jobCheckpoints.at(0).number <= firstCheckpoint && jobCheckpoints.at(-1).number >= latestCheckpoint;
630
- if (checkpointOverlap && ![
631
- 'failed',
632
- 'stopped',
633
- 'timed-out'
634
- ].includes(job.getState())) {
635
- jobs.push(job.getId());
636
- }
943
+ if (!provenBlockNumber || provenBlockNumber <= 0) {
944
+ return false;
637
945
  }
638
- return jobs;
639
- }
640
- checkMaximumPendingJobs() {
641
- const { proverNodeMaxPendingJobs: maxPendingJobs } = this.config;
642
- if (maxPendingJobs > 0 && this.jobs.size >= maxPendingJobs) {
643
- throw new Error(`Maximum pending proving jobs ${maxPendingJobs} reached. Cannot create new job.`);
644
- }
645
- }
646
- async createProvingJob(epochNumber, opts = {}) {
647
- this.checkMaximumPendingJobs();
648
- this.publisher = await this.publisherFactory.create();
649
- // Gather all data for this epoch
650
- const epochData = await this.gatherEpochData(epochNumber);
651
- const fromCheckpoint = epochData.checkpoints[0].number;
652
- const toCheckpoint = epochData.checkpoints.at(-1).number;
653
- const fromBlock = epochData.checkpoints[0].blocks[0].number;
654
- const lastBlock = epochData.checkpoints.at(-1).blocks.at(-1);
655
- const toBlock = lastBlock.number;
656
- this.log.verbose(`Creating proving job for epoch ${epochNumber} for checkpoint range ${fromCheckpoint} to ${toCheckpoint} and block range ${fromBlock} to ${toBlock}`);
657
- // Fast forward world state to right before the target block and get a fork
658
- const lastBlockHash = await lastBlock.header.hash();
659
- await this.worldState.syncImmediate(toBlock, lastBlockHash);
660
- // Create a processor factory
661
- const publicProcessorFactory = new PublicProcessorFactory(this.contractDataSource, this.dateProvider, this.telemetryClient, this.log.getBindings());
662
- // Set deadline for this job to run. It will abort if it takes too long.
663
- const deadlineTs = getProofSubmissionDeadlineTimestamp(epochNumber, await this.getL1Constants());
664
- const deadline = new Date(Number(deadlineTs) * 1000);
665
- const job = this.doCreateEpochProvingJob(epochData, deadline, publicProcessorFactory, this.publisher, opts);
666
- this.jobs.set(job.getId(), job);
667
- return job;
946
+ const provenHeader = (await this.l2BlockSource.getBlockData({
947
+ number: BlockNumber(provenBlockNumber)
948
+ }))?.header;
949
+ if (!provenHeader) {
950
+ return false;
951
+ }
952
+ const provenEpoch = getEpochAtSlot(provenHeader.getSlot(), l1Constants);
953
+ if (epochNumber < provenEpoch) {
954
+ return true;
955
+ }
956
+ if (epochNumber > provenEpoch) {
957
+ return false;
958
+ }
959
+ return this.isProvenBlockLastOfItsEpoch(BlockNumber(provenBlockNumber), provenEpoch, l1Constants);
668
960
  }
669
- getL1Constants() {
670
- return this.l2BlockSource.getL1Constants();
961
+ /** Protected for direct unit-test access. */ async isProvenBlockLastOfItsEpoch(provenBlockNumber, provenEpoch, l1Constants) {
962
+ const nextHeader = (await this.l2BlockSource.getBlockData({
963
+ number: BlockNumber(provenBlockNumber + 1)
964
+ }))?.header;
965
+ if (nextHeader) {
966
+ return getEpochAtSlot(nextHeader.getSlot(), l1Constants) > provenEpoch;
967
+ }
968
+ return this.l2BlockSource.isEpochComplete(provenEpoch);
671
969
  }
672
- async gatherEpochData(epochNumber) {
673
- const publishedCheckpoints = await this.l2BlockSource.getCheckpoints({
674
- epoch: epochNumber
970
+ /**
971
+ * Resolves the last fully-proven epoch from L1 proven state, used to seed the catch-up cursor (via
972
+ * `computeStartingCheckpoint`) and `lastExpiredEpoch`. The fully-proven epoch is `provenEpoch` when the
973
+ * proven tip is the last block of its epoch, otherwise `provenEpoch - 1`, or `undefined` if no block is
974
+ * proven yet (so a restart reprocesses the partially-proven epoch rather than trusting a stale tip).
975
+ */ async resolveLastFullyProvenEpoch() {
976
+ const provenBlockNumber = await this.l2BlockSource.getBlockNumber({
977
+ tag: 'proven'
675
978
  });
676
- if (publishedCheckpoints.length === 0) {
677
- throw new EmptyEpochError(epochNumber);
678
- }
679
- const checkpoints = publishedCheckpoints.map((p)=>p.checkpoint);
680
- const attestations = publishedCheckpoints.at(-1)?.attestations ?? [];
681
- const txArray = await this.gatherTxs(epochNumber, checkpoints);
682
- const txs = new Map(txArray.map((tx)=>[
683
- tx.getTxHash().toString(),
684
- tx
685
- ]));
686
- const l1ToL2Messages = await this.gatherMessages(epochNumber, checkpoints);
687
- const [firstBlock] = checkpoints[0].blocks;
688
- const previousBlockHeader = await this.gatherPreviousBlockHeader(epochNumber, firstBlock.number - 1);
979
+ if (!provenBlockNumber || provenBlockNumber <= 0) {
980
+ return {
981
+ lastFullyProvenEpoch: undefined
982
+ };
983
+ }
984
+ const l1Constants = await this.getL1Constants();
985
+ const provenHeader = (await this.l2BlockSource.getBlockData({
986
+ number: BlockNumber(provenBlockNumber)
987
+ }))?.header;
988
+ if (!provenHeader) {
989
+ return {
990
+ lastFullyProvenEpoch: undefined
991
+ };
992
+ }
993
+ const provenEpoch = getEpochAtSlot(provenHeader.getSlot(), l1Constants);
994
+ if (await this.isProvenBlockLastOfItsEpoch(BlockNumber(provenBlockNumber), provenEpoch, l1Constants)) {
995
+ return {
996
+ lastFullyProvenEpoch: provenEpoch
997
+ };
998
+ }
999
+ const lastFullyProvenEpoch = provenEpoch > 0 ? EpochNumber(provenEpoch - 1) : undefined;
689
1000
  return {
690
- checkpoints,
691
- txs,
692
- l1ToL2Messages,
693
- epochNumber,
694
- previousBlockHeader,
695
- attestations
1001
+ lastFullyProvenEpoch
696
1002
  };
697
1003
  }
698
- async gatherTxs(epochNumber, checkpoints) {
699
- const deadline = new Date(this.dateProvider.now() + this.config.txGatheringTimeoutMs);
700
- const txProvider = this.p2pClient.getTxProvider();
701
- const blocks = checkpoints.flatMap((checkpoint)=>checkpoint.blocks);
702
- const txsByBlock = await Promise.all(blocks.map((block)=>txProvider.getTxsForBlock(block, {
703
- deadline
704
- })));
705
- const txs = txsByBlock.map(({ txs })=>txs).flat();
706
- const missingTxs = txsByBlock.map(({ missingTxs })=>missingTxs).flat();
707
- if (missingTxs.length === 0) {
708
- this.log.verbose(`Gathered all ${txs.length} txs for epoch ${epochNumber}`, {
709
- epochNumber
710
- });
711
- return txs;
1004
+ /**
1005
+ * Resolves the catch-up cursor seed: the last checkpoint of the last fully-proven epoch, or 0 if none. Seeding
1006
+ * from a checkpoint (rather than a checkpointed tip) guarantees a restart reprocesses every checkpoint of the
1007
+ * partially-proven epoch, since the checkpointed tip can sit ahead of the last fully-proven checkpoint.
1008
+ */ async computeStartingCheckpoint(lastFullyProvenEpoch) {
1009
+ if (lastFullyProvenEpoch === undefined) {
1010
+ return CheckpointNumber.ZERO;
712
1011
  }
713
- throw new Error(`Txs not found for epoch ${epochNumber}: ${missingTxs.map((hash)=>hash.toString()).join(', ')}`);
714
- }
715
- async gatherMessages(epochNumber, checkpoints) {
716
- const messages = await Promise.all(checkpoints.map((c)=>this.l1ToL2MessageSource.getL1ToL2Messages(c.number)));
717
- const messageCount = sum(messages.map((m)=>m.length));
718
- this.log.verbose(`Gathered all ${messageCount} messages for epoch ${epochNumber}`, {
719
- epochNumber
1012
+ const checkpoints = await this.l2BlockSource.getCheckpointsData({
1013
+ epoch: lastFullyProvenEpoch
720
1014
  });
721
- const messagesByCheckpoint = {};
722
- for(let i = 0; i < checkpoints.length; i++){
723
- messagesByCheckpoint[checkpoints[i].number] = messages[i];
724
- }
725
- return messagesByCheckpoint;
1015
+ return checkpoints.at(-1)?.checkpointNumber ?? CheckpointNumber.ZERO;
726
1016
  }
727
- async gatherPreviousBlockHeader(epochNumber, previousBlockNumber) {
1017
+ async gatherPreviousBlockHeader(previousBlockNumber) {
728
1018
  const data = await this.l2BlockSource.getBlockData({
729
1019
  number: BlockNumber(previousBlockNumber)
730
1020
  });
731
1021
  if (!data?.header) {
732
- throw new Error(`Previous block header ${previousBlockNumber} not found for proving epoch ${epochNumber}`);
1022
+ throw new Error(`Previous block header ${previousBlockNumber} not found`);
733
1023
  }
734
- this.log.verbose(`Gathered previous block header ${data.header.getBlockNumber()} for epoch ${epochNumber}`);
735
1024
  return data.header;
736
1025
  }
737
- /** Extracted for testing purposes. */ doCreateEpochProvingJob(data, deadline, publicProcessorFactory, publisher, opts = {}) {
738
- const { proverNodeMaxParallelBlocksPerEpoch: parallelBlockLimit, proverNodeDisableProofPublish } = this.config;
739
- return new EpochProvingJob(data, this.worldState, this.prover.createEpochProver(), publicProcessorFactory, publisher, this.l2BlockSource, this.jobMetrics, deadline, {
740
- parallelBlockLimit,
741
- skipSubmitProof: proverNodeDisableProofPublish,
742
- ...opts
743
- }, this.log.getBindings());
744
- }
745
- /** Extracted for testing purposes. */ async triggerMonitors() {
746
- await this.epochsMonitor.work();
747
- }
748
1026
  validateConfig() {
749
1027
  if (this.config.proverNodeFailedEpochStore && (!this.config.dataDirectory || !this.config.l1ChainId || this.config.rollupVersion === undefined)) {
750
1028
  this.log.warn(`Invalid prover-node config (missing dataDirectory, l1ChainId, or rollupVersion)`, pick(this.config, 'proverNodeFailedEpochStore', 'dataDirectory', 'l1ChainId', 'rollupVersion'));
@@ -752,9 +1030,5 @@ _dec = trackSpan('ProverNode.createProvingJob', (epochNumber)=>({
752
1030
  }
753
1031
  }
754
1032
  }
755
- class EmptyEpochError extends Error {
756
- constructor(epochNumber){
757
- super(`No blocks found for epoch ${epochNumber}`);
758
- this.name = 'EmptyEpochError';
759
- }
760
- }
1033
+ // Re-export so handlers can compare states externally.
1034
+ export { EpochProvingJobTerminalState };