@aztec/validator-client 0.0.1-commit.c80b6263 → 0.0.1-commit.cd76b27

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/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 type {
36
- BlockProposal,
37
- BlockProposalOptions,
38
- CheckpointAttestation,
39
- CheckpointProposalCore,
40
- CheckpointProposalOptions,
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
- // Used to check if we are sending the same proposal twice
84
- private previousProposal?: BlockProposal;
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
- let validatorKeyStore: ExtendedValidatorKeyStore = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
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
- validatorKeyStore = new HAKeyStore(validatorKeyStore, signer);
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
- await this.p2pClient.addCheckpointAttestations(attestations);
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
- const timeoutSeconds = 10; // TODO(palla/mbps): This should map to the timetable settings
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
- // Compute the previous checkpoint out hashes for the epoch.
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 previousCheckpoints = (await this.blockSource.getCheckpointsForEpoch(epoch))
587
- .filter(b => b.number < checkpointNumber)
588
- .sort((a, b) => a.number - b.number);
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
- private async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
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
- // TODO(palla/mbps): Prevent double proposals properly
734
- // if (this.previousProposal?.slotNumber === blockHeader.globalVariables.slotNumber) {
735
- // this.log.verbose(`Already made a proposal for the same slot, skipping proposal`);
736
- // return Promise.resolve(undefined);
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.previousProposal = newProposal;
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
- return await this.validationService.createCheckpointProposal(
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.