@aztec/prover-node 5.0.0-private.20260319 → 5.0.0-rc.1

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 (71) hide show
  1. package/README.md +506 -0
  2. package/dest/actions/download-epoch-proving-job.js +1 -1
  3. package/dest/actions/rerun-epoch-proving-job.d.ts +4 -3
  4. package/dest/actions/rerun-epoch-proving-job.d.ts.map +1 -1
  5. package/dest/actions/rerun-epoch-proving-job.js +103 -21
  6. package/dest/bin/run-failed-epoch.js +1 -3
  7. package/dest/checkpoint-store.d.ts +83 -0
  8. package/dest/checkpoint-store.d.ts.map +1 -0
  9. package/dest/checkpoint-store.js +181 -0
  10. package/dest/config.d.ts +1 -1
  11. package/dest/config.d.ts.map +1 -1
  12. package/dest/config.js +1 -1
  13. package/dest/factory.d.ts +1 -1
  14. package/dest/factory.d.ts.map +1 -1
  15. package/dest/factory.js +22 -8
  16. package/dest/index.d.ts +2 -1
  17. package/dest/index.d.ts.map +1 -1
  18. package/dest/index.js +1 -0
  19. package/dest/job/checkpoint-prover.d.ts +134 -0
  20. package/dest/job/checkpoint-prover.d.ts.map +1 -0
  21. package/dest/job/checkpoint-prover.js +350 -0
  22. package/dest/job/epoch-session.d.ts +146 -0
  23. package/dest/job/epoch-session.d.ts.map +1 -0
  24. package/dest/job/epoch-session.js +709 -0
  25. package/dest/job/top-tree-job.d.ts +82 -0
  26. package/dest/job/top-tree-job.d.ts.map +1 -0
  27. package/dest/job/top-tree-job.js +152 -0
  28. package/dest/metrics.d.ts +29 -5
  29. package/dest/metrics.d.ts.map +1 -1
  30. package/dest/metrics.js +73 -9
  31. package/dest/monitors/epoch-monitor.js +6 -2
  32. package/dest/proof-publishing-service.d.ts +159 -0
  33. package/dest/proof-publishing-service.d.ts.map +1 -0
  34. package/dest/proof-publishing-service.js +334 -0
  35. package/dest/prover-node-publisher.d.ts +18 -11
  36. package/dest/prover-node-publisher.d.ts.map +1 -1
  37. package/dest/prover-node-publisher.js +195 -57
  38. package/dest/prover-node.d.ts +96 -68
  39. package/dest/prover-node.d.ts.map +1 -1
  40. package/dest/prover-node.js +382 -227
  41. package/dest/prover-publisher-factory.d.ts +2 -2
  42. package/dest/prover-publisher-factory.d.ts.map +1 -1
  43. package/dest/prover-publisher-factory.js +3 -3
  44. package/dest/session-manager.d.ts +158 -0
  45. package/dest/session-manager.d.ts.map +1 -0
  46. package/dest/session-manager.js +452 -0
  47. package/dest/test/index.d.ts +7 -6
  48. package/dest/test/index.d.ts.map +1 -1
  49. package/package.json +23 -23
  50. package/src/actions/download-epoch-proving-job.ts +1 -1
  51. package/src/actions/rerun-epoch-proving-job.ts +114 -28
  52. package/src/bin/run-failed-epoch.ts +1 -2
  53. package/src/checkpoint-store.ts +213 -0
  54. package/src/config.ts +2 -1
  55. package/src/factory.ts +18 -10
  56. package/src/index.ts +1 -0
  57. package/src/job/checkpoint-prover.ts +465 -0
  58. package/src/job/epoch-session.ts +424 -0
  59. package/src/job/top-tree-job.ts +227 -0
  60. package/src/metrics.ts +88 -12
  61. package/src/monitors/epoch-monitor.ts +2 -2
  62. package/src/proof-publishing-service.ts +424 -0
  63. package/src/prover-node-publisher.ts +220 -67
  64. package/src/prover-node.ts +439 -249
  65. package/src/prover-publisher-factory.ts +3 -3
  66. package/src/session-manager.ts +552 -0
  67. package/src/test/index.ts +6 -6
  68. package/dest/job/epoch-proving-job.d.ts +0 -63
  69. package/dest/job/epoch-proving-job.d.ts.map +0 -1
  70. package/dest/job/epoch-proving-job.js +0 -762
  71. package/src/job/epoch-proving-job.ts +0 -465
@@ -19,9 +19,9 @@ import type { L1PublishProofStats } from '@aztec/stdlib/stats';
19
19
  import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
20
20
 
21
21
  import { inspect } from 'util';
22
- import { type Hex, type TransactionReceipt, encodeFunctionData } from 'viem';
22
+ import { type Hex, type TransactionReceipt, encodeFunctionData, formatEther, formatGwei } from 'viem';
23
23
 
24
- import { ProverNodePublisherMetrics } from './metrics.js';
24
+ import { type EstimatedSubmitProofStats, ProverNodePublisherMetrics } from './metrics.js';
25
25
 
26
26
  /** Arguments to the submitEpochProof method of the rollup contract */
27
27
  export type L1SubmitEpochProofArgs = {
@@ -36,7 +36,6 @@ export type L1SubmitEpochProofArgs = {
36
36
  };
37
37
 
38
38
  export class ProverNodePublisher {
39
- private interrupted = false;
40
39
  private metrics: ProverNodePublisherMetrics;
41
40
 
42
41
  protected log: Logger;
@@ -67,23 +66,6 @@ export class ProverNodePublisher {
67
66
  return this.rollupContract;
68
67
  }
69
68
 
70
- /**
71
- * Calling `interrupt` will cause any in progress call to `publishRollup` to return `false` asap.
72
- * Be warned, the call may return false even if the tx subsequently gets successfully mined.
73
- * In practice this shouldn't matter, as we'll only ever be calling `interrupt` when we know it's going to fail.
74
- * A call to `restart` is required before you can continue publishing.
75
- */
76
- public interrupt() {
77
- this.interrupted = true;
78
- this.l1TxUtils.interrupt();
79
- }
80
-
81
- /** Restarts the publisher after calling `interrupt`. */
82
- public restart() {
83
- this.interrupted = false;
84
- this.l1TxUtils.restart();
85
- }
86
-
87
69
  public getSenderAddress() {
88
70
  return this.l1TxUtils.getSenderAddress();
89
71
  }
@@ -96,54 +78,52 @@ export class ProverNodePublisher {
96
78
  proof: Proof;
97
79
  batchedBlobInputs: BatchedBlob;
98
80
  attestations: ViemCommitteeAttestation[];
81
+ /** Wall-clock deadline (proof-submission window end) past which the L1 tx should stop retrying. */
82
+ deadline?: Date;
99
83
  }): Promise<boolean> {
100
84
  const { epochNumber, fromCheckpoint, toCheckpoint } = args;
101
85
  const ctx = { epochNumber, fromCheckpoint, toCheckpoint };
102
86
 
103
- if (!this.interrupted) {
104
- const timer = new Timer();
105
- // Validate epoch proof range and hashes are correct before submitting
106
- await this.validateEpochProofSubmission(args);
107
-
108
- const txReceipt = await this.sendSubmitEpochProofTx(args);
109
- if (!txReceipt) {
110
- this.log.error(`Failed to mine submitEpochProof tx`, undefined, ctx);
111
- return false;
112
- }
87
+ const timer = new Timer();
88
+ // Validate epoch proof range and hashes are correct before submitting
89
+ await this.validateEpochProofSubmission(args);
113
90
 
114
- try {
115
- this.metrics.recordSenderBalance(
116
- await this.l1TxUtils.getSenderBalance(),
117
- this.l1TxUtils.getSenderAddress().toString(),
118
- );
119
- } catch (err) {
120
- this.log.warn(`Failed to record the ETH balance of the prover node: ${err}`);
121
- }
91
+ const txReceipt = await this.sendSubmitEpochProofTx(args);
92
+ if (!txReceipt) {
93
+ this.log.error(`Failed to mine submitEpochProof tx`, undefined, ctx);
94
+ return false;
95
+ }
122
96
 
123
- // Tx was mined successfully
124
- if (txReceipt.status === 'success') {
125
- const tx = await this.l1TxUtils.getTransactionStats(txReceipt.transactionHash);
126
- const stats: L1PublishProofStats = {
127
- gasPrice: txReceipt.effectiveGasPrice,
128
- gasUsed: txReceipt.gasUsed,
129
- transactionHash: txReceipt.transactionHash,
130
- calldataGas: tx!.calldataGas,
131
- calldataSize: tx!.calldataSize,
132
- sender: tx!.sender,
133
- blobDataGas: 0n,
134
- blobGasUsed: 0n,
135
- eventName: 'proof-published-to-l1',
136
- };
137
- this.log.info(`Published epoch proof to L1 rollup contract`, { ...stats, ...ctx });
138
- this.metrics.recordSubmitProof(timer.ms(), stats);
139
- return true;
140
- }
97
+ try {
98
+ this.metrics.recordSenderBalance(
99
+ await this.l1TxUtils.getSenderBalance(),
100
+ this.l1TxUtils.getSenderAddress().toString(),
101
+ );
102
+ } catch (err) {
103
+ this.log.warn(`Failed to record the ETH balance of the prover node: ${err}`);
104
+ }
141
105
 
142
- this.metrics.recordFailedTx();
143
- this.log.error(`Rollup submitEpochProof tx reverted ${txReceipt.transactionHash}`, undefined, ctx);
106
+ // Tx was mined successfully
107
+ if (txReceipt.status === 'success') {
108
+ const tx = await this.l1TxUtils.getTransactionStats(txReceipt.transactionHash);
109
+ const stats: L1PublishProofStats = {
110
+ gasPrice: txReceipt.effectiveGasPrice,
111
+ gasUsed: txReceipt.gasUsed,
112
+ transactionHash: txReceipt.transactionHash,
113
+ calldataGas: tx!.calldataGas,
114
+ calldataSize: tx!.calldataSize,
115
+ sender: tx!.sender,
116
+ blobDataGas: 0n,
117
+ blobGasUsed: 0n,
118
+ eventName: 'proof-published-to-l1',
119
+ };
120
+ this.log.info(`Published epoch proof to L1 rollup contract`, { ...stats, ...ctx });
121
+ this.metrics.recordSubmitProof(timer.ms(), stats);
122
+ return true;
144
123
  }
145
124
 
146
- this.log.verbose('Checkpoint data syncing interrupted', ctx);
125
+ this.metrics.recordFailedTx();
126
+ this.log.error(`Rollup submitEpochProof tx reverted ${txReceipt.transactionHash}`, undefined, ctx);
147
127
  return false;
148
128
  }
149
129
 
@@ -168,7 +148,7 @@ export class ProverNodePublisher {
168
148
  // toCheckpoint can't be greater than pending
169
149
  if (toCheckpoint > pending) {
170
150
  throw new Error(
171
- `Cannot submit epoch proof for ${fromCheckpoint}-${toCheckpoint} as pending checkpoint is ${pending}`,
151
+ `Cannot submit epoch proof for ${fromCheckpoint}-${toCheckpoint} as proposed checkpoint is ${pending}`,
172
152
  );
173
153
  }
174
154
 
@@ -203,16 +183,89 @@ export class ProverNodePublisher {
203
183
  const argsPublicInputs = [...publicInputs.toFields()];
204
184
 
205
185
  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
- );
186
+ throw await reportPublicInputsMismatch({
187
+ rollupPublicInputs,
188
+ argsPublicInputs,
189
+ fromCheckpoint,
190
+ toCheckpoint,
191
+ rollupContract: this.rollupContract,
192
+ log: this.log,
193
+ });
210
194
  }
211
195
  }
212
196
 
197
+ /**
198
+ * Estimates what submitting the epoch proof would have cost on L1 without actually sending it.
199
+ * Runs the same validation as `submitEpochProof`, encodes the calldata, estimates gas, and records metrics.
200
+ * Used when proof publishing is disabled (e.g. PROVER_NODE_DISABLE_PROOF_PUBLISH=true on mainnet).
201
+ */
202
+ public async analyzeEpochProofSubmission(args: {
203
+ epochNumber: EpochNumber;
204
+ fromCheckpoint: CheckpointNumber;
205
+ toCheckpoint: CheckpointNumber;
206
+ publicInputs: RootRollupPublicInputs;
207
+ proof: Proof;
208
+ batchedBlobInputs: BatchedBlob;
209
+ attestations: ViemCommitteeAttestation[];
210
+ }): Promise<void> {
211
+ const { epochNumber, fromCheckpoint, toCheckpoint } = args;
212
+
213
+ await this.validateEpochProofSubmission(args);
214
+
215
+ const data = this.encodeSubmitEpochProofCalldata(args);
216
+ const senderAddress = this.l1TxUtils.getSenderAddress();
217
+
218
+ const [gasLimit, gasPrice, latestBlock] = await Promise.all([
219
+ this.l1TxUtils.estimateGas(senderAddress.toString() as `0x${string}`, { to: this.rollupContract.address, data }),
220
+ this.l1TxUtils.getGasPrice(),
221
+ this.l1TxUtils.client.getBlock({ blockTag: 'latest' }),
222
+ ]);
223
+
224
+ const baseFeePerGas = latestBlock.baseFeePerGas ?? 0n;
225
+ const { maxPriorityFeePerGas } = gasPrice;
226
+
227
+ const effectiveFeePerGas = baseFeePerGas + maxPriorityFeePerGas;
228
+ const estimatedTotalFee = gasLimit * effectiveFeePerGas;
229
+
230
+ const stats: EstimatedSubmitProofStats = {
231
+ gasLimit,
232
+ baseFeePerGas,
233
+ maxPriorityFeePerGas,
234
+ estimatedTotalFee,
235
+ };
236
+
237
+ this.log.info(`Estimated epoch proof submission cost (not submitted)`, {
238
+ epochNumber,
239
+ fromCheckpoint,
240
+ toCheckpoint,
241
+ gasLimit: gasLimit.toString(),
242
+ baseFeePerGas: formatGwei(baseFeePerGas),
243
+ maxPriorityFeePerGas: formatGwei(maxPriorityFeePerGas),
244
+ estimatedTotalFeeEth: formatEther(estimatedTotalFee),
245
+ });
246
+
247
+ this.metrics.recordEstimatedSubmitProof(stats);
248
+ }
249
+
250
+ private encodeSubmitEpochProofCalldata(args: {
251
+ fromCheckpoint: CheckpointNumber;
252
+ toCheckpoint: CheckpointNumber;
253
+ publicInputs: RootRollupPublicInputs;
254
+ proof: Proof;
255
+ batchedBlobInputs: BatchedBlob;
256
+ attestations: ViemCommitteeAttestation[];
257
+ }): Hex {
258
+ return encodeFunctionData({
259
+ abi: RollupAbi,
260
+ functionName: 'submitEpochRootProof',
261
+ args: [this.getSubmitEpochProofArgs(args)],
262
+ });
263
+ }
264
+
213
265
  private async sendSubmitEpochProofTx(args: {
214
266
  fromCheckpoint: CheckpointNumber;
215
267
  toCheckpoint: CheckpointNumber;
268
+ deadline?: Date;
216
269
  publicInputs: RootRollupPublicInputs;
217
270
  proof: Proof;
218
271
  batchedBlobInputs: BatchedBlob;
@@ -231,7 +284,10 @@ export class ProverNodePublisher {
231
284
  args: txArgs,
232
285
  });
233
286
  try {
234
- const { receipt } = await this.l1TxUtils.sendAndMonitorTransaction({ to: this.rollupContract.address, data });
287
+ const { receipt } = await this.l1TxUtils.sendAndMonitorTransaction(
288
+ { to: this.rollupContract.address, data },
289
+ { txTimeoutAt: args.deadline },
290
+ );
235
291
  if (receipt.status !== 'success') {
236
292
  const errorMsg = await this.l1TxUtils.tryGetErrorFromRevertedTx(
237
293
  data,
@@ -296,11 +352,108 @@ export class ProverNodePublisher {
296
352
  end: argsArray[1],
297
353
  args: argsArray[2],
298
354
  fees: argsArray[3],
299
- attestations: new CommitteeAttestationsAndSigners(
355
+ attestations: CommitteeAttestationsAndSigners.packAttestations(
300
356
  args.attestations.map(a => CommitteeAttestation.fromViem(a)),
301
- ).getPackedAttestations(),
357
+ ),
302
358
  blobInputs: argsArray[4],
303
359
  proof: proofHex,
304
360
  };
305
361
  }
306
362
  }
363
+
364
+ /**
365
+ * Decodes a `Root rollup public inputs mismatch`, fetches the on-chain CheckpointLog for any
366
+ * mismatching `checkpointHeaderHashes[i]`, emits a structured error log, and returns a thrown-ready
367
+ * Error with a human-readable summary.
368
+ *
369
+ * Layout of `RootRollupPublicInputs.toFields()`:
370
+ * [0] previousArchiveRoot
371
+ * [1] endArchiveRoot
372
+ * [2] outHash
373
+ * [3 .. 3+N-1] checkpointHeaderHashes[i] for i in 0..N-1 (N = MAX_CHECKPOINTS_PER_EPOCH)
374
+ * [3+N .. 3+3N-1] fees[i] = (recipient, value) for i in 0..N-1
375
+ * [3+3N .. 3+3N+4] EpochConstantData (chainId, version, vkTreeRoot, protocolContractsHash, proverId)
376
+ * [3+3N+5 ..] blobPublicInputs (FinalBlobAccumulator)
377
+ */
378
+ async function reportPublicInputsMismatch(input: {
379
+ rollupPublicInputs: readonly Fr[];
380
+ argsPublicInputs: readonly Fr[];
381
+ fromCheckpoint: CheckpointNumber;
382
+ toCheckpoint: CheckpointNumber;
383
+ rollupContract: RollupContract;
384
+ log: Logger;
385
+ }): Promise<Error> {
386
+ const { rollupPublicInputs, argsPublicInputs, fromCheckpoint, toCheckpoint, rollupContract, log } = input;
387
+ const N = MAX_CHECKPOINTS_PER_EPOCH;
388
+ const constantsStart = 3 + 3 * N;
389
+ const blobStart = constantsStart + 5;
390
+ const constantLabels = ['chainId', 'version', 'vkTreeRoot', 'protocolContractsHash', 'proverId'];
391
+
392
+ const diffs: { index: number; label: string; rollup: Fr; computed: Fr; checkpointIndex?: number }[] = [];
393
+ const len = Math.max(rollupPublicInputs.length, argsPublicInputs.length);
394
+ for (let i = 0; i < len; i++) {
395
+ const a = rollupPublicInputs[i] ?? Fr.ZERO;
396
+ const b = argsPublicInputs[i] ?? Fr.ZERO;
397
+ if (a.equals(b)) {
398
+ continue;
399
+ }
400
+ let label: string;
401
+ let checkpointIndex: number | undefined;
402
+ if (i === 0) {
403
+ label = 'previousArchiveRoot';
404
+ } else if (i === 1) {
405
+ label = 'endArchiveRoot';
406
+ } else if (i === 2) {
407
+ label = 'outHash';
408
+ } else if (i < 3 + N) {
409
+ checkpointIndex = i - 3;
410
+ label = `checkpointHeaderHashes[${checkpointIndex}]`;
411
+ } else if (i < 3 + 3 * N) {
412
+ const feePairIndex = i - (3 + N);
413
+ const feeIndex = Math.floor(feePairIndex / 2);
414
+ const sub = feePairIndex % 2 === 0 ? 'recipient' : 'value';
415
+ label = `fees[${feeIndex}].${sub}`;
416
+ } else if (i < blobStart) {
417
+ label = `constants.${constantLabels[i - constantsStart]}`;
418
+ } else {
419
+ label = `blobPublicInputs[${i - blobStart}]`;
420
+ }
421
+ diffs.push({ index: i, label, rollup: a, computed: b, checkpointIndex });
422
+ }
423
+
424
+ // For each mismatching checkpointHeaderHash, fetch the L1 CheckpointLog so the operator can
425
+ // see what was published on-chain alongside the prover's recomputed hash.
426
+ const onChainCheckpoints = await Promise.all(
427
+ diffs
428
+ .filter(d => d.checkpointIndex !== undefined)
429
+ .map(async d => {
430
+ const checkpointNumber = CheckpointNumber(fromCheckpoint + d.checkpointIndex!);
431
+ try {
432
+ const cp = await rollupContract.getCheckpoint(checkpointNumber);
433
+ return { checkpointIndex: d.checkpointIndex!, checkpointNumber, headerHash: cp.headerHash.toString() };
434
+ } catch (err) {
435
+ return { checkpointIndex: d.checkpointIndex!, checkpointNumber, error: (err as Error).message };
436
+ }
437
+ }),
438
+ );
439
+
440
+ log.error(`Root rollup public inputs mismatch`, undefined, {
441
+ fromCheckpoint,
442
+ toCheckpoint,
443
+ numDiffs: diffs.length,
444
+ diffs: diffs.map(d => ({
445
+ index: d.index,
446
+ label: d.label,
447
+ rollup: d.rollup.toString(),
448
+ computed: d.computed.toString(),
449
+ })),
450
+ onChainCheckpoints,
451
+ });
452
+
453
+ const fmt = (inputs: readonly Fr[]) => inputs.map(x => x.toString()).join(', ');
454
+ const summary = diffs.map(d => `[${d.index} ${d.label}] L1=${d.rollup} prover=${d.computed}`).join('\n');
455
+ return new Error(
456
+ `Root rollup public inputs mismatch (${diffs.length} fields differ):\n${summary}\n` +
457
+ `Rollup: ${fmt(rollupPublicInputs)}\nComputed:${fmt(argsPublicInputs)}`,
458
+ );
459
+ }