@aztec/validator-client 0.0.1-commit.0b941701 → 0.0.1-commit.10bd49492

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 (50) hide show
  1. package/README.md +63 -18
  2. package/dest/block_proposal_handler.d.ts +7 -7
  3. package/dest/block_proposal_handler.d.ts.map +1 -1
  4. package/dest/block_proposal_handler.js +125 -64
  5. package/dest/checkpoint_builder.d.ts +21 -12
  6. package/dest/checkpoint_builder.d.ts.map +1 -1
  7. package/dest/checkpoint_builder.js +107 -39
  8. package/dest/config.d.ts +1 -1
  9. package/dest/config.d.ts.map +1 -1
  10. package/dest/config.js +28 -4
  11. package/dest/duties/validation_service.d.ts +2 -2
  12. package/dest/duties/validation_service.d.ts.map +1 -1
  13. package/dest/duties/validation_service.js +5 -11
  14. package/dest/factory.d.ts +1 -1
  15. package/dest/factory.d.ts.map +1 -1
  16. package/dest/factory.js +2 -1
  17. package/dest/index.d.ts +1 -2
  18. package/dest/index.d.ts.map +1 -1
  19. package/dest/index.js +0 -1
  20. package/dest/key_store/ha_key_store.d.ts +1 -1
  21. package/dest/key_store/ha_key_store.d.ts.map +1 -1
  22. package/dest/key_store/ha_key_store.js +3 -3
  23. package/dest/metrics.d.ts +12 -3
  24. package/dest/metrics.d.ts.map +1 -1
  25. package/dest/metrics.js +46 -5
  26. package/dest/validator.d.ts +40 -14
  27. package/dest/validator.d.ts.map +1 -1
  28. package/dest/validator.js +213 -57
  29. package/package.json +19 -17
  30. package/src/block_proposal_handler.ts +155 -85
  31. package/src/checkpoint_builder.ts +144 -38
  32. package/src/config.ts +28 -4
  33. package/src/duties/validation_service.ts +11 -10
  34. package/src/factory.ts +1 -0
  35. package/src/index.ts +0 -1
  36. package/src/key_store/ha_key_store.ts +3 -3
  37. package/src/metrics.ts +63 -6
  38. package/src/validator.ts +263 -68
  39. package/dest/tx_validator/index.d.ts +0 -3
  40. package/dest/tx_validator/index.d.ts.map +0 -1
  41. package/dest/tx_validator/index.js +0 -2
  42. package/dest/tx_validator/nullifier_cache.d.ts +0 -14
  43. package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
  44. package/dest/tx_validator/nullifier_cache.js +0 -24
  45. package/dest/tx_validator/tx_validator_factory.d.ts +0 -18
  46. package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
  47. package/dest/tx_validator/tx_validator_factory.js +0 -54
  48. package/src/tx_validator/index.ts +0 -2
  49. package/src/tx_validator/nullifier_cache.ts +0 -30
  50. package/src/tx_validator/tx_validator_factory.ts +0 -135
package/src/validator.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { BlobClientInterface } from '@aztec/blob-client/client';
2
2
  import { type Blob, getBlobsPerL1Block } from '@aztec/blob-lib';
3
3
  import type { EpochCache } from '@aztec/epoch-cache';
4
+ import { validateFeeAssetPriceModifier } from '@aztec/ethereum/contracts';
4
5
  import {
5
6
  BlockNumber,
6
7
  CheckpointNumber,
@@ -18,33 +19,36 @@ import { RunningPromise } from '@aztec/foundation/running-promise';
18
19
  import { sleep } from '@aztec/foundation/sleep';
19
20
  import { DateProvider } from '@aztec/foundation/timer';
20
21
  import type { KeystoreManager } from '@aztec/node-keystore';
21
- import type { P2P, PeerId, TxProvider } from '@aztec/p2p';
22
+ import type { DuplicateAttestationInfo, DuplicateProposalInfo, P2P, PeerId } from '@aztec/p2p';
22
23
  import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
23
24
  import { OffenseType, WANT_TO_SLASH_EVENT, type Watcher, type WatcherEmitter } from '@aztec/slasher';
24
25
  import type { AztecAddress } from '@aztec/stdlib/aztec-address';
25
26
  import type { CommitteeAttestationsAndSigners, L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
26
- import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
27
+ import { validateCheckpoint } from '@aztec/stdlib/checkpoint';
28
+ import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
27
29
  import type {
28
30
  CreateCheckpointProposalLastBlockData,
31
+ ITxProvider,
29
32
  Validator,
30
33
  ValidatorClientFullConfig,
31
34
  WorldStateSynchronizer,
32
35
  } from '@aztec/stdlib/interfaces/server';
33
- import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
34
- import type {
35
- BlockProposal,
36
- BlockProposalOptions,
37
- CheckpointAttestation,
38
- CheckpointProposalCore,
39
- CheckpointProposalOptions,
36
+ import { type L1ToL2MessageSource, accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
37
+ import {
38
+ type BlockProposal,
39
+ type BlockProposalOptions,
40
+ type CheckpointAttestation,
41
+ CheckpointProposal,
42
+ type CheckpointProposalCore,
43
+ type CheckpointProposalOptions,
40
44
  } from '@aztec/stdlib/p2p';
41
- import { CheckpointProposal } from '@aztec/stdlib/p2p';
42
45
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
43
46
  import type { BlockHeader, CheckpointGlobalVariables, Tx } from '@aztec/stdlib/tx';
44
47
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
45
48
  import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
46
- import { createHASigner } from '@aztec/validator-ha-signer/factory';
49
+ import { createHASigner, createLocalSignerWithProtection } from '@aztec/validator-ha-signer/factory';
47
50
  import { DutyType, type SigningContext } from '@aztec/validator-ha-signer/types';
51
+ import type { ValidatorHASigner } from '@aztec/validator-ha-signer/validator-ha-signer';
48
52
 
49
53
  import { EventEmitter } from 'events';
50
54
  import type { TypedDataDefinition } from 'viem';
@@ -75,22 +79,24 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
75
79
  private validationService: ValidationService;
76
80
  private metrics: ValidatorMetrics;
77
81
  private log: Logger;
78
-
79
82
  // Whether it has already registered handlers on the p2p client
80
83
  private hasRegisteredHandlers = false;
81
84
 
82
- // Used to check if we are sending the same proposal twice
83
- private previousProposal?: BlockProposal;
85
+ /** Tracks the last block proposal we created, to detect duplicate proposal attempts. */
86
+ private lastProposedBlock?: BlockProposal;
87
+
88
+ /** Tracks the last checkpoint proposal we created. */
89
+ private lastProposedCheckpoint?: CheckpointProposal;
84
90
 
85
91
  private lastEpochForCommitteeUpdateLoop: EpochNumber | undefined;
86
92
  private epochCacheUpdateLoop: RunningPromise;
93
+ /** Tracks the last epoch in which each attester successfully submitted at least one attestation. */
94
+ private lastAttestedEpochByAttester: Map<string, EpochNumber> = new Map();
87
95
 
88
96
  private proposersOfInvalidBlocks: Set<string> = new Set();
89
97
 
90
- // TODO(palla/mbps): Remove this once checkpoint validation is stable and we can validate all blocks properly.
91
- // Tracks slots for which we have successfully validated a block proposal, so we can attest to checkpoint proposals for those slots.
92
- // eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
93
- private validatedBlockSlots: Set<SlotNumber> = new Set();
98
+ /** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
99
+ private lastAttestedProposal?: CheckpointProposalCore;
94
100
 
95
101
  protected constructor(
96
102
  private keyStore: ExtendedValidatorKeyStore,
@@ -103,6 +109,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
103
109
  private l1ToL2MessageSource: L1ToL2MessageSource,
104
110
  private config: ValidatorClientFullConfig,
105
111
  private blobClient: BlobClientInterface,
112
+ private slashingProtectionSigner: ValidatorHASigner,
106
113
  private dateProvider: DateProvider = new DateProvider(),
107
114
  telemetry: TelemetryClient = getTelemetryClient(),
108
115
  log = createLogger('validator'),
@@ -156,6 +163,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
156
163
  this.log.trace(`No committee found for slot`);
157
164
  return;
158
165
  }
166
+ this.metrics.setCurrentEpoch(epoch);
159
167
  if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
160
168
  const me = this.getValidatorAddresses();
161
169
  const committeeSet = new Set(committee.map(v => v.toString()));
@@ -184,7 +192,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
184
192
  p2pClient: P2P,
185
193
  blockSource: L2BlockSource & L2BlockSink,
186
194
  l1ToL2MessageSource: L1ToL2MessageSource,
187
- txProvider: TxProvider,
195
+ txProvider: ITxProvider,
188
196
  keyStoreManager: KeystoreManager,
189
197
  blobClient: BlobClientInterface,
190
198
  dateProvider: DateProvider = new DateProvider(),
@@ -193,6 +201,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
193
201
  const metrics = new ValidatorMetrics(telemetry);
194
202
  const blockProposalValidator = new BlockProposalValidator(epochCache, {
195
203
  txsPermitted: !config.disableTransactions,
204
+ maxTxsPerBlock: config.validateMaxTxsPerBlock,
196
205
  });
197
206
  const blockProposalHandler = new BlockProposalHandler(
198
207
  checkpointsBuilder,
@@ -208,16 +217,28 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
208
217
  telemetry,
209
218
  );
210
219
 
211
- let validatorKeyStore: ExtendedValidatorKeyStore = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
220
+ const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
221
+ let slashingProtectionSigner: ValidatorHASigner;
212
222
  if (config.haSigningEnabled) {
223
+ // Multi-node HA mode: use PostgreSQL-backed distributed locking.
213
224
  // If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
214
225
  const haConfig = {
215
226
  ...config,
216
227
  maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000,
217
228
  };
218
- const { signer } = await createHASigner(haConfig);
219
- validatorKeyStore = new HAKeyStore(validatorKeyStore, signer);
229
+ ({ signer: slashingProtectionSigner } = await createHASigner(haConfig, {
230
+ telemetryClient: telemetry,
231
+ dateProvider,
232
+ }));
233
+ } else {
234
+ // Single-node mode: use LMDB-backed local signing protection.
235
+ // This prevents double-signing if the node crashes and restarts mid-proposal.
236
+ ({ signer: slashingProtectionSigner } = await createLocalSignerWithProtection(config, {
237
+ telemetryClient: telemetry,
238
+ dateProvider,
239
+ }));
220
240
  }
241
+ const validatorKeyStore: ExtendedValidatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, slashingProtectionSigner);
221
242
 
222
243
  const validator = new ValidatorClient(
223
244
  validatorKeyStore,
@@ -230,6 +251,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
230
251
  l1ToL2MessageSource,
231
252
  config,
232
253
  blobClient,
254
+ slashingProtectionSigner,
233
255
  dateProvider,
234
256
  telemetry,
235
257
  );
@@ -267,6 +289,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
267
289
  this.config = { ...this.config, ...config };
268
290
  }
269
291
 
292
+ public reloadKeystore(newManager: KeystoreManager): void {
293
+ const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
294
+ this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
295
+ this.validationService = new ValidationService(this.keyStore, this.log.createChild('validation-service'));
296
+ }
297
+
270
298
  public async start() {
271
299
  if (this.epochCacheUpdateLoop.isRunning()) {
272
300
  this.log.warn(`Validator client already started`);
@@ -313,6 +341,16 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
313
341
  ): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
314
342
  this.p2pClient.registerCheckpointProposalHandler(checkpointHandler);
315
343
 
344
+ // Duplicate proposal handler - triggers slashing for equivocation
345
+ this.p2pClient.registerDuplicateProposalCallback((info: DuplicateProposalInfo) => {
346
+ this.handleDuplicateProposal(info);
347
+ });
348
+
349
+ // Duplicate attestation handler - triggers slashing for attestation equivocation
350
+ this.p2pClient.registerDuplicateAttestationCallback((info: DuplicateAttestationInfo) => {
351
+ this.handleDuplicateAttestation(info);
352
+ });
353
+
316
354
  const myAddresses = this.getValidatorAddresses();
317
355
  this.p2pClient.registerThisValidatorAddresses(myAddresses);
318
356
 
@@ -340,6 +378,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
340
378
  return false;
341
379
  }
342
380
 
381
+ // Ignore proposals from ourselves (may happen in HA setups)
382
+ if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
383
+ this.log.debug(`Ignoring block proposal from self for slot ${slotNumber}`, {
384
+ proposer: proposer.toString(),
385
+ slotNumber,
386
+ });
387
+ return false;
388
+ }
389
+
343
390
  // Check if we're in the committee (for metrics purposes)
344
391
  const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
345
392
  const partOfCommittee = inCommittee.length > 0;
@@ -369,9 +416,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
369
416
  );
370
417
 
371
418
  if (!validationResult.isValid) {
372
- this.log.warn(`Block proposal validation failed: ${validationResult.reason}`, proposalInfo);
373
-
374
419
  const reason = validationResult.reason || 'unknown';
420
+
421
+ this.log.warn(`Block proposal validation failed: ${reason}`, proposalInfo);
422
+
375
423
  // Classify failure reason: bad proposal vs node issue
376
424
  const badProposalReasons: BlockProposalValidationFailureReason[] = [
377
425
  'invalid_proposal',
@@ -413,10 +461,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
413
461
  return false;
414
462
  }
415
463
 
416
- // TODO(palla/mbps): Remove this once checkpoint validation is stable.
417
- // Track that we successfully validated a block for this slot, so we can attest to checkpoint proposals for it.
418
- this.validatedBlockSlots.add(slotNumber);
419
-
420
464
  return true;
421
465
  }
422
466
 
@@ -445,6 +489,23 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
445
489
  return undefined;
446
490
  }
447
491
 
492
+ // Ignore proposals from ourselves (may happen in HA setups)
493
+ if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
494
+ this.log.debug(`Ignoring block proposal from self for slot ${slotNumber}`, {
495
+ proposer: proposer.toString(),
496
+ slotNumber,
497
+ });
498
+ return undefined;
499
+ }
500
+
501
+ // Validate fee asset price modifier is within allowed range
502
+ if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
503
+ this.log.warn(
504
+ `Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${slotNumber}`,
505
+ );
506
+ return undefined;
507
+ }
508
+
448
509
  // Check that I have any address in current committee before attesting
449
510
  const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
450
511
  const partOfCommittee = inCommittee.length > 0;
@@ -453,25 +514,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
453
514
  slotNumber,
454
515
  archive: proposal.archive.toString(),
455
516
  proposer: proposer.toString(),
456
- txCount: proposal.txHashes.length,
457
517
  };
458
518
  this.log.info(`Received checkpoint proposal for slot ${slotNumber}`, {
459
519
  ...proposalInfo,
460
- txHashes: proposal.txHashes.map(t => t.toString()),
461
520
  fishermanMode: this.config.fishermanMode || false,
462
521
  });
463
522
 
464
- // TODO(palla/mbps): Remove this once checkpoint validation is stable.
465
- // Check that we have successfully validated a block for this slot before attesting to the checkpoint.
466
- if (!this.validatedBlockSlots.has(slotNumber)) {
467
- this.log.warn(`No validated block found for slot ${slotNumber}, refusing to attest to checkpoint`, proposalInfo);
468
- return undefined;
469
- }
470
-
471
523
  // Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
472
- // TODO(palla/mbps): Change default to false once checkpoint validation is stable.
473
- if (this.config.skipCheckpointProposalValidation !== false) {
474
- this.log.verbose(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
524
+ if (this.config.skipCheckpointProposalValidation) {
525
+ this.log.warn(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
475
526
  } else {
476
527
  const validationResult = await this.validateCheckpointProposal(proposal, proposalInfo);
477
528
  if (!validationResult.isValid) {
@@ -501,6 +552,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
501
552
 
502
553
  this.metrics.incSuccessfulAttestations(inCommittee.length);
503
554
 
555
+ // Track epoch participation per attester: count each (attester, epoch) pair at most once
556
+ const proposalEpoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
557
+ for (const attester of inCommittee) {
558
+ const key = attester.toString();
559
+ const lastEpoch = this.lastAttestedEpochByAttester.get(key);
560
+ if (lastEpoch === undefined || proposalEpoch > lastEpoch) {
561
+ this.lastAttestedEpochByAttester.set(key, proposalEpoch);
562
+ this.metrics.incAttestedEpochCount(attester);
563
+ }
564
+ }
565
+
504
566
  // Determine which validators should attest
505
567
  let attestors: EthAddress[];
506
568
  if (partOfCommittee) {
@@ -526,15 +588,45 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
526
588
  return undefined;
527
589
  }
528
590
 
529
- return this.createCheckpointAttestationsFromProposal(proposal, attestors);
591
+ return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
592
+ }
593
+
594
+ /**
595
+ * Checks if we should attest to a slot based on equivocation prevention rules.
596
+ * @returns true if we should attest, false if we should skip
597
+ */
598
+ private shouldAttestToSlot(slotNumber: SlotNumber): boolean {
599
+ // If attestToEquivocatedProposals is true, always allow
600
+ if (this.config.attestToEquivocatedProposals) {
601
+ return true;
602
+ }
603
+
604
+ // Check if incoming slot is strictly greater than last attested
605
+ if (this.lastAttestedProposal && slotNumber <= this.lastAttestedProposal.slotNumber) {
606
+ this.log.warn(
607
+ `Refusing to process a proposal for slot ${slotNumber} given we already attested to a proposal for slot ${this.lastAttestedProposal.slotNumber}`,
608
+ );
609
+ return false;
610
+ }
611
+
612
+ return true;
530
613
  }
531
614
 
532
615
  private async createCheckpointAttestationsFromProposal(
533
616
  proposal: CheckpointProposalCore,
534
617
  attestors: EthAddress[] = [],
535
- ): Promise<CheckpointAttestation[]> {
618
+ ): Promise<CheckpointAttestation[] | undefined> {
619
+ // Equivocation check: must happen right before signing to minimize the race window
620
+ if (!this.shouldAttestToSlot(proposal.slotNumber)) {
621
+ return undefined;
622
+ }
623
+
536
624
  const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
537
- await this.p2pClient.addCheckpointAttestations(attestations);
625
+
626
+ // Track the proposal we attested to (to prevent equivocation)
627
+ this.lastAttestedProposal = proposal;
628
+
629
+ await this.p2pClient.addOwnCheckpointAttestations(attestations);
538
630
  return attestations;
539
631
  }
540
632
 
@@ -547,7 +639,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
547
639
  proposalInfo: LogData,
548
640
  ): Promise<{ isValid: true } | { isValid: false; reason: string }> {
549
641
  const slot = proposal.slotNumber;
550
- const timeoutSeconds = 10;
642
+
643
+ // Timeout block syncing at the start of the next slot
644
+ const config = this.checkpointsBuilder.getConfig();
645
+ const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
646
+ const timeoutSeconds = Math.max(1, nextSlotTimestampSeconds - Math.floor(this.dateProvider.now() / 1000));
551
647
 
552
648
  // Wait for last block to sync by archive
553
649
  let lastBlockHeader: BlockHeader | undefined;
@@ -582,6 +678,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
582
678
  return { isValid: false, reason: 'no_blocks_for_slot' };
583
679
  }
584
680
 
681
+ // Ensure the last block for this slot matches the archive in the checkpoint proposal
682
+ if (!blocks.at(-1)?.archive.root.equals(proposal.archive)) {
683
+ this.log.warn(`Last block archive mismatch for checkpoint proposal`, proposalInfo);
684
+ return { isValid: false, reason: 'last_block_archive_mismatch' };
685
+ }
686
+
585
687
  this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
586
688
  ...proposalInfo,
587
689
  blockNumbers: blocks.map(b => b.number),
@@ -595,14 +697,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
595
697
  // Get L1-to-L2 messages for this checkpoint
596
698
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
597
699
 
598
- // Compute the previous checkpoint out hashes for the epoch.
599
- // TODO: There can be a more efficient way to get the previous checkpoint out hashes without having to fetch the
600
- // actual checkpoints and the blocks/txs in them.
700
+ // Collect the out hashes of all the checkpoints before this one in the same epoch
601
701
  const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
602
- const previousCheckpoints = (await this.blockSource.getCheckpointsForEpoch(epoch))
603
- .filter(b => b.number < checkpointNumber)
604
- .sort((a, b) => a.number - b.number);
605
- const previousCheckpointOutHashes = previousCheckpoints.map(c => c.getCheckpointOutHash());
702
+ const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch))
703
+ .filter(c => c.checkpointNumber < checkpointNumber)
704
+ .map(c => c.checkpointOutHash);
606
705
 
607
706
  // Fork world state at the block before the first block
608
707
  const parentBlockNumber = BlockNumber(firstBlock.number - 1);
@@ -613,10 +712,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
613
712
  const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(
614
713
  checkpointNumber,
615
714
  constants,
715
+ proposal.feeAssetPriceModifier,
616
716
  l1ToL2Messages,
617
717
  previousCheckpointOutHashes,
618
718
  fork,
619
719
  blocks,
720
+ this.log.getBindings(),
620
721
  );
621
722
 
622
723
  // Complete the checkpoint to get computed values
@@ -642,18 +743,36 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
642
743
  return { isValid: false, reason: 'archive_mismatch' };
643
744
  }
644
745
 
645
- // Check that the accumulated out hash matches the value in the proposal.
646
- const computedOutHash = computedCheckpoint.getCheckpointOutHash();
647
- const proposalOutHash = proposal.checkpointHeader.epochOutHash;
648
- if (!computedOutHash.equals(proposalOutHash)) {
746
+ // Check that the accumulated epoch out hash matches the value in the proposal.
747
+ // The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
748
+ const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
749
+ const computedEpochOutHash = accumulateCheckpointOutHashes([...previousCheckpointOutHashes, checkpointOutHash]);
750
+ const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
751
+ if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
649
752
  this.log.warn(`Epoch out hash mismatch`, {
650
- proposalOutHash: proposalOutHash.toString(),
651
- computedOutHash: computedOutHash.toString(),
753
+ proposalEpochOutHash: proposalEpochOutHash.toString(),
754
+ computedEpochOutHash: computedEpochOutHash.toString(),
755
+ checkpointOutHash: checkpointOutHash.toString(),
756
+ previousCheckpointOutHashes: previousCheckpointOutHashes.map(h => h.toString()),
652
757
  ...proposalInfo,
653
758
  });
654
759
  return { isValid: false, reason: 'out_hash_mismatch' };
655
760
  }
656
761
 
762
+ // Final round of validations on the checkpoint, just in case.
763
+ try {
764
+ validateCheckpoint(computedCheckpoint, {
765
+ rollupManaLimit: this.checkpointsBuilder.getConfig().rollupManaLimit,
766
+ maxDABlockGas: this.config.validateMaxDABlockGas,
767
+ maxL2BlockGas: this.config.validateMaxL2BlockGas,
768
+ maxTxsPerBlock: this.config.validateMaxTxsPerBlock,
769
+ maxTxsPerCheckpoint: this.config.validateMaxTxsPerCheckpoint,
770
+ });
771
+ } catch (err) {
772
+ this.log.warn(`Checkpoint validation failed: ${err}`, proposalInfo);
773
+ return { isValid: false, reason: 'checkpoint_validation_failed' };
774
+ }
775
+
657
776
  this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
658
777
  return { isValid: true };
659
778
  } finally {
@@ -670,6 +789,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
670
789
  chainId: gv.chainId,
671
790
  version: gv.version,
672
791
  slotNumber: gv.slotNumber,
792
+ timestamp: gv.timestamp,
673
793
  coinbase: gv.coinbase,
674
794
  feeRecipient: gv.feeRecipient,
675
795
  gasFees: gv.gasFees,
@@ -679,7 +799,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
679
799
  /**
680
800
  * Uploads blobs for a checkpoint to the filestore (fire and forget).
681
801
  */
682
- private async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
802
+ protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
683
803
  try {
684
804
  const lastBlockHeader = await this.blockSource.getBlockHeaderByArchive(proposal.archive);
685
805
  if (!lastBlockHeader) {
@@ -694,7 +814,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
694
814
  }
695
815
 
696
816
  const blobFields = blocks.flatMap(b => b.toBlobFields());
697
- const blobs: Blob[] = getBlobsPerL1Block(blobFields);
817
+ const blobs: Blob[] = await getBlobsPerL1Block(blobFields);
698
818
  await this.blobClient.sendBlobsToFilestore(blobs);
699
819
  this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
700
820
  ...proposalInfo,
@@ -732,6 +852,52 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
732
852
  ]);
733
853
  }
734
854
 
855
+ /**
856
+ * Handle detection of a duplicate proposal (equivocation).
857
+ * Emits a slash event when a proposer sends multiple proposals for the same position.
858
+ */
859
+ private handleDuplicateProposal(info: DuplicateProposalInfo): void {
860
+ const { slot, proposer, type } = info;
861
+
862
+ this.log.warn(`Triggering slash event for duplicate ${type} proposal from ${proposer.toString()} at slot ${slot}`, {
863
+ proposer: proposer.toString(),
864
+ slot,
865
+ type,
866
+ });
867
+
868
+ // Emit slash event
869
+ this.emit(WANT_TO_SLASH_EVENT, [
870
+ {
871
+ validator: proposer,
872
+ amount: this.config.slashDuplicateProposalPenalty,
873
+ offenseType: OffenseType.DUPLICATE_PROPOSAL,
874
+ epochOrSlot: BigInt(slot),
875
+ },
876
+ ]);
877
+ }
878
+
879
+ /**
880
+ * Handle detection of a duplicate attestation (equivocation).
881
+ * Emits a slash event when an attester signs attestations for different proposals at the same slot.
882
+ */
883
+ private handleDuplicateAttestation(info: DuplicateAttestationInfo): void {
884
+ const { slot, attester } = info;
885
+
886
+ this.log.warn(`Triggering slash event for duplicate attestation from ${attester.toString()} at slot ${slot}`, {
887
+ attester: attester.toString(),
888
+ slot,
889
+ });
890
+
891
+ this.emit(WANT_TO_SLASH_EVENT, [
892
+ {
893
+ validator: attester,
894
+ amount: this.config.slashDuplicateAttestationPenalty,
895
+ offenseType: OffenseType.DUPLICATE_ATTESTATION,
896
+ epochOrSlot: BigInt(slot),
897
+ },
898
+ ]);
899
+ }
900
+
735
901
  async createBlockProposal(
736
902
  blockHeader: BlockHeader,
737
903
  indexWithinCheckpoint: IndexWithinCheckpoint,
@@ -739,13 +905,21 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
739
905
  archive: Fr,
740
906
  txs: Tx[],
741
907
  proposerAddress: EthAddress | undefined,
742
- options: BlockProposalOptions,
908
+ options: BlockProposalOptions = {},
743
909
  ): Promise<BlockProposal> {
744
- // TODO(palla/mbps): Prevent double proposals properly
745
- // if (this.previousProposal?.slotNumber === blockHeader.globalVariables.slotNumber) {
746
- // this.log.verbose(`Already made a proposal for the same slot, skipping proposal`);
747
- // return Promise.resolve(undefined);
748
- // }
910
+ // Validate that we're not creating a proposal for an older or equal position
911
+ if (this.lastProposedBlock) {
912
+ const lastSlot = this.lastProposedBlock.slotNumber;
913
+ const lastIndex = this.lastProposedBlock.indexWithinCheckpoint;
914
+ const newSlot = blockHeader.globalVariables.slotNumber;
915
+
916
+ if (newSlot < lastSlot || (newSlot === lastSlot && indexWithinCheckpoint <= lastIndex)) {
917
+ throw new Error(
918
+ `Cannot create block proposal for slot ${newSlot} index ${indexWithinCheckpoint}: ` +
919
+ `already proposed block for slot ${lastSlot} index ${lastIndex}`,
920
+ );
921
+ }
922
+ }
749
923
 
750
924
  this.log.info(
751
925
  `Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`,
@@ -762,25 +936,42 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
762
936
  broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
763
937
  },
764
938
  );
765
- this.previousProposal = newProposal;
939
+ this.lastProposedBlock = newProposal;
766
940
  return newProposal;
767
941
  }
768
942
 
769
943
  async createCheckpointProposal(
770
944
  checkpointHeader: CheckpointHeader,
771
945
  archive: Fr,
946
+ feeAssetPriceModifier: bigint,
772
947
  lastBlockInfo: CreateCheckpointProposalLastBlockData | undefined,
773
948
  proposerAddress: EthAddress | undefined,
774
- options: CheckpointProposalOptions,
949
+ options: CheckpointProposalOptions = {},
775
950
  ): Promise<CheckpointProposal> {
951
+ // Validate that we're not creating a proposal for an older or equal slot
952
+ if (this.lastProposedCheckpoint) {
953
+ const lastSlot = this.lastProposedCheckpoint.slotNumber;
954
+ const newSlot = checkpointHeader.slotNumber;
955
+
956
+ if (newSlot <= lastSlot) {
957
+ throw new Error(
958
+ `Cannot create checkpoint proposal for slot ${newSlot}: ` +
959
+ `already proposed checkpoint for slot ${lastSlot}`,
960
+ );
961
+ }
962
+ }
963
+
776
964
  this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
777
- return await this.validationService.createCheckpointProposal(
965
+ const newProposal = await this.validationService.createCheckpointProposal(
778
966
  checkpointHeader,
779
967
  archive,
968
+ feeAssetPriceModifier,
780
969
  lastBlockInfo,
781
970
  proposerAddress,
782
971
  options,
783
972
  );
973
+ this.lastProposedCheckpoint = newProposal;
974
+ return newProposal;
784
975
  }
785
976
 
786
977
  async broadcastBlockProposal(proposal: BlockProposal): Promise<void> {
@@ -802,6 +993,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
802
993
  this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
803
994
  const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
804
995
 
996
+ if (!attestations) {
997
+ return [];
998
+ }
999
+
805
1000
  // We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
806
1001
  // other nodes can see that our validators did attest to this block proposal, and do not slash us
807
1002
  // due to inactivity for missed attestations.
@@ -1,3 +0,0 @@
1
- export * from './nullifier_cache.js';
2
- export * from './tx_validator_factory.js';
3
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy90eF92YWxpZGF0b3IvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxzQkFBc0IsQ0FBQztBQUNyQyxjQUFjLDJCQUEyQixDQUFDIn0=
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tx_validator/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,2BAA2B,CAAC"}
@@ -1,2 +0,0 @@
1
- export * from './nullifier_cache.js';
2
- export * from './tx_validator_factory.js';
@@ -1,14 +0,0 @@
1
- import type { NullifierSource } from '@aztec/p2p';
2
- import type { MerkleTreeReadOperations } from '@aztec/stdlib/interfaces/server';
3
- /**
4
- * Implements a nullifier source by checking a DB and an in-memory collection.
5
- * Intended for validating transactions as they are added to a block.
6
- */
7
- export declare class NullifierCache implements NullifierSource {
8
- private db;
9
- nullifiers: Set<string>;
10
- constructor(db: MerkleTreeReadOperations);
11
- nullifiersExist(nullifiers: Buffer[]): Promise<boolean[]>;
12
- addNullifiers(nullifiers: Buffer[]): void;
13
- }
14
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibnVsbGlmaWVyX2NhY2hlLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvdHhfdmFsaWRhdG9yL251bGxpZmllcl9jYWNoZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssRUFBRSxlQUFlLEVBQUUsTUFBTSxZQUFZLENBQUM7QUFDbEQsT0FBTyxLQUFLLEVBQUUsd0JBQXdCLEVBQUUsTUFBTSxpQ0FBaUMsQ0FBQztBQUdoRjs7O0dBR0c7QUFDSCxxQkFBYSxjQUFlLFlBQVcsZUFBZTtJQUd4QyxPQUFPLENBQUMsRUFBRTtJQUZ0QixVQUFVLEVBQUUsR0FBRyxDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBRXhCLFlBQW9CLEVBQUUsRUFBRSx3QkFBd0IsRUFFL0M7SUFFWSxlQUFlLENBQUMsVUFBVSxFQUFFLE1BQU0sRUFBRSxHQUFHLE9BQU8sQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQU9yRTtJQUVNLGFBQWEsQ0FBQyxVQUFVLEVBQUUsTUFBTSxFQUFFLFFBSXhDO0NBQ0YifQ==
@@ -1 +0,0 @@
1
- {"version":3,"file":"nullifier_cache.d.ts","sourceRoot":"","sources":["../../src/tx_validator/nullifier_cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAGhF;;;GAGG;AACH,qBAAa,cAAe,YAAW,eAAe;IAGxC,OAAO,CAAC,EAAE;IAFtB,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAExB,YAAoB,EAAE,EAAE,wBAAwB,EAE/C;IAEY,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAOrE;IAEM,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,QAIxC;CACF"}
@@ -1,24 +0,0 @@
1
- import { MerkleTreeId } from '@aztec/stdlib/trees';
2
- /**
3
- * Implements a nullifier source by checking a DB and an in-memory collection.
4
- * Intended for validating transactions as they are added to a block.
5
- */ export class NullifierCache {
6
- db;
7
- nullifiers;
8
- constructor(db){
9
- this.db = db;
10
- this.nullifiers = new Set();
11
- }
12
- async nullifiersExist(nullifiers) {
13
- const cacheResults = nullifiers.map((n)=>this.nullifiers.has(n.toString()));
14
- const toCheckDb = nullifiers.filter((_n, index)=>!cacheResults[index]);
15
- const dbHits = await this.db.findLeafIndices(MerkleTreeId.NULLIFIER_TREE, toCheckDb);
16
- let dbIndex = 0;
17
- return nullifiers.map((_n, index)=>cacheResults[index] || dbHits[dbIndex++] !== undefined);
18
- }
19
- addNullifiers(nullifiers) {
20
- for (const nullifier of nullifiers){
21
- this.nullifiers.add(nullifier.toString());
22
- }
23
- }
24
- }