@aztec/validator-client 0.0.1-commit.993d240 → 0.0.1-commit.9a89641

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);
@@ -498,6 +480,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
498
480
  'failed_txs',
499
481
  'in_hash_mismatch',
500
482
  'parent_block_wrong_slot',
483
+ 'duplicate_txs',
484
+ 'invalid_embedded_txs',
501
485
  ];
502
486
 
503
487
  if (badProposalReasons.includes(reason as BlockProposalValidationFailureReason)) {
@@ -564,7 +548,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
564
548
 
565
549
  // Ignore proposals from ourselves (may happen in HA setups)
566
550
  if (proposer && this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
567
- this.log.debug(`Ignoring block proposal from self for slot ${proposalSlotNumber}`, {
551
+ this.log.debug(`Not attesting to block proposal from self for slot ${proposalSlotNumber}`, {
568
552
  proposer: proposer.toString(),
569
553
  proposalSlotNumber,
570
554
  });
@@ -757,8 +741,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
757
741
  return;
758
742
  }
759
743
 
760
- this.markInvalidProposalSlot(proposal.slotNumber);
761
-
744
+ // The slot is already marked invalid by the all-nodes checkpoint handler that invokes this callback,
745
+ // so we only emit the proposer slash event here.
762
746
  if (this.slashInvalidCheckpointProposal(proposal)) {
763
747
  this.log.info(`Detected invalid checkpoint proposal offense`, {
764
748
  ...proposalInfo,
@@ -797,12 +781,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
797
781
  }
798
782
 
799
783
  private markInvalidProposalSlot(slotNumber: SlotNumber): void {
800
- this.slotsWithInvalidProposals.add(slotNumber);
784
+ this.proposalHandler.markInvalidProposalSlot(slotNumber);
801
785
  }
802
786
 
803
787
  private handleCheckpointAttestation(attestation: CheckpointAttestation): void {
804
788
  const slotNumber = attestation.slotNumber;
805
- if (!this.slotsWithInvalidProposals.has(slotNumber) || this.slotsWithProposalEquivocation.has(slotNumber)) {
789
+ if (
790
+ !this.proposalHandler.hasInvalidProposals(slotNumber) ||
791
+ this.proposalHandler.hasProposalEquivocation(slotNumber)
792
+ ) {
806
793
  return;
807
794
  }
808
795
 
@@ -841,13 +828,43 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
841
828
  ]);
842
829
  }
843
830
 
831
+ /**
832
+ * Handle detection of an oversized block proposal: one whose index within its checkpoint lands at or
833
+ * beyond the consensus per-checkpoint block limit. A single signed proposal at an illegal index is
834
+ * self-contained evidence, so emit an invalid-block-proposal slash event for the proposer, deduped per
835
+ * (proposer, slot) since the p2p layer reports every oversized proposal it stores.
836
+ */
837
+ private handleOversizedProposal(info: OversizedProposalInfo): void {
838
+ const { slot, proposer } = info;
839
+ const offenseType = OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL;
840
+ if (!this.oversizedProposalOffenseKeys.addIfAbsent(`${proposer.toString()}:${offenseType}:${slot}`)) {
841
+ return;
842
+ }
843
+
844
+ this.log.info(`Detected oversized block proposal offense from ${proposer.toString()} at slot ${slot}`, {
845
+ proposer: proposer.toString(),
846
+ slot,
847
+ amount: this.config.slashBroadcastedInvalidBlockPenalty,
848
+ offenseType: getOffenseTypeName(offenseType),
849
+ });
850
+
851
+ this.emit(WANT_TO_SLASH_EVENT, [
852
+ {
853
+ validator: proposer,
854
+ amount: this.config.slashBroadcastedInvalidBlockPenalty,
855
+ offenseType,
856
+ epochOrSlot: BigInt(slot),
857
+ },
858
+ ]);
859
+ }
860
+
844
861
  /**
845
862
  * Handle detection of a duplicate proposal (equivocation).
846
863
  * Emits a slash event when a proposer sends multiple proposals for the same position.
847
864
  */
848
865
  private handleDuplicateProposal(info: DuplicateProposalInfo): void {
849
866
  const { slot, proposer, type } = info;
850
- this.slotsWithProposalEquivocation.add(slot);
867
+ this.proposalHandler.markProposalEquivocation(slot);
851
868
 
852
869
  this.log.info(`Detected duplicate ${type} proposal offense from ${proposer.toString()} at slot ${slot}`, {
853
870
  proposer: proposer.toString(),