@aztec/validator-client 6.0.0-nightly.20260604 → 6.0.0-nightly.20260721

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,13 +35,14 @@ 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,
42
42
  type CoordinationSignatureContext,
43
43
  } from '@aztec/stdlib/p2p';
44
44
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
45
+ import { ConsensusTimetable } from '@aztec/stdlib/timetable';
45
46
  import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
46
47
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
47
48
  import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
@@ -57,6 +58,7 @@ import { EventEmitter } from 'events';
57
58
  import type { TypedDataDefinition } from 'viem';
58
59
 
59
60
  import type { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
61
+ import { DEFAULT_MAX_GOSSIP_CLOCK_DISPARITY_MS } from './config.js';
60
62
  import { ValidationService } from './duties/validation_service.js';
61
63
  import { HAKeyStore } from './key_store/ha_key_store.js';
62
64
  import type { ExtendedValidatorKeyStore } from './key_store/interface.js';
@@ -64,48 +66,18 @@ import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
64
66
  import { ValidatorMetrics } from './metrics.js';
65
67
  import {
66
68
  type BlockProposalValidationFailureReason,
67
- type CheckpointProposalValidationFailureReason,
68
69
  type CheckpointProposalValidationFailureResult,
69
70
  ProposalHandler,
71
+ SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT,
72
+ SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT,
70
73
  } from './proposal_handler.js';
71
74
 
72
75
  // We maintain a set of proposers who have proposed invalid blocks.
73
76
  // Just cap the set to avoid unbounded growth.
74
77
  const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
75
- const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
76
78
  const MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS = 1000;
77
79
  const MAX_TRACKED_BAD_ATTESTATIONS = 10_000;
78
80
 
79
- // What errors from the block proposal handler result in slashing
80
- const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT: BlockProposalValidationFailureReason[] = [
81
- 'state_mismatch',
82
- 'failed_txs',
83
- 'global_variables_mismatch',
84
- 'invalid_proposal',
85
- 'parent_block_wrong_slot',
86
- 'in_hash_mismatch',
87
- ];
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
-
109
81
  /**
110
82
  * Validator Client
111
83
  */
@@ -129,10 +101,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
129
101
  private lastAttestedEpochByAttester: Map<string, EpochNumber> = new Map();
130
102
 
131
103
  private proposersOfInvalidBlocks = FifoSet.withLimit<string>(MAX_PROPOSERS_OF_INVALID_BLOCKS);
132
- private slotsWithInvalidProposals = FifoSet.withLimit<SlotNumber>(MAX_TRACKED_INVALID_PROPOSAL_SLOTS);
133
104
  private invalidCheckpointProposalOffenseKeys = FifoSet.withLimit<string>(MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS);
105
+ private oversizedProposalOffenseKeys = FifoSet.withLimit<string>(MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS);
134
106
  private badAttestationOffenseKeys = FifoSet.withLimit<string>(MAX_TRACKED_BAD_ATTESTATIONS);
135
- private slotsWithProposalEquivocation = FifoSet.withLimit<SlotNumber>(MAX_TRACKED_INVALID_PROPOSAL_SLOTS);
136
107
 
137
108
  /** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
138
109
  private lastAttestedProposal?: CheckpointProposalCore;
@@ -246,7 +217,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
246
217
  slashingProtectionDb?: SlashingProtectionDatabase,
247
218
  ) {
248
219
  const metrics = new ValidatorMetrics(telemetry);
249
- const blockProposalValidator = new BlockProposalValidator(epochCache, {
220
+ const consensusTimetable = new ConsensusTimetable({
221
+ l1Constants: epochCache.getL1Constants(),
222
+ blockDuration: config.blockDurationMs / 1000,
223
+ });
224
+ const blockProposalValidator = new BlockProposalValidator(epochCache, consensusTimetable, {
250
225
  txsPermitted: !config.disableTransactions,
251
226
  maxTxsPerBlock: config.validateMaxTxsPerBlock,
252
227
  maxBlocksPerCheckpoint: config.maxBlocksPerCheckpoint,
@@ -255,6 +230,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
255
230
  chainId: config.l1ChainId,
256
231
  rollupAddress: config.rollupAddress,
257
232
  },
233
+ clockDisparityMs: config.maxGossipClockDisparityMs ?? DEFAULT_MAX_GOSSIP_CLOCK_DISPARITY_MS,
258
234
  });
259
235
  const proposalHandler = new ProposalHandler(
260
236
  checkpointsBuilder,
@@ -264,6 +240,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
264
240
  txProvider,
265
241
  blockProposalValidator,
266
242
  epochCache,
243
+ consensusTimetable,
267
244
  config,
268
245
  blobClient,
269
246
  reexecutionTracker,
@@ -355,11 +332,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
355
332
  }
356
333
 
357
334
  public hasProposalEquivocation(slotNumber: SlotNumber): boolean {
358
- return this.slotsWithProposalEquivocation.has(slotNumber);
335
+ return this.proposalHandler.hasProposalEquivocation(slotNumber);
359
336
  }
360
337
 
361
338
  public hasInvalidProposals(slotNumber: SlotNumber): boolean {
362
- return this.slotsWithInvalidProposals.has(slotNumber);
339
+ return this.proposalHandler.hasInvalidProposals(slotNumber);
363
340
  }
364
341
 
365
342
  public updateConfig(config: Partial<ValidatorClientFullConfig>) {
@@ -428,6 +405,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
428
405
  this.handleDuplicateProposal(info);
429
406
  });
430
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
+
431
413
  // Duplicate attestation handler - triggers slashing for attestation equivocation
432
414
  this.p2pClient.registerDuplicateAttestationCallback((info: DuplicateAttestationInfo) => {
433
415
  this.handleDuplicateAttestation(info);
@@ -564,7 +546,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
564
546
 
565
547
  // Ignore proposals from ourselves (may happen in HA setups)
566
548
  if (proposer && this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
567
- this.log.debug(`Ignoring block proposal from self for slot ${proposalSlotNumber}`, {
549
+ this.log.debug(`Not attesting to block proposal from self for slot ${proposalSlotNumber}`, {
568
550
  proposer: proposer.toString(),
569
551
  proposalSlotNumber,
570
552
  });
@@ -757,8 +739,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
757
739
  return;
758
740
  }
759
741
 
760
- this.markInvalidProposalSlot(proposal.slotNumber);
761
-
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.
762
744
  if (this.slashInvalidCheckpointProposal(proposal)) {
763
745
  this.log.info(`Detected invalid checkpoint proposal offense`, {
764
746
  ...proposalInfo,
@@ -797,12 +779,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
797
779
  }
798
780
 
799
781
  private markInvalidProposalSlot(slotNumber: SlotNumber): void {
800
- this.slotsWithInvalidProposals.add(slotNumber);
782
+ this.proposalHandler.markInvalidProposalSlot(slotNumber);
801
783
  }
802
784
 
803
785
  private handleCheckpointAttestation(attestation: CheckpointAttestation): void {
804
786
  const slotNumber = attestation.slotNumber;
805
- if (!this.slotsWithInvalidProposals.has(slotNumber) || this.slotsWithProposalEquivocation.has(slotNumber)) {
787
+ if (
788
+ !this.proposalHandler.hasInvalidProposals(slotNumber) ||
789
+ this.proposalHandler.hasProposalEquivocation(slotNumber)
790
+ ) {
806
791
  return;
807
792
  }
808
793
 
@@ -841,13 +826,43 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
841
826
  ]);
842
827
  }
843
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
+
844
859
  /**
845
860
  * Handle detection of a duplicate proposal (equivocation).
846
861
  * Emits a slash event when a proposer sends multiple proposals for the same position.
847
862
  */
848
863
  private handleDuplicateProposal(info: DuplicateProposalInfo): void {
849
864
  const { slot, proposer, type } = info;
850
- this.slotsWithProposalEquivocation.add(slot);
865
+ this.proposalHandler.markProposalEquivocation(slot);
851
866
 
852
867
  this.log.info(`Detected duplicate ${type} proposal offense from ${proposer.toString()} at slot ${slot}`, {
853
868
  proposer: proposer.toString(),