@aztec/prover-node 0.0.1-commit.b2a5d0dd1 → 0.0.1-commit.b3d3157a

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.
@@ -1,7 +1,5 @@
1
- import { NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP } from '@aztec/constants';
2
1
  import { asyncPool } from '@aztec/foundation/async-pool';
3
2
  import { BlockNumber, EpochNumber } from '@aztec/foundation/branded-types';
4
- import { padArrayEnd } from '@aztec/foundation/collection';
5
3
  import { Fr } from '@aztec/foundation/curves/bn254';
6
4
  import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
7
5
  import { RunningPromise, promiseWithResolvers } from '@aztec/foundation/promise';
@@ -20,8 +18,8 @@ import {
20
18
  EpochProvingJobTerminalState,
21
19
  type ForkMerkleTreeOperations,
22
20
  } from '@aztec/stdlib/interfaces/server';
21
+ import { appendL1ToL2MessagesToTree } from '@aztec/stdlib/messaging';
23
22
  import { CheckpointConstantData } from '@aztec/stdlib/rollup';
24
- import { MerkleTreeId } from '@aztec/stdlib/trees';
25
23
  import type { ProcessedTx, Tx } from '@aztec/stdlib/tx';
26
24
  import { Attributes, type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client';
27
25
 
@@ -48,6 +46,7 @@ export class EpochProvingJob implements Traceable {
48
46
  private uuid: string;
49
47
 
50
48
  private runPromise: Promise<void> | undefined;
49
+ private abortController = new AbortController();
51
50
  private epochCheckPromise: RunningPromise | undefined;
52
51
  private deadlineTimeoutHandler: NodeJS.Timeout | undefined;
53
52
 
@@ -172,7 +171,7 @@ export class EpochProvingJob implements Traceable {
172
171
  ? AVM_MAX_CONCURRENT_SIMULATIONS
173
172
  : this.checkpoints.length;
174
173
 
175
- await asyncPool(parallelism, this.checkpoints, async checkpoint => {
174
+ await this.processCheckpoints(parallelism, async checkpoint => {
176
175
  this.checkState();
177
176
  const checkpointTimer = new Timer();
178
177
 
@@ -191,11 +190,12 @@ export class EpochProvingJob implements Traceable {
191
190
  const previousHeader = previousBlockHeaders[checkpointIndex];
192
191
  const l1ToL2Messages = this.getL1ToL2Messages(checkpoint);
193
192
 
194
- this.log.verbose(`Starting processing checkpoint ${checkpoint.number}`, {
193
+ this.log.debug(`Starting processing checkpoint ${checkpoint.number}`, {
195
194
  number: checkpoint.number,
196
195
  checkpointHash: checkpoint.hash().toString(),
197
- lastArchive: checkpoint.header.lastArchiveRoot,
198
- previousHeader: previousHeader.hash(),
196
+ headerHash: checkpoint.header.hash().toString(),
197
+ numL1ToL2Messages: l1ToL2Messages.length,
198
+ previousBlockNumber: previousHeader.globalVariables.blockNumber,
199
199
  uuid: this.uuid,
200
200
  });
201
201
 
@@ -229,22 +229,26 @@ export class EpochProvingJob implements Traceable {
229
229
 
230
230
  // Process public fns. L1 to L2 messages are only inserted for the first block of a checkpoint,
231
231
  // as the fork for subsequent blocks already includes them from the previous block's synced state.
232
- const db = await this.createFork(
233
- BlockNumber(block.number - 1),
234
- blockIndex === 0 ? l1ToL2Messages : undefined,
235
- );
236
- const config = PublicSimulatorConfig.from({
237
- proverId: this.prover.getProverId().toField(),
238
- skipFeeEnforcement: false,
239
- collectDebugLogs: false,
240
- collectHints: true,
241
- collectPublicInputs: true,
242
- collectStatistics: false,
243
- });
244
- const publicProcessor = this.publicProcessorFactory.create(db, globalVariables, config);
245
- const processed = await this.processTxs(publicProcessor, txs);
246
- await this.prover.addTxs(processed);
247
- await db.close();
232
+ {
233
+ await using db = await this.createFork(
234
+ BlockNumber(block.number - 1),
235
+ blockIndex === 0 ? l1ToL2Messages : undefined,
236
+ );
237
+ this.checkState();
238
+ const config = PublicSimulatorConfig.from({
239
+ proverId: this.prover.getProverId().toField(),
240
+ skipFeeEnforcement: false,
241
+ collectDebugLogs: false,
242
+ collectHints: true,
243
+ collectPublicInputs: true,
244
+ collectStatistics: false,
245
+ });
246
+ const publicProcessor = this.publicProcessorFactory.create(db, globalVariables, config);
247
+ const processed = await this.processTxs(publicProcessor, txs);
248
+ this.checkState();
249
+ await this.prover.addTxs(processed);
250
+ }
251
+ this.checkState();
248
252
  this.log.verbose(`Processed all ${txs.length} txs for block ${block.number}`, {
249
253
  blockNumber: block.number,
250
254
  blockHash: (await block.hash()).toString(),
@@ -338,25 +342,56 @@ export class EpochProvingJob implements Traceable {
338
342
  */
339
343
  private async createFork(blockNumber: BlockNumber, l1ToL2Messages: Fr[] | undefined) {
340
344
  this.log.verbose(`Creating fork at ${blockNumber}`, { blockNumber });
341
- const db = await this.dbProvider.fork(blockNumber);
345
+ // temporary stack to control fork lifetime
346
+ await using cleanup = new AsyncDisposableStack();
347
+ const db = cleanup.use(await this.dbProvider.fork(blockNumber));
342
348
 
343
349
  if (l1ToL2Messages !== undefined) {
344
350
  this.log.verbose(`Inserting ${l1ToL2Messages.length} L1 to L2 messages in fork`, {
345
351
  blockNumber,
346
352
  l1ToL2Messages: l1ToL2Messages.map(m => m.toString()),
347
353
  });
348
- const l1ToL2MessagesPadded = padArrayEnd<Fr, number>(
349
- l1ToL2Messages,
350
- Fr.ZERO,
351
- NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP,
352
- 'Too many L1 to L2 messages',
353
- );
354
- await db.appendLeaves(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, l1ToL2MessagesPadded);
354
+ await appendL1ToL2MessagesToTree(db, l1ToL2Messages);
355
355
  }
356
356
 
357
+ // everything run succesfully so we can release this stack and give control of the fork's lifetime to the caller
358
+ cleanup.move();
357
359
  return db;
358
360
  }
359
361
 
362
+ private async processCheckpoints(
363
+ parallelism: number,
364
+ processCheckpoint: (checkpoint: Checkpoint) => Promise<void>,
365
+ ): Promise<void> {
366
+ let hasError = false;
367
+ let firstError: unknown;
368
+
369
+ await asyncPool(Math.max(parallelism, 1), this.checkpoints, async checkpoint => {
370
+ if (hasError || this.abortController.signal.aborted) {
371
+ return;
372
+ }
373
+
374
+ try {
375
+ this.checkState();
376
+ await processCheckpoint(checkpoint);
377
+ } catch (err) {
378
+ if (!hasError) {
379
+ hasError = true;
380
+ firstError = err;
381
+ this.failProcessing();
382
+ }
383
+ }
384
+ });
385
+
386
+ if (hasError) {
387
+ throw firstError;
388
+ }
389
+
390
+ if (this.abortController.signal.aborted) {
391
+ this.checkState();
392
+ }
393
+ }
394
+
360
395
  private progressState(state: EpochProvingJobState) {
361
396
  this.checkState();
362
397
  this.state = state;
@@ -370,12 +405,24 @@ export class EpochProvingJob implements Traceable {
370
405
 
371
406
  public async stop(state: EpochProvingJobTerminalState = 'stopped') {
372
407
  this.state = state;
373
- this.prover.cancel();
408
+ this.interruptProcessing();
374
409
  if (this.runPromise) {
375
410
  await this.runPromise;
376
411
  }
377
412
  }
378
413
 
414
+ private failProcessing() {
415
+ if (!EpochProvingJobTerminalState.includes(this.state)) {
416
+ this.state = 'failed';
417
+ }
418
+ this.interruptProcessing();
419
+ }
420
+
421
+ private interruptProcessing() {
422
+ this.abortController.abort();
423
+ this.prover.cancel();
424
+ }
425
+
379
426
  private scheduleDeadlineStop() {
380
427
  const deadline = this.deadline;
381
428
  if (deadline) {
@@ -410,7 +457,9 @@ export class EpochProvingJob implements Traceable {
410
457
  const intervalMs = Math.ceil((await l2BlockSource.getL1Constants()).ethereumSlotDuration / 2) * 1000;
411
458
  this.epochCheckPromise = new RunningPromise(
412
459
  async () => {
413
- const blockHeaders = await l2BlockSource.getCheckpointedBlockHeadersForEpoch(this.epochNumber);
460
+ const blockHeaders = (
461
+ await l2BlockSource.getBlocksData({ epoch: this.epochNumber, onlyCheckpointed: true })
462
+ ).map(d => d.header);
414
463
  const blockHashes = await Promise.all(blockHeaders.map(header => header.hash()));
415
464
  const thisBlocks = this.checkpoints.flatMap(checkpoint => checkpoint.blocks);
416
465
  const thisBlockHashes = await Promise.all(thisBlocks.map(block => block.hash()));
@@ -449,7 +498,11 @@ export class EpochProvingJob implements Traceable {
449
498
 
450
499
  private async processTxs(publicProcessor: PublicProcessor, txs: Tx[]): Promise<ProcessedTx[]> {
451
500
  const { deadline } = this;
452
- const [processedTxs, failedTxs] = await publicProcessor.process(txs, { deadline });
501
+ const [processedTxs, failedTxs] = await publicProcessor.process(txs, {
502
+ deadline,
503
+ signal: this.abortController.signal,
504
+ });
505
+ this.checkState();
453
506
 
454
507
  if (failedTxs.length) {
455
508
  const failedTxHashes = await Promise.all(failedTxs.map(({ tx }) => tx.getTxHash()));
@@ -96,9 +96,9 @@ export class EpochMonitor implements Traceable {
96
96
  }
97
97
 
98
98
  private async getEpochNumberToProve() {
99
- const lastBlockProven = await this.l2BlockSource.getProvenBlockNumber();
99
+ const lastBlockProven = (await this.l2BlockSource.getBlockNumber({ tag: 'proven' })) ?? BlockNumber.ZERO;
100
100
  const firstBlockToProve = BlockNumber(lastBlockProven + 1);
101
- const firstBlockHeaderToProve = await this.l2BlockSource.getBlockHeader(firstBlockToProve);
101
+ const firstBlockHeaderToProve = (await this.l2BlockSource.getBlockData({ number: firstBlockToProve }))?.header;
102
102
  if (!firstBlockHeaderToProve) {
103
103
  return { epochToProve: undefined, blockNumber: firstBlockToProve };
104
104
  }
@@ -8,11 +8,13 @@ import { areArraysEqual } from '@aztec/foundation/collection';
8
8
  import { Fr } from '@aztec/foundation/curves/bn254';
9
9
  import { EthAddress } from '@aztec/foundation/eth-address';
10
10
  import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
11
+ import { retryUntil } from '@aztec/foundation/retry';
11
12
  import type { Tuple } from '@aztec/foundation/serialize';
12
13
  import { Timer } from '@aztec/foundation/timer';
13
14
  import { RollupAbi } from '@aztec/l1-artifacts';
14
15
  import type { PublisherConfig, TxSenderConfig } from '@aztec/sequencer-client';
15
16
  import { CommitteeAttestation, CommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
17
+ import { getProofSubmissionDeadlineTimestamp } from '@aztec/stdlib/epoch-helpers';
16
18
  import type { Proof } from '@aztec/stdlib/proofs';
17
19
  import type { FeeRecipient, RootRollupPublicInputs } from '@aztec/stdlib/rollup';
18
20
  import type { L1PublishProofStats } from '@aztec/stdlib/stats';
@@ -101,6 +103,11 @@ export class ProverNodePublisher {
101
103
  const ctx = { epochNumber, fromCheckpoint, toCheckpoint };
102
104
 
103
105
  if (!this.interrupted) {
106
+ if (!(await this.waitUntilStartBuildsOnProven(args))) {
107
+ this.log.verbose('Checkpoint data syncing interrupted', ctx);
108
+ return false;
109
+ }
110
+
104
111
  const timer = new Timer();
105
112
  // Validate epoch proof range and hashes are correct before submitting
106
113
  await this.validateEpochProofSubmission(args);
@@ -147,6 +154,53 @@ export class ProverNodePublisher {
147
154
  return false;
148
155
  }
149
156
 
157
+ private async waitUntilStartBuildsOnProven(args: { epochNumber: EpochNumber; fromCheckpoint: CheckpointNumber }) {
158
+ const { epochNumber, fromCheckpoint } = args;
159
+ const provenCheckpoint = await this.getProvenCheckpoint();
160
+ if (this.isStartBuildingOnProven(fromCheckpoint, provenCheckpoint)) {
161
+ return true;
162
+ }
163
+
164
+ const timeout = await this.getSecondsUntilProofSubmissionWindowEnd(epochNumber);
165
+ this.log.info(`Waiting for proven checkpoint to reach proof start`, {
166
+ epochNumber,
167
+ fromCheckpoint,
168
+ provenCheckpoint,
169
+ timeout,
170
+ });
171
+
172
+ await retryUntil(
173
+ async () => {
174
+ if (this.interrupted) {
175
+ return true;
176
+ }
177
+
178
+ const proven = await this.getProvenCheckpoint();
179
+ this.log.verbose(`Proven checkpoint is at ${proven} (waiting for ${fromCheckpoint - 1})`, { epochNumber });
180
+ return this.isStartBuildingOnProven(fromCheckpoint, proven) ? true : undefined;
181
+ },
182
+ `proven checkpoint to reach ${fromCheckpoint - 1}`,
183
+ timeout,
184
+ 4,
185
+ );
186
+
187
+ return !this.interrupted;
188
+ }
189
+
190
+ private async getProvenCheckpoint() {
191
+ return (await this.rollupContract.getTips()).proven;
192
+ }
193
+
194
+ private isStartBuildingOnProven(fromCheckpoint: CheckpointNumber, provenCheckpoint: CheckpointNumber) {
195
+ return fromCheckpoint - 1 <= provenCheckpoint;
196
+ }
197
+
198
+ private async getSecondsUntilProofSubmissionWindowEnd(epochNumber: EpochNumber) {
199
+ const deadline = getProofSubmissionDeadlineTimestamp(epochNumber, await this.rollupContract.getRollupConstants());
200
+ const now = BigInt(Math.floor(Date.now() / 1000));
201
+ return Math.max(Number(deadline - now), 0.001);
202
+ }
203
+
150
204
  private async validateEpochProofSubmission(args: {
151
205
  fromCheckpoint: CheckpointNumber;
152
206
  toCheckpoint: CheckpointNumber;
@@ -203,10 +257,14 @@ export class ProverNodePublisher {
203
257
  const argsPublicInputs = [...publicInputs.toFields()];
204
258
 
205
259
  if (!areArraysEqual(rollupPublicInputs, argsPublicInputs, (a, b) => a.equals(b))) {
206
- const fmt = (inputs: Fr[] | readonly string[]) => inputs.map(x => x.toString()).join(', ');
207
- throw new Error(
208
- `Root rollup public inputs mismatch:\nRollup: ${fmt(rollupPublicInputs)}\nComputed:${fmt(argsPublicInputs)}`,
209
- );
260
+ throw await reportPublicInputsMismatch({
261
+ rollupPublicInputs,
262
+ argsPublicInputs,
263
+ fromCheckpoint,
264
+ toCheckpoint,
265
+ rollupContract: this.rollupContract,
266
+ log: this.log,
267
+ });
210
268
  }
211
269
  }
212
270
 
@@ -364,11 +422,108 @@ export class ProverNodePublisher {
364
422
  end: argsArray[1],
365
423
  args: argsArray[2],
366
424
  fees: argsArray[3],
367
- attestations: new CommitteeAttestationsAndSigners(
425
+ attestations: CommitteeAttestationsAndSigners.packAttestations(
368
426
  args.attestations.map(a => CommitteeAttestation.fromViem(a)),
369
- ).getPackedAttestations(),
427
+ ),
370
428
  blobInputs: argsArray[4],
371
429
  proof: proofHex,
372
430
  };
373
431
  }
374
432
  }
433
+
434
+ /**
435
+ * Decodes a `Root rollup public inputs mismatch`, fetches the on-chain CheckpointLog for any
436
+ * mismatching `checkpointHeaderHashes[i]`, emits a structured error log, and returns a thrown-ready
437
+ * Error with a human-readable summary.
438
+ *
439
+ * Layout of `RootRollupPublicInputs.toFields()`:
440
+ * [0] previousArchiveRoot
441
+ * [1] endArchiveRoot
442
+ * [2] outHash
443
+ * [3 .. 3+N-1] checkpointHeaderHashes[i] for i in 0..N-1 (N = MAX_CHECKPOINTS_PER_EPOCH)
444
+ * [3+N .. 3+3N-1] fees[i] = (recipient, value) for i in 0..N-1
445
+ * [3+3N .. 3+3N+4] EpochConstantData (chainId, version, vkTreeRoot, protocolContractsHash, proverId)
446
+ * [3+3N+5 ..] blobPublicInputs (FinalBlobAccumulator)
447
+ */
448
+ async function reportPublicInputsMismatch(input: {
449
+ rollupPublicInputs: readonly Fr[];
450
+ argsPublicInputs: readonly Fr[];
451
+ fromCheckpoint: CheckpointNumber;
452
+ toCheckpoint: CheckpointNumber;
453
+ rollupContract: RollupContract;
454
+ log: Logger;
455
+ }): Promise<Error> {
456
+ const { rollupPublicInputs, argsPublicInputs, fromCheckpoint, toCheckpoint, rollupContract, log } = input;
457
+ const N = MAX_CHECKPOINTS_PER_EPOCH;
458
+ const constantsStart = 3 + 3 * N;
459
+ const blobStart = constantsStart + 5;
460
+ const constantLabels = ['chainId', 'version', 'vkTreeRoot', 'protocolContractsHash', 'proverId'];
461
+
462
+ const diffs: { index: number; label: string; rollup: Fr; computed: Fr; checkpointIndex?: number }[] = [];
463
+ const len = Math.max(rollupPublicInputs.length, argsPublicInputs.length);
464
+ for (let i = 0; i < len; i++) {
465
+ const a = rollupPublicInputs[i] ?? Fr.ZERO;
466
+ const b = argsPublicInputs[i] ?? Fr.ZERO;
467
+ if (a.equals(b)) {
468
+ continue;
469
+ }
470
+ let label: string;
471
+ let checkpointIndex: number | undefined;
472
+ if (i === 0) {
473
+ label = 'previousArchiveRoot';
474
+ } else if (i === 1) {
475
+ label = 'endArchiveRoot';
476
+ } else if (i === 2) {
477
+ label = 'outHash';
478
+ } else if (i < 3 + N) {
479
+ checkpointIndex = i - 3;
480
+ label = `checkpointHeaderHashes[${checkpointIndex}]`;
481
+ } else if (i < 3 + 3 * N) {
482
+ const feePairIndex = i - (3 + N);
483
+ const feeIndex = Math.floor(feePairIndex / 2);
484
+ const sub = feePairIndex % 2 === 0 ? 'recipient' : 'value';
485
+ label = `fees[${feeIndex}].${sub}`;
486
+ } else if (i < blobStart) {
487
+ label = `constants.${constantLabels[i - constantsStart]}`;
488
+ } else {
489
+ label = `blobPublicInputs[${i - blobStart}]`;
490
+ }
491
+ diffs.push({ index: i, label, rollup: a, computed: b, checkpointIndex });
492
+ }
493
+
494
+ // For each mismatching checkpointHeaderHash, fetch the L1 CheckpointLog so the operator can
495
+ // see what was published on-chain alongside the prover's recomputed hash.
496
+ const onChainCheckpoints = await Promise.all(
497
+ diffs
498
+ .filter(d => d.checkpointIndex !== undefined)
499
+ .map(async d => {
500
+ const checkpointNumber = CheckpointNumber(fromCheckpoint + d.checkpointIndex!);
501
+ try {
502
+ const cp = await rollupContract.getCheckpoint(checkpointNumber);
503
+ return { checkpointIndex: d.checkpointIndex!, checkpointNumber, headerHash: cp.headerHash.toString() };
504
+ } catch (err) {
505
+ return { checkpointIndex: d.checkpointIndex!, checkpointNumber, error: (err as Error).message };
506
+ }
507
+ }),
508
+ );
509
+
510
+ log.error(`Root rollup public inputs mismatch`, undefined, {
511
+ fromCheckpoint,
512
+ toCheckpoint,
513
+ numDiffs: diffs.length,
514
+ diffs: diffs.map(d => ({
515
+ index: d.index,
516
+ label: d.label,
517
+ rollup: d.rollup.toString(),
518
+ computed: d.computed.toString(),
519
+ })),
520
+ onChainCheckpoints,
521
+ });
522
+
523
+ const fmt = (inputs: readonly Fr[]) => inputs.map(x => x.toString()).join(', ');
524
+ const summary = diffs.map(d => `[${d.index} ${d.label}] L1=${d.rollup} prover=${d.computed}`).join('\n');
525
+ return new Error(
526
+ `Root rollup public inputs mismatch (${diffs.length} fields differ):\n${summary}\n` +
527
+ `Rollup: ${fmt(rollupPublicInputs)}\nComputed:${fmt(argsPublicInputs)}`,
528
+ );
529
+ }
@@ -15,7 +15,6 @@ import type { ContractDataSource } from '@aztec/stdlib/contract';
15
15
  import { getProofSubmissionDeadlineTimestamp } from '@aztec/stdlib/epoch-helpers';
16
16
  import {
17
17
  type EpochProverManager,
18
- EpochProvingJobTerminalState,
19
18
  type ITxProvider,
20
19
  type ProverNodeApi,
21
20
  type Service,
@@ -39,7 +38,7 @@ import {
39
38
  import { uploadEpochProofFailure } from './actions/upload-epoch-proof-failure.js';
40
39
  import type { SpecificProverNodeConfig } from './config.js';
41
40
  import type { EpochProvingJobData } from './job/epoch-proving-job-data.js';
42
- import { EpochProvingJob, type EpochProvingJobState } from './job/epoch-proving-job.js';
41
+ import { EpochProvingJob, type EpochProvingJobOptions, type EpochProvingJobState } from './job/epoch-proving-job.js';
43
42
  import { ProverNodeJobMetrics, ProverNodeRewardsMetrics } from './metrics.js';
44
43
  import type { EpochMonitor, EpochMonitorHandler } from './monitors/epoch-monitor.js';
45
44
  import type { ProverNodePublisher } from './prover-node-publisher.js';
@@ -60,6 +59,7 @@ export class ProverNode implements EpochMonitorHandler, ProverNodeApi, Traceable
60
59
  private config: ProverNodeOptions;
61
60
  private jobMetrics: ProverNodeJobMetrics;
62
61
  private rewardsMetrics: ProverNodeRewardsMetrics;
62
+ private startingProofEpochs: Set<EpochNumber> = new Set();
63
63
 
64
64
  public readonly tracer: Tracer;
65
65
 
@@ -127,15 +127,7 @@ export class ProverNode implements EpochMonitorHandler, ProverNodeApi, Traceable
127
127
  this.log.debug(`Running jobs as ${epochNumber} is ready to prove`, {
128
128
  jobs: Array.from(this.jobs.values()).map(job => `${job.getEpochNumber()}:${job.getId()}`),
129
129
  });
130
- const activeJobs = await this.getActiveJobsForEpoch(epochNumber);
131
- if (activeJobs.length > 0) {
132
- this.log.warn(`Not starting proof for ${epochNumber} since there are active jobs for the epoch`, {
133
- activeJobs: activeJobs.map(job => job.uuid),
134
- });
135
- return true;
136
- }
137
- await this.startProof(epochNumber);
138
- return true;
130
+ return await this.startProofIfNeeded(epochNumber);
139
131
  } catch (err) {
140
132
  if (err instanceof EmptyEpochError) {
141
133
  this.log.info(`Not starting proof for ${epochNumber} since no blocks were found`);
@@ -166,10 +158,10 @@ export class ProverNode implements EpochMonitorHandler, ProverNodeApi, Traceable
166
158
  async stop() {
167
159
  this.log.info('Stopping ProverNode');
168
160
  await this.epochsMonitor.stop();
169
- await this.prover.stop();
170
- await tryStop(this.publisherFactory);
171
161
  this.publisher?.interrupt();
172
162
  await Promise.all(Array.from(this.jobs.values()).map(job => job.stop()));
163
+ await this.prover.stop();
164
+ await tryStop(this.publisherFactory);
173
165
  this.rewardsMetrics.stop();
174
166
  this.l1Metrics.stop();
175
167
  await this.telemetryClient.stop();
@@ -191,7 +183,38 @@ export class ProverNode implements EpochMonitorHandler, ProverNodeApi, Traceable
191
183
  * Starts a proving process and returns immediately.
192
184
  */
193
185
  public async startProof(epochNumber: EpochNumber) {
194
- const job = await this.createProvingJob(epochNumber, { skipEpochCheck: true });
186
+ await this.startProofIfNeeded(epochNumber, { skipEpochCheck: true });
187
+ }
188
+
189
+ private async startProofIfNeeded(epochNumber: EpochNumber, opts: EpochProvingJobOptions = {}): Promise<boolean> {
190
+ if (this.startingProofEpochs.has(epochNumber)) {
191
+ this.log.warn(`Not starting proof for ${epochNumber} since a proof is already being started for the epoch`, {
192
+ epochNumber,
193
+ });
194
+ return false;
195
+ }
196
+
197
+ this.startingProofEpochs.add(epochNumber);
198
+
199
+ try {
200
+ const activeJobs = await this.activeJobsCoverEpoch(epochNumber);
201
+ if (activeJobs.length > 0) {
202
+ this.log.warn(`Not starting proof for ${epochNumber} since an active job already covers the epoch`, {
203
+ epochNumber,
204
+ activeJobs,
205
+ });
206
+ return true;
207
+ }
208
+
209
+ await this.startProofInternal(epochNumber, opts);
210
+ return true;
211
+ } finally {
212
+ this.startingProofEpochs.delete(epochNumber);
213
+ }
214
+ }
215
+
216
+ private async startProofInternal(epochNumber: EpochNumber, opts: EpochProvingJobOptions = {}) {
217
+ const job = await this.createProvingJob(epochNumber, opts);
195
218
  void this.runJob(job);
196
219
  }
197
220
 
@@ -245,20 +268,42 @@ export class ProverNode implements EpochMonitorHandler, ProverNodeApi, Traceable
245
268
  * Returns an array of jobs being processed.
246
269
  */
247
270
  public getJobs(): Promise<{ uuid: string; status: EpochProvingJobState; epochNumber: EpochNumber }[]> {
248
- return Promise.resolve(
249
- Array.from(this.jobs.entries()).map(([uuid, job]) => ({
250
- uuid,
251
- status: job.getState(),
252
- epochNumber: job.getEpochNumber(),
253
- })),
254
- );
271
+ return Promise.resolve(this.getJobsInternal());
272
+ }
273
+
274
+ private getJobsInternal(): { uuid: string; status: EpochProvingJobState; epochNumber: EpochNumber }[] {
275
+ return Array.from(this.jobs.entries()).map(([uuid, job]) => ({
276
+ uuid,
277
+ status: job.getState(),
278
+ epochNumber: job.getEpochNumber(),
279
+ }));
255
280
  }
256
281
 
257
- protected async getActiveJobsForEpoch(
258
- epochNumber: EpochNumber,
259
- ): Promise<{ uuid: string; status: EpochProvingJobState }[]> {
260
- const jobs = await this.getJobs();
261
- return jobs.filter(job => job.epochNumber === epochNumber && !EpochProvingJobTerminalState.includes(job.status));
282
+ private async activeJobsCoverEpoch(epochNumber: EpochNumber): Promise<string[]> {
283
+ const checkpoints = await this.l2BlockSource.getCheckpointsData({ epoch: epochNumber });
284
+ if (checkpoints.length === 0) {
285
+ return [];
286
+ }
287
+
288
+ const firstCheckpoint = checkpoints.at(0)!.checkpointNumber;
289
+ const latestCheckpoint = checkpoints.at(-1)!.checkpointNumber;
290
+
291
+ const jobs: string[] = [];
292
+ for (const job of this.jobs.values()) {
293
+ if (job.getEpochNumber() !== epochNumber) {
294
+ continue;
295
+ }
296
+
297
+ const jobCheckpoints = job.getProvingData().checkpoints;
298
+ const checkpointOverlap =
299
+ jobCheckpoints.at(0)!.number <= firstCheckpoint && jobCheckpoints.at(-1)!.number >= latestCheckpoint;
300
+
301
+ if (checkpointOverlap && !['failed', 'stopped', 'timed-out'].includes(job.getState())) {
302
+ jobs.push(job.getId());
303
+ }
304
+ }
305
+
306
+ return jobs;
262
307
  }
263
308
 
264
309
  private checkMaximumPendingJobs() {
@@ -312,26 +357,21 @@ export class ProverNode implements EpochMonitorHandler, ProverNodeApi, Traceable
312
357
 
313
358
  @trackSpan('ProverNode.gatherEpochData', epochNumber => ({ [Attributes.EPOCH_NUMBER]: epochNumber }))
314
359
  private async gatherEpochData(epochNumber: EpochNumber): Promise<EpochProvingJobData> {
315
- const checkpoints = await this.gatherCheckpoints(epochNumber);
360
+ const publishedCheckpoints = await this.l2BlockSource.getCheckpoints({ epoch: epochNumber });
361
+ if (publishedCheckpoints.length === 0) {
362
+ throw new EmptyEpochError(epochNumber);
363
+ }
364
+ const checkpoints = publishedCheckpoints.map(p => p.checkpoint);
365
+ const attestations = publishedCheckpoints.at(-1)?.attestations ?? [];
316
366
  const txArray = await this.gatherTxs(epochNumber, checkpoints);
317
367
  const txs = new Map<string, Tx>(txArray.map(tx => [tx.getTxHash().toString(), tx]));
318
368
  const l1ToL2Messages = await this.gatherMessages(epochNumber, checkpoints);
319
369
  const [firstBlock] = checkpoints[0].blocks;
320
370
  const previousBlockHeader = await this.gatherPreviousBlockHeader(epochNumber, firstBlock.number - 1);
321
- const [lastPublishedCheckpoint] = await this.l2BlockSource.getCheckpoints(checkpoints.at(-1)!.number, 1);
322
- const attestations = lastPublishedCheckpoint?.attestations ?? [];
323
371
 
324
372
  return { checkpoints, txs, l1ToL2Messages, epochNumber, previousBlockHeader, attestations };
325
373
  }
326
374
 
327
- private async gatherCheckpoints(epochNumber: EpochNumber) {
328
- const checkpoints = await this.l2BlockSource.getCheckpointsForEpoch(epochNumber);
329
- if (checkpoints.length === 0) {
330
- throw new EmptyEpochError(epochNumber);
331
- }
332
- return checkpoints;
333
- }
334
-
335
375
  private async gatherTxs(epochNumber: EpochNumber, checkpoints: Checkpoint[]) {
336
376
  const deadline = new Date(this.dateProvider.now() + this.config.txGatheringTimeoutMs);
337
377
  const txProvider = this.p2pClient.getTxProvider();
@@ -360,16 +400,13 @@ export class ProverNode implements EpochMonitorHandler, ProverNodeApi, Traceable
360
400
  }
361
401
 
362
402
  private async gatherPreviousBlockHeader(epochNumber: EpochNumber, previousBlockNumber: number) {
363
- const header = await (previousBlockNumber === 0
364
- ? this.worldState.getCommitted().getInitialHeader()
365
- : this.l2BlockSource.getBlockHeader(BlockNumber(previousBlockNumber)));
366
-
367
- if (!header) {
403
+ const data = await this.l2BlockSource.getBlockData({ number: BlockNumber(previousBlockNumber) });
404
+ if (!data?.header) {
368
405
  throw new Error(`Previous block header ${previousBlockNumber} not found for proving epoch ${epochNumber}`);
369
406
  }
370
407
 
371
- this.log.verbose(`Gathered previous block header ${header.getBlockNumber()} for epoch ${epochNumber}`);
372
- return header;
408
+ this.log.verbose(`Gathered previous block header ${data.header.getBlockNumber()} for epoch ${epochNumber}`);
409
+ return data.header;
373
410
  }
374
411
 
375
412
  /** Extracted for testing purposes. */