@aztec/prover-node 0.66.0 → 0.67.1-devnet

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 (46) hide show
  1. package/dest/bond/bond-manager.d.ts.map +1 -1
  2. package/dest/bond/bond-manager.js +3 -3
  3. package/dest/bond/token-contract.d.ts.map +1 -1
  4. package/dest/bond/token-contract.js +3 -3
  5. package/dest/config.d.ts +7 -7
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +7 -7
  8. package/dest/factory.d.ts +2 -2
  9. package/dest/factory.d.ts.map +1 -1
  10. package/dest/factory.js +6 -10
  11. package/dest/job/epoch-proving-job.d.ts +3 -1
  12. package/dest/job/epoch-proving-job.d.ts.map +1 -1
  13. package/dest/job/epoch-proving-job.js +146 -122
  14. package/dest/metrics.d.ts +1 -0
  15. package/dest/metrics.d.ts.map +1 -1
  16. package/dest/metrics.js +3 -5
  17. package/dest/monitors/claims-monitor.d.ts +4 -2
  18. package/dest/monitors/claims-monitor.d.ts.map +1 -1
  19. package/dest/monitors/claims-monitor.js +52 -35
  20. package/dest/monitors/epoch-monitor.d.ts +4 -2
  21. package/dest/monitors/epoch-monitor.d.ts.map +1 -1
  22. package/dest/monitors/epoch-monitor.js +48 -32
  23. package/dest/prover-coordination/factory.d.ts.map +1 -1
  24. package/dest/prover-coordination/factory.js +5 -5
  25. package/dest/prover-node.d.ts +8 -7
  26. package/dest/prover-node.d.ts.map +1 -1
  27. package/dest/prover-node.js +224 -202
  28. package/package.json +22 -17
  29. package/src/bond/bond-manager.ts +2 -2
  30. package/src/bond/token-contract.ts +2 -2
  31. package/src/config.ts +7 -7
  32. package/src/factory.ts +5 -11
  33. package/src/job/epoch-proving-job.ts +32 -23
  34. package/src/metrics.ts +2 -5
  35. package/src/monitors/claims-monitor.ts +15 -5
  36. package/src/monitors/epoch-monitor.ts +13 -5
  37. package/src/prover-coordination/factory.ts +9 -3
  38. package/src/prover-node.ts +35 -23
  39. package/dest/prover-cache/cache_manager.d.ts +0 -15
  40. package/dest/prover-cache/cache_manager.d.ts.map +0 -1
  41. package/dest/prover-cache/cache_manager.js +0 -57
  42. package/dest/prover-cache/kv_cache.d.ts +0 -11
  43. package/dest/prover-cache/kv_cache.d.ts.map +0 -1
  44. package/dest/prover-cache/kv_cache.js +0 -20
  45. package/src/prover-cache/cache_manager.ts +0 -69
  46. package/src/prover-cache/kv_cache.ts +0 -27
package/src/factory.ts CHANGED
@@ -2,7 +2,7 @@ import { type Archiver, createArchiver } from '@aztec/archiver';
2
2
  import { type ProverCoordination, type ProvingJobBroker } from '@aztec/circuit-types';
3
3
  import { createEthereumChain } from '@aztec/ethereum';
4
4
  import { Buffer32 } from '@aztec/foundation/buffer';
5
- import { type DebugLogger, createDebugLogger } from '@aztec/foundation/log';
5
+ import { type Logger, createLogger } from '@aztec/foundation/log';
6
6
  import { type DataStoreConfig } from '@aztec/kv-store/config';
7
7
  import { RollupAbi } from '@aztec/l1-artifacts';
8
8
  import { createProverClient } from '@aztec/prover-client';
@@ -12,14 +12,12 @@ import { type TelemetryClient } from '@aztec/telemetry-client';
12
12
  import { NoopTelemetryClient } from '@aztec/telemetry-client/noop';
13
13
  import { createWorldStateSynchronizer } from '@aztec/world-state';
14
14
 
15
- import { join } from 'path';
16
15
  import { createPublicClient, getAddress, getContract, http } from 'viem';
17
16
 
18
17
  import { createBondManager } from './bond/factory.js';
19
18
  import { type ProverNodeConfig, type QuoteProviderConfig } from './config.js';
20
19
  import { ClaimsMonitor } from './monitors/claims-monitor.js';
21
20
  import { EpochMonitor } from './monitors/epoch-monitor.js';
22
- import { ProverCacheManager } from './prover-cache/cache_manager.js';
23
21
  import { createProverCoordination } from './prover-coordination/factory.js';
24
22
  import { ProverNode, type ProverNodeOptions } from './prover-node.js';
25
23
  import { HttpQuoteProvider } from './quote-provider/http.js';
@@ -31,7 +29,7 @@ export async function createProverNode(
31
29
  config: ProverNodeConfig & DataStoreConfig,
32
30
  deps: {
33
31
  telemetry?: TelemetryClient;
34
- log?: DebugLogger;
32
+ log?: Logger;
35
33
  aztecNodeTxProvider?: ProverCoordination;
36
34
  archiver?: Archiver;
37
35
  publisher?: L1Publisher;
@@ -39,7 +37,7 @@ export async function createProverNode(
39
37
  } = {},
40
38
  ) {
41
39
  const telemetry = deps.telemetry ?? new NoopTelemetryClient();
42
- const log = deps.log ?? createDebugLogger('aztec:prover');
40
+ const log = deps.log ?? createLogger('prover-node');
43
41
  const archiver = deps.archiver ?? (await createArchiver(config, telemetry, { blockUntilSync: true }));
44
42
  log.verbose(`Created archiver and synced to block ${await archiver.getBlockNumber()}`);
45
43
 
@@ -71,16 +69,13 @@ export async function createProverNode(
71
69
  maxParallelBlocksPerEpoch: config.proverNodeMaxParallelBlocksPerEpoch,
72
70
  };
73
71
 
74
- const claimsMonitor = new ClaimsMonitor(publisher, proverNodeConfig);
75
- const epochMonitor = new EpochMonitor(archiver, proverNodeConfig);
72
+ const claimsMonitor = new ClaimsMonitor(publisher, telemetry, proverNodeConfig);
73
+ const epochMonitor = new EpochMonitor(archiver, telemetry, proverNodeConfig);
76
74
 
77
75
  const rollupContract = publisher.getRollupContract();
78
76
  const walletClient = publisher.getClient();
79
77
  const bondManager = await createBondManager(rollupContract, walletClient, config);
80
78
 
81
- const cacheDir = config.cacheDir ? join(config.cacheDir, `prover_${config.proverId}`) : undefined;
82
- const cacheManager = new ProverCacheManager(cacheDir);
83
-
84
79
  return new ProverNode(
85
80
  prover,
86
81
  publisher,
@@ -95,7 +90,6 @@ export async function createProverNode(
95
90
  epochMonitor,
96
91
  bondManager,
97
92
  telemetry,
98
- cacheManager,
99
93
  proverNodeConfig,
100
94
  );
101
95
  }
@@ -12,11 +12,12 @@ import {
12
12
  type TxHash,
13
13
  } from '@aztec/circuit-types';
14
14
  import { asyncPool } from '@aztec/foundation/async-pool';
15
- import { createDebugLogger } from '@aztec/foundation/log';
15
+ import { createLogger } from '@aztec/foundation/log';
16
16
  import { promiseWithResolvers } from '@aztec/foundation/promise';
17
17
  import { Timer } from '@aztec/foundation/timer';
18
18
  import { type L1Publisher } from '@aztec/sequencer-client';
19
19
  import { type PublicProcessor, type PublicProcessorFactory } from '@aztec/simulator';
20
+ import { Attributes, type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client';
20
21
 
21
22
  import * as crypto from 'node:crypto';
22
23
 
@@ -27,13 +28,15 @@ import { type ProverNodeMetrics } from '../metrics.js';
27
28
  * re-executes their public calls, generates a rollup proof, and submits it to L1. This job will update the
28
29
  * world state as part of public call execution via the public processor.
29
30
  */
30
- export class EpochProvingJob {
31
+ export class EpochProvingJob implements Traceable {
31
32
  private state: EpochProvingJobState = 'initialized';
32
- private log = createDebugLogger('aztec:epoch-proving-job');
33
+ private log = createLogger('prover-node:epoch-proving-job');
33
34
  private uuid: string;
34
35
 
35
36
  private runPromise: Promise<void> | undefined;
36
37
 
38
+ public readonly tracer: Tracer;
39
+
37
40
  constructor(
38
41
  private dbProvider: ForkMerkleTreeOperations,
39
42
  private epochNumber: bigint,
@@ -49,6 +52,7 @@ export class EpochProvingJob {
49
52
  private cleanUp: (job: EpochProvingJob) => Promise<void> = () => Promise.resolve(),
50
53
  ) {
51
54
  this.uuid = crypto.randomUUID();
55
+ this.tracer = metrics.client.getTracer('EpochProvingJob');
52
56
  }
53
57
 
54
58
  public getId(): string {
@@ -62,11 +66,20 @@ export class EpochProvingJob {
62
66
  /**
63
67
  * Proves the given epoch and submits the proof to L1.
64
68
  */
69
+ @trackSpan('EpochProvingJob.run', function () {
70
+ return { [Attributes.EPOCH_NUMBER]: Number(this.epochNumber) };
71
+ })
65
72
  public async run() {
66
73
  const epochNumber = Number(this.epochNumber);
67
74
  const epochSize = this.blocks.length;
68
- const firstBlockNumber = this.blocks[0].number;
69
- this.log.info(`Starting epoch proving job`, { firstBlockNumber, epochSize, epochNumber, uuid: this.uuid });
75
+ const [fromBlock, toBlock] = [this.blocks[0].number, this.blocks.at(-1)!.number];
76
+ this.log.info(`Starting epoch ${epochNumber} proving job with blocks ${fromBlock} to ${toBlock}`, {
77
+ fromBlock,
78
+ toBlock,
79
+ epochSize,
80
+ epochNumber,
81
+ uuid: this.uuid,
82
+ });
70
83
  this.state = 'processing';
71
84
  const timer = new Timer();
72
85
 
@@ -74,17 +87,17 @@ export class EpochProvingJob {
74
87
  this.runPromise = promise;
75
88
 
76
89
  try {
77
- this.prover.startNewEpoch(epochNumber, firstBlockNumber, epochSize);
90
+ this.prover.startNewEpoch(epochNumber, fromBlock, epochSize);
78
91
 
79
92
  await asyncPool(this.config.parallelBlockLimit, this.blocks, async block => {
80
93
  const globalVariables = block.header.globalVariables;
81
94
  const txHashes = block.body.txEffects.map(tx => tx.txHash);
82
95
  const txCount = block.body.numberOfTxsIncludingPadded;
83
96
  const l1ToL2Messages = await this.getL1ToL2Messages(block);
84
- const txs = await this.getTxs(txHashes);
97
+ const txs = await this.getTxs(txHashes, block.number);
85
98
  const previousHeader = await this.getBlockHeader(block.number - 1);
86
99
 
87
- this.log.verbose(`Starting block processing`, {
100
+ this.log.verbose(`Starting processing block ${block.number}`, {
88
101
  number: block.number,
89
102
  blockHash: block.hash().toString(),
90
103
  lastArchive: block.header.lastArchive.root,
@@ -95,16 +108,16 @@ export class EpochProvingJob {
95
108
  uuid: this.uuid,
96
109
  ...globalVariables,
97
110
  });
98
-
99
111
  // Start block proving
100
- await this.prover.startNewBlock(txCount, globalVariables, l1ToL2Messages);
112
+ await this.prover.startNewBlock(globalVariables, l1ToL2Messages);
101
113
 
102
114
  // Process public fns
103
115
  const db = await this.dbProvider.fork(block.number - 1);
104
116
  const publicProcessor = this.publicProcessorFactory.create(db, previousHeader, globalVariables);
105
- await this.processTxs(publicProcessor, txs, txCount);
117
+ const processed = await this.processTxs(publicProcessor, txs, txCount);
118
+ await this.prover.addTxs(processed);
106
119
  await db.close();
107
- this.log.verbose(`Processed all txs for block`, {
120
+ this.log.verbose(`Processed all ${txs.length} txs for block ${block.number}`, {
108
121
  blockNumber: block.number,
109
122
  blockHash: block.hash().toString(),
110
123
  uuid: this.uuid,
@@ -116,17 +129,16 @@ export class EpochProvingJob {
116
129
 
117
130
  this.state = 'awaiting-prover';
118
131
  const { publicInputs, proof } = await this.prover.finaliseEpoch();
119
- this.log.info(`Finalised proof for epoch`, { epochNumber, uuid: this.uuid, duration: timer.ms() });
132
+ this.log.info(`Finalised proof for epoch ${epochNumber}`, { epochNumber, uuid: this.uuid, duration: timer.ms() });
120
133
 
121
134
  this.state = 'publishing-proof';
122
- const [fromBlock, toBlock] = [this.blocks[0].number, this.blocks.at(-1)!.number];
123
135
  await this.publisher.submitEpochProof({ fromBlock, toBlock, epochNumber, publicInputs, proof });
124
136
  this.log.info(`Submitted proof for epoch`, { epochNumber, uuid: this.uuid });
125
137
 
126
138
  this.state = 'completed';
127
139
  this.metrics.recordProvingJob(timer);
128
140
  } catch (err) {
129
- this.log.error(`Error running epoch prover job`, err, { uuid: this.uuid });
141
+ this.log.error(`Error running epoch ${epochNumber} prover job`, err, { uuid: this.uuid, epochNumber });
130
142
  this.state = 'failed';
131
143
  } finally {
132
144
  await this.cleanUp(this);
@@ -149,13 +161,15 @@ export class EpochProvingJob {
149
161
  return this.l2BlockSource.getBlockHeader(blockNumber);
150
162
  }
151
163
 
152
- private async getTxs(txHashes: TxHash[]): Promise<Tx[]> {
164
+ private async getTxs(txHashes: TxHash[], blockNumber: number): Promise<Tx[]> {
153
165
  const txs = await Promise.all(
154
166
  txHashes.map(txHash => this.coordination.getTxByHash(txHash).then(tx => [txHash, tx] as const)),
155
167
  );
156
168
  const notFound = txs.filter(([_, tx]) => !tx);
157
169
  if (notFound.length) {
158
- throw new Error(`Txs not found: ${notFound.map(([txHash]) => txHash.toString()).join(', ')}`);
170
+ throw new Error(
171
+ `Txs not found for block ${blockNumber}: ${notFound.map(([txHash]) => txHash.toString()).join(', ')}`,
172
+ );
159
173
  }
160
174
  return txs.map(([_, tx]) => tx!);
161
175
  }
@@ -169,12 +183,7 @@ export class EpochProvingJob {
169
183
  txs: Tx[],
170
184
  totalNumberOfTxs: number,
171
185
  ): Promise<ProcessedTx[]> {
172
- const [processedTxs, failedTxs] = await publicProcessor.process(
173
- txs,
174
- totalNumberOfTxs,
175
- this.prover,
176
- new EmptyTxValidator(),
177
- );
186
+ const [processedTxs, failedTxs] = await publicProcessor.process(txs, totalNumberOfTxs, new EmptyTxValidator());
178
187
 
179
188
  if (failedTxs.length) {
180
189
  throw new Error(
package/src/metrics.ts CHANGED
@@ -1,18 +1,15 @@
1
1
  import { type Timer } from '@aztec/foundation/timer';
2
- import { type Histogram, Metrics, type TelemetryClient, ValueType, millisecondBuckets } from '@aztec/telemetry-client';
2
+ import { type Histogram, Metrics, type TelemetryClient, ValueType } from '@aztec/telemetry-client';
3
3
 
4
4
  export class ProverNodeMetrics {
5
5
  provingJobDuration: Histogram;
6
6
 
7
- constructor(client: TelemetryClient, name = 'ProverNode') {
7
+ constructor(public readonly client: TelemetryClient, name = 'ProverNode') {
8
8
  const meter = client.getMeter(name);
9
9
  this.provingJobDuration = meter.createHistogram(Metrics.PROVER_NODE_JOB_DURATION, {
10
10
  description: 'Duration of proving job',
11
11
  unit: 'ms',
12
12
  valueType: ValueType.INT,
13
- advice: {
14
- explicitBucketBoundaries: millisecondBuckets(2), // 60 buckets spanning an interval of ~100ms to ~1hour
15
- },
16
13
  });
17
14
  }
18
15
 
@@ -1,22 +1,30 @@
1
1
  import { type EpochProofClaim } from '@aztec/circuit-types';
2
2
  import { type EthAddress } from '@aztec/circuits.js';
3
- import { createDebugLogger } from '@aztec/foundation/log';
3
+ import { createLogger } from '@aztec/foundation/log';
4
4
  import { RunningPromise } from '@aztec/foundation/running-promise';
5
5
  import { type L1Publisher } from '@aztec/sequencer-client';
6
+ import { type TelemetryClient, type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client';
6
7
 
7
8
  export interface ClaimsMonitorHandler {
8
9
  handleClaim(proofClaim: EpochProofClaim): Promise<void>;
9
10
  }
10
11
 
11
- export class ClaimsMonitor {
12
+ export class ClaimsMonitor implements Traceable {
12
13
  private runningPromise: RunningPromise;
13
- private log = createDebugLogger('aztec:prover-node:claims-monitor');
14
+ private log = createLogger('prover-node:claims-monitor');
14
15
 
15
16
  private handler: ClaimsMonitorHandler | undefined;
16
17
  private lastClaimEpochNumber: bigint | undefined;
17
18
 
18
- constructor(private readonly l1Publisher: L1Publisher, private options: { pollingIntervalMs: number }) {
19
- this.runningPromise = new RunningPromise(this.work.bind(this), this.options.pollingIntervalMs);
19
+ public readonly tracer: Tracer;
20
+
21
+ constructor(
22
+ private readonly l1Publisher: L1Publisher,
23
+ telemetry: TelemetryClient,
24
+ private options: { pollingIntervalMs: number },
25
+ ) {
26
+ this.tracer = telemetry.getTracer('ClaimsMonitor');
27
+ this.runningPromise = new RunningPromise(this.work.bind(this), this.log, this.options.pollingIntervalMs);
20
28
  }
21
29
 
22
30
  public start(handler: ClaimsMonitorHandler) {
@@ -31,9 +39,11 @@ export class ClaimsMonitor {
31
39
  this.log.info('Stopped ClaimsMonitor');
32
40
  }
33
41
 
42
+ @trackSpan('ClaimsMonitor.work')
34
43
  public async work() {
35
44
  const proofClaim = await this.l1Publisher.getProofClaim();
36
45
  if (!proofClaim) {
46
+ this.log.trace(`Found no proof claim`);
37
47
  return;
38
48
  }
39
49
 
@@ -1,22 +1,29 @@
1
1
  import { type L2BlockSource } from '@aztec/circuit-types';
2
- import { createDebugLogger } from '@aztec/foundation/log';
2
+ import { createLogger } from '@aztec/foundation/log';
3
3
  import { RunningPromise } from '@aztec/foundation/running-promise';
4
+ import { type TelemetryClient, type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client';
4
5
 
5
6
  export interface EpochMonitorHandler {
6
7
  handleInitialEpochSync(epochNumber: bigint): Promise<void>;
7
8
  handleEpochCompleted(epochNumber: bigint): Promise<void>;
8
9
  }
9
10
 
10
- export class EpochMonitor {
11
+ export class EpochMonitor implements Traceable {
11
12
  private runningPromise: RunningPromise;
12
- private log = createDebugLogger('aztec:prover-node:epoch-monitor');
13
+ private log = createLogger('prover-node:epoch-monitor');
14
+ public readonly tracer: Tracer;
13
15
 
14
16
  private handler: EpochMonitorHandler | undefined;
15
17
 
16
18
  private latestEpochNumber: bigint | undefined;
17
19
 
18
- constructor(private readonly l2BlockSource: L2BlockSource, private options: { pollingIntervalMs: number }) {
19
- this.runningPromise = new RunningPromise(this.work.bind(this), this.options.pollingIntervalMs);
20
+ constructor(
21
+ private readonly l2BlockSource: L2BlockSource,
22
+ telemetry: TelemetryClient,
23
+ private options: { pollingIntervalMs: number },
24
+ ) {
25
+ this.tracer = telemetry.getTracer('EpochMonitor');
26
+ this.runningPromise = new RunningPromise(this.work.bind(this), this.log, this.options.pollingIntervalMs);
20
27
  }
21
28
 
22
29
  public start(handler: EpochMonitorHandler) {
@@ -30,6 +37,7 @@ export class EpochMonitor {
30
37
  this.log.info('Stopped EpochMonitor');
31
38
  }
32
39
 
40
+ @trackSpan('EpochMonitor.work')
33
41
  public async work() {
34
42
  if (!this.latestEpochNumber) {
35
43
  const epochNumber = await this.l2BlockSource.getL2EpochNumber();
@@ -1,7 +1,12 @@
1
1
  import { type ArchiveSource, type Archiver } from '@aztec/archiver';
2
2
  import { BBCircuitVerifier, TestCircuitVerifier } from '@aztec/bb-prover';
3
- import { type ProverCoordination, type WorldStateSynchronizer, createAztecNodeClient } from '@aztec/circuit-types';
4
- import { createDebugLogger } from '@aztec/foundation/log';
3
+ import {
4
+ P2PClientType,
5
+ type ProverCoordination,
6
+ type WorldStateSynchronizer,
7
+ createAztecNodeClient,
8
+ } from '@aztec/circuit-types';
9
+ import { createLogger } from '@aztec/foundation/log';
5
10
  import { type DataStoreConfig } from '@aztec/kv-store/config';
6
11
  import { createP2PClient } from '@aztec/p2p';
7
12
  import { type TelemetryClient } from '@aztec/telemetry-client';
@@ -26,7 +31,7 @@ export async function createProverCoordination(
26
31
  config: ProverNodeConfig & DataStoreConfig,
27
32
  deps: ProverCoordinationDeps,
28
33
  ): Promise<ProverCoordination> {
29
- const log = createDebugLogger('aztec:createProverCoordination');
34
+ const log = createLogger('prover-node:prover-coordination');
30
35
 
31
36
  if (deps.aztecNodeTxProvider) {
32
37
  log.info('Using prover coordination via aztec node');
@@ -42,6 +47,7 @@ export async function createProverCoordination(
42
47
 
43
48
  const proofVerifier = config.realProofs ? await BBCircuitVerifier.new(config) : new TestCircuitVerifier();
44
49
  const p2pClient = await createP2PClient(
50
+ P2PClientType.Prover,
45
51
  config,
46
52
  deps.archiver,
47
53
  proofVerifier,
@@ -6,7 +6,7 @@ import {
6
6
  type L1ToL2MessageSource,
7
7
  type L2Block,
8
8
  type L2BlockSource,
9
- type ProverCache,
9
+ type P2PClientType,
10
10
  type ProverCoordination,
11
11
  type ProverNodeApi,
12
12
  type Service,
@@ -15,19 +15,18 @@ import {
15
15
  } from '@aztec/circuit-types';
16
16
  import { type ContractDataSource } from '@aztec/circuits.js';
17
17
  import { compact } from '@aztec/foundation/collection';
18
- import { sha256 } from '@aztec/foundation/crypto';
19
- import { createDebugLogger } from '@aztec/foundation/log';
18
+ import { createLogger } from '@aztec/foundation/log';
20
19
  import { type Maybe } from '@aztec/foundation/types';
20
+ import { type P2P } from '@aztec/p2p';
21
21
  import { type L1Publisher } from '@aztec/sequencer-client';
22
22
  import { PublicProcessorFactory } from '@aztec/simulator';
23
- import { type TelemetryClient } from '@aztec/telemetry-client';
23
+ import { Attributes, type TelemetryClient, type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client';
24
24
 
25
25
  import { type BondManager } from './bond/bond-manager.js';
26
26
  import { EpochProvingJob, type EpochProvingJobState } from './job/epoch-proving-job.js';
27
27
  import { ProverNodeMetrics } from './metrics.js';
28
28
  import { type ClaimsMonitor, type ClaimsMonitorHandler } from './monitors/claims-monitor.js';
29
29
  import { type EpochMonitor, type EpochMonitorHandler } from './monitors/epoch-monitor.js';
30
- import { type ProverCacheManager } from './prover-cache/cache_manager.js';
31
30
  import { type QuoteProvider } from './quote-provider/index.js';
32
31
  import { type QuoteSigner } from './quote-signer.js';
33
32
 
@@ -43,14 +42,16 @@ export type ProverNodeOptions = {
43
42
  * from a tx source in the p2p network or an external node, re-executes their public functions, creates a rollup
44
43
  * proof for the epoch, and submits it to L1.
45
44
  */
46
- export class ProverNode implements ClaimsMonitorHandler, EpochMonitorHandler, ProverNodeApi {
47
- private log = createDebugLogger('aztec:prover-node');
45
+ export class ProverNode implements ClaimsMonitorHandler, EpochMonitorHandler, ProverNodeApi, Traceable {
46
+ private log = createLogger('prover-node');
48
47
 
49
48
  private latestEpochWeAreProving: bigint | undefined;
50
49
  private jobs: Map<string, EpochProvingJob> = new Map();
51
50
  private options: ProverNodeOptions;
52
51
  private metrics: ProverNodeMetrics;
53
52
 
53
+ public readonly tracer: Tracer;
54
+
54
55
  constructor(
55
56
  private readonly prover: EpochProverManager,
56
57
  private readonly publisher: L1Publisher,
@@ -65,7 +66,6 @@ export class ProverNode implements ClaimsMonitorHandler, EpochMonitorHandler, Pr
65
66
  private readonly epochsMonitor: EpochMonitor,
66
67
  private readonly bondManager: BondManager,
67
68
  private readonly telemetryClient: TelemetryClient,
68
- private readonly proverCacheManager: ProverCacheManager,
69
69
  options: Partial<ProverNodeOptions> = {},
70
70
  ) {
71
71
  this.options = {
@@ -76,6 +76,15 @@ export class ProverNode implements ClaimsMonitorHandler, EpochMonitorHandler, Pr
76
76
  };
77
77
 
78
78
  this.metrics = new ProverNodeMetrics(telemetryClient, 'ProverNode');
79
+ this.tracer = telemetryClient.getTracer('ProverNode');
80
+ }
81
+
82
+ public getP2P() {
83
+ const asP2PClient = this.coordination as P2P<P2PClientType.Prover>;
84
+ if (typeof asP2PClient.isP2PClient === 'function' && asP2PClient.isP2PClient()) {
85
+ return asP2PClient;
86
+ }
87
+ return undefined;
79
88
  }
80
89
 
81
90
  async handleClaim(proofClaim: EpochProofClaim): Promise<void> {
@@ -84,6 +93,12 @@ export class ProverNode implements ClaimsMonitorHandler, EpochMonitorHandler, Pr
84
93
  return;
85
94
  }
86
95
 
96
+ const provenEpoch = await this.l2BlockSource.getProvenL2EpochNumber();
97
+ if (provenEpoch !== undefined && proofClaim.epochToProve <= provenEpoch) {
98
+ this.log.verbose(`Claim for epoch ${proofClaim.epochToProve} is already proven`);
99
+ return;
100
+ }
101
+
87
102
  try {
88
103
  await this.startProof(proofClaim.epochToProve);
89
104
  this.latestEpochWeAreProving = proofClaim.epochToProve;
@@ -108,12 +123,8 @@ export class ProverNode implements ClaimsMonitorHandler, EpochMonitorHandler, Pr
108
123
  try {
109
124
  const claim = await this.publisher.getProofClaim();
110
125
  if (!claim || claim.epochToProve < epochNumber) {
126
+ this.log.verbose(`Handling epoch ${epochNumber} completed as initial sync`);
111
127
  await this.handleEpochCompleted(epochNumber);
112
- } else if (claim && claim.bondProvider.equals(this.publisher.getSenderAddress())) {
113
- const lastEpochProven = await this.l2BlockSource.getProvenL2EpochNumber();
114
- if (lastEpochProven === undefined || lastEpochProven < claim.epochToProve) {
115
- await this.handleClaim(claim);
116
- }
117
128
  }
118
129
  } catch (err) {
119
130
  this.log.error(`Error handling initial epoch sync`, err);
@@ -128,9 +139,14 @@ export class ProverNode implements ClaimsMonitorHandler, EpochMonitorHandler, Pr
128
139
  try {
129
140
  // Construct a quote for the epoch
130
141
  const blocks = await this.l2BlockSource.getBlocksForEpoch(epochNumber);
142
+ if (blocks.length === 0) {
143
+ this.log.info(`No blocks found for epoch ${epochNumber}`);
144
+ return;
145
+ }
146
+
131
147
  const partialQuote = await this.quoteProvider.getQuote(Number(epochNumber), blocks);
132
148
  if (!partialQuote) {
133
- this.log.verbose(`No quote produced for epoch ${epochNumber}`);
149
+ this.log.info(`No quote produced for epoch ${epochNumber}`);
134
150
  return;
135
151
  }
136
152
 
@@ -231,6 +247,7 @@ export class ProverNode implements ClaimsMonitorHandler, EpochMonitorHandler, Pr
231
247
  return maxPendingJobs === 0 || this.jobs.size < maxPendingJobs;
232
248
  }
233
249
 
250
+ @trackSpan('ProverNode.createProvingJob', epochNumber => ({ [Attributes.EPOCH_NUMBER]: Number(epochNumber) }))
234
251
  private async createProvingJob(epochNumber: bigint) {
235
252
  if (!this.checkMaximumPendingJobs()) {
236
253
  throw new Error(`Maximum pending proving jobs ${this.options.maxPendingJobs} reached. Cannot create new job.`);
@@ -251,16 +268,12 @@ export class ProverNode implements ClaimsMonitorHandler, EpochMonitorHandler, Pr
251
268
  // Create a processor using the forked world state
252
269
  const publicProcessorFactory = new PublicProcessorFactory(this.contractDataSource, this.telemetryClient);
253
270
 
254
- const epochHash = sha256(Buffer.concat(blocks.map(block => block.hash().toBuffer())));
255
- const proverCache = await this.proverCacheManager.openCache(epochNumber, epochHash);
256
-
257
- const cleanUp = async () => {
258
- await proverCache.close();
259
- await this.proverCacheManager.removeStaleCaches(epochNumber);
271
+ const cleanUp = () => {
260
272
  this.jobs.delete(job.getId());
273
+ return Promise.resolve();
261
274
  };
262
275
 
263
- const job = this.doCreateEpochProvingJob(epochNumber, blocks, proverCache, publicProcessorFactory, cleanUp);
276
+ const job = this.doCreateEpochProvingJob(epochNumber, blocks, publicProcessorFactory, cleanUp);
264
277
  this.jobs.set(job.getId(), job);
265
278
  return job;
266
279
  }
@@ -269,7 +282,6 @@ export class ProverNode implements ClaimsMonitorHandler, EpochMonitorHandler, Pr
269
282
  protected doCreateEpochProvingJob(
270
283
  epochNumber: bigint,
271
284
  blocks: L2Block[],
272
- proverCache: ProverCache,
273
285
  publicProcessorFactory: PublicProcessorFactory,
274
286
  cleanUp: () => Promise<void>,
275
287
  ) {
@@ -277,7 +289,7 @@ export class ProverNode implements ClaimsMonitorHandler, EpochMonitorHandler, Pr
277
289
  this.worldState,
278
290
  epochNumber,
279
291
  blocks,
280
- this.prover.createEpochProver(proverCache),
292
+ this.prover.createEpochProver(),
281
293
  publicProcessorFactory,
282
294
  this.publisher,
283
295
  this.l2BlockSource,
@@ -1,15 +0,0 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
- /// <reference types="node" resolution-mode="require"/>
3
- import { type ProverCache } from '@aztec/circuit-types';
4
- export declare class ProverCacheManager {
5
- private cacheDir?;
6
- private log;
7
- constructor(cacheDir?: string | undefined, log?: import("@aztec/foundation/log").Logger);
8
- openCache(epochNumber: bigint, epochHash: Buffer): Promise<ProverCache>;
9
- /**
10
- * Removes all caches for epochs older than the given epoch (including)
11
- * @param upToAndIncludingEpoch - The epoch number up to which to remove caches
12
- */
13
- removeStaleCaches(upToAndIncludingEpoch: bigint): Promise<void>;
14
- }
15
- //# sourceMappingURL=cache_manager.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cache_manager.d.ts","sourceRoot":"","sources":["../../src/prover-cache/cache_manager.ts"],"names":[],"mappings":";;AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAexD,qBAAa,kBAAkB;IACjB,OAAO,CAAC,QAAQ,CAAC;IAAU,OAAO,CAAC,GAAG;gBAA9B,QAAQ,CAAC,oBAAQ,EAAU,GAAG,yCAAuD;IAE5F,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAsBpF;;;OAGG;IACU,iBAAiB,CAAC,qBAAqB,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAwB7E"}
@@ -1,57 +0,0 @@
1
- import { createDebugLogger } from '@aztec/foundation/log';
2
- import { AztecLmdbStore } from '@aztec/kv-store/lmdb';
3
- import { InMemoryProverCache } from '@aztec/prover-client';
4
- import { mkdir, readFile, readdir, rm, writeFile } from 'fs/promises';
5
- import { join } from 'path';
6
- import { KVProverCache } from './kv_cache.js';
7
- const EPOCH_DIR_PREFIX = 'epoch';
8
- const EPOCH_DIR_SEPARATOR = '_';
9
- const EPOCH_HASH_FILENAME = 'epoch_hash.txt';
10
- export class ProverCacheManager {
11
- constructor(cacheDir, log = createDebugLogger('aztec:prover-node:cache-manager')) {
12
- this.cacheDir = cacheDir;
13
- this.log = log;
14
- }
15
- async openCache(epochNumber, epochHash) {
16
- if (!this.cacheDir) {
17
- return new InMemoryProverCache();
18
- }
19
- const epochDir = EPOCH_DIR_PREFIX + EPOCH_DIR_SEPARATOR + epochNumber;
20
- const dataDir = join(this.cacheDir, epochDir);
21
- const storedEpochHash = await readFile(join(dataDir, EPOCH_HASH_FILENAME), 'hex').catch(() => Buffer.alloc(0));
22
- if (storedEpochHash.toString() !== epochHash.toString()) {
23
- await rm(dataDir, { recursive: true, force: true });
24
- }
25
- await mkdir(dataDir, { recursive: true });
26
- await writeFile(join(dataDir, EPOCH_HASH_FILENAME), epochHash.toString('hex'));
27
- const store = AztecLmdbStore.open(dataDir);
28
- this.log.debug(`Created new database for epoch ${epochNumber} at ${dataDir}`);
29
- const cleanup = () => store.close();
30
- return new KVProverCache(store, cleanup);
31
- }
32
- /**
33
- * Removes all caches for epochs older than the given epoch (including)
34
- * @param upToAndIncludingEpoch - The epoch number up to which to remove caches
35
- */
36
- async removeStaleCaches(upToAndIncludingEpoch) {
37
- if (!this.cacheDir) {
38
- return;
39
- }
40
- const entries = await readdir(this.cacheDir, { withFileTypes: true }).catch(() => []);
41
- for (const item of entries) {
42
- if (!item.isDirectory()) {
43
- continue;
44
- }
45
- const [prefix, epochNumber] = item.name.split(EPOCH_DIR_SEPARATOR);
46
- if (prefix !== EPOCH_DIR_PREFIX) {
47
- continue;
48
- }
49
- const epochNumberInt = BigInt(epochNumber);
50
- if (epochNumberInt <= upToAndIncludingEpoch) {
51
- this.log.info(`Removing old epoch database for epoch ${epochNumberInt} at ${join(this.cacheDir, item.name)}`);
52
- await rm(join(this.cacheDir, item.name), { recursive: true });
53
- }
54
- }
55
- }
56
- }
57
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2FjaGVfbWFuYWdlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9wcm92ZXItY2FjaGUvY2FjaGVfbWFuYWdlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxPQUFPLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSx1QkFBdUIsQ0FBQztBQUMxRCxPQUFPLEVBQUUsY0FBYyxFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFDdEQsT0FBTyxFQUFFLG1CQUFtQixFQUFFLE1BQU0sc0JBQXNCLENBQUM7QUFHM0QsT0FBTyxFQUFFLEtBQUssRUFBRSxRQUFRLEVBQUUsT0FBTyxFQUFFLEVBQUUsRUFBRSxTQUFTLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFDdEUsT0FBTyxFQUFFLElBQUksRUFBRSxNQUFNLE1BQU0sQ0FBQztBQUU1QixPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sZUFBZSxDQUFDO0FBRTlDLE1BQU0sZ0JBQWdCLEdBQUcsT0FBTyxDQUFDO0FBQ2pDLE1BQU0sbUJBQW1CLEdBQUcsR0FBRyxDQUFDO0FBQ2hDLE1BQU0sbUJBQW1CLEdBQUcsZ0JBQWdCLENBQUM7QUFFN0MsTUFBTSxPQUFPLGtCQUFrQjtJQUM3QixZQUFvQixRQUFpQixFQUFVLE1BQU0saUJBQWlCLENBQUMsaUNBQWlDLENBQUM7UUFBckYsYUFBUSxHQUFSLFFBQVEsQ0FBUztRQUFVLFFBQUcsR0FBSCxHQUFHLENBQXVEO0lBQUcsQ0FBQztJQUV0RyxLQUFLLENBQUMsU0FBUyxDQUFDLFdBQW1CLEVBQUUsU0FBaUI7UUFDM0QsSUFBSSxDQUFDLElBQUksQ0FBQyxRQUFRLEVBQUUsQ0FBQztZQUNuQixPQUFPLElBQUksbUJBQW1CLEVBQUUsQ0FBQztRQUNuQyxDQUFDO1FBRUQsTUFBTSxRQUFRLEdBQUcsZ0JBQWdCLEdBQUcsbUJBQW1CLEdBQUcsV0FBVyxDQUFDO1FBQ3RFLE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsUUFBUSxFQUFFLFFB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
@@ -1,11 +0,0 @@
1
- import type { ProverCache, ProvingJobStatus } from '@aztec/circuit-types';
2
- import type { AztecKVStore } from '@aztec/kv-store';
3
- export declare class KVProverCache implements ProverCache {
4
- private cleanup?;
5
- private proofs;
6
- constructor(store: AztecKVStore, cleanup?: (() => Promise<void>) | undefined);
7
- getProvingJobStatus(jobId: string): Promise<ProvingJobStatus>;
8
- setProvingJobStatus(jobId: string, status: ProvingJobStatus): Promise<void>;
9
- close(): Promise<void>;
10
- }
11
- //# sourceMappingURL=kv_cache.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"kv_cache.d.ts","sourceRoot":"","sources":["../../src/prover-cache/kv_cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC1E,OAAO,KAAK,EAAE,YAAY,EAAY,MAAM,iBAAiB,CAAC;AAE9D,qBAAa,aAAc,YAAW,WAAW;IAGd,OAAO,CAAC,OAAO,CAAC;IAFjD,OAAO,CAAC,MAAM,CAA2B;gBAE7B,KAAK,EAAE,YAAY,EAAU,OAAO,CAAC,SAAQ,QAAQ,IAAI,CAAC,aAAA;IAItE,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAS7D,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
@@ -1,20 +0,0 @@
1
- export class KVProverCache {
2
- constructor(store, cleanup) {
3
- this.cleanup = cleanup;
4
- this.proofs = store.openMap('prover_node_proof_status');
5
- }
6
- getProvingJobStatus(jobId) {
7
- const item = this.proofs.get(jobId);
8
- if (!item) {
9
- return Promise.resolve({ status: 'not-found' });
10
- }
11
- return Promise.resolve(JSON.parse(item));
12
- }
13
- setProvingJobStatus(jobId, status) {
14
- return this.proofs.set(jobId, JSON.stringify(status));
15
- }
16
- async close() {
17
- await this.cleanup?.();
18
- }
19
- }
20
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoia3ZfY2FjaGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvcHJvdmVyLWNhY2hlL2t2X2NhY2hlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUdBLE1BQU0sT0FBTyxhQUFhO0lBR3hCLFlBQVksS0FBbUIsRUFBVSxPQUE2QjtRQUE3QixZQUFPLEdBQVAsT0FBTyxDQUFzQjtRQUNwRSxJQUFJLENBQUMsTUFBTSxHQUFHLEtBQUssQ0FBQyxPQUFPLENBQUMsMEJBQTBCLENBQUMsQ0FBQztJQUMxRCxDQUFDO0lBRUQsbUJBQW1CLENBQUMsS0FBYTtRQUMvQixNQUFNLElBQUksR0FBRyxJQUFJLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUMsQ0FBQztRQUNwQyxJQUFJLENBQUMsSUFBSSxFQUFFLENBQUM7WUFDVixPQUFPLE9BQU8sQ0FBQyxPQUFPLENBQUMsRUFBRSxNQUFNLEVBQUUsV0FBVyxFQUFFLENBQUMsQ0FBQztRQUNsRCxDQUFDO1FBRUQsT0FBTyxPQUFPLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztJQUMzQyxDQUFDO0lBRUQsbUJBQW1CLENBQUMsS0FBYSxFQUFFLE1BQXdCO1FBQ3pELE9BQU8sSUFBSSxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsS0FBSyxFQUFFLElBQUksQ0FBQyxTQUFTLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQztJQUN4RCxDQUFDO0lBRUQsS0FBSyxDQUFDLEtBQUs7UUFDVCxNQUFNLElBQUksQ0FBQyxPQUFPLEVBQUUsRUFBRSxDQUFDO0lBQ3pCLENBQUM7Q0FDRiJ9