@aztec/validator-client 0.0.1-commit.b2a5d0dd1 → 0.0.1-commit.b3d3157a
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/README.md +10 -9
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +15 -5
- package/dest/duties/validation_service.d.ts +6 -5
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +24 -18
- package/dest/factory.d.ts +4 -1
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +8 -3
- package/dest/proposal_handler.d.ts +38 -12
- package/dest/proposal_handler.d.ts.map +1 -1
- package/dest/proposal_handler.js +232 -134
- package/dest/validator.d.ts +15 -2
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +182 -42
- package/package.json +19 -19
- package/src/config.ts +15 -4
- package/src/duties/validation_service.ts +38 -21
- package/src/factory.ts +10 -0
- package/src/proposal_handler.ts +278 -159
- package/src/validator.ts +227 -52
package/src/validator.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { CheckpointNumber, EpochNumber, IndexWithinCheckpoint, SlotNumber } from
|
|
|
5
5
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
6
6
|
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
7
7
|
import type { Signature } from '@aztec/foundation/eth-signature';
|
|
8
|
+
import { FifoSet } from '@aztec/foundation/fifo-set';
|
|
8
9
|
import { type LogData, type Logger, createLogger } from '@aztec/foundation/log';
|
|
9
10
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
10
11
|
import { sleep } from '@aztec/foundation/sleep';
|
|
@@ -12,9 +13,17 @@ import { DateProvider } from '@aztec/foundation/timer';
|
|
|
12
13
|
import type { KeystoreManager } from '@aztec/node-keystore';
|
|
13
14
|
import type { DuplicateAttestationInfo, DuplicateProposalInfo, P2P, PeerId } from '@aztec/p2p';
|
|
14
15
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
OffenseType,
|
|
18
|
+
WANT_TO_CLEAR_SLASH_EVENT,
|
|
19
|
+
WANT_TO_SLASH_EVENT,
|
|
20
|
+
type Watcher,
|
|
21
|
+
type WatcherEmitter,
|
|
22
|
+
getOffenseTypeName,
|
|
23
|
+
} from '@aztec/slasher';
|
|
16
24
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
17
25
|
import type { CommitteeAttestationsAndSigners, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
26
|
+
import type { CheckpointReexecutionTracker } from '@aztec/stdlib/checkpoint';
|
|
18
27
|
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
19
28
|
import type {
|
|
20
29
|
ITxProvider,
|
|
@@ -30,6 +39,7 @@ import {
|
|
|
30
39
|
CheckpointProposal,
|
|
31
40
|
type CheckpointProposalCore,
|
|
32
41
|
type CheckpointProposalOptions,
|
|
42
|
+
type CoordinationSignatureContext,
|
|
33
43
|
} from '@aztec/stdlib/p2p';
|
|
34
44
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
35
45
|
import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
|
|
@@ -52,18 +62,50 @@ import { HAKeyStore } from './key_store/ha_key_store.js';
|
|
|
52
62
|
import type { ExtendedValidatorKeyStore } from './key_store/interface.js';
|
|
53
63
|
import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
|
|
54
64
|
import { ValidatorMetrics } from './metrics.js';
|
|
55
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
type BlockProposalValidationFailureReason,
|
|
67
|
+
type CheckpointProposalValidationFailureReason,
|
|
68
|
+
type CheckpointProposalValidationFailureResult,
|
|
69
|
+
ProposalHandler,
|
|
70
|
+
} from './proposal_handler.js';
|
|
56
71
|
|
|
57
72
|
// We maintain a set of proposers who have proposed invalid blocks.
|
|
58
73
|
// Just cap the set to avoid unbounded growth.
|
|
59
74
|
const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
|
|
75
|
+
const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
|
|
76
|
+
const MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS = 1000;
|
|
77
|
+
const MAX_TRACKED_BAD_ATTESTATIONS = 10_000;
|
|
60
78
|
|
|
61
79
|
// What errors from the block proposal handler result in slashing
|
|
62
80
|
const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT: BlockProposalValidationFailureReason[] = [
|
|
63
81
|
'state_mismatch',
|
|
64
82
|
'failed_txs',
|
|
83
|
+
'global_variables_mismatch',
|
|
84
|
+
'invalid_proposal',
|
|
85
|
+
'parent_block_wrong_slot',
|
|
86
|
+
'in_hash_mismatch',
|
|
65
87
|
];
|
|
66
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
|
+
|
|
67
109
|
/**
|
|
68
110
|
* Validator Client
|
|
69
111
|
*/
|
|
@@ -86,7 +128,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
86
128
|
/** Tracks the last epoch in which each attester successfully submitted at least one attestation. */
|
|
87
129
|
private lastAttestedEpochByAttester: Map<string, EpochNumber> = new Map();
|
|
88
130
|
|
|
89
|
-
private proposersOfInvalidBlocks
|
|
131
|
+
private proposersOfInvalidBlocks = FifoSet.withLimit<string>(MAX_PROPOSERS_OF_INVALID_BLOCKS);
|
|
132
|
+
private slotsWithInvalidProposals = FifoSet.withLimit<SlotNumber>(MAX_TRACKED_INVALID_PROPOSAL_SLOTS);
|
|
133
|
+
private invalidCheckpointProposalOffenseKeys = FifoSet.withLimit<string>(MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS);
|
|
134
|
+
private badAttestationOffenseKeys = FifoSet.withLimit<string>(MAX_TRACKED_BAD_ATTESTATIONS);
|
|
135
|
+
private slotsWithProposalEquivocation = FifoSet.withLimit<SlotNumber>(MAX_TRACKED_INVALID_PROPOSAL_SLOTS);
|
|
90
136
|
|
|
91
137
|
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
|
|
92
138
|
private lastAttestedProposal?: CheckpointProposalCore;
|
|
@@ -115,11 +161,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
115
161
|
this.tracer = telemetry.getTracer('Validator');
|
|
116
162
|
this.metrics = new ValidatorMetrics(telemetry);
|
|
117
163
|
|
|
118
|
-
this.validationService = new ValidationService(
|
|
164
|
+
this.validationService = new ValidationService(
|
|
165
|
+
keyStore,
|
|
166
|
+
this.getSignatureContext(),
|
|
167
|
+
this.log.createChild('validation-service'),
|
|
168
|
+
);
|
|
169
|
+
this.proposalHandler.setCheckpointProposalValidationFailureCallback((proposal, result, proposalInfo) =>
|
|
170
|
+
this.handleInvalidCheckpointProposal(proposal, result, proposalInfo),
|
|
171
|
+
);
|
|
119
172
|
|
|
120
173
|
// Refresh epoch cache every second to trigger alert if participation in committee changes
|
|
121
174
|
this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), this.log, 1000);
|
|
122
|
-
|
|
123
175
|
const myAddresses = this.getValidatorAddresses();
|
|
124
176
|
this.log.verbose(`Initialized validator with addresses: ${myAddresses.map(a => a.toString()).join(', ')}`);
|
|
125
177
|
}
|
|
@@ -188,6 +240,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
188
240
|
txProvider: ITxProvider,
|
|
189
241
|
keyStoreManager: KeystoreManager,
|
|
190
242
|
blobClient: BlobClientInterface,
|
|
243
|
+
reexecutionTracker: CheckpointReexecutionTracker,
|
|
191
244
|
dateProvider: DateProvider = new DateProvider(),
|
|
192
245
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
193
246
|
slashingProtectionDb?: SlashingProtectionDatabase,
|
|
@@ -196,6 +249,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
196
249
|
const blockProposalValidator = new BlockProposalValidator(epochCache, {
|
|
197
250
|
txsPermitted: !config.disableTransactions,
|
|
198
251
|
maxTxsPerBlock: config.validateMaxTxsPerBlock,
|
|
252
|
+
maxBlocksPerCheckpoint: config.maxBlocksPerCheckpoint,
|
|
253
|
+
skipSlotValidation: config.skipProposalSlotValidation,
|
|
254
|
+
signatureContext: {
|
|
255
|
+
chainId: config.l1ChainId,
|
|
256
|
+
rollupAddress: config.rollupAddress,
|
|
257
|
+
},
|
|
199
258
|
});
|
|
200
259
|
const proposalHandler = new ProposalHandler(
|
|
201
260
|
checkpointsBuilder,
|
|
@@ -207,6 +266,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
207
266
|
epochCache,
|
|
208
267
|
config,
|
|
209
268
|
blobClient,
|
|
269
|
+
reexecutionTracker,
|
|
210
270
|
metrics,
|
|
211
271
|
dateProvider,
|
|
212
272
|
telemetry,
|
|
@@ -275,6 +335,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
275
335
|
return this.keyStore.signTypedDataWithAddress(addr, msg, context);
|
|
276
336
|
}
|
|
277
337
|
|
|
338
|
+
private getSignatureContext(): CoordinationSignatureContext {
|
|
339
|
+
return {
|
|
340
|
+
chainId: this.config.l1ChainId,
|
|
341
|
+
rollupAddress: this.config.rollupAddress,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
278
345
|
public getCoinbaseForAttestor(attestor: EthAddress): EthAddress {
|
|
279
346
|
return this.keyStore.getCoinbaseAddress(attestor);
|
|
280
347
|
}
|
|
@@ -287,14 +354,27 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
287
354
|
return this.config;
|
|
288
355
|
}
|
|
289
356
|
|
|
357
|
+
public hasProposalEquivocation(slotNumber: SlotNumber): boolean {
|
|
358
|
+
return this.slotsWithProposalEquivocation.has(slotNumber);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
public hasInvalidProposals(slotNumber: SlotNumber): boolean {
|
|
362
|
+
return this.slotsWithInvalidProposals.has(slotNumber);
|
|
363
|
+
}
|
|
364
|
+
|
|
290
365
|
public updateConfig(config: Partial<ValidatorClientFullConfig>) {
|
|
291
366
|
this.config = { ...this.config, ...config };
|
|
367
|
+
this.proposalHandler.updateConfig(config);
|
|
292
368
|
}
|
|
293
369
|
|
|
294
370
|
public reloadKeystore(newManager: KeystoreManager): void {
|
|
295
371
|
const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
|
|
296
372
|
this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
|
|
297
|
-
this.validationService = new ValidationService(
|
|
373
|
+
this.validationService = new ValidationService(
|
|
374
|
+
this.keyStore,
|
|
375
|
+
this.getSignatureContext(),
|
|
376
|
+
this.log.createChild('validation-service'),
|
|
377
|
+
);
|
|
298
378
|
}
|
|
299
379
|
|
|
300
380
|
public async start() {
|
|
@@ -353,6 +433,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
353
433
|
this.handleDuplicateAttestation(info);
|
|
354
434
|
});
|
|
355
435
|
|
|
436
|
+
this.p2pClient.registerCheckpointAttestationCallback((attestation: CheckpointAttestation) => {
|
|
437
|
+
this.handleCheckpointAttestation(attestation);
|
|
438
|
+
});
|
|
439
|
+
|
|
356
440
|
const myAddresses = this.getValidatorAddresses();
|
|
357
441
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
358
442
|
|
|
@@ -399,21 +483,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
399
483
|
fishermanMode: this.config.fishermanMode || false,
|
|
400
484
|
});
|
|
401
485
|
|
|
402
|
-
// Reexecute
|
|
403
|
-
|
|
404
|
-
const { slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } = this.config;
|
|
405
|
-
const shouldReexecute =
|
|
406
|
-
fishermanMode ||
|
|
407
|
-
slashBroadcastedInvalidBlockPenalty > 0n ||
|
|
408
|
-
partOfCommittee ||
|
|
409
|
-
alwaysReexecuteBlockProposals ||
|
|
410
|
-
this.blobClient.canUpload();
|
|
411
|
-
|
|
412
|
-
const validationResult = await this.proposalHandler.handleBlockProposal(
|
|
413
|
-
proposal,
|
|
414
|
-
proposalSender,
|
|
415
|
-
!!shouldReexecute && !escapeHatchOpen,
|
|
416
|
-
);
|
|
486
|
+
// Reexecute outside the escape hatch so slashing observers can detect invalid proposals even when penalties are 0.
|
|
487
|
+
const validationResult = await this.proposalHandler.handleBlockProposal(proposal, proposalSender, !escapeHatchOpen);
|
|
417
488
|
|
|
418
489
|
if (!validationResult.isValid) {
|
|
419
490
|
const reason = validationResult.reason || 'unknown';
|
|
@@ -436,15 +507,18 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
436
507
|
this.metrics.incFailedAttestationsNodeIssue(1, reason, partOfCommittee);
|
|
437
508
|
}
|
|
438
509
|
|
|
439
|
-
// Slash invalid block proposals (can happen even when not in committee)
|
|
440
510
|
if (
|
|
441
511
|
!escapeHatchOpen &&
|
|
442
512
|
validationResult.reason &&
|
|
443
|
-
SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason)
|
|
444
|
-
slashBroadcastedInvalidBlockPenalty > 0n
|
|
513
|
+
SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason)
|
|
445
514
|
) {
|
|
446
|
-
this.log.
|
|
515
|
+
this.log.info(`Detected invalid block proposal offense`, {
|
|
516
|
+
...proposalInfo,
|
|
517
|
+
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
518
|
+
offenseType: getOffenseTypeName(OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL),
|
|
519
|
+
});
|
|
447
520
|
this.slashInvalidBlock(proposal);
|
|
521
|
+
this.markInvalidProposalSlot(proposal.slotNumber);
|
|
448
522
|
}
|
|
449
523
|
return false;
|
|
450
524
|
}
|
|
@@ -483,6 +557,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
483
557
|
return undefined;
|
|
484
558
|
}
|
|
485
559
|
|
|
560
|
+
// Early-out for equivocation: refuses if we've already attested to a higher slot.
|
|
561
|
+
if (!this.shouldAttestToSlot(proposalSlotNumber)) {
|
|
562
|
+
return undefined;
|
|
563
|
+
}
|
|
564
|
+
|
|
486
565
|
// Ignore proposals from ourselves (may happen in HA setups)
|
|
487
566
|
if (proposer && this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
488
567
|
this.log.debug(`Ignoring block proposal from self for slot ${proposalSlotNumber}`, {
|
|
@@ -624,7 +703,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
624
703
|
*/
|
|
625
704
|
protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
|
|
626
705
|
try {
|
|
627
|
-
const lastBlockHeader = await this.blockSource.
|
|
706
|
+
const lastBlockHeader = (await this.blockSource.getBlockData({ archive: proposal.archive }))?.header;
|
|
628
707
|
if (!lastBlockHeader) {
|
|
629
708
|
this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
|
|
630
709
|
return;
|
|
@@ -657,12 +736,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
657
736
|
return;
|
|
658
737
|
}
|
|
659
738
|
|
|
660
|
-
// Trim the set if it's too big.
|
|
661
|
-
if (this.proposersOfInvalidBlocks.size > MAX_PROPOSERS_OF_INVALID_BLOCKS) {
|
|
662
|
-
// remove oldest proposer. `values` is guaranteed to be in insertion order.
|
|
663
|
-
this.proposersOfInvalidBlocks.delete(this.proposersOfInvalidBlocks.values().next().value!);
|
|
664
|
-
}
|
|
665
|
-
|
|
666
739
|
this.proposersOfInvalidBlocks.add(proposer.toString());
|
|
667
740
|
|
|
668
741
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
@@ -675,20 +748,115 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
675
748
|
]);
|
|
676
749
|
}
|
|
677
750
|
|
|
751
|
+
private handleInvalidCheckpointProposal(
|
|
752
|
+
proposal: CheckpointProposalCore,
|
|
753
|
+
result: CheckpointProposalValidationFailureResult,
|
|
754
|
+
proposalInfo: LogData,
|
|
755
|
+
): void {
|
|
756
|
+
if (!SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT[result.reason]) {
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
this.markInvalidProposalSlot(proposal.slotNumber);
|
|
761
|
+
|
|
762
|
+
if (this.slashInvalidCheckpointProposal(proposal)) {
|
|
763
|
+
this.log.info(`Detected invalid checkpoint proposal offense`, {
|
|
764
|
+
...proposalInfo,
|
|
765
|
+
reason: result.reason,
|
|
766
|
+
amount: this.config.slashBroadcastedInvalidCheckpointProposalPenalty,
|
|
767
|
+
offenseType: getOffenseTypeName(OffenseType.BROADCASTED_INVALID_CHECKPOINT_PROPOSAL),
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
private slashInvalidCheckpointProposal(proposal: CheckpointProposalCore): boolean {
|
|
773
|
+
const proposer = proposal.getSender();
|
|
774
|
+
if (!proposer) {
|
|
775
|
+
this.log.warn(`Cannot slash checkpoint proposal with invalid signature`, {
|
|
776
|
+
slotNumber: proposal.slotNumber,
|
|
777
|
+
archive: proposal.archive.toString(),
|
|
778
|
+
});
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const offenseType = OffenseType.BROADCASTED_INVALID_CHECKPOINT_PROPOSAL;
|
|
783
|
+
const offenseKey = `${proposer.toString()}:${offenseType}:${proposal.slotNumber}`;
|
|
784
|
+
if (!this.invalidCheckpointProposalOffenseKeys.addIfAbsent(offenseKey)) {
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
789
|
+
{
|
|
790
|
+
validator: proposer,
|
|
791
|
+
amount: this.config.slashBroadcastedInvalidCheckpointProposalPenalty,
|
|
792
|
+
offenseType,
|
|
793
|
+
epochOrSlot: BigInt(proposal.slotNumber),
|
|
794
|
+
},
|
|
795
|
+
]);
|
|
796
|
+
return true;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
private markInvalidProposalSlot(slotNumber: SlotNumber): void {
|
|
800
|
+
this.slotsWithInvalidProposals.add(slotNumber);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
private handleCheckpointAttestation(attestation: CheckpointAttestation): void {
|
|
804
|
+
const slotNumber = attestation.slotNumber;
|
|
805
|
+
if (!this.slotsWithInvalidProposals.has(slotNumber) || this.slotsWithProposalEquivocation.has(slotNumber)) {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const attester = attestation.getSender();
|
|
810
|
+
if (!attester) {
|
|
811
|
+
this.log.warn(`Cannot slash checkpoint attestation with invalid signature`, {
|
|
812
|
+
slotNumber,
|
|
813
|
+
archive: attestation.archive.toString(),
|
|
814
|
+
});
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
this.slashAttestedToInvalidCheckpointProposal(slotNumber, attester);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
private slashAttestedToInvalidCheckpointProposal(slotNumber: SlotNumber, attester: EthAddress): void {
|
|
822
|
+
const offenseKey = `${attester.toString()}:${OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL}:${slotNumber}`;
|
|
823
|
+
if (!this.badAttestationOffenseKeys.addIfAbsent(offenseKey)) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
this.log.info(`Detected attestation to invalid checkpoint proposal offense`, {
|
|
828
|
+
attester: attester.toString(),
|
|
829
|
+
slotNumber,
|
|
830
|
+
amount: this.config.slashAttestInvalidCheckpointProposalPenalty,
|
|
831
|
+
offenseType: getOffenseTypeName(OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL),
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
835
|
+
{
|
|
836
|
+
validator: attester,
|
|
837
|
+
amount: this.config.slashAttestInvalidCheckpointProposalPenalty,
|
|
838
|
+
offenseType: OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL,
|
|
839
|
+
epochOrSlot: BigInt(slotNumber),
|
|
840
|
+
},
|
|
841
|
+
]);
|
|
842
|
+
}
|
|
843
|
+
|
|
678
844
|
/**
|
|
679
845
|
* Handle detection of a duplicate proposal (equivocation).
|
|
680
846
|
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
681
847
|
*/
|
|
682
848
|
private handleDuplicateProposal(info: DuplicateProposalInfo): void {
|
|
683
849
|
const { slot, proposer, type } = info;
|
|
850
|
+
this.slotsWithProposalEquivocation.add(slot);
|
|
684
851
|
|
|
685
|
-
this.log.
|
|
852
|
+
this.log.info(`Detected duplicate ${type} proposal offense from ${proposer.toString()} at slot ${slot}`, {
|
|
686
853
|
proposer: proposer.toString(),
|
|
687
854
|
slot,
|
|
688
855
|
type,
|
|
856
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
857
|
+
offenseType: getOffenseTypeName(OffenseType.DUPLICATE_PROPOSAL),
|
|
689
858
|
});
|
|
690
859
|
|
|
691
|
-
// Emit slash event
|
|
692
860
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
693
861
|
{
|
|
694
862
|
validator: proposer,
|
|
@@ -697,6 +865,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
697
865
|
epochOrSlot: BigInt(slot),
|
|
698
866
|
},
|
|
699
867
|
]);
|
|
868
|
+
|
|
869
|
+
this.emit(WANT_TO_CLEAR_SLASH_EVENT, [
|
|
870
|
+
{
|
|
871
|
+
offenseType: OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL,
|
|
872
|
+
epochOrSlot: BigInt(slot),
|
|
873
|
+
},
|
|
874
|
+
]);
|
|
700
875
|
}
|
|
701
876
|
|
|
702
877
|
/**
|
|
@@ -706,9 +881,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
706
881
|
private handleDuplicateAttestation(info: DuplicateAttestationInfo): void {
|
|
707
882
|
const { slot, attester } = info;
|
|
708
883
|
|
|
709
|
-
this.log.
|
|
884
|
+
this.log.info(`Detected duplicate attestation offense from ${attester.toString()} at slot ${slot}`, {
|
|
710
885
|
attester: attester.toString(),
|
|
711
886
|
slot,
|
|
887
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
888
|
+
offenseType: getOffenseTypeName(OffenseType.DUPLICATE_ATTESTATION),
|
|
712
889
|
});
|
|
713
890
|
|
|
714
891
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
@@ -758,7 +935,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
758
935
|
proposerAddress,
|
|
759
936
|
{
|
|
760
937
|
...options,
|
|
761
|
-
broadcastInvalidBlockProposal:
|
|
938
|
+
broadcastInvalidBlockProposal:
|
|
939
|
+
options.broadcastInvalidBlockProposal || this.config.broadcastInvalidBlockProposal,
|
|
762
940
|
},
|
|
763
941
|
);
|
|
764
942
|
this.lastProposedBlock = newProposal;
|
|
@@ -798,6 +976,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
798
976
|
options,
|
|
799
977
|
);
|
|
800
978
|
this.lastProposedCheckpoint = newProposal;
|
|
979
|
+
// Self-record this slot's outcome on the re-execution tracker. Proposers don't run their
|
|
980
|
+
// own proposals through `handleCheckpointProposal`, so without this call the proposer's
|
|
981
|
+
// sentinel would see no outcome for slots it proposed and would mis-attribute itself as
|
|
982
|
+
// inactive. We pass the locally-computed `archive` (not `newProposal.archive`, which may
|
|
983
|
+
// be intentionally corrupted under test-only flags); from the proposer's local-view
|
|
984
|
+
// perspective the work it just completed is valid by definition.
|
|
985
|
+
this.proposalHandler.recordOwnCheckpointProposalAsValid(checkpointHeader.slotNumber, archive, checkpointNumber);
|
|
801
986
|
return newProposal;
|
|
802
987
|
}
|
|
803
988
|
|
|
@@ -860,31 +1045,21 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
860
1045
|
|
|
861
1046
|
await this.collectOwnAttestations(proposal, checkpointNumber);
|
|
862
1047
|
|
|
863
|
-
const
|
|
1048
|
+
const proposalPayloadHash = proposal.getPayloadHash();
|
|
864
1049
|
const myAddresses = this.getValidatorAddresses();
|
|
865
1050
|
|
|
866
1051
|
let attestations: CheckpointAttestation[] = [];
|
|
867
1052
|
while (true) {
|
|
868
|
-
//
|
|
869
|
-
//
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
if (!attestation.archive.equals(proposal.archive)) {
|
|
873
|
-
this.log.warn(
|
|
874
|
-
`Received attestation for slot ${slot} with mismatched archive from ${attestation.getSender()?.toString()}`,
|
|
875
|
-
{ attestationArchive: attestation.archive.toString(), proposalArchive: proposal.archive.toString() },
|
|
876
|
-
);
|
|
877
|
-
return false;
|
|
878
|
-
}
|
|
879
|
-
return true;
|
|
880
|
-
},
|
|
881
|
-
);
|
|
1053
|
+
// The pool already filters by proposal payload hash; if any attestation slips through with a
|
|
1054
|
+
// mismatched payload hash, drop it defensively. Equivocations are emitted as separate slash
|
|
1055
|
+
// events from libp2p_service.
|
|
1056
|
+
const collectedAttestations = await this.p2pClient.getCheckpointAttestationsForSlot(slot, proposalPayloadHash);
|
|
882
1057
|
|
|
883
1058
|
// Log new attestations we collected
|
|
884
1059
|
const oldSenders = attestations.map(attestation => attestation.getSender());
|
|
885
1060
|
for (const collected of collectedAttestations) {
|
|
886
1061
|
const collectedSender = collected.getSender();
|
|
887
|
-
// Skip attestations with invalid signatures
|
|
1062
|
+
// Skip attestations with invalid signatures. Should not happen as we don't add invalid attestations to our pool.
|
|
888
1063
|
if (!collectedSender) {
|
|
889
1064
|
this.log.warn(`Skipping attestation with invalid signature for slot ${slot}`);
|
|
890
1065
|
continue;
|