@aztec/validator-client 0.0.1-commit.b2a5d0dd1 → 0.0.1-commit.b3d3157a

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/src/validator.ts CHANGED
@@ -5,6 +5,7 @@ import { CheckpointNumber, EpochNumber, IndexWithinCheckpoint, SlotNumber } from
5
5
  import { Fr } from '@aztec/foundation/curves/bn254';
6
6
  import type { EthAddress } from '@aztec/foundation/eth-address';
7
7
  import type { Signature } from '@aztec/foundation/eth-signature';
8
+ import { FifoSet } from '@aztec/foundation/fifo-set';
8
9
  import { type LogData, type Logger, createLogger } from '@aztec/foundation/log';
9
10
  import { RunningPromise } from '@aztec/foundation/running-promise';
10
11
  import { sleep } from '@aztec/foundation/sleep';
@@ -12,9 +13,17 @@ import { DateProvider } from '@aztec/foundation/timer';
12
13
  import type { KeystoreManager } from '@aztec/node-keystore';
13
14
  import type { DuplicateAttestationInfo, DuplicateProposalInfo, P2P, PeerId } from '@aztec/p2p';
14
15
  import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
15
- import { OffenseType, WANT_TO_SLASH_EVENT, type Watcher, type WatcherEmitter } from '@aztec/slasher';
16
+ import {
17
+ OffenseType,
18
+ WANT_TO_CLEAR_SLASH_EVENT,
19
+ WANT_TO_SLASH_EVENT,
20
+ type Watcher,
21
+ type WatcherEmitter,
22
+ getOffenseTypeName,
23
+ } from '@aztec/slasher';
16
24
  import type { AztecAddress } from '@aztec/stdlib/aztec-address';
17
25
  import type { CommitteeAttestationsAndSigners, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
26
+ import type { CheckpointReexecutionTracker } from '@aztec/stdlib/checkpoint';
18
27
  import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
19
28
  import type {
20
29
  ITxProvider,
@@ -30,6 +39,7 @@ import {
30
39
  CheckpointProposal,
31
40
  type CheckpointProposalCore,
32
41
  type CheckpointProposalOptions,
42
+ type CoordinationSignatureContext,
33
43
  } from '@aztec/stdlib/p2p';
34
44
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
35
45
  import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
@@ -52,18 +62,50 @@ import { HAKeyStore } from './key_store/ha_key_store.js';
52
62
  import type { ExtendedValidatorKeyStore } from './key_store/interface.js';
53
63
  import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
54
64
  import { ValidatorMetrics } from './metrics.js';
55
- import { type BlockProposalValidationFailureReason, ProposalHandler } from './proposal_handler.js';
65
+ import {
66
+ type BlockProposalValidationFailureReason,
67
+ type CheckpointProposalValidationFailureReason,
68
+ type CheckpointProposalValidationFailureResult,
69
+ ProposalHandler,
70
+ } from './proposal_handler.js';
56
71
 
57
72
  // We maintain a set of proposers who have proposed invalid blocks.
58
73
  // Just cap the set to avoid unbounded growth.
59
74
  const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
75
+ const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
76
+ const MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS = 1000;
77
+ const MAX_TRACKED_BAD_ATTESTATIONS = 10_000;
60
78
 
61
79
  // What errors from the block proposal handler result in slashing
62
80
  const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT: BlockProposalValidationFailureReason[] = [
63
81
  'state_mismatch',
64
82
  'failed_txs',
83
+ 'global_variables_mismatch',
84
+ 'invalid_proposal',
85
+ 'parent_block_wrong_slot',
86
+ 'in_hash_mismatch',
65
87
  ];
66
88
 
89
+ const SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT: Record<CheckpointProposalValidationFailureReason, boolean> = {
90
+ // enabled
91
+ ['invalid_fee_asset_price_modifier']: true,
92
+ ['checkpoint_header_mismatch']: true,
93
+ // These late mismatches should normally be caught by earlier checks, but if reached after validating the local
94
+ // checkpoint inputs, the proposer-signed payload disagrees with deterministic recomputation.
95
+ ['archive_mismatch']: true,
96
+ ['out_hash_mismatch']: true,
97
+ ['no_blocks_for_slot']: true,
98
+ ['too_many_blocks_in_checkpoint']: true,
99
+ ['checkpoint_validation_failed']: true,
100
+ ['last_block_archive_mismatch']: true,
101
+
102
+ // disabled
103
+ ['invalid_signature']: false,
104
+ ['last_block_not_found']: false,
105
+ ['block_fetch_error']: false,
106
+ ['checkpoint_already_published']: false,
107
+ };
108
+
67
109
  /**
68
110
  * Validator Client
69
111
  */
@@ -86,7 +128,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
86
128
  /** Tracks the last epoch in which each attester successfully submitted at least one attestation. */
87
129
  private lastAttestedEpochByAttester: Map<string, EpochNumber> = new Map();
88
130
 
89
- private proposersOfInvalidBlocks: Set<string> = new Set();
131
+ private proposersOfInvalidBlocks = FifoSet.withLimit<string>(MAX_PROPOSERS_OF_INVALID_BLOCKS);
132
+ private slotsWithInvalidProposals = FifoSet.withLimit<SlotNumber>(MAX_TRACKED_INVALID_PROPOSAL_SLOTS);
133
+ private invalidCheckpointProposalOffenseKeys = FifoSet.withLimit<string>(MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS);
134
+ private badAttestationOffenseKeys = FifoSet.withLimit<string>(MAX_TRACKED_BAD_ATTESTATIONS);
135
+ private slotsWithProposalEquivocation = FifoSet.withLimit<SlotNumber>(MAX_TRACKED_INVALID_PROPOSAL_SLOTS);
90
136
 
91
137
  /** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
92
138
  private lastAttestedProposal?: CheckpointProposalCore;
@@ -115,11 +161,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
115
161
  this.tracer = telemetry.getTracer('Validator');
116
162
  this.metrics = new ValidatorMetrics(telemetry);
117
163
 
118
- this.validationService = new ValidationService(keyStore, this.log.createChild('validation-service'));
164
+ this.validationService = new ValidationService(
165
+ keyStore,
166
+ this.getSignatureContext(),
167
+ this.log.createChild('validation-service'),
168
+ );
169
+ this.proposalHandler.setCheckpointProposalValidationFailureCallback((proposal, result, proposalInfo) =>
170
+ this.handleInvalidCheckpointProposal(proposal, result, proposalInfo),
171
+ );
119
172
 
120
173
  // Refresh epoch cache every second to trigger alert if participation in committee changes
121
174
  this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), this.log, 1000);
122
-
123
175
  const myAddresses = this.getValidatorAddresses();
124
176
  this.log.verbose(`Initialized validator with addresses: ${myAddresses.map(a => a.toString()).join(', ')}`);
125
177
  }
@@ -188,6 +240,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
188
240
  txProvider: ITxProvider,
189
241
  keyStoreManager: KeystoreManager,
190
242
  blobClient: BlobClientInterface,
243
+ reexecutionTracker: CheckpointReexecutionTracker,
191
244
  dateProvider: DateProvider = new DateProvider(),
192
245
  telemetry: TelemetryClient = getTelemetryClient(),
193
246
  slashingProtectionDb?: SlashingProtectionDatabase,
@@ -196,6 +249,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
196
249
  const blockProposalValidator = new BlockProposalValidator(epochCache, {
197
250
  txsPermitted: !config.disableTransactions,
198
251
  maxTxsPerBlock: config.validateMaxTxsPerBlock,
252
+ maxBlocksPerCheckpoint: config.maxBlocksPerCheckpoint,
253
+ skipSlotValidation: config.skipProposalSlotValidation,
254
+ signatureContext: {
255
+ chainId: config.l1ChainId,
256
+ rollupAddress: config.rollupAddress,
257
+ },
199
258
  });
200
259
  const proposalHandler = new ProposalHandler(
201
260
  checkpointsBuilder,
@@ -207,6 +266,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
207
266
  epochCache,
208
267
  config,
209
268
  blobClient,
269
+ reexecutionTracker,
210
270
  metrics,
211
271
  dateProvider,
212
272
  telemetry,
@@ -275,6 +335,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
275
335
  return this.keyStore.signTypedDataWithAddress(addr, msg, context);
276
336
  }
277
337
 
338
+ private getSignatureContext(): CoordinationSignatureContext {
339
+ return {
340
+ chainId: this.config.l1ChainId,
341
+ rollupAddress: this.config.rollupAddress,
342
+ };
343
+ }
344
+
278
345
  public getCoinbaseForAttestor(attestor: EthAddress): EthAddress {
279
346
  return this.keyStore.getCoinbaseAddress(attestor);
280
347
  }
@@ -287,14 +354,27 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
287
354
  return this.config;
288
355
  }
289
356
 
357
+ public hasProposalEquivocation(slotNumber: SlotNumber): boolean {
358
+ return this.slotsWithProposalEquivocation.has(slotNumber);
359
+ }
360
+
361
+ public hasInvalidProposals(slotNumber: SlotNumber): boolean {
362
+ return this.slotsWithInvalidProposals.has(slotNumber);
363
+ }
364
+
290
365
  public updateConfig(config: Partial<ValidatorClientFullConfig>) {
291
366
  this.config = { ...this.config, ...config };
367
+ this.proposalHandler.updateConfig(config);
292
368
  }
293
369
 
294
370
  public reloadKeystore(newManager: KeystoreManager): void {
295
371
  const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
296
372
  this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
297
- this.validationService = new ValidationService(this.keyStore, this.log.createChild('validation-service'));
373
+ this.validationService = new ValidationService(
374
+ this.keyStore,
375
+ this.getSignatureContext(),
376
+ this.log.createChild('validation-service'),
377
+ );
298
378
  }
299
379
 
300
380
  public async start() {
@@ -353,6 +433,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
353
433
  this.handleDuplicateAttestation(info);
354
434
  });
355
435
 
436
+ this.p2pClient.registerCheckpointAttestationCallback((attestation: CheckpointAttestation) => {
437
+ this.handleCheckpointAttestation(attestation);
438
+ });
439
+
356
440
  const myAddresses = this.getValidatorAddresses();
357
441
  this.p2pClient.registerThisValidatorAddresses(myAddresses);
358
442
 
@@ -399,21 +483,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
399
483
  fishermanMode: this.config.fishermanMode || false,
400
484
  });
401
485
 
402
- // Reexecute txs if we are part of the committee, or if slashing is enabled, or if we are configured to always reexecute.
403
- // In fisherman mode, we always reexecute to validate proposals.
404
- const { slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } = this.config;
405
- const shouldReexecute =
406
- fishermanMode ||
407
- slashBroadcastedInvalidBlockPenalty > 0n ||
408
- partOfCommittee ||
409
- alwaysReexecuteBlockProposals ||
410
- this.blobClient.canUpload();
411
-
412
- const validationResult = await this.proposalHandler.handleBlockProposal(
413
- proposal,
414
- proposalSender,
415
- !!shouldReexecute && !escapeHatchOpen,
416
- );
486
+ // Reexecute outside the escape hatch so slashing observers can detect invalid proposals even when penalties are 0.
487
+ const validationResult = await this.proposalHandler.handleBlockProposal(proposal, proposalSender, !escapeHatchOpen);
417
488
 
418
489
  if (!validationResult.isValid) {
419
490
  const reason = validationResult.reason || 'unknown';
@@ -436,15 +507,18 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
436
507
  this.metrics.incFailedAttestationsNodeIssue(1, reason, partOfCommittee);
437
508
  }
438
509
 
439
- // Slash invalid block proposals (can happen even when not in committee)
440
510
  if (
441
511
  !escapeHatchOpen &&
442
512
  validationResult.reason &&
443
- SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) &&
444
- slashBroadcastedInvalidBlockPenalty > 0n
513
+ SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason)
445
514
  ) {
446
- this.log.warn(`Slashing proposer for invalid block proposal`, proposalInfo);
515
+ this.log.info(`Detected invalid block proposal offense`, {
516
+ ...proposalInfo,
517
+ amount: this.config.slashBroadcastedInvalidBlockPenalty,
518
+ offenseType: getOffenseTypeName(OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL),
519
+ });
447
520
  this.slashInvalidBlock(proposal);
521
+ this.markInvalidProposalSlot(proposal.slotNumber);
448
522
  }
449
523
  return false;
450
524
  }
@@ -483,6 +557,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
483
557
  return undefined;
484
558
  }
485
559
 
560
+ // Early-out for equivocation: refuses if we've already attested to a higher slot.
561
+ if (!this.shouldAttestToSlot(proposalSlotNumber)) {
562
+ return undefined;
563
+ }
564
+
486
565
  // Ignore proposals from ourselves (may happen in HA setups)
487
566
  if (proposer && this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
488
567
  this.log.debug(`Ignoring block proposal from self for slot ${proposalSlotNumber}`, {
@@ -624,7 +703,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
624
703
  */
625
704
  protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
626
705
  try {
627
- const lastBlockHeader = await this.blockSource.getBlockHeaderByArchive(proposal.archive);
706
+ const lastBlockHeader = (await this.blockSource.getBlockData({ archive: proposal.archive }))?.header;
628
707
  if (!lastBlockHeader) {
629
708
  this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
630
709
  return;
@@ -657,12 +736,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
657
736
  return;
658
737
  }
659
738
 
660
- // Trim the set if it's too big.
661
- if (this.proposersOfInvalidBlocks.size > MAX_PROPOSERS_OF_INVALID_BLOCKS) {
662
- // remove oldest proposer. `values` is guaranteed to be in insertion order.
663
- this.proposersOfInvalidBlocks.delete(this.proposersOfInvalidBlocks.values().next().value!);
664
- }
665
-
666
739
  this.proposersOfInvalidBlocks.add(proposer.toString());
667
740
 
668
741
  this.emit(WANT_TO_SLASH_EVENT, [
@@ -675,20 +748,115 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
675
748
  ]);
676
749
  }
677
750
 
751
+ private handleInvalidCheckpointProposal(
752
+ proposal: CheckpointProposalCore,
753
+ result: CheckpointProposalValidationFailureResult,
754
+ proposalInfo: LogData,
755
+ ): void {
756
+ if (!SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT[result.reason]) {
757
+ return;
758
+ }
759
+
760
+ this.markInvalidProposalSlot(proposal.slotNumber);
761
+
762
+ if (this.slashInvalidCheckpointProposal(proposal)) {
763
+ this.log.info(`Detected invalid checkpoint proposal offense`, {
764
+ ...proposalInfo,
765
+ reason: result.reason,
766
+ amount: this.config.slashBroadcastedInvalidCheckpointProposalPenalty,
767
+ offenseType: getOffenseTypeName(OffenseType.BROADCASTED_INVALID_CHECKPOINT_PROPOSAL),
768
+ });
769
+ }
770
+ }
771
+
772
+ private slashInvalidCheckpointProposal(proposal: CheckpointProposalCore): boolean {
773
+ const proposer = proposal.getSender();
774
+ if (!proposer) {
775
+ this.log.warn(`Cannot slash checkpoint proposal with invalid signature`, {
776
+ slotNumber: proposal.slotNumber,
777
+ archive: proposal.archive.toString(),
778
+ });
779
+ return false;
780
+ }
781
+
782
+ const offenseType = OffenseType.BROADCASTED_INVALID_CHECKPOINT_PROPOSAL;
783
+ const offenseKey = `${proposer.toString()}:${offenseType}:${proposal.slotNumber}`;
784
+ if (!this.invalidCheckpointProposalOffenseKeys.addIfAbsent(offenseKey)) {
785
+ return false;
786
+ }
787
+
788
+ this.emit(WANT_TO_SLASH_EVENT, [
789
+ {
790
+ validator: proposer,
791
+ amount: this.config.slashBroadcastedInvalidCheckpointProposalPenalty,
792
+ offenseType,
793
+ epochOrSlot: BigInt(proposal.slotNumber),
794
+ },
795
+ ]);
796
+ return true;
797
+ }
798
+
799
+ private markInvalidProposalSlot(slotNumber: SlotNumber): void {
800
+ this.slotsWithInvalidProposals.add(slotNumber);
801
+ }
802
+
803
+ private handleCheckpointAttestation(attestation: CheckpointAttestation): void {
804
+ const slotNumber = attestation.slotNumber;
805
+ if (!this.slotsWithInvalidProposals.has(slotNumber) || this.slotsWithProposalEquivocation.has(slotNumber)) {
806
+ return;
807
+ }
808
+
809
+ const attester = attestation.getSender();
810
+ if (!attester) {
811
+ this.log.warn(`Cannot slash checkpoint attestation with invalid signature`, {
812
+ slotNumber,
813
+ archive: attestation.archive.toString(),
814
+ });
815
+ return;
816
+ }
817
+
818
+ this.slashAttestedToInvalidCheckpointProposal(slotNumber, attester);
819
+ }
820
+
821
+ private slashAttestedToInvalidCheckpointProposal(slotNumber: SlotNumber, attester: EthAddress): void {
822
+ const offenseKey = `${attester.toString()}:${OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL}:${slotNumber}`;
823
+ if (!this.badAttestationOffenseKeys.addIfAbsent(offenseKey)) {
824
+ return;
825
+ }
826
+
827
+ this.log.info(`Detected attestation to invalid checkpoint proposal offense`, {
828
+ attester: attester.toString(),
829
+ slotNumber,
830
+ amount: this.config.slashAttestInvalidCheckpointProposalPenalty,
831
+ offenseType: getOffenseTypeName(OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL),
832
+ });
833
+
834
+ this.emit(WANT_TO_SLASH_EVENT, [
835
+ {
836
+ validator: attester,
837
+ amount: this.config.slashAttestInvalidCheckpointProposalPenalty,
838
+ offenseType: OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL,
839
+ epochOrSlot: BigInt(slotNumber),
840
+ },
841
+ ]);
842
+ }
843
+
678
844
  /**
679
845
  * Handle detection of a duplicate proposal (equivocation).
680
846
  * Emits a slash event when a proposer sends multiple proposals for the same position.
681
847
  */
682
848
  private handleDuplicateProposal(info: DuplicateProposalInfo): void {
683
849
  const { slot, proposer, type } = info;
850
+ this.slotsWithProposalEquivocation.add(slot);
684
851
 
685
- this.log.warn(`Triggering slash event for duplicate ${type} proposal from ${proposer.toString()} at slot ${slot}`, {
852
+ this.log.info(`Detected duplicate ${type} proposal offense from ${proposer.toString()} at slot ${slot}`, {
686
853
  proposer: proposer.toString(),
687
854
  slot,
688
855
  type,
856
+ amount: this.config.slashDuplicateProposalPenalty,
857
+ offenseType: getOffenseTypeName(OffenseType.DUPLICATE_PROPOSAL),
689
858
  });
690
859
 
691
- // Emit slash event
692
860
  this.emit(WANT_TO_SLASH_EVENT, [
693
861
  {
694
862
  validator: proposer,
@@ -697,6 +865,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
697
865
  epochOrSlot: BigInt(slot),
698
866
  },
699
867
  ]);
868
+
869
+ this.emit(WANT_TO_CLEAR_SLASH_EVENT, [
870
+ {
871
+ offenseType: OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL,
872
+ epochOrSlot: BigInt(slot),
873
+ },
874
+ ]);
700
875
  }
701
876
 
702
877
  /**
@@ -706,9 +881,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
706
881
  private handleDuplicateAttestation(info: DuplicateAttestationInfo): void {
707
882
  const { slot, attester } = info;
708
883
 
709
- this.log.warn(`Triggering slash event for duplicate attestation from ${attester.toString()} at slot ${slot}`, {
884
+ this.log.info(`Detected duplicate attestation offense from ${attester.toString()} at slot ${slot}`, {
710
885
  attester: attester.toString(),
711
886
  slot,
887
+ amount: this.config.slashDuplicateAttestationPenalty,
888
+ offenseType: getOffenseTypeName(OffenseType.DUPLICATE_ATTESTATION),
712
889
  });
713
890
 
714
891
  this.emit(WANT_TO_SLASH_EVENT, [
@@ -758,7 +935,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
758
935
  proposerAddress,
759
936
  {
760
937
  ...options,
761
- broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
938
+ broadcastInvalidBlockProposal:
939
+ options.broadcastInvalidBlockProposal || this.config.broadcastInvalidBlockProposal,
762
940
  },
763
941
  );
764
942
  this.lastProposedBlock = newProposal;
@@ -798,6 +976,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
798
976
  options,
799
977
  );
800
978
  this.lastProposedCheckpoint = newProposal;
979
+ // Self-record this slot's outcome on the re-execution tracker. Proposers don't run their
980
+ // own proposals through `handleCheckpointProposal`, so without this call the proposer's
981
+ // sentinel would see no outcome for slots it proposed and would mis-attribute itself as
982
+ // inactive. We pass the locally-computed `archive` (not `newProposal.archive`, which may
983
+ // be intentionally corrupted under test-only flags); from the proposer's local-view
984
+ // perspective the work it just completed is valid by definition.
985
+ this.proposalHandler.recordOwnCheckpointProposalAsValid(checkpointHeader.slotNumber, archive, checkpointNumber);
801
986
  return newProposal;
802
987
  }
803
988
 
@@ -860,31 +1045,21 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
860
1045
 
861
1046
  await this.collectOwnAttestations(proposal, checkpointNumber);
862
1047
 
863
- const proposalId = proposal.archive.toString();
1048
+ const proposalPayloadHash = proposal.getPayloadHash();
864
1049
  const myAddresses = this.getValidatorAddresses();
865
1050
 
866
1051
  let attestations: CheckpointAttestation[] = [];
867
1052
  while (true) {
868
- // Filter out attestations with a mismatching archive. This should NOT happen since we have verified
869
- // the proposer signature (ie our own) before accepting the attestation into the pool via the p2p client.
870
- const collectedAttestations = (await this.p2pClient.getCheckpointAttestationsForSlot(slot, proposalId)).filter(
871
- attestation => {
872
- if (!attestation.archive.equals(proposal.archive)) {
873
- this.log.warn(
874
- `Received attestation for slot ${slot} with mismatched archive from ${attestation.getSender()?.toString()}`,
875
- { attestationArchive: attestation.archive.toString(), proposalArchive: proposal.archive.toString() },
876
- );
877
- return false;
878
- }
879
- return true;
880
- },
881
- );
1053
+ // The pool already filters by proposal payload hash; if any attestation slips through with a
1054
+ // mismatched payload hash, drop it defensively. Equivocations are emitted as separate slash
1055
+ // events from libp2p_service.
1056
+ const collectedAttestations = await this.p2pClient.getCheckpointAttestationsForSlot(slot, proposalPayloadHash);
882
1057
 
883
1058
  // Log new attestations we collected
884
1059
  const oldSenders = attestations.map(attestation => attestation.getSender());
885
1060
  for (const collected of collectedAttestations) {
886
1061
  const collectedSender = collected.getSender();
887
- // Skip attestations with invalid signatures
1062
+ // Skip attestations with invalid signatures. Should not happen as we don't add invalid attestations to our pool.
888
1063
  if (!collectedSender) {
889
1064
  this.log.warn(`Skipping attestation with invalid signature for slot ${slot}`);
890
1065
  continue;