@aztec/prover-node 0.0.0-test.0
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.
- package/README.md +1 -0
- package/dest/config.d.ts +28 -0
- package/dest/config.d.ts.map +1 -0
- package/dest/config.js +71 -0
- package/dest/factory.d.ts +25 -0
- package/dest/factory.d.ts.map +1 -0
- package/dest/factory.js +60 -0
- package/dest/http.d.ts +8 -0
- package/dest/http.d.ts.map +1 -0
- package/dest/http.js +9 -0
- package/dest/index.d.ts +6 -0
- package/dest/index.d.ts.map +1 -0
- package/dest/index.js +5 -0
- package/dest/job/epoch-proving-job.d.ts +54 -0
- package/dest/job/epoch-proving-job.d.ts.map +1 -0
- package/dest/job/epoch-proving-job.js +255 -0
- package/dest/metrics.d.ts +26 -0
- package/dest/metrics.d.ts.map +1 -0
- package/dest/metrics.js +129 -0
- package/dest/monitors/epoch-monitor.d.ts +40 -0
- package/dest/monitors/epoch-monitor.d.ts.map +1 -0
- package/dest/monitors/epoch-monitor.js +104 -0
- package/dest/monitors/index.d.ts +2 -0
- package/dest/monitors/index.d.ts.map +1 -0
- package/dest/monitors/index.js +1 -0
- package/dest/prover-coordination/config.d.ts +7 -0
- package/dest/prover-coordination/config.d.ts.map +1 -0
- package/dest/prover-coordination/config.js +11 -0
- package/dest/prover-coordination/factory.d.ts +22 -0
- package/dest/prover-coordination/factory.d.ts.map +1 -0
- package/dest/prover-coordination/factory.js +42 -0
- package/dest/prover-coordination/index.d.ts +3 -0
- package/dest/prover-coordination/index.d.ts.map +1 -0
- package/dest/prover-coordination/index.js +2 -0
- package/dest/prover-node-publisher.d.ts +61 -0
- package/dest/prover-node-publisher.d.ts.map +1 -0
- package/dest/prover-node-publisher.js +192 -0
- package/dest/prover-node.d.ts +102 -0
- package/dest/prover-node.d.ts.map +1 -0
- package/dest/prover-node.js +258 -0
- package/dest/test/index.d.ts +10 -0
- package/dest/test/index.d.ts.map +1 -0
- package/dest/test/index.js +5 -0
- package/package.json +98 -0
- package/src/config.ts +115 -0
- package/src/factory.ts +99 -0
- package/src/http.ts +13 -0
- package/src/index.ts +5 -0
- package/src/job/epoch-proving-job.ts +255 -0
- package/src/metrics.ts +164 -0
- package/src/monitors/epoch-monitor.ts +106 -0
- package/src/monitors/index.ts +1 -0
- package/src/prover-coordination/config.ts +17 -0
- package/src/prover-coordination/factory.ts +72 -0
- package/src/prover-coordination/index.ts +2 -0
- package/src/prover-node-publisher.ts +286 -0
- package/src/prover-node.ts +335 -0
- package/src/test/index.ts +11 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ArchiveSource, Archiver } from '@aztec/archiver';
|
|
2
|
+
import { BBCircuitVerifier, TestCircuitVerifier } from '@aztec/bb-prover';
|
|
3
|
+
import type { EpochCache } from '@aztec/epoch-cache';
|
|
4
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
5
|
+
import type { DataStoreConfig } from '@aztec/kv-store/config';
|
|
6
|
+
import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree';
|
|
7
|
+
import { createP2PClient } from '@aztec/p2p';
|
|
8
|
+
import { protocolContractTreeRoot } from '@aztec/protocol-contracts';
|
|
9
|
+
import { createAztecNodeClient } from '@aztec/stdlib/interfaces/client';
|
|
10
|
+
import type { ProverCoordination, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
|
|
11
|
+
import { P2PClientType } from '@aztec/stdlib/p2p';
|
|
12
|
+
import { getComponentsVersionsFromConfig } from '@aztec/stdlib/versioning';
|
|
13
|
+
import { type TelemetryClient, makeTracedFetch } from '@aztec/telemetry-client';
|
|
14
|
+
|
|
15
|
+
import type { ProverNodeConfig } from '../config.js';
|
|
16
|
+
|
|
17
|
+
// We return a reference to the P2P client so that the prover node can stop the service when it shuts down.
|
|
18
|
+
type ProverCoordinationDeps = {
|
|
19
|
+
aztecNodeTxProvider?: ProverCoordination;
|
|
20
|
+
worldStateSynchronizer?: WorldStateSynchronizer;
|
|
21
|
+
archiver?: Archiver | ArchiveSource;
|
|
22
|
+
telemetry?: TelemetryClient;
|
|
23
|
+
epochCache?: EpochCache;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates a prover coordination service.
|
|
28
|
+
* If p2p is enabled, prover coordination is done via p2p.
|
|
29
|
+
* If an Aztec node URL is provided, prover coordination is done via the Aztec node over http.
|
|
30
|
+
* If an aztec node is provided, it is returned directly.
|
|
31
|
+
*/
|
|
32
|
+
export async function createProverCoordination(
|
|
33
|
+
config: ProverNodeConfig & DataStoreConfig,
|
|
34
|
+
deps: ProverCoordinationDeps,
|
|
35
|
+
): Promise<ProverCoordination> {
|
|
36
|
+
const log = createLogger('prover-node:prover-coordination');
|
|
37
|
+
|
|
38
|
+
if (deps.aztecNodeTxProvider) {
|
|
39
|
+
log.info('Using prover coordination via aztec node');
|
|
40
|
+
return deps.aztecNodeTxProvider;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (config.p2pEnabled) {
|
|
44
|
+
log.info('Using prover coordination via p2p');
|
|
45
|
+
|
|
46
|
+
if (!deps.archiver || !deps.worldStateSynchronizer || !deps.telemetry || !deps.epochCache) {
|
|
47
|
+
throw new Error('Missing dependencies for p2p prover coordination');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const proofVerifier = config.realProofs ? await BBCircuitVerifier.new(config) : new TestCircuitVerifier();
|
|
51
|
+
const p2pClient = await createP2PClient(
|
|
52
|
+
P2PClientType.Prover,
|
|
53
|
+
config,
|
|
54
|
+
deps.archiver,
|
|
55
|
+
proofVerifier,
|
|
56
|
+
deps.worldStateSynchronizer,
|
|
57
|
+
deps.epochCache,
|
|
58
|
+
deps.telemetry,
|
|
59
|
+
);
|
|
60
|
+
await p2pClient.start();
|
|
61
|
+
|
|
62
|
+
return p2pClient;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (config.proverCoordinationNodeUrl) {
|
|
66
|
+
log.info('Using prover coordination via node url');
|
|
67
|
+
const versions = getComponentsVersionsFromConfig(config, protocolContractTreeRoot, getVKTreeRoot());
|
|
68
|
+
return createAztecNodeClient(config.proverCoordinationNodeUrl, versions, makeTracedFetch([1, 2, 3], false));
|
|
69
|
+
} else {
|
|
70
|
+
throw new Error(`Aztec Node URL for Tx Provider is not set.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { AGGREGATION_OBJECT_LENGTH, AZTEC_MAX_EPOCH_DURATION } from '@aztec/constants';
|
|
2
|
+
import type { L1TxUtils, RollupContract } from '@aztec/ethereum';
|
|
3
|
+
import { makeTuple } from '@aztec/foundation/array';
|
|
4
|
+
import { areArraysEqual, times } from '@aztec/foundation/collection';
|
|
5
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
6
|
+
import { Fr } from '@aztec/foundation/fields';
|
|
7
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
8
|
+
import { type Tuple, serializeToBuffer } from '@aztec/foundation/serialize';
|
|
9
|
+
import { InterruptibleSleep } from '@aztec/foundation/sleep';
|
|
10
|
+
import { Timer } from '@aztec/foundation/timer';
|
|
11
|
+
import { RollupAbi } from '@aztec/l1-artifacts';
|
|
12
|
+
import type { PublisherConfig, TxSenderConfig } from '@aztec/sequencer-client';
|
|
13
|
+
import type { Proof } from '@aztec/stdlib/proofs';
|
|
14
|
+
import type { FeeRecipient, RootRollupPublicInputs } from '@aztec/stdlib/rollup';
|
|
15
|
+
import type { L1PublishProofStats } from '@aztec/stdlib/stats';
|
|
16
|
+
import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
|
|
17
|
+
|
|
18
|
+
import { type Hex, type TransactionReceipt, encodeFunctionData } from 'viem';
|
|
19
|
+
|
|
20
|
+
import { ProverNodeMetrics } from './metrics.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Stats for a sent transaction.
|
|
24
|
+
*/
|
|
25
|
+
/** Arguments to the submitEpochProof method of the rollup contract */
|
|
26
|
+
export type L1SubmitEpochProofArgs = {
|
|
27
|
+
epochSize: number;
|
|
28
|
+
previousArchive: Fr;
|
|
29
|
+
endArchive: Fr;
|
|
30
|
+
previousBlockHash: Fr;
|
|
31
|
+
endBlockHash: Fr;
|
|
32
|
+
endTimestamp: Fr;
|
|
33
|
+
outHash: Fr;
|
|
34
|
+
proverId: Fr;
|
|
35
|
+
fees: Tuple<FeeRecipient, typeof AZTEC_MAX_EPOCH_DURATION>;
|
|
36
|
+
proof: Proof;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export class ProverNodePublisher {
|
|
40
|
+
private interruptibleSleep = new InterruptibleSleep();
|
|
41
|
+
private sleepTimeMs: number;
|
|
42
|
+
private interrupted = false;
|
|
43
|
+
private metrics: ProverNodeMetrics;
|
|
44
|
+
|
|
45
|
+
protected log = createLogger('prover-node:l1-tx-publisher');
|
|
46
|
+
|
|
47
|
+
protected rollupContract: RollupContract;
|
|
48
|
+
|
|
49
|
+
public readonly l1TxUtils: L1TxUtils;
|
|
50
|
+
|
|
51
|
+
constructor(
|
|
52
|
+
config: TxSenderConfig & PublisherConfig,
|
|
53
|
+
deps: {
|
|
54
|
+
rollupContract: RollupContract;
|
|
55
|
+
l1TxUtils: L1TxUtils;
|
|
56
|
+
telemetry?: TelemetryClient;
|
|
57
|
+
},
|
|
58
|
+
) {
|
|
59
|
+
this.sleepTimeMs = config?.l1PublishRetryIntervalMS ?? 60_000;
|
|
60
|
+
|
|
61
|
+
const telemetry = deps.telemetry ?? getTelemetryClient();
|
|
62
|
+
|
|
63
|
+
this.metrics = new ProverNodeMetrics(telemetry, 'ProverNode');
|
|
64
|
+
|
|
65
|
+
this.rollupContract = deps.rollupContract;
|
|
66
|
+
this.l1TxUtils = deps.l1TxUtils;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Calling `interrupt` will cause any in progress call to `publishRollup` to return `false` asap.
|
|
71
|
+
* Be warned, the call may return false even if the tx subsequently gets successfully mined.
|
|
72
|
+
* In practice this shouldn't matter, as we'll only ever be calling `interrupt` when we know it's going to fail.
|
|
73
|
+
* A call to `restart` is required before you can continue publishing.
|
|
74
|
+
*/
|
|
75
|
+
public interrupt() {
|
|
76
|
+
this.interrupted = true;
|
|
77
|
+
this.interruptibleSleep.interrupt();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Restarts the publisher after calling `interrupt`. */
|
|
81
|
+
public restart() {
|
|
82
|
+
this.interrupted = false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public getSenderAddress() {
|
|
86
|
+
return EthAddress.fromString(this.l1TxUtils.getSenderAddress());
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public async submitEpochProof(args: {
|
|
90
|
+
epochNumber: number;
|
|
91
|
+
fromBlock: number;
|
|
92
|
+
toBlock: number;
|
|
93
|
+
publicInputs: RootRollupPublicInputs;
|
|
94
|
+
proof: Proof;
|
|
95
|
+
}): Promise<boolean> {
|
|
96
|
+
const { epochNumber, fromBlock, toBlock } = args;
|
|
97
|
+
const ctx = { epochNumber, fromBlock, toBlock };
|
|
98
|
+
if (!this.interrupted) {
|
|
99
|
+
const timer = new Timer();
|
|
100
|
+
|
|
101
|
+
// Validate epoch proof range and hashes are correct before submitting
|
|
102
|
+
await this.validateEpochProofSubmission(args);
|
|
103
|
+
|
|
104
|
+
const txReceipt = await this.sendSubmitEpochProofTx(args);
|
|
105
|
+
if (!txReceipt) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
this.metrics.recordSenderBalance(await this.l1TxUtils.getSenderBalance(), this.l1TxUtils.getSenderAddress());
|
|
111
|
+
} catch (err) {
|
|
112
|
+
this.log.warn(`Failed to record the ETH balance of the prover node: ${err}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Tx was mined successfully
|
|
116
|
+
if (txReceipt.status) {
|
|
117
|
+
const tx = await this.l1TxUtils.getTransactionStats(txReceipt.transactionHash);
|
|
118
|
+
const stats: L1PublishProofStats = {
|
|
119
|
+
gasPrice: txReceipt.effectiveGasPrice,
|
|
120
|
+
gasUsed: txReceipt.gasUsed,
|
|
121
|
+
transactionHash: txReceipt.transactionHash,
|
|
122
|
+
calldataGas: tx!.calldataGas,
|
|
123
|
+
calldataSize: tx!.calldataSize,
|
|
124
|
+
sender: tx!.sender,
|
|
125
|
+
blobDataGas: 0n,
|
|
126
|
+
blobGasUsed: 0n,
|
|
127
|
+
eventName: 'proof-published-to-l1',
|
|
128
|
+
};
|
|
129
|
+
this.log.info(`Published epoch proof to L1 rollup contract`, { ...stats, ...ctx });
|
|
130
|
+
this.metrics.recordSubmitProof(timer.ms(), stats);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.metrics.recordFailedTx();
|
|
135
|
+
this.log.error(`Rollup.submitEpochProof tx status failed: ${txReceipt.transactionHash}`, ctx);
|
|
136
|
+
await this.sleepOrInterrupted();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.log.verbose('L2 block data syncing interrupted while processing blocks.', ctx);
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private async validateEpochProofSubmission(args: {
|
|
144
|
+
fromBlock: number;
|
|
145
|
+
toBlock: number;
|
|
146
|
+
publicInputs: RootRollupPublicInputs;
|
|
147
|
+
proof: Proof;
|
|
148
|
+
}) {
|
|
149
|
+
const { fromBlock, toBlock, publicInputs, proof } = args;
|
|
150
|
+
|
|
151
|
+
// Check that the block numbers match the expected epoch to be proven
|
|
152
|
+
const { pendingBlockNumber: pending, provenBlockNumber: proven } = await this.rollupContract.getTips();
|
|
153
|
+
if (proven !== BigInt(fromBlock) - 1n) {
|
|
154
|
+
throw new Error(`Cannot submit epoch proof for ${fromBlock}-${toBlock} as proven block is ${proven}`);
|
|
155
|
+
}
|
|
156
|
+
if (toBlock > pending) {
|
|
157
|
+
throw new Error(`Cannot submit epoch proof for ${fromBlock}-${toBlock} as pending block is ${pending}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check the block hash and archive for the immediate block before the epoch
|
|
161
|
+
const blockLog = await this.rollupContract.getBlock(proven);
|
|
162
|
+
if (publicInputs.previousArchive.root.toString() !== blockLog.archive) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Previous archive root mismatch: ${publicInputs.previousArchive.root.toString()} !== ${blockLog.archive}`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
// TODO: Remove zero check once we inject the proper zero blockhash
|
|
168
|
+
if (blockLog.blockHash !== Fr.ZERO.toString() && publicInputs.previousBlockHash.toString() !== blockLog.blockHash) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Previous block hash mismatch: ${publicInputs.previousBlockHash.toString()} !== ${blockLog.blockHash}`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check the block hash and archive for the last block in the epoch
|
|
175
|
+
const endBlockLog = await this.rollupContract.getBlock(BigInt(toBlock));
|
|
176
|
+
if (publicInputs.endArchive.root.toString() !== endBlockLog.archive) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`End archive root mismatch: ${publicInputs.endArchive.root.toString()} !== ${endBlockLog.archive}`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (publicInputs.endBlockHash.toString() !== endBlockLog.blockHash) {
|
|
182
|
+
throw new Error(`End block hash mismatch: ${publicInputs.endBlockHash.toString()} !== ${endBlockLog.blockHash}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Compare the public inputs computed by the contract with the ones injected
|
|
186
|
+
const rollupPublicInputs = await this.rollupContract.getEpochProofPublicInputs(this.getSubmitEpochProofArgs(args));
|
|
187
|
+
const aggregationObject = proof.isEmpty()
|
|
188
|
+
? times(AGGREGATION_OBJECT_LENGTH, Fr.zero)
|
|
189
|
+
: proof.extractAggregationObject();
|
|
190
|
+
const argsPublicInputs = [...publicInputs.toFields(), ...aggregationObject];
|
|
191
|
+
|
|
192
|
+
if (!areArraysEqual(rollupPublicInputs.map(Fr.fromHexString), argsPublicInputs, (a, b) => a.equals(b))) {
|
|
193
|
+
const fmt = (inputs: Fr[] | readonly string[]) => inputs.map(x => x.toString()).join(', ');
|
|
194
|
+
throw new Error(
|
|
195
|
+
`Root rollup public inputs mismatch:\nRollup: ${fmt(rollupPublicInputs)}\nComputed:${fmt(argsPublicInputs)}`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private async sendSubmitEpochProofTx(args: {
|
|
201
|
+
fromBlock: number;
|
|
202
|
+
toBlock: number;
|
|
203
|
+
publicInputs: RootRollupPublicInputs;
|
|
204
|
+
proof: Proof;
|
|
205
|
+
}): Promise<TransactionReceipt | undefined> {
|
|
206
|
+
const proofHex: Hex = `0x${args.proof.withoutPublicInputs().toString('hex')}`;
|
|
207
|
+
const argsArray = this.getSubmitEpochProofArgs(args);
|
|
208
|
+
|
|
209
|
+
const txArgs = [
|
|
210
|
+
{
|
|
211
|
+
start: argsArray[0],
|
|
212
|
+
end: argsArray[1],
|
|
213
|
+
args: argsArray[2],
|
|
214
|
+
fees: argsArray[3],
|
|
215
|
+
blobPublicInputs: argsArray[4],
|
|
216
|
+
aggregationObject: argsArray[5],
|
|
217
|
+
proof: proofHex,
|
|
218
|
+
},
|
|
219
|
+
] as const;
|
|
220
|
+
|
|
221
|
+
this.log.info(`SubmitEpochProof proofSize=${args.proof.withoutPublicInputs().length} bytes`);
|
|
222
|
+
const data = encodeFunctionData({
|
|
223
|
+
abi: RollupAbi,
|
|
224
|
+
functionName: 'submitEpochRootProof',
|
|
225
|
+
args: txArgs,
|
|
226
|
+
});
|
|
227
|
+
try {
|
|
228
|
+
const { receipt } = await this.l1TxUtils.sendAndMonitorTransaction({
|
|
229
|
+
to: this.rollupContract.address,
|
|
230
|
+
data,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
return receipt;
|
|
234
|
+
} catch (err) {
|
|
235
|
+
this.log.error(`Rollup submit epoch proof failed`, err);
|
|
236
|
+
const errorMsg = await this.l1TxUtils.tryGetErrorFromRevertedTx(
|
|
237
|
+
data,
|
|
238
|
+
{
|
|
239
|
+
args: [...txArgs],
|
|
240
|
+
functionName: 'submitEpochRootProof',
|
|
241
|
+
abi: RollupAbi,
|
|
242
|
+
address: this.rollupContract.address,
|
|
243
|
+
},
|
|
244
|
+
/*blobInputs*/ undefined,
|
|
245
|
+
/*stateOverride*/ [],
|
|
246
|
+
);
|
|
247
|
+
this.log.error(`Rollup submit epoch proof tx reverted. ${errorMsg}`);
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private getSubmitEpochProofArgs(args: {
|
|
253
|
+
fromBlock: number;
|
|
254
|
+
toBlock: number;
|
|
255
|
+
publicInputs: RootRollupPublicInputs;
|
|
256
|
+
proof: Proof;
|
|
257
|
+
}) {
|
|
258
|
+
return [
|
|
259
|
+
BigInt(args.fromBlock),
|
|
260
|
+
BigInt(args.toBlock),
|
|
261
|
+
{
|
|
262
|
+
previousArchive: args.publicInputs.previousArchive.root.toString(),
|
|
263
|
+
endArchive: args.publicInputs.endArchive.root.toString(),
|
|
264
|
+
previousBlockHash: args.publicInputs.previousBlockHash.toString(),
|
|
265
|
+
endBlockHash: args.publicInputs.endBlockHash.toString(),
|
|
266
|
+
endTimestamp: args.publicInputs.endTimestamp.toBigInt(),
|
|
267
|
+
outHash: args.publicInputs.outHash.toString(),
|
|
268
|
+
proverId: EthAddress.fromField(args.publicInputs.proverId).toString(),
|
|
269
|
+
},
|
|
270
|
+
makeTuple(AZTEC_MAX_EPOCH_DURATION * 2, i =>
|
|
271
|
+
i % 2 === 0
|
|
272
|
+
? args.publicInputs.fees[i / 2].recipient.toField().toString()
|
|
273
|
+
: args.publicInputs.fees[(i - 1) / 2].value.toString(),
|
|
274
|
+
),
|
|
275
|
+
`0x${args.publicInputs.blobPublicInputs
|
|
276
|
+
.filter((_, i) => i < args.toBlock - args.fromBlock + 1)
|
|
277
|
+
.map(b => b.toString())
|
|
278
|
+
.join(``)}`,
|
|
279
|
+
`0x${serializeToBuffer(args.proof.extractAggregationObject()).toString('hex')}`,
|
|
280
|
+
] as const;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
protected async sleepOrInterrupted() {
|
|
284
|
+
await this.interruptibleSleep.sleep(this.sleepTimeMs);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { compact } from '@aztec/foundation/collection';
|
|
2
|
+
import { memoize } from '@aztec/foundation/decorators';
|
|
3
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
4
|
+
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
5
|
+
import { DateProvider } from '@aztec/foundation/timer';
|
|
6
|
+
import type { Maybe } from '@aztec/foundation/types';
|
|
7
|
+
import type { P2P } from '@aztec/p2p';
|
|
8
|
+
import { PublicProcessorFactory } from '@aztec/simulator/server';
|
|
9
|
+
import type { L2Block, L2BlockSource } from '@aztec/stdlib/block';
|
|
10
|
+
import type { ContractDataSource } from '@aztec/stdlib/contract';
|
|
11
|
+
import { getTimestampRangeForEpoch } from '@aztec/stdlib/epoch-helpers';
|
|
12
|
+
import {
|
|
13
|
+
type EpochProverManager,
|
|
14
|
+
EpochProvingJobTerminalState,
|
|
15
|
+
type ProverCoordination,
|
|
16
|
+
type ProverNodeApi,
|
|
17
|
+
type Service,
|
|
18
|
+
type WorldStateSynchronizer,
|
|
19
|
+
tryStop,
|
|
20
|
+
} from '@aztec/stdlib/interfaces/server';
|
|
21
|
+
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
22
|
+
import type { P2PClientType } from '@aztec/stdlib/p2p';
|
|
23
|
+
import type { Tx, TxHash } from '@aztec/stdlib/tx';
|
|
24
|
+
import {
|
|
25
|
+
Attributes,
|
|
26
|
+
type TelemetryClient,
|
|
27
|
+
type Traceable,
|
|
28
|
+
type Tracer,
|
|
29
|
+
getTelemetryClient,
|
|
30
|
+
trackSpan,
|
|
31
|
+
} from '@aztec/telemetry-client';
|
|
32
|
+
|
|
33
|
+
import { EpochProvingJob, type EpochProvingJobState } from './job/epoch-proving-job.js';
|
|
34
|
+
import { ProverNodeMetrics } from './metrics.js';
|
|
35
|
+
import type { EpochMonitor, EpochMonitorHandler } from './monitors/epoch-monitor.js';
|
|
36
|
+
import type { ProverNodePublisher } from './prover-node-publisher.js';
|
|
37
|
+
|
|
38
|
+
export type ProverNodeOptions = {
|
|
39
|
+
pollingIntervalMs: number;
|
|
40
|
+
maxPendingJobs: number;
|
|
41
|
+
maxParallelBlocksPerEpoch: number;
|
|
42
|
+
txGatheringTimeoutMs: number;
|
|
43
|
+
txGatheringIntervalMs: number;
|
|
44
|
+
txGatheringMaxParallelRequests: number;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* An Aztec Prover Node is a standalone process that monitors the unfinalised chain on L1 for unproven blocks,
|
|
49
|
+
* submits bids for proving them, and monitors if they are accepted. If so, the prover node fetches the txs
|
|
50
|
+
* from a tx source in the p2p network or an external node, re-executes their public functions, creates a rollup
|
|
51
|
+
* proof for the epoch, and submits it to L1.
|
|
52
|
+
*/
|
|
53
|
+
export class ProverNode implements EpochMonitorHandler, ProverNodeApi, Traceable {
|
|
54
|
+
private log = createLogger('prover-node');
|
|
55
|
+
private dateProvider = new DateProvider();
|
|
56
|
+
|
|
57
|
+
private latestEpochWeAreProving: bigint | undefined;
|
|
58
|
+
private jobs: Map<string, EpochProvingJob> = new Map();
|
|
59
|
+
private cachedEpochData: { epochNumber: bigint; blocks: L2Block[]; txs: Tx[] } | undefined = undefined;
|
|
60
|
+
private options: ProverNodeOptions;
|
|
61
|
+
private metrics: ProverNodeMetrics;
|
|
62
|
+
|
|
63
|
+
private txFetcher: RunningPromise;
|
|
64
|
+
private lastBlockNumber: number | undefined;
|
|
65
|
+
|
|
66
|
+
public readonly tracer: Tracer;
|
|
67
|
+
|
|
68
|
+
constructor(
|
|
69
|
+
protected readonly prover: EpochProverManager,
|
|
70
|
+
protected readonly publisher: ProverNodePublisher,
|
|
71
|
+
protected readonly l2BlockSource: L2BlockSource & Maybe<Service>,
|
|
72
|
+
protected readonly l1ToL2MessageSource: L1ToL2MessageSource,
|
|
73
|
+
protected readonly contractDataSource: ContractDataSource,
|
|
74
|
+
protected readonly worldState: WorldStateSynchronizer,
|
|
75
|
+
protected readonly coordination: ProverCoordination & Maybe<Service>,
|
|
76
|
+
protected readonly epochsMonitor: EpochMonitor,
|
|
77
|
+
options: Partial<ProverNodeOptions> = {},
|
|
78
|
+
protected readonly telemetryClient: TelemetryClient = getTelemetryClient(),
|
|
79
|
+
) {
|
|
80
|
+
this.options = {
|
|
81
|
+
pollingIntervalMs: 1_000,
|
|
82
|
+
maxPendingJobs: 100,
|
|
83
|
+
maxParallelBlocksPerEpoch: 32,
|
|
84
|
+
txGatheringTimeoutMs: 60_000,
|
|
85
|
+
txGatheringIntervalMs: 1_000,
|
|
86
|
+
txGatheringMaxParallelRequests: 100,
|
|
87
|
+
...compact(options),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
this.metrics = new ProverNodeMetrics(telemetryClient, 'ProverNode');
|
|
91
|
+
this.tracer = telemetryClient.getTracer('ProverNode');
|
|
92
|
+
this.txFetcher = new RunningPromise(() => this.checkForTxs(), this.log, this.options.txGatheringIntervalMs);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public getProverId() {
|
|
96
|
+
return this.prover.getProverId();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public getP2P() {
|
|
100
|
+
const asP2PClient = this.coordination as P2P<P2PClientType.Prover>;
|
|
101
|
+
if (typeof asP2PClient.isP2PClient === 'function' && asP2PClient.isP2PClient()) {
|
|
102
|
+
return asP2PClient;
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Handles an epoch being completed by starting a proof for it if there are no active jobs for it.
|
|
109
|
+
* @param epochNumber - The epoch number that was just completed.
|
|
110
|
+
*/
|
|
111
|
+
async handleEpochReadyToProve(epochNumber: bigint): Promise<void> {
|
|
112
|
+
try {
|
|
113
|
+
this.log.debug('jobs', JSON.stringify(this.jobs, null, 2));
|
|
114
|
+
const activeJobs = await this.getActiveJobsForEpoch(epochNumber);
|
|
115
|
+
if (activeJobs.length > 0) {
|
|
116
|
+
this.log.info(`Not starting proof for ${epochNumber} since there are active jobs`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// TODO: we probably want to skip starting a proof if we are too far into the current epoch
|
|
120
|
+
await this.startProof(epochNumber);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (err instanceof EmptyEpochError) {
|
|
123
|
+
this.log.info(`Not starting proof for ${epochNumber} since no blocks were found`);
|
|
124
|
+
} else {
|
|
125
|
+
this.log.error(`Error handling epoch completed`, err);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Starts the prover node so it periodically checks for unproven epochs in the unfinalised chain from L1 and
|
|
132
|
+
* starts proving jobs for them.
|
|
133
|
+
*/
|
|
134
|
+
start() {
|
|
135
|
+
this.txFetcher.start();
|
|
136
|
+
this.epochsMonitor.start(this);
|
|
137
|
+
this.log.info('Started ProverNode', this.options);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Stops the prover node and all its dependencies.
|
|
142
|
+
*/
|
|
143
|
+
async stop() {
|
|
144
|
+
this.log.info('Stopping ProverNode');
|
|
145
|
+
await this.txFetcher.stop();
|
|
146
|
+
await this.epochsMonitor.stop();
|
|
147
|
+
await this.prover.stop();
|
|
148
|
+
await tryStop(this.l2BlockSource);
|
|
149
|
+
this.publisher.interrupt();
|
|
150
|
+
await Promise.all(Array.from(this.jobs.values()).map(job => job.stop()));
|
|
151
|
+
await this.worldState.stop();
|
|
152
|
+
await tryStop(this.coordination);
|
|
153
|
+
await this.telemetryClient.stop();
|
|
154
|
+
this.log.info('Stopped ProverNode');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Creates a proof for a block range. Returns once the proof has been submitted to L1.
|
|
159
|
+
*/
|
|
160
|
+
public async prove(epochNumber: number | bigint) {
|
|
161
|
+
const job = await this.createProvingJob(BigInt(epochNumber));
|
|
162
|
+
return job.run();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Starts a proving process and returns immediately.
|
|
167
|
+
*/
|
|
168
|
+
public async startProof(epochNumber: number | bigint) {
|
|
169
|
+
const job = await this.createProvingJob(BigInt(epochNumber));
|
|
170
|
+
void job.run().catch(err => this.log.error(`Error proving epoch ${epochNumber}`, err));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Returns the prover instance.
|
|
175
|
+
*/
|
|
176
|
+
public getProver() {
|
|
177
|
+
return this.prover;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Returns an array of jobs being processed.
|
|
182
|
+
*/
|
|
183
|
+
public getJobs(): Promise<{ uuid: string; status: EpochProvingJobState; epochNumber: number }[]> {
|
|
184
|
+
return Promise.resolve(
|
|
185
|
+
Array.from(this.jobs.entries()).map(([uuid, job]) => ({
|
|
186
|
+
uuid,
|
|
187
|
+
status: job.getState(),
|
|
188
|
+
epochNumber: Number(job.getEpochNumber()),
|
|
189
|
+
})),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
protected async getActiveJobsForEpoch(
|
|
194
|
+
epochBigInt: bigint,
|
|
195
|
+
): Promise<{ uuid: string; status: EpochProvingJobState }[]> {
|
|
196
|
+
const jobs = await this.getJobs();
|
|
197
|
+
const epochNumber = Number(epochBigInt);
|
|
198
|
+
return jobs.filter(job => job.epochNumber === epochNumber && !EpochProvingJobTerminalState.includes(job.status));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private checkMaximumPendingJobs() {
|
|
202
|
+
const { maxPendingJobs } = this.options;
|
|
203
|
+
return maxPendingJobs === 0 || this.jobs.size < maxPendingJobs;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@trackSpan('ProverNode.createProvingJob', epochNumber => ({ [Attributes.EPOCH_NUMBER]: Number(epochNumber) }))
|
|
207
|
+
private async createProvingJob(epochNumber: bigint) {
|
|
208
|
+
if (!this.checkMaximumPendingJobs()) {
|
|
209
|
+
throw new Error(`Maximum pending proving jobs ${this.options.maxPendingJobs} reached. Cannot create new job.`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Gather blocks for this epoch
|
|
213
|
+
const cachedEpochData = this.cachedEpochData?.epochNumber === epochNumber ? this.cachedEpochData : undefined;
|
|
214
|
+
const { blocks, txs } = cachedEpochData ?? (await this.gatherEpochData(epochNumber));
|
|
215
|
+
|
|
216
|
+
const fromBlock = blocks[0].number;
|
|
217
|
+
const toBlock = blocks.at(-1)!.number;
|
|
218
|
+
|
|
219
|
+
// Fast forward world state to right before the target block and get a fork
|
|
220
|
+
this.log.verbose(`Creating proving job for epoch ${epochNumber} for block range ${fromBlock} to ${toBlock}`);
|
|
221
|
+
await this.worldState.syncImmediate(toBlock);
|
|
222
|
+
|
|
223
|
+
// Create a processor using the forked world state
|
|
224
|
+
const publicProcessorFactory = new PublicProcessorFactory(
|
|
225
|
+
this.contractDataSource,
|
|
226
|
+
this.dateProvider,
|
|
227
|
+
this.telemetryClient,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const cleanUp = () => {
|
|
231
|
+
this.jobs.delete(job.getId());
|
|
232
|
+
return Promise.resolve();
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const [_, endTimestamp] = getTimestampRangeForEpoch(epochNumber + 1n, await this.getL1Constants());
|
|
236
|
+
const deadline = new Date(Number(endTimestamp) * 1000);
|
|
237
|
+
|
|
238
|
+
const job = this.doCreateEpochProvingJob(epochNumber, deadline, blocks, txs, publicProcessorFactory, cleanUp);
|
|
239
|
+
this.jobs.set(job.getId(), job);
|
|
240
|
+
return job;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
@memoize
|
|
244
|
+
private getL1Constants() {
|
|
245
|
+
return this.l2BlockSource.getL1Constants();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Monitors for new blocks and requests their txs from the p2p layer to ensure they are available for proving. */
|
|
249
|
+
@trackSpan('ProverNode.checkForTxs')
|
|
250
|
+
private async checkForTxs() {
|
|
251
|
+
const blockNumber = await this.l2BlockSource.getBlockNumber();
|
|
252
|
+
if (this.lastBlockNumber === undefined || blockNumber > this.lastBlockNumber) {
|
|
253
|
+
const block = await this.l2BlockSource.getBlock(blockNumber);
|
|
254
|
+
if (!block) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const txHashes = block.body.txEffects.map(tx => tx.txHash);
|
|
258
|
+
this.log.verbose(`Fetching ${txHashes.length} tx hashes for block number ${blockNumber} from coordination`);
|
|
259
|
+
await this.coordination.getTxsByHash(txHashes); // This stores the txs in the tx pool, no need to persist them here
|
|
260
|
+
this.lastBlockNumber = blockNumber;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@trackSpan('ProverNode.gatherEpochData', epochNumber => ({ [Attributes.EPOCH_NUMBER]: Number(epochNumber) }))
|
|
265
|
+
private async gatherEpochData(epochNumber: bigint) {
|
|
266
|
+
// Gather blocks for this epoch and their txs
|
|
267
|
+
const blocks = await this.gatherBlocks(epochNumber);
|
|
268
|
+
const txs = await this.gatherTxs(epochNumber, blocks);
|
|
269
|
+
|
|
270
|
+
return { blocks, txs };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private async gatherBlocks(epochNumber: bigint) {
|
|
274
|
+
const blocks = await this.l2BlockSource.getBlocksForEpoch(epochNumber);
|
|
275
|
+
if (blocks.length === 0) {
|
|
276
|
+
throw new EmptyEpochError(epochNumber);
|
|
277
|
+
}
|
|
278
|
+
return blocks;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private async gatherTxs(epochNumber: bigint, blocks: L2Block[]) {
|
|
282
|
+
const txsToFind: TxHash[] = blocks.flatMap(block => block.body.txEffects.map(tx => tx.txHash));
|
|
283
|
+
const txs = await this.coordination.getTxsByHash(txsToFind);
|
|
284
|
+
|
|
285
|
+
if (txs.length === txsToFind.length) {
|
|
286
|
+
this.log.verbose(`Gathered all ${txs.length} txs for epoch ${epochNumber}`, { epochNumber });
|
|
287
|
+
return txs;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const txHashesFound = await Promise.all(txs.map(tx => tx.getTxHash()));
|
|
291
|
+
const missingTxHashes = txsToFind
|
|
292
|
+
.filter(txHashToFind => !txHashesFound.some(txHashFound => txHashToFind.equals(txHashFound)))
|
|
293
|
+
.join(', ');
|
|
294
|
+
|
|
295
|
+
throw new Error(`Txs not found for epoch ${epochNumber}: ${missingTxHashes}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Extracted for testing purposes. */
|
|
299
|
+
protected doCreateEpochProvingJob(
|
|
300
|
+
epochNumber: bigint,
|
|
301
|
+
deadline: Date | undefined,
|
|
302
|
+
blocks: L2Block[],
|
|
303
|
+
txs: Tx[],
|
|
304
|
+
publicProcessorFactory: PublicProcessorFactory,
|
|
305
|
+
cleanUp: () => Promise<void>,
|
|
306
|
+
) {
|
|
307
|
+
return new EpochProvingJob(
|
|
308
|
+
this.worldState,
|
|
309
|
+
epochNumber,
|
|
310
|
+
blocks,
|
|
311
|
+
txs,
|
|
312
|
+
this.prover.createEpochProver(),
|
|
313
|
+
publicProcessorFactory,
|
|
314
|
+
this.publisher,
|
|
315
|
+
this.l2BlockSource,
|
|
316
|
+
this.l1ToL2MessageSource,
|
|
317
|
+
this.metrics,
|
|
318
|
+
deadline,
|
|
319
|
+
{ parallelBlockLimit: this.options.maxParallelBlocksPerEpoch },
|
|
320
|
+
cleanUp,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Extracted for testing purposes. */
|
|
325
|
+
protected async triggerMonitors() {
|
|
326
|
+
await this.epochsMonitor.work();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
class EmptyEpochError extends Error {
|
|
331
|
+
constructor(epochNumber: bigint) {
|
|
332
|
+
super(`No blocks found for epoch ${epochNumber}`);
|
|
333
|
+
this.name = 'EmptyEpochError';
|
|
334
|
+
}
|
|
335
|
+
}
|