@aztec/validator-client 0.0.1-commit.7d4e6cd → 0.0.1-commit.87a0206

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 +51 -24
  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 +18 -13
  41. package/dest/validator.d.ts.map +1 -1
  42. package/dest/validator.js +107 -81
  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 +128 -94
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) {
@@ -170,6 +183,10 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
170
183
  // and processed separately via the block handler above.
171
184
  const checkpointHandler = (checkpoint, proposalSender)=>this.attestToCheckpointProposal(checkpoint, proposalSender);
172
185
  this.p2pClient.registerCheckpointProposalHandler(checkpointHandler);
186
+ // Duplicate proposal handler - triggers slashing for equivocation
187
+ this.p2pClient.registerDuplicateProposalCallback((info)=>{
188
+ this.handleDuplicateProposal(info);
189
+ });
173
190
  const myAddresses = this.getValidatorAddresses();
174
191
  this.p2pClient.registerThisValidatorAddresses(myAddresses);
175
192
  await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
@@ -181,6 +198,9 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
181
198
  * @returns true if the proposal is valid, false otherwise
182
199
  */ async validateBlockProposal(proposal, proposalSender) {
183
200
  const slotNumber = proposal.slotNumber;
201
+ // Note: During escape hatch, we still want to "validate" proposals for observability,
202
+ // but we intentionally reject them and disable slashing invalid block and attestation flow.
203
+ const escapeHatchOpen = await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber);
184
204
  const proposer = proposal.getSender();
185
205
  // Reject proposals with invalid signatures
186
206
  if (!proposer) {
@@ -203,7 +223,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
203
223
  // In fisherman mode, we always reexecute to validate proposals.
204
224
  const { validatorReexecute, slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } = this.config;
205
225
  const shouldReexecute = fishermanMode || slashBroadcastedInvalidBlockPenalty > 0n && validatorReexecute || partOfCommittee && validatorReexecute || alwaysReexecuteBlockProposals || this.blobClient.canUpload();
206
- const validationResult = await this.blockProposalHandler.handleBlockProposal(proposal, proposalSender, !!shouldReexecute);
226
+ const validationResult = await this.blockProposalHandler.handleBlockProposal(proposal, proposalSender, !!shouldReexecute && !escapeHatchOpen);
207
227
  if (!validationResult.isValid) {
208
228
  this.log.warn(`Block proposal validation failed: ${validationResult.reason}`, proposalInfo);
209
229
  const reason = validationResult.reason || 'unknown';
@@ -222,7 +242,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
222
242
  this.metrics.incFailedAttestationsNodeIssue(1, reason, partOfCommittee);
223
243
  }
224
244
  // 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) {
245
+ if (!escapeHatchOpen && validationResult.reason && SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) && slashBroadcastedInvalidBlockPenalty > 0n) {
226
246
  this.log.warn(`Slashing proposer for invalid block proposal`, proposalInfo);
227
247
  this.slashInvalidBlock(proposal);
228
248
  }
@@ -231,11 +251,13 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
231
251
  this.log.info(`Validated block proposal for slot ${slotNumber}`, {
232
252
  ...proposalInfo,
233
253
  inCommittee: partOfCommittee,
234
- fishermanMode: this.config.fishermanMode || false
254
+ fishermanMode: this.config.fishermanMode || false,
255
+ escapeHatchOpen
235
256
  });
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);
257
+ if (escapeHatchOpen) {
258
+ this.log.warn(`Escape hatch open for slot ${slotNumber}, rejecting block proposal`, proposalInfo);
259
+ return false;
260
+ }
239
261
  return true;
240
262
  }
241
263
  /**
@@ -246,6 +268,11 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
246
268
  */ async attestToCheckpointProposal(proposal, _proposalSender) {
247
269
  const slotNumber = proposal.slotNumber;
248
270
  const proposer = proposal.getSender();
271
+ // If escape hatch is open for this slot's epoch, do not attest.
272
+ if (await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber)) {
273
+ this.log.warn(`Escape hatch open for slot ${slotNumber}, skipping checkpoint attestation handling`);
274
+ return undefined;
275
+ }
249
276
  // Reject proposals with invalid signatures
250
277
  if (!proposer) {
251
278
  this.log.warn(`Received checkpoint proposal with invalid signature for slot ${slotNumber}`);
@@ -265,16 +292,9 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
265
292
  txHashes: proposal.txHashes.map((t)=>t.toString()),
266
293
  fishermanMode: this.config.fishermanMode || false
267
294
  });
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
295
  // 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);
296
+ if (this.config.skipCheckpointProposalValidation) {
297
+ this.log.warn(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
278
298
  } else {
279
299
  const validationResult = await this.validateCheckpointProposal(proposal, proposalInfo);
280
300
  if (!validationResult.isValid) {
@@ -325,7 +345,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
325
345
  }
326
346
  async createCheckpointAttestationsFromProposal(proposal, attestors = []) {
327
347
  const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
328
- await this.p2pClient.addCheckpointAttestations(attestations);
348
+ await this.p2pClient.addOwnCheckpointAttestations(attestations);
329
349
  return attestations;
330
350
  }
331
351
  /**
@@ -333,7 +353,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
333
353
  * @returns Validation result with isValid flag and reason if invalid.
334
354
  */ async validateCheckpointProposal(proposal, proposalInfo) {
335
355
  const slot = proposal.slotNumber;
336
- const timeoutSeconds = 10;
356
+ const timeoutSeconds = 10; // TODO(palla/mbps): This should map to the timetable settings
337
357
  // Wait for last block to sync by archive
338
358
  let lastBlockHeader;
339
359
  try {
@@ -362,18 +382,8 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
362
382
  reason: 'last_block_not_found'
363
383
  };
364
384
  }
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
385
  // Get all full blocks for the slot and checkpoint
376
- const blocks = await this.getBlocksForSlot(slot, lastBlockHeader, checkpointNumber);
386
+ const blocks = await this.blockSource.getBlocksForSlot(slot);
377
387
  if (blocks.length === 0) {
378
388
  this.log.warn(`No blocks found for slot ${slot}`, proposalInfo);
379
389
  return {
@@ -388,14 +398,21 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
388
398
  // Get checkpoint constants from first block
389
399
  const firstBlock = blocks[0];
390
400
  const constants = this.extractCheckpointConstants(firstBlock);
401
+ const checkpointNumber = firstBlock.checkpointNumber;
391
402
  // Get L1-to-L2 messages for this checkpoint
392
403
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
404
+ // Compute the previous checkpoint out hashes for the epoch.
405
+ // TODO: There can be a more efficient way to get the previous checkpoint out hashes without having to fetch the
406
+ // actual checkpoints and the blocks/txs in them.
407
+ const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
408
+ const previousCheckpoints = (await this.blockSource.getCheckpointsForEpoch(epoch)).filter((b)=>b.number < checkpointNumber).sort((a, b)=>a.number - b.number);
409
+ const previousCheckpointOutHashes = previousCheckpoints.map((c)=>c.getCheckpointOutHash());
393
410
  // Fork world state at the block before the first block
394
411
  const parentBlockNumber = BlockNumber(firstBlock.number - 1);
395
412
  const fork = await this.worldState.fork(parentBlockNumber);
396
413
  try {
397
414
  // Create checkpoint builder with all existing blocks
398
- const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, l1ToL2Messages, fork, blocks);
415
+ const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, l1ToL2Messages, previousCheckpointOutHashes, fork, blocks, this.log.getBindings());
399
416
  // Complete the checkpoint to get computed values
400
417
  const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
401
418
  // Compare checkpoint header with proposal
@@ -422,6 +439,27 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
422
439
  reason: 'archive_mismatch'
423
440
  };
424
441
  }
442
+ // Check that the accumulated epoch out hash matches the value in the proposal.
443
+ // The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
444
+ const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
445
+ const computedEpochOutHash = accumulateCheckpointOutHashes([
446
+ ...previousCheckpointOutHashes,
447
+ checkpointOutHash
448
+ ]);
449
+ const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
450
+ if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
451
+ this.log.warn(`Epoch out hash mismatch`, {
452
+ proposalEpochOutHash: proposalEpochOutHash.toString(),
453
+ computedEpochOutHash: computedEpochOutHash.toString(),
454
+ checkpointOutHash: checkpointOutHash.toString(),
455
+ previousCheckpointOutHashes: previousCheckpointOutHashes.map((h)=>h.toString()),
456
+ ...proposalInfo
457
+ });
458
+ return {
459
+ isValid: false,
460
+ reason: 'out_hash_mismatch'
461
+ };
462
+ }
425
463
  this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
426
464
  return {
427
465
  isValid: true
@@ -431,36 +469,6 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
431
469
  }
432
470
  }
433
471
  /**
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
472
  * Extract checkpoint global variables from a block.
465
473
  */ extractCheckpointConstants(block) {
466
474
  const gv = block.header.globalVariables;
@@ -482,13 +490,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
482
490
  this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
483
491
  return;
484
492
  }
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);
493
+ const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
492
494
  if (blocks.length === 0) {
493
495
  this.log.warn(`No blocks found for blob upload`, proposalInfo);
494
496
  return;
@@ -526,7 +528,27 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
526
528
  }
527
529
  ]);
528
530
  }
529
- async createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, options) {
531
+ /**
532
+ * Handle detection of a duplicate proposal (equivocation).
533
+ * Emits a slash event when a proposer sends multiple proposals for the same position.
534
+ */ handleDuplicateProposal(info) {
535
+ const { slot, proposer, type } = info;
536
+ this.log.warn(`Triggering slash event for duplicate ${type} proposal from ${proposer.toString()} at slot ${slot}`, {
537
+ proposer: proposer.toString(),
538
+ slot,
539
+ type
540
+ });
541
+ // Emit slash event
542
+ this.emit(WANT_TO_SLASH_EVENT, [
543
+ {
544
+ validator: proposer,
545
+ amount: this.config.slashDuplicateProposalPenalty,
546
+ offenseType: OffenseType.DUPLICATE_PROPOSAL,
547
+ epochOrSlot: BigInt(slot)
548
+ }
549
+ ]);
550
+ }
551
+ async createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, options = {}) {
530
552
  // TODO(palla/mbps): Prevent double proposals properly
531
553
  // if (this.previousProposal?.slotNumber === blockHeader.globalVariables.slotNumber) {
532
554
  // this.log.verbose(`Already made a proposal for the same slot, skipping proposal`);
@@ -540,15 +562,15 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
540
562
  this.previousProposal = newProposal;
541
563
  return newProposal;
542
564
  }
543
- async createCheckpointProposal(checkpointHeader, archive, lastBlockInfo, proposerAddress, options) {
565
+ async createCheckpointProposal(checkpointHeader, archive, lastBlockInfo, proposerAddress, options = {}) {
544
566
  this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
545
567
  return await this.validationService.createCheckpointProposal(checkpointHeader, archive, lastBlockInfo, proposerAddress, options);
546
568
  }
547
569
  async broadcastBlockProposal(proposal) {
548
570
  await this.p2pClient.broadcastProposal(proposal);
549
571
  }
550
- async signAttestationsAndSigners(attestationsAndSigners, proposer) {
551
- return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer);
572
+ async signAttestationsAndSigners(attestationsAndSigners, proposer, slot, blockNumber) {
573
+ return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer, slot, blockNumber);
552
574
  }
553
575
  async collectOwnAttestations(proposal) {
554
576
  const slot = proposal.slotNumber;
@@ -630,7 +652,11 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
630
652
  return Buffer.alloc(0);
631
653
  }
632
654
  const payloadToSign = authRequest.getPayloadToSign();
633
- const signature = await this.keyStore.signMessageWithAddress(addressToUse, payloadToSign);
655
+ // AUTH_REQUEST doesn't require HA protection - multiple signatures are safe
656
+ const context = {
657
+ dutyType: DutyType.AUTH_REQUEST
658
+ };
659
+ const signature = await this.keyStore.signMessageWithAddress(addressToUse, payloadToSign, context);
634
660
  const authResponse = new AuthResponse(statusMessage, signature);
635
661
  return authResponse.toBuffer();
636
662
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/validator-client",
3
- "version": "0.0.1-commit.7d4e6cd",
3
+ "version": "0.0.1-commit.87a0206",
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.7d4e6cd",
68
- "@aztec/blob-lib": "0.0.1-commit.7d4e6cd",
69
- "@aztec/constants": "0.0.1-commit.7d4e6cd",
70
- "@aztec/epoch-cache": "0.0.1-commit.7d4e6cd",
71
- "@aztec/ethereum": "0.0.1-commit.7d4e6cd",
72
- "@aztec/foundation": "0.0.1-commit.7d4e6cd",
73
- "@aztec/node-keystore": "0.0.1-commit.7d4e6cd",
74
- "@aztec/noir-protocol-circuits-types": "0.0.1-commit.7d4e6cd",
75
- "@aztec/p2p": "0.0.1-commit.7d4e6cd",
76
- "@aztec/protocol-contracts": "0.0.1-commit.7d4e6cd",
77
- "@aztec/prover-client": "0.0.1-commit.7d4e6cd",
78
- "@aztec/simulator": "0.0.1-commit.7d4e6cd",
79
- "@aztec/slasher": "0.0.1-commit.7d4e6cd",
80
- "@aztec/stdlib": "0.0.1-commit.7d4e6cd",
81
- "@aztec/telemetry-client": "0.0.1-commit.7d4e6cd",
67
+ "@aztec/blob-client": "0.0.1-commit.87a0206",
68
+ "@aztec/blob-lib": "0.0.1-commit.87a0206",
69
+ "@aztec/constants": "0.0.1-commit.87a0206",
70
+ "@aztec/epoch-cache": "0.0.1-commit.87a0206",
71
+ "@aztec/ethereum": "0.0.1-commit.87a0206",
72
+ "@aztec/foundation": "0.0.1-commit.87a0206",
73
+ "@aztec/node-keystore": "0.0.1-commit.87a0206",
74
+ "@aztec/noir-protocol-circuits-types": "0.0.1-commit.87a0206",
75
+ "@aztec/p2p": "0.0.1-commit.87a0206",
76
+ "@aztec/protocol-contracts": "0.0.1-commit.87a0206",
77
+ "@aztec/prover-client": "0.0.1-commit.87a0206",
78
+ "@aztec/simulator": "0.0.1-commit.87a0206",
79
+ "@aztec/slasher": "0.0.1-commit.87a0206",
80
+ "@aztec/stdlib": "0.0.1-commit.87a0206",
81
+ "@aztec/telemetry-client": "0.0.1-commit.87a0206",
82
+ "@aztec/validator-ha-signer": "0.0.1-commit.87a0206",
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.87a0206",
90
+ "@aztec/world-state": "0.0.1-commit.87a0206",
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