@aztec-labs/validator-client 6.0.0-rc.1 → 6.0.0-rc.2

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
@@ -2,6 +2,7 @@ import type { BlobClientInterface } from '@aztec-labs/blob-client/client';
2
2
  import { type Blob, getBlobsPerL1Block } from '@aztec-labs/blob-lib';
3
3
  import type { EpochCache } from '@aztec-labs/epoch-cache';
4
4
  import { CheckpointNumber, EpochNumber, IndexWithinCheckpoint, SlotNumber } from '@aztec-labs/foundation/branded-types';
5
+ import { compactArray } from '@aztec-labs/foundation/collection';
5
6
  import { Fr } from '@aztec-labs/foundation/curves/bn254';
6
7
  import type { EthAddress } from '@aztec-labs/foundation/eth-address';
7
8
  import { Signature } from '@aztec-labs/foundation/eth-signature';
@@ -475,27 +476,18 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
475
476
 
476
477
  this.log.warn(`Block proposal validation failed: ${reason}`, proposalInfo);
477
478
 
478
- // Classify failure reason: bad proposal vs node issue
479
- const badProposalReasons: BlockProposalValidationFailureReason[] = [
480
- 'invalid_proposal',
481
- 'state_mismatch',
482
- 'failed_txs',
483
- 'parent_block_wrong_slot',
484
- 'duplicate_txs',
485
- 'invalid_embedded_txs',
486
- ];
487
-
488
- if (badProposalReasons.includes(reason as BlockProposalValidationFailureReason)) {
479
+ // A slashable reject is a bad proposal, not a node issue, so branch the metric on the slashable set. A separate
480
+ // reason list would need every slashable reason (e.g. global_variables_mismatch) kept in sync by hand.
481
+ if (SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT[reason as BlockProposalValidationFailureReason]) {
489
482
  this.metrics.incFailedAttestationsBadProposal(1, reason, partOfCommittee);
490
483
  } else {
491
- // Node issues so we can't validate
492
484
  this.metrics.incFailedAttestationsNodeIssue(1, reason, partOfCommittee);
493
485
  }
494
486
 
495
487
  if (
496
488
  !escapeHatchOpen &&
497
489
  validationResult.reason &&
498
- SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason)
490
+ SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT[validationResult.reason]
499
491
  ) {
500
492
  this.log.info(`Detected invalid block proposal offense`, {
501
493
  ...proposalInfo,
@@ -609,19 +601,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
609
601
  },
610
602
  );
611
603
 
612
- this.metrics.incSuccessfulAttestations(inCommittee.length);
613
-
614
- // Track epoch participation per attester: count each (attester, epoch) pair at most once
615
- const proposalEpoch = getEpochAtSlot(proposalSlotNumber, this.epochCache.getL1Constants());
616
- for (const attester of inCommittee) {
617
- const key = attester.toString();
618
- const lastEpoch = this.lastAttestedEpochByAttester.get(key);
619
- if (lastEpoch === undefined || proposalEpoch > lastEpoch) {
620
- this.lastAttestedEpochByAttester.set(key, proposalEpoch);
621
- this.metrics.incAttestedEpochCount(attester);
622
- }
623
- }
624
-
625
604
  // Determine which validators should attest
626
605
  let attestors: EthAddress[];
627
606
  if (partOfCommittee) {
@@ -644,10 +623,34 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
644
623
  ...proposalInfo,
645
624
  attestors: attestors.map(a => a.toString()),
646
625
  });
626
+ this.recordAttestationParticipation(inCommittee, proposalSlotNumber);
647
627
  return undefined;
648
628
  }
649
629
 
650
- return await this.createCheckpointAttestationsFromProposal(proposal, attestors, checkpointNumber);
630
+ const attestations = await this.createCheckpointAttestationsFromProposal(proposal, attestors, checkpointNumber);
631
+ // Counted from the attestations actually kept, so one refused for equivocation, discarded past the deadline, or
632
+ // signed by another HA node is not reported as this node's participation.
633
+ if (attestations) {
634
+ this.recordAttestationParticipation(
635
+ compactArray(attestations.map(attestation => attestation.getSender())),
636
+ proposalSlotNumber,
637
+ );
638
+ }
639
+ return attestations;
640
+ }
641
+
642
+ /** Records successful attestations and each attester's epoch participation, counting an (attester, epoch) once. */
643
+ private recordAttestationParticipation(attesters: EthAddress[], slot: SlotNumber): void {
644
+ this.metrics.incSuccessfulAttestations(attesters.length);
645
+ const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
646
+ for (const attester of attesters) {
647
+ const key = attester.toString();
648
+ const lastEpoch = this.lastAttestedEpochByAttester.get(key);
649
+ if (lastEpoch === undefined || epoch > lastEpoch) {
650
+ this.lastAttestedEpochByAttester.set(key, epoch);
651
+ this.metrics.incAttestedEpochCount(attester);
652
+ }
653
+ }
651
654
  }
652
655
 
653
656
  /**
@@ -681,15 +684,52 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
681
684
  return undefined;
682
685
  }
683
686
 
687
+ // Deadline check, for the same reason and in the same place. Validation can run up to the attestation deadline
688
+ // and past it — the archiver sync waits are bounded by the deadline, and the Inbox endpoint gate keeps a minimum
689
+ // window beyond it on purpose — so it is re-read here, with nothing left between it and the signature. Finishing
690
+ // a late validation is still worth doing for telemetry; signing on it is not. Both the peer path and the
691
+ // proposer's own `collectOwnAttestations` sign through here, so this is the one place that covers both.
692
+ //
693
+ // The cutoff is the deadline itself, not the deadline widened by `maxGossipClockDisparityMs`. The deadline is the
694
+ // sender's obligation; that tolerance is the receiver's allowance for clock skew and propagation delay on
695
+ // messages sent in time, and a sender that spends it leaves none for the network. Signing late for the sake of
696
+ // inactivity-slashing credit is a poor trade too: a late attestation only counts if it still reaches peers inside
697
+ // their tolerance, and one further out is dropped, or penalized as stale, by every peer.
698
+ if (this.isPastAttestationDeadline(proposal, 'validation')) {
699
+ return undefined;
700
+ }
701
+
684
702
  const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors, checkpointNumber);
685
703
 
686
- // Track the proposal we attested to (to prevent equivocation)
704
+ // Track the proposal we attested to (to prevent equivocation). This is recorded even when the signature is
705
+ // discarded below: the attestors have signed for this slot, so a second proposal for it must still be refused.
687
706
  this.lastAttestedProposal = proposal;
688
707
 
708
+ // Signing is not instant: a remote signer or HA coordination can take seconds. A signature produced past the
709
+ // deadline is discarded rather than stored and gossiped, for the reasons above.
710
+ if (this.isPastAttestationDeadline(proposal, 'signing')) {
711
+ return undefined;
712
+ }
713
+
689
714
  await this.p2pClient.addOwnCheckpointAttestations(attestations);
690
715
  return attestations;
691
716
  }
692
717
 
718
+ /** Whether the attestation deadline for the proposal's slot has passed, logging which step overran it if so. */
719
+ private isPastAttestationDeadline(proposal: CheckpointProposalCore, step: 'validation' | 'signing'): boolean {
720
+ const attestationDeadline = this.proposalHandler.getAttestationDeadline(proposal.slotNumber);
721
+ if (+attestationDeadline > this.dateProvider.now()) {
722
+ return false;
723
+ }
724
+ this.log.warn(`Attestation deadline for slot ${proposal.slotNumber} passed during ${step}, not attesting`, {
725
+ slot: proposal.slotNumber,
726
+ archive: proposal.archive.toString(),
727
+ attestationDeadline: attestationDeadline.toISOString(),
728
+ step,
729
+ });
730
+ return true;
731
+ }
732
+
693
733
  /**
694
734
  * Uploads blobs for a checkpoint to the filestore (fire and forget).
695
735
  */