@aztec/prover-node 0.0.1-commit.c31f2472 → 0.0.1-commit.c52d6e7

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