@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/dest/proposal_handler.d.ts +24 -6
- package/dest/proposal_handler.d.ts.map +1 -1
- package/dest/proposal_handler.js +182 -38
- package/dest/streaming_inbox_checks.d.ts +1 -1
- package/dest/streaming_inbox_checks.d.ts.map +1 -1
- package/dest/streaming_inbox_checks.js +13 -7
- package/dest/validator.d.ts +5 -1
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +59 -25
- package/package.json +19 -19
- package/src/proposal_handler.ts +236 -42
- package/src/streaming_inbox_checks.ts +15 -6
- package/src/validator.ts +68 -28
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
|
-
//
|
|
479
|
-
|
|
480
|
-
|
|
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
|
|
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
|
-
|
|
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
|
*/
|