@aztec/validator-client 0.0.1-commit.42ee6df9b → 0.0.1-commit.431c48d
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 +12 -9
- package/dest/checkpoint_builder.d.ts +12 -4
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +14 -3
- package/dest/config.d.ts +9 -3
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +23 -5
- package/dest/duties/validation_service.d.ts +12 -13
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +32 -38
- package/dest/factory.d.ts +5 -2
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +6 -6
- package/dest/key_store/web3signer_key_store.d.ts +10 -2
- package/dest/key_store/web3signer_key_store.d.ts.map +1 -1
- package/dest/key_store/web3signer_key_store.js +32 -41
- package/dest/metrics.d.ts +5 -1
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +15 -1
- package/dest/proposal_handler.d.ts +84 -18
- package/dest/proposal_handler.d.ts.map +1 -1
- package/dest/proposal_handler.js +510 -171
- package/dest/validator.d.ts +32 -13
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +208 -65
- package/package.json +19 -19
- package/src/checkpoint_builder.ts +16 -5
- package/src/config.ts +31 -7
- package/src/duties/validation_service.ts +51 -47
- package/src/factory.ts +12 -5
- package/src/key_store/web3signer_key_store.ts +43 -59
- package/src/metrics.ts +20 -0
- package/src/proposal_handler.ts +590 -199
- package/src/validator.ts +280 -88
package/src/validator.ts
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
import type { BlobClientInterface } from '@aztec/blob-client/client';
|
|
2
2
|
import { type Blob, getBlobsPerL1Block } from '@aztec/blob-lib';
|
|
3
3
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
4
|
-
import {
|
|
5
|
-
BlockNumber,
|
|
6
|
-
CheckpointNumber,
|
|
7
|
-
EpochNumber,
|
|
8
|
-
IndexWithinCheckpoint,
|
|
9
|
-
SlotNumber,
|
|
10
|
-
} from '@aztec/foundation/branded-types';
|
|
4
|
+
import { CheckpointNumber, EpochNumber, IndexWithinCheckpoint, SlotNumber } from '@aztec/foundation/branded-types';
|
|
11
5
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
12
6
|
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
13
|
-
import
|
|
7
|
+
import { Signature } from '@aztec/foundation/eth-signature';
|
|
8
|
+
import { FifoSet } from '@aztec/foundation/fifo-set';
|
|
14
9
|
import { type LogData, type Logger, createLogger } from '@aztec/foundation/log';
|
|
15
10
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
16
11
|
import { sleep } from '@aztec/foundation/sleep';
|
|
17
12
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
18
13
|
import type { KeystoreManager } from '@aztec/node-keystore';
|
|
19
|
-
import type { DuplicateAttestationInfo, DuplicateProposalInfo, P2P, PeerId } from '@aztec/p2p';
|
|
20
|
-
import { AuthRequest, AuthResponse,
|
|
21
|
-
import {
|
|
14
|
+
import type { DuplicateAttestationInfo, DuplicateProposalInfo, OversizedProposalInfo, P2P, PeerId } from '@aztec/p2p';
|
|
15
|
+
import { AuthRequest, AuthResponse, ReqRespSubProtocol } from '@aztec/p2p';
|
|
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';
|
|
22
24
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
23
25
|
import type { CommitteeAttestationsAndSigners, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
26
|
+
import type { CheckpointReexecutionTracker } from '@aztec/stdlib/checkpoint';
|
|
24
27
|
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
25
28
|
import type {
|
|
26
|
-
CreateCheckpointProposalLastBlockData,
|
|
27
29
|
ITxProvider,
|
|
28
30
|
Validator,
|
|
29
31
|
ValidatorClientFullConfig,
|
|
@@ -33,12 +35,16 @@ import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
|
33
35
|
import {
|
|
34
36
|
type BlockProposal,
|
|
35
37
|
type BlockProposalOptions,
|
|
36
|
-
|
|
38
|
+
CheckpointAttestation,
|
|
37
39
|
CheckpointProposal,
|
|
38
40
|
type CheckpointProposalCore,
|
|
39
41
|
type CheckpointProposalOptions,
|
|
42
|
+
type CoordinationSignatureContext,
|
|
43
|
+
type ValidatedBlockProposal,
|
|
44
|
+
type ValidatedCheckpointProposalCore,
|
|
40
45
|
} from '@aztec/stdlib/p2p';
|
|
41
46
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
47
|
+
import { ConsensusTimetable } from '@aztec/stdlib/timetable';
|
|
42
48
|
import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
|
|
43
49
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
44
50
|
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
@@ -59,17 +65,19 @@ import { HAKeyStore } from './key_store/ha_key_store.js';
|
|
|
59
65
|
import type { ExtendedValidatorKeyStore } from './key_store/interface.js';
|
|
60
66
|
import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
|
|
61
67
|
import { ValidatorMetrics } from './metrics.js';
|
|
62
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
type BlockProposalValidationFailureReason,
|
|
70
|
+
type CheckpointProposalValidationFailureResult,
|
|
71
|
+
ProposalHandler,
|
|
72
|
+
SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT,
|
|
73
|
+
SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT,
|
|
74
|
+
} from './proposal_handler.js';
|
|
63
75
|
|
|
64
76
|
// We maintain a set of proposers who have proposed invalid blocks.
|
|
65
77
|
// Just cap the set to avoid unbounded growth.
|
|
66
78
|
const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT: BlockProposalValidationFailureReason[] = [
|
|
70
|
-
'state_mismatch',
|
|
71
|
-
'failed_txs',
|
|
72
|
-
];
|
|
79
|
+
const MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS = 1000;
|
|
80
|
+
const MAX_TRACKED_BAD_ATTESTATIONS = 10_000;
|
|
73
81
|
|
|
74
82
|
/**
|
|
75
83
|
* Validator Client
|
|
@@ -93,7 +101,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
93
101
|
/** Tracks the last epoch in which each attester successfully submitted at least one attestation. */
|
|
94
102
|
private lastAttestedEpochByAttester: Map<string, EpochNumber> = new Map();
|
|
95
103
|
|
|
96
|
-
private proposersOfInvalidBlocks
|
|
104
|
+
private proposersOfInvalidBlocks = FifoSet.withLimit<string>(MAX_PROPOSERS_OF_INVALID_BLOCKS);
|
|
105
|
+
private invalidCheckpointProposalOffenseKeys = FifoSet.withLimit<string>(MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS);
|
|
106
|
+
private oversizedProposalOffenseKeys = FifoSet.withLimit<string>(MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS);
|
|
107
|
+
private badAttestationOffenseKeys = FifoSet.withLimit<string>(MAX_TRACKED_BAD_ATTESTATIONS);
|
|
97
108
|
|
|
98
109
|
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
|
|
99
110
|
private lastAttestedProposal?: CheckpointProposalCore;
|
|
@@ -122,11 +133,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
122
133
|
this.tracer = telemetry.getTracer('Validator');
|
|
123
134
|
this.metrics = new ValidatorMetrics(telemetry);
|
|
124
135
|
|
|
125
|
-
this.validationService = new ValidationService(
|
|
136
|
+
this.validationService = new ValidationService(
|
|
137
|
+
keyStore,
|
|
138
|
+
this.getSignatureContext(),
|
|
139
|
+
this.log.createChild('validation-service'),
|
|
140
|
+
);
|
|
141
|
+
this.proposalHandler.setCheckpointProposalValidationFailureCallback((proposal, result, proposalInfo) =>
|
|
142
|
+
this.handleInvalidCheckpointProposal(proposal, result, proposalInfo),
|
|
143
|
+
);
|
|
126
144
|
|
|
127
145
|
// Refresh epoch cache every second to trigger alert if participation in committee changes
|
|
128
146
|
this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), this.log, 1000);
|
|
129
|
-
|
|
130
147
|
const myAddresses = this.getValidatorAddresses();
|
|
131
148
|
this.log.verbose(`Initialized validator with addresses: ${myAddresses.map(a => a.toString()).join(', ')}`);
|
|
132
149
|
}
|
|
@@ -195,14 +212,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
195
212
|
txProvider: ITxProvider,
|
|
196
213
|
keyStoreManager: KeystoreManager,
|
|
197
214
|
blobClient: BlobClientInterface,
|
|
215
|
+
reexecutionTracker: CheckpointReexecutionTracker,
|
|
198
216
|
dateProvider: DateProvider = new DateProvider(),
|
|
199
217
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
200
218
|
slashingProtectionDb?: SlashingProtectionDatabase,
|
|
201
219
|
) {
|
|
202
220
|
const metrics = new ValidatorMetrics(telemetry);
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
221
|
+
const consensusTimetable = new ConsensusTimetable({
|
|
222
|
+
l1Constants: epochCache.getL1Constants(),
|
|
223
|
+
blockDuration: config.blockDurationMs / 1000,
|
|
206
224
|
});
|
|
207
225
|
const proposalHandler = new ProposalHandler(
|
|
208
226
|
checkpointsBuilder,
|
|
@@ -210,13 +228,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
210
228
|
blockSource,
|
|
211
229
|
l1ToL2MessageSource,
|
|
212
230
|
txProvider,
|
|
213
|
-
blockProposalValidator,
|
|
214
231
|
epochCache,
|
|
232
|
+
consensusTimetable,
|
|
215
233
|
config,
|
|
216
234
|
blobClient,
|
|
235
|
+
reexecutionTracker,
|
|
217
236
|
metrics,
|
|
218
237
|
dateProvider,
|
|
219
238
|
telemetry,
|
|
239
|
+
undefined,
|
|
220
240
|
);
|
|
221
241
|
|
|
222
242
|
const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
@@ -281,6 +301,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
281
301
|
return this.keyStore.signTypedDataWithAddress(addr, msg, context);
|
|
282
302
|
}
|
|
283
303
|
|
|
304
|
+
private getSignatureContext(): CoordinationSignatureContext {
|
|
305
|
+
return {
|
|
306
|
+
chainId: this.config.l1ChainId,
|
|
307
|
+
rollupAddress: this.config.rollupAddress,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
284
311
|
public getCoinbaseForAttestor(attestor: EthAddress): EthAddress {
|
|
285
312
|
return this.keyStore.getCoinbaseAddress(attestor);
|
|
286
313
|
}
|
|
@@ -293,14 +320,27 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
293
320
|
return this.config;
|
|
294
321
|
}
|
|
295
322
|
|
|
323
|
+
public hasProposalEquivocation(slotNumber: SlotNumber): boolean {
|
|
324
|
+
return this.proposalHandler.hasProposalEquivocation(slotNumber);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
public hasInvalidProposals(slotNumber: SlotNumber): boolean {
|
|
328
|
+
return this.proposalHandler.hasInvalidProposals(slotNumber);
|
|
329
|
+
}
|
|
330
|
+
|
|
296
331
|
public updateConfig(config: Partial<ValidatorClientFullConfig>) {
|
|
297
332
|
this.config = { ...this.config, ...config };
|
|
333
|
+
this.proposalHandler.updateConfig(config);
|
|
298
334
|
}
|
|
299
335
|
|
|
300
336
|
public reloadKeystore(newManager: KeystoreManager): void {
|
|
301
337
|
const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
|
|
302
338
|
this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
|
|
303
|
-
this.validationService = new ValidationService(
|
|
339
|
+
this.validationService = new ValidationService(
|
|
340
|
+
this.keyStore,
|
|
341
|
+
this.getSignatureContext(),
|
|
342
|
+
this.log.createChild('validation-service'),
|
|
343
|
+
);
|
|
304
344
|
}
|
|
305
345
|
|
|
306
346
|
public async start() {
|
|
@@ -336,7 +376,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
336
376
|
this.log.debug(`Registering validator handlers for p2p client`);
|
|
337
377
|
|
|
338
378
|
// Block proposal handler - validates but does NOT attest (validators only attest to checkpoints)
|
|
339
|
-
const blockHandler = (block:
|
|
379
|
+
const blockHandler = (block: ValidatedBlockProposal, proposalSender: PeerId): Promise<boolean> =>
|
|
340
380
|
this.validateBlockProposal(block, proposalSender);
|
|
341
381
|
this.p2pClient.registerBlockProposalHandler(blockHandler);
|
|
342
382
|
|
|
@@ -344,7 +384,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
344
384
|
// The checkpoint is received as CheckpointProposalCore since the lastBlock is extracted
|
|
345
385
|
// and processed separately via the block handler above.
|
|
346
386
|
const checkpointHandler = (
|
|
347
|
-
checkpoint:
|
|
387
|
+
checkpoint: ValidatedCheckpointProposalCore,
|
|
348
388
|
proposalSender: PeerId,
|
|
349
389
|
): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
|
|
350
390
|
this.p2pClient.registerValidatorCheckpointProposalHandler(checkpointHandler);
|
|
@@ -354,11 +394,20 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
354
394
|
this.handleDuplicateProposal(info);
|
|
355
395
|
});
|
|
356
396
|
|
|
397
|
+
// Oversized proposal handler - triggers slashing for proposals beyond the per-checkpoint block limit
|
|
398
|
+
this.p2pClient.registerOversizedProposalCallback((info: OversizedProposalInfo) => {
|
|
399
|
+
this.handleOversizedProposal(info);
|
|
400
|
+
});
|
|
401
|
+
|
|
357
402
|
// Duplicate attestation handler - triggers slashing for attestation equivocation
|
|
358
403
|
this.p2pClient.registerDuplicateAttestationCallback((info: DuplicateAttestationInfo) => {
|
|
359
404
|
this.handleDuplicateAttestation(info);
|
|
360
405
|
});
|
|
361
406
|
|
|
407
|
+
this.p2pClient.registerCheckpointAttestationCallback((attestation: CheckpointAttestation) => {
|
|
408
|
+
this.handleCheckpointAttestation(attestation);
|
|
409
|
+
});
|
|
410
|
+
|
|
362
411
|
const myAddresses = this.getValidatorAddresses();
|
|
363
412
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
364
413
|
|
|
@@ -371,7 +420,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
371
420
|
* Note: Validators do NOT attest to individual blocks - attestations are only for checkpoint proposals.
|
|
372
421
|
* @returns true if the proposal is valid, false otherwise
|
|
373
422
|
*/
|
|
374
|
-
async validateBlockProposal(proposal:
|
|
423
|
+
async validateBlockProposal(proposal: ValidatedBlockProposal, proposalSender: PeerId): Promise<boolean> {
|
|
375
424
|
const slotNumber = proposal.slotNumber;
|
|
376
425
|
|
|
377
426
|
// Note: During escape hatch, we still want to "validate" proposals for observability,
|
|
@@ -405,21 +454,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
405
454
|
fishermanMode: this.config.fishermanMode || false,
|
|
406
455
|
});
|
|
407
456
|
|
|
408
|
-
// Reexecute
|
|
409
|
-
|
|
410
|
-
const { slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } = this.config;
|
|
411
|
-
const shouldReexecute =
|
|
412
|
-
fishermanMode ||
|
|
413
|
-
slashBroadcastedInvalidBlockPenalty > 0n ||
|
|
414
|
-
partOfCommittee ||
|
|
415
|
-
alwaysReexecuteBlockProposals ||
|
|
416
|
-
this.blobClient.canUpload();
|
|
417
|
-
|
|
418
|
-
const validationResult = await this.proposalHandler.handleBlockProposal(
|
|
419
|
-
proposal,
|
|
420
|
-
proposalSender,
|
|
421
|
-
!!shouldReexecute && !escapeHatchOpen,
|
|
422
|
-
);
|
|
457
|
+
// Reexecute outside the escape hatch so slashing observers can detect invalid proposals even when penalties are 0.
|
|
458
|
+
const validationResult = await this.proposalHandler.handleBlockProposal(proposal, proposalSender, !escapeHatchOpen);
|
|
423
459
|
|
|
424
460
|
if (!validationResult.isValid) {
|
|
425
461
|
const reason = validationResult.reason || 'unknown';
|
|
@@ -433,6 +469,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
433
469
|
'failed_txs',
|
|
434
470
|
'in_hash_mismatch',
|
|
435
471
|
'parent_block_wrong_slot',
|
|
472
|
+
'duplicate_txs',
|
|
473
|
+
'invalid_embedded_txs',
|
|
436
474
|
];
|
|
437
475
|
|
|
438
476
|
if (badProposalReasons.includes(reason as BlockProposalValidationFailureReason)) {
|
|
@@ -442,15 +480,18 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
442
480
|
this.metrics.incFailedAttestationsNodeIssue(1, reason, partOfCommittee);
|
|
443
481
|
}
|
|
444
482
|
|
|
445
|
-
// Slash invalid block proposals (can happen even when not in committee)
|
|
446
483
|
if (
|
|
447
484
|
!escapeHatchOpen &&
|
|
448
485
|
validationResult.reason &&
|
|
449
|
-
SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason)
|
|
450
|
-
slashBroadcastedInvalidBlockPenalty > 0n
|
|
486
|
+
SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason)
|
|
451
487
|
) {
|
|
452
|
-
this.log.
|
|
488
|
+
this.log.info(`Detected invalid block proposal offense`, {
|
|
489
|
+
...proposalInfo,
|
|
490
|
+
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
491
|
+
offenseType: getOffenseTypeName(OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL),
|
|
492
|
+
});
|
|
453
493
|
this.slashInvalidBlock(proposal);
|
|
494
|
+
this.markInvalidProposalSlot(proposal.slotNumber);
|
|
454
495
|
}
|
|
455
496
|
return false;
|
|
456
497
|
}
|
|
@@ -477,7 +518,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
477
518
|
* @returns Checkpoint attestations if valid, undefined otherwise
|
|
478
519
|
*/
|
|
479
520
|
async attestToCheckpointProposal(
|
|
480
|
-
proposal:
|
|
521
|
+
proposal: ValidatedCheckpointProposalCore,
|
|
481
522
|
_proposalSender: PeerId,
|
|
482
523
|
): Promise<CheckpointAttestation[] | undefined> {
|
|
483
524
|
const proposalSlotNumber = proposal.slotNumber;
|
|
@@ -489,9 +530,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
489
530
|
return undefined;
|
|
490
531
|
}
|
|
491
532
|
|
|
533
|
+
// Early-out for equivocation: refuses if we've already attested to a higher slot.
|
|
534
|
+
if (!this.shouldAttestToSlot(proposalSlotNumber)) {
|
|
535
|
+
return undefined;
|
|
536
|
+
}
|
|
537
|
+
|
|
492
538
|
// Ignore proposals from ourselves (may happen in HA setups)
|
|
493
539
|
if (proposer && this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
494
|
-
this.log.debug(`
|
|
540
|
+
this.log.debug(`Not attesting to block proposal from self for slot ${proposalSlotNumber}`, {
|
|
495
541
|
proposer: proposer.toString(),
|
|
496
542
|
proposalSlotNumber,
|
|
497
543
|
});
|
|
@@ -514,14 +560,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
514
560
|
|
|
515
561
|
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set).
|
|
516
562
|
// Uses the cached result from the all-nodes callback if available (avoids double validation).
|
|
563
|
+
let checkpointNumber: CheckpointNumber;
|
|
517
564
|
if (this.config.skipCheckpointProposalValidation) {
|
|
518
565
|
this.log.warn(`Skipping checkpoint proposal validation for slot ${proposalSlotNumber}`, proposalInfo);
|
|
566
|
+
checkpointNumber = CheckpointNumber(0);
|
|
519
567
|
} else {
|
|
520
568
|
const validationResult = await this.proposalHandler.handleCheckpointProposal(proposal, proposalInfo);
|
|
521
569
|
if (!validationResult.isValid) {
|
|
522
570
|
this.log.warn(`Checkpoint proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
523
571
|
return undefined;
|
|
524
572
|
}
|
|
573
|
+
checkpointNumber = validationResult.checkpointNumber;
|
|
525
574
|
}
|
|
526
575
|
|
|
527
576
|
// Check that I have any address in current committee before attesting
|
|
@@ -579,7 +628,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
579
628
|
return undefined;
|
|
580
629
|
}
|
|
581
630
|
|
|
582
|
-
return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
631
|
+
return await this.createCheckpointAttestationsFromProposal(proposal, attestors, checkpointNumber);
|
|
583
632
|
}
|
|
584
633
|
|
|
585
634
|
/**
|
|
@@ -606,13 +655,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
606
655
|
private async createCheckpointAttestationsFromProposal(
|
|
607
656
|
proposal: CheckpointProposalCore,
|
|
608
657
|
attestors: EthAddress[] = [],
|
|
658
|
+
checkpointNumber: CheckpointNumber,
|
|
609
659
|
): Promise<CheckpointAttestation[] | undefined> {
|
|
610
660
|
// Equivocation check: must happen right before signing to minimize the race window
|
|
611
661
|
if (!this.shouldAttestToSlot(proposal.slotNumber)) {
|
|
612
662
|
return undefined;
|
|
613
663
|
}
|
|
614
664
|
|
|
615
|
-
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
665
|
+
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors, checkpointNumber);
|
|
616
666
|
|
|
617
667
|
// Track the proposal we attested to (to prevent equivocation)
|
|
618
668
|
this.lastAttestedProposal = proposal;
|
|
@@ -626,7 +676,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
626
676
|
*/
|
|
627
677
|
protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
|
|
628
678
|
try {
|
|
629
|
-
const lastBlockHeader = await this.blockSource.
|
|
679
|
+
const lastBlockHeader = (await this.blockSource.getBlockData({ archive: proposal.archive }))?.header;
|
|
630
680
|
if (!lastBlockHeader) {
|
|
631
681
|
this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
|
|
632
682
|
return;
|
|
@@ -659,12 +709,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
659
709
|
return;
|
|
660
710
|
}
|
|
661
711
|
|
|
662
|
-
// Trim the set if it's too big.
|
|
663
|
-
if (this.proposersOfInvalidBlocks.size > MAX_PROPOSERS_OF_INVALID_BLOCKS) {
|
|
664
|
-
// remove oldest proposer. `values` is guaranteed to be in insertion order.
|
|
665
|
-
this.proposersOfInvalidBlocks.delete(this.proposersOfInvalidBlocks.values().next().value!);
|
|
666
|
-
}
|
|
667
|
-
|
|
668
712
|
this.proposersOfInvalidBlocks.add(proposer.toString());
|
|
669
713
|
|
|
670
714
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
@@ -677,20 +721,148 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
677
721
|
]);
|
|
678
722
|
}
|
|
679
723
|
|
|
724
|
+
private handleInvalidCheckpointProposal(
|
|
725
|
+
proposal: CheckpointProposalCore,
|
|
726
|
+
result: CheckpointProposalValidationFailureResult,
|
|
727
|
+
proposalInfo: LogData,
|
|
728
|
+
): void {
|
|
729
|
+
if (!SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT[result.reason]) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// The slot is already marked invalid by the all-nodes checkpoint handler that invokes this callback,
|
|
734
|
+
// so we only emit the proposer slash event here.
|
|
735
|
+
if (this.slashInvalidCheckpointProposal(proposal)) {
|
|
736
|
+
this.log.info(`Detected invalid checkpoint proposal offense`, {
|
|
737
|
+
...proposalInfo,
|
|
738
|
+
reason: result.reason,
|
|
739
|
+
amount: this.config.slashBroadcastedInvalidCheckpointProposalPenalty,
|
|
740
|
+
offenseType: getOffenseTypeName(OffenseType.BROADCASTED_INVALID_CHECKPOINT_PROPOSAL),
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
private slashInvalidCheckpointProposal(proposal: CheckpointProposalCore): boolean {
|
|
746
|
+
const proposer = proposal.getSender();
|
|
747
|
+
if (!proposer) {
|
|
748
|
+
this.log.warn(`Cannot slash checkpoint proposal with invalid signature`, {
|
|
749
|
+
slotNumber: proposal.slotNumber,
|
|
750
|
+
archive: proposal.archive.toString(),
|
|
751
|
+
});
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const offenseType = OffenseType.BROADCASTED_INVALID_CHECKPOINT_PROPOSAL;
|
|
756
|
+
const offenseKey = `${proposer.toString()}:${offenseType}:${proposal.slotNumber}`;
|
|
757
|
+
if (!this.invalidCheckpointProposalOffenseKeys.addIfAbsent(offenseKey)) {
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
762
|
+
{
|
|
763
|
+
validator: proposer,
|
|
764
|
+
amount: this.config.slashBroadcastedInvalidCheckpointProposalPenalty,
|
|
765
|
+
offenseType,
|
|
766
|
+
epochOrSlot: BigInt(proposal.slotNumber),
|
|
767
|
+
},
|
|
768
|
+
]);
|
|
769
|
+
return true;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
private markInvalidProposalSlot(slotNumber: SlotNumber): void {
|
|
773
|
+
this.proposalHandler.markInvalidProposalSlot(slotNumber);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
private handleCheckpointAttestation(attestation: CheckpointAttestation): void {
|
|
777
|
+
const slotNumber = attestation.slotNumber;
|
|
778
|
+
if (
|
|
779
|
+
!this.proposalHandler.hasInvalidProposals(slotNumber) ||
|
|
780
|
+
this.proposalHandler.hasProposalEquivocation(slotNumber)
|
|
781
|
+
) {
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const attester = attestation.getSender();
|
|
786
|
+
if (!attester) {
|
|
787
|
+
this.log.warn(`Cannot slash checkpoint attestation with invalid signature`, {
|
|
788
|
+
slotNumber,
|
|
789
|
+
archive: attestation.archive.toString(),
|
|
790
|
+
});
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
this.slashAttestedToInvalidCheckpointProposal(slotNumber, attester);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
private slashAttestedToInvalidCheckpointProposal(slotNumber: SlotNumber, attester: EthAddress): void {
|
|
798
|
+
const offenseKey = `${attester.toString()}:${OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL}:${slotNumber}`;
|
|
799
|
+
if (!this.badAttestationOffenseKeys.addIfAbsent(offenseKey)) {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
this.log.info(`Detected attestation to invalid checkpoint proposal offense`, {
|
|
804
|
+
attester: attester.toString(),
|
|
805
|
+
slotNumber,
|
|
806
|
+
amount: this.config.slashAttestInvalidCheckpointProposalPenalty,
|
|
807
|
+
offenseType: getOffenseTypeName(OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL),
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
811
|
+
{
|
|
812
|
+
validator: attester,
|
|
813
|
+
amount: this.config.slashAttestInvalidCheckpointProposalPenalty,
|
|
814
|
+
offenseType: OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL,
|
|
815
|
+
epochOrSlot: BigInt(slotNumber),
|
|
816
|
+
},
|
|
817
|
+
]);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Handle detection of an oversized block proposal: one whose index within its checkpoint lands at or
|
|
822
|
+
* beyond the consensus per-checkpoint block limit. A single signed proposal at an illegal index is
|
|
823
|
+
* self-contained evidence, so emit an invalid-block-proposal slash event for the proposer, deduped per
|
|
824
|
+
* (proposer, slot) since the p2p layer reports every oversized proposal it stores.
|
|
825
|
+
*/
|
|
826
|
+
private handleOversizedProposal(info: OversizedProposalInfo): void {
|
|
827
|
+
const { slot, proposer } = info;
|
|
828
|
+
const offenseType = OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL;
|
|
829
|
+
if (!this.oversizedProposalOffenseKeys.addIfAbsent(`${proposer.toString()}:${offenseType}:${slot}`)) {
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
this.log.info(`Detected oversized block proposal offense from ${proposer.toString()} at slot ${slot}`, {
|
|
834
|
+
proposer: proposer.toString(),
|
|
835
|
+
slot,
|
|
836
|
+
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
837
|
+
offenseType: getOffenseTypeName(offenseType),
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
841
|
+
{
|
|
842
|
+
validator: proposer,
|
|
843
|
+
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
844
|
+
offenseType,
|
|
845
|
+
epochOrSlot: BigInt(slot),
|
|
846
|
+
},
|
|
847
|
+
]);
|
|
848
|
+
}
|
|
849
|
+
|
|
680
850
|
/**
|
|
681
851
|
* Handle detection of a duplicate proposal (equivocation).
|
|
682
852
|
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
683
853
|
*/
|
|
684
854
|
private handleDuplicateProposal(info: DuplicateProposalInfo): void {
|
|
685
855
|
const { slot, proposer, type } = info;
|
|
856
|
+
this.proposalHandler.markProposalEquivocation(slot);
|
|
686
857
|
|
|
687
|
-
this.log.
|
|
858
|
+
this.log.info(`Detected duplicate ${type} proposal offense from ${proposer.toString()} at slot ${slot}`, {
|
|
688
859
|
proposer: proposer.toString(),
|
|
689
860
|
slot,
|
|
690
861
|
type,
|
|
862
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
863
|
+
offenseType: getOffenseTypeName(OffenseType.DUPLICATE_PROPOSAL),
|
|
691
864
|
});
|
|
692
865
|
|
|
693
|
-
// Emit slash event
|
|
694
866
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
695
867
|
{
|
|
696
868
|
validator: proposer,
|
|
@@ -699,6 +871,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
699
871
|
epochOrSlot: BigInt(slot),
|
|
700
872
|
},
|
|
701
873
|
]);
|
|
874
|
+
|
|
875
|
+
this.emit(WANT_TO_CLEAR_SLASH_EVENT, [
|
|
876
|
+
{
|
|
877
|
+
offenseType: OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL,
|
|
878
|
+
epochOrSlot: BigInt(slot),
|
|
879
|
+
},
|
|
880
|
+
]);
|
|
702
881
|
}
|
|
703
882
|
|
|
704
883
|
/**
|
|
@@ -708,9 +887,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
708
887
|
private handleDuplicateAttestation(info: DuplicateAttestationInfo): void {
|
|
709
888
|
const { slot, attester } = info;
|
|
710
889
|
|
|
711
|
-
this.log.
|
|
890
|
+
this.log.info(`Detected duplicate attestation offense from ${attester.toString()} at slot ${slot}`, {
|
|
712
891
|
attester: attester.toString(),
|
|
713
892
|
slot,
|
|
893
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
894
|
+
offenseType: getOffenseTypeName(OffenseType.DUPLICATE_ATTESTATION),
|
|
714
895
|
});
|
|
715
896
|
|
|
716
897
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
@@ -725,6 +906,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
725
906
|
|
|
726
907
|
async createBlockProposal(
|
|
727
908
|
blockHeader: BlockHeader,
|
|
909
|
+
checkpointNumber: CheckpointNumber,
|
|
728
910
|
indexWithinCheckpoint: IndexWithinCheckpoint,
|
|
729
911
|
inHash: Fr,
|
|
730
912
|
archive: Fr,
|
|
@@ -751,6 +933,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
751
933
|
);
|
|
752
934
|
const newProposal = await this.validationService.createBlockProposal(
|
|
753
935
|
blockHeader,
|
|
936
|
+
checkpointNumber,
|
|
754
937
|
indexWithinCheckpoint,
|
|
755
938
|
inHash,
|
|
756
939
|
archive,
|
|
@@ -758,7 +941,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
758
941
|
proposerAddress,
|
|
759
942
|
{
|
|
760
943
|
...options,
|
|
761
|
-
broadcastInvalidBlockProposal:
|
|
944
|
+
broadcastInvalidBlockProposal:
|
|
945
|
+
options.broadcastInvalidBlockProposal || this.config.broadcastInvalidBlockProposal,
|
|
762
946
|
},
|
|
763
947
|
);
|
|
764
948
|
this.lastProposedBlock = newProposal;
|
|
@@ -768,8 +952,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
768
952
|
async createCheckpointProposal(
|
|
769
953
|
checkpointHeader: CheckpointHeader,
|
|
770
954
|
archive: Fr,
|
|
955
|
+
checkpointNumber: CheckpointNumber,
|
|
771
956
|
feeAssetPriceModifier: bigint,
|
|
772
|
-
|
|
957
|
+
lastBlockProposal: BlockProposal | undefined,
|
|
773
958
|
proposerAddress: EthAddress | undefined,
|
|
774
959
|
options: CheckpointProposalOptions = {},
|
|
775
960
|
): Promise<CheckpointProposal> {
|
|
@@ -790,12 +975,20 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
790
975
|
const newProposal = await this.validationService.createCheckpointProposal(
|
|
791
976
|
checkpointHeader,
|
|
792
977
|
archive,
|
|
978
|
+
checkpointNumber,
|
|
793
979
|
feeAssetPriceModifier,
|
|
794
|
-
|
|
980
|
+
lastBlockProposal,
|
|
795
981
|
proposerAddress,
|
|
796
982
|
options,
|
|
797
983
|
);
|
|
798
984
|
this.lastProposedCheckpoint = newProposal;
|
|
985
|
+
// Self-record this slot's outcome on the re-execution tracker. Proposers don't run their
|
|
986
|
+
// own proposals through `handleCheckpointProposal`, so without this call the proposer's
|
|
987
|
+
// sentinel would see no outcome for slots it proposed and would mis-attribute itself as
|
|
988
|
+
// inactive. We pass the locally-computed `archive` (not `newProposal.archive`, which may
|
|
989
|
+
// be intentionally corrupted under test-only flags); from the proposer's local-view
|
|
990
|
+
// perspective the work it just completed is valid by definition.
|
|
991
|
+
this.proposalHandler.recordOwnCheckpointProposalAsValid(checkpointHeader.slotNumber, archive, checkpointNumber);
|
|
799
992
|
return newProposal;
|
|
800
993
|
}
|
|
801
994
|
|
|
@@ -807,16 +1000,24 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
807
1000
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
808
1001
|
proposer: EthAddress,
|
|
809
1002
|
slot: SlotNumber,
|
|
810
|
-
|
|
1003
|
+
checkpointNumber: CheckpointNumber,
|
|
811
1004
|
): Promise<Signature> {
|
|
812
|
-
return await this.validationService.signAttestationsAndSigners(
|
|
1005
|
+
return await this.validationService.signAttestationsAndSigners(
|
|
1006
|
+
attestationsAndSigners,
|
|
1007
|
+
proposer,
|
|
1008
|
+
slot,
|
|
1009
|
+
checkpointNumber,
|
|
1010
|
+
);
|
|
813
1011
|
}
|
|
814
1012
|
|
|
815
|
-
async collectOwnAttestations(
|
|
1013
|
+
async collectOwnAttestations(
|
|
1014
|
+
proposal: CheckpointProposal,
|
|
1015
|
+
checkpointNumber: CheckpointNumber,
|
|
1016
|
+
): Promise<CheckpointAttestation[]> {
|
|
816
1017
|
const slot = proposal.slotNumber;
|
|
817
1018
|
const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
|
|
818
1019
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
|
|
819
|
-
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
1020
|
+
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee, checkpointNumber);
|
|
820
1021
|
|
|
821
1022
|
if (!attestations) {
|
|
822
1023
|
return [];
|
|
@@ -835,6 +1036,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
835
1036
|
proposal: CheckpointProposal,
|
|
836
1037
|
required: number,
|
|
837
1038
|
deadline: Date,
|
|
1039
|
+
checkpointNumber: CheckpointNumber,
|
|
838
1040
|
): Promise<CheckpointAttestation[]> {
|
|
839
1041
|
// Wait and poll the p2pClient's attestation pool for this checkpoint until we have enough attestations
|
|
840
1042
|
const slot = proposal.slotNumber;
|
|
@@ -847,33 +1049,23 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
847
1049
|
throw new AttestationTimeoutError(0, required, slot);
|
|
848
1050
|
}
|
|
849
1051
|
|
|
850
|
-
await this.collectOwnAttestations(proposal);
|
|
1052
|
+
await this.collectOwnAttestations(proposal, checkpointNumber);
|
|
851
1053
|
|
|
852
|
-
const
|
|
1054
|
+
const proposalPayloadHash = proposal.getPayloadHash();
|
|
853
1055
|
const myAddresses = this.getValidatorAddresses();
|
|
854
1056
|
|
|
855
1057
|
let attestations: CheckpointAttestation[] = [];
|
|
856
1058
|
while (true) {
|
|
857
|
-
//
|
|
858
|
-
//
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
if (!attestation.archive.equals(proposal.archive)) {
|
|
862
|
-
this.log.warn(
|
|
863
|
-
`Received attestation for slot ${slot} with mismatched archive from ${attestation.getSender()?.toString()}`,
|
|
864
|
-
{ attestationArchive: attestation.archive.toString(), proposalArchive: proposal.archive.toString() },
|
|
865
|
-
);
|
|
866
|
-
return false;
|
|
867
|
-
}
|
|
868
|
-
return true;
|
|
869
|
-
},
|
|
870
|
-
);
|
|
1059
|
+
// The pool already filters by proposal payload hash; if any attestation slips through with a
|
|
1060
|
+
// mismatched payload hash, drop it defensively. Equivocations are emitted as separate slash
|
|
1061
|
+
// events from libp2p_service.
|
|
1062
|
+
const collectedAttestations = await this.p2pClient.getCheckpointAttestationsForSlot(slot, proposalPayloadHash);
|
|
871
1063
|
|
|
872
1064
|
// Log new attestations we collected
|
|
873
1065
|
const oldSenders = attestations.map(attestation => attestation.getSender());
|
|
874
1066
|
for (const collected of collectedAttestations) {
|
|
875
1067
|
const collectedSender = collected.getSender();
|
|
876
|
-
// Skip attestations with invalid signatures
|
|
1068
|
+
// Skip attestations with invalid signatures. Should not happen as we don't add invalid attestations to our pool.
|
|
877
1069
|
if (!collectedSender) {
|
|
878
1070
|
this.log.warn(`Skipping attestation with invalid signature for slot ${slot}`);
|
|
879
1071
|
continue;
|