@aztec/validator-client 0.0.1-commit.e588bc7e5 → 0.0.1-commit.e5a3663dd

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.
@@ -76,7 +76,9 @@ export type BlockProposalValidationFailureResult = {
76
76
 
77
77
  export type BlockProposalValidationResult = BlockProposalValidationSuccessResult | BlockProposalValidationFailureResult;
78
78
 
79
- export type CheckpointProposalValidationResult = { isValid: true } | { isValid: false; reason: string };
79
+ export type CheckpointProposalValidationResult =
80
+ | { isValid: true; checkpointNumber: CheckpointNumber }
81
+ | { isValid: false; reason: string };
80
82
 
81
83
  type CheckpointComputationResult =
82
84
  | { checkpointNumber: CheckpointNumber; reason?: undefined }
@@ -94,7 +96,7 @@ export class ProposalHandler {
94
96
  };
95
97
 
96
98
  /** Archiver reference for setting proposed checkpoints (pipelining). Set via register(). */
97
- private archiver?: Pick<Archiver, 'setProposedCheckpoint' | 'getL1Constants'>;
99
+ private archiver?: Pick<Archiver, 'addProposedCheckpoint' | 'getL1Constants'>;
98
100
 
99
101
  /** Returns current validator addresses for own-proposal detection. Set via register(). */
100
102
  private getOwnValidatorAddresses?: () => string[];
@@ -130,7 +132,7 @@ export class ProposalHandler {
130
132
  register(
131
133
  p2pClient: P2P,
132
134
  shouldReexecute: boolean,
133
- archiver?: Pick<Archiver, 'setProposedCheckpoint' | 'getL1Constants'>,
135
+ archiver?: Pick<Archiver, 'addProposedCheckpoint' | 'getL1Constants'>,
134
136
  getOwnValidatorAddresses?: () => string[],
135
137
  ): ProposalHandler {
136
138
  this.archiver = archiver;
@@ -175,6 +177,7 @@ export class ProposalHandler {
175
177
  _sender: PeerId,
176
178
  ): Promise<CheckpointAttestation[] | undefined> => {
177
179
  try {
180
+ const pipeliningTimer = new Timer();
178
181
  const proposalInfo: LogData = {
179
182
  slot: proposal.slotNumber,
180
183
  archive: proposal.archive.toString(),
@@ -196,7 +199,10 @@ export class ProposalHandler {
196
199
 
197
200
  const result = await this.handleCheckpointProposal(proposal, proposalInfo);
198
201
  if (result.isValid && this.archiver && this.epochCache.isProposerPipeliningEnabled()) {
199
- await this.setProposedCheckpointFromValidation(proposal);
202
+ const set = await this.setProposedCheckpointFromValidation(proposal);
203
+ if (set) {
204
+ this.metrics?.recordCheckpointProposalToPipelinedStateDuration(pipeliningTimer.ms());
205
+ }
200
206
  }
201
207
  } catch (err) {
202
208
  this.log.warn(`Error handling checkpoint proposal for slot ${proposal.slotNumber}`, { err });
@@ -374,7 +380,6 @@ export class ProposalHandler {
374
380
 
375
381
  private async getParentBlock(proposal: BlockProposal): Promise<'genesis' | BlockData | undefined> {
376
382
  const parentArchive = proposal.blockHeader.lastArchive.root;
377
- const slot = proposal.slotNumber;
378
383
  const config = this.checkpointsBuilder.getConfig();
379
384
  const { genesisArchiveRoot } = await this.blockSource.getGenesisValues();
380
385
 
@@ -382,7 +387,7 @@ export class ProposalHandler {
382
387
  return 'genesis';
383
388
  }
384
389
 
385
- const deadline = this.getReexecutionDeadline(slot, config);
390
+ const deadline = this.getReexecutionDeadline(proposal.slotNumber, config);
386
391
  const currentTime = this.dateProvider.now();
387
392
  const timeoutDurationMs = deadline.getTime() - currentTime;
388
393
 
@@ -531,8 +536,14 @@ export class ProposalHandler {
531
536
  return undefined;
532
537
  }
533
538
 
534
- private getReexecutionDeadline(slot: SlotNumber, config: { l1GenesisTime: bigint; slotDuration: number }): Date {
535
- const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
539
+ private getReexecutionDeadline(
540
+ slotNumber: SlotNumber,
541
+ config: { l1GenesisTime: bigint; slotDuration: number },
542
+ ): Date {
543
+ // Under proposer pipelining, the proposal slot may be ahead of wall clock time.
544
+ // Reexecution budgets should still be bounded by the current slot we are in now.
545
+ const wallclockSlot = slotNumber - this.epochCache.pipeliningOffset();
546
+ const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(wallclockSlot + 1), config));
536
547
  return new Date(nextSlotTimestampSeconds * 1000);
537
548
  }
538
549
 
@@ -545,8 +556,9 @@ export class ProposalHandler {
545
556
  }
546
557
 
547
558
  // Make a quick check before triggering an archiver sync
559
+ // If we are pipelining and have a pending checkpoint number stored, we will allow the block proposal to be for a slot further
548
560
  const syncedSlot = await this.blockSource.getSyncedL2SlotNumber();
549
- if (syncedSlot !== undefined && syncedSlot + 1 >= slot) {
561
+ if (syncedSlot !== undefined && syncedSlot + 1 + this.epochCache.pipeliningOffset() >= slot) {
550
562
  return true;
551
563
  }
552
564
 
@@ -555,8 +567,8 @@ export class ProposalHandler {
555
567
  return await retryUntil(
556
568
  async () => {
557
569
  await this.blockSource.syncImmediate();
558
- const syncedSlot = await this.blockSource.getSyncedL2SlotNumber();
559
- return syncedSlot !== undefined && syncedSlot + 1 >= slot;
570
+ const updatedSyncedSlot = await this.blockSource.getSyncedL2SlotNumber();
571
+ return updatedSyncedSlot !== undefined && updatedSyncedSlot + 1 >= slot;
560
572
  },
561
573
  'wait for block source sync',
562
574
  timeoutMs / 1000,
@@ -815,6 +827,16 @@ export class ProposalHandler {
815
827
  return { isValid: false, reason: 'last_block_archive_mismatch' };
816
828
  }
817
829
 
830
+ const maxBlocksPerCheckpoint = this.config.maxBlocksPerCheckpoint;
831
+ if (maxBlocksPerCheckpoint !== undefined && blocks.length > maxBlocksPerCheckpoint) {
832
+ this.log.warn(`Checkpoint proposal exceeds maxBlocksPerCheckpoint`, {
833
+ ...proposalInfo,
834
+ blocksInProposal: blocks.length,
835
+ maxBlocksPerCheckpoint,
836
+ });
837
+ return { isValid: false, reason: 'too_many_blocks_in_checkpoint' };
838
+ }
839
+
818
840
  this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
819
841
  ...proposalInfo,
820
842
  blockNumbers: blocks.map(b => b.number),
@@ -904,7 +926,7 @@ export class ProposalHandler {
904
926
  }
905
927
 
906
928
  this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
907
- return { isValid: true };
929
+ return { isValid: true, checkpointNumber };
908
930
  }
909
931
 
910
932
  /** Extracts checkpoint global variables from a block. */
@@ -961,19 +983,19 @@ export class ProposalHandler {
961
983
  * Used after successful validation of a foreign proposal.
962
984
  * Does not retry since we already waited for the block during validation.
963
985
  */
964
- private async setProposedCheckpointFromValidation(proposal: CheckpointProposalCore): Promise<void> {
986
+ private async setProposedCheckpointFromValidation(proposal: CheckpointProposalCore): Promise<boolean> {
965
987
  if (!this.archiver) {
966
- return;
988
+ return false;
967
989
  }
968
990
  const blockData = await this.blockSource.getBlockDataByArchive(proposal.archive);
969
991
  if (!blockData) {
970
992
  this.log.debug(`Block data not found for checkpoint proposal archive, cannot set proposed checkpoint`, {
971
993
  archive: proposal.archive.toString(),
972
994
  });
973
- return;
995
+ return false;
974
996
  }
975
997
 
976
- await this.archiver.setProposedCheckpoint({
998
+ await this.archiver.addProposedCheckpoint({
977
999
  header: proposal.checkpointHeader,
978
1000
  checkpointNumber: blockData.checkpointNumber,
979
1001
  startBlock: BlockNumber(blockData.header.getBlockNumber() - blockData.indexWithinCheckpoint),
@@ -981,6 +1003,7 @@ export class ProposalHandler {
981
1003
  totalManaUsed: proposal.checkpointHeader.totalManaUsed.toBigInt(),
982
1004
  feeAssetPriceModifier: proposal.feeAssetPriceModifier,
983
1005
  });
1006
+ return true;
984
1007
  }
985
1008
 
986
1009
  /**
@@ -988,9 +1011,9 @@ export class ProposalHandler {
988
1011
  * Retries fetching block data since the checkpoint proposal often arrives before the last block
989
1012
  * finishes re-execution.
990
1013
  */
991
- private async setProposedCheckpointFromBlocks(proposal: CheckpointProposalCore): Promise<void> {
1014
+ private async setProposedCheckpointFromBlocks(proposal: CheckpointProposalCore): Promise<boolean> {
992
1015
  if (!this.archiver) {
993
- return;
1016
+ return false;
994
1017
  }
995
1018
  let blockData = await this.blockSource.getBlockDataByArchive(proposal.archive);
996
1019
 
@@ -1010,7 +1033,7 @@ export class ProposalHandler {
1010
1033
  }
1011
1034
 
1012
1035
  if (blockData) {
1013
- await this.archiver.setProposedCheckpoint({
1036
+ await this.archiver.addProposedCheckpoint({
1014
1037
  header: proposal.checkpointHeader,
1015
1038
  checkpointNumber: blockData.checkpointNumber,
1016
1039
  startBlock: BlockNumber(blockData.header.getBlockNumber() - blockData.indexWithinCheckpoint),
@@ -1018,10 +1041,12 @@ export class ProposalHandler {
1018
1041
  totalManaUsed: proposal.checkpointHeader.totalManaUsed.toBigInt(),
1019
1042
  feeAssetPriceModifier: proposal.feeAssetPriceModifier,
1020
1043
  });
1044
+ return true;
1021
1045
  } else {
1022
1046
  this.log.debug(`Block data not found for own checkpoint proposal archive, cannot set proposed checkpoint`, {
1023
1047
  archive: proposal.archive.toString(),
1024
1048
  });
1049
+ return false;
1025
1050
  }
1026
1051
  }
1027
1052
  }
package/src/validator.ts CHANGED
@@ -1,13 +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 {
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
7
  import type { Signature } from '@aztec/foundation/eth-signature';
@@ -23,7 +17,6 @@ import type { AztecAddress } from '@aztec/stdlib/aztec-address';
23
17
  import type { CommitteeAttestationsAndSigners, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
24
18
  import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
25
19
  import type {
26
- CreateCheckpointProposalLastBlockData,
27
20
  ITxProvider,
28
21
  Validator,
29
22
  ValidatorClientFullConfig,
@@ -37,6 +30,7 @@ import {
37
30
  CheckpointProposal,
38
31
  type CheckpointProposalCore,
39
32
  type CheckpointProposalOptions,
33
+ type CoordinationSignatureContext,
40
34
  } from '@aztec/stdlib/p2p';
41
35
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
42
36
  import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
@@ -122,7 +116,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
122
116
  this.tracer = telemetry.getTracer('Validator');
123
117
  this.metrics = new ValidatorMetrics(telemetry);
124
118
 
125
- this.validationService = new ValidationService(keyStore, this.log.createChild('validation-service'));
119
+ this.validationService = new ValidationService(
120
+ keyStore,
121
+ this.getSignatureContext(),
122
+ this.log.createChild('validation-service'),
123
+ );
126
124
 
127
125
  // Refresh epoch cache every second to trigger alert if participation in committee changes
128
126
  this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), this.log, 1000);
@@ -203,6 +201,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
203
201
  const blockProposalValidator = new BlockProposalValidator(epochCache, {
204
202
  txsPermitted: !config.disableTransactions,
205
203
  maxTxsPerBlock: config.validateMaxTxsPerBlock,
204
+ maxBlocksPerCheckpoint: config.maxBlocksPerCheckpoint,
205
+ signatureContext: {
206
+ chainId: config.l1ChainId,
207
+ rollupAddress: config.l1Contracts.rollupAddress,
208
+ },
206
209
  });
207
210
  const proposalHandler = new ProposalHandler(
208
211
  checkpointsBuilder,
@@ -217,6 +220,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
217
220
  metrics,
218
221
  dateProvider,
219
222
  telemetry,
223
+ undefined,
220
224
  );
221
225
 
222
226
  const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
@@ -281,6 +285,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
281
285
  return this.keyStore.signTypedDataWithAddress(addr, msg, context);
282
286
  }
283
287
 
288
+ private getSignatureContext(): CoordinationSignatureContext {
289
+ return {
290
+ chainId: this.config.l1ChainId,
291
+ rollupAddress: this.config.l1Contracts.rollupAddress,
292
+ };
293
+ }
294
+
284
295
  public getCoinbaseForAttestor(attestor: EthAddress): EthAddress {
285
296
  return this.keyStore.getCoinbaseAddress(attestor);
286
297
  }
@@ -300,7 +311,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
300
311
  public reloadKeystore(newManager: KeystoreManager): void {
301
312
  const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
302
313
  this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
303
- this.validationService = new ValidationService(this.keyStore, this.log.createChild('validation-service'));
314
+ this.validationService = new ValidationService(
315
+ this.keyStore,
316
+ this.getSignatureContext(),
317
+ this.log.createChild('validation-service'),
318
+ );
304
319
  }
305
320
 
306
321
  public async start() {
@@ -514,14 +529,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
514
529
 
515
530
  // Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set).
516
531
  // Uses the cached result from the all-nodes callback if available (avoids double validation).
532
+ let checkpointNumber: CheckpointNumber;
517
533
  if (this.config.skipCheckpointProposalValidation) {
518
534
  this.log.warn(`Skipping checkpoint proposal validation for slot ${proposalSlotNumber}`, proposalInfo);
535
+ checkpointNumber = CheckpointNumber(0);
519
536
  } else {
520
537
  const validationResult = await this.proposalHandler.handleCheckpointProposal(proposal, proposalInfo);
521
538
  if (!validationResult.isValid) {
522
539
  this.log.warn(`Checkpoint proposal validation failed: ${validationResult.reason}`, proposalInfo);
523
540
  return undefined;
524
541
  }
542
+ checkpointNumber = validationResult.checkpointNumber;
525
543
  }
526
544
 
527
545
  // Check that I have any address in current committee before attesting
@@ -579,7 +597,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
579
597
  return undefined;
580
598
  }
581
599
 
582
- return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
600
+ return await this.createCheckpointAttestationsFromProposal(proposal, attestors, checkpointNumber);
583
601
  }
584
602
 
585
603
  /**
@@ -606,13 +624,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
606
624
  private async createCheckpointAttestationsFromProposal(
607
625
  proposal: CheckpointProposalCore,
608
626
  attestors: EthAddress[] = [],
627
+ checkpointNumber: CheckpointNumber,
609
628
  ): Promise<CheckpointAttestation[] | undefined> {
610
629
  // Equivocation check: must happen right before signing to minimize the race window
611
630
  if (!this.shouldAttestToSlot(proposal.slotNumber)) {
612
631
  return undefined;
613
632
  }
614
633
 
615
- const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
634
+ const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors, checkpointNumber);
616
635
 
617
636
  // Track the proposal we attested to (to prevent equivocation)
618
637
  this.lastAttestedProposal = proposal;
@@ -725,6 +744,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
725
744
 
726
745
  async createBlockProposal(
727
746
  blockHeader: BlockHeader,
747
+ checkpointNumber: CheckpointNumber,
728
748
  indexWithinCheckpoint: IndexWithinCheckpoint,
729
749
  inHash: Fr,
730
750
  archive: Fr,
@@ -751,6 +771,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
751
771
  );
752
772
  const newProposal = await this.validationService.createBlockProposal(
753
773
  blockHeader,
774
+ checkpointNumber,
754
775
  indexWithinCheckpoint,
755
776
  inHash,
756
777
  archive,
@@ -768,8 +789,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
768
789
  async createCheckpointProposal(
769
790
  checkpointHeader: CheckpointHeader,
770
791
  archive: Fr,
792
+ checkpointNumber: CheckpointNumber,
771
793
  feeAssetPriceModifier: bigint,
772
- lastBlockInfo: CreateCheckpointProposalLastBlockData | undefined,
794
+ lastBlockProposal: BlockProposal | undefined,
773
795
  proposerAddress: EthAddress | undefined,
774
796
  options: CheckpointProposalOptions = {},
775
797
  ): Promise<CheckpointProposal> {
@@ -790,8 +812,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
790
812
  const newProposal = await this.validationService.createCheckpointProposal(
791
813
  checkpointHeader,
792
814
  archive,
815
+ checkpointNumber,
793
816
  feeAssetPriceModifier,
794
- lastBlockInfo,
817
+ lastBlockProposal,
795
818
  proposerAddress,
796
819
  options,
797
820
  );
@@ -807,16 +830,24 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
807
830
  attestationsAndSigners: CommitteeAttestationsAndSigners,
808
831
  proposer: EthAddress,
809
832
  slot: SlotNumber,
810
- blockNumber: BlockNumber | CheckpointNumber,
833
+ checkpointNumber: CheckpointNumber,
811
834
  ): Promise<Signature> {
812
- return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer, slot, blockNumber);
835
+ return await this.validationService.signAttestationsAndSigners(
836
+ attestationsAndSigners,
837
+ proposer,
838
+ slot,
839
+ checkpointNumber,
840
+ );
813
841
  }
814
842
 
815
- async collectOwnAttestations(proposal: CheckpointProposal): Promise<CheckpointAttestation[]> {
843
+ async collectOwnAttestations(
844
+ proposal: CheckpointProposal,
845
+ checkpointNumber: CheckpointNumber,
846
+ ): Promise<CheckpointAttestation[]> {
816
847
  const slot = proposal.slotNumber;
817
848
  const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
818
849
  this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
819
- const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
850
+ const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee, checkpointNumber);
820
851
 
821
852
  if (!attestations) {
822
853
  return [];
@@ -835,6 +866,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
835
866
  proposal: CheckpointProposal,
836
867
  required: number,
837
868
  deadline: Date,
869
+ checkpointNumber: CheckpointNumber,
838
870
  ): Promise<CheckpointAttestation[]> {
839
871
  // Wait and poll the p2pClient's attestation pool for this checkpoint until we have enough attestations
840
872
  const slot = proposal.slotNumber;
@@ -847,7 +879,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
847
879
  throw new AttestationTimeoutError(0, required, slot);
848
880
  }
849
881
 
850
- await this.collectOwnAttestations(proposal);
882
+ await this.collectOwnAttestations(proposal, checkpointNumber);
851
883
 
852
884
  const proposalId = proposal.archive.toString();
853
885
  const myAddresses = this.getValidatorAddresses();
@@ -860,7 +892,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
860
892
  attestation => {
861
893
  if (!attestation.archive.equals(proposal.archive)) {
862
894
  this.log.warn(
863
- `Received attestation for slot ${slot} with mismatched archive from ${attestation.getSender()?.toString()}`,
895
+ `Received attestation for slot ${slot} with mismatched archive from ${attestation
896
+ .getSender()
897
+ ?.toString()}`,
864
898
  { attestationArchive: attestation.archive.toString(), proposalArchive: proposal.archive.toString() },
865
899
  );
866
900
  return false;