@aztec/validator-client 4.0.0-devnet.2-patch.4 → 4.0.0-devnet.3-patch.0

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.
Files changed (50) hide show
  1. package/README.md +41 -0
  2. package/dest/checkpoint_builder.d.ts +19 -6
  3. package/dest/checkpoint_builder.d.ts.map +1 -1
  4. package/dest/checkpoint_builder.js +115 -39
  5. package/dest/config.d.ts +1 -1
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +20 -0
  8. package/dest/duties/validation_service.d.ts +1 -1
  9. package/dest/duties/validation_service.d.ts.map +1 -1
  10. package/dest/duties/validation_service.js +3 -9
  11. package/dest/factory.d.ts +7 -4
  12. package/dest/factory.d.ts.map +1 -1
  13. package/dest/factory.js +6 -5
  14. package/dest/index.d.ts +2 -3
  15. package/dest/index.d.ts.map +1 -1
  16. package/dest/index.js +1 -2
  17. package/dest/key_store/ha_key_store.js +1 -1
  18. package/dest/metrics.d.ts +10 -2
  19. package/dest/metrics.d.ts.map +1 -1
  20. package/dest/metrics.js +12 -0
  21. package/dest/proposal_handler.d.ts +94 -0
  22. package/dest/proposal_handler.d.ts.map +1 -0
  23. package/dest/{block_proposal_handler.js → proposal_handler.js} +356 -36
  24. package/dest/validator.d.ts +11 -22
  25. package/dest/validator.d.ts.map +1 -1
  26. package/dest/validator.js +41 -217
  27. package/package.json +19 -19
  28. package/src/checkpoint_builder.ts +135 -39
  29. package/src/config.ts +20 -0
  30. package/src/duties/validation_service.ts +3 -9
  31. package/src/factory.ts +9 -3
  32. package/src/index.ts +1 -2
  33. package/src/key_store/ha_key_store.ts +1 -1
  34. package/src/metrics.ts +19 -1
  35. package/src/{block_proposal_handler.ts → proposal_handler.ts} +412 -44
  36. package/src/validator.ts +48 -240
  37. package/dest/block_proposal_handler.d.ts +0 -63
  38. package/dest/block_proposal_handler.d.ts.map +0 -1
  39. package/dest/tx_validator/index.d.ts +0 -3
  40. package/dest/tx_validator/index.d.ts.map +0 -1
  41. package/dest/tx_validator/index.js +0 -2
  42. package/dest/tx_validator/nullifier_cache.d.ts +0 -14
  43. package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
  44. package/dest/tx_validator/nullifier_cache.js +0 -24
  45. package/dest/tx_validator/tx_validator_factory.d.ts +0 -19
  46. package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
  47. package/dest/tx_validator/tx_validator_factory.js +0 -54
  48. package/src/tx_validator/index.ts +0 -2
  49. package/src/tx_validator/nullifier_cache.ts +0 -30
  50. package/src/tx_validator/tx_validator_factory.ts +0 -154
@@ -63,18 +63,24 @@ function _ts_dispose_resources(env) {
63
63
  return next();
64
64
  })(env);
65
65
  }
66
+ import { encodeCheckpointBlobDataFromBlocks, getBlobsPerL1Block } from '@aztec/blob-lib';
66
67
  import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
68
+ import { validateFeeAssetPriceModifier } from '@aztec/ethereum/contracts';
67
69
  import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
70
+ import { pick } from '@aztec/foundation/collection';
68
71
  import { Fr } from '@aztec/foundation/curves/bn254';
69
72
  import { TimeoutError } from '@aztec/foundation/error';
70
73
  import { createLogger } from '@aztec/foundation/log';
71
74
  import { retryUntil } from '@aztec/foundation/retry';
72
75
  import { DateProvider, Timer } from '@aztec/foundation/timer';
76
+ import { validateCheckpoint } from '@aztec/stdlib/checkpoint';
73
77
  import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
74
- import { computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
75
- import { ReExFailedTxsError, ReExStateMismatchError, ReExTimeoutError, TransactionsNotAvailableError } from '@aztec/stdlib/validators';
78
+ import { Gas } from '@aztec/stdlib/gas';
79
+ import { accumulateCheckpointOutHashes, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
80
+ import { MerkleTreeId } from '@aztec/stdlib/trees';
81
+ import { ReExFailedTxsError, ReExInitialStateMismatchError, ReExStateMismatchError, ReExTimeoutError, TransactionsNotAvailableError } from '@aztec/stdlib/validators';
76
82
  import { getTelemetryClient } from '@aztec/telemetry-client';
77
- export class BlockProposalHandler {
83
+ /** Handles block and checkpoint proposals for both validator and non-validator nodes. */ export class ProposalHandler {
78
84
  checkpointsBuilder;
79
85
  worldState;
80
86
  blockSource;
@@ -83,11 +89,12 @@ export class BlockProposalHandler {
83
89
  blockProposalValidator;
84
90
  epochCache;
85
91
  config;
92
+ blobClient;
86
93
  metrics;
87
94
  dateProvider;
88
95
  log;
89
96
  tracer;
90
- constructor(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, config, metrics, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator:block-proposal-handler')){
97
+ constructor(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, config, blobClient, metrics, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator:proposal-handler')){
91
98
  this.checkpointsBuilder = checkpointsBuilder;
92
99
  this.worldState = worldState;
93
100
  this.blockSource = blockSource;
@@ -96,31 +103,39 @@ export class BlockProposalHandler {
96
103
  this.blockProposalValidator = blockProposalValidator;
97
104
  this.epochCache = epochCache;
98
105
  this.config = config;
106
+ this.blobClient = blobClient;
99
107
  this.metrics = metrics;
100
108
  this.dateProvider = dateProvider;
101
109
  this.log = log;
102
110
  if (config.fishermanMode) {
103
111
  this.log = this.log.createChild('[FISHERMAN]');
104
112
  }
105
- this.tracer = telemetry.getTracer('BlockProposalHandler');
113
+ this.tracer = telemetry.getTracer('ProposalHandler');
106
114
  }
107
- registerForReexecution(p2pClient) {
108
- // Non-validator handler that re-executes for monitoring but does not attest.
115
+ /**
116
+ * Registers non-validator handlers for block and checkpoint proposals on the p2p client.
117
+ * Block proposals are always registered. Checkpoint proposals are registered if the blob client can upload.
118
+ */ register(p2pClient, shouldReexecute) {
119
+ // Non-validator handler that processes or re-executes for monitoring but does not attest.
109
120
  // Returns boolean indicating whether the proposal was valid.
110
- const handler = async (proposal, proposalSender)=>{
121
+ const blockHandler = async (proposal, proposalSender)=>{
111
122
  try {
112
- const result = await this.handleBlockProposal(proposal, proposalSender, true);
123
+ const { slotNumber, blockNumber } = proposal;
124
+ const result = await this.handleBlockProposal(proposal, proposalSender, shouldReexecute);
113
125
  if (result.isValid) {
114
- this.log.info(`Non-validator reexecution completed for slot ${proposal.slotNumber}`, {
126
+ this.log.info(`Non-validator block proposal ${blockNumber} at slot ${slotNumber} handled`, {
115
127
  blockNumber: result.blockNumber,
128
+ slotNumber,
116
129
  reexecutionTimeMs: result.reexecutionResult?.reexecutionTimeMs,
117
130
  totalManaUsed: result.reexecutionResult?.totalManaUsed,
118
- numTxs: result.reexecutionResult?.block?.body?.txEffects?.length ?? 0
131
+ numTxs: result.reexecutionResult?.block?.body?.txEffects?.length ?? 0,
132
+ reexecuted: shouldReexecute
119
133
  });
120
134
  return true;
121
135
  } else {
122
- this.log.warn(`Non-validator reexecution failed for slot ${proposal.slotNumber}`, {
136
+ this.log.warn(`Non-validator block proposal ${blockNumber} at slot ${slotNumber} failed processing with ${result.reason}`, {
123
137
  blockNumber: result.blockNumber,
138
+ slotNumber,
124
139
  reason: result.reason
125
140
  });
126
141
  return false;
@@ -130,7 +145,30 @@ export class BlockProposalHandler {
130
145
  return false;
131
146
  }
132
147
  };
133
- p2pClient.registerBlockProposalHandler(handler);
148
+ p2pClient.registerBlockProposalHandler(blockHandler);
149
+ // Register checkpoint proposal handler if blob uploads are enabled and we are reexecuting
150
+ if (this.blobClient.canUpload() && shouldReexecute) {
151
+ const checkpointHandler = async (checkpoint, _sender)=>{
152
+ try {
153
+ const proposalInfo = {
154
+ proposalSlotNumber: checkpoint.slotNumber,
155
+ archive: checkpoint.archive.toString(),
156
+ proposer: checkpoint.getSender()?.toString()
157
+ };
158
+ const result = await this.handleCheckpointProposal(checkpoint, proposalInfo);
159
+ if (result.isValid) {
160
+ this.log.info(`Non-validator checkpoint proposal at slot ${checkpoint.slotNumber} handled`, proposalInfo);
161
+ } else {
162
+ this.log.warn(`Non-validator checkpoint proposal at slot ${checkpoint.slotNumber} failed: ${result.reason}`, proposalInfo);
163
+ }
164
+ } catch (error) {
165
+ this.log.error('Error processing checkpoint proposal in non-validator handler', error);
166
+ }
167
+ // Non-validators don't attest
168
+ return undefined;
169
+ };
170
+ p2pClient.registerCheckpointProposalHandler(checkpointHandler);
171
+ }
134
172
  return this;
135
173
  }
136
174
  async handleBlockProposal(proposal, proposalSender, shouldReexecute) {
@@ -147,7 +185,9 @@ export class BlockProposalHandler {
147
185
  }
148
186
  const proposalInfo = {
149
187
  ...proposal.toBlockInfo(),
150
- proposer: proposer.toString()
188
+ proposer: proposer.toString(),
189
+ blockNumber: undefined,
190
+ checkpointNumber: undefined
151
191
  };
152
192
  this.log.info(`Processing proposal for slot ${slotNumber}`, {
153
193
  ...proposalInfo,
@@ -163,7 +203,22 @@ export class BlockProposalHandler {
163
203
  reason: 'invalid_proposal'
164
204
  };
165
205
  }
166
- // Check that the parent proposal is a block we know, otherwise reexecution would fail
206
+ // Ensure the block source is synced before checking for existing blocks,
207
+ // since a pending checkpoint prune may remove blocks we'd otherwise find.
208
+ // This affects mostly the block_number_already_exists check, since a pending
209
+ // checkpoint prune could remove a block that would conflict with this proposal.
210
+ // TODO(@Maddiaa0): This may break staggered slots.
211
+ const blockSourceSync = await this.waitForBlockSourceSync(slotNumber);
212
+ if (!blockSourceSync) {
213
+ this.log.warn(`Block source is not synced, skipping processing`, proposalInfo);
214
+ return {
215
+ isValid: false,
216
+ reason: 'block_source_not_synced'
217
+ };
218
+ }
219
+ // Check that the parent proposal is a block we know, otherwise reexecution would fail.
220
+ // If we don't find it immediately, we keep retrying for a while; it may be we still
221
+ // need to process other block proposals to get to it.
167
222
  const parentBlock = await this.getParentBlock(proposal);
168
223
  if (parentBlock === undefined) {
169
224
  this.log.warn(`Parent block for proposal not found, skipping processing`, proposalInfo);
@@ -186,6 +241,7 @@ export class BlockProposalHandler {
186
241
  }
187
242
  // Compute the block number based on the parent block
188
243
  const blockNumber = parentBlock === 'genesis' ? BlockNumber(INITIAL_L2_BLOCK_NUM) : BlockNumber(parentBlock.header.getBlockNumber() + 1);
244
+ proposalInfo.blockNumber = blockNumber;
189
245
  // Check that this block number does not exist already
190
246
  const existingBlock = await this.blockSource.getBlockHeader(blockNumber);
191
247
  if (existingBlock) {
@@ -202,6 +258,14 @@ export class BlockProposalHandler {
202
258
  pinnedPeer: proposalSender,
203
259
  deadline: this.getReexecutionDeadline(slotNumber, config)
204
260
  });
261
+ // If reexecution is disabled, bail. We were just interested in triggering tx collection.
262
+ if (!shouldReexecute) {
263
+ this.log.info(`Received valid block ${blockNumber} proposal at index ${proposal.indexWithinCheckpoint} on slot ${slotNumber}`, proposalInfo);
264
+ return {
265
+ isValid: true,
266
+ blockNumber
267
+ };
268
+ }
205
269
  // Compute the checkpoint number for this block and validate checkpoint consistency
206
270
  const checkpointResult = this.computeCheckpointNumber(proposal, parentBlock, proposalInfo);
207
271
  if (checkpointResult.reason) {
@@ -212,6 +276,7 @@ export class BlockProposalHandler {
212
276
  };
213
277
  }
214
278
  const checkpointNumber = checkpointResult.checkpointNumber;
279
+ proposalInfo.checkpointNumber = checkpointNumber;
215
280
  // Check that I have the same set of l1ToL2Messages as the proposal
216
281
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
217
282
  const computedInHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
@@ -240,31 +305,32 @@ export class BlockProposalHandler {
240
305
  reason: 'txs_not_available'
241
306
  };
242
307
  }
308
+ // Collect the out hashes of all the checkpoints before this one in the same epoch
309
+ const epoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
310
+ const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch)).filter((c)=>c.checkpointNumber < checkpointNumber).map((c)=>c.checkpointOutHash);
243
311
  // Try re-executing the transactions in the proposal if needed
244
312
  let reexecutionResult;
245
- if (shouldReexecute) {
246
- // Collect the out hashes of all the checkpoints before this one in the same epoch
247
- const epoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
248
- const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch)).filter((c)=>c.checkpointNumber < checkpointNumber).map((c)=>c.checkpointOutHash);
249
- try {
250
- this.log.verbose(`Re-executing transactions in the proposal`, proposalInfo);
251
- reexecutionResult = await this.reexecuteTransactions(proposal, blockNumber, checkpointNumber, txs, l1ToL2Messages, previousCheckpointOutHashes);
252
- } catch (error) {
253
- this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo);
254
- const reason = this.getReexecuteFailureReason(error);
255
- return {
256
- isValid: false,
257
- blockNumber,
258
- reason,
259
- reexecutionResult
260
- };
261
- }
313
+ try {
314
+ this.log.verbose(`Re-executing transactions in the proposal`, proposalInfo);
315
+ reexecutionResult = await this.reexecuteTransactions(proposal, blockNumber, checkpointNumber, txs, l1ToL2Messages, previousCheckpointOutHashes);
316
+ } catch (error) {
317
+ this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo);
318
+ const reason = this.getReexecuteFailureReason(error);
319
+ return {
320
+ isValid: false,
321
+ blockNumber,
322
+ reason,
323
+ reexecutionResult
324
+ };
262
325
  }
263
326
  // If we succeeded, push this block into the archiver (unless disabled)
264
327
  if (reexecutionResult?.block && this.config.skipPushProposedBlocksToArchiver === false) {
265
328
  await this.blockSource.addBlock(reexecutionResult?.block);
266
329
  }
267
- this.log.info(`Successfully processed block ${blockNumber} proposal at index ${proposal.indexWithinCheckpoint} on slot ${slotNumber}`, proposalInfo);
330
+ this.log.info(`Successfully re-executed block ${blockNumber} proposal at index ${proposal.indexWithinCheckpoint} on slot ${slotNumber}`, {
331
+ ...proposalInfo,
332
+ ...pick(reexecutionResult, 'reexecutionTimeMs', 'totalManaUsed')
333
+ });
268
334
  return {
269
335
  isValid: true,
270
336
  blockNumber,
@@ -429,8 +495,39 @@ export class BlockProposalHandler {
429
495
  const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
430
496
  return new Date(nextSlotTimestampSeconds * 1000);
431
497
  }
498
+ /** Waits for the block source to sync L1 data up to at least the slot before the given one. */ async waitForBlockSourceSync(slot) {
499
+ const deadline = this.getReexecutionDeadline(slot, this.checkpointsBuilder.getConfig());
500
+ const timeoutMs = deadline.getTime() - this.dateProvider.now();
501
+ if (slot === 0) {
502
+ return true;
503
+ }
504
+ // Make a quick check before triggering an archiver sync
505
+ const syncedSlot = await this.blockSource.getSyncedL2SlotNumber();
506
+ if (syncedSlot !== undefined && syncedSlot + 1 >= slot) {
507
+ return true;
508
+ }
509
+ try {
510
+ // Trigger an immediate sync of the block source, and wait until it reports being synced to the required slot
511
+ return await retryUntil(async ()=>{
512
+ await this.blockSource.syncImmediate();
513
+ const syncedSlot = await this.blockSource.getSyncedL2SlotNumber();
514
+ return syncedSlot !== undefined && syncedSlot + 1 >= slot;
515
+ }, 'wait for block source sync', timeoutMs / 1000, 0.5);
516
+ } catch (err) {
517
+ if (err instanceof TimeoutError) {
518
+ this.log.warn(`Timed out waiting for block source to sync to slot ${slot}`);
519
+ return false;
520
+ } else {
521
+ throw err;
522
+ }
523
+ }
524
+ }
432
525
  getReexecuteFailureReason(err) {
433
- if (err instanceof ReExStateMismatchError) {
526
+ if (err instanceof TransactionsNotAvailableError) {
527
+ return 'txs_not_available';
528
+ } else if (err instanceof ReExInitialStateMismatchError) {
529
+ return 'initial_state_mismatch';
530
+ } else if (err instanceof ReExStateMismatchError) {
434
531
  return 'state_mismatch';
435
532
  } else if (err instanceof ReExFailedTxsError) {
436
533
  return 'failed_txs';
@@ -464,6 +561,12 @@ export class BlockProposalHandler {
464
561
  const parentBlockNumber = BlockNumber(blockNumber - 1);
465
562
  await this.worldState.syncImmediate(parentBlockNumber);
466
563
  const fork = _ts_add_disposable_resource(env, await this.worldState.fork(parentBlockNumber), true);
564
+ // Verify the fork's archive root matches the proposal's expected last archive.
565
+ // If they don't match, our world state synced to a different chain and reexecution would fail.
566
+ const forkArchiveRoot = new Fr((await fork.getTreeInfo(MerkleTreeId.ARCHIVE)).root);
567
+ if (!forkArchiveRoot.equals(proposal.blockHeader.lastArchive.root)) {
568
+ throw new ReExInitialStateMismatchError(proposal.blockHeader.lastArchive.root, forkArchiveRoot);
569
+ }
467
570
  // Build checkpoint constants from proposal (excludes blockNumber which is per-block)
468
571
  const constants = {
469
572
  chainId: new Fr(config.l1ChainId),
@@ -478,16 +581,22 @@ export class BlockProposalHandler {
478
581
  const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, 0n, l1ToL2Messages, previousCheckpointOutHashes, fork, priorBlocks, this.log.getBindings());
479
582
  // Build the new block
480
583
  const deadline = this.getReexecutionDeadline(slot, config);
584
+ const maxBlockGas = this.config.validateMaxL2BlockGas !== undefined || this.config.validateMaxDABlockGas !== undefined ? new Gas(this.config.validateMaxDABlockGas ?? Infinity, this.config.validateMaxL2BlockGas ?? Infinity) : undefined;
481
585
  const result = await checkpointBuilder.buildBlock(txs, blockNumber, blockHeader.globalVariables.timestamp, {
586
+ isBuildingProposal: false,
587
+ minValidTxs: 0,
482
588
  deadline,
483
- expectedEndState: blockHeader.state
589
+ expectedEndState: blockHeader.state,
590
+ maxTransactions: this.config.validateMaxTxsPerBlock,
591
+ maxBlockGas
484
592
  });
485
593
  const { block, failedTxs } = result;
486
594
  const numFailedTxs = failedTxs.length;
487
- this.log.verbose(`Transaction re-execution complete for slot ${slot}`, {
595
+ this.log.verbose(`Block proposal ${blockNumber} at slot ${slot} transaction re-execution complete`, {
488
596
  numFailedTxs,
489
597
  numProposalTxs: txHashes.length,
490
598
  numProcessedTxs: block.body.txEffects.length,
599
+ blockNumber,
491
600
  slot
492
601
  });
493
602
  if (numFailedTxs > 0) {
@@ -529,4 +638,215 @@ export class BlockProposalHandler {
529
638
  if (result) await result;
530
639
  }
531
640
  }
641
+ /**
642
+ * Validates a checkpoint proposal and uploads blobs if configured.
643
+ * Used by both non-validator nodes (via register) and the validator client (via delegation).
644
+ */ async handleCheckpointProposal(proposal, proposalInfo) {
645
+ const proposer = proposal.getSender();
646
+ if (!proposer) {
647
+ this.log.warn(`Received checkpoint proposal with invalid signature for slot ${proposal.slotNumber}`);
648
+ return {
649
+ isValid: false,
650
+ reason: 'invalid_signature'
651
+ };
652
+ }
653
+ if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
654
+ this.log.warn(`Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${proposal.slotNumber}`);
655
+ return {
656
+ isValid: false,
657
+ reason: 'invalid_fee_asset_price_modifier'
658
+ };
659
+ }
660
+ const result = await this.validateCheckpointProposal(proposal, proposalInfo);
661
+ // Upload blobs to filestore if validation passed (fire and forget)
662
+ if (result.isValid) {
663
+ this.tryUploadBlobsForCheckpoint(proposal, proposalInfo);
664
+ }
665
+ return result;
666
+ }
667
+ /**
668
+ * Validates a checkpoint proposal by building the full checkpoint and comparing it with the proposal.
669
+ * @returns Validation result with isValid flag and reason if invalid.
670
+ */ async validateCheckpointProposal(proposal, proposalInfo) {
671
+ const slot = proposal.slotNumber;
672
+ // Timeout block syncing at the start of the next slot
673
+ const config = this.checkpointsBuilder.getConfig();
674
+ const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
675
+ const timeoutSeconds = Math.max(1, nextSlotTimestampSeconds - Math.floor(this.dateProvider.now() / 1000));
676
+ // Wait for last block to sync by archive
677
+ let lastBlockHeader;
678
+ try {
679
+ lastBlockHeader = await retryUntil(async ()=>{
680
+ await this.blockSource.syncImmediate();
681
+ return this.blockSource.getBlockHeaderByArchive(proposal.archive);
682
+ }, `waiting for block with archive ${proposal.archive.toString()} for slot ${slot}`, timeoutSeconds, 0.5);
683
+ } catch (err) {
684
+ if (err instanceof TimeoutError) {
685
+ this.log.warn(`Timed out waiting for block with archive matching checkpoint proposal`, proposalInfo);
686
+ return {
687
+ isValid: false,
688
+ reason: 'last_block_not_found'
689
+ };
690
+ }
691
+ this.log.error(`Error fetching last block for checkpoint proposal`, err, proposalInfo);
692
+ return {
693
+ isValid: false,
694
+ reason: 'block_fetch_error'
695
+ };
696
+ }
697
+ if (!lastBlockHeader) {
698
+ this.log.warn(`Last block not found for checkpoint proposal`, proposalInfo);
699
+ return {
700
+ isValid: false,
701
+ reason: 'last_block_not_found'
702
+ };
703
+ }
704
+ // Get all full blocks for the slot and checkpoint
705
+ const blocks = await this.blockSource.getBlocksForSlot(slot);
706
+ if (blocks.length === 0) {
707
+ this.log.warn(`No blocks found for slot ${slot}`, proposalInfo);
708
+ return {
709
+ isValid: false,
710
+ reason: 'no_blocks_for_slot'
711
+ };
712
+ }
713
+ // Ensure the last block for this slot matches the archive in the checkpoint proposal
714
+ if (!blocks.at(-1)?.archive.root.equals(proposal.archive)) {
715
+ this.log.warn(`Last block archive mismatch for checkpoint proposal`, proposalInfo);
716
+ return {
717
+ isValid: false,
718
+ reason: 'last_block_archive_mismatch'
719
+ };
720
+ }
721
+ this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
722
+ ...proposalInfo,
723
+ blockNumbers: blocks.map((b)=>b.number)
724
+ });
725
+ // Get checkpoint constants from first block
726
+ const firstBlock = blocks[0];
727
+ const constants = this.extractCheckpointConstants(firstBlock);
728
+ const checkpointNumber = firstBlock.checkpointNumber;
729
+ // Get L1-to-L2 messages for this checkpoint
730
+ const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
731
+ // Collect the out hashes of all the checkpoints before this one in the same epoch
732
+ const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
733
+ const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch)).filter((c)=>c.checkpointNumber < checkpointNumber).map((c)=>c.checkpointOutHash);
734
+ // Fork world state at the block before the first block
735
+ const parentBlockNumber = BlockNumber(firstBlock.number - 1);
736
+ const fork = await this.worldState.fork(parentBlockNumber);
737
+ try {
738
+ // Create checkpoint builder with all existing blocks
739
+ const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, proposal.feeAssetPriceModifier, l1ToL2Messages, previousCheckpointOutHashes, fork, blocks, this.log.getBindings());
740
+ // Complete the checkpoint to get computed values
741
+ const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
742
+ // Compare checkpoint header with proposal
743
+ if (!computedCheckpoint.header.equals(proposal.checkpointHeader)) {
744
+ this.log.warn(`Checkpoint header mismatch`, {
745
+ ...proposalInfo,
746
+ computed: computedCheckpoint.header.toInspect(),
747
+ proposal: proposal.checkpointHeader.toInspect()
748
+ });
749
+ return {
750
+ isValid: false,
751
+ reason: 'checkpoint_header_mismatch'
752
+ };
753
+ }
754
+ // Compare archive root with proposal
755
+ if (!computedCheckpoint.archive.root.equals(proposal.archive)) {
756
+ this.log.warn(`Archive root mismatch`, {
757
+ ...proposalInfo,
758
+ computed: computedCheckpoint.archive.root.toString(),
759
+ proposal: proposal.archive.toString()
760
+ });
761
+ return {
762
+ isValid: false,
763
+ reason: 'archive_mismatch'
764
+ };
765
+ }
766
+ // Check that the accumulated epoch out hash matches the value in the proposal.
767
+ // The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
768
+ const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
769
+ const computedEpochOutHash = accumulateCheckpointOutHashes([
770
+ ...previousCheckpointOutHashes,
771
+ checkpointOutHash
772
+ ]);
773
+ const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
774
+ if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
775
+ this.log.warn(`Epoch out hash mismatch`, {
776
+ proposalEpochOutHash: proposalEpochOutHash.toString(),
777
+ computedEpochOutHash: computedEpochOutHash.toString(),
778
+ checkpointOutHash: checkpointOutHash.toString(),
779
+ previousCheckpointOutHashes: previousCheckpointOutHashes.map((h)=>h.toString()),
780
+ ...proposalInfo
781
+ });
782
+ return {
783
+ isValid: false,
784
+ reason: 'out_hash_mismatch'
785
+ };
786
+ }
787
+ // Final round of validations on the checkpoint, just in case.
788
+ try {
789
+ validateCheckpoint(computedCheckpoint, {
790
+ rollupManaLimit: this.checkpointsBuilder.getConfig().rollupManaLimit,
791
+ maxDABlockGas: this.config.validateMaxDABlockGas,
792
+ maxL2BlockGas: this.config.validateMaxL2BlockGas,
793
+ maxTxsPerBlock: this.config.validateMaxTxsPerBlock,
794
+ maxTxsPerCheckpoint: this.config.validateMaxTxsPerCheckpoint
795
+ });
796
+ } catch (err) {
797
+ this.log.warn(`Checkpoint validation failed: ${err}`, proposalInfo);
798
+ return {
799
+ isValid: false,
800
+ reason: 'checkpoint_validation_failed'
801
+ };
802
+ }
803
+ this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
804
+ return {
805
+ isValid: true
806
+ };
807
+ } finally{
808
+ await fork.close();
809
+ }
810
+ }
811
+ /** Extracts checkpoint global variables from a block. */ extractCheckpointConstants(block) {
812
+ const gv = block.header.globalVariables;
813
+ return {
814
+ chainId: gv.chainId,
815
+ version: gv.version,
816
+ slotNumber: gv.slotNumber,
817
+ timestamp: gv.timestamp,
818
+ coinbase: gv.coinbase,
819
+ feeRecipient: gv.feeRecipient,
820
+ gasFees: gv.gasFees
821
+ };
822
+ }
823
+ /** Triggers blob upload for a checkpoint if the blob client can upload (fire and forget). */ tryUploadBlobsForCheckpoint(proposal, proposalInfo) {
824
+ if (this.blobClient.canUpload()) {
825
+ void this.uploadBlobsForCheckpoint(proposal, proposalInfo);
826
+ }
827
+ }
828
+ /** Uploads blobs for a checkpoint to the filestore. */ async uploadBlobsForCheckpoint(proposal, proposalInfo) {
829
+ try {
830
+ const lastBlockHeader = await this.blockSource.getBlockHeaderByArchive(proposal.archive);
831
+ if (!lastBlockHeader) {
832
+ this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
833
+ return;
834
+ }
835
+ const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
836
+ if (blocks.length === 0) {
837
+ this.log.warn(`No blocks found for blob upload`, proposalInfo);
838
+ return;
839
+ }
840
+ const blockBlobData = blocks.map((b)=>b.toBlockBlobData());
841
+ const blobFields = encodeCheckpointBlobDataFromBlocks(blockBlobData);
842
+ const blobs = await getBlobsPerL1Block(blobFields);
843
+ await this.blobClient.sendBlobsToFilestore(blobs);
844
+ this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
845
+ ...proposalInfo,
846
+ numBlobs: blobs.length
847
+ });
848
+ } catch (err) {
849
+ this.log.warn(`Failed to upload blobs for checkpoint: ${err}`, proposalInfo);
850
+ }
851
+ }
532
852
  }
@@ -4,7 +4,7 @@ import { BlockNumber, CheckpointNumber, IndexWithinCheckpoint, SlotNumber } from
4
4
  import { Fr } from '@aztec/foundation/curves/bn254';
5
5
  import type { EthAddress } from '@aztec/foundation/eth-address';
6
6
  import type { Signature } from '@aztec/foundation/eth-signature';
7
- import { type LogData, type Logger } from '@aztec/foundation/log';
7
+ import { type Logger } from '@aztec/foundation/log';
8
8
  import { DateProvider } from '@aztec/foundation/timer';
9
9
  import type { KeystoreManager } from '@aztec/node-keystore';
10
10
  import type { P2P, PeerId } from '@aztec/p2p';
@@ -12,17 +12,17 @@ import { type Watcher, type WatcherEmitter } from '@aztec/slasher';
12
12
  import type { AztecAddress } from '@aztec/stdlib/aztec-address';
13
13
  import type { CommitteeAttestationsAndSigners, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
14
14
  import type { CreateCheckpointProposalLastBlockData, ITxProvider, Validator, ValidatorClientFullConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
15
- import { type L1ToL2MessageSource } from '@aztec/stdlib/messaging';
15
+ import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
16
16
  import { type BlockProposal, type BlockProposalOptions, type CheckpointAttestation, CheckpointProposal, type CheckpointProposalCore, type CheckpointProposalOptions } from '@aztec/stdlib/p2p';
17
17
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
18
18
  import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
19
19
  import { type TelemetryClient, type Tracer } from '@aztec/telemetry-client';
20
- import { type SigningContext } from '@aztec/validator-ha-signer/types';
20
+ import { type SigningContext, type SlashingProtectionDatabase } from '@aztec/validator-ha-signer/types';
21
21
  import type { ValidatorHASigner } from '@aztec/validator-ha-signer/validator-ha-signer';
22
22
  import type { TypedDataDefinition } from 'viem';
23
- import { BlockProposalHandler } from './block_proposal_handler.js';
24
23
  import type { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
25
24
  import type { ExtendedValidatorKeyStore } from './key_store/interface.js';
25
+ import { ProposalHandler } from './proposal_handler.js';
26
26
  declare const ValidatorClient_base: new () => WatcherEmitter;
27
27
  /**
28
28
  * Validator Client
@@ -31,11 +31,7 @@ export declare class ValidatorClient extends ValidatorClient_base implements Val
31
31
  private keyStore;
32
32
  private epochCache;
33
33
  private p2pClient;
34
- private blockProposalHandler;
35
- private blockSource;
36
- private checkpointsBuilder;
37
- private worldState;
38
- private l1ToL2MessageSource;
34
+ private proposalHandler;
39
35
  private config;
40
36
  private blobClient;
41
37
  private haSigner;
@@ -51,15 +47,17 @@ export declare class ValidatorClient extends ValidatorClient_base implements Val
51
47
  private lastProposedCheckpoint?;
52
48
  private lastEpochForCommitteeUpdateLoop;
53
49
  private epochCacheUpdateLoop;
50
+ /** Tracks the last epoch in which each attester successfully submitted at least one attestation. */
51
+ private lastAttestedEpochByAttester;
54
52
  private proposersOfInvalidBlocks;
55
53
  /** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
56
54
  private lastAttestedProposal?;
57
- protected constructor(keyStore: ExtendedValidatorKeyStore, epochCache: EpochCache, p2pClient: P2P, blockProposalHandler: BlockProposalHandler, blockSource: L2BlockSource, checkpointsBuilder: FullNodeCheckpointsBuilder, worldState: WorldStateSynchronizer, l1ToL2MessageSource: L1ToL2MessageSource, config: ValidatorClientFullConfig, blobClient: BlobClientInterface, haSigner: ValidatorHASigner | undefined, dateProvider?: DateProvider, telemetry?: TelemetryClient, log?: Logger);
55
+ protected constructor(keyStore: ExtendedValidatorKeyStore, epochCache: EpochCache, p2pClient: P2P, proposalHandler: ProposalHandler, config: ValidatorClientFullConfig, blobClient: BlobClientInterface, haSigner: ValidatorHASigner | undefined, dateProvider?: DateProvider, telemetry?: TelemetryClient, log?: Logger);
58
56
  static validateKeyStoreConfiguration(keyStoreManager: KeystoreManager, logger?: Logger): void;
59
57
  private handleEpochCommitteeUpdate;
60
- static new(config: ValidatorClientFullConfig, checkpointsBuilder: FullNodeCheckpointsBuilder, worldState: WorldStateSynchronizer, epochCache: EpochCache, p2pClient: P2P, blockSource: L2BlockSource & L2BlockSink, l1ToL2MessageSource: L1ToL2MessageSource, txProvider: ITxProvider, keyStoreManager: KeystoreManager, blobClient: BlobClientInterface, dateProvider?: DateProvider, telemetry?: TelemetryClient): Promise<ValidatorClient>;
58
+ static new(config: ValidatorClientFullConfig, checkpointsBuilder: FullNodeCheckpointsBuilder, worldState: WorldStateSynchronizer, epochCache: EpochCache, p2pClient: P2P, blockSource: L2BlockSource & L2BlockSink, l1ToL2MessageSource: L1ToL2MessageSource, txProvider: ITxProvider, keyStoreManager: KeystoreManager, blobClient: BlobClientInterface, dateProvider?: DateProvider, telemetry?: TelemetryClient, slashingProtectionDb?: SlashingProtectionDatabase): Promise<ValidatorClient>;
61
59
  getValidatorAddresses(): EthAddress[];
62
- getBlockProposalHandler(): BlockProposalHandler;
60
+ getProposalHandler(): ProposalHandler;
63
61
  signWithAddress(addr: EthAddress, msg: TypedDataDefinition, context: SigningContext): Promise<Signature>;
64
62
  getCoinbaseForAttestor(attestor: EthAddress): EthAddress;
65
63
  getFeeRecipientForAttestor(attestor: EthAddress): AztecAddress;
@@ -89,15 +87,6 @@ export declare class ValidatorClient extends ValidatorClient_base implements Val
89
87
  */
90
88
  private shouldAttestToSlot;
91
89
  private createCheckpointAttestationsFromProposal;
92
- private validateCheckpointProposal;
93
- /**
94
- * Extract checkpoint global variables from a block.
95
- */
96
- private extractCheckpointConstants;
97
- /**
98
- * Uploads blobs for a checkpoint to the filestore (fire and forget).
99
- */
100
- protected uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void>;
101
90
  private slashInvalidBlock;
102
91
  /**
103
92
  * Handle detection of a duplicate proposal (equivocation).
@@ -118,4 +107,4 @@ export declare class ValidatorClient extends ValidatorClient_base implements Val
118
107
  private handleAuthRequest;
119
108
  }
120
109
  export {};
121
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmFsaWRhdG9yLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvdmFsaWRhdG9yLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxFQUFFLG1CQUFtQixFQUFFLE1BQU0sMkJBQTJCLENBQUM7QUFFckUsT0FBTyxLQUFLLEVBQUUsVUFBVSxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFFckQsT0FBTyxFQUNMLFdBQVcsRUFDWCxnQkFBZ0IsRUFFaEIscUJBQXFCLEVBQ3JCLFVBQVUsRUFDWCxNQUFNLGlDQUFpQyxDQUFDO0FBQ3pDLE9BQU8sRUFBRSxFQUFFLEVBQUUsTUFBTSxnQ0FBZ0MsQ0FBQztBQUVwRCxPQUFPLEtBQUssRUFBRSxVQUFVLEVBQUUsTUFBTSwrQkFBK0IsQ0FBQztBQUNoRSxPQUFPLEtBQUssRUFBRSxTQUFTLEVBQUUsTUFBTSxpQ0FBaUMsQ0FBQztBQUNqRSxPQUFPLEVBQUUsS0FBSyxPQUFPLEVBQUUsS0FBSyxNQUFNLEVBQWdCLE1BQU0sdUJBQXVCLENBQUM7QUFJaEYsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQ3ZELE9BQU8sS0FBSyxFQUFFLGVBQWUsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBQzVELE9BQU8sS0FBSyxFQUFtRCxHQUFHLEVBQUUsTUFBTSxFQUFFLE1BQU0sWUFBWSxDQUFDO0FBRS9GLE9BQU8sRUFBb0MsS0FBSyxPQUFPLEVBQUUsS0FBSyxjQUFjLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUNyRyxPQUFPLEtBQUssRUFBRSxZQUFZLEVBQUUsTUFBTSw2QkFBNkIsQ0FBQztBQUNoRSxPQUFPLEtBQUssRUFBRSwrQkFBK0IsRUFBVyxXQUFXLEVBQUUsYUFBYSxFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFFaEgsT0FBTyxLQUFLLEVBQ1YscUNBQXFDLEVBQ3JDLFdBQVcsRUFDWCxTQUFTLEVBQ1QseUJBQXlCLEVBQ3pCLHNCQUFzQixFQUN2QixN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
110
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmFsaWRhdG9yLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvdmFsaWRhdG9yLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxFQUFFLG1CQUFtQixFQUFFLE1BQU0sMkJBQTJCLENBQUM7QUFDckUsT0FBTyxLQUFLLEVBQUUsVUFBVSxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDckQsT0FBTyxFQUNMLFdBQVcsRUFDWCxnQkFBZ0IsRUFFaEIscUJBQXFCLEVBQ3JCLFVBQVUsRUFDWCxNQUFNLGlDQUFpQyxDQUFDO0FBQ3pDLE9BQU8sRUFBRSxFQUFFLEVBQUUsTUFBTSxnQ0FBZ0MsQ0FBQztBQUNwRCxPQUFPLEtBQUssRUFBRSxVQUFVLEVBQUUsTUFBTSwrQkFBK0IsQ0FBQztBQUNoRSxPQUFPLEtBQUssRUFBRSxTQUFTLEVBQUUsTUFBTSxpQ0FBaUMsQ0FBQztBQUNqRSxPQUFPLEVBQUUsS0FBSyxNQUFNLEVBQWdCLE1BQU0sdUJBQXVCLENBQUM7QUFHbEUsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQ3ZELE9BQU8sS0FBSyxFQUFFLGVBQWUsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBQzVELE9BQU8sS0FBSyxFQUFtRCxHQUFHLEVBQUUsTUFBTSxFQUFFLE1BQU0sWUFBWSxDQUFDO0FBRS9GLE9BQU8sRUFBb0MsS0FBSyxPQUFPLEVBQUUsS0FBSyxjQUFjLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUNyRyxPQUFPLEtBQUssRUFBRSxZQUFZLEVBQUUsTUFBTSw2QkFBNkIsQ0FBQztBQUNoRSxPQUFPLEtBQUssRUFBRSwrQkFBK0IsRUFBRSxXQUFXLEVBQUUsYUFBYSxFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFFdkcsT0FBTyxLQUFLLEVBQ1YscUNBQXFDLEVBQ3JDLFdBQVcsRUFDWCxTQUFTLEVBQ1QseUJBQXlCLEVBQ3pCLHNCQUFzQixFQUN2QixNQUFNLGlDQUFpQyxDQUFD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