@aztec/prover-node 0.0.1-commit.3469e52 → 0.0.1-commit.350e0a4d

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 +518 -0
  2. package/dest/actions/download-epoch-proving-job.js +1 -1
  3. package/dest/actions/rerun-epoch-proving-job.d.ts +6 -4
  4. package/dest/actions/rerun-epoch-proving-job.d.ts.map +1 -1
  5. package/dest/actions/rerun-epoch-proving-job.js +187 -27
  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 +87 -0
  10. package/dest/checkpoint-store.d.ts.map +1 -0
  11. package/dest/checkpoint-store.js +186 -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 -69
  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 +134 -0
  22. package/dest/job/checkpoint-prover.d.ts.map +1 -0
  23. package/dest/job/checkpoint-prover.js +353 -0
  24. package/dest/job/epoch-session.d.ts +146 -0
  25. package/dest/job/epoch-session.d.ts.map +1 -0
  26. package/dest/job/{epoch-proving-job.js → epoch-session.js} +277 -295
  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 +112 -7
  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 +28 -17
  40. package/dest/prover-node-publisher.d.ts.map +1 -1
  41. package/dest/prover-node-publisher.js +205 -65
  42. package/dest/prover-node.d.ts +118 -70
  43. package/dest/prover-node.d.ts.map +1 -1
  44. package/dest/prover-node.js +489 -226
  45. package/dest/prover-publisher-factory.d.ts +9 -5
  46. package/dest/prover-publisher-factory.d.ts.map +1 -1
  47. package/dest/prover-publisher-factory.js +8 -5
  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 +452 -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 +117 -29
  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 +218 -0
  59. package/src/config.ts +35 -32
  60. package/src/factory.ts +69 -112
  61. package/src/index.ts +1 -0
  62. package/src/job/checkpoint-prover.ts +468 -0
  63. package/src/job/epoch-session.ts +436 -0
  64. package/src/job/top-tree-job.ts +227 -0
  65. package/src/metrics.ts +129 -12
  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 +241 -81
  69. package/src/prover-node.ts +563 -254
  70. package/src/prover-publisher-factory.ts +19 -10
  71. package/src/session-manager.ts +552 -0
  72. package/src/test/index.ts +6 -6
  73. package/dest/job/epoch-proving-job.d.ts +0 -62
  74. package/dest/job/epoch-proving-job.d.ts.map +0 -1
  75. package/src/job/epoch-proving-job.ts +0 -427
@@ -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
- import { createLogger } from '@aztec/foundation/log';
11
- import type { Tuple } from '@aztec/foundation/serialize';
9
+ import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
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
- protected log = createLogger('prover-node:l1-tx-publisher');
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,14 +49,18 @@ export class ProverNodePublisher {
50
49
  deps: {
51
50
  rollupContract: RollupContract;
52
51
  l1TxUtils: L1TxUtils;
52
+ proofSubmissionTarget?: EthAddress;
53
53
  telemetry?: TelemetryClient;
54
54
  },
55
+ bindings?: LoggerBindings,
55
56
  ) {
56
57
  const telemetry = deps.telemetry ?? getTelemetryClient();
57
58
 
58
59
  this.metrics = new ProverNodePublisherMetrics(telemetry, 'ProverNode');
60
+ this.log = createLogger('prover-node:l1-tx-publisher', bindings);
59
61
 
60
62
  this.rollupContract = deps.rollupContract;
63
+ this.proofSubmissionTarget = deps.proofSubmissionTarget?.toString() ?? deps.rollupContract.address;
61
64
  this.l1TxUtils = deps.l1TxUtils;
62
65
  }
63
66
 
@@ -65,23 +68,6 @@ export class ProverNodePublisher {
65
68
  return this.rollupContract;
66
69
  }
67
70
 
68
- /**
69
- * Calling `interrupt` will cause any in progress call to `publishRollup` to return `false` asap.
70
- * Be warned, the call may return false even if the tx subsequently gets successfully mined.
71
- * In practice this shouldn't matter, as we'll only ever be calling `interrupt` when we know it's going to fail.
72
- * A call to `restart` is required before you can continue publishing.
73
- */
74
- public interrupt() {
75
- this.interrupted = true;
76
- this.l1TxUtils.interrupt();
77
- }
78
-
79
- /** Restarts the publisher after calling `interrupt`. */
80
- public restart() {
81
- this.interrupted = false;
82
- this.l1TxUtils.restart();
83
- }
84
-
85
71
  public getSenderAddress() {
86
72
  return this.l1TxUtils.getSenderAddress();
87
73
  }
@@ -94,54 +80,53 @@ export class ProverNodePublisher {
94
80
  proof: Proof;
95
81
  batchedBlobInputs: BatchedBlob;
96
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;
97
86
  }): Promise<boolean> {
98
87
  const { epochNumber, fromCheckpoint, toCheckpoint } = args;
99
88
  const ctx = { epochNumber, fromCheckpoint, toCheckpoint };
100
89
 
101
- if (!this.interrupted) {
102
- const timer = new Timer();
103
- // Validate epoch proof range and hashes are correct before submitting
104
- 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);
105
93
 
106
- const txReceipt = await this.sendSubmitEpochProofTx(args);
107
- if (!txReceipt) {
108
- this.log.error(`Failed to mine submitEpochProof tx`, undefined, ctx);
109
- return false;
110
- }
111
-
112
- try {
113
- this.metrics.recordSenderBalance(
114
- await this.l1TxUtils.getSenderBalance(),
115
- this.l1TxUtils.getSenderAddress().toString(),
116
- );
117
- } catch (err) {
118
- this.log.warn(`Failed to record the ETH balance of the prover node: ${err}`);
119
- }
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
+ }
120
99
 
121
- // Tx was mined successfully
122
- if (txReceipt.status === 'success') {
123
- const tx = await this.l1TxUtils.getTransactionStats(txReceipt.transactionHash);
124
- const stats: L1PublishProofStats = {
125
- gasPrice: txReceipt.effectiveGasPrice,
126
- gasUsed: txReceipt.gasUsed,
127
- transactionHash: txReceipt.transactionHash,
128
- calldataGas: tx!.calldataGas,
129
- calldataSize: tx!.calldataSize,
130
- sender: tx!.sender,
131
- blobDataGas: 0n,
132
- blobGasUsed: 0n,
133
- eventName: 'proof-published-to-l1',
134
- };
135
- this.log.info(`Published epoch proof to L1 rollup contract`, { ...stats, ...ctx });
136
- this.metrics.recordSubmitProof(timer.ms(), stats);
137
- return true;
138
- }
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
+ }
139
108
 
140
- this.metrics.recordFailedTx();
141
- 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;
142
126
  }
143
127
 
144
- this.log.verbose('Checkpoint data syncing interrupted', ctx);
128
+ this.metrics.recordFailedTx();
129
+ this.log.error(`Rollup submitEpochProof tx reverted ${txReceipt.transactionHash}`, undefined, ctx);
145
130
  return false;
146
131
  }
147
132
 
@@ -152,6 +137,7 @@ export class ProverNodePublisher {
152
137
  proof: Proof;
153
138
  batchedBlobInputs: BatchedBlob;
154
139
  attestations: ViemCommitteeAttestation[];
140
+ headers: CheckpointHeader[];
155
141
  }) {
156
142
  const { fromCheckpoint, toCheckpoint, publicInputs, batchedBlobInputs } = args;
157
143
 
@@ -166,7 +152,7 @@ export class ProverNodePublisher {
166
152
  // toCheckpoint can't be greater than pending
167
153
  if (toCheckpoint > pending) {
168
154
  throw new Error(
169
- `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}`,
170
156
  );
171
157
  }
172
158
 
@@ -201,20 +187,96 @@ export class ProverNodePublisher {
201
187
  const argsPublicInputs = [...publicInputs.toFields()];
202
188
 
203
189
  if (!areArraysEqual(rollupPublicInputs, argsPublicInputs, (a, b) => a.equals(b))) {
204
- const fmt = (inputs: Fr[] | readonly string[]) => inputs.map(x => x.toString()).join(', ');
205
- throw new Error(
206
- `Root rollup public inputs mismatch:\nRollup: ${fmt(rollupPublicInputs)}\nComputed:${fmt(argsPublicInputs)}`,
207
- );
190
+ throw await reportPublicInputsMismatch({
191
+ rollupPublicInputs,
192
+ argsPublicInputs,
193
+ fromCheckpoint,
194
+ toCheckpoint,
195
+ rollupContract: this.rollupContract,
196
+ log: this.log,
197
+ });
208
198
  }
209
199
  }
210
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
+
211
271
  private async sendSubmitEpochProofTx(args: {
212
272
  fromCheckpoint: CheckpointNumber;
213
273
  toCheckpoint: CheckpointNumber;
274
+ deadline?: Date;
214
275
  publicInputs: RootRollupPublicInputs;
215
276
  proof: Proof;
216
277
  batchedBlobInputs: BatchedBlob;
217
278
  attestations: ViemCommitteeAttestation[];
279
+ headers: CheckpointHeader[];
218
280
  }): Promise<TransactionReceipt | undefined> {
219
281
  const txArgs = [this.getSubmitEpochProofArgs(args)] as const;
220
282
 
@@ -229,7 +291,10 @@ export class ProverNodePublisher {
229
291
  args: txArgs,
230
292
  });
231
293
  try {
232
- 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
+ );
233
298
  if (receipt.status !== 'success') {
234
299
  const errorMsg = await this.l1TxUtils.tryGetErrorFromRevertedTx(
235
300
  data,
@@ -237,7 +302,7 @@ export class ProverNodePublisher {
237
302
  args: [...txArgs],
238
303
  functionName: 'submitEpochRootProof',
239
304
  abi: RollupAbi,
240
- address: this.rollupContract.address,
305
+ address: this.proofSubmissionTarget,
241
306
  },
242
307
  /*blobInputs*/ undefined,
243
308
  /*stateOverride*/ [],
@@ -258,6 +323,7 @@ export class ProverNodePublisher {
258
323
  publicInputs: RootRollupPublicInputs;
259
324
  batchedBlobInputs: BatchedBlob;
260
325
  attestations: ViemCommitteeAttestation[];
326
+ headers: CheckpointHeader[];
261
327
  }) {
262
328
  // Returns arguments for EpochProofLib.sol -> getEpochProofPublicInputs()
263
329
  return [
@@ -269,11 +335,7 @@ export class ProverNodePublisher {
269
335
  outHash: args.publicInputs.outHash.toString(),
270
336
  proverId: EthAddress.fromField(args.publicInputs.constants.proverId).toString(),
271
337
  } /*_args*/,
272
- makeTuple(AZTEC_MAX_EPOCH_DURATION * 2, i =>
273
- i % 2 === 0
274
- ? args.publicInputs.fees[i / 2].recipient.toField().toString()
275
- : args.publicInputs.fees[(i - 1) / 2].value.toString(),
276
- ) /*_fees*/,
338
+ args.headers.map(header => header.toViem()) /*_headers*/,
277
339
  getEthBlobEvaluationInputs(args.batchedBlobInputs) /*_blobPublicInputs*/,
278
340
  ] as const;
279
341
  }
@@ -285,6 +347,7 @@ export class ProverNodePublisher {
285
347
  proof: Proof;
286
348
  batchedBlobInputs: BatchedBlob;
287
349
  attestations: ViemCommitteeAttestation[];
350
+ headers: CheckpointHeader[];
288
351
  }) {
289
352
  // Returns arguments for EpochProofLib.sol -> submitEpochRootProof()
290
353
  const proofHex: Hex = `0x${args.proof.withoutPublicInputs().toString('hex')}`;
@@ -293,12 +356,109 @@ export class ProverNodePublisher {
293
356
  start: argsArray[0],
294
357
  end: argsArray[1],
295
358
  args: argsArray[2],
296
- fees: argsArray[3],
297
- attestations: new CommitteeAttestationsAndSigners(
359
+ headers: argsArray[3],
360
+ attestations: CommitteeAttestationsAndSigners.packAttestations(
298
361
  args.attestations.map(a => CommitteeAttestation.fromViem(a)),
299
- ).getPackedAttestations(),
362
+ ),
300
363
  blobInputs: argsArray[4],
301
364
  proof: proofHex,
302
365
  };
303
366
  }
304
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
+ }