@aztec/prover-node 0.0.1-commit.f2ce05ee → 0.0.1-commit.f5a9928

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 (75) hide show
  1. package/README.md +572 -0
  2. package/dest/actions/download-epoch-proving-job.js +1 -1
  3. package/dest/actions/rerun-epoch-proving-job.d.ts +14 -4
  4. package/dest/actions/rerun-epoch-proving-job.d.ts.map +1 -1
  5. package/dest/actions/rerun-epoch-proving-job.js +146 -24
  6. package/dest/actions/upload-epoch-proof-failure.d.ts +2 -2
  7. package/dest/actions/upload-epoch-proof-failure.d.ts.map +1 -1
  8. package/dest/bin/run-failed-epoch.js +6 -5
  9. package/dest/checkpoint-store.d.ts +95 -0
  10. package/dest/checkpoint-store.d.ts.map +1 -0
  11. package/dest/checkpoint-store.js +178 -0
  12. package/dest/config.d.ts +7 -8
  13. package/dest/config.d.ts.map +1 -1
  14. package/dest/config.js +24 -20
  15. package/dest/factory.d.ts +19 -13
  16. package/dest/factory.d.ts.map +1 -1
  17. package/dest/factory.js +41 -65
  18. package/dest/index.d.ts +2 -1
  19. package/dest/index.d.ts.map +1 -1
  20. package/dest/index.js +1 -0
  21. package/dest/job/checkpoint-prover.d.ts +165 -0
  22. package/dest/job/checkpoint-prover.d.ts.map +1 -0
  23. package/dest/job/checkpoint-prover.js +405 -0
  24. package/dest/job/epoch-session.d.ts +160 -0
  25. package/dest/job/epoch-session.d.ts.map +1 -0
  26. package/dest/job/{epoch-proving-job.js → epoch-session.js} +301 -298
  27. package/dest/job/top-tree-job.d.ts +82 -0
  28. package/dest/job/top-tree-job.d.ts.map +1 -0
  29. package/dest/job/top-tree-job.js +152 -0
  30. package/dest/metrics.d.ts +40 -3
  31. package/dest/metrics.d.ts.map +1 -1
  32. package/dest/metrics.js +101 -4
  33. package/dest/monitors/epoch-monitor.d.ts +1 -1
  34. package/dest/monitors/epoch-monitor.d.ts.map +1 -1
  35. package/dest/monitors/epoch-monitor.js +11 -9
  36. package/dest/proof-publishing-service.d.ts +161 -0
  37. package/dest/proof-publishing-service.d.ts.map +1 -0
  38. package/dest/proof-publishing-service.js +335 -0
  39. package/dest/prover-node-publisher.d.ts +25 -15
  40. package/dest/prover-node-publisher.d.ts.map +1 -1
  41. package/dest/prover-node-publisher.js +201 -62
  42. package/dest/prover-node.d.ts +144 -69
  43. package/dest/prover-node.d.ts.map +1 -1
  44. package/dest/prover-node.js +543 -221
  45. package/dest/prover-publisher-factory.d.ts +6 -4
  46. package/dest/prover-publisher-factory.d.ts.map +1 -1
  47. package/dest/prover-publisher-factory.js +4 -3
  48. package/dest/session-manager.d.ts +158 -0
  49. package/dest/session-manager.d.ts.map +1 -0
  50. package/dest/session-manager.js +492 -0
  51. package/dest/test/index.d.ts +7 -6
  52. package/dest/test/index.d.ts.map +1 -1
  53. package/package.json +23 -22
  54. package/src/actions/download-epoch-proving-job.ts +1 -1
  55. package/src/actions/rerun-epoch-proving-job.ts +178 -33
  56. package/src/actions/upload-epoch-proof-failure.ts +1 -1
  57. package/src/bin/run-failed-epoch.ts +5 -3
  58. package/src/checkpoint-store.ts +212 -0
  59. package/src/config.ts +35 -32
  60. package/src/factory.ts +69 -111
  61. package/src/index.ts +1 -0
  62. package/src/job/checkpoint-prover.ts +538 -0
  63. package/src/job/epoch-session.ts +462 -0
  64. package/src/job/top-tree-job.ts +227 -0
  65. package/src/metrics.ts +123 -10
  66. package/src/monitors/epoch-monitor.ts +5 -6
  67. package/src/proof-publishing-service.ts +427 -0
  68. package/src/prover-node-publisher.ts +236 -78
  69. package/src/prover-node.ts +628 -247
  70. package/src/prover-publisher-factory.ts +8 -5
  71. package/src/session-manager.ts +592 -0
  72. package/src/test/index.ts +6 -6
  73. package/dest/job/epoch-proving-job.d.ts +0 -63
  74. package/dest/job/epoch-proving-job.d.ts.map +0 -1
  75. package/src/job/epoch-proving-job.ts +0 -435
@@ -371,371 +371,374 @@ function _apply_decs_2203_r(targetClass, memberDecs, classDecs, parentClass) {
371
371
  return (_apply_decs_2203_r = applyDecs2203RFactory())(targetClass, memberDecs, classDecs, parentClass);
372
372
  }
373
373
  var _dec, _initProto;
374
- import { NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP } from '@aztec/constants';
375
- import { asyncPool } from '@aztec/foundation/async-pool';
376
374
  import { BlockNumber } from '@aztec/foundation/branded-types';
377
- import { padArrayEnd } from '@aztec/foundation/collection';
378
- import { Fr } from '@aztec/foundation/curves/bn254';
379
375
  import { createLogger } from '@aztec/foundation/log';
380
- import { RunningPromise, promiseWithResolvers } from '@aztec/foundation/promise';
376
+ import { sleep } from '@aztec/foundation/sleep';
381
377
  import { Timer } from '@aztec/foundation/timer';
382
- import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree';
383
- import { protocolContractsHash } from '@aztec/protocol-contracts';
384
- import { buildFinalBlobChallenges } from '@aztec/prover-client/helpers';
385
- import { PublicSimulatorConfig } from '@aztec/stdlib/avm';
378
+ import { TopTreeCancelledError } from '@aztec/prover-client/orchestrator';
386
379
  import { EpochProvingJobTerminalState } from '@aztec/stdlib/interfaces/server';
387
- import { CheckpointConstantData } from '@aztec/stdlib/rollup';
388
- import { MerkleTreeId } from '@aztec/stdlib/trees';
389
380
  import { Attributes, trackSpan } from '@aztec/telemetry-client';
390
381
  import * as crypto from 'node:crypto';
391
- import { validateEpochProvingJobData } from './epoch-proving-job-data.js';
392
- _dec = trackSpan('EpochProvingJob.run', function() {
382
+ import { TopTreeJob } from './top-tree-job.js';
383
+ /** Stable string key for use in maps. */ export function specKey(spec) {
384
+ return `${spec.kind}:${spec.epochNumber}:${spec.fromSlot}-${spec.toSlot}`;
385
+ }
386
+ _dec = trackSpan('EpochSession.start', function() {
393
387
  return {
394
- [Attributes.EPOCH_NUMBER]: this.data.epochNumber
388
+ [Attributes.EPOCH_NUMBER]: this.spec.epochNumber
395
389
  };
396
390
  });
397
391
  /**
398
- * Job that grabs a range of blocks from the unfinalized chain from L1, gets their txs given their hashes,
399
- * re-executes their public calls, generates a rollup proof, and submits it to L1. This job will update the
400
- * world state as part of public call execution via the public processor.
401
- */ export class EpochProvingJob {
402
- data;
403
- dbProvider;
404
- prover;
405
- publicProcessorFactory;
406
- publisher;
407
- l2BlockSource;
408
- metrics;
409
- deadline;
410
- config;
392
+ * One attempt at proving and publishing a contiguous slot range. The `SessionSpec` and
393
+ * the prover set are both frozen at construction time; the session does not adapt to
394
+ * reorgs or extensions of canonical content. Instead, `SessionManager` owns the
395
+ * reconciliation loop and replaces invalidated sessions wholesale (cancel + construct
396
+ * a fresh session with the new prover set).
397
+ *
398
+ * Each session does three things in sequence:
399
+ *
400
+ * 1. Run a `TopTreeJob` over its frozen prover subset to produce the epoch proof.
401
+ * 2. Hand the proof to the shared `ProofPublishingService` as a `PublishCandidate`.
402
+ * 3. Translate the service's outcome into a terminal session state.
403
+ *
404
+ * Everything to do with submission — predecessor gating, same-epoch dedup, deadline
405
+ * enforcement, and the L1 transaction itself — is the publishing service's concern.
406
+ * The session is just the producer of one candidate and the observer of its outcome.
407
+ *
408
+ * Lifecycle (happy path):
409
+ *
410
+ * initialized → awaiting-checkpoints → awaiting-root → publishing-proof → completed
411
+ *
412
+ * Terminal states map the publishing outcome: `published` → `completed`, `superseded` →
413
+ * `superseded`, `expired` → `timed-out`, `withdrawn` → `cancelled`. A fault ends the attempt in one
414
+ * of two terminal states depending on its cause: `stopped` if a checkpoint prover under it failed
415
+ * (possibly a prune — the reconciler will rebuild over a fresh prover on re-add), or `failed` if the
416
+ * session's own top-tree/submit work failed while every prover was healthy (a genuine, non-prune
417
+ * failure the reconciler retains and uploads — see `hasFailed()`).
418
+ * Additionally, the session-level deadline fires `cancel('deadline')` and transitions
419
+ * to `timed-out` for the pre-submit window (top-tree proving) — the publishing service
420
+ * handles the post-submit window via the candidate's `deadline`.
421
+ *
422
+ * `cancel()` is idempotent. It marks the session terminal, calls
423
+ * `publishingService.withdraw(uuid)` to drop any queued candidate (an in-flight publish
424
+ * runs to natural completion; the session has already settled), and tears down the
425
+ * top-tree job if proving is still in progress.
426
+ */ export class EpochSession {
427
+ spec;
428
+ deps;
411
429
  static{
412
430
  ({ e: [_initProto] } = _apply_decs_2203_r(this, [
413
431
  [
414
432
  _dec,
415
433
  2,
416
- "run"
434
+ "start"
417
435
  ]
418
436
  ], []));
419
437
  }
420
- state;
421
- log;
438
+ tracer;
422
439
  uuid;
423
- runPromise;
424
- epochCheckPromise;
440
+ log;
441
+ state;
425
442
  deadlineTimeoutHandler;
426
- tracer;
427
- constructor(data, dbProvider, prover, publicProcessorFactory, publisher, l2BlockSource, metrics, deadline, config, bindings){
428
- this.data = data;
429
- this.dbProvider = dbProvider;
430
- this.prover = prover;
431
- this.publicProcessorFactory = publicProcessorFactory;
432
- this.publisher = publisher;
433
- this.l2BlockSource = l2BlockSource;
434
- this.metrics = metrics;
435
- this.deadline = deadline;
436
- this.config = config;
443
+ topTreeJob;
444
+ /** Cancelled top-tree jobs whose teardown is still in flight. Awaited at session stop. */ pendingTopTreeCleanups;
445
+ completionPromise;
446
+ resolveCompletion;
447
+ /** Stable reference; never mutated after construction. */ checkpoints;
448
+ constructor(spec, checkpoints, deps){
449
+ this.spec = spec;
450
+ this.deps = deps;
437
451
  this.state = (_initProto(this), 'initialized');
438
- validateEpochProvingJobData(data);
452
+ this.pendingTopTreeCleanups = [];
453
+ if (checkpoints.length === 0) {
454
+ throw new Error(`Cannot construct EpochSession for ${specKey(spec)}: empty checkpoint set`);
455
+ }
456
+ this.checkpoints = [
457
+ ...checkpoints
458
+ ];
439
459
  this.uuid = crypto.randomUUID();
440
- this.log = createLogger('prover-node:epoch-proving-job', {
441
- ...bindings,
442
- instanceId: `epoch-${data.epochNumber}`
460
+ this.log = createLogger('prover-node:epoch-session', {
461
+ ...deps.bindings,
462
+ instanceId: `session-${spec.kind}-${spec.epochNumber}-${spec.fromSlot}-${spec.toSlot}`
463
+ });
464
+ this.tracer = deps.metrics.tracer;
465
+ this.completionPromise = new Promise((resolve)=>{
466
+ this.resolveCompletion = resolve;
467
+ });
468
+ this.scheduleDeadlineStop();
469
+ this.log.info(`Created EpochSession ${this.uuid}`, {
470
+ uuid: this.uuid,
471
+ ...spec,
472
+ checkpointCount: this.checkpoints.length,
473
+ checkpointIds: this.checkpoints.map((c)=>c.id)
443
474
  });
444
- this.tracer = metrics.tracer;
445
475
  }
446
476
  getId() {
447
477
  return this.uuid;
448
478
  }
479
+ getSpec() {
480
+ return this.spec;
481
+ }
449
482
  getState() {
450
483
  return this.state;
451
484
  }
452
485
  getEpochNumber() {
453
- return this.data.epochNumber;
486
+ return this.spec.epochNumber;
487
+ }
488
+ getKind() {
489
+ return this.spec.kind;
454
490
  }
455
491
  getDeadline() {
456
- return this.deadline;
492
+ return this.deps.deadline;
493
+ }
494
+ getCheckpoints() {
495
+ return this.checkpoints;
457
496
  }
458
- getProvingData() {
459
- return this.data;
497
+ /** Resolves when the session reaches a terminal state. */ whenDone() {
498
+ return this.completionPromise;
460
499
  }
461
- get epochNumber() {
462
- return this.data.epochNumber;
500
+ /** True if the session is in a terminal state. */ isTerminal() {
501
+ return EpochProvingJobTerminalState.includes(this.state);
463
502
  }
464
- get checkpoints() {
465
- return this.data.checkpoints;
503
+ /**
504
+ * True if the session ended in its own genuine failure — top-tree proving or L1 submission failed
505
+ * while every checkpoint prover succeeded. Because healthy provers rule out a prune-induced fault,
506
+ * this is a race-free "the epoch could not be proven" signal: the reconciler retains such a (full)
507
+ * session rather than re-proving it, and uploads a post-mortem. A `stopped` session (a checkpoint
508
+ * prover failed under it) is NOT a session failure in this sense.
509
+ */ hasFailed() {
510
+ return this.state === 'failed';
466
511
  }
467
- get txs() {
468
- return this.data.txs;
512
+ /** First block this session proves. */ getStartBlockNumber() {
513
+ return BlockNumber(this.checkpoints[0].checkpoint.blocks[0].number);
469
514
  }
470
- get attestations() {
471
- return this.data.attestations;
515
+ /** Last block this session proves. */ getEndBlockNumber() {
516
+ const lastCheckpoint = this.checkpoints[this.checkpoints.length - 1];
517
+ return BlockNumber(lastCheckpoint.checkpoint.blocks[lastCheckpoint.checkpoint.blocks.length - 1].number);
472
518
  }
473
519
  /**
474
- * Proves the given epoch and submits the proof to L1.
475
- */ async run() {
476
- this.scheduleDeadlineStop();
477
- if (!this.config.skipEpochCheck) {
478
- await this.scheduleEpochCheck();
479
- }
480
- const attestations = this.attestations.map((attestation)=>attestation.toViem());
481
- const epochNumber = this.epochNumber;
482
- const epochSizeCheckpoints = this.checkpoints.length;
483
- const epochSizeBlocks = this.checkpoints.reduce((accum, checkpoint)=>accum + checkpoint.blocks.length, 0);
484
- const epochSizeTxs = this.checkpoints.reduce((accum, checkpoint)=>accum + checkpoint.blocks.reduce((accumC, block)=>accumC + block.body.txEffects.length, 0), 0);
485
- const fromCheckpoint = this.checkpoints[0].number;
486
- const toCheckpoint = this.checkpoints.at(-1).number;
487
- const fromBlock = this.checkpoints[0].blocks[0].number;
488
- const toBlock = this.checkpoints.at(-1).blocks.at(-1).number;
489
- this.log.info(`Starting epoch ${epochNumber} proving job with checkpoints ${fromCheckpoint} to ${toCheckpoint}`, {
490
- fromBlock,
491
- toBlock,
492
- epochSizeTxs,
493
- epochNumber,
494
- uuid: this.uuid
495
- });
496
- this.progressState('processing');
497
- const timer = new Timer();
498
- const { promise, resolve } = promiseWithResolvers();
499
- this.runPromise = promise;
520
+ * Kicks off proving + submission. Fires and forgets callers should await `whenDone()`.
521
+ * Returns a promise that resolves to the final state for callers that want to wait inline.
522
+ */ async start() {
500
523
  try {
501
- const blobFieldsPerCheckpoint = this.checkpoints.map((checkpoint)=>checkpoint.toBlobFields());
502
- const finalBlobBatchingChallenges = await buildFinalBlobChallenges(blobFieldsPerCheckpoint);
503
- this.prover.startNewEpoch(epochNumber, epochSizeCheckpoints, finalBlobBatchingChallenges);
504
- await this.prover.startChonkVerifierCircuits(Array.from(this.txs.values()));
505
- // Everything in the epoch should have the same chainId and version.
506
- const { chainId, version } = this.checkpoints[0].blocks[0].header.globalVariables;
507
- const previousBlockHeaders = this.gatherPreviousBlockHeaders();
508
- await asyncPool(this.config.parallelBlockLimit ?? 32, this.checkpoints, async (checkpoint)=>{
509
- this.checkState();
510
- const checkpointIndex = checkpoint.number - fromCheckpoint;
511
- const checkpointConstants = CheckpointConstantData.from({
512
- chainId,
513
- version,
514
- vkTreeRoot: getVKTreeRoot(),
515
- protocolContractsHash: protocolContractsHash,
516
- proverId: this.prover.getProverId().toField(),
517
- slotNumber: checkpoint.header.slotNumber,
518
- coinbase: checkpoint.header.coinbase,
519
- feeRecipient: checkpoint.header.feeRecipient,
520
- gasFees: checkpoint.header.gasFees
521
- });
522
- const previousHeader = previousBlockHeaders[checkpointIndex];
523
- const l1ToL2Messages = this.getL1ToL2Messages(checkpoint);
524
- this.log.verbose(`Starting processing checkpoint ${checkpoint.number}`, {
525
- number: checkpoint.number,
526
- checkpointHash: checkpoint.hash().toString(),
527
- lastArchive: checkpoint.header.lastArchiveRoot,
528
- previousHeader: previousHeader.hash(),
529
- uuid: this.uuid
530
- });
531
- await this.prover.startNewCheckpoint(checkpointIndex, checkpointConstants, l1ToL2Messages, checkpoint.blocks.length, previousHeader);
532
- for (const block of checkpoint.blocks){
533
- const globalVariables = block.header.globalVariables;
534
- const txs = this.getTxs(block);
535
- this.log.verbose(`Starting processing block ${block.number}`, {
536
- number: block.number,
537
- blockHash: (await block.hash()).toString(),
538
- lastArchive: block.header.lastArchive.root,
539
- noteHashTreeRoot: block.header.state.partial.noteHashTree.root,
540
- nullifierTreeRoot: block.header.state.partial.nullifierTree.root,
541
- publicDataTreeRoot: block.header.state.partial.publicDataTree.root,
542
- ...globalVariables,
543
- numTxs: txs.length
544
- });
545
- // Start block proving
546
- await this.prover.startNewBlock(block.number, globalVariables.timestamp, txs.length);
547
- // Process public fns
548
- const db = await this.createFork(BlockNumber(block.number - 1), l1ToL2Messages);
549
- const config = PublicSimulatorConfig.from({
550
- proverId: this.prover.getProverId().toField(),
551
- skipFeeEnforcement: false,
552
- collectDebugLogs: false,
553
- collectHints: true,
554
- collectPublicInputs: true,
555
- collectStatistics: false
556
- });
557
- const publicProcessor = this.publicProcessorFactory.create(db, globalVariables, config);
558
- const processed = await this.processTxs(publicProcessor, txs);
559
- await this.prover.addTxs(processed);
560
- await db.close();
561
- this.log.verbose(`Processed all ${txs.length} txs for block ${block.number}`, {
562
- blockNumber: block.number,
563
- blockHash: (await block.hash()).toString(),
564
- uuid: this.uuid
565
- });
566
- // Mark block as completed to pad it
567
- const expectedBlockHeader = block.header;
568
- await this.prover.setBlockCompleted(block.number, expectedBlockHeader);
569
- }
570
- });
571
- const executionTime = timer.ms();
572
- this.progressState('awaiting-prover');
573
- const { publicInputs, proof, batchedBlobInputs } = await this.prover.finalizeEpoch();
574
- this.log.info(`Finalized proof for epoch ${epochNumber}`, {
575
- epochNumber,
576
- uuid: this.uuid,
577
- duration: timer.ms()
578
- });
579
- this.progressState('publishing-proof');
580
- if (this.config.skipSubmitProof) {
581
- this.log.info(`Proof publishing is disabled. Dropping valid proof for epoch ${epochNumber} (checkpoints ${fromCheckpoint} to ${toCheckpoint})`);
582
- this.state = 'completed';
583
- this.metrics.recordProvingJob(executionTime, timer.ms(), epochSizeCheckpoints, epochSizeBlocks, epochSizeTxs);
584
- return;
585
- }
586
- const success = await this.publisher.submitEpochProof({
587
- fromCheckpoint,
588
- toCheckpoint,
589
- epochNumber,
590
- publicInputs,
591
- proof,
592
- batchedBlobInputs,
593
- attestations
594
- });
595
- if (!success) {
596
- throw new Error('Failed to submit epoch proof to L1');
597
- }
598
- this.log.info(`Submitted proof for epoch ${epochNumber} (checkpoints ${fromCheckpoint} to ${toCheckpoint})`, {
599
- epochNumber,
600
- uuid: this.uuid
601
- });
602
- this.state = 'completed';
603
- this.metrics.recordProvingJob(executionTime, timer.ms(), epochSizeCheckpoints, epochSizeBlocks, epochSizeTxs);
524
+ await this.run();
604
525
  } catch (err) {
605
- if (err && err.name === 'HaltExecutionError') {
606
- this.log.warn(`Halted execution of epoch ${epochNumber} prover job`, {
607
- uuid: this.uuid,
608
- epochNumber,
609
- details: err.message
610
- });
611
- return;
612
- }
613
- this.log.error(`Error running epoch ${epochNumber} prover job`, err, {
526
+ this.log.error(`Error in EpochSession ${this.uuid}`, err, {
614
527
  uuid: this.uuid,
615
- epochNumber
528
+ ...this.spec
616
529
  });
617
- if (this.state === 'processing' || this.state === 'awaiting-prover' || this.state === 'publishing-proof') {
618
- this.state = 'failed';
530
+ // Distinguish the two ways an attempt can fault:
531
+ // - a checkpoint prover in the set has failed OR was cancelled (a sub-tree fault, a prune-induced
532
+ // fork fault, or a control-plane cancel that reached this catch before the reconcile marked the
533
+ // session 'cancelled'): end in the non-declaring terminal 'stopped'. This is not the session's own
534
+ // failure and may be a prune, so the reconciler does not upload it; a re-add installs a fresh prover.
535
+ // - no prover failed or was cancelled, yet top-tree proving or L1 submission failed: this is the
536
+ // session's own, genuine failure — and, because every prover is healthy and un-cancelled, it is
537
+ // definitively NOT a prune. End in terminal 'failed' so the reconciler retains it (no pointless
538
+ // re-prove) and uploads a race-free post-mortem.
539
+ if (!this.isTerminal()) {
540
+ this.state = this.checkpoints.some((c)=>c.isFailed() || c.isCancelled()) ? 'stopped' : 'failed';
619
541
  }
620
542
  } finally{
621
543
  clearTimeout(this.deadlineTimeoutHandler);
622
- await this.epochCheckPromise?.stop();
623
- await this.prover.stop();
624
- resolve();
544
+ await this.teardownTopTreeIfNeeded();
545
+ this.resolveCompletion(this.state);
625
546
  }
547
+ return this.state;
626
548
  }
627
549
  /**
628
- * Create a new db fork for tx processing, inserting all L1 to L2.
629
- * REFACTOR: The prover already spawns a db fork of its own for each block, so we may be able to do away with just one fork.
630
- */ async createFork(blockNumber, l1ToL2Messages) {
631
- const db = await this.dbProvider.fork(blockNumber);
632
- const l1ToL2MessagesPadded = padArrayEnd(l1ToL2Messages, Fr.ZERO, NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, 'Too many L1 to L2 messages');
633
- this.log.verbose(`Creating fork at ${blockNumber} with ${l1ToL2Messages.length} L1 to L2 messages`, {
634
- blockNumber,
635
- l1ToL2Messages: l1ToL2Messages.map((m)=>m.toString())
550
+ * Cancels the session. Idempotent. Withdraws any submitted candidate from the
551
+ * publishing service so the in-flight publisher (if any) is interrupted.
552
+ */ async cancel(reason = 'cancelled', { abortJobs = true } = {}) {
553
+ if (this.isTerminal()) {
554
+ return;
555
+ }
556
+ this.log.info(`Cancelling EpochSession ${this.uuid}: ${reason}`, {
557
+ uuid: this.uuid,
558
+ ...this.spec,
559
+ previousState: this.state,
560
+ reason
636
561
  });
637
- await db.appendLeaves(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, l1ToL2MessagesPadded);
638
- return db;
639
- }
640
- progressState(state) {
641
- this.checkState();
642
- this.state = state;
643
- }
644
- checkState() {
645
- if (this.state === 'timed-out' || this.state === 'stopped' || this.state === 'failed' || this.state === 'reorg') {
646
- throw new HaltExecutionError(this.state);
562
+ this.state = 'cancelled';
563
+ try {
564
+ this.deps.publishingService.withdraw(this.uuid);
565
+ } catch (err) {
566
+ this.log.error(`Error withdrawing candidate from publishing service`, err);
647
567
  }
648
- }
649
- async stop(state = 'stopped') {
650
- this.state = state;
651
- this.prover.cancel();
652
- if (this.runPromise) {
653
- await this.runPromise;
568
+ if (this.topTreeJob && !this.topTreeJob.isCancelled()) {
569
+ const job = this.topTreeJob;
570
+ this.topTreeJob = undefined;
571
+ // On a clean shutdown we leave the in-flight broker jobs alone so a restart can reuse them;
572
+ // other cancellations (reorg, supersede, deadline) abort them since their inputs are stale.
573
+ job.cancel(abortJobs);
574
+ this.pendingTopTreeCleanups.push(job);
654
575
  }
576
+ await this.teardownTopTreeIfNeeded();
577
+ this.resolveCompletion(this.state);
655
578
  }
656
- scheduleDeadlineStop() {
657
- const deadline = this.deadline;
658
- if (deadline) {
659
- const timeout = deadline.getTime() - Date.now();
660
- if (timeout <= 0) {
661
- throw new Error('Cannot start job with deadline in the past');
579
+ async run() {
580
+ const timer = new Timer();
581
+ if (this.deps.config.finalizationDelayMs && this.deps.config.finalizationDelayMs > 0) {
582
+ this.log.warn(`Waiting ${this.deps.config.finalizationDelayMs}ms before starting top-tree proving`, {
583
+ uuid: this.uuid,
584
+ ...this.spec
585
+ });
586
+ await sleep(this.deps.config.finalizationDelayMs);
587
+ if (this.isTerminal()) {
588
+ return;
662
589
  }
663
- this.deadlineTimeoutHandler = setTimeout(()=>{
664
- if (EpochProvingJobTerminalState.includes(this.state)) {
665
- return;
666
- }
667
- this.log.warn('Stopping job due to deadline hit', {
590
+ }
591
+ // Stage 1 — top-tree proving.
592
+ const topTreeJob = new TopTreeJob(this.spec.epochNumber, this.checkpoints, {
593
+ proverFactory: this.deps.proverFactory,
594
+ metrics: this.deps.metrics,
595
+ log: this.log,
596
+ hooks: this.toTopTreeHooks()
597
+ });
598
+ this.topTreeJob = topTreeJob;
599
+ const { fromCheckpoint, toCheckpoint, count } = topTreeJob.getRange();
600
+ this.state = 'awaiting-checkpoints';
601
+ let proof;
602
+ try {
603
+ proof = await topTreeJob.start();
604
+ } catch (err) {
605
+ if (err instanceof TopTreeCancelledError) {
606
+ // Session cancel kicked off the underlying teardown; nothing more to do here.
607
+ this.log.info(`Top-tree cancelled for EpochSession ${this.uuid}`, {
668
608
  uuid: this.uuid,
669
- epochNumber: this.epochNumber
670
- });
671
- this.stop('timed-out').catch((err)=>{
672
- this.log.error('Error stopping job', err, {
673
- uuid: this.uuid,
674
- epochNumber: this.epochNumber
675
- });
609
+ ...this.spec
676
610
  });
677
- }, timeout);
611
+ return;
612
+ }
613
+ throw err;
678
614
  }
679
- }
680
- /**
681
- * Kicks off a running promise that queries the archiver for the set of L2 blocks of the current epoch.
682
- * If those change, stops the proving job with a `rerun` state, so the node re-enqueues it.
683
- */ async scheduleEpochCheck() {
684
- const l2BlockSource = this.l2BlockSource;
685
- if (!l2BlockSource) {
686
- this.log.warn(`No L2 block source available, skipping epoch check`);
615
+ this.topTreeJob = undefined;
616
+ this.log.info(`Top-tree proof ready for EpochSession ${this.uuid}`, {
617
+ uuid: this.uuid,
618
+ ...this.spec,
619
+ fromCheckpoint,
620
+ toCheckpoint,
621
+ durationMs: timer.ms()
622
+ });
623
+ // Stage 2 — hand the proof to the publishing service and wait for its verdict.
624
+ await this.submitProof(proof, fromCheckpoint, toCheckpoint, count, timer);
625
+ }
626
+ async submitProof(proof, fromCheckpoint, toCheckpoint, checkpointCount, timer) {
627
+ // Attestations come from the highest-numbered registered checkpoint — that's the one
628
+ // whose attestations the L1 contract checks for the proven range.
629
+ const lastCheckpoint = this.checkpoints[this.checkpoints.length - 1];
630
+ const attestations = lastCheckpoint.attestations.map((a)=>a.toViem());
631
+ const epochSizeBlocks = this.checkpoints.reduce((acc, c)=>acc + c.checkpoint.blocks.length, 0);
632
+ const epochSizeTxs = this.checkpoints.reduce((acc, c)=>acc + c.checkpoint.blocks.reduce((bAcc, block)=>bAcc + block.body.txEffects.length, 0), 0);
633
+ // Reflect the publish phase. Guard against a terminal state set concurrently by cancel() — the
634
+ // post-submit isTerminal() check below relies on cancel still winning.
635
+ if (!this.isTerminal()) {
636
+ this.state = 'publishing-proof';
637
+ }
638
+ const outcome = await this.deps.publishingService.submit({
639
+ id: this.uuid,
640
+ epoch: this.spec.epochNumber,
641
+ kind: this.spec.kind,
642
+ startBlock: this.getStartBlockNumber(),
643
+ endBlock: this.getEndBlockNumber(),
644
+ deadline: this.deps.deadline,
645
+ fromCheckpoint,
646
+ toCheckpoint,
647
+ publicInputs: proof.publicInputs,
648
+ proof: proof.proof,
649
+ batchedBlobInputs: proof.batchedBlobInputs,
650
+ attestations,
651
+ headers: this.checkpoints.map((c)=>c.checkpoint.header)
652
+ });
653
+ if (this.isTerminal()) {
654
+ // cancel() already set the terminal state — don't clobber it.
687
655
  return;
688
656
  }
689
- const intervalMs = Math.ceil((await l2BlockSource.getL1Constants()).ethereumSlotDuration / 2) * 1000;
690
- this.epochCheckPromise = new RunningPromise(async ()=>{
691
- const blockHeaders = await l2BlockSource.getCheckpointedBlockHeadersForEpoch(this.epochNumber);
692
- const blockHashes = await Promise.all(blockHeaders.map((header)=>header.hash()));
693
- const thisBlocks = this.checkpoints.flatMap((checkpoint)=>checkpoint.blocks);
694
- const thisBlockHashes = await Promise.all(thisBlocks.map((block)=>block.hash()));
695
- if (blockHeaders.length !== thisBlocks.length || !blockHashes.every((block, i)=>block.equals(thisBlockHashes[i]))) {
696
- this.log.warn('Epoch blocks changed underfoot', {
657
+ switch(outcome){
658
+ case 'published':
659
+ this.log.info(`Submitted proof for epoch ${this.spec.epochNumber} (checkpoints ${fromCheckpoint}..${toCheckpoint})`, {
697
660
  uuid: this.uuid,
698
- epochNumber: this.epochNumber,
699
- oldBlockHashes: thisBlockHashes,
700
- newBlockHashes: blockHashes
661
+ ...this.spec
701
662
  });
702
- void this.stop('reorg');
703
- }
704
- }, this.log, intervalMs).start();
705
- this.log.verbose(`Scheduled epoch check for epoch ${this.epochNumber} every ${intervalMs}ms`);
706
- }
707
- /* Returns the last block header in the previous checkpoint for all checkpoints in the epoch */ gatherPreviousBlockHeaders() {
708
- const lastBlocks = this.checkpoints.map((checkpoint)=>checkpoint.blocks.at(-1));
709
- return [
710
- this.data.previousBlockHeader,
711
- ...lastBlocks.map((block)=>block.header).slice(0, -1)
712
- ];
663
+ this.state = 'completed';
664
+ this.deps.metrics.recordProvingJob(timer.ms(), checkpointCount, epochSizeBlocks, epochSizeTxs);
665
+ return;
666
+ case 'superseded':
667
+ this.log.info(`EpochSession ${this.uuid} superseded by a longer candidate`, {
668
+ uuid: this.uuid,
669
+ ...this.spec
670
+ });
671
+ this.state = 'superseded';
672
+ return;
673
+ case 'withdrawn':
674
+ // cancel() ran but the terminal-state check above missed it. Defensive: treat as cancelled.
675
+ this.state = 'cancelled';
676
+ return;
677
+ case 'expired':
678
+ this.log.warn(`EpochSession ${this.uuid} expired before publishing`, {
679
+ uuid: this.uuid,
680
+ ...this.spec
681
+ });
682
+ this.state = 'timed-out';
683
+ return;
684
+ case 'failed':
685
+ throw new Error('Failed to submit epoch proof to L1');
686
+ }
713
687
  }
714
- getTxs(block) {
715
- return block.body.txEffects.map((txEffect)=>this.txs.get(txEffect.txHash.toString()));
688
+ async teardownTopTreeIfNeeded() {
689
+ if (this.topTreeJob) {
690
+ const job = this.topTreeJob;
691
+ this.topTreeJob = undefined;
692
+ job.cancel();
693
+ this.pendingTopTreeCleanups.push(job);
694
+ }
695
+ if (this.pendingTopTreeCleanups.length > 0) {
696
+ await Promise.allSettled(this.pendingTopTreeCleanups.map((j)=>j.whenDone()));
697
+ this.pendingTopTreeCleanups.length = 0;
698
+ }
716
699
  }
717
- getL1ToL2Messages(checkpoint) {
718
- return this.data.l1ToL2Messages[checkpoint.number];
700
+ scheduleDeadlineStop() {
701
+ const deadline = this.deps.deadline;
702
+ if (!deadline) {
703
+ return;
704
+ }
705
+ const timeout = Math.max(deadline.getTime() - this.deps.dateProvider.now(), 0);
706
+ this.deadlineTimeoutHandler = setTimeout(()=>{
707
+ void this.handleDeadline();
708
+ }, timeout);
719
709
  }
720
- async processTxs(publicProcessor, txs) {
721
- const { deadline } = this;
722
- const [processedTxs, failedTxs] = await publicProcessor.process(txs, {
723
- deadline
724
- });
725
- if (failedTxs.length) {
726
- const failedTxHashes = await Promise.all(failedTxs.map(({ tx })=>tx.getTxHash()));
727
- throw new Error(`Txs failed processing: ${failedTxs.map(({ error }, index)=>`${failedTxHashes[index]} (${error})`).join(', ')}`);
710
+ /**
711
+ * Returns a promise that resolves once cancellation has propagated and the state has
712
+ * been flipped from 'cancelled' to 'timed-out'. Protected so unit tests can drive the
713
+ * deadline path without waiting on the real `setTimeout` to fire.
714
+ */ async handleDeadline() {
715
+ if (this.isTerminal()) {
716
+ return;
728
717
  }
729
- if (processedTxs.length !== txs.length) {
730
- throw new Error(`Failed to process all txs: processed ${processedTxs.length} out of ${txs.length}`);
718
+ this.log.warn(`EpochSession ${this.uuid} hit deadline`, {
719
+ uuid: this.uuid,
720
+ ...this.spec
721
+ });
722
+ await this.cancel('deadline');
723
+ // After cancel, override state if it was the canonical timeout case so observers see 'timed-out'.
724
+ if (this.state === 'cancelled') {
725
+ this.state = 'timed-out';
731
726
  }
732
- return processedTxs;
733
727
  }
734
- }
735
- class HaltExecutionError extends Error {
736
- state;
737
- constructor(state){
738
- super(`Halted execution due to state ${state}`), this.state = state;
739
- this.name = 'HaltExecutionError';
728
+ toTopTreeHooks() {
729
+ const hooks = this.deps.hooks;
730
+ return {
731
+ // `beforeProve` fires once the sub-tree (checkpoint block) proofs are ready and the root prove is
732
+ // about to start — the boundary between `awaiting-checkpoints` and proving the top tree. Don't
733
+ // clobber a terminal state set concurrently by cancel().
734
+ beforeProve: async ()=>{
735
+ if (!this.isTerminal()) {
736
+ this.state = 'awaiting-root';
737
+ }
738
+ await hooks?.beforeTopTreeProve?.();
739
+ },
740
+ afterProve: hooks?.afterTopTreeProve,
741
+ proveOverride: hooks?.topTreeProveOverride
742
+ };
740
743
  }
741
744
  }