@aztec/validator-client 0.0.1-commit.c80b6263 → 0.0.1-commit.cf93bcc56
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 +21 -18
- package/dest/block_proposal_handler.d.ts +2 -2
- package/dest/block_proposal_handler.d.ts.map +1 -1
- package/dest/block_proposal_handler.js +20 -34
- package/dest/checkpoint_builder.d.ts +5 -9
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +22 -14
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +4 -0
- package/dest/duties/validation_service.d.ts +2 -2
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +3 -3
- package/dest/key_store/ha_key_store.d.ts +1 -1
- package/dest/key_store/ha_key_store.d.ts.map +1 -1
- package/dest/key_store/ha_key_store.js +2 -2
- package/dest/validator.d.ts +33 -8
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +161 -27
- package/package.json +19 -19
- package/src/block_proposal_handler.ts +28 -48
- package/src/checkpoint_builder.ts +23 -12
- package/src/config.ts +4 -0
- package/src/duties/validation_service.ts +9 -2
- package/src/key_store/ha_key_store.ts +2 -2
- package/src/validator.ts +218 -34
package/src/validator.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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 { validateFeeAssetPriceModifier } from '@aztec/ethereum/contracts';
|
|
4
5
|
import {
|
|
5
6
|
BlockNumber,
|
|
6
7
|
CheckpointNumber,
|
|
@@ -18,12 +19,12 @@ import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
|
18
19
|
import { sleep } from '@aztec/foundation/sleep';
|
|
19
20
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
20
21
|
import type { KeystoreManager } from '@aztec/node-keystore';
|
|
21
|
-
import type { P2P, PeerId } from '@aztec/p2p';
|
|
22
|
+
import type { DuplicateAttestationInfo, DuplicateProposalInfo, P2P, PeerId } from '@aztec/p2p';
|
|
22
23
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
23
24
|
import { OffenseType, WANT_TO_SLASH_EVENT, type Watcher, type WatcherEmitter } from '@aztec/slasher';
|
|
24
25
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
25
26
|
import type { CommitteeAttestationsAndSigners, L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
26
|
-
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
27
|
+
import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
27
28
|
import type {
|
|
28
29
|
CreateCheckpointProposalLastBlockData,
|
|
29
30
|
ITxProvider,
|
|
@@ -32,20 +33,21 @@ import type {
|
|
|
32
33
|
WorldStateSynchronizer,
|
|
33
34
|
} from '@aztec/stdlib/interfaces/server';
|
|
34
35
|
import { type L1ToL2MessageSource, accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
|
|
35
|
-
import
|
|
36
|
-
BlockProposal,
|
|
37
|
-
BlockProposalOptions,
|
|
38
|
-
CheckpointAttestation,
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
import {
|
|
37
|
+
type BlockProposal,
|
|
38
|
+
type BlockProposalOptions,
|
|
39
|
+
type CheckpointAttestation,
|
|
40
|
+
CheckpointProposal,
|
|
41
|
+
type CheckpointProposalCore,
|
|
42
|
+
type CheckpointProposalOptions,
|
|
41
43
|
} from '@aztec/stdlib/p2p';
|
|
42
|
-
import { CheckpointProposal } from '@aztec/stdlib/p2p';
|
|
43
44
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
44
45
|
import type { BlockHeader, CheckpointGlobalVariables, Tx } from '@aztec/stdlib/tx';
|
|
45
46
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
46
47
|
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
47
48
|
import { createHASigner } from '@aztec/validator-ha-signer/factory';
|
|
48
49
|
import { DutyType, type SigningContext } from '@aztec/validator-ha-signer/types';
|
|
50
|
+
import type { ValidatorHASigner } from '@aztec/validator-ha-signer/validator-ha-signer';
|
|
49
51
|
|
|
50
52
|
import { EventEmitter } from 'events';
|
|
51
53
|
import type { TypedDataDefinition } from 'viem';
|
|
@@ -76,18 +78,23 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
76
78
|
private validationService: ValidationService;
|
|
77
79
|
private metrics: ValidatorMetrics;
|
|
78
80
|
private log: Logger;
|
|
79
|
-
|
|
80
81
|
// Whether it has already registered handlers on the p2p client
|
|
81
82
|
private hasRegisteredHandlers = false;
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
private
|
|
84
|
+
/** Tracks the last block proposal we created, to detect duplicate proposal attempts. */
|
|
85
|
+
private lastProposedBlock?: BlockProposal;
|
|
86
|
+
|
|
87
|
+
/** Tracks the last checkpoint proposal we created. */
|
|
88
|
+
private lastProposedCheckpoint?: CheckpointProposal;
|
|
85
89
|
|
|
86
90
|
private lastEpochForCommitteeUpdateLoop: EpochNumber | undefined;
|
|
87
91
|
private epochCacheUpdateLoop: RunningPromise;
|
|
88
92
|
|
|
89
93
|
private proposersOfInvalidBlocks: Set<string> = new Set();
|
|
90
94
|
|
|
95
|
+
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
|
|
96
|
+
private lastAttestedProposal?: CheckpointProposalCore;
|
|
97
|
+
|
|
91
98
|
protected constructor(
|
|
92
99
|
private keyStore: ExtendedValidatorKeyStore,
|
|
93
100
|
private epochCache: EpochCache,
|
|
@@ -99,6 +106,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
99
106
|
private l1ToL2MessageSource: L1ToL2MessageSource,
|
|
100
107
|
private config: ValidatorClientFullConfig,
|
|
101
108
|
private blobClient: BlobClientInterface,
|
|
109
|
+
private haSigner: ValidatorHASigner | undefined,
|
|
102
110
|
private dateProvider: DateProvider = new DateProvider(),
|
|
103
111
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
104
112
|
log = createLogger('validator'),
|
|
@@ -204,7 +212,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
204
212
|
telemetry,
|
|
205
213
|
);
|
|
206
214
|
|
|
207
|
-
|
|
215
|
+
const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
216
|
+
let validatorKeyStore: ExtendedValidatorKeyStore = nodeKeystoreAdapter;
|
|
217
|
+
let haSigner: ValidatorHASigner | undefined;
|
|
208
218
|
if (config.haSigningEnabled) {
|
|
209
219
|
// If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
|
|
210
220
|
const haConfig = {
|
|
@@ -212,7 +222,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
212
222
|
maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000,
|
|
213
223
|
};
|
|
214
224
|
const { signer } = await createHASigner(haConfig);
|
|
215
|
-
|
|
225
|
+
haSigner = signer;
|
|
226
|
+
validatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, signer);
|
|
216
227
|
}
|
|
217
228
|
|
|
218
229
|
const validator = new ValidatorClient(
|
|
@@ -226,6 +237,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
226
237
|
l1ToL2MessageSource,
|
|
227
238
|
config,
|
|
228
239
|
blobClient,
|
|
240
|
+
haSigner,
|
|
229
241
|
dateProvider,
|
|
230
242
|
telemetry,
|
|
231
243
|
);
|
|
@@ -263,6 +275,28 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
263
275
|
this.config = { ...this.config, ...config };
|
|
264
276
|
}
|
|
265
277
|
|
|
278
|
+
public reloadKeystore(newManager: KeystoreManager): void {
|
|
279
|
+
if (this.config.haSigningEnabled && !this.haSigner) {
|
|
280
|
+
this.log.warn(
|
|
281
|
+
'HA signing is enabled in config but was not initialized at startup. ' +
|
|
282
|
+
'Restart the node to enable HA signing.',
|
|
283
|
+
);
|
|
284
|
+
} else if (!this.config.haSigningEnabled && this.haSigner) {
|
|
285
|
+
this.log.warn(
|
|
286
|
+
'HA signing was disabled via config update but the HA signer is still active. ' +
|
|
287
|
+
'Restart the node to fully disable HA signing.',
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
|
|
292
|
+
if (this.haSigner) {
|
|
293
|
+
this.keyStore = new HAKeyStore(newAdapter, this.haSigner);
|
|
294
|
+
} else {
|
|
295
|
+
this.keyStore = newAdapter;
|
|
296
|
+
}
|
|
297
|
+
this.validationService = new ValidationService(this.keyStore, this.log.createChild('validation-service'));
|
|
298
|
+
}
|
|
299
|
+
|
|
266
300
|
public async start() {
|
|
267
301
|
if (this.epochCacheUpdateLoop.isRunning()) {
|
|
268
302
|
this.log.warn(`Validator client already started`);
|
|
@@ -309,6 +343,16 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
309
343
|
): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
|
|
310
344
|
this.p2pClient.registerCheckpointProposalHandler(checkpointHandler);
|
|
311
345
|
|
|
346
|
+
// Duplicate proposal handler - triggers slashing for equivocation
|
|
347
|
+
this.p2pClient.registerDuplicateProposalCallback((info: DuplicateProposalInfo) => {
|
|
348
|
+
this.handleDuplicateProposal(info);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Duplicate attestation handler - triggers slashing for attestation equivocation
|
|
352
|
+
this.p2pClient.registerDuplicateAttestationCallback((info: DuplicateAttestationInfo) => {
|
|
353
|
+
this.handleDuplicateAttestation(info);
|
|
354
|
+
});
|
|
355
|
+
|
|
312
356
|
const myAddresses = this.getValidatorAddresses();
|
|
313
357
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
314
358
|
|
|
@@ -336,6 +380,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
336
380
|
return false;
|
|
337
381
|
}
|
|
338
382
|
|
|
383
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
384
|
+
if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
385
|
+
this.log.warn(`Ignoring block proposal from self for slot ${slotNumber}`, {
|
|
386
|
+
proposer: proposer.toString(),
|
|
387
|
+
slotNumber,
|
|
388
|
+
});
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
|
|
339
392
|
// Check if we're in the committee (for metrics purposes)
|
|
340
393
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
341
394
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -437,6 +490,23 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
437
490
|
return undefined;
|
|
438
491
|
}
|
|
439
492
|
|
|
493
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
494
|
+
if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
495
|
+
this.log.warn(`Ignoring block proposal from self for slot ${slotNumber}`, {
|
|
496
|
+
proposer: proposer.toString(),
|
|
497
|
+
slotNumber,
|
|
498
|
+
});
|
|
499
|
+
return undefined;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Validate fee asset price modifier is within allowed range
|
|
503
|
+
if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
|
|
504
|
+
this.log.warn(
|
|
505
|
+
`Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${slotNumber}`,
|
|
506
|
+
);
|
|
507
|
+
return undefined;
|
|
508
|
+
}
|
|
509
|
+
|
|
440
510
|
// Check that I have any address in current committee before attesting
|
|
441
511
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
442
512
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -510,15 +580,45 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
510
580
|
return undefined;
|
|
511
581
|
}
|
|
512
582
|
|
|
513
|
-
return this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
583
|
+
return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Checks if we should attest to a slot based on equivocation prevention rules.
|
|
588
|
+
* @returns true if we should attest, false if we should skip
|
|
589
|
+
*/
|
|
590
|
+
private shouldAttestToSlot(slotNumber: SlotNumber): boolean {
|
|
591
|
+
// If attestToEquivocatedProposals is true, always allow
|
|
592
|
+
if (this.config.attestToEquivocatedProposals) {
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Check if incoming slot is strictly greater than last attested
|
|
597
|
+
if (this.lastAttestedProposal && slotNumber <= this.lastAttestedProposal.slotNumber) {
|
|
598
|
+
this.log.warn(
|
|
599
|
+
`Refusing to process a proposal for slot ${slotNumber} given we already attested to a proposal for slot ${this.lastAttestedProposal.slotNumber}`,
|
|
600
|
+
);
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return true;
|
|
514
605
|
}
|
|
515
606
|
|
|
516
607
|
private async createCheckpointAttestationsFromProposal(
|
|
517
608
|
proposal: CheckpointProposalCore,
|
|
518
609
|
attestors: EthAddress[] = [],
|
|
519
|
-
): Promise<CheckpointAttestation[]> {
|
|
610
|
+
): Promise<CheckpointAttestation[] | undefined> {
|
|
611
|
+
// Equivocation check: must happen right before signing to minimize the race window
|
|
612
|
+
if (!this.shouldAttestToSlot(proposal.slotNumber)) {
|
|
613
|
+
return undefined;
|
|
614
|
+
}
|
|
615
|
+
|
|
520
616
|
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
521
|
-
|
|
617
|
+
|
|
618
|
+
// Track the proposal we attested to (to prevent equivocation)
|
|
619
|
+
this.lastAttestedProposal = proposal;
|
|
620
|
+
|
|
621
|
+
await this.p2pClient.addOwnCheckpointAttestations(attestations);
|
|
522
622
|
return attestations;
|
|
523
623
|
}
|
|
524
624
|
|
|
@@ -531,7 +631,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
531
631
|
proposalInfo: LogData,
|
|
532
632
|
): Promise<{ isValid: true } | { isValid: false; reason: string }> {
|
|
533
633
|
const slot = proposal.slotNumber;
|
|
534
|
-
|
|
634
|
+
|
|
635
|
+
// Timeout block syncing at the start of the next slot
|
|
636
|
+
const config = this.checkpointsBuilder.getConfig();
|
|
637
|
+
const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
|
|
638
|
+
const timeoutSeconds = Math.max(1, nextSlotTimestampSeconds - Math.floor(this.dateProvider.now() / 1000));
|
|
535
639
|
|
|
536
640
|
// Wait for last block to sync by archive
|
|
537
641
|
let lastBlockHeader: BlockHeader | undefined;
|
|
@@ -566,6 +670,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
566
670
|
return { isValid: false, reason: 'no_blocks_for_slot' };
|
|
567
671
|
}
|
|
568
672
|
|
|
673
|
+
// Ensure the last block for this slot matches the archive in the checkpoint proposal
|
|
674
|
+
if (!blocks.at(-1)?.archive.root.equals(proposal.archive)) {
|
|
675
|
+
this.log.warn(`Last block archive mismatch for checkpoint proposal`, proposalInfo);
|
|
676
|
+
return { isValid: false, reason: 'last_block_archive_mismatch' };
|
|
677
|
+
}
|
|
678
|
+
|
|
569
679
|
this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
|
|
570
680
|
...proposalInfo,
|
|
571
681
|
blockNumbers: blocks.map(b => b.number),
|
|
@@ -579,14 +689,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
579
689
|
// Get L1-to-L2 messages for this checkpoint
|
|
580
690
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
581
691
|
|
|
582
|
-
//
|
|
583
|
-
// TODO: There can be a more efficient way to get the previous checkpoint out hashes without having to fetch the
|
|
584
|
-
// actual checkpoints and the blocks/txs in them.
|
|
692
|
+
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
585
693
|
const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
|
|
586
|
-
const
|
|
587
|
-
.filter(
|
|
588
|
-
.
|
|
589
|
-
const previousCheckpointOutHashes = previousCheckpoints.map(c => c.getCheckpointOutHash());
|
|
694
|
+
const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch))
|
|
695
|
+
.filter(c => c.checkpointNumber < checkpointNumber)
|
|
696
|
+
.map(c => c.checkpointOutHash);
|
|
590
697
|
|
|
591
698
|
// Fork world state at the block before the first block
|
|
592
699
|
const parentBlockNumber = BlockNumber(firstBlock.number - 1);
|
|
@@ -597,6 +704,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
597
704
|
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(
|
|
598
705
|
checkpointNumber,
|
|
599
706
|
constants,
|
|
707
|
+
proposal.feeAssetPriceModifier,
|
|
600
708
|
l1ToL2Messages,
|
|
601
709
|
previousCheckpointOutHashes,
|
|
602
710
|
fork,
|
|
@@ -659,6 +767,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
659
767
|
chainId: gv.chainId,
|
|
660
768
|
version: gv.version,
|
|
661
769
|
slotNumber: gv.slotNumber,
|
|
770
|
+
timestamp: gv.timestamp,
|
|
662
771
|
coinbase: gv.coinbase,
|
|
663
772
|
feeRecipient: gv.feeRecipient,
|
|
664
773
|
gasFees: gv.gasFees,
|
|
@@ -668,7 +777,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
668
777
|
/**
|
|
669
778
|
* Uploads blobs for a checkpoint to the filestore (fire and forget).
|
|
670
779
|
*/
|
|
671
|
-
|
|
780
|
+
protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
|
|
672
781
|
try {
|
|
673
782
|
const lastBlockHeader = await this.blockSource.getBlockHeaderByArchive(proposal.archive);
|
|
674
783
|
if (!lastBlockHeader) {
|
|
@@ -683,7 +792,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
683
792
|
}
|
|
684
793
|
|
|
685
794
|
const blobFields = blocks.flatMap(b => b.toBlobFields());
|
|
686
|
-
const blobs: Blob[] = getBlobsPerL1Block(blobFields);
|
|
795
|
+
const blobs: Blob[] = await getBlobsPerL1Block(blobFields);
|
|
687
796
|
await this.blobClient.sendBlobsToFilestore(blobs);
|
|
688
797
|
this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
|
|
689
798
|
...proposalInfo,
|
|
@@ -721,6 +830,52 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
721
830
|
]);
|
|
722
831
|
}
|
|
723
832
|
|
|
833
|
+
/**
|
|
834
|
+
* Handle detection of a duplicate proposal (equivocation).
|
|
835
|
+
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
836
|
+
*/
|
|
837
|
+
private handleDuplicateProposal(info: DuplicateProposalInfo): void {
|
|
838
|
+
const { slot, proposer, type } = info;
|
|
839
|
+
|
|
840
|
+
this.log.warn(`Triggering slash event for duplicate ${type} proposal from ${proposer.toString()} at slot ${slot}`, {
|
|
841
|
+
proposer: proposer.toString(),
|
|
842
|
+
slot,
|
|
843
|
+
type,
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
// Emit slash event
|
|
847
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
848
|
+
{
|
|
849
|
+
validator: proposer,
|
|
850
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
851
|
+
offenseType: OffenseType.DUPLICATE_PROPOSAL,
|
|
852
|
+
epochOrSlot: BigInt(slot),
|
|
853
|
+
},
|
|
854
|
+
]);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Handle detection of a duplicate attestation (equivocation).
|
|
859
|
+
* Emits a slash event when an attester signs attestations for different proposals at the same slot.
|
|
860
|
+
*/
|
|
861
|
+
private handleDuplicateAttestation(info: DuplicateAttestationInfo): void {
|
|
862
|
+
const { slot, attester } = info;
|
|
863
|
+
|
|
864
|
+
this.log.warn(`Triggering slash event for duplicate attestation from ${attester.toString()} at slot ${slot}`, {
|
|
865
|
+
attester: attester.toString(),
|
|
866
|
+
slot,
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
870
|
+
{
|
|
871
|
+
validator: attester,
|
|
872
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
873
|
+
offenseType: OffenseType.DUPLICATE_ATTESTATION,
|
|
874
|
+
epochOrSlot: BigInt(slot),
|
|
875
|
+
},
|
|
876
|
+
]);
|
|
877
|
+
}
|
|
878
|
+
|
|
724
879
|
async createBlockProposal(
|
|
725
880
|
blockHeader: BlockHeader,
|
|
726
881
|
indexWithinCheckpoint: IndexWithinCheckpoint,
|
|
@@ -730,11 +885,19 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
730
885
|
proposerAddress: EthAddress | undefined,
|
|
731
886
|
options: BlockProposalOptions = {},
|
|
732
887
|
): Promise<BlockProposal> {
|
|
733
|
-
//
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
888
|
+
// Validate that we're not creating a proposal for an older or equal position
|
|
889
|
+
if (this.lastProposedBlock) {
|
|
890
|
+
const lastSlot = this.lastProposedBlock.slotNumber;
|
|
891
|
+
const lastIndex = this.lastProposedBlock.indexWithinCheckpoint;
|
|
892
|
+
const newSlot = blockHeader.globalVariables.slotNumber;
|
|
893
|
+
|
|
894
|
+
if (newSlot < lastSlot || (newSlot === lastSlot && indexWithinCheckpoint <= lastIndex)) {
|
|
895
|
+
throw new Error(
|
|
896
|
+
`Cannot create block proposal for slot ${newSlot} index ${indexWithinCheckpoint}: ` +
|
|
897
|
+
`already proposed block for slot ${lastSlot} index ${lastIndex}`,
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
738
901
|
|
|
739
902
|
this.log.info(
|
|
740
903
|
`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`,
|
|
@@ -751,25 +914,42 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
751
914
|
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
|
|
752
915
|
},
|
|
753
916
|
);
|
|
754
|
-
this.
|
|
917
|
+
this.lastProposedBlock = newProposal;
|
|
755
918
|
return newProposal;
|
|
756
919
|
}
|
|
757
920
|
|
|
758
921
|
async createCheckpointProposal(
|
|
759
922
|
checkpointHeader: CheckpointHeader,
|
|
760
923
|
archive: Fr,
|
|
924
|
+
feeAssetPriceModifier: bigint,
|
|
761
925
|
lastBlockInfo: CreateCheckpointProposalLastBlockData | undefined,
|
|
762
926
|
proposerAddress: EthAddress | undefined,
|
|
763
927
|
options: CheckpointProposalOptions = {},
|
|
764
928
|
): Promise<CheckpointProposal> {
|
|
929
|
+
// Validate that we're not creating a proposal for an older or equal slot
|
|
930
|
+
if (this.lastProposedCheckpoint) {
|
|
931
|
+
const lastSlot = this.lastProposedCheckpoint.slotNumber;
|
|
932
|
+
const newSlot = checkpointHeader.slotNumber;
|
|
933
|
+
|
|
934
|
+
if (newSlot <= lastSlot) {
|
|
935
|
+
throw new Error(
|
|
936
|
+
`Cannot create checkpoint proposal for slot ${newSlot}: ` +
|
|
937
|
+
`already proposed checkpoint for slot ${lastSlot}`,
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
765
942
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
766
|
-
|
|
943
|
+
const newProposal = await this.validationService.createCheckpointProposal(
|
|
767
944
|
checkpointHeader,
|
|
768
945
|
archive,
|
|
946
|
+
feeAssetPriceModifier,
|
|
769
947
|
lastBlockInfo,
|
|
770
948
|
proposerAddress,
|
|
771
949
|
options,
|
|
772
950
|
);
|
|
951
|
+
this.lastProposedCheckpoint = newProposal;
|
|
952
|
+
return newProposal;
|
|
773
953
|
}
|
|
774
954
|
|
|
775
955
|
async broadcastBlockProposal(proposal: BlockProposal): Promise<void> {
|
|
@@ -791,6 +971,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
791
971
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
|
|
792
972
|
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
793
973
|
|
|
974
|
+
if (!attestations) {
|
|
975
|
+
return [];
|
|
976
|
+
}
|
|
977
|
+
|
|
794
978
|
// We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
|
|
795
979
|
// other nodes can see that our validators did attest to this block proposal, and do not slash us
|
|
796
980
|
// due to inactivity for missed attestations.
|