@aztec/validator-client 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.
Files changed (45) hide show
  1. package/dest/config.d.ts +22 -0
  2. package/dest/config.d.ts.map +1 -0
  3. package/dest/config.js +31 -0
  4. package/dest/duties/validation_service.d.ts +29 -0
  5. package/dest/duties/validation_service.d.ts.map +1 -0
  6. package/dest/duties/validation_service.js +35 -0
  7. package/dest/errors/index.d.ts +2 -0
  8. package/dest/errors/index.d.ts.map +1 -0
  9. package/dest/errors/index.js +1 -0
  10. package/dest/errors/validator.error.d.ts +29 -0
  11. package/dest/errors/validator.error.d.ts.map +1 -0
  12. package/dest/errors/validator.error.js +45 -0
  13. package/dest/factory.d.ts +13 -0
  14. package/dest/factory.d.ts.map +1 -0
  15. package/dest/factory.js +11 -0
  16. package/dest/index.d.ts +4 -0
  17. package/dest/index.d.ts.map +1 -0
  18. package/dest/index.js +3 -0
  19. package/dest/key_store/index.d.ts +3 -0
  20. package/dest/key_store/index.d.ts.map +1 -0
  21. package/dest/key_store/index.js +2 -0
  22. package/dest/key_store/interface.d.ts +25 -0
  23. package/dest/key_store/interface.d.ts.map +1 -0
  24. package/dest/key_store/interface.js +4 -0
  25. package/dest/key_store/local_key_store.d.ts +28 -0
  26. package/dest/key_store/local_key_store.d.ts.map +1 -0
  27. package/dest/key_store/local_key_store.js +32 -0
  28. package/dest/metrics.d.ts +11 -0
  29. package/dest/metrics.d.ts.map +1 -0
  30. package/dest/metrics.js +35 -0
  31. package/dest/validator.d.ts +81 -0
  32. package/dest/validator.d.ts.map +1 -0
  33. package/dest/validator.js +246 -0
  34. package/package.json +94 -0
  35. package/src/config.ts +56 -0
  36. package/src/duties/validation_service.ts +45 -0
  37. package/src/errors/index.ts +1 -0
  38. package/src/errors/validator.error.ts +55 -0
  39. package/src/factory.ts +28 -0
  40. package/src/index.ts +3 -0
  41. package/src/key_store/index.ts +2 -0
  42. package/src/key_store/interface.ts +26 -0
  43. package/src/key_store/local_key_store.ts +46 -0
  44. package/src/metrics.ts +50 -0
  45. package/src/validator.ts +362 -0
@@ -0,0 +1,362 @@
1
+ import type { EpochCache } from '@aztec/epoch-cache';
2
+ import { Buffer32 } from '@aztec/foundation/buffer';
3
+ import type { Fr } from '@aztec/foundation/fields';
4
+ import { createLogger } from '@aztec/foundation/log';
5
+ import { RunningPromise } from '@aztec/foundation/running-promise';
6
+ import { sleep } from '@aztec/foundation/sleep';
7
+ import { DateProvider, type Timer } from '@aztec/foundation/timer';
8
+ import type { P2P } from '@aztec/p2p';
9
+ import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
10
+ import type { L2Block } from '@aztec/stdlib/block';
11
+ import type { BlockAttestation, BlockProposal } from '@aztec/stdlib/p2p';
12
+ import type { BlockHeader, GlobalVariables, Tx, TxHash } from '@aztec/stdlib/tx';
13
+ import { type TelemetryClient, WithTracer, getTelemetryClient } from '@aztec/telemetry-client';
14
+
15
+ import type { ValidatorClientConfig } from './config.js';
16
+ import { ValidationService } from './duties/validation_service.js';
17
+ import {
18
+ AttestationTimeoutError,
19
+ BlockBuilderNotProvidedError,
20
+ InvalidValidatorPrivateKeyError,
21
+ ReExFailedTxsError,
22
+ ReExStateMismatchError,
23
+ ReExTimeoutError,
24
+ TransactionsNotAvailableError,
25
+ } from './errors/validator.error.js';
26
+ import type { ValidatorKeyStore } from './key_store/interface.js';
27
+ import { LocalKeyStore } from './key_store/local_key_store.js';
28
+ import { ValidatorMetrics } from './metrics.js';
29
+
30
+ /**
31
+ * Callback function for building a block
32
+ *
33
+ * We reuse the sequencer's block building functionality for re-execution
34
+ */
35
+ type BlockBuilderCallback = (
36
+ txs: Iterable<Tx> | AsyncIterableIterator<Tx>,
37
+ globalVariables: GlobalVariables,
38
+ opts?: { validateOnly?: boolean },
39
+ ) => Promise<{
40
+ block: L2Block;
41
+ publicProcessorDuration: number;
42
+ numTxs: number;
43
+ numFailedTxs: number;
44
+ blockBuildingTimer: Timer;
45
+ }>;
46
+
47
+ export interface Validator {
48
+ start(): Promise<void>;
49
+ registerBlockProposalHandler(): void;
50
+ registerBlockBuilder(blockBuilder: BlockBuilderCallback): void;
51
+
52
+ // Block validation responsibilities
53
+ createBlockProposal(header: BlockHeader, archive: Fr, txs: TxHash[]): Promise<BlockProposal | undefined>;
54
+ attestToProposal(proposal: BlockProposal): void;
55
+
56
+ broadcastBlockProposal(proposal: BlockProposal): void;
57
+ collectAttestations(proposal: BlockProposal, required: number, deadline: Date): Promise<BlockAttestation[]>;
58
+ }
59
+
60
+ /**
61
+ * Validator Client
62
+ */
63
+ export class ValidatorClient extends WithTracer implements Validator {
64
+ private validationService: ValidationService;
65
+ private metrics: ValidatorMetrics;
66
+
67
+ // Used to check if we are sending the same proposal twice
68
+ private previousProposal?: BlockProposal;
69
+
70
+ // Callback registered to: sequencer.buildBlock
71
+ private blockBuilder?: BlockBuilderCallback = undefined;
72
+
73
+ private epochCacheUpdateLoop: RunningPromise;
74
+
75
+ private blockProposalValidator: BlockProposalValidator;
76
+
77
+ constructor(
78
+ private keyStore: ValidatorKeyStore,
79
+ private epochCache: EpochCache,
80
+ private p2pClient: P2P,
81
+ private config: ValidatorClientConfig,
82
+ private dateProvider: DateProvider = new DateProvider(),
83
+ telemetry: TelemetryClient = getTelemetryClient(),
84
+ private log = createLogger('validator'),
85
+ ) {
86
+ // Instantiate tracer
87
+ super(telemetry, 'Validator');
88
+ this.metrics = new ValidatorMetrics(telemetry);
89
+
90
+ this.validationService = new ValidationService(keyStore);
91
+
92
+ this.blockProposalValidator = new BlockProposalValidator(epochCache);
93
+
94
+ // Refresh epoch cache every second to trigger commiteeChanged event
95
+ this.epochCacheUpdateLoop = new RunningPromise(
96
+ () =>
97
+ this.epochCache
98
+ .getCommittee()
99
+ .then(() => {})
100
+ .catch(err => log.error('Error updating validator committee', err)),
101
+ log,
102
+ 1000,
103
+ );
104
+
105
+ // Listen to commiteeChanged event to alert operator when their validator has entered the committee
106
+ this.epochCache.on('committeeChanged', (newCommittee, epochNumber) => {
107
+ const me = this.keyStore.getAddress();
108
+ if (newCommittee.some(addr => addr.equals(me))) {
109
+ this.log.info(`Validator ${me.toString()} is on the validator committee for epoch ${epochNumber}`);
110
+ } else {
111
+ this.log.verbose(`Validator ${me.toString()} not on the validator committee for epoch ${epochNumber}`);
112
+ }
113
+ });
114
+
115
+ this.log.verbose(`Initialized validator with address ${this.keyStore.getAddress().toString()}`);
116
+ }
117
+
118
+ static new(
119
+ config: ValidatorClientConfig,
120
+ epochCache: EpochCache,
121
+ p2pClient: P2P,
122
+ dateProvider: DateProvider = new DateProvider(),
123
+ telemetry: TelemetryClient = getTelemetryClient(),
124
+ ) {
125
+ if (!config.validatorPrivateKey) {
126
+ throw new InvalidValidatorPrivateKeyError();
127
+ }
128
+
129
+ const privateKey = validatePrivateKey(config.validatorPrivateKey);
130
+ const localKeyStore = new LocalKeyStore(privateKey);
131
+
132
+ const validator = new ValidatorClient(localKeyStore, epochCache, p2pClient, config, dateProvider, telemetry);
133
+ validator.registerBlockProposalHandler();
134
+ return validator;
135
+ }
136
+
137
+ public async start() {
138
+ // Sync the committee from the smart contract
139
+ // https://github.com/AztecProtocol/aztec-packages/issues/7962
140
+
141
+ const me = this.keyStore.getAddress();
142
+ const inCommittee = await this.epochCache.isInCommittee(me);
143
+ if (inCommittee) {
144
+ this.log.info(`Started validator with address ${me.toString()} in current validator committee`);
145
+ } else {
146
+ this.log.info(`Started validator with address ${me.toString()}`);
147
+ }
148
+ this.epochCacheUpdateLoop.start();
149
+ return Promise.resolve();
150
+ }
151
+
152
+ public async stop() {
153
+ await this.epochCacheUpdateLoop.stop();
154
+ }
155
+
156
+ public registerBlockProposalHandler() {
157
+ const handler = (block: BlockProposal): Promise<BlockAttestation | undefined> => {
158
+ return this.attestToProposal(block);
159
+ };
160
+ this.p2pClient.registerBlockProposalHandler(handler);
161
+ }
162
+
163
+ /**
164
+ * Register a callback function for building a block
165
+ *
166
+ * We reuse the sequencer's block building functionality for re-execution
167
+ */
168
+ public registerBlockBuilder(blockBuilder: BlockBuilderCallback) {
169
+ this.blockBuilder = blockBuilder;
170
+ }
171
+
172
+ async attestToProposal(proposal: BlockProposal): Promise<BlockAttestation | undefined> {
173
+ const slotNumber = proposal.slotNumber.toNumber();
174
+ const proposalInfo = {
175
+ slotNumber,
176
+ blockNumber: proposal.payload.header.globalVariables.blockNumber.toNumber(),
177
+ archive: proposal.payload.archive.toString(),
178
+ txCount: proposal.payload.txHashes.length,
179
+ txHashes: proposal.payload.txHashes.map(txHash => txHash.toString()),
180
+ };
181
+ this.log.verbose(`Received request to attest for slot ${slotNumber}`);
182
+
183
+ // Check that I am in the committee
184
+ if (!(await this.epochCache.isInCommittee(this.keyStore.getAddress()))) {
185
+ this.log.verbose(`Not in the committee, skipping attestation`);
186
+ return undefined;
187
+ }
188
+
189
+ // Check that the proposal is from the current proposer, or the next proposer.
190
+ const invalidProposal = await this.blockProposalValidator.validate(proposal);
191
+ if (invalidProposal) {
192
+ this.log.verbose(`Proposal is not valid, skipping attestation`);
193
+ return undefined;
194
+ }
195
+
196
+ // Check that all of the transactions in the proposal are available in the tx pool before attesting
197
+ this.log.verbose(`Processing attestation for slot ${slotNumber}`, proposalInfo);
198
+ try {
199
+ await this.ensureTransactionsAreAvailable(proposal);
200
+
201
+ if (this.config.validatorReexecute) {
202
+ this.log.verbose(`Re-executing transactions in the proposal before attesting`);
203
+ await this.reExecuteTransactions(proposal);
204
+ }
205
+ } catch (error: any) {
206
+ // If the transactions are not available, then we should not attempt to attest
207
+ if (error instanceof TransactionsNotAvailableError) {
208
+ this.log.error(`Transactions not available, skipping attestation`, error, proposalInfo);
209
+ } else {
210
+ // This branch most commonly be hit if the transactions are available, but the re-execution fails
211
+ // Catch all error handler
212
+ this.log.error(`Failed to attest to proposal`, error, proposalInfo);
213
+ }
214
+ return undefined;
215
+ }
216
+
217
+ // Provided all of the above checks pass, we can attest to the proposal
218
+ this.log.info(`Attesting to proposal for slot ${slotNumber}`, proposalInfo);
219
+
220
+ // If the above function does not throw an error, then we can attest to the proposal
221
+ return this.validationService.attestToProposal(proposal);
222
+ }
223
+
224
+ /**
225
+ * Re-execute the transactions in the proposal and check that the state updates match the header state
226
+ * @param proposal - The proposal to re-execute
227
+ */
228
+ async reExecuteTransactions(proposal: BlockProposal) {
229
+ const { header, txHashes } = proposal.payload;
230
+
231
+ const txs = (await Promise.all(txHashes.map(tx => this.p2pClient.getTxByHash(tx)))).filter(
232
+ tx => tx !== undefined,
233
+ ) as Tx[];
234
+
235
+ // If we cannot request all of the transactions, then we should fail
236
+ if (txs.length !== txHashes.length) {
237
+ throw new TransactionsNotAvailableError(txHashes);
238
+ }
239
+
240
+ // Assertion: This check will fail if re-execution is not enabled
241
+ if (this.blockBuilder === undefined) {
242
+ throw new BlockBuilderNotProvidedError();
243
+ }
244
+
245
+ // Use the sequencer's block building logic to re-execute the transactions
246
+ const stopTimer = this.metrics.reExecutionTimer();
247
+ const { block, numFailedTxs } = await this.blockBuilder(txs, header.globalVariables, {
248
+ validateOnly: true,
249
+ });
250
+ stopTimer();
251
+
252
+ this.log.verbose(`Transaction re-execution complete`);
253
+
254
+ if (numFailedTxs > 0) {
255
+ await this.metrics.recordFailedReexecution(proposal);
256
+ throw new ReExFailedTxsError(numFailedTxs);
257
+ }
258
+
259
+ if (block.body.txEffects.length !== txHashes.length) {
260
+ await this.metrics.recordFailedReexecution(proposal);
261
+ throw new ReExTimeoutError();
262
+ }
263
+
264
+ // This function will throw an error if state updates do not match
265
+ if (!block.archive.root.equals(proposal.archive)) {
266
+ await this.metrics.recordFailedReexecution(proposal);
267
+ throw new ReExStateMismatchError();
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Ensure that all of the transactions in the proposal are available in the tx pool before attesting
273
+ *
274
+ * 1. Check if the local tx pool contains all of the transactions in the proposal
275
+ * 2. If any transactions are not in the local tx pool, request them from the network
276
+ * 3. If we cannot retrieve them from the network, throw an error
277
+ * @param proposal - The proposal to attest to
278
+ */
279
+ async ensureTransactionsAreAvailable(proposal: BlockProposal) {
280
+ const txHashes: TxHash[] = proposal.payload.txHashes;
281
+ const transactionStatuses = await Promise.all(txHashes.map(txHash => this.p2pClient.getTxStatus(txHash)));
282
+
283
+ const missingTxs = txHashes.filter((_, index) => !['pending', 'mined'].includes(transactionStatuses[index] ?? ''));
284
+
285
+ if (missingTxs.length === 0) {
286
+ return; // All transactions are available
287
+ }
288
+
289
+ this.log.verbose(`Missing ${missingTxs.length} transactions in the tx pool, requesting from the network`);
290
+
291
+ const requestedTxs = await this.p2pClient.requestTxs(missingTxs);
292
+ if (requestedTxs.some(tx => tx === undefined)) {
293
+ throw new TransactionsNotAvailableError(missingTxs);
294
+ }
295
+ }
296
+
297
+ async createBlockProposal(header: BlockHeader, archive: Fr, txs: TxHash[]): Promise<BlockProposal | undefined> {
298
+ if (this.previousProposal?.slotNumber.equals(header.globalVariables.slotNumber)) {
299
+ this.log.verbose(`Already made a proposal for the same slot, skipping proposal`);
300
+ return Promise.resolve(undefined);
301
+ }
302
+
303
+ const newProposal = await this.validationService.createBlockProposal(header, archive, txs);
304
+ this.previousProposal = newProposal;
305
+ return newProposal;
306
+ }
307
+
308
+ broadcastBlockProposal(proposal: BlockProposal): void {
309
+ this.p2pClient.broadcastProposal(proposal);
310
+ }
311
+
312
+ // TODO(https://github.com/AztecProtocol/aztec-packages/issues/7962)
313
+ async collectAttestations(proposal: BlockProposal, required: number, deadline: Date): Promise<BlockAttestation[]> {
314
+ // Wait and poll the p2pClient's attestation pool for this block until we have enough attestations
315
+ const slot = proposal.payload.header.globalVariables.slotNumber.toBigInt();
316
+ this.log.debug(`Collecting ${required} attestations for slot ${slot} with deadline ${deadline.toISOString()}`);
317
+
318
+ if (+deadline < this.dateProvider.now()) {
319
+ this.log.error(
320
+ `Deadline ${deadline.toISOString()} for collecting ${required} attestations for slot ${slot} is in the past`,
321
+ );
322
+ throw new AttestationTimeoutError(required, slot);
323
+ }
324
+
325
+ const proposalId = proposal.archive.toString();
326
+ const myAttestation = await this.validationService.attestToProposal(proposal);
327
+
328
+ let attestations: BlockAttestation[] = [];
329
+ while (true) {
330
+ const collectedAttestations = [myAttestation, ...(await this.p2pClient.getAttestationsForSlot(slot, proposalId))];
331
+ const oldSenders = await Promise.all(attestations.map(attestation => attestation.getSender()));
332
+ for (const collected of collectedAttestations) {
333
+ const collectedSender = await collected.getSender();
334
+ if (!oldSenders.some(sender => sender.equals(collectedSender))) {
335
+ this.log.debug(`Received attestation for slot ${slot} from ${collectedSender.toString()}`);
336
+ }
337
+ }
338
+ attestations = collectedAttestations;
339
+
340
+ if (attestations.length >= required) {
341
+ this.log.verbose(`Collected all ${required} attestations for slot ${slot}`);
342
+ return attestations;
343
+ }
344
+
345
+ if (+deadline < this.dateProvider.now()) {
346
+ this.log.error(`Timeout ${deadline.toISOString()} waiting for ${required} attestations for slot ${slot}`);
347
+ throw new AttestationTimeoutError(required, slot);
348
+ }
349
+
350
+ this.log.debug(`Collected ${attestations.length} attestations so far`);
351
+ await sleep(this.config.attestationPollingIntervalMs);
352
+ }
353
+ }
354
+ }
355
+
356
+ function validatePrivateKey(privateKey: string): Buffer32 {
357
+ try {
358
+ return Buffer32.fromString(privateKey);
359
+ } catch (error) {
360
+ throw new InvalidValidatorPrivateKeyError();
361
+ }
362
+ }