@aztec/validator-client 0.0.1-commit.f5d02921e → 0.0.1-commit.f7ea82942

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/dest/validator.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { getBlobsPerL1Block } from '@aztec/blob-lib';
2
+ import { CheckpointNumber } from '@aztec/foundation/branded-types';
2
3
  import { createLogger } from '@aztec/foundation/log';
3
4
  import { RunningPromise } from '@aztec/foundation/running-promise';
4
5
  import { sleep } from '@aztec/foundation/sleep';
@@ -114,7 +115,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
114
115
  txsPermitted: !config.disableTransactions,
115
116
  maxTxsPerBlock: config.validateMaxTxsPerBlock
116
117
  });
117
- const proposalHandler = new ProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, config, blobClient, metrics, dateProvider, telemetry);
118
+ const proposalHandler = new ProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, config, blobClient, metrics, dateProvider, telemetry, undefined);
118
119
  const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
119
120
  let slashingProtectionSigner;
120
121
  if (slashingProtectionDb) {
@@ -330,14 +331,17 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
330
331
  });
331
332
  // Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set).
332
333
  // Uses the cached result from the all-nodes callback if available (avoids double validation).
334
+ let checkpointNumber;
333
335
  if (this.config.skipCheckpointProposalValidation) {
334
336
  this.log.warn(`Skipping checkpoint proposal validation for slot ${proposalSlotNumber}`, proposalInfo);
337
+ checkpointNumber = CheckpointNumber(0);
335
338
  } else {
336
339
  const validationResult = await this.proposalHandler.handleCheckpointProposal(proposal, proposalInfo);
337
340
  if (!validationResult.isValid) {
338
341
  this.log.warn(`Checkpoint proposal validation failed: ${validationResult.reason}`, proposalInfo);
339
342
  return undefined;
340
343
  }
344
+ checkpointNumber = validationResult.checkpointNumber;
341
345
  }
342
346
  // Check that I have any address in current committee before attesting
343
347
  // In fisherman mode, we still create attestations for validation even if not in committee
@@ -384,7 +388,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
384
388
  });
385
389
  return undefined;
386
390
  }
387
- return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
391
+ return await this.createCheckpointAttestationsFromProposal(proposal, attestors, checkpointNumber);
388
392
  }
389
393
  /**
390
394
  * Checks if we should attest to a slot based on equivocation prevention rules.
@@ -401,12 +405,12 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
401
405
  }
402
406
  return true;
403
407
  }
404
- async createCheckpointAttestationsFromProposal(proposal, attestors = []) {
408
+ async createCheckpointAttestationsFromProposal(proposal, attestors = [], checkpointNumber) {
405
409
  // Equivocation check: must happen right before signing to minimize the race window
406
410
  if (!this.shouldAttestToSlot(proposal.slotNumber)) {
407
411
  return undefined;
408
412
  }
409
- const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
413
+ const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors, checkpointNumber);
410
414
  // Track the proposal we attested to (to prevent equivocation)
411
415
  this.lastAttestedProposal = proposal;
412
416
  await this.p2pClient.addOwnCheckpointAttestations(attestations);
@@ -497,7 +501,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
497
501
  }
498
502
  ]);
499
503
  }
500
- async createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, options = {}) {
504
+ async createBlockProposal(blockHeader, checkpointNumber, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, options = {}) {
501
505
  // Validate that we're not creating a proposal for an older or equal position
502
506
  if (this.lastProposedBlock) {
503
507
  const lastSlot = this.lastProposedBlock.slotNumber;
@@ -508,14 +512,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
508
512
  }
509
513
  }
510
514
  this.log.info(`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`);
511
- const newProposal = await this.validationService.createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, {
515
+ const newProposal = await this.validationService.createBlockProposal(blockHeader, checkpointNumber, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, {
512
516
  ...options,
513
517
  broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal
514
518
  });
515
519
  this.lastProposedBlock = newProposal;
516
520
  return newProposal;
517
521
  }
518
- async createCheckpointProposal(checkpointHeader, archive, feeAssetPriceModifier, lastBlockInfo, proposerAddress, options = {}) {
522
+ async createCheckpointProposal(checkpointHeader, archive, checkpointNumber, feeAssetPriceModifier, lastBlockProposal, proposerAddress, options = {}) {
519
523
  // Validate that we're not creating a proposal for an older or equal slot
520
524
  if (this.lastProposedCheckpoint) {
521
525
  const lastSlot = this.lastProposedCheckpoint.slotNumber;
@@ -525,23 +529,23 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
525
529
  }
526
530
  }
527
531
  this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
528
- const newProposal = await this.validationService.createCheckpointProposal(checkpointHeader, archive, feeAssetPriceModifier, lastBlockInfo, proposerAddress, options);
532
+ const newProposal = await this.validationService.createCheckpointProposal(checkpointHeader, archive, checkpointNumber, feeAssetPriceModifier, lastBlockProposal, proposerAddress, options);
529
533
  this.lastProposedCheckpoint = newProposal;
530
534
  return newProposal;
531
535
  }
532
536
  async broadcastBlockProposal(proposal) {
533
537
  await this.p2pClient.broadcastProposal(proposal);
534
538
  }
535
- async signAttestationsAndSigners(attestationsAndSigners, proposer, slot, blockNumber) {
536
- return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer, slot, blockNumber);
539
+ async signAttestationsAndSigners(attestationsAndSigners, proposer, slot, checkpointNumber) {
540
+ return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer, slot, checkpointNumber);
537
541
  }
538
- async collectOwnAttestations(proposal) {
542
+ async collectOwnAttestations(proposal, checkpointNumber) {
539
543
  const slot = proposal.slotNumber;
540
544
  const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
541
545
  this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, {
542
546
  inCommittee
543
547
  });
544
- const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
548
+ const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee, checkpointNumber);
545
549
  if (!attestations) {
546
550
  return [];
547
551
  }
@@ -553,7 +557,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
553
557
  });
554
558
  return attestations;
555
559
  }
556
- async collectAttestations(proposal, required, deadline) {
560
+ async collectAttestations(proposal, required, deadline, checkpointNumber) {
557
561
  // Wait and poll the p2pClient's attestation pool for this checkpoint until we have enough attestations
558
562
  const slot = proposal.slotNumber;
559
563
  this.log.debug(`Collecting ${required} attestations for slot ${slot} with deadline ${deadline.toISOString()}`);
@@ -561,7 +565,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
561
565
  this.log.error(`Deadline ${deadline.toISOString()} for collecting ${required} attestations for slot ${slot} is in the past`);
562
566
  throw new AttestationTimeoutError(0, required, slot);
563
567
  }
564
- await this.collectOwnAttestations(proposal);
568
+ await this.collectOwnAttestations(proposal, checkpointNumber);
565
569
  const proposalId = proposal.archive.toString();
566
570
  const myAddresses = this.getValidatorAddresses();
567
571
  let attestations = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/validator-client",
3
- "version": "0.0.1-commit.f5d02921e",
3
+ "version": "0.0.1-commit.f7ea82942",
4
4
  "main": "dest/index.js",
5
5
  "type": "module",
6
6
  "exports": {
@@ -64,30 +64,30 @@
64
64
  ]
65
65
  },
66
66
  "dependencies": {
67
- "@aztec/blob-client": "0.0.1-commit.f5d02921e",
68
- "@aztec/blob-lib": "0.0.1-commit.f5d02921e",
69
- "@aztec/constants": "0.0.1-commit.f5d02921e",
70
- "@aztec/epoch-cache": "0.0.1-commit.f5d02921e",
71
- "@aztec/ethereum": "0.0.1-commit.f5d02921e",
72
- "@aztec/foundation": "0.0.1-commit.f5d02921e",
73
- "@aztec/node-keystore": "0.0.1-commit.f5d02921e",
74
- "@aztec/noir-protocol-circuits-types": "0.0.1-commit.f5d02921e",
75
- "@aztec/p2p": "0.0.1-commit.f5d02921e",
76
- "@aztec/protocol-contracts": "0.0.1-commit.f5d02921e",
77
- "@aztec/prover-client": "0.0.1-commit.f5d02921e",
78
- "@aztec/simulator": "0.0.1-commit.f5d02921e",
79
- "@aztec/slasher": "0.0.1-commit.f5d02921e",
80
- "@aztec/stdlib": "0.0.1-commit.f5d02921e",
81
- "@aztec/telemetry-client": "0.0.1-commit.f5d02921e",
82
- "@aztec/validator-ha-signer": "0.0.1-commit.f5d02921e",
67
+ "@aztec/blob-client": "0.0.1-commit.f7ea82942",
68
+ "@aztec/blob-lib": "0.0.1-commit.f7ea82942",
69
+ "@aztec/constants": "0.0.1-commit.f7ea82942",
70
+ "@aztec/epoch-cache": "0.0.1-commit.f7ea82942",
71
+ "@aztec/ethereum": "0.0.1-commit.f7ea82942",
72
+ "@aztec/foundation": "0.0.1-commit.f7ea82942",
73
+ "@aztec/node-keystore": "0.0.1-commit.f7ea82942",
74
+ "@aztec/noir-protocol-circuits-types": "0.0.1-commit.f7ea82942",
75
+ "@aztec/p2p": "0.0.1-commit.f7ea82942",
76
+ "@aztec/protocol-contracts": "0.0.1-commit.f7ea82942",
77
+ "@aztec/prover-client": "0.0.1-commit.f7ea82942",
78
+ "@aztec/simulator": "0.0.1-commit.f7ea82942",
79
+ "@aztec/slasher": "0.0.1-commit.f7ea82942",
80
+ "@aztec/stdlib": "0.0.1-commit.f7ea82942",
81
+ "@aztec/telemetry-client": "0.0.1-commit.f7ea82942",
82
+ "@aztec/validator-ha-signer": "0.0.1-commit.f7ea82942",
83
83
  "koa": "^2.16.1",
84
84
  "koa-router": "^13.1.1",
85
85
  "tslib": "^2.4.0",
86
86
  "viem": "npm:@aztec/viem@2.38.2"
87
87
  },
88
88
  "devDependencies": {
89
- "@aztec/archiver": "0.0.1-commit.f5d02921e",
90
- "@aztec/world-state": "0.0.1-commit.f5d02921e",
89
+ "@aztec/archiver": "0.0.1-commit.f7ea82942",
90
+ "@aztec/world-state": "0.0.1-commit.f7ea82942",
91
91
  "@electric-sql/pglite": "^0.3.14",
92
92
  "@jest/globals": "^30.0.0",
93
93
  "@types/jest": "^30.0.0",
package/src/config.ts CHANGED
@@ -75,22 +75,22 @@ export const validatorClientConfigMappings: ConfigMappingsType<ValidatorClientCo
75
75
  validateMaxL2BlockGas: {
76
76
  env: 'VALIDATOR_MAX_L2_BLOCK_GAS',
77
77
  description: 'Maximum L2 block gas for validation. Proposals exceeding this limit are rejected.',
78
- parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
78
+ parseEnv: (val: string) => parseInt(val, 10),
79
79
  },
80
80
  validateMaxDABlockGas: {
81
81
  env: 'VALIDATOR_MAX_DA_BLOCK_GAS',
82
82
  description: 'Maximum DA block gas for validation. Proposals exceeding this limit are rejected.',
83
- parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
83
+ parseEnv: (val: string) => parseInt(val, 10),
84
84
  },
85
85
  validateMaxTxsPerBlock: {
86
86
  env: 'VALIDATOR_MAX_TX_PER_BLOCK',
87
87
  description: 'Maximum transactions per block for validation. Proposals exceeding this limit are rejected.',
88
- parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
88
+ parseEnv: (val: string) => parseInt(val, 10),
89
89
  },
90
90
  validateMaxTxsPerCheckpoint: {
91
91
  env: 'VALIDATOR_MAX_TX_PER_CHECKPOINT',
92
92
  description: 'Maximum transactions per checkpoint for validation. Proposals exceeding this limit are rejected.',
93
- parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
93
+ parseEnv: (val: string) => parseInt(val, 10),
94
94
  },
95
95
  ...localSignerConfigMappings,
96
96
  ...validatorHASignerConfigMappings,
@@ -1,9 +1,4 @@
1
- import {
2
- BlockNumber,
3
- type CheckpointNumber,
4
- IndexWithinCheckpoint,
5
- type SlotNumber,
6
- } from '@aztec/foundation/branded-types';
1
+ import { type CheckpointNumber, IndexWithinCheckpoint, type SlotNumber } from '@aztec/foundation/branded-types';
7
2
  import { Buffer32 } from '@aztec/foundation/buffer';
8
3
  import { keccak256 } from '@aztec/foundation/crypto/keccak';
9
4
  import { Fr } from '@aztec/foundation/curves/bn254';
@@ -11,7 +6,6 @@ import type { EthAddress } from '@aztec/foundation/eth-address';
11
6
  import type { Signature } from '@aztec/foundation/eth-signature';
12
7
  import { createLogger } from '@aztec/foundation/log';
13
8
  import type { CommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
14
- import type { CreateCheckpointProposalLastBlockData } from '@aztec/stdlib/interfaces/server';
15
9
  import {
16
10
  BlockProposal,
17
11
  type BlockProposalOptions,
@@ -52,6 +46,7 @@ export class ValidationService {
52
46
  */
53
47
  public createBlockProposal(
54
48
  blockHeader: BlockHeader,
49
+ checkpointNumber: CheckpointNumber,
55
50
  blockIndexWithinCheckpoint: IndexWithinCheckpoint,
56
51
  inHash: Fr,
57
52
  archive: Fr,
@@ -72,6 +67,7 @@ export class ValidationService {
72
67
 
73
68
  return BlockProposal.createProposalFromSigner(
74
69
  blockHeader,
70
+ checkpointNumber,
75
71
  blockIndexWithinCheckpoint,
76
72
  inHash,
77
73
  archive,
@@ -86,7 +82,7 @@ export class ValidationService {
86
82
  *
87
83
  * @param checkpointHeader - The checkpoint header containing aggregated data
88
84
  * @param archive - The archive of the checkpoint
89
- * @param lastBlockInfo - Info about the last block (header, index, txs) or undefined
85
+ * @param lastBlockProposal - Signed block proposal for the last block in the checkpoint, or undefined
90
86
  * @param proposerAttesterAddress - The address of the proposer
91
87
  * @param options - Checkpoint proposal options
92
88
  *
@@ -95,14 +91,17 @@ export class ValidationService {
95
91
  public createCheckpointProposal(
96
92
  checkpointHeader: CheckpointHeader,
97
93
  archive: Fr,
94
+ checkpointNumber: CheckpointNumber,
98
95
  feeAssetPriceModifier: bigint,
99
- lastBlockInfo: CreateCheckpointProposalLastBlockData | undefined,
96
+ lastBlockProposal: BlockProposal | undefined,
100
97
  proposerAttesterAddress: EthAddress | undefined,
101
98
  options: CheckpointProposalOptions,
102
99
  ): Promise<CheckpointProposal> {
103
- // For testing: change the archive to trigger state_mismatch validation failure
100
+ // For testing: change the archive to trigger state_mismatch validation failure.
101
+ // If there's a last block proposal, use its (already invalid) archive to keep signatures consistent
102
+ // so P2P validation passes and the slasher can detect the offense.
104
103
  if (options.broadcastInvalidCheckpointProposal) {
105
- archive = Fr.random();
104
+ archive = lastBlockProposal?.archiveRoot ?? Fr.random();
106
105
  this.log.warn(`Creating INVALID checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
107
106
  }
108
107
 
@@ -112,19 +111,12 @@ export class ValidationService {
112
111
  return this.keyStore.signMessageWithAddress(address, payload, context);
113
112
  };
114
113
 
115
- // Last block to include in the proposal
116
- const lastBlock = lastBlockInfo && {
117
- blockHeader: lastBlockInfo.blockHeader,
118
- indexWithinCheckpoint: lastBlockInfo.indexWithinCheckpoint,
119
- txHashes: lastBlockInfo.txs.map(tx => tx.getTxHash()),
120
- txs: options.publishFullTxs ? lastBlockInfo.txs : undefined,
121
- };
122
-
123
114
  return CheckpointProposal.createProposalFromSigner(
124
115
  checkpointHeader,
125
116
  archive,
117
+ checkpointNumber,
126
118
  feeAssetPriceModifier,
127
- lastBlock,
119
+ lastBlockProposal,
128
120
  payloadSigner,
129
121
  );
130
122
  }
@@ -142,6 +134,7 @@ export class ValidationService {
142
134
  async attestToCheckpointProposal(
143
135
  proposal: CheckpointProposalCore,
144
136
  attestors: EthAddress[],
137
+ checkpointNumber: CheckpointNumber,
145
138
  ): Promise<CheckpointAttestation[]> {
146
139
  // Create the attestation payload from the checkpoint proposal
147
140
  const payload = new ConsensusPayload(proposal.checkpointHeader, proposal.archive, proposal.feeAssetPriceModifier);
@@ -149,14 +142,9 @@ export class ValidationService {
149
142
  keccak256(payload.getPayloadToSign(SignatureDomainSeparator.checkpointAttestation)),
150
143
  );
151
144
 
152
- // TODO(spy/ha): Use checkpointNumber instead of blockNumber once CheckpointHeader includes it.
153
- // CheckpointProposalCore doesn't have lastBlock info, so use 0 as a proxy.
154
- // blockNumber is NOT used for the primary key so it's safe to use here.
155
- // See CheckpointHeader TODO and SigningContext types documentation.
156
- const blockNumber = BlockNumber(0);
157
145
  const context: SigningContext = {
158
146
  slot: proposal.slotNumber,
159
- blockNumber,
147
+ checkpointNumber,
160
148
  dutyType: DutyType.ATTESTATION,
161
149
  };
162
150
 
@@ -195,7 +183,6 @@ export class ValidationService {
195
183
  * @param attestationsAndSigners - The attestations and signers to sign
196
184
  * @param proposer - The proposer address to sign with
197
185
  * @param slot - The slot number for HA signing context
198
- * @param blockNumber - The block or checkpoint number for HA signing context
199
186
  * @returns signature
200
187
  * @throws DutyAlreadySignedError if already signed by another HA node
201
188
  * @throws SlashingProtectionError if attempting to sign different data for same slot
@@ -204,11 +191,11 @@ export class ValidationService {
204
191
  attestationsAndSigners: CommitteeAttestationsAndSigners,
205
192
  proposer: EthAddress,
206
193
  slot: SlotNumber,
207
- blockNumber: BlockNumber | CheckpointNumber,
194
+ checkpointNumber: CheckpointNumber,
208
195
  ): Promise<Signature> {
209
196
  const context: SigningContext = {
210
197
  slot,
211
- blockNumber,
198
+ checkpointNumber,
212
199
  dutyType: DutyType.ATTESTATIONS_AND_SIGNERS,
213
200
  };
214
201
 
package/src/factory.ts CHANGED
@@ -46,6 +46,7 @@ export function createProposalHandler(
46
46
  metrics,
47
47
  deps.dateProvider,
48
48
  deps.telemetry,
49
+ undefined,
49
50
  );
50
51
  }
51
52
 
package/src/metrics.ts CHANGED
@@ -24,6 +24,8 @@ export class ValidatorMetrics {
24
24
  private reexMana: Histogram;
25
25
  private reexTx: Histogram;
26
26
  private reexDuration: Gauge;
27
+ private checkpointProposalToPipelinedStateDuration: Histogram;
28
+ private checkpointProposalReceiveOffsetFromNextSlotBoundary: Histogram;
27
29
 
28
30
  constructor(telemetryClient: TelemetryClient) {
29
31
  const meter = telemetryClient.getMeter('Validator');
@@ -77,6 +79,12 @@ export class ValidatorMetrics {
77
79
  this.reexTx = meter.createHistogram(Metrics.VALIDATOR_RE_EXECUTION_TX_COUNT);
78
80
 
79
81
  this.reexDuration = meter.createGauge(Metrics.VALIDATOR_RE_EXECUTION_TIME);
82
+ this.checkpointProposalToPipelinedStateDuration = meter.createHistogram(
83
+ Metrics.VALIDATOR_CHECKPOINT_PROPOSAL_TO_PIPELINED_STATE_DURATION,
84
+ );
85
+ this.checkpointProposalReceiveOffsetFromNextSlotBoundary = meter.createHistogram(
86
+ Metrics.VALIDATOR_CHECKPOINT_PROPOSAL_RECEIVE_OFFSET_FROM_NEXT_SLOT_BOUNDARY,
87
+ );
80
88
  }
81
89
 
82
90
  public recordReex(time: number, txs: number, mManaTotal: number) {
@@ -85,6 +93,16 @@ export class ValidatorMetrics {
85
93
  this.reexMana.record(mManaTotal);
86
94
  }
87
95
 
96
+ public recordCheckpointProposalToPipelinedStateDuration(durationMs: number) {
97
+ this.checkpointProposalToPipelinedStateDuration.record(Math.ceil(durationMs));
98
+ }
99
+
100
+ public recordCheckpointProposalReceiveOffsetFromNextSlotBoundary(offsetMs: number) {
101
+ this.checkpointProposalReceiveOffsetFromNextSlotBoundary.record(Math.ceil(Math.abs(offsetMs)), {
102
+ [Attributes.SLOT_BOUNDARY_SIDE]: offsetMs < 0 ? 'before' : 'after',
103
+ });
104
+ }
105
+
88
106
  public recordFailedReexecution(proposal: BlockProposal) {
89
107
  const proposer = proposal.getSender();
90
108
  this.failedReexecutionCounter.add(1, {
@@ -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 }
@@ -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,
@@ -904,7 +916,7 @@ export class ProposalHandler {
904
916
  }
905
917
 
906
918
  this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
907
- return { isValid: true };
919
+ return { isValid: true, checkpointNumber };
908
920
  }
909
921
 
910
922
  /** Extracts checkpoint global variables from a block. */
@@ -961,16 +973,16 @@ export class ProposalHandler {
961
973
  * Used after successful validation of a foreign proposal.
962
974
  * Does not retry since we already waited for the block during validation.
963
975
  */
964
- private async setProposedCheckpointFromValidation(proposal: CheckpointProposalCore): Promise<void> {
976
+ private async setProposedCheckpointFromValidation(proposal: CheckpointProposalCore): Promise<boolean> {
965
977
  if (!this.archiver) {
966
- return;
978
+ return false;
967
979
  }
968
980
  const blockData = await this.blockSource.getBlockDataByArchive(proposal.archive);
969
981
  if (!blockData) {
970
982
  this.log.debug(`Block data not found for checkpoint proposal archive, cannot set proposed checkpoint`, {
971
983
  archive: proposal.archive.toString(),
972
984
  });
973
- return;
985
+ return false;
974
986
  }
975
987
 
976
988
  await this.archiver.setProposedCheckpoint({
@@ -981,6 +993,7 @@ export class ProposalHandler {
981
993
  totalManaUsed: proposal.checkpointHeader.totalManaUsed.toBigInt(),
982
994
  feeAssetPriceModifier: proposal.feeAssetPriceModifier,
983
995
  });
996
+ return true;
984
997
  }
985
998
 
986
999
  /**
@@ -988,9 +1001,9 @@ export class ProposalHandler {
988
1001
  * Retries fetching block data since the checkpoint proposal often arrives before the last block
989
1002
  * finishes re-execution.
990
1003
  */
991
- private async setProposedCheckpointFromBlocks(proposal: CheckpointProposalCore): Promise<void> {
1004
+ private async setProposedCheckpointFromBlocks(proposal: CheckpointProposalCore): Promise<boolean> {
992
1005
  if (!this.archiver) {
993
- return;
1006
+ return false;
994
1007
  }
995
1008
  let blockData = await this.blockSource.getBlockDataByArchive(proposal.archive);
996
1009
 
@@ -1018,10 +1031,12 @@ export class ProposalHandler {
1018
1031
  totalManaUsed: proposal.checkpointHeader.totalManaUsed.toBigInt(),
1019
1032
  feeAssetPriceModifier: proposal.feeAssetPriceModifier,
1020
1033
  });
1034
+ return true;
1021
1035
  } else {
1022
1036
  this.log.debug(`Block data not found for own checkpoint proposal archive, cannot set proposed checkpoint`, {
1023
1037
  archive: proposal.archive.toString(),
1024
1038
  });
1039
+ return false;
1025
1040
  }
1026
1041
  }
1027
1042
  }