@aztec/validator-client 0.0.1-commit.8c0b8ff → 0.0.1-commit.8cb2d04d8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dest/validator.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { getBlobsPerL1Block } from '@aztec/blob-lib';
2
+ import { CheckpointNumber } from '@aztec/foundation/branded-types';
1
3
  import { createLogger } from '@aztec/foundation/log';
2
4
  import { RunningPromise } from '@aztec/foundation/running-promise';
3
5
  import { sleep } from '@aztec/foundation/sleep';
@@ -7,7 +9,7 @@ import { OffenseType, WANT_TO_SLASH_EVENT } from '@aztec/slasher';
7
9
  import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
8
10
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
9
11
  import { getTelemetryClient } from '@aztec/telemetry-client';
10
- import { createHASigner, createSignerFromSharedDb } from '@aztec/validator-ha-signer/factory';
12
+ import { createHASigner, createLocalSignerWithProtection, createSignerFromSharedDb } from '@aztec/validator-ha-signer/factory';
11
13
  import { DutyType } from '@aztec/validator-ha-signer/types';
12
14
  import { EventEmitter } from 'events';
13
15
  import { ValidationService } from './duties/validation_service.js';
@@ -30,9 +32,13 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
30
32
  epochCache;
31
33
  p2pClient;
32
34
  proposalHandler;
35
+ blockSource;
36
+ checkpointsBuilder;
37
+ worldState;
38
+ l1ToL2MessageSource;
33
39
  config;
34
40
  blobClient;
35
- haSigner;
41
+ slashingProtectionSigner;
36
42
  dateProvider;
37
43
  tracer;
38
44
  validationService;
@@ -47,8 +53,8 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
47
53
  /** Tracks the last epoch in which each attester successfully submitted at least one attestation. */ lastAttestedEpochByAttester;
48
54
  proposersOfInvalidBlocks;
49
55
  /** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */ lastAttestedProposal;
50
- constructor(keyStore, epochCache, p2pClient, proposalHandler, config, blobClient, haSigner, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
51
- super(), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.proposalHandler = proposalHandler, this.config = config, this.blobClient = blobClient, this.haSigner = haSigner, this.dateProvider = dateProvider, this.hasRegisteredHandlers = false, this.lastAttestedEpochByAttester = new Map(), this.proposersOfInvalidBlocks = new Set();
56
+ constructor(keyStore, epochCache, p2pClient, proposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, slashingProtectionSigner, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
57
+ super(), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.proposalHandler = proposalHandler, this.blockSource = blockSource, this.checkpointsBuilder = checkpointsBuilder, this.worldState = worldState, this.l1ToL2MessageSource = l1ToL2MessageSource, this.config = config, this.blobClient = blobClient, this.slashingProtectionSigner = slashingProtectionSigner, this.dateProvider = dateProvider, this.hasRegisteredHandlers = false, this.lastAttestedEpochByAttester = new Map(), this.proposersOfInvalidBlocks = new Set();
52
58
  // Create child logger with fisherman prefix if in fisherman mode
53
59
  this.log = config.fishermanMode ? log.createChild('[FISHERMAN]') : log;
54
60
  this.tracer = telemetry.getTracer('Validator');
@@ -109,26 +115,36 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
109
115
  txsPermitted: !config.disableTransactions,
110
116
  maxTxsPerBlock: config.validateMaxTxsPerBlock
111
117
  });
112
- const proposalHandler = new ProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, config, blobClient, metrics, dateProvider, telemetry);
118
+ const proposalHandler = new ProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, config, blobClient, metrics, dateProvider, telemetry, undefined);
113
119
  const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
114
- let validatorKeyStore = nodeKeystoreAdapter;
115
- let haSigner;
120
+ let slashingProtectionSigner;
116
121
  if (slashingProtectionDb) {
117
122
  // Shared database mode: use a pre-existing database (e.g. for testing HA setups).
118
- const { signer } = createSignerFromSharedDb(slashingProtectionDb, config);
119
- haSigner = signer;
120
- validatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, signer);
123
+ ({ signer: slashingProtectionSigner } = createSignerFromSharedDb(slashingProtectionDb, config, {
124
+ telemetryClient: telemetry,
125
+ dateProvider
126
+ }));
121
127
  } else if (config.haSigningEnabled) {
128
+ // Multi-node HA mode: use PostgreSQL-backed distributed locking.
122
129
  // If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
123
130
  const haConfig = {
124
131
  ...config,
125
132
  maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000
126
133
  };
127
- const { signer } = await createHASigner(haConfig);
128
- haSigner = signer;
129
- validatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, signer);
130
- }
131
- const validator = new ValidatorClient(validatorKeyStore, epochCache, p2pClient, proposalHandler, config, blobClient, haSigner, dateProvider, telemetry);
134
+ ({ signer: slashingProtectionSigner } = await createHASigner(haConfig, {
135
+ telemetryClient: telemetry,
136
+ dateProvider
137
+ }));
138
+ } else {
139
+ // Single-node mode: use LMDB-backed local signing protection.
140
+ // This prevents double-signing if the node crashes and restarts mid-proposal.
141
+ ({ signer: slashingProtectionSigner } = await createLocalSignerWithProtection(config, {
142
+ telemetryClient: telemetry,
143
+ dateProvider
144
+ }));
145
+ }
146
+ const validatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, slashingProtectionSigner);
147
+ const validator = new ValidatorClient(validatorKeyStore, epochCache, p2pClient, proposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, slashingProtectionSigner, dateProvider, telemetry);
132
148
  return validator;
133
149
  }
134
150
  getValidatorAddresses() {
@@ -156,17 +172,8 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
156
172
  };
157
173
  }
158
174
  reloadKeystore(newManager) {
159
- if (this.config.haSigningEnabled && !this.haSigner) {
160
- this.log.warn('HA signing is enabled in config but was not initialized at startup. ' + 'Restart the node to enable HA signing.');
161
- } else if (!this.config.haSigningEnabled && this.haSigner) {
162
- this.log.warn('HA signing was disabled via config update but the HA signer is still active. ' + 'Restart the node to fully disable HA signing.');
163
- }
164
175
  const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
165
- if (this.haSigner) {
166
- this.keyStore = new HAKeyStore(newAdapter, this.haSigner);
167
- } else {
168
- this.keyStore = newAdapter;
169
- }
176
+ this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
170
177
  this.validationService = new ValidationService(this.keyStore, this.log.createChild('validation-service'));
171
178
  }
172
179
  async start() {
@@ -200,7 +207,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
200
207
  // The checkpoint is received as CheckpointProposalCore since the lastBlock is extracted
201
208
  // and processed separately via the block handler above.
202
209
  const checkpointHandler = (checkpoint, proposalSender)=>this.attestToCheckpointProposal(checkpoint, proposalSender);
203
- this.p2pClient.registerCheckpointProposalHandler(checkpointHandler);
210
+ this.p2pClient.registerValidatorCheckpointProposalHandler(checkpointHandler);
204
211
  // Duplicate proposal handler - triggers slashing for equivocation
205
212
  this.p2pClient.registerDuplicateProposalCallback((info)=>{
206
213
  this.handleDuplicateProposal(info);
@@ -250,8 +257,8 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
250
257
  });
251
258
  // Reexecute txs if we are part of the committee, or if slashing is enabled, or if we are configured to always reexecute.
252
259
  // In fisherman mode, we always reexecute to validate proposals.
253
- const { validatorReexecute, slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } = this.config;
254
- const shouldReexecute = fishermanMode || slashBroadcastedInvalidBlockPenalty > 0n && validatorReexecute || partOfCommittee && validatorReexecute || alwaysReexecuteBlockProposals || this.blobClient.canUpload();
260
+ const { slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } = this.config;
261
+ const shouldReexecute = fishermanMode || slashBroadcastedInvalidBlockPenalty > 0n || partOfCommittee || alwaysReexecuteBlockProposals || this.blobClient.canUpload();
255
262
  const validationResult = await this.proposalHandler.handleBlockProposal(proposal, proposalSender, !!shouldReexecute && !escapeHatchOpen);
256
263
  if (!validationResult.isValid) {
257
264
  const reason = validationResult.reason || 'unknown';
@@ -295,42 +302,46 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
295
302
  * the lastBlock is extracted and processed separately via the block handler.
296
303
  * @returns Checkpoint attestations if valid, undefined otherwise
297
304
  */ async attestToCheckpointProposal(proposal, _proposalSender) {
298
- const slotNumber = proposal.slotNumber;
305
+ const proposalSlotNumber = proposal.slotNumber;
299
306
  const proposer = proposal.getSender();
300
307
  // If escape hatch is open for this slot's epoch, do not attest.
301
- if (await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber)) {
302
- this.log.warn(`Escape hatch open for slot ${slotNumber}, skipping checkpoint attestation handling`);
308
+ if (await this.epochCache.isEscapeHatchOpenAtSlot(proposalSlotNumber)) {
309
+ this.log.warn(`Escape hatch open for slot ${proposalSlotNumber}, skipping checkpoint attestation handling`);
303
310
  return undefined;
304
311
  }
305
312
  // Ignore proposals from ourselves (may happen in HA setups)
306
313
  if (proposer && this.getValidatorAddresses().some((addr)=>addr.equals(proposer))) {
307
- this.log.debug(`Ignoring block proposal from self for slot ${slotNumber}`, {
314
+ this.log.debug(`Ignoring block proposal from self for slot ${proposalSlotNumber}`, {
308
315
  proposer: proposer.toString(),
309
- slotNumber
316
+ proposalSlotNumber
310
317
  });
311
318
  return undefined;
312
319
  }
313
320
  // Check that I have any address in the committee where this checkpoint will land before attesting
314
- const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
321
+ const inCommittee = await this.epochCache.filterInCommittee(proposalSlotNumber, this.getValidatorAddresses());
315
322
  const partOfCommittee = inCommittee.length > 0;
316
323
  const proposalInfo = {
317
- slotNumber,
324
+ proposalSlotNumber,
318
325
  archive: proposal.archive.toString(),
319
326
  proposer: proposer?.toString()
320
327
  };
321
- this.log.info(`Received checkpoint proposal for slot ${slotNumber}`, {
328
+ this.log.info(`Received checkpoint proposal for slot ${proposalSlotNumber}`, {
322
329
  ...proposalInfo,
323
330
  fishermanMode: this.config.fishermanMode || false
324
331
  });
325
- // Validate the checkpoint proposal and upload blobs (unless skipCheckpointProposalValidation is set)
332
+ // Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set).
333
+ // Uses the cached result from the all-nodes callback if available (avoids double validation).
334
+ let checkpointNumber;
326
335
  if (this.config.skipCheckpointProposalValidation) {
327
- this.log.warn(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
336
+ this.log.warn(`Skipping checkpoint proposal validation for slot ${proposalSlotNumber}`, proposalInfo);
337
+ checkpointNumber = CheckpointNumber(0);
328
338
  } else {
329
339
  const validationResult = await this.proposalHandler.handleCheckpointProposal(proposal, proposalInfo);
330
340
  if (!validationResult.isValid) {
331
341
  this.log.warn(`Checkpoint proposal validation failed: ${validationResult.reason}`, proposalInfo);
332
342
  return undefined;
333
343
  }
344
+ checkpointNumber = validationResult.checkpointNumber;
334
345
  }
335
346
  // Check that I have any address in current committee before attesting
336
347
  // In fisherman mode, we still create attestations for validation even if not in committee
@@ -339,14 +350,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
339
350
  return undefined;
340
351
  }
341
352
  // Provided all of the above checks pass, we can attest to the proposal
342
- this.log.info(`${partOfCommittee ? 'Attesting to' : 'Validated'} checkpoint proposal for slot ${slotNumber}`, {
353
+ this.log.info(`${partOfCommittee ? 'Attesting to' : 'Validated'} checkpoint proposal for slot ${proposalSlotNumber}`, {
343
354
  ...proposalInfo,
344
355
  inCommittee: partOfCommittee,
345
356
  fishermanMode: this.config.fishermanMode || false
346
357
  });
347
358
  this.metrics.incSuccessfulAttestations(inCommittee.length);
348
359
  // Track epoch participation per attester: count each (attester, epoch) pair at most once
349
- const proposalEpoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
360
+ const proposalEpoch = getEpochAtSlot(proposalSlotNumber, this.epochCache.getL1Constants());
350
361
  for (const attester of inCommittee){
351
362
  const key = attester.toString();
352
363
  const lastEpoch = this.lastAttestedEpochByAttester.get(key);
@@ -371,13 +382,13 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
371
382
  }
372
383
  if (this.config.fishermanMode) {
373
384
  // bail out early and don't save attestations to the pool in fisherman mode
374
- this.log.info(`Creating checkpoint attestations for slot ${slotNumber}`, {
385
+ this.log.info(`Creating checkpoint attestations for slot ${proposalSlotNumber}`, {
375
386
  ...proposalInfo,
376
387
  attestors: attestors.map((a)=>a.toString())
377
388
  });
378
389
  return undefined;
379
390
  }
380
- return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
391
+ return await this.createCheckpointAttestationsFromProposal(proposal, attestors, checkpointNumber);
381
392
  }
382
393
  /**
383
394
  * Checks if we should attest to a slot based on equivocation prevention rules.
@@ -394,17 +405,42 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
394
405
  }
395
406
  return true;
396
407
  }
397
- async createCheckpointAttestationsFromProposal(proposal, attestors = []) {
408
+ async createCheckpointAttestationsFromProposal(proposal, attestors = [], checkpointNumber) {
398
409
  // Equivocation check: must happen right before signing to minimize the race window
399
410
  if (!this.shouldAttestToSlot(proposal.slotNumber)) {
400
411
  return undefined;
401
412
  }
402
- const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
413
+ const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors, checkpointNumber);
403
414
  // Track the proposal we attested to (to prevent equivocation)
404
415
  this.lastAttestedProposal = proposal;
405
416
  await this.p2pClient.addOwnCheckpointAttestations(attestations);
406
417
  return attestations;
407
418
  }
419
+ /**
420
+ * Uploads blobs for a checkpoint to the filestore (fire and forget).
421
+ */ async uploadBlobsForCheckpoint(proposal, proposalInfo) {
422
+ try {
423
+ const lastBlockHeader = await this.blockSource.getBlockHeaderByArchive(proposal.archive);
424
+ if (!lastBlockHeader) {
425
+ this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
426
+ return;
427
+ }
428
+ const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
429
+ if (blocks.length === 0) {
430
+ this.log.warn(`No blocks found for blob upload`, proposalInfo);
431
+ return;
432
+ }
433
+ const blobFields = blocks.flatMap((b)=>b.toBlobFields());
434
+ const blobs = await getBlobsPerL1Block(blobFields);
435
+ await this.blobClient.sendBlobsToFilestore(blobs);
436
+ this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
437
+ ...proposalInfo,
438
+ numBlobs: blobs.length
439
+ });
440
+ } catch (err) {
441
+ this.log.warn(`Failed to upload blobs for checkpoint: ${err}`, proposalInfo);
442
+ }
443
+ }
408
444
  slashInvalidBlock(proposal) {
409
445
  const proposer = proposal.getSender();
410
446
  // Skip if signature is invalid (shouldn't happen since we validate earlier)
@@ -465,7 +501,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
465
501
  }
466
502
  ]);
467
503
  }
468
- async createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, options = {}) {
504
+ async createBlockProposal(blockHeader, checkpointNumber, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, options = {}) {
469
505
  // Validate that we're not creating a proposal for an older or equal position
470
506
  if (this.lastProposedBlock) {
471
507
  const lastSlot = this.lastProposedBlock.slotNumber;
@@ -476,14 +512,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
476
512
  }
477
513
  }
478
514
  this.log.info(`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`);
479
- const newProposal = await this.validationService.createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, {
515
+ const newProposal = await this.validationService.createBlockProposal(blockHeader, checkpointNumber, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, {
480
516
  ...options,
481
517
  broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal
482
518
  });
483
519
  this.lastProposedBlock = newProposal;
484
520
  return newProposal;
485
521
  }
486
- async createCheckpointProposal(checkpointHeader, archive, feeAssetPriceModifier, lastBlockInfo, proposerAddress, options = {}) {
522
+ async createCheckpointProposal(checkpointHeader, archive, checkpointNumber, feeAssetPriceModifier, lastBlockProposal, proposerAddress, options = {}) {
487
523
  // Validate that we're not creating a proposal for an older or equal slot
488
524
  if (this.lastProposedCheckpoint) {
489
525
  const lastSlot = this.lastProposedCheckpoint.slotNumber;
@@ -493,23 +529,23 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
493
529
  }
494
530
  }
495
531
  this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
496
- const newProposal = await this.validationService.createCheckpointProposal(checkpointHeader, archive, feeAssetPriceModifier, lastBlockInfo, proposerAddress, options);
532
+ const newProposal = await this.validationService.createCheckpointProposal(checkpointHeader, archive, checkpointNumber, feeAssetPriceModifier, lastBlockProposal, proposerAddress, options);
497
533
  this.lastProposedCheckpoint = newProposal;
498
534
  return newProposal;
499
535
  }
500
536
  async broadcastBlockProposal(proposal) {
501
537
  await this.p2pClient.broadcastProposal(proposal);
502
538
  }
503
- async signAttestationsAndSigners(attestationsAndSigners, proposer, slot, blockNumber) {
504
- return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer, slot, blockNumber);
539
+ async signAttestationsAndSigners(attestationsAndSigners, proposer, slot, checkpointNumber) {
540
+ return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer, slot, checkpointNumber);
505
541
  }
506
- async collectOwnAttestations(proposal) {
542
+ async collectOwnAttestations(proposal, checkpointNumber) {
507
543
  const slot = proposal.slotNumber;
508
544
  const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
509
545
  this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, {
510
546
  inCommittee
511
547
  });
512
- const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
548
+ const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee, checkpointNumber);
513
549
  if (!attestations) {
514
550
  return [];
515
551
  }
@@ -521,7 +557,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
521
557
  });
522
558
  return attestations;
523
559
  }
524
- async collectAttestations(proposal, required, deadline) {
560
+ async collectAttestations(proposal, required, deadline, checkpointNumber) {
525
561
  // Wait and poll the p2pClient's attestation pool for this checkpoint until we have enough attestations
526
562
  const slot = proposal.slotNumber;
527
563
  this.log.debug(`Collecting ${required} attestations for slot ${slot} with deadline ${deadline.toISOString()}`);
@@ -529,7 +565,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
529
565
  this.log.error(`Deadline ${deadline.toISOString()} for collecting ${required} attestations for slot ${slot} is in the past`);
530
566
  throw new AttestationTimeoutError(0, required, slot);
531
567
  }
532
- await this.collectOwnAttestations(proposal);
568
+ await this.collectOwnAttestations(proposal, checkpointNumber);
533
569
  const proposalId = proposal.archive.toString();
534
570
  const myAddresses = this.getValidatorAddresses();
535
571
  let attestations = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/validator-client",
3
- "version": "0.0.1-commit.8c0b8ff",
3
+ "version": "0.0.1-commit.8cb2d04d8",
4
4
  "main": "dest/index.js",
5
5
  "type": "module",
6
6
  "exports": {
@@ -64,30 +64,30 @@
64
64
  ]
65
65
  },
66
66
  "dependencies": {
67
- "@aztec/blob-client": "0.0.1-commit.8c0b8ff",
68
- "@aztec/blob-lib": "0.0.1-commit.8c0b8ff",
69
- "@aztec/constants": "0.0.1-commit.8c0b8ff",
70
- "@aztec/epoch-cache": "0.0.1-commit.8c0b8ff",
71
- "@aztec/ethereum": "0.0.1-commit.8c0b8ff",
72
- "@aztec/foundation": "0.0.1-commit.8c0b8ff",
73
- "@aztec/node-keystore": "0.0.1-commit.8c0b8ff",
74
- "@aztec/noir-protocol-circuits-types": "0.0.1-commit.8c0b8ff",
75
- "@aztec/p2p": "0.0.1-commit.8c0b8ff",
76
- "@aztec/protocol-contracts": "0.0.1-commit.8c0b8ff",
77
- "@aztec/prover-client": "0.0.1-commit.8c0b8ff",
78
- "@aztec/simulator": "0.0.1-commit.8c0b8ff",
79
- "@aztec/slasher": "0.0.1-commit.8c0b8ff",
80
- "@aztec/stdlib": "0.0.1-commit.8c0b8ff",
81
- "@aztec/telemetry-client": "0.0.1-commit.8c0b8ff",
82
- "@aztec/validator-ha-signer": "0.0.1-commit.8c0b8ff",
67
+ "@aztec/blob-client": "0.0.1-commit.8cb2d04d8",
68
+ "@aztec/blob-lib": "0.0.1-commit.8cb2d04d8",
69
+ "@aztec/constants": "0.0.1-commit.8cb2d04d8",
70
+ "@aztec/epoch-cache": "0.0.1-commit.8cb2d04d8",
71
+ "@aztec/ethereum": "0.0.1-commit.8cb2d04d8",
72
+ "@aztec/foundation": "0.0.1-commit.8cb2d04d8",
73
+ "@aztec/node-keystore": "0.0.1-commit.8cb2d04d8",
74
+ "@aztec/noir-protocol-circuits-types": "0.0.1-commit.8cb2d04d8",
75
+ "@aztec/p2p": "0.0.1-commit.8cb2d04d8",
76
+ "@aztec/protocol-contracts": "0.0.1-commit.8cb2d04d8",
77
+ "@aztec/prover-client": "0.0.1-commit.8cb2d04d8",
78
+ "@aztec/simulator": "0.0.1-commit.8cb2d04d8",
79
+ "@aztec/slasher": "0.0.1-commit.8cb2d04d8",
80
+ "@aztec/stdlib": "0.0.1-commit.8cb2d04d8",
81
+ "@aztec/telemetry-client": "0.0.1-commit.8cb2d04d8",
82
+ "@aztec/validator-ha-signer": "0.0.1-commit.8cb2d04d8",
83
83
  "koa": "^2.16.1",
84
84
  "koa-router": "^13.1.1",
85
85
  "tslib": "^2.4.0",
86
86
  "viem": "npm:@aztec/viem@2.38.2"
87
87
  },
88
88
  "devDependencies": {
89
- "@aztec/archiver": "0.0.1-commit.8c0b8ff",
90
- "@aztec/world-state": "0.0.1-commit.8c0b8ff",
89
+ "@aztec/archiver": "0.0.1-commit.8cb2d04d8",
90
+ "@aztec/world-state": "0.0.1-commit.8cb2d04d8",
91
91
  "@electric-sql/pglite": "^0.3.14",
92
92
  "@jest/globals": "^30.0.0",
93
93
  "@types/jest": "^30.0.0",
@@ -130,7 +130,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
130
130
  await forkCheckpoint.commit();
131
131
 
132
132
  // Add block to checkpoint
133
- const block = await this.checkpointBuilder.addBlock(globalVariables, processedTxs, {
133
+ const { block } = await this.checkpointBuilder.addBlock(globalVariables, processedTxs, {
134
134
  expectedEndState: opts.expectedEndState,
135
135
  });
136
136
 
package/src/config.ts CHANGED
@@ -6,8 +6,8 @@ import {
6
6
  secretValueConfigHelper,
7
7
  } from '@aztec/foundation/config';
8
8
  import { EthAddress } from '@aztec/foundation/eth-address';
9
+ import { localSignerConfigMappings, validatorHASignerConfigMappings } from '@aztec/stdlib/ha-signing';
9
10
  import type { ValidatorClientConfig } from '@aztec/stdlib/interfaces/server';
10
- import { validatorHASignerConfigMappings } from '@aztec/validator-ha-signer/config';
11
11
 
12
12
  export type { ValidatorClientConfig };
13
13
 
@@ -49,11 +49,6 @@ export const validatorClientConfigMappings: ConfigMappingsType<ValidatorClientCo
49
49
  description: 'Interval between polling for new attestations',
50
50
  ...numberConfigHelper(200),
51
51
  },
52
- validatorReexecute: {
53
- env: 'VALIDATOR_REEXECUTE',
54
- description: 'Re-execute transactions before attesting',
55
- ...booleanConfigHelper(true),
56
- },
57
52
  alwaysReexecuteBlockProposals: {
58
53
  description:
59
54
  'Whether to always reexecute block proposals, even for non-validator nodes (useful for monitoring network status).',
@@ -80,23 +75,24 @@ export const validatorClientConfigMappings: ConfigMappingsType<ValidatorClientCo
80
75
  validateMaxL2BlockGas: {
81
76
  env: 'VALIDATOR_MAX_L2_BLOCK_GAS',
82
77
  description: 'Maximum L2 block gas for validation. Proposals exceeding this limit are rejected.',
83
- parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
78
+ parseEnv: (val: string) => parseInt(val, 10),
84
79
  },
85
80
  validateMaxDABlockGas: {
86
81
  env: 'VALIDATOR_MAX_DA_BLOCK_GAS',
87
82
  description: 'Maximum DA block gas for validation. Proposals exceeding this limit are rejected.',
88
- parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
83
+ parseEnv: (val: string) => parseInt(val, 10),
89
84
  },
90
85
  validateMaxTxsPerBlock: {
91
86
  env: 'VALIDATOR_MAX_TX_PER_BLOCK',
92
87
  description: 'Maximum transactions per block for validation. Proposals exceeding this limit are rejected.',
93
- parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
88
+ parseEnv: (val: string) => parseInt(val, 10),
94
89
  },
95
90
  validateMaxTxsPerCheckpoint: {
96
91
  env: 'VALIDATOR_MAX_TX_PER_CHECKPOINT',
97
92
  description: 'Maximum transactions per checkpoint for validation. Proposals exceeding this limit are rejected.',
98
- parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
93
+ parseEnv: (val: string) => parseInt(val, 10),
99
94
  },
95
+ ...localSignerConfigMappings,
100
96
  ...validatorHASignerConfigMappings,
101
97
  };
102
98
 
@@ -1,9 +1,4 @@
1
- import {
2
- BlockNumber,
3
- type CheckpointNumber,
4
- IndexWithinCheckpoint,
5
- type SlotNumber,
6
- } from '@aztec/foundation/branded-types';
1
+ import { type CheckpointNumber, IndexWithinCheckpoint, type SlotNumber } from '@aztec/foundation/branded-types';
7
2
  import { Buffer32 } from '@aztec/foundation/buffer';
8
3
  import { keccak256 } from '@aztec/foundation/crypto/keccak';
9
4
  import { Fr } from '@aztec/foundation/curves/bn254';
@@ -11,7 +6,6 @@ import type { EthAddress } from '@aztec/foundation/eth-address';
11
6
  import type { Signature } from '@aztec/foundation/eth-signature';
12
7
  import { createLogger } from '@aztec/foundation/log';
13
8
  import type { CommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
14
- import type { CreateCheckpointProposalLastBlockData } from '@aztec/stdlib/interfaces/server';
15
9
  import {
16
10
  BlockProposal,
17
11
  type BlockProposalOptions,
@@ -52,6 +46,7 @@ export class ValidationService {
52
46
  */
53
47
  public createBlockProposal(
54
48
  blockHeader: BlockHeader,
49
+ checkpointNumber: CheckpointNumber,
55
50
  blockIndexWithinCheckpoint: IndexWithinCheckpoint,
56
51
  inHash: Fr,
57
52
  archive: Fr,
@@ -72,6 +67,7 @@ export class ValidationService {
72
67
 
73
68
  return BlockProposal.createProposalFromSigner(
74
69
  blockHeader,
70
+ checkpointNumber,
75
71
  blockIndexWithinCheckpoint,
76
72
  inHash,
77
73
  archive,
@@ -86,7 +82,7 @@ export class ValidationService {
86
82
  *
87
83
  * @param checkpointHeader - The checkpoint header containing aggregated data
88
84
  * @param archive - The archive of the checkpoint
89
- * @param lastBlockInfo - Info about the last block (header, index, txs) or undefined
85
+ * @param lastBlockProposal - Signed block proposal for the last block in the checkpoint, or undefined
90
86
  * @param proposerAttesterAddress - The address of the proposer
91
87
  * @param options - Checkpoint proposal options
92
88
  *
@@ -95,14 +91,17 @@ export class ValidationService {
95
91
  public createCheckpointProposal(
96
92
  checkpointHeader: CheckpointHeader,
97
93
  archive: Fr,
94
+ checkpointNumber: CheckpointNumber,
98
95
  feeAssetPriceModifier: bigint,
99
- lastBlockInfo: CreateCheckpointProposalLastBlockData | undefined,
96
+ lastBlockProposal: BlockProposal | undefined,
100
97
  proposerAttesterAddress: EthAddress | undefined,
101
98
  options: CheckpointProposalOptions,
102
99
  ): Promise<CheckpointProposal> {
103
- // For testing: change the archive to trigger state_mismatch validation failure
100
+ // For testing: change the archive to trigger state_mismatch validation failure.
101
+ // If there's a last block proposal, use its (already invalid) archive to keep signatures consistent
102
+ // so P2P validation passes and the slasher can detect the offense.
104
103
  if (options.broadcastInvalidCheckpointProposal) {
105
- archive = Fr.random();
104
+ archive = lastBlockProposal?.archiveRoot ?? Fr.random();
106
105
  this.log.warn(`Creating INVALID checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
107
106
  }
108
107
 
@@ -112,19 +111,12 @@ export class ValidationService {
112
111
  return this.keyStore.signMessageWithAddress(address, payload, context);
113
112
  };
114
113
 
115
- // Last block to include in the proposal
116
- const lastBlock = lastBlockInfo && {
117
- blockHeader: lastBlockInfo.blockHeader,
118
- indexWithinCheckpoint: lastBlockInfo.indexWithinCheckpoint,
119
- txHashes: lastBlockInfo.txs.map(tx => tx.getTxHash()),
120
- txs: options.publishFullTxs ? lastBlockInfo.txs : undefined,
121
- };
122
-
123
114
  return CheckpointProposal.createProposalFromSigner(
124
115
  checkpointHeader,
125
116
  archive,
117
+ checkpointNumber,
126
118
  feeAssetPriceModifier,
127
- lastBlock,
119
+ lastBlockProposal,
128
120
  payloadSigner,
129
121
  );
130
122
  }
@@ -142,6 +134,7 @@ export class ValidationService {
142
134
  async attestToCheckpointProposal(
143
135
  proposal: CheckpointProposalCore,
144
136
  attestors: EthAddress[],
137
+ checkpointNumber: CheckpointNumber,
145
138
  ): Promise<CheckpointAttestation[]> {
146
139
  // Create the attestation payload from the checkpoint proposal
147
140
  const payload = new ConsensusPayload(proposal.checkpointHeader, proposal.archive, proposal.feeAssetPriceModifier);
@@ -149,14 +142,9 @@ export class ValidationService {
149
142
  keccak256(payload.getPayloadToSign(SignatureDomainSeparator.checkpointAttestation)),
150
143
  );
151
144
 
152
- // TODO(spy/ha): Use checkpointNumber instead of blockNumber once CheckpointHeader includes it.
153
- // CheckpointProposalCore doesn't have lastBlock info, so use 0 as a proxy.
154
- // blockNumber is NOT used for the primary key so it's safe to use here.
155
- // See CheckpointHeader TODO and SigningContext types documentation.
156
- const blockNumber = BlockNumber(0);
157
145
  const context: SigningContext = {
158
146
  slot: proposal.slotNumber,
159
- blockNumber,
147
+ checkpointNumber,
160
148
  dutyType: DutyType.ATTESTATION,
161
149
  };
162
150
 
@@ -195,7 +183,6 @@ export class ValidationService {
195
183
  * @param attestationsAndSigners - The attestations and signers to sign
196
184
  * @param proposer - The proposer address to sign with
197
185
  * @param slot - The slot number for HA signing context
198
- * @param blockNumber - The block or checkpoint number for HA signing context
199
186
  * @returns signature
200
187
  * @throws DutyAlreadySignedError if already signed by another HA node
201
188
  * @throws SlashingProtectionError if attempting to sign different data for same slot
@@ -204,11 +191,11 @@ export class ValidationService {
204
191
  attestationsAndSigners: CommitteeAttestationsAndSigners,
205
192
  proposer: EthAddress,
206
193
  slot: SlotNumber,
207
- blockNumber: BlockNumber | CheckpointNumber,
194
+ checkpointNumber: CheckpointNumber,
208
195
  ): Promise<Signature> {
209
196
  const context: SigningContext = {
210
197
  slot,
211
- blockNumber,
198
+ checkpointNumber,
212
199
  dutyType: DutyType.ATTESTATIONS_AND_SIGNERS,
213
200
  };
214
201
 
package/src/factory.ts CHANGED
@@ -46,6 +46,7 @@ export function createProposalHandler(
46
46
  metrics,
47
47
  deps.dateProvider,
48
48
  deps.telemetry,
49
+ undefined,
49
50
  );
50
51
  }
51
52
 
package/src/metrics.ts CHANGED
@@ -24,6 +24,8 @@ export class ValidatorMetrics {
24
24
  private reexMana: Histogram;
25
25
  private reexTx: Histogram;
26
26
  private reexDuration: Gauge;
27
+ private checkpointProposalToPipelinedStateDuration: Histogram;
28
+ private checkpointProposalReceiveOffsetFromNextSlotBoundary: Histogram;
27
29
 
28
30
  constructor(telemetryClient: TelemetryClient) {
29
31
  const meter = telemetryClient.getMeter('Validator');
@@ -77,6 +79,12 @@ export class ValidatorMetrics {
77
79
  this.reexTx = meter.createHistogram(Metrics.VALIDATOR_RE_EXECUTION_TX_COUNT);
78
80
 
79
81
  this.reexDuration = meter.createGauge(Metrics.VALIDATOR_RE_EXECUTION_TIME);
82
+ this.checkpointProposalToPipelinedStateDuration = meter.createHistogram(
83
+ Metrics.VALIDATOR_CHECKPOINT_PROPOSAL_TO_PIPELINED_STATE_DURATION,
84
+ );
85
+ this.checkpointProposalReceiveOffsetFromNextSlotBoundary = meter.createHistogram(
86
+ Metrics.VALIDATOR_CHECKPOINT_PROPOSAL_RECEIVE_OFFSET_FROM_NEXT_SLOT_BOUNDARY,
87
+ );
80
88
  }
81
89
 
82
90
  public recordReex(time: number, txs: number, mManaTotal: number) {
@@ -85,6 +93,16 @@ export class ValidatorMetrics {
85
93
  this.reexMana.record(mManaTotal);
86
94
  }
87
95
 
96
+ public recordCheckpointProposalToPipelinedStateDuration(durationMs: number) {
97
+ this.checkpointProposalToPipelinedStateDuration.record(Math.ceil(durationMs));
98
+ }
99
+
100
+ public recordCheckpointProposalReceiveOffsetFromNextSlotBoundary(offsetMs: number) {
101
+ this.checkpointProposalReceiveOffsetFromNextSlotBoundary.record(Math.ceil(Math.abs(offsetMs)), {
102
+ [Attributes.SLOT_BOUNDARY_SIDE]: offsetMs < 0 ? 'before' : 'after',
103
+ });
104
+ }
105
+
88
106
  public recordFailedReexecution(proposal: BlockProposal) {
89
107
  const proposer = proposal.getSender();
90
108
  this.failedReexecutionCounter.add(1, {