@aztec/validator-client 5.0.0-rc.1 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/validator.ts CHANGED
@@ -4,14 +4,14 @@ import type { EpochCache } from '@aztec/epoch-cache';
4
4
  import { CheckpointNumber, EpochNumber, IndexWithinCheckpoint, SlotNumber } from '@aztec/foundation/branded-types';
5
5
  import { Fr } from '@aztec/foundation/curves/bn254';
6
6
  import type { EthAddress } from '@aztec/foundation/eth-address';
7
- import type { Signature } from '@aztec/foundation/eth-signature';
7
+ import { Signature } from '@aztec/foundation/eth-signature';
8
8
  import { FifoSet } from '@aztec/foundation/fifo-set';
9
9
  import { type LogData, type Logger, createLogger } from '@aztec/foundation/log';
10
10
  import { RunningPromise } from '@aztec/foundation/running-promise';
11
11
  import { sleep } from '@aztec/foundation/sleep';
12
12
  import { DateProvider } from '@aztec/foundation/timer';
13
13
  import type { KeystoreManager } from '@aztec/node-keystore';
14
- import type { DuplicateAttestationInfo, DuplicateProposalInfo, P2P, PeerId } from '@aztec/p2p';
14
+ import type { DuplicateAttestationInfo, DuplicateProposalInfo, OversizedProposalInfo, P2P, PeerId } from '@aztec/p2p';
15
15
  import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
16
16
  import {
17
17
  OffenseType,
@@ -35,7 +35,7 @@ import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
35
35
  import {
36
36
  type BlockProposal,
37
37
  type BlockProposalOptions,
38
- type CheckpointAttestation,
38
+ CheckpointAttestation,
39
39
  CheckpointProposal,
40
40
  type CheckpointProposalCore,
41
41
  type CheckpointProposalOptions,
@@ -66,48 +66,18 @@ import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
66
66
  import { ValidatorMetrics } from './metrics.js';
67
67
  import {
68
68
  type BlockProposalValidationFailureReason,
69
- type CheckpointProposalValidationFailureReason,
70
69
  type CheckpointProposalValidationFailureResult,
71
70
  ProposalHandler,
71
+ SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT,
72
+ SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT,
72
73
  } from './proposal_handler.js';
73
74
 
74
75
  // We maintain a set of proposers who have proposed invalid blocks.
75
76
  // Just cap the set to avoid unbounded growth.
76
77
  const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
77
- const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
78
78
  const MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS = 1000;
79
79
  const MAX_TRACKED_BAD_ATTESTATIONS = 10_000;
80
80
 
81
- // What errors from the block proposal handler result in slashing
82
- const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT: BlockProposalValidationFailureReason[] = [
83
- 'state_mismatch',
84
- 'failed_txs',
85
- 'global_variables_mismatch',
86
- 'invalid_proposal',
87
- 'parent_block_wrong_slot',
88
- 'in_hash_mismatch',
89
- ];
90
-
91
- const SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT: Record<CheckpointProposalValidationFailureReason, boolean> = {
92
- // enabled
93
- ['invalid_fee_asset_price_modifier']: true,
94
- ['checkpoint_header_mismatch']: true,
95
- // These late mismatches should normally be caught by earlier checks, but if reached after validating the local
96
- // checkpoint inputs, the proposer-signed payload disagrees with deterministic recomputation.
97
- ['archive_mismatch']: true,
98
- ['out_hash_mismatch']: true,
99
- ['no_blocks_for_slot']: true,
100
- ['too_many_blocks_in_checkpoint']: true,
101
- ['checkpoint_validation_failed']: true,
102
- ['last_block_archive_mismatch']: true,
103
-
104
- // disabled
105
- ['invalid_signature']: false,
106
- ['last_block_not_found']: false,
107
- ['block_fetch_error']: false,
108
- ['checkpoint_already_published']: false,
109
- };
110
-
111
81
  /**
112
82
  * Validator Client
113
83
  */
@@ -131,10 +101,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
131
101
  private lastAttestedEpochByAttester: Map<string, EpochNumber> = new Map();
132
102
 
133
103
  private proposersOfInvalidBlocks = FifoSet.withLimit<string>(MAX_PROPOSERS_OF_INVALID_BLOCKS);
134
- private slotsWithInvalidProposals = FifoSet.withLimit<SlotNumber>(MAX_TRACKED_INVALID_PROPOSAL_SLOTS);
135
104
  private invalidCheckpointProposalOffenseKeys = FifoSet.withLimit<string>(MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS);
105
+ private oversizedProposalOffenseKeys = FifoSet.withLimit<string>(MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS);
136
106
  private badAttestationOffenseKeys = FifoSet.withLimit<string>(MAX_TRACKED_BAD_ATTESTATIONS);
137
- private slotsWithProposalEquivocation = FifoSet.withLimit<SlotNumber>(MAX_TRACKED_INVALID_PROPOSAL_SLOTS);
138
107
 
139
108
  /** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
140
109
  private lastAttestedProposal?: CheckpointProposalCore;
@@ -363,11 +332,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
363
332
  }
364
333
 
365
334
  public hasProposalEquivocation(slotNumber: SlotNumber): boolean {
366
- return this.slotsWithProposalEquivocation.has(slotNumber);
335
+ return this.proposalHandler.hasProposalEquivocation(slotNumber);
367
336
  }
368
337
 
369
338
  public hasInvalidProposals(slotNumber: SlotNumber): boolean {
370
- return this.slotsWithInvalidProposals.has(slotNumber);
339
+ return this.proposalHandler.hasInvalidProposals(slotNumber);
371
340
  }
372
341
 
373
342
  public updateConfig(config: Partial<ValidatorClientFullConfig>) {
@@ -436,6 +405,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
436
405
  this.handleDuplicateProposal(info);
437
406
  });
438
407
 
408
+ // Oversized proposal handler - triggers slashing for proposals beyond the per-checkpoint block limit
409
+ this.p2pClient.registerOversizedProposalCallback((info: OversizedProposalInfo) => {
410
+ this.handleOversizedProposal(info);
411
+ });
412
+
439
413
  // Duplicate attestation handler - triggers slashing for attestation equivocation
440
414
  this.p2pClient.registerDuplicateAttestationCallback((info: DuplicateAttestationInfo) => {
441
415
  this.handleDuplicateAttestation(info);
@@ -765,8 +739,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
765
739
  return;
766
740
  }
767
741
 
768
- this.markInvalidProposalSlot(proposal.slotNumber);
769
-
742
+ // The slot is already marked invalid by the all-nodes checkpoint handler that invokes this callback,
743
+ // so we only emit the proposer slash event here.
770
744
  if (this.slashInvalidCheckpointProposal(proposal)) {
771
745
  this.log.info(`Detected invalid checkpoint proposal offense`, {
772
746
  ...proposalInfo,
@@ -805,12 +779,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
805
779
  }
806
780
 
807
781
  private markInvalidProposalSlot(slotNumber: SlotNumber): void {
808
- this.slotsWithInvalidProposals.add(slotNumber);
782
+ this.proposalHandler.markInvalidProposalSlot(slotNumber);
809
783
  }
810
784
 
811
785
  private handleCheckpointAttestation(attestation: CheckpointAttestation): void {
812
786
  const slotNumber = attestation.slotNumber;
813
- if (!this.slotsWithInvalidProposals.has(slotNumber) || this.slotsWithProposalEquivocation.has(slotNumber)) {
787
+ if (
788
+ !this.proposalHandler.hasInvalidProposals(slotNumber) ||
789
+ this.proposalHandler.hasProposalEquivocation(slotNumber)
790
+ ) {
814
791
  return;
815
792
  }
816
793
 
@@ -849,13 +826,43 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
849
826
  ]);
850
827
  }
851
828
 
829
+ /**
830
+ * Handle detection of an oversized block proposal: one whose index within its checkpoint lands at or
831
+ * beyond the consensus per-checkpoint block limit. A single signed proposal at an illegal index is
832
+ * self-contained evidence, so emit an invalid-block-proposal slash event for the proposer, deduped per
833
+ * (proposer, slot) since the p2p layer reports every oversized proposal it stores.
834
+ */
835
+ private handleOversizedProposal(info: OversizedProposalInfo): void {
836
+ const { slot, proposer } = info;
837
+ const offenseType = OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL;
838
+ if (!this.oversizedProposalOffenseKeys.addIfAbsent(`${proposer.toString()}:${offenseType}:${slot}`)) {
839
+ return;
840
+ }
841
+
842
+ this.log.info(`Detected oversized block proposal offense from ${proposer.toString()} at slot ${slot}`, {
843
+ proposer: proposer.toString(),
844
+ slot,
845
+ amount: this.config.slashBroadcastedInvalidBlockPenalty,
846
+ offenseType: getOffenseTypeName(offenseType),
847
+ });
848
+
849
+ this.emit(WANT_TO_SLASH_EVENT, [
850
+ {
851
+ validator: proposer,
852
+ amount: this.config.slashBroadcastedInvalidBlockPenalty,
853
+ offenseType,
854
+ epochOrSlot: BigInt(slot),
855
+ },
856
+ ]);
857
+ }
858
+
852
859
  /**
853
860
  * Handle detection of a duplicate proposal (equivocation).
854
861
  * Emits a slash event when a proposer sends multiple proposals for the same position.
855
862
  */
856
863
  private handleDuplicateProposal(info: DuplicateProposalInfo): void {
857
864
  const { slot, proposer, type } = info;
858
- this.slotsWithProposalEquivocation.add(slot);
865
+ this.proposalHandler.markProposalEquivocation(slot);
859
866
 
860
867
  this.log.info(`Detected duplicate ${type} proposal offense from ${proposer.toString()} at slot ${slot}`, {
861
868
  proposer: proposer.toString(),