@aztec/prover-node 0.85.0 → 0.86.0-nightly.20250426

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 (73) hide show
  1. package/dest/actions/download-epoch-proving-job.d.ts +18 -0
  2. package/dest/actions/download-epoch-proving-job.d.ts.map +1 -0
  3. package/dest/actions/download-epoch-proving-job.js +37 -0
  4. package/dest/actions/index.d.ts +3 -0
  5. package/dest/actions/index.d.ts.map +1 -0
  6. package/dest/actions/index.js +2 -0
  7. package/dest/actions/rerun-epoch-proving-job.d.ts +11 -0
  8. package/dest/actions/rerun-epoch-proving-job.d.ts.map +1 -0
  9. package/dest/actions/rerun-epoch-proving-job.js +38 -0
  10. package/dest/actions/upload-epoch-proof-failure.d.ts +15 -0
  11. package/dest/actions/upload-epoch-proof-failure.d.ts.map +1 -0
  12. package/dest/actions/upload-epoch-proof-failure.js +78 -0
  13. package/dest/bin/run-failed-epoch.d.ts +2 -0
  14. package/dest/bin/run-failed-epoch.d.ts.map +1 -0
  15. package/dest/bin/run-failed-epoch.js +67 -0
  16. package/dest/config.d.ts +4 -4
  17. package/dest/config.d.ts.map +1 -1
  18. package/dest/config.js +12 -7
  19. package/dest/factory.d.ts +3 -2
  20. package/dest/factory.d.ts.map +1 -1
  21. package/dest/factory.js +13 -13
  22. package/dest/index.d.ts +1 -0
  23. package/dest/index.d.ts.map +1 -1
  24. package/dest/index.js +1 -0
  25. package/dest/job/epoch-proving-job-data.d.ts +17 -0
  26. package/dest/job/epoch-proving-job-data.d.ts.map +1 -0
  27. package/dest/job/epoch-proving-job-data.js +45 -0
  28. package/dest/job/epoch-proving-job.d.ts +9 -9
  29. package/dest/job/epoch-proving-job.d.ts.map +1 -1
  30. package/dest/job/epoch-proving-job.js +38 -21
  31. package/dest/metrics.d.ts +27 -4
  32. package/dest/metrics.d.ts.map +1 -1
  33. package/dest/metrics.js +104 -36
  34. package/dest/monitors/epoch-monitor.d.ts +1 -1
  35. package/dest/monitors/epoch-monitor.d.ts.map +1 -1
  36. package/dest/monitors/epoch-monitor.js +7 -2
  37. package/dest/prover-coordination/combined-prover-coordination.d.ts +22 -0
  38. package/dest/prover-coordination/combined-prover-coordination.d.ts.map +1 -0
  39. package/dest/prover-coordination/combined-prover-coordination.js +136 -0
  40. package/dest/prover-coordination/config.d.ts +1 -1
  41. package/dest/prover-coordination/config.d.ts.map +1 -1
  42. package/dest/prover-coordination/config.js +5 -4
  43. package/dest/prover-coordination/factory.d.ts +5 -4
  44. package/dest/prover-coordination/factory.d.ts.map +1 -1
  45. package/dest/prover-coordination/factory.js +23 -14
  46. package/dest/prover-node-publisher.d.ts +1 -2
  47. package/dest/prover-node-publisher.d.ts.map +1 -1
  48. package/dest/prover-node-publisher.js +7 -13
  49. package/dest/prover-node.d.ts +26 -24
  50. package/dest/prover-node.d.ts.map +1 -1
  51. package/dest/prover-node.js +103 -51
  52. package/dest/test/index.d.ts +4 -2
  53. package/dest/test/index.d.ts.map +1 -1
  54. package/dest/test/index.js +1 -1
  55. package/package.json +22 -23
  56. package/src/actions/download-epoch-proving-job.ts +44 -0
  57. package/src/actions/index.ts +2 -0
  58. package/src/actions/rerun-epoch-proving-job.ts +60 -0
  59. package/src/actions/upload-epoch-proof-failure.ts +88 -0
  60. package/src/bin/run-failed-epoch.ts +77 -0
  61. package/src/config.ts +16 -10
  62. package/src/factory.ts +31 -17
  63. package/src/index.ts +1 -0
  64. package/src/job/epoch-proving-job-data.ts +68 -0
  65. package/src/job/epoch-proving-job.ts +53 -23
  66. package/src/metrics.ts +111 -38
  67. package/src/monitors/epoch-monitor.ts +5 -3
  68. package/src/prover-coordination/combined-prover-coordination.ts +160 -0
  69. package/src/prover-coordination/config.ts +6 -5
  70. package/src/prover-coordination/factory.ts +37 -25
  71. package/src/prover-node-publisher.ts +9 -18
  72. package/src/prover-node.ts +151 -74
  73. package/src/test/index.ts +5 -2
@@ -10,14 +10,14 @@ import {
10
10
  EpochProvingJobTerminalState,
11
11
  type ForkMerkleTreeOperations,
12
12
  } from '@aztec/stdlib/interfaces/server';
13
- import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
14
13
  import type { ProcessedTx, Tx } from '@aztec/stdlib/tx';
15
14
  import { Attributes, type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client';
16
15
 
17
16
  import * as crypto from 'node:crypto';
18
17
 
19
- import type { ProverNodeMetrics } from '../metrics.js';
18
+ import type { ProverNodeJobMetrics } from '../metrics.js';
20
19
  import type { ProverNodePublisher } from '../prover-node-publisher.js';
20
+ import { type EpochProvingJobData, validateEpochProvingJobData } from './epoch-proving-job-data.js';
21
21
 
22
22
  /**
23
23
  * Job that grabs a range of blocks from the unfinalised chain from L1, gets their txs given their hashes,
@@ -36,21 +36,19 @@ export class EpochProvingJob implements Traceable {
36
36
  public readonly tracer: Tracer;
37
37
 
38
38
  constructor(
39
- private dbProvider: ForkMerkleTreeOperations,
40
- private epochNumber: bigint,
41
- private blocks: L2Block[],
42
- private txs: Tx[],
39
+ private data: EpochProvingJobData,
40
+ private dbProvider: Pick<ForkMerkleTreeOperations, 'fork'>,
43
41
  private prover: EpochProver,
44
42
  private publicProcessorFactory: PublicProcessorFactory,
45
- private publisher: ProverNodePublisher,
46
- private l2BlockSource: L2BlockSource,
47
- private l1ToL2MessageSource: L1ToL2MessageSource,
48
- private metrics: ProverNodeMetrics,
43
+ private publisher: Pick<ProverNodePublisher, 'submitEpochProof'>,
44
+ private l2BlockSource: L2BlockSource | undefined,
45
+ private metrics: ProverNodeJobMetrics,
49
46
  private deadline: Date | undefined,
50
47
  private config: { parallelBlockLimit: number } = { parallelBlockLimit: 32 },
51
48
  ) {
49
+ validateEpochProvingJobData(data);
52
50
  this.uuid = crypto.randomUUID();
53
- this.tracer = metrics.client.getTracer('EpochProvingJob');
51
+ this.tracer = metrics.tracer;
54
52
  }
55
53
 
56
54
  public getId(): string {
@@ -62,18 +60,34 @@ export class EpochProvingJob implements Traceable {
62
60
  }
63
61
 
64
62
  public getEpochNumber(): bigint {
65
- return this.epochNumber;
63
+ return this.data.epochNumber;
66
64
  }
67
65
 
68
66
  public getDeadline(): Date | undefined {
69
67
  return this.deadline;
70
68
  }
71
69
 
70
+ public getProvingData(): EpochProvingJobData {
71
+ return this.data;
72
+ }
73
+
74
+ private get epochNumber() {
75
+ return this.data.epochNumber;
76
+ }
77
+
78
+ private get blocks() {
79
+ return this.data.blocks;
80
+ }
81
+
82
+ private get txs() {
83
+ return this.data.txs;
84
+ }
85
+
72
86
  /**
73
87
  * Proves the given epoch and submits the proof to L1.
74
88
  */
75
89
  @trackSpan('EpochProvingJob.run', function () {
76
- return { [Attributes.EPOCH_NUMBER]: Number(this.epochNumber) };
90
+ return { [Attributes.EPOCH_NUMBER]: Number(this.data.epochNumber) };
77
91
  })
78
92
  public async run() {
79
93
  this.scheduleDeadlineStop();
@@ -105,8 +119,8 @@ export class EpochProvingJob implements Traceable {
105
119
 
106
120
  const globalVariables = block.header.globalVariables;
107
121
  const txs = await this.getTxs(block);
108
- const l1ToL2Messages = await this.getL1ToL2Messages(block);
109
- const previousHeader = (await this.getBlockHeader(block.number - 1))!;
122
+ const l1ToL2Messages = this.getL1ToL2Messages(block);
123
+ const previousHeader = this.getBlockHeader(block.number - 1)!;
110
124
 
111
125
  this.log.verbose(`Starting processing block ${block.number}`, {
112
126
  number: block.number,
@@ -221,10 +235,16 @@ export class EpochProvingJob implements Traceable {
221
235
  * If those change, stops the proving job with a `rerun` state, so the node re-enqueues it.
222
236
  */
223
237
  private async scheduleEpochCheck() {
224
- const intervalMs = Math.ceil((await this.l2BlockSource.getL1Constants()).ethereumSlotDuration / 2) * 1000;
238
+ const l2BlockSource = this.l2BlockSource;
239
+ if (!l2BlockSource) {
240
+ this.log.warn(`No L2 block source available, skipping epoch check`);
241
+ return;
242
+ }
243
+
244
+ const intervalMs = Math.ceil((await l2BlockSource.getL1Constants()).ethereumSlotDuration / 2) * 1000;
225
245
  this.epochCheckPromise = new RunningPromise(
226
246
  async () => {
227
- const blocks = await this.l2BlockSource.getBlockHeadersForEpoch(this.epochNumber);
247
+ const blocks = await l2BlockSource.getBlockHeadersForEpoch(this.epochNumber);
228
248
  const blockHashes = await Promise.all(blocks.map(block => block.hash()));
229
249
  const thisBlockHashes = await Promise.all(this.blocks.map(block => block.hash()));
230
250
  if (
@@ -246,12 +266,22 @@ export class EpochProvingJob implements Traceable {
246
266
  this.log.verbose(`Scheduled epoch check for epoch ${this.epochNumber} every ${intervalMs}ms`);
247
267
  }
248
268
 
249
- /* Returns the header for the given block number, or the genesis block for block zero. */
250
- private async getBlockHeader(blockNumber: number) {
251
- if (blockNumber === 0) {
252
- return (await this.dbProvider.fork()).getInitialHeader();
269
+ /* Returns the header for the given block number based on the epoch proving job data. */
270
+ private getBlockHeader(blockNumber: number) {
271
+ const block = this.blocks.find(b => b.number === blockNumber);
272
+ if (block) {
273
+ return block.header;
253
274
  }
254
- return this.l2BlockSource.getBlockHeader(blockNumber);
275
+
276
+ if (blockNumber === Number(this.data.previousBlockHeader.getBlockNumber())) {
277
+ return this.data.previousBlockHeader;
278
+ }
279
+
280
+ throw new Error(
281
+ `Block header not found for block number ${blockNumber} (got ${this.blocks
282
+ .map(b => b.number)
283
+ .join(', ')} and previous header ${this.data.previousBlockHeader.getBlockNumber()})`,
284
+ );
255
285
  }
256
286
 
257
287
  private async getTxs(block: L2Block): Promise<Tx[]> {
@@ -263,7 +293,7 @@ export class EpochProvingJob implements Traceable {
263
293
  }
264
294
 
265
295
  private getL1ToL2Messages(block: L2Block) {
266
- return this.l1ToL2MessageSource.getL1ToL2Messages(BigInt(block.number));
296
+ return this.data.l1ToL2Messages[block.number];
267
297
  }
268
298
 
269
299
  private async processTxs(publicProcessor: PublicProcessor, txs: Tx[]): Promise<ProcessedTx[]> {
package/src/metrics.ts CHANGED
@@ -1,107 +1,187 @@
1
+ import type { RollupContract } from '@aztec/ethereum';
2
+ import type { EthAddress } from '@aztec/foundation/eth-address';
1
3
  import { createLogger } from '@aztec/foundation/log';
2
4
  import type { L1PublishProofStats, L1PublishStats } from '@aztec/stdlib/stats';
3
5
  import {
4
6
  Attributes,
7
+ type BatchObservableResult,
5
8
  type Gauge,
6
9
  type Histogram,
10
+ type Meter,
7
11
  Metrics,
12
+ type ObservableGauge,
8
13
  type TelemetryClient,
14
+ type Tracer,
9
15
  type UpDownCounter,
10
16
  ValueType,
11
17
  } from '@aztec/telemetry-client';
12
18
 
13
- import { formatEther } from 'viem';
19
+ import { formatEther, formatUnits } from 'viem';
14
20
 
15
- export class ProverNodeMetrics {
21
+ export class ProverNodeJobMetrics {
16
22
  proverEpochExecutionDuration: Histogram;
17
23
  provingJobDuration: Histogram;
18
24
  provingJobBlocks: Gauge;
19
25
  provingJobTransactions: Gauge;
20
26
 
21
- gasPrice: Histogram;
22
- txCount: UpDownCounter;
23
- txDuration: Histogram;
24
- txGas: Histogram;
25
- txCalldataSize: Histogram;
26
- txCalldataGas: Histogram;
27
- txBlobDataGasUsed: Histogram;
28
- txBlobDataGasCost: Histogram;
29
- txTotalFee: Histogram;
30
-
31
- private senderBalance: Gauge;
32
-
33
27
  constructor(
34
- public readonly client: TelemetryClient,
35
- name = 'ProverNode',
28
+ private meter: Meter,
29
+ public readonly tracer: Tracer,
36
30
  private logger = createLogger('prover-node:publisher:metrics'),
37
31
  ) {
38
- const meter = client.getMeter(name);
39
- this.proverEpochExecutionDuration = meter.createHistogram(Metrics.PROVER_NODE_EXECUTION_DURATION, {
32
+ this.proverEpochExecutionDuration = this.meter.createHistogram(Metrics.PROVER_NODE_EXECUTION_DURATION, {
40
33
  description: 'Duration of execution of an epoch by the prover',
41
34
  unit: 'ms',
42
35
  valueType: ValueType.INT,
43
36
  });
44
- this.provingJobDuration = meter.createHistogram(Metrics.PROVER_NODE_JOB_DURATION, {
37
+ this.provingJobDuration = this.meter.createHistogram(Metrics.PROVER_NODE_JOB_DURATION, {
45
38
  description: 'Duration of proving job',
46
39
  unit: 's',
47
40
  valueType: ValueType.DOUBLE,
48
41
  });
49
- this.provingJobBlocks = meter.createGauge(Metrics.PROVER_NODE_JOB_BLOCKS, {
42
+ this.provingJobBlocks = this.meter.createGauge(Metrics.PROVER_NODE_JOB_BLOCKS, {
50
43
  description: 'Number of blocks in a proven epoch',
51
44
  valueType: ValueType.INT,
52
45
  });
53
- this.provingJobTransactions = meter.createGauge(Metrics.PROVER_NODE_JOB_TRANSACTIONS, {
46
+ this.provingJobTransactions = this.meter.createGauge(Metrics.PROVER_NODE_JOB_TRANSACTIONS, {
54
47
  description: 'Number of transactions in a proven epoch',
55
48
  valueType: ValueType.INT,
56
49
  });
50
+ }
51
+
52
+ public recordProvingJob(executionTimeMs: number, totalTimeMs: number, numBlocks: number, numTxs: number) {
53
+ this.proverEpochExecutionDuration.record(Math.ceil(executionTimeMs));
54
+ this.provingJobDuration.record(totalTimeMs / 1000);
55
+ this.provingJobBlocks.record(Math.floor(numBlocks));
56
+ this.provingJobTransactions.record(Math.floor(numTxs));
57
+ }
58
+ }
59
+
60
+ export class ProverNodeRewardsMetrics {
61
+ private rewards: ObservableGauge;
62
+ private accumulatedRewards: UpDownCounter;
63
+ private prevEpoch = -1n;
64
+ private proofSubmissionWindow = 0n;
65
+
66
+ constructor(
67
+ private meter: Meter,
68
+ private coinbase: EthAddress,
69
+ private rollup: RollupContract,
70
+ private logger = createLogger('prover-node:publisher:metrics'),
71
+ ) {
72
+ this.rewards = this.meter.createObservableGauge(Metrics.PROVER_NODE_REWARDS_PER_EPOCH, {
73
+ valueType: ValueType.DOUBLE,
74
+ description: 'The rewards earned',
75
+ });
76
+
77
+ this.accumulatedRewards = this.meter.createUpDownCounter(Metrics.PROVER_NODE_REWARDS_TOTAL, {
78
+ valueType: ValueType.DOUBLE,
79
+ description: 'The rewards earned (total)',
80
+ });
81
+ }
82
+
83
+ public async start() {
84
+ this.proofSubmissionWindow = await this.rollup.getProofSubmissionWindow();
85
+ this.meter.addBatchObservableCallback(this.observe, [this.rewards]);
86
+ }
87
+
88
+ public stop() {
89
+ this.meter.removeBatchObservableCallback(this.observe, [this.rewards]);
90
+ }
91
+
92
+ private observe = async (observer: BatchObservableResult): Promise<void> => {
93
+ const slot = await this.rollup.getSlotNumber();
94
+
95
+ // look at the prev epoch so that we get an accurate value, after proof submission window has closed
96
+ if (slot > this.proofSubmissionWindow) {
97
+ const closedEpoch = await this.rollup.getEpochNumberForSlotNumber(slot - this.proofSubmissionWindow);
98
+ const rewards = await this.rollup.getSpecificProverRewardsForEpoch(closedEpoch, this.coinbase);
99
+
100
+ const fmt = parseFloat(formatUnits(rewards, 18));
101
+
102
+ observer.observe(this.rewards, fmt, {
103
+ [Attributes.COINBASE]: this.coinbase.toString(),
104
+ });
105
+
106
+ // only accumulate once per epoch
107
+ if (closedEpoch > this.prevEpoch) {
108
+ this.prevEpoch = closedEpoch;
109
+ this.accumulatedRewards.add(fmt, {
110
+ [Attributes.COINBASE]: this.coinbase.toString(),
111
+ });
112
+ }
113
+ }
114
+ };
115
+ }
57
116
 
58
- this.gasPrice = meter.createHistogram(Metrics.L1_PUBLISHER_GAS_PRICE, {
117
+ export class ProverNodePublisherMetrics {
118
+ gasPrice: Histogram;
119
+ txCount: UpDownCounter;
120
+ txDuration: Histogram;
121
+ txGas: Histogram;
122
+ txCalldataSize: Histogram;
123
+ txCalldataGas: Histogram;
124
+ txBlobDataGasUsed: Histogram;
125
+ txBlobDataGasCost: Histogram;
126
+ txTotalFee: Histogram;
127
+
128
+ private senderBalance: Gauge;
129
+ private meter: Meter;
130
+
131
+ constructor(
132
+ public readonly client: TelemetryClient,
133
+ name = 'ProverNode',
134
+ private logger = createLogger('prover-node:publisher:metrics'),
135
+ ) {
136
+ this.meter = client.getMeter(name);
137
+
138
+ this.gasPrice = this.meter.createHistogram(Metrics.L1_PUBLISHER_GAS_PRICE, {
59
139
  description: 'The gas price used for transactions',
60
140
  unit: 'gwei',
61
141
  valueType: ValueType.DOUBLE,
62
142
  });
63
143
 
64
- this.txCount = meter.createUpDownCounter(Metrics.L1_PUBLISHER_TX_COUNT, {
144
+ this.txCount = this.meter.createUpDownCounter(Metrics.L1_PUBLISHER_TX_COUNT, {
65
145
  description: 'The number of transactions processed',
66
146
  });
67
147
 
68
- this.txDuration = meter.createHistogram(Metrics.L1_PUBLISHER_TX_DURATION, {
148
+ this.txDuration = this.meter.createHistogram(Metrics.L1_PUBLISHER_TX_DURATION, {
69
149
  description: 'The duration of transaction processing',
70
150
  unit: 'ms',
71
151
  valueType: ValueType.INT,
72
152
  });
73
153
 
74
- this.txGas = meter.createHistogram(Metrics.L1_PUBLISHER_TX_GAS, {
154
+ this.txGas = this.meter.createHistogram(Metrics.L1_PUBLISHER_TX_GAS, {
75
155
  description: 'The gas consumed by transactions',
76
156
  unit: 'gas',
77
157
  valueType: ValueType.INT,
78
158
  });
79
159
 
80
- this.txCalldataSize = meter.createHistogram(Metrics.L1_PUBLISHER_TX_CALLDATA_SIZE, {
160
+ this.txCalldataSize = this.meter.createHistogram(Metrics.L1_PUBLISHER_TX_CALLDATA_SIZE, {
81
161
  description: 'The size of the calldata in transactions',
82
162
  unit: 'By',
83
163
  valueType: ValueType.INT,
84
164
  });
85
165
 
86
- this.txCalldataGas = meter.createHistogram(Metrics.L1_PUBLISHER_TX_CALLDATA_GAS, {
166
+ this.txCalldataGas = this.meter.createHistogram(Metrics.L1_PUBLISHER_TX_CALLDATA_GAS, {
87
167
  description: 'The gas consumed by the calldata in transactions',
88
168
  unit: 'gas',
89
169
  valueType: ValueType.INT,
90
170
  });
91
171
 
92
- this.txBlobDataGasUsed = meter.createHistogram(Metrics.L1_PUBLISHER_TX_BLOBDATA_GAS_USED, {
172
+ this.txBlobDataGasUsed = this.meter.createHistogram(Metrics.L1_PUBLISHER_TX_BLOBDATA_GAS_USED, {
93
173
  description: 'The amount of blob gas used in transactions',
94
174
  unit: 'gas',
95
175
  valueType: ValueType.INT,
96
176
  });
97
177
 
98
- this.txBlobDataGasCost = meter.createHistogram(Metrics.L1_PUBLISHER_TX_BLOBDATA_GAS_COST, {
178
+ this.txBlobDataGasCost = this.meter.createHistogram(Metrics.L1_PUBLISHER_TX_BLOBDATA_GAS_COST, {
99
179
  description: 'The gas cost of blobs in transactions',
100
180
  unit: 'gwei',
101
181
  valueType: ValueType.INT,
102
182
  });
103
183
 
104
- this.txTotalFee = meter.createHistogram(Metrics.L1_PUBLISHER_TX_TOTAL_FEE, {
184
+ this.txTotalFee = this.meter.createHistogram(Metrics.L1_PUBLISHER_TX_TOTAL_FEE, {
105
185
  description: 'How much L1 tx costs',
106
186
  unit: 'gwei',
107
187
  valueType: ValueType.DOUBLE,
@@ -112,7 +192,7 @@ export class ProverNodeMetrics {
112
192
  },
113
193
  });
114
194
 
115
- this.senderBalance = meter.createGauge(Metrics.L1_PUBLISHER_BALANCE, {
195
+ this.senderBalance = this.meter.createGauge(Metrics.L1_PUBLISHER_BALANCE, {
116
196
  unit: 'eth',
117
197
  description: 'The balance of the sender address',
118
198
  valueType: ValueType.DOUBLE,
@@ -130,13 +210,6 @@ export class ProverNodeMetrics {
130
210
  this.recordTx(durationMs, stats);
131
211
  }
132
212
 
133
- public recordProvingJob(executionTimeMs: number, totalTimeMs: number, numBlocks: number, numTxs: number) {
134
- this.proverEpochExecutionDuration.record(Math.ceil(executionTimeMs));
135
- this.provingJobDuration.record(totalTimeMs / 1000);
136
- this.provingJobBlocks.record(Math.floor(numBlocks));
137
- this.provingJobTransactions.record(Math.floor(numTxs));
138
- }
139
-
140
213
  public recordSenderBalance(wei: bigint, senderAddress: string) {
141
214
  const eth = parseFloat(formatEther(wei, 'wei'));
142
215
  this.senderBalance.record(eth, {
@@ -11,7 +11,7 @@ import {
11
11
  } from '@aztec/telemetry-client';
12
12
 
13
13
  export interface EpochMonitorHandler {
14
- handleEpochReadyToProve(epochNumber: bigint): Promise<void>;
14
+ handleEpochReadyToProve(epochNumber: bigint): Promise<boolean>;
15
15
  }
16
16
 
17
17
  /**
@@ -71,6 +71,7 @@ export class EpochMonitor implements Traceable {
71
71
  @trackSpan('EpochMonitor.work')
72
72
  public async work() {
73
73
  const { epochToProve, blockNumber, slotNumber } = await this.getEpochNumberToProve();
74
+ this.log.debug(`Epoch to prove: ${epochToProve}`, { blockNumber, slotNumber });
74
75
  if (epochToProve === undefined) {
75
76
  this.log.trace(`Next block to prove ${blockNumber} not yet mined`, { blockNumber });
76
77
  return;
@@ -87,8 +88,9 @@ export class EpochMonitor implements Traceable {
87
88
  }
88
89
 
89
90
  this.log.debug(`Epoch ${epochToProve} is ready to be proven`);
90
- await this.handler?.handleEpochReadyToProve(epochToProve);
91
- this.latestEpochNumber = epochToProve;
91
+ if (await this.handler?.handleEpochReadyToProve(epochToProve)) {
92
+ this.latestEpochNumber = epochToProve;
93
+ }
92
94
  }
93
95
 
94
96
  private async getEpochNumberToProve() {
@@ -0,0 +1,160 @@
1
+ import { asyncPool } from '@aztec/foundation/async-pool';
2
+ import { createLogger } from '@aztec/foundation/log';
3
+ import type { P2P } from '@aztec/p2p';
4
+ import type { P2PClient, ProverCoordination } from '@aztec/stdlib/interfaces/server';
5
+ import { type Tx, TxHash } from '@aztec/stdlib/tx';
6
+
7
+ export type CombinedCoordinationOptions = {
8
+ // These options apply to http tx gathering only
9
+ txGatheringBatchSize: number;
10
+ txGatheringMaxParallelRequestsPerNode: number;
11
+ };
12
+
13
+ interface CoordinationPool {
14
+ getTxsByHash(txHashes: TxHash[]): Promise<(Tx | undefined)[]>;
15
+ hasTxsInPool(txHashes: TxHash[]): Promise<boolean[]>;
16
+ getTxsByHashFromPool(txHashes: TxHash[]): Promise<(Tx | undefined)[]>;
17
+ addTxs(txs: Tx[]): Promise<void>;
18
+ }
19
+
20
+ export interface TxSource {
21
+ getTxsByHash(txHashes: TxHash[]): Promise<(Tx | undefined)[]>;
22
+ }
23
+
24
+ // Wraps the p2p client into a coordination pool
25
+ class P2PCoordinationPool implements CoordinationPool {
26
+ constructor(private readonly p2p: P2P) {}
27
+ getTxsByHash(txHashes: TxHash[]): Promise<(Tx | undefined)[]> {
28
+ return this.p2p.getTxsByHash(txHashes);
29
+ }
30
+ hasTxsInPool(txHashes: TxHash[]): Promise<boolean[]> {
31
+ return this.p2p.hasTxsInPool(txHashes);
32
+ }
33
+ getTxsByHashFromPool(txHashes: TxHash[]): Promise<(Tx | undefined)[]> {
34
+ return this.p2p.getTxsByHashFromPool(txHashes);
35
+ }
36
+ addTxs(txs: Tx[]): Promise<void> {
37
+ return this.p2p.addTxs(txs);
38
+ }
39
+ }
40
+
41
+ // Wraps an in memory tx pool into a coordination pool. Used for testing when no p2p/tx pool is available.
42
+ class InMemoryCoordinationPool implements CoordinationPool {
43
+ private txs: Map<string, Tx> = new Map();
44
+ getTxsByHash(txHashes: TxHash[]): Promise<(Tx | undefined)[]> {
45
+ return Promise.resolve(txHashes.map(hash => this.txs.get(hash.toString())));
46
+ }
47
+ hasTxsInPool(txHashes: TxHash[]): Promise<boolean[]> {
48
+ return Promise.resolve(txHashes.map(hash => this.txs.has(hash.toString())));
49
+ }
50
+ getTxsByHashFromPool(txHashes: TxHash[]): Promise<(Tx | undefined)[]> {
51
+ return this.getTxsByHash(txHashes);
52
+ }
53
+ async addTxs(txs: Tx[]): Promise<void> {
54
+ const hashes = await Promise.all(txs.map(tx => tx.getTxHash()));
55
+ txs.forEach((tx, index) => this.txs.set(hashes[index].toString(), tx));
56
+ }
57
+ }
58
+
59
+ // Class to implement combined transaction retrieval from p2p and any available nodes
60
+ export class CombinedProverCoordination implements ProverCoordination {
61
+ constructor(
62
+ public readonly p2p: P2P | undefined,
63
+ public readonly aztecNodes: TxSource[],
64
+ private readonly options: CombinedCoordinationOptions = {
65
+ txGatheringBatchSize: 10,
66
+ txGatheringMaxParallelRequestsPerNode: 10,
67
+ },
68
+ private readonly log = createLogger('prover-node:combined-prover-coordination'),
69
+ ) {}
70
+
71
+ public getP2PClient(): P2PClient | undefined {
72
+ return this.p2p;
73
+ }
74
+
75
+ public async getTxsByHash(txHashes: TxHash[]): Promise<Tx[]> {
76
+ const pool = this.p2p ? new P2PCoordinationPool(this.p2p) : new InMemoryCoordinationPool();
77
+ await this.#gatherTxs(txHashes, pool);
78
+ const availability = await pool.hasTxsInPool(txHashes);
79
+ const notFound = txHashes.filter((_, index) => !availability[index]);
80
+ if (notFound.length > 0) {
81
+ throw new Error(`Could not find txs: ${notFound.map(tx => tx.toString())}`);
82
+ }
83
+ const txs = await pool.getTxsByHashFromPool(txHashes);
84
+ return txs.filter(tx => tx !== undefined) as Tx[];
85
+ }
86
+
87
+ public async gatherTxs(txHashes: TxHash[]): Promise<void> {
88
+ const pool = this.p2p ? new P2PCoordinationPool(this.p2p) : new InMemoryCoordinationPool();
89
+ await this.#gatherTxs(txHashes, pool);
90
+ }
91
+
92
+ async #gatherTxs(txHashes: TxHash[], pool: CoordinationPool): Promise<void> {
93
+ const availability = await pool.hasTxsInPool(txHashes);
94
+ const notFound = txHashes.filter((_, index) => !availability[index]);
95
+ const txsToFind = new Set(notFound.map(tx => tx.toString()));
96
+ if (txsToFind.size === 0) {
97
+ this.log.info(`Check for ${txHashes.length} txs found all in the pool`);
98
+ return;
99
+ }
100
+ this.log.info(`Check for ${txHashes.length} txs found ${txsToFind.size} missing. Will gather from nodes and p2p`);
101
+ const originalToFind = txsToFind.size;
102
+ await this.#gatherTxsFromAllNodes(txsToFind, pool);
103
+ if (txsToFind.size === 0) {
104
+ this.log.info(`Found all ${originalToFind} txs directly from nodes`);
105
+ return;
106
+ }
107
+ const toFindFromP2P = txsToFind.size;
108
+ this.log.verbose(`Gathering ${toFindFromP2P} txs from p2p network`);
109
+ const foundFromP2P = await pool.getTxsByHash([...txsToFind].map(tx => TxHash.fromString(tx)));
110
+ const numFoundFromNodes = originalToFind - toFindFromP2P;
111
+ const numNotFound = toFindFromP2P - foundFromP2P.length;
112
+ if (numNotFound === 0) {
113
+ this.log.info(
114
+ `Found all ${originalToFind} txs. ${numFoundFromNodes} from nodes, ${foundFromP2P.length} from p2p`,
115
+ );
116
+ return;
117
+ }
118
+ this.log.warn(
119
+ `Failed to find ${numNotFound} txs from any source. Found ${foundFromP2P.length} from p2p and ${numFoundFromNodes} from nodes`,
120
+ );
121
+ }
122
+
123
+ async #gatherTxsFromAllNodes(txsToFind: Set<string>, pool: CoordinationPool) {
124
+ if (txsToFind.size === 0 || this.aztecNodes.length === 0) {
125
+ return;
126
+ }
127
+ await Promise.all(this.aztecNodes.map(aztecNode => this.#gatherTxsFromNode(txsToFind, aztecNode, pool)));
128
+ }
129
+
130
+ async #gatherTxsFromNode(txsToFind: Set<string>, aztecNode: TxSource, pool: CoordinationPool) {
131
+ const totalTxsRequired = txsToFind.size;
132
+
133
+ // It's possible that the set is empty as we already found the txs
134
+ if (totalTxsRequired === 0) {
135
+ return;
136
+ }
137
+ let totalTxsGathered = 0;
138
+
139
+ const batches: string[][] = [];
140
+ const allTxs: string[] = [...txsToFind];
141
+ while (allTxs.length) {
142
+ const batch = allTxs.splice(0, this.options.txGatheringBatchSize);
143
+ batches.push(batch);
144
+ }
145
+
146
+ await asyncPool(this.options.txGatheringMaxParallelRequestsPerNode, batches, async batch => {
147
+ try {
148
+ const txs = (await aztecNode.getTxsByHash(batch.map(b => TxHash.fromString(b)))).filter((tx): tx is Tx => !!tx);
149
+ const hashes = await Promise.all(txs.map(tx => tx.getTxHash()));
150
+ await pool.addTxs(txs);
151
+ hashes.forEach(hash => txsToFind.delete(hash.toString()));
152
+ totalTxsGathered += txs.length;
153
+ } catch (err) {
154
+ this.log.error(`Error gathering txs from aztec node: ${err}`);
155
+ return;
156
+ }
157
+ });
158
+ this.log.verbose(`Gathered ${totalTxsGathered} of ${totalTxsRequired} txs from a node`);
159
+ }
160
+ }
@@ -1,14 +1,15 @@
1
1
  import { type ConfigMappingsType, getConfigFromMappings } from '@aztec/foundation/config';
2
2
 
3
3
  export type ProverCoordinationConfig = {
4
- proverCoordinationNodeUrl: string | undefined;
4
+ proverCoordinationNodeUrls: string[];
5
5
  };
6
6
 
7
7
  export const proverCoordinationConfigMappings: ConfigMappingsType<ProverCoordinationConfig> = {
8
- proverCoordinationNodeUrl: {
9
- env: 'PROVER_COORDINATION_NODE_URL',
10
- description: 'The URL of the tx provider node',
11
- parseEnv: (val: string) => val,
8
+ proverCoordinationNodeUrls: {
9
+ env: 'PROVER_COORDINATION_NODE_URLS',
10
+ description: 'The URLs of the tx provider nodes',
11
+ parseEnv: (val: string) => val.split(',').map(url => url.trim().replace(/\/$/, '')),
12
+ defaultValue: [],
12
13
  },
13
14
  };
14
15