@aztec/validator-client 0.0.1-commit.b655e406 → 0.0.1-commit.c7c42ec

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 (37) hide show
  1. package/dest/block_proposal_handler.d.ts +7 -6
  2. package/dest/block_proposal_handler.d.ts.map +1 -1
  3. package/dest/block_proposal_handler.js +17 -13
  4. package/dest/config.d.ts +1 -1
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +5 -0
  7. package/dest/duties/validation_service.d.ts +4 -4
  8. package/dest/duties/validation_service.d.ts.map +1 -1
  9. package/dest/duties/validation_service.js +7 -8
  10. package/dest/factory.d.ts +3 -1
  11. package/dest/factory.d.ts.map +1 -1
  12. package/dest/factory.js +1 -1
  13. package/dest/index.d.ts +1 -1
  14. package/dest/key_store/index.d.ts +1 -1
  15. package/dest/key_store/interface.d.ts +1 -1
  16. package/dest/key_store/local_key_store.d.ts +1 -1
  17. package/dest/key_store/local_key_store.d.ts.map +1 -1
  18. package/dest/key_store/local_key_store.js +1 -1
  19. package/dest/key_store/node_keystore_adapter.d.ts +1 -1
  20. package/dest/key_store/node_keystore_adapter.d.ts.map +1 -1
  21. package/dest/key_store/web3signer_key_store.d.ts +1 -7
  22. package/dest/key_store/web3signer_key_store.d.ts.map +1 -1
  23. package/dest/key_store/web3signer_key_store.js +1 -1
  24. package/dest/metrics.d.ts +1 -1
  25. package/dest/metrics.d.ts.map +1 -1
  26. package/dest/validator.d.ts +12 -8
  27. package/dest/validator.d.ts.map +1 -1
  28. package/dest/validator.js +100 -28
  29. package/package.json +16 -14
  30. package/src/block_proposal_handler.ts +26 -23
  31. package/src/config.ts +6 -0
  32. package/src/duties/validation_service.ts +7 -10
  33. package/src/factory.ts +3 -0
  34. package/src/key_store/local_key_store.ts +1 -1
  35. package/src/key_store/node_keystore_adapter.ts +1 -1
  36. package/src/key_store/web3signer_key_store.ts +1 -1
  37. package/src/validator.ts +118 -41
package/src/validator.ts CHANGED
@@ -1,7 +1,10 @@
1
+ import type { FileStoreBlobClient } from '@aztec/blob-client/filestore';
2
+ import { getBlobsPerL1Block } from '@aztec/blob-lib';
1
3
  import type { EpochCache } from '@aztec/epoch-cache';
4
+ import { BlockNumber, EpochNumber } from '@aztec/foundation/branded-types';
5
+ import { Fr } from '@aztec/foundation/curves/bn254';
2
6
  import type { EthAddress } from '@aztec/foundation/eth-address';
3
7
  import type { Signature } from '@aztec/foundation/eth-signature';
4
- import { Fr } from '@aztec/foundation/fields';
5
8
  import { type Logger, createLogger } from '@aztec/foundation/log';
6
9
  import { RunningPromise } from '@aztec/foundation/running-promise';
7
10
  import { sleep } from '@aztec/foundation/sleep';
@@ -16,9 +19,9 @@ import type { IFullNodeBlockBuilder, Validator, ValidatorClientFullConfig } from
16
19
  import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
17
20
  import type { BlockAttestation, BlockProposal, BlockProposalOptions } from '@aztec/stdlib/p2p';
18
21
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
19
- import type { StateReference, Tx } from '@aztec/stdlib/tx';
22
+ import type { Tx } from '@aztec/stdlib/tx';
20
23
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
21
- import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
24
+ import { Attributes, type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
22
25
 
23
26
  import { EventEmitter } from 'events';
24
27
  import type { TypedDataDefinition } from 'viem';
@@ -45,6 +48,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
45
48
  public readonly tracer: Tracer;
46
49
  private validationService: ValidationService;
47
50
  private metrics: ValidatorMetrics;
51
+ private log: Logger;
48
52
 
49
53
  // Whether it has already registered handlers on the p2p client
50
54
  private hasRegisteredHandlers = false;
@@ -52,7 +56,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
52
56
  // Used to check if we are sending the same proposal twice
53
57
  private previousProposal?: BlockProposal;
54
58
 
55
- private lastEpochForCommitteeUpdateLoop: bigint | undefined;
59
+ private lastEpochForCommitteeUpdateLoop: EpochNumber | undefined;
56
60
  private epochCacheUpdateLoop: RunningPromise;
57
61
 
58
62
  private proposersOfInvalidBlocks: Set<string> = new Set();
@@ -63,18 +67,23 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
63
67
  private p2pClient: P2P,
64
68
  private blockProposalHandler: BlockProposalHandler,
65
69
  private config: ValidatorClientFullConfig,
70
+ private fileStoreBlobUploadClient: FileStoreBlobClient | undefined,
66
71
  private dateProvider: DateProvider = new DateProvider(),
67
72
  telemetry: TelemetryClient = getTelemetryClient(),
68
- private log = createLogger('validator'),
73
+ log = createLogger('validator'),
69
74
  ) {
70
75
  super();
76
+
77
+ // Create child logger with fisherman prefix if in fisherman mode
78
+ this.log = config.fishermanMode ? log.createChild('[FISHERMAN]') : log;
79
+
71
80
  this.tracer = telemetry.getTracer('Validator');
72
81
  this.metrics = new ValidatorMetrics(telemetry);
73
82
 
74
- this.validationService = new ValidationService(keyStore, log.createChild('validation-service'));
83
+ this.validationService = new ValidationService(keyStore, this.log.createChild('validation-service'));
75
84
 
76
85
  // Refresh epoch cache every second to trigger alert if participation in committee changes
77
- this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), log, 1000);
86
+ this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), this.log, 1000);
78
87
 
79
88
  const myAddresses = this.getValidatorAddresses();
80
89
  this.log.verbose(`Initialized validator with addresses: ${myAddresses.map(a => a.toString()).join(', ')}`);
@@ -141,6 +150,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
141
150
  l1ToL2MessageSource: L1ToL2MessageSource,
142
151
  txProvider: TxProvider,
143
152
  keyStoreManager: KeystoreManager,
153
+ fileStoreBlobUploadClient?: FileStoreBlobClient,
144
154
  dateProvider: DateProvider = new DateProvider(),
145
155
  telemetry: TelemetryClient = getTelemetryClient(),
146
156
  ) {
@@ -166,6 +176,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
166
176
  p2pClient,
167
177
  blockProposalHandler,
168
178
  config,
179
+ fileStoreBlobUploadClient,
169
180
  dateProvider,
170
181
  telemetry,
171
182
  );
@@ -186,7 +197,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
186
197
  // Proxy method for backwards compatibility with tests
187
198
  public reExecuteTransactions(
188
199
  proposal: BlockProposal,
189
- blockNumber: number,
200
+ blockNumber: BlockNumber,
190
201
  txs: any[],
191
202
  l1ToL2Messages: Fr[],
192
203
  ): Promise<any> {
@@ -223,14 +234,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
223
234
 
224
235
  const myAddresses = this.getValidatorAddresses();
225
236
  const inCommittee = await this.epochCache.filterInCommittee('now', myAddresses);
237
+ this.log.info(`Started validator with addresses: ${myAddresses.map(a => a.toString()).join(', ')}`);
226
238
  if (inCommittee.length > 0) {
227
- this.log.info(
228
- `Started validator with addresses in current validator committee: ${inCommittee
229
- .map(a => a.toString())
230
- .join(', ')}`,
231
- );
232
- } else {
233
- this.log.info(`Started validator with addresses: ${myAddresses.map(a => a.toString()).join(', ')}`);
239
+ this.log.info(`Addresses in current validator committee: ${inCommittee.map(a => a.toString()).join(', ')}`);
234
240
  }
235
241
  this.epochCacheUpdateLoop.start();
236
242
 
@@ -258,8 +264,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
258
264
  }
259
265
  }
260
266
 
267
+ @trackSpan('validator.attestToProposal', (proposal, proposalSender) => ({
268
+ [Attributes.BLOCK_HASH]: proposal.payload.header.hash.toString(),
269
+ [Attributes.PEER_ID]: proposalSender.toString(),
270
+ }))
261
271
  async attestToProposal(proposal: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> {
262
- const slotNumber = proposal.slotNumber.toBigInt();
272
+ const slotNumber = proposal.slotNumber;
263
273
  const proposer = proposal.getSender();
264
274
 
265
275
  // Reject proposals with invalid signatures
@@ -276,15 +286,20 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
276
286
  this.log.info(`Received proposal for slot ${slotNumber}`, {
277
287
  ...proposalInfo,
278
288
  txHashes: proposal.txHashes.map(t => t.toString()),
289
+ fishermanMode: this.config.fishermanMode || false,
279
290
  });
280
291
 
281
292
  // Reexecute txs if we are part of the committee so we can attest, or if slashing is enabled so we can slash
282
293
  // invalid proposals even when not in the committee, or if we are configured to always reexecute for monitoring purposes.
283
- const { validatorReexecute, slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals } = this.config;
294
+ // In fisherman mode, we always reexecute to validate proposals.
295
+ const { validatorReexecute, slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } =
296
+ this.config;
284
297
  const shouldReexecute =
298
+ fishermanMode ||
285
299
  (slashBroadcastedInvalidBlockPenalty > 0n && validatorReexecute) ||
286
300
  (partOfCommittee && validatorReexecute) ||
287
- alwaysReexecuteBlockProposals;
301
+ alwaysReexecuteBlockProposals ||
302
+ this.fileStoreBlobUploadClient;
288
303
 
289
304
  const validationResult = await this.blockProposalHandler.handleBlockProposal(
290
305
  proposal,
@@ -325,17 +340,61 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
325
340
  }
326
341
 
327
342
  // Check that I have any address in current committee before attesting
328
- if (!partOfCommittee) {
343
+ // In fisherman mode, we still create attestations for validation even if not in committee
344
+ if (!partOfCommittee && !this.config.fishermanMode) {
329
345
  this.log.verbose(`No validator in the current committee, skipping attestation`, proposalInfo);
330
346
  return undefined;
331
347
  }
332
348
 
333
349
  // Provided all of the above checks pass, we can attest to the proposal
334
- this.log.info(`Attesting to proposal for slot ${slotNumber}`, proposalInfo);
350
+ this.log.info(`${partOfCommittee ? 'Attesting to' : 'Validated'} proposal for slot ${slotNumber}`, {
351
+ ...proposalInfo,
352
+ inCommittee: partOfCommittee,
353
+ fishermanMode: this.config.fishermanMode || false,
354
+ });
355
+
335
356
  this.metrics.incSuccessfulAttestations(inCommittee.length);
336
357
 
358
+ // Upload blobs to filestore after successful re-execution (fire-and-forget)
359
+ if (validationResult.reexecutionResult?.block && this.fileStoreBlobUploadClient) {
360
+ void Promise.resolve().then(async () => {
361
+ try {
362
+ const blobFields = validationResult.reexecutionResult!.block.getCheckpointBlobFields();
363
+ const blobs = getBlobsPerL1Block(blobFields);
364
+ await this.fileStoreBlobUploadClient!.saveBlobs(blobs, true);
365
+ this.log.debug(`Uploaded ${blobs.length} blobs to filestore from re-execution`, proposalInfo);
366
+ } catch (err) {
367
+ this.log.warn(`Failed to upload blobs from re-execution`, err);
368
+ }
369
+ });
370
+ }
371
+
337
372
  // If the above function does not throw an error, then we can attest to the proposal
338
- return this.createBlockAttestationsFromProposal(proposal, inCommittee);
373
+ // Determine which validators should attest
374
+ let attestors: EthAddress[];
375
+ if (partOfCommittee) {
376
+ attestors = inCommittee;
377
+ } else if (this.config.fishermanMode) {
378
+ // In fisherman mode, create attestations for validation purposes even if not in committee. These won't be broadcast.
379
+ attestors = this.getValidatorAddresses();
380
+ } else {
381
+ attestors = [];
382
+ }
383
+
384
+ // Only create attestations if we have attestors
385
+ if (attestors.length === 0) {
386
+ return undefined;
387
+ }
388
+
389
+ if (this.config.fishermanMode) {
390
+ // bail out early and don't save attestations to the pool in fisherman mode
391
+ this.log.info(`Creating attestations for proposal for slot ${slotNumber}`, {
392
+ ...proposalInfo,
393
+ attestors: attestors.map(a => a.toString()),
394
+ });
395
+ return undefined;
396
+ }
397
+ return this.createBlockAttestationsFromProposal(proposal, attestors);
339
398
  }
340
399
 
341
400
  private slashInvalidBlock(proposal: BlockProposal) {
@@ -360,37 +419,47 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
360
419
  validator: proposer,
361
420
  amount: this.config.slashBroadcastedInvalidBlockPenalty,
362
421
  offenseType: OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL,
363
- epochOrSlot: proposal.slotNumber.toBigInt(),
422
+ epochOrSlot: BigInt(proposal.slotNumber),
364
423
  },
365
424
  ]);
366
425
  }
367
426
 
427
+ // TODO(palla/mbps): Block proposal should not require a checkpoint proposal
368
428
  async createBlockProposal(
369
- blockNumber: number,
429
+ blockNumber: BlockNumber,
370
430
  header: CheckpointHeader,
371
431
  archive: Fr,
372
- stateReference: StateReference,
373
432
  txs: Tx[],
374
433
  proposerAddress: EthAddress | undefined,
375
434
  options: BlockProposalOptions,
376
- ): Promise<BlockProposal | undefined> {
377
- if (this.previousProposal?.slotNumber.equals(header.slotNumber)) {
378
- this.log.verbose(`Already made a proposal for the same slot, skipping proposal`);
379
- return Promise.resolve(undefined);
380
- }
381
-
382
- const newProposal = await this.validationService.createBlockProposal(
383
- header,
384
- archive,
385
- stateReference,
386
- txs,
387
- proposerAddress,
388
- { ...options, broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal },
389
- );
435
+ ): Promise<BlockProposal> {
436
+ // TODO(palla/mbps): Prevent double proposals properly
437
+ // if (this.previousProposal?.slotNumber === header.slotNumber) {
438
+ // this.log.verbose(`Already made a proposal for the same slot, skipping proposal`);
439
+ // return Promise.resolve(undefined);
440
+ // }
441
+
442
+ this.log.info(`Assembling block proposal for block ${blockNumber} slot ${header.slotNumber}`);
443
+ const newProposal = await this.validationService.createBlockProposal(header, archive, txs, proposerAddress, {
444
+ ...options,
445
+ broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
446
+ });
390
447
  this.previousProposal = newProposal;
391
448
  return newProposal;
392
449
  }
393
450
 
451
+ // TODO(palla/mbps): Effectively create a checkpoint proposal different from a block proposal
452
+ createCheckpointProposal(
453
+ header: CheckpointHeader,
454
+ archive: Fr,
455
+ txs: Tx[],
456
+ proposerAddress: EthAddress | undefined,
457
+ options: BlockProposalOptions,
458
+ ): Promise<BlockProposal> {
459
+ this.log.info(`Assembling checkpoint proposal for slot ${header.slotNumber}`);
460
+ return this.createBlockProposal(0 as BlockNumber, header, archive, txs, proposerAddress, options);
461
+ }
462
+
394
463
  async broadcastBlockProposal(proposal: BlockProposal): Promise<void> {
395
464
  await this.p2pClient.broadcastProposal(proposal);
396
465
  }
@@ -403,15 +472,23 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
403
472
  }
404
473
 
405
474
  async collectOwnAttestations(proposal: BlockProposal): Promise<BlockAttestation[]> {
406
- const slot = proposal.payload.header.slotNumber.toBigInt();
475
+ const slot = proposal.payload.header.slotNumber;
407
476
  const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
408
477
  this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
409
- return this.createBlockAttestationsFromProposal(proposal, inCommittee);
478
+ const attestations = await this.createBlockAttestationsFromProposal(proposal, inCommittee);
479
+
480
+ // We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
481
+ // other nodes can see that our validators did attest to this block proposal, and do not slash us
482
+ // due to inactivity for missed attestations.
483
+ void this.p2pClient.broadcastAttestations(attestations).catch(err => {
484
+ this.log.error(`Failed to broadcast self-attestations for slot ${slot}`, err);
485
+ });
486
+ return attestations;
410
487
  }
411
488
 
412
489
  async collectAttestations(proposal: BlockProposal, required: number, deadline: Date): Promise<BlockAttestation[]> {
413
490
  // Wait and poll the p2pClient's attestation pool for this block until we have enough attestations
414
- const slot = proposal.payload.header.slotNumber.toBigInt();
491
+ const slot = proposal.payload.header.slotNumber;
415
492
  this.log.debug(`Collecting ${required} attestations for slot ${slot} with deadline ${deadline.toISOString()}`);
416
493
 
417
494
  if (+deadline < this.dateProvider.now()) {