@aztec/validator-client 0.0.1-commit.1bea0213 → 0.0.1-commit.21ecf947b
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 +3 -4
- package/dest/block_proposal_handler.d.ts.map +1 -1
- package/dest/block_proposal_handler.js +5 -4
- package/dest/checkpoint_builder.d.ts +10 -11
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +42 -27
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +6 -3
- 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/metrics.d.ts +4 -3
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +34 -5
- package/dest/tx_validator/tx_validator_factory.d.ts +4 -3
- package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
- package/dest/tx_validator/tx_validator_factory.js +16 -16
- package/dest/validator.d.ts +29 -11
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +144 -40
- package/package.json +19 -17
- package/src/block_proposal_handler.ts +8 -6
- package/src/checkpoint_builder.ts +62 -25
- package/src/config.ts +6 -3
- package/src/duties/validation_service.ts +9 -2
- package/src/key_store/ha_key_store.ts +2 -2
- package/src/metrics.ts +45 -6
- package/src/tx_validator/tx_validator_factory.ts +51 -32
- package/src/validator.ts +192 -50
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,27 +19,28 @@ 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
|
|
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,
|
|
30
|
+
ITxProvider,
|
|
29
31
|
Validator,
|
|
30
32
|
ValidatorClientFullConfig,
|
|
31
33
|
WorldStateSynchronizer,
|
|
32
34
|
} from '@aztec/stdlib/interfaces/server';
|
|
33
|
-
import type
|
|
34
|
-
import
|
|
35
|
-
BlockProposal,
|
|
36
|
-
BlockProposalOptions,
|
|
37
|
-
CheckpointAttestation,
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
import { type L1ToL2MessageSource, accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
|
|
36
|
+
import {
|
|
37
|
+
type BlockProposal,
|
|
38
|
+
type BlockProposalOptions,
|
|
39
|
+
type CheckpointAttestation,
|
|
40
|
+
CheckpointProposal,
|
|
41
|
+
type CheckpointProposalCore,
|
|
42
|
+
type CheckpointProposalOptions,
|
|
40
43
|
} from '@aztec/stdlib/p2p';
|
|
41
|
-
import { CheckpointProposal } from '@aztec/stdlib/p2p';
|
|
42
44
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
43
45
|
import type { BlockHeader, CheckpointGlobalVariables, Tx } from '@aztec/stdlib/tx';
|
|
44
46
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
@@ -79,18 +81,19 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
79
81
|
// Whether it has already registered handlers on the p2p client
|
|
80
82
|
private hasRegisteredHandlers = false;
|
|
81
83
|
|
|
82
|
-
|
|
83
|
-
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;
|
|
84
89
|
|
|
85
90
|
private lastEpochForCommitteeUpdateLoop: EpochNumber | undefined;
|
|
86
91
|
private epochCacheUpdateLoop: RunningPromise;
|
|
87
92
|
|
|
88
93
|
private proposersOfInvalidBlocks: Set<string> = new Set();
|
|
89
94
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
|
|
93
|
-
private validatedBlockSlots: Set<SlotNumber> = new Set();
|
|
95
|
+
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
|
|
96
|
+
private lastAttestedProposal?: CheckpointProposalCore;
|
|
94
97
|
|
|
95
98
|
protected constructor(
|
|
96
99
|
private keyStore: ExtendedValidatorKeyStore,
|
|
@@ -184,7 +187,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
184
187
|
p2pClient: P2P,
|
|
185
188
|
blockSource: L2BlockSource & L2BlockSink,
|
|
186
189
|
l1ToL2MessageSource: L1ToL2MessageSource,
|
|
187
|
-
txProvider:
|
|
190
|
+
txProvider: ITxProvider,
|
|
188
191
|
keyStoreManager: KeystoreManager,
|
|
189
192
|
blobClient: BlobClientInterface,
|
|
190
193
|
dateProvider: DateProvider = new DateProvider(),
|
|
@@ -313,6 +316,16 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
313
316
|
): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
|
|
314
317
|
this.p2pClient.registerCheckpointProposalHandler(checkpointHandler);
|
|
315
318
|
|
|
319
|
+
// Duplicate proposal handler - triggers slashing for equivocation
|
|
320
|
+
this.p2pClient.registerDuplicateProposalCallback((info: DuplicateProposalInfo) => {
|
|
321
|
+
this.handleDuplicateProposal(info);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Duplicate attestation handler - triggers slashing for attestation equivocation
|
|
325
|
+
this.p2pClient.registerDuplicateAttestationCallback((info: DuplicateAttestationInfo) => {
|
|
326
|
+
this.handleDuplicateAttestation(info);
|
|
327
|
+
});
|
|
328
|
+
|
|
316
329
|
const myAddresses = this.getValidatorAddresses();
|
|
317
330
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
318
331
|
|
|
@@ -340,6 +353,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
340
353
|
return false;
|
|
341
354
|
}
|
|
342
355
|
|
|
356
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
357
|
+
if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
358
|
+
this.log.warn(`Ignoring block proposal from self for slot ${slotNumber}`, {
|
|
359
|
+
proposer: proposer.toString(),
|
|
360
|
+
slotNumber,
|
|
361
|
+
});
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
|
|
343
365
|
// Check if we're in the committee (for metrics purposes)
|
|
344
366
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
345
367
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -413,10 +435,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
413
435
|
return false;
|
|
414
436
|
}
|
|
415
437
|
|
|
416
|
-
// TODO(palla/mbps): Remove this once checkpoint validation is stable.
|
|
417
|
-
// Track that we successfully validated a block for this slot, so we can attest to checkpoint proposals for it.
|
|
418
|
-
this.validatedBlockSlots.add(slotNumber);
|
|
419
|
-
|
|
420
438
|
return true;
|
|
421
439
|
}
|
|
422
440
|
|
|
@@ -445,6 +463,23 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
445
463
|
return undefined;
|
|
446
464
|
}
|
|
447
465
|
|
|
466
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
467
|
+
if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
468
|
+
this.log.warn(`Ignoring block proposal from self for slot ${slotNumber}`, {
|
|
469
|
+
proposer: proposer.toString(),
|
|
470
|
+
slotNumber,
|
|
471
|
+
});
|
|
472
|
+
return undefined;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Validate fee asset price modifier is within allowed range
|
|
476
|
+
if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
|
|
477
|
+
this.log.warn(
|
|
478
|
+
`Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${slotNumber}`,
|
|
479
|
+
);
|
|
480
|
+
return undefined;
|
|
481
|
+
}
|
|
482
|
+
|
|
448
483
|
// Check that I have any address in current committee before attesting
|
|
449
484
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
450
485
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -461,17 +496,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
461
496
|
fishermanMode: this.config.fishermanMode || false,
|
|
462
497
|
});
|
|
463
498
|
|
|
464
|
-
// TODO(palla/mbps): Remove this once checkpoint validation is stable.
|
|
465
|
-
// Check that we have successfully validated a block for this slot before attesting to the checkpoint.
|
|
466
|
-
if (!this.validatedBlockSlots.has(slotNumber)) {
|
|
467
|
-
this.log.warn(`No validated block found for slot ${slotNumber}, refusing to attest to checkpoint`, proposalInfo);
|
|
468
|
-
return undefined;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
499
|
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
this.log.verbose(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
|
|
500
|
+
if (this.config.skipCheckpointProposalValidation) {
|
|
501
|
+
this.log.warn(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
|
|
475
502
|
} else {
|
|
476
503
|
const validationResult = await this.validateCheckpointProposal(proposal, proposalInfo);
|
|
477
504
|
if (!validationResult.isValid) {
|
|
@@ -526,15 +553,45 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
526
553
|
return undefined;
|
|
527
554
|
}
|
|
528
555
|
|
|
529
|
-
return this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
556
|
+
return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Checks if we should attest to a slot based on equivocation prevention rules.
|
|
561
|
+
* @returns true if we should attest, false if we should skip
|
|
562
|
+
*/
|
|
563
|
+
private shouldAttestToSlot(slotNumber: SlotNumber): boolean {
|
|
564
|
+
// If attestToEquivocatedProposals is true, always allow
|
|
565
|
+
if (this.config.attestToEquivocatedProposals) {
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Check if incoming slot is strictly greater than last attested
|
|
570
|
+
if (this.lastAttestedProposal && slotNumber <= this.lastAttestedProposal.slotNumber) {
|
|
571
|
+
this.log.warn(
|
|
572
|
+
`Refusing to process a proposal for slot ${slotNumber} given we already attested to a proposal for slot ${this.lastAttestedProposal.slotNumber}`,
|
|
573
|
+
);
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return true;
|
|
530
578
|
}
|
|
531
579
|
|
|
532
580
|
private async createCheckpointAttestationsFromProposal(
|
|
533
581
|
proposal: CheckpointProposalCore,
|
|
534
582
|
attestors: EthAddress[] = [],
|
|
535
|
-
): Promise<CheckpointAttestation[]> {
|
|
583
|
+
): Promise<CheckpointAttestation[] | undefined> {
|
|
584
|
+
// Equivocation check: must happen right before signing to minimize the race window
|
|
585
|
+
if (!this.shouldAttestToSlot(proposal.slotNumber)) {
|
|
586
|
+
return undefined;
|
|
587
|
+
}
|
|
588
|
+
|
|
536
589
|
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
537
|
-
|
|
590
|
+
|
|
591
|
+
// Track the proposal we attested to (to prevent equivocation)
|
|
592
|
+
this.lastAttestedProposal = proposal;
|
|
593
|
+
|
|
594
|
+
await this.p2pClient.addOwnCheckpointAttestations(attestations);
|
|
538
595
|
return attestations;
|
|
539
596
|
}
|
|
540
597
|
|
|
@@ -547,7 +604,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
547
604
|
proposalInfo: LogData,
|
|
548
605
|
): Promise<{ isValid: true } | { isValid: false; reason: string }> {
|
|
549
606
|
const slot = proposal.slotNumber;
|
|
550
|
-
|
|
607
|
+
|
|
608
|
+
// Timeout block syncing at the start of the next slot
|
|
609
|
+
const config = this.checkpointsBuilder.getConfig();
|
|
610
|
+
const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
|
|
611
|
+
const timeoutSeconds = Math.max(1, nextSlotTimestampSeconds - Math.floor(this.dateProvider.now() / 1000));
|
|
551
612
|
|
|
552
613
|
// Wait for last block to sync by archive
|
|
553
614
|
let lastBlockHeader: BlockHeader | undefined;
|
|
@@ -613,10 +674,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
613
674
|
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(
|
|
614
675
|
checkpointNumber,
|
|
615
676
|
constants,
|
|
677
|
+
proposal.feeAssetPriceModifier,
|
|
616
678
|
l1ToL2Messages,
|
|
617
679
|
previousCheckpointOutHashes,
|
|
618
680
|
fork,
|
|
619
681
|
blocks,
|
|
682
|
+
this.log.getBindings(),
|
|
620
683
|
);
|
|
621
684
|
|
|
622
685
|
// Complete the checkpoint to get computed values
|
|
@@ -642,13 +705,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
642
705
|
return { isValid: false, reason: 'archive_mismatch' };
|
|
643
706
|
}
|
|
644
707
|
|
|
645
|
-
// Check that the accumulated out hash matches the value in the proposal.
|
|
646
|
-
|
|
647
|
-
const
|
|
648
|
-
|
|
708
|
+
// Check that the accumulated epoch out hash matches the value in the proposal.
|
|
709
|
+
// The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
|
|
710
|
+
const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
|
|
711
|
+
const computedEpochOutHash = accumulateCheckpointOutHashes([...previousCheckpointOutHashes, checkpointOutHash]);
|
|
712
|
+
const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
|
|
713
|
+
if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
|
|
649
714
|
this.log.warn(`Epoch out hash mismatch`, {
|
|
650
|
-
|
|
651
|
-
|
|
715
|
+
proposalEpochOutHash: proposalEpochOutHash.toString(),
|
|
716
|
+
computedEpochOutHash: computedEpochOutHash.toString(),
|
|
717
|
+
checkpointOutHash: checkpointOutHash.toString(),
|
|
718
|
+
previousCheckpointOutHashes: previousCheckpointOutHashes.map(h => h.toString()),
|
|
652
719
|
...proposalInfo,
|
|
653
720
|
});
|
|
654
721
|
return { isValid: false, reason: 'out_hash_mismatch' };
|
|
@@ -732,6 +799,52 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
732
799
|
]);
|
|
733
800
|
}
|
|
734
801
|
|
|
802
|
+
/**
|
|
803
|
+
* Handle detection of a duplicate proposal (equivocation).
|
|
804
|
+
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
805
|
+
*/
|
|
806
|
+
private handleDuplicateProposal(info: DuplicateProposalInfo): void {
|
|
807
|
+
const { slot, proposer, type } = info;
|
|
808
|
+
|
|
809
|
+
this.log.warn(`Triggering slash event for duplicate ${type} proposal from ${proposer.toString()} at slot ${slot}`, {
|
|
810
|
+
proposer: proposer.toString(),
|
|
811
|
+
slot,
|
|
812
|
+
type,
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
// Emit slash event
|
|
816
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
817
|
+
{
|
|
818
|
+
validator: proposer,
|
|
819
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
820
|
+
offenseType: OffenseType.DUPLICATE_PROPOSAL,
|
|
821
|
+
epochOrSlot: BigInt(slot),
|
|
822
|
+
},
|
|
823
|
+
]);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Handle detection of a duplicate attestation (equivocation).
|
|
828
|
+
* Emits a slash event when an attester signs attestations for different proposals at the same slot.
|
|
829
|
+
*/
|
|
830
|
+
private handleDuplicateAttestation(info: DuplicateAttestationInfo): void {
|
|
831
|
+
const { slot, attester } = info;
|
|
832
|
+
|
|
833
|
+
this.log.warn(`Triggering slash event for duplicate attestation from ${attester.toString()} at slot ${slot}`, {
|
|
834
|
+
attester: attester.toString(),
|
|
835
|
+
slot,
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
839
|
+
{
|
|
840
|
+
validator: attester,
|
|
841
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
842
|
+
offenseType: OffenseType.DUPLICATE_ATTESTATION,
|
|
843
|
+
epochOrSlot: BigInt(slot),
|
|
844
|
+
},
|
|
845
|
+
]);
|
|
846
|
+
}
|
|
847
|
+
|
|
735
848
|
async createBlockProposal(
|
|
736
849
|
blockHeader: BlockHeader,
|
|
737
850
|
indexWithinCheckpoint: IndexWithinCheckpoint,
|
|
@@ -739,13 +852,21 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
739
852
|
archive: Fr,
|
|
740
853
|
txs: Tx[],
|
|
741
854
|
proposerAddress: EthAddress | undefined,
|
|
742
|
-
options: BlockProposalOptions,
|
|
855
|
+
options: BlockProposalOptions = {},
|
|
743
856
|
): Promise<BlockProposal> {
|
|
744
|
-
//
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
857
|
+
// Validate that we're not creating a proposal for an older or equal position
|
|
858
|
+
if (this.lastProposedBlock) {
|
|
859
|
+
const lastSlot = this.lastProposedBlock.slotNumber;
|
|
860
|
+
const lastIndex = this.lastProposedBlock.indexWithinCheckpoint;
|
|
861
|
+
const newSlot = blockHeader.globalVariables.slotNumber;
|
|
862
|
+
|
|
863
|
+
if (newSlot < lastSlot || (newSlot === lastSlot && indexWithinCheckpoint <= lastIndex)) {
|
|
864
|
+
throw new Error(
|
|
865
|
+
`Cannot create block proposal for slot ${newSlot} index ${indexWithinCheckpoint}: ` +
|
|
866
|
+
`already proposed block for slot ${lastSlot} index ${lastIndex}`,
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
749
870
|
|
|
750
871
|
this.log.info(
|
|
751
872
|
`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`,
|
|
@@ -762,25 +883,42 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
762
883
|
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
|
|
763
884
|
},
|
|
764
885
|
);
|
|
765
|
-
this.
|
|
886
|
+
this.lastProposedBlock = newProposal;
|
|
766
887
|
return newProposal;
|
|
767
888
|
}
|
|
768
889
|
|
|
769
890
|
async createCheckpointProposal(
|
|
770
891
|
checkpointHeader: CheckpointHeader,
|
|
771
892
|
archive: Fr,
|
|
893
|
+
feeAssetPriceModifier: bigint,
|
|
772
894
|
lastBlockInfo: CreateCheckpointProposalLastBlockData | undefined,
|
|
773
895
|
proposerAddress: EthAddress | undefined,
|
|
774
|
-
options: CheckpointProposalOptions,
|
|
896
|
+
options: CheckpointProposalOptions = {},
|
|
775
897
|
): Promise<CheckpointProposal> {
|
|
898
|
+
// Validate that we're not creating a proposal for an older or equal slot
|
|
899
|
+
if (this.lastProposedCheckpoint) {
|
|
900
|
+
const lastSlot = this.lastProposedCheckpoint.slotNumber;
|
|
901
|
+
const newSlot = checkpointHeader.slotNumber;
|
|
902
|
+
|
|
903
|
+
if (newSlot <= lastSlot) {
|
|
904
|
+
throw new Error(
|
|
905
|
+
`Cannot create checkpoint proposal for slot ${newSlot}: ` +
|
|
906
|
+
`already proposed checkpoint for slot ${lastSlot}`,
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
776
911
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
777
|
-
|
|
912
|
+
const newProposal = await this.validationService.createCheckpointProposal(
|
|
778
913
|
checkpointHeader,
|
|
779
914
|
archive,
|
|
915
|
+
feeAssetPriceModifier,
|
|
780
916
|
lastBlockInfo,
|
|
781
917
|
proposerAddress,
|
|
782
918
|
options,
|
|
783
919
|
);
|
|
920
|
+
this.lastProposedCheckpoint = newProposal;
|
|
921
|
+
return newProposal;
|
|
784
922
|
}
|
|
785
923
|
|
|
786
924
|
async broadcastBlockProposal(proposal: BlockProposal): Promise<void> {
|
|
@@ -802,6 +940,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
802
940
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
|
|
803
941
|
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
804
942
|
|
|
943
|
+
if (!attestations) {
|
|
944
|
+
return [];
|
|
945
|
+
}
|
|
946
|
+
|
|
805
947
|
// We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
|
|
806
948
|
// other nodes can see that our validators did attest to this block proposal, and do not slash us
|
|
807
949
|
// due to inactivity for missed attestations.
|