@aztec/validator-client 0.0.1-commit.96bb3f7 → 0.0.1-commit.a072138

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 (57) hide show
  1. package/README.md +41 -15
  2. package/dest/block_proposal_handler.d.ts +8 -8
  3. package/dest/block_proposal_handler.d.ts.map +1 -1
  4. package/dest/block_proposal_handler.js +27 -32
  5. package/dest/checkpoint_builder.d.ts +21 -25
  6. package/dest/checkpoint_builder.d.ts.map +1 -1
  7. package/dest/checkpoint_builder.js +50 -32
  8. package/dest/config.d.ts +1 -1
  9. package/dest/config.d.ts.map +1 -1
  10. package/dest/config.js +8 -14
  11. package/dest/duties/validation_service.d.ts +19 -6
  12. package/dest/duties/validation_service.d.ts.map +1 -1
  13. package/dest/duties/validation_service.js +72 -19
  14. package/dest/factory.d.ts +2 -2
  15. package/dest/factory.d.ts.map +1 -1
  16. package/dest/factory.js +1 -1
  17. package/dest/key_store/ha_key_store.d.ts +99 -0
  18. package/dest/key_store/ha_key_store.d.ts.map +1 -0
  19. package/dest/key_store/ha_key_store.js +208 -0
  20. package/dest/key_store/index.d.ts +2 -1
  21. package/dest/key_store/index.d.ts.map +1 -1
  22. package/dest/key_store/index.js +1 -0
  23. package/dest/key_store/interface.d.ts +36 -6
  24. package/dest/key_store/interface.d.ts.map +1 -1
  25. package/dest/key_store/local_key_store.d.ts +10 -5
  26. package/dest/key_store/local_key_store.d.ts.map +1 -1
  27. package/dest/key_store/local_key_store.js +8 -4
  28. package/dest/key_store/node_keystore_adapter.d.ts +18 -5
  29. package/dest/key_store/node_keystore_adapter.d.ts.map +1 -1
  30. package/dest/key_store/node_keystore_adapter.js +18 -4
  31. package/dest/key_store/web3signer_key_store.d.ts +10 -5
  32. package/dest/key_store/web3signer_key_store.d.ts.map +1 -1
  33. package/dest/key_store/web3signer_key_store.js +8 -4
  34. package/dest/metrics.d.ts +4 -3
  35. package/dest/metrics.d.ts.map +1 -1
  36. package/dest/metrics.js +34 -5
  37. package/dest/tx_validator/tx_validator_factory.d.ts +4 -3
  38. package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
  39. package/dest/tx_validator/tx_validator_factory.js +17 -16
  40. package/dest/validator.d.ts +13 -13
  41. package/dest/validator.d.ts.map +1 -1
  42. package/dest/validator.js +82 -80
  43. package/package.json +21 -17
  44. package/src/block_proposal_handler.ts +41 -42
  45. package/src/checkpoint_builder.ts +85 -38
  46. package/src/config.ts +7 -13
  47. package/src/duties/validation_service.ts +91 -23
  48. package/src/factory.ts +1 -0
  49. package/src/key_store/ha_key_store.ts +269 -0
  50. package/src/key_store/index.ts +1 -0
  51. package/src/key_store/interface.ts +44 -5
  52. package/src/key_store/local_key_store.ts +13 -4
  53. package/src/key_store/node_keystore_adapter.ts +27 -4
  54. package/src/key_store/web3signer_key_store.ts +17 -4
  55. package/src/metrics.ts +45 -6
  56. package/src/tx_validator/tx_validator_factory.ts +52 -31
  57. package/src/validator.ts +98 -93
package/dest/validator.js CHANGED
@@ -8,11 +8,16 @@ import { sleep } from '@aztec/foundation/sleep';
8
8
  import { DateProvider } from '@aztec/foundation/timer';
9
9
  import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
10
10
  import { OffenseType, WANT_TO_SLASH_EVENT } from '@aztec/slasher';
11
+ import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
12
+ import { accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
11
13
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
12
14
  import { getTelemetryClient } from '@aztec/telemetry-client';
15
+ import { createHASigner } from '@aztec/validator-ha-signer/factory';
16
+ import { DutyType } from '@aztec/validator-ha-signer/types';
13
17
  import { EventEmitter } from 'events';
14
18
  import { BlockProposalHandler } from './block_proposal_handler.js';
15
19
  import { ValidationService } from './duties/validation_service.js';
20
+ import { HAKeyStore } from './key_store/ha_key_store.js';
16
21
  import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
17
22
  import { ValidatorMetrics } from './metrics.js';
18
23
  // We maintain a set of proposers who have proposed invalid blocks.
@@ -48,12 +53,8 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
48
53
  lastEpochForCommitteeUpdateLoop;
49
54
  epochCacheUpdateLoop;
50
55
  proposersOfInvalidBlocks;
51
- // TODO(palla/mbps): Remove this once checkpoint validation is stable and we can validate all blocks properly.
52
- // Tracks slots for which we have successfully validated a block proposal, so we can attest to checkpoint proposals for those slots.
53
- // eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
54
- validatedBlockSlots;
55
56
  constructor(keyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
56
- super(), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.blockProposalHandler = blockProposalHandler, this.blockSource = blockSource, this.checkpointsBuilder = checkpointsBuilder, this.worldState = worldState, this.l1ToL2MessageSource = l1ToL2MessageSource, this.config = config, this.blobClient = blobClient, this.dateProvider = dateProvider, this.hasRegisteredHandlers = false, this.proposersOfInvalidBlocks = new Set(), this.validatedBlockSlots = new Set();
57
+ super(), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.blockProposalHandler = blockProposalHandler, this.blockSource = blockSource, this.checkpointsBuilder = checkpointsBuilder, this.worldState = worldState, this.l1ToL2MessageSource = l1ToL2MessageSource, this.config = config, this.blobClient = blobClient, this.dateProvider = dateProvider, this.hasRegisteredHandlers = false, this.proposersOfInvalidBlocks = new Set();
57
58
  // Create child logger with fisherman prefix if in fisherman mode
58
59
  this.log = config.fishermanMode ? log.createChild('[FISHERMAN]') : log;
59
60
  this.tracer = telemetry.getTracer('Validator');
@@ -107,13 +108,23 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
107
108
  this.log.error(`Error updating epoch committee`, err);
108
109
  }
109
110
  }
110
- static new(config, checkpointsBuilder, worldState, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, keyStoreManager, blobClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient()) {
111
+ static async new(config, checkpointsBuilder, worldState, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, keyStoreManager, blobClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient()) {
111
112
  const metrics = new ValidatorMetrics(telemetry);
112
113
  const blockProposalValidator = new BlockProposalValidator(epochCache, {
113
114
  txsPermitted: !config.disableTransactions
114
115
  });
115
- const blockProposalHandler = new BlockProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, config, metrics, dateProvider, telemetry);
116
- const validator = new ValidatorClient(NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager), epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, dateProvider, telemetry);
116
+ const blockProposalHandler = new BlockProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, config, metrics, dateProvider, telemetry);
117
+ let validatorKeyStore = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
118
+ if (config.haSigningEnabled) {
119
+ // If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
120
+ const haConfig = {
121
+ ...config,
122
+ maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000
123
+ };
124
+ const { signer } = await createHASigner(haConfig);
125
+ validatorKeyStore = new HAKeyStore(validatorKeyStore, signer);
126
+ }
127
+ const validator = new ValidatorClient(validatorKeyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, dateProvider, telemetry);
117
128
  return validator;
118
129
  }
119
130
  getValidatorAddresses() {
@@ -122,8 +133,8 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
122
133
  getBlockProposalHandler() {
123
134
  return this.blockProposalHandler;
124
135
  }
125
- signWithAddress(addr, msg) {
126
- return this.keyStore.signTypedDataWithAddress(addr, msg);
136
+ signWithAddress(addr, msg, context) {
137
+ return this.keyStore.signTypedDataWithAddress(addr, msg, context);
127
138
  }
128
139
  getCoinbaseForAttestor(attestor) {
129
140
  return this.keyStore.getCoinbaseAddress(attestor);
@@ -145,6 +156,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
145
156
  this.log.warn(`Validator client already started`);
146
157
  return;
147
158
  }
159
+ await this.keyStore.start();
148
160
  await this.registerHandlers();
149
161
  const myAddresses = this.getValidatorAddresses();
150
162
  const inCommittee = await this.epochCache.filterInCommittee('now', myAddresses);
@@ -157,6 +169,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
157
169
  }
158
170
  async stop() {
159
171
  await this.epochCacheUpdateLoop.stop();
172
+ await this.keyStore.stop();
160
173
  }
161
174
  /** Register handlers on the p2p client */ async registerHandlers() {
162
175
  if (!this.hasRegisteredHandlers) {
@@ -181,6 +194,9 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
181
194
  * @returns true if the proposal is valid, false otherwise
182
195
  */ async validateBlockProposal(proposal, proposalSender) {
183
196
  const slotNumber = proposal.slotNumber;
197
+ // Note: During escape hatch, we still want to "validate" proposals for observability,
198
+ // but we intentionally reject them and disable slashing invalid block and attestation flow.
199
+ const escapeHatchOpen = await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber);
184
200
  const proposer = proposal.getSender();
185
201
  // Reject proposals with invalid signatures
186
202
  if (!proposer) {
@@ -203,7 +219,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
203
219
  // In fisherman mode, we always reexecute to validate proposals.
204
220
  const { validatorReexecute, slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } = this.config;
205
221
  const shouldReexecute = fishermanMode || slashBroadcastedInvalidBlockPenalty > 0n && validatorReexecute || partOfCommittee && validatorReexecute || alwaysReexecuteBlockProposals || this.blobClient.canUpload();
206
- const validationResult = await this.blockProposalHandler.handleBlockProposal(proposal, proposalSender, !!shouldReexecute);
222
+ const validationResult = await this.blockProposalHandler.handleBlockProposal(proposal, proposalSender, !!shouldReexecute && !escapeHatchOpen);
207
223
  if (!validationResult.isValid) {
208
224
  this.log.warn(`Block proposal validation failed: ${validationResult.reason}`, proposalInfo);
209
225
  const reason = validationResult.reason || 'unknown';
@@ -222,7 +238,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
222
238
  this.metrics.incFailedAttestationsNodeIssue(1, reason, partOfCommittee);
223
239
  }
224
240
  // Slash invalid block proposals (can happen even when not in committee)
225
- if (validationResult.reason && SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) && slashBroadcastedInvalidBlockPenalty > 0n) {
241
+ if (!escapeHatchOpen && validationResult.reason && SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) && slashBroadcastedInvalidBlockPenalty > 0n) {
226
242
  this.log.warn(`Slashing proposer for invalid block proposal`, proposalInfo);
227
243
  this.slashInvalidBlock(proposal);
228
244
  }
@@ -231,11 +247,13 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
231
247
  this.log.info(`Validated block proposal for slot ${slotNumber}`, {
232
248
  ...proposalInfo,
233
249
  inCommittee: partOfCommittee,
234
- fishermanMode: this.config.fishermanMode || false
250
+ fishermanMode: this.config.fishermanMode || false,
251
+ escapeHatchOpen
235
252
  });
236
- // TODO(palla/mbps): Remove this once checkpoint validation is stable.
237
- // Track that we successfully validated a block for this slot, so we can attest to checkpoint proposals for it.
238
- this.validatedBlockSlots.add(slotNumber);
253
+ if (escapeHatchOpen) {
254
+ this.log.warn(`Escape hatch open for slot ${slotNumber}, rejecting block proposal`, proposalInfo);
255
+ return false;
256
+ }
239
257
  return true;
240
258
  }
241
259
  /**
@@ -246,6 +264,11 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
246
264
  */ async attestToCheckpointProposal(proposal, _proposalSender) {
247
265
  const slotNumber = proposal.slotNumber;
248
266
  const proposer = proposal.getSender();
267
+ // If escape hatch is open for this slot's epoch, do not attest.
268
+ if (await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber)) {
269
+ this.log.warn(`Escape hatch open for slot ${slotNumber}, skipping checkpoint attestation handling`);
270
+ return undefined;
271
+ }
249
272
  // Reject proposals with invalid signatures
250
273
  if (!proposer) {
251
274
  this.log.warn(`Received checkpoint proposal with invalid signature for slot ${slotNumber}`);
@@ -265,16 +288,9 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
265
288
  txHashes: proposal.txHashes.map((t)=>t.toString()),
266
289
  fishermanMode: this.config.fishermanMode || false
267
290
  });
268
- // TODO(palla/mbps): Remove this once checkpoint validation is stable.
269
- // Check that we have successfully validated a block for this slot before attesting to the checkpoint.
270
- if (!this.validatedBlockSlots.has(slotNumber)) {
271
- this.log.warn(`No validated block found for slot ${slotNumber}, refusing to attest to checkpoint`, proposalInfo);
272
- return undefined;
273
- }
274
291
  // Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
275
- // TODO(palla/mbps): Change default to false once checkpoint validation is stable.
276
- if (this.config.skipCheckpointProposalValidation !== false) {
277
- this.log.verbose(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
292
+ if (this.config.skipCheckpointProposalValidation) {
293
+ this.log.warn(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
278
294
  } else {
279
295
  const validationResult = await this.validateCheckpointProposal(proposal, proposalInfo);
280
296
  if (!validationResult.isValid) {
@@ -333,7 +349,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
333
349
  * @returns Validation result with isValid flag and reason if invalid.
334
350
  */ async validateCheckpointProposal(proposal, proposalInfo) {
335
351
  const slot = proposal.slotNumber;
336
- const timeoutSeconds = 10;
352
+ const timeoutSeconds = 10; // TODO(palla/mbps): This should map to the timetable settings
337
353
  // Wait for last block to sync by archive
338
354
  let lastBlockHeader;
339
355
  try {
@@ -362,18 +378,8 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
362
378
  reason: 'last_block_not_found'
363
379
  };
364
380
  }
365
- // Get the last full block to determine checkpoint number
366
- const lastBlock = await this.blockSource.getL2BlockNew(lastBlockHeader.getBlockNumber());
367
- if (!lastBlock) {
368
- this.log.warn(`Last block ${lastBlockHeader.getBlockNumber()} not found`, proposalInfo);
369
- return {
370
- isValid: false,
371
- reason: 'last_block_not_found'
372
- };
373
- }
374
- const checkpointNumber = lastBlock.checkpointNumber;
375
381
  // Get all full blocks for the slot and checkpoint
376
- const blocks = await this.getBlocksForSlot(slot, lastBlockHeader, checkpointNumber);
382
+ const blocks = await this.blockSource.getBlocksForSlot(slot);
377
383
  if (blocks.length === 0) {
378
384
  this.log.warn(`No blocks found for slot ${slot}`, proposalInfo);
379
385
  return {
@@ -388,14 +394,21 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
388
394
  // Get checkpoint constants from first block
389
395
  const firstBlock = blocks[0];
390
396
  const constants = this.extractCheckpointConstants(firstBlock);
397
+ const checkpointNumber = firstBlock.checkpointNumber;
391
398
  // Get L1-to-L2 messages for this checkpoint
392
399
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
400
+ // Compute the previous checkpoint out hashes for the epoch.
401
+ // TODO: There can be a more efficient way to get the previous checkpoint out hashes without having to fetch the
402
+ // actual checkpoints and the blocks/txs in them.
403
+ const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
404
+ const previousCheckpoints = (await this.blockSource.getCheckpointsForEpoch(epoch)).filter((b)=>b.number < checkpointNumber).sort((a, b)=>a.number - b.number);
405
+ const previousCheckpointOutHashes = previousCheckpoints.map((c)=>c.getCheckpointOutHash());
393
406
  // Fork world state at the block before the first block
394
407
  const parentBlockNumber = BlockNumber(firstBlock.number - 1);
395
408
  const fork = await this.worldState.fork(parentBlockNumber);
396
409
  try {
397
410
  // Create checkpoint builder with all existing blocks
398
- const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, l1ToL2Messages, fork, blocks);
411
+ const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, l1ToL2Messages, previousCheckpointOutHashes, fork, blocks, this.log.getBindings());
399
412
  // Complete the checkpoint to get computed values
400
413
  const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
401
414
  // Compare checkpoint header with proposal
@@ -422,6 +435,27 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
422
435
  reason: 'archive_mismatch'
423
436
  };
424
437
  }
438
+ // Check that the accumulated epoch out hash matches the value in the proposal.
439
+ // The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
440
+ const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
441
+ const computedEpochOutHash = accumulateCheckpointOutHashes([
442
+ ...previousCheckpointOutHashes,
443
+ checkpointOutHash
444
+ ]);
445
+ const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
446
+ if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
447
+ this.log.warn(`Epoch out hash mismatch`, {
448
+ proposalEpochOutHash: proposalEpochOutHash.toString(),
449
+ computedEpochOutHash: computedEpochOutHash.toString(),
450
+ checkpointOutHash: checkpointOutHash.toString(),
451
+ previousCheckpointOutHashes: previousCheckpointOutHashes.map((h)=>h.toString()),
452
+ ...proposalInfo
453
+ });
454
+ return {
455
+ isValid: false,
456
+ reason: 'out_hash_mismatch'
457
+ };
458
+ }
425
459
  this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
426
460
  return {
427
461
  isValid: true
@@ -431,36 +465,6 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
431
465
  }
432
466
  }
433
467
  /**
434
- * Get all full blocks for a given slot and checkpoint by walking backwards from the last block.
435
- * Returns blocks in ascending order (earliest to latest).
436
- * TODO(palla/mbps): Add getL2BlocksForSlot() to L2BlockSource interface for efficiency.
437
- */ async getBlocksForSlot(slot, lastBlockHeader, checkpointNumber) {
438
- const blocks = [];
439
- let currentHeader = lastBlockHeader;
440
- const { genesisArchiveRoot } = await this.blockSource.getGenesisValues();
441
- while(currentHeader.getSlot() === slot){
442
- const block = await this.blockSource.getL2BlockNew(currentHeader.getBlockNumber());
443
- if (!block) {
444
- this.log.warn(`Block ${currentHeader.getBlockNumber()} not found while getting blocks for slot ${slot}`);
445
- break;
446
- }
447
- if (block.checkpointNumber !== checkpointNumber) {
448
- break;
449
- }
450
- blocks.unshift(block);
451
- const prevArchive = currentHeader.lastArchive.root;
452
- if (prevArchive.equals(genesisArchiveRoot)) {
453
- break;
454
- }
455
- const prevHeader = await this.blockSource.getBlockHeaderByArchive(prevArchive);
456
- if (!prevHeader || prevHeader.getSlot() !== slot) {
457
- break;
458
- }
459
- currentHeader = prevHeader;
460
- }
461
- return blocks;
462
- }
463
- /**
464
468
  * Extract checkpoint global variables from a block.
465
469
  */ extractCheckpointConstants(block) {
466
470
  const gv = block.header.globalVariables;
@@ -482,13 +486,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
482
486
  this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
483
487
  return;
484
488
  }
485
- // Get the last full block to determine checkpoint number
486
- const lastBlock = await this.blockSource.getL2BlockNew(lastBlockHeader.getBlockNumber());
487
- if (!lastBlock) {
488
- this.log.warn(`Failed to get last block for blob upload`, proposalInfo);
489
- return;
490
- }
491
- const blocks = await this.getBlocksForSlot(proposal.slotNumber, lastBlockHeader, lastBlock.checkpointNumber);
489
+ const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
492
490
  if (blocks.length === 0) {
493
491
  this.log.warn(`No blocks found for blob upload`, proposalInfo);
494
492
  return;
@@ -526,7 +524,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
526
524
  }
527
525
  ]);
528
526
  }
529
- async createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, options) {
527
+ async createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, options = {}) {
530
528
  // TODO(palla/mbps): Prevent double proposals properly
531
529
  // if (this.previousProposal?.slotNumber === blockHeader.globalVariables.slotNumber) {
532
530
  // this.log.verbose(`Already made a proposal for the same slot, skipping proposal`);
@@ -540,15 +538,15 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
540
538
  this.previousProposal = newProposal;
541
539
  return newProposal;
542
540
  }
543
- async createCheckpointProposal(checkpointHeader, archive, lastBlockInfo, proposerAddress, options) {
541
+ async createCheckpointProposal(checkpointHeader, archive, lastBlockInfo, proposerAddress, options = {}) {
544
542
  this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
545
543
  return await this.validationService.createCheckpointProposal(checkpointHeader, archive, lastBlockInfo, proposerAddress, options);
546
544
  }
547
545
  async broadcastBlockProposal(proposal) {
548
546
  await this.p2pClient.broadcastProposal(proposal);
549
547
  }
550
- async signAttestationsAndSigners(attestationsAndSigners, proposer) {
551
- return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer);
548
+ async signAttestationsAndSigners(attestationsAndSigners, proposer, slot, blockNumber) {
549
+ return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer, slot, blockNumber);
552
550
  }
553
551
  async collectOwnAttestations(proposal) {
554
552
  const slot = proposal.slotNumber;
@@ -630,7 +628,11 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
630
628
  return Buffer.alloc(0);
631
629
  }
632
630
  const payloadToSign = authRequest.getPayloadToSign();
633
- const signature = await this.keyStore.signMessageWithAddress(addressToUse, payloadToSign);
631
+ // AUTH_REQUEST doesn't require HA protection - multiple signatures are safe
632
+ const context = {
633
+ dutyType: DutyType.AUTH_REQUEST
634
+ };
635
+ const signature = await this.keyStore.signMessageWithAddress(addressToUse, payloadToSign, context);
634
636
  const authResponse = new AuthResponse(statusMessage, signature);
635
637
  return authResponse.toBuffer();
636
638
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/validator-client",
3
- "version": "0.0.1-commit.96bb3f7",
3
+ "version": "0.0.1-commit.a072138",
4
4
  "main": "dest/index.js",
5
5
  "type": "module",
6
6
  "exports": {
@@ -64,31 +64,35 @@
64
64
  ]
65
65
  },
66
66
  "dependencies": {
67
- "@aztec/blob-client": "0.0.1-commit.96bb3f7",
68
- "@aztec/blob-lib": "0.0.1-commit.96bb3f7",
69
- "@aztec/constants": "0.0.1-commit.96bb3f7",
70
- "@aztec/epoch-cache": "0.0.1-commit.96bb3f7",
71
- "@aztec/ethereum": "0.0.1-commit.96bb3f7",
72
- "@aztec/foundation": "0.0.1-commit.96bb3f7",
73
- "@aztec/node-keystore": "0.0.1-commit.96bb3f7",
74
- "@aztec/noir-protocol-circuits-types": "0.0.1-commit.96bb3f7",
75
- "@aztec/p2p": "0.0.1-commit.96bb3f7",
76
- "@aztec/protocol-contracts": "0.0.1-commit.96bb3f7",
77
- "@aztec/prover-client": "0.0.1-commit.96bb3f7",
78
- "@aztec/simulator": "0.0.1-commit.96bb3f7",
79
- "@aztec/slasher": "0.0.1-commit.96bb3f7",
80
- "@aztec/stdlib": "0.0.1-commit.96bb3f7",
81
- "@aztec/telemetry-client": "0.0.1-commit.96bb3f7",
67
+ "@aztec/blob-client": "0.0.1-commit.a072138",
68
+ "@aztec/blob-lib": "0.0.1-commit.a072138",
69
+ "@aztec/constants": "0.0.1-commit.a072138",
70
+ "@aztec/epoch-cache": "0.0.1-commit.a072138",
71
+ "@aztec/ethereum": "0.0.1-commit.a072138",
72
+ "@aztec/foundation": "0.0.1-commit.a072138",
73
+ "@aztec/node-keystore": "0.0.1-commit.a072138",
74
+ "@aztec/noir-protocol-circuits-types": "0.0.1-commit.a072138",
75
+ "@aztec/p2p": "0.0.1-commit.a072138",
76
+ "@aztec/protocol-contracts": "0.0.1-commit.a072138",
77
+ "@aztec/prover-client": "0.0.1-commit.a072138",
78
+ "@aztec/simulator": "0.0.1-commit.a072138",
79
+ "@aztec/slasher": "0.0.1-commit.a072138",
80
+ "@aztec/stdlib": "0.0.1-commit.a072138",
81
+ "@aztec/telemetry-client": "0.0.1-commit.a072138",
82
+ "@aztec/validator-ha-signer": "0.0.1-commit.a072138",
82
83
  "koa": "^2.16.1",
83
84
  "koa-router": "^13.1.1",
84
85
  "tslib": "^2.4.0",
85
86
  "viem": "npm:@aztec/viem@2.38.2"
86
87
  },
87
88
  "devDependencies": {
89
+ "@aztec/archiver": "0.0.1-commit.a072138",
90
+ "@aztec/world-state": "0.0.1-commit.a072138",
91
+ "@electric-sql/pglite": "^0.3.14",
88
92
  "@jest/globals": "^30.0.0",
89
93
  "@types/jest": "^30.0.0",
90
94
  "@types/node": "^22.15.17",
91
- "@typescript/native-preview": "7.0.0-dev.20251126.1",
95
+ "@typescript/native-preview": "7.0.0-dev.20260113.1",
92
96
  "jest": "^30.0.0",
93
97
  "jest-mock-extended": "^4.0.0",
94
98
  "ts-node": "^10.9.1",
@@ -1,17 +1,22 @@
1
1
  import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
2
+ import type { EpochCache } from '@aztec/epoch-cache';
2
3
  import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
4
+ import { chunkBy } from '@aztec/foundation/collection';
3
5
  import { Fr } from '@aztec/foundation/curves/bn254';
4
6
  import { TimeoutError } from '@aztec/foundation/error';
5
7
  import { createLogger } from '@aztec/foundation/log';
6
8
  import { retryUntil } from '@aztec/foundation/retry';
7
9
  import { DateProvider, Timer } from '@aztec/foundation/timer';
8
10
  import type { P2P, PeerId } from '@aztec/p2p';
9
- import { TxProvider } from '@aztec/p2p';
10
11
  import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
11
- import type { L2BlockNew, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
12
- import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
13
- import type { ValidatorClientFullConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
14
- import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
12
+ import type { L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
13
+ import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
14
+ import type { ITxProvider, ValidatorClientFullConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
15
+ import {
16
+ type L1ToL2MessageSource,
17
+ computeCheckpointOutHash,
18
+ computeInHashFromL1ToL2Messages,
19
+ } from '@aztec/stdlib/messaging';
15
20
  import type { BlockProposal } from '@aztec/stdlib/p2p';
16
21
  import { BlockHeader, type CheckpointGlobalVariables, type FailedTx, type Tx } from '@aztec/stdlib/tx';
17
22
  import {
@@ -39,7 +44,7 @@ export type BlockProposalValidationFailureReason =
39
44
  | 'unknown_error';
40
45
 
41
46
  type ReexecuteTransactionsResult = {
42
- block: L2BlockNew;
47
+ block: L2Block;
43
48
  failedTxs: FailedTx[];
44
49
  reexecutionTimeMs: number;
45
50
  totalManaUsed: number;
@@ -72,8 +77,9 @@ export class BlockProposalHandler {
72
77
  private worldState: WorldStateSynchronizer,
73
78
  private blockSource: L2BlockSource & L2BlockSink,
74
79
  private l1ToL2MessageSource: L1ToL2MessageSource,
75
- private txProvider: TxProvider,
80
+ private txProvider: ITxProvider,
76
81
  private blockProposalValidator: BlockProposalValidator,
82
+ private epochCache: EpochCache,
77
83
  private config: ValidatorClientFullConfig,
78
84
  private metrics?: ValidatorMetrics,
79
85
  private dateProvider: DateProvider = new DateProvider(),
@@ -140,8 +146,8 @@ export class BlockProposalHandler {
140
146
 
141
147
  // Check that the proposal is from the current proposer, or the next proposer
142
148
  // This should have been handled by the p2p layer, but we double check here out of caution
143
- const invalidProposal = await this.blockProposalValidator.validate(proposal);
144
- if (invalidProposal) {
149
+ const validationResult = await this.blockProposalValidator.validate(proposal);
150
+ if (validationResult.result !== 'accept') {
145
151
  this.log.warn(`Proposal is not valid, skipping processing`, proposalInfo);
146
152
  return { isValid: false, reason: 'invalid_proposal' };
147
153
  }
@@ -153,9 +159,9 @@ export class BlockProposalHandler {
153
159
  return { isValid: false, reason: 'parent_block_not_found' };
154
160
  }
155
161
 
156
- // Check that the parent block's slot is less than the proposal's slot (should not happen, but we check anyway)
157
- if (parentBlockHeader !== 'genesis' && parentBlockHeader.getSlot() >= slotNumber) {
158
- this.log.warn(`Parent block slot is greater than or equal to proposal slot, skipping processing`, {
162
+ // Check that the parent block's slot is not greater than the proposal's slot.
163
+ if (parentBlockHeader !== 'genesis' && parentBlockHeader.getSlot() > slotNumber) {
164
+ this.log.warn(`Parent block slot is greater than proposal slot, skipping processing`, {
159
165
  parentBlockSlot: parentBlockHeader.getSlot().toString(),
160
166
  proposalSlot: slotNumber.toString(),
161
167
  ...proposalInfo,
@@ -212,6 +218,18 @@ export class BlockProposalHandler {
212
218
  // Try re-executing the transactions in the proposal if needed
213
219
  let reexecutionResult;
214
220
  if (shouldReexecute) {
221
+ // Compute the previous checkpoint out hashes for the epoch.
222
+ // TODO(leila/mbps): There can be a more efficient way to get the previous checkpoint out
223
+ // hashes without having to fetch all the blocks.
224
+ const epoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
225
+ const checkpointedBlocks = (await this.blockSource.getCheckpointedBlocksForEpoch(epoch))
226
+ .filter(b => b.block.number < blockNumber)
227
+ .sort((a, b) => a.block.number - b.block.number);
228
+ const blocksByCheckpoint = chunkBy(checkpointedBlocks, b => b.checkpointNumber);
229
+ const previousCheckpointOutHashes = blocksByCheckpoint.map(checkpointBlocks =>
230
+ computeCheckpointOutHash(checkpointBlocks.map(b => b.block.body.txEffects.map(tx => tx.l2ToL1Msgs))),
231
+ );
232
+
215
233
  try {
216
234
  this.log.verbose(`Re-executing transactions in the proposal`, proposalInfo);
217
235
  reexecutionResult = await this.reexecuteTransactions(
@@ -220,6 +238,7 @@ export class BlockProposalHandler {
220
238
  checkpointNumber,
221
239
  txs,
222
240
  l1ToL2Messages,
241
+ previousCheckpointOutHashes,
223
242
  );
224
243
  } catch (error) {
225
244
  this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo);
@@ -229,7 +248,6 @@ export class BlockProposalHandler {
229
248
  }
230
249
 
231
250
  // If we succeeded, push this block into the archiver (unless disabled)
232
- // TODO(palla/mbps): Change default to false once block sync is stable.
233
251
  if (reexecutionResult?.block && this.config.skipPushProposedBlocksToArchiver === false) {
234
252
  await this.blockSource.addBlock(reexecutionResult?.block);
235
253
  }
@@ -297,7 +315,7 @@ export class BlockProposalHandler {
297
315
  // TODO(palla/mbps): The block header should include the checkpoint number to avoid this lookup,
298
316
  // or at least the L2BlockSource should return a different struct that includes it.
299
317
  const parentBlockNumber = parentBlockHeader.getBlockNumber();
300
- const parentBlock = await this.blockSource.getL2BlockNew(parentBlockNumber);
318
+ const parentBlock = await this.blockSource.getL2Block(parentBlockNumber);
301
319
  if (!parentBlock) {
302
320
  this.log.warn(`Parent block ${parentBlockNumber} not found in archiver`, proposalInfo);
303
321
  return { reason: 'invalid_proposal' };
@@ -338,7 +356,7 @@ export class BlockProposalHandler {
338
356
  */
339
357
  private validateNonFirstBlockInCheckpoint(
340
358
  proposal: BlockProposal,
341
- parentBlock: L2BlockNew,
359
+ parentBlock: L2Block,
342
360
  proposalInfo: object,
343
361
  ): CheckpointComputationResult | undefined {
344
362
  const proposalGlobals = proposal.blockHeader.globalVariables;
@@ -414,31 +432,7 @@ export class BlockProposalHandler {
414
432
 
415
433
  private getReexecutionDeadline(slot: SlotNumber, config: { l1GenesisTime: bigint; slotDuration: number }): Date {
416
434
  const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
417
- const msNeededForPropagationAndPublishing = this.config.validatorReexecuteDeadlineMs;
418
- return new Date(nextSlotTimestampSeconds * 1000 - msNeededForPropagationAndPublishing);
419
- }
420
-
421
- /**
422
- * Gets all prior blocks in the same checkpoint (same slot and checkpoint number) up to but not including upToBlockNumber.
423
- */
424
- private async getBlocksInCheckpoint(
425
- slot: SlotNumber,
426
- upToBlockNumber: BlockNumber,
427
- checkpointNumber: CheckpointNumber,
428
- ): Promise<L2BlockNew[]> {
429
- const blocks: L2BlockNew[] = [];
430
- let currentBlockNumber = BlockNumber(upToBlockNumber - 1);
431
-
432
- while (currentBlockNumber >= INITIAL_L2_BLOCK_NUM) {
433
- const block = await this.blockSource.getL2BlockNew(currentBlockNumber);
434
- if (!block || block.header.getSlot() !== slot || block.checkpointNumber !== checkpointNumber) {
435
- break;
436
- }
437
- blocks.unshift(block);
438
- currentBlockNumber = BlockNumber(currentBlockNumber - 1);
439
- }
440
-
441
- return blocks;
435
+ return new Date(nextSlotTimestampSeconds * 1000);
442
436
  }
443
437
 
444
438
  private getReexecuteFailureReason(err: any) {
@@ -459,6 +453,7 @@ export class BlockProposalHandler {
459
453
  checkpointNumber: CheckpointNumber,
460
454
  txs: Tx[],
461
455
  l1ToL2Messages: Fr[],
456
+ previousCheckpointOutHashes: Fr[],
462
457
  ): Promise<ReexecuteTransactionsResult> {
463
458
  const { blockHeader, txHashes } = proposal;
464
459
 
@@ -473,11 +468,13 @@ export class BlockProposalHandler {
473
468
  const slot = proposal.slotNumber;
474
469
  const config = this.checkpointsBuilder.getConfig();
475
470
 
476
- // Get prior blocks in this checkpoint (same slot and checkpoint number)
477
- const priorBlocks = await this.getBlocksInCheckpoint(slot, blockNumber, checkpointNumber);
471
+ // Get prior blocks in this checkpoint (same slot before current block)
472
+ const allBlocksInSlot = await this.blockSource.getBlocksForSlot(slot);
473
+ const priorBlocks = allBlocksInSlot.filter(b => b.number < blockNumber && b.header.getSlot() === slot);
478
474
 
479
475
  // Fork before the block to be built
480
476
  const parentBlockNumber = BlockNumber(blockNumber - 1);
477
+ await this.worldState.syncImmediate(parentBlockNumber);
481
478
  using fork = await this.worldState.fork(parentBlockNumber);
482
479
 
483
480
  // Build checkpoint constants from proposal (excludes blockNumber and timestamp which are per-block)
@@ -495,8 +492,10 @@ export class BlockProposalHandler {
495
492
  checkpointNumber,
496
493
  constants,
497
494
  l1ToL2Messages,
495
+ previousCheckpointOutHashes,
498
496
  fork,
499
497
  priorBlocks,
498
+ this.log.getBindings(),
500
499
  );
501
500
 
502
501
  // Build the new block