@aztec/validator-client 0.0.1-commit.e6bd8901 → 0.0.1-commit.ec5f612

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 (46) hide show
  1. package/README.md +21 -18
  2. package/dest/block_proposal_handler.d.ts +4 -5
  3. package/dest/block_proposal_handler.d.ts.map +1 -1
  4. package/dest/block_proposal_handler.js +23 -36
  5. package/dest/checkpoint_builder.d.ts +14 -12
  6. package/dest/checkpoint_builder.d.ts.map +1 -1
  7. package/dest/checkpoint_builder.js +51 -31
  8. package/dest/config.d.ts +1 -1
  9. package/dest/config.d.ts.map +1 -1
  10. package/dest/config.js +7 -4
  11. package/dest/duties/validation_service.d.ts +2 -2
  12. package/dest/duties/validation_service.d.ts.map +1 -1
  13. package/dest/duties/validation_service.js +3 -3
  14. package/dest/index.d.ts +1 -2
  15. package/dest/index.d.ts.map +1 -1
  16. package/dest/index.js +0 -1
  17. package/dest/key_store/ha_key_store.d.ts +1 -1
  18. package/dest/key_store/ha_key_store.d.ts.map +1 -1
  19. package/dest/key_store/ha_key_store.js +2 -2
  20. package/dest/metrics.d.ts +4 -3
  21. package/dest/metrics.d.ts.map +1 -1
  22. package/dest/metrics.js +34 -5
  23. package/dest/validator.d.ts +38 -14
  24. package/dest/validator.d.ts.map +1 -1
  25. package/dest/validator.js +182 -51
  26. package/package.json +19 -17
  27. package/src/block_proposal_handler.ts +34 -53
  28. package/src/checkpoint_builder.ts +74 -30
  29. package/src/config.ts +7 -4
  30. package/src/duties/validation_service.ts +9 -2
  31. package/src/index.ts +0 -1
  32. package/src/key_store/ha_key_store.ts +2 -2
  33. package/src/metrics.ts +45 -6
  34. package/src/validator.ts +236 -63
  35. package/dest/tx_validator/index.d.ts +0 -3
  36. package/dest/tx_validator/index.d.ts.map +0 -1
  37. package/dest/tx_validator/index.js +0 -2
  38. package/dest/tx_validator/nullifier_cache.d.ts +0 -14
  39. package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
  40. package/dest/tx_validator/nullifier_cache.js +0 -24
  41. package/dest/tx_validator/tx_validator_factory.d.ts +0 -18
  42. package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
  43. package/dest/tx_validator/tx_validator_factory.js +0 -54
  44. package/src/tx_validator/index.ts +0 -2
  45. package/src/tx_validator/nullifier_cache.ts +0 -30
  46. package/src/tx_validator/tx_validator_factory.ts +0 -135
package/dest/validator.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { getBlobsPerL1Block } from '@aztec/blob-lib';
2
- import { BlockNumber } from '@aztec/foundation/branded-types';
2
+ import { validateFeeAssetPriceModifier } from '@aztec/ethereum/contracts';
3
+ import { BlockNumber, SlotNumber } from '@aztec/foundation/branded-types';
3
4
  import { TimeoutError } from '@aztec/foundation/error';
4
5
  import { createLogger } from '@aztec/foundation/log';
5
6
  import { retryUntil } from '@aztec/foundation/retry';
@@ -8,7 +9,8 @@ import { sleep } from '@aztec/foundation/sleep';
8
9
  import { DateProvider } from '@aztec/foundation/timer';
9
10
  import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
10
11
  import { OffenseType, WANT_TO_SLASH_EVENT } from '@aztec/slasher';
11
- import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
12
+ import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
13
+ import { accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
12
14
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
13
15
  import { getTelemetryClient } from '@aztec/telemetry-client';
14
16
  import { createHASigner } from '@aztec/validator-ha-signer/factory';
@@ -40,6 +42,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
40
42
  l1ToL2MessageSource;
41
43
  config;
42
44
  blobClient;
45
+ haSigner;
43
46
  dateProvider;
44
47
  tracer;
45
48
  validationService;
@@ -47,17 +50,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
47
50
  log;
48
51
  // Whether it has already registered handlers on the p2p client
49
52
  hasRegisteredHandlers;
50
- // Used to check if we are sending the same proposal twice
51
- previousProposal;
53
+ /** Tracks the last block proposal we created, to detect duplicate proposal attempts. */ lastProposedBlock;
54
+ /** Tracks the last checkpoint proposal we created. */ lastProposedCheckpoint;
52
55
  lastEpochForCommitteeUpdateLoop;
53
56
  epochCacheUpdateLoop;
54
57
  proposersOfInvalidBlocks;
55
- // TODO(palla/mbps): Remove this once checkpoint validation is stable and we can validate all blocks properly.
56
- // Tracks slots for which we have successfully validated a block proposal, so we can attest to checkpoint proposals for those slots.
57
- // eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
58
- validatedBlockSlots;
59
- constructor(keyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
60
- 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();
58
+ /** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */ lastAttestedProposal;
59
+ constructor(keyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, haSigner, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
60
+ 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.haSigner = haSigner, this.dateProvider = dateProvider, this.hasRegisteredHandlers = false, this.proposersOfInvalidBlocks = new Set();
61
61
  // Create child logger with fisherman prefix if in fisherman mode
62
62
  this.log = config.fishermanMode ? log.createChild('[FISHERMAN]') : log;
63
63
  this.tracer = telemetry.getTracer('Validator');
@@ -117,17 +117,23 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
117
117
  txsPermitted: !config.disableTransactions
118
118
  });
119
119
  const blockProposalHandler = new BlockProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, config, metrics, dateProvider, telemetry);
120
- let validatorKeyStore = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
120
+ const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
121
+ let validatorKeyStore = nodeKeystoreAdapter;
122
+ let haSigner;
121
123
  if (config.haSigningEnabled) {
122
124
  // If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
123
125
  const haConfig = {
124
126
  ...config,
125
127
  maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000
126
128
  };
127
- const { signer } = await createHASigner(haConfig);
128
- validatorKeyStore = new HAKeyStore(validatorKeyStore, signer);
129
+ const { signer } = await createHASigner(haConfig, {
130
+ telemetryClient: telemetry,
131
+ dateProvider
132
+ });
133
+ haSigner = signer;
134
+ validatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, signer);
129
135
  }
130
- const validator = new ValidatorClient(validatorKeyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, dateProvider, telemetry);
136
+ const validator = new ValidatorClient(validatorKeyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, haSigner, dateProvider, telemetry);
131
137
  return validator;
132
138
  }
133
139
  getValidatorAddresses() {
@@ -154,6 +160,20 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
154
160
  ...config
155
161
  };
156
162
  }
163
+ reloadKeystore(newManager) {
164
+ if (this.config.haSigningEnabled && !this.haSigner) {
165
+ this.log.warn('HA signing is enabled in config but was not initialized at startup. ' + 'Restart the node to enable HA signing.');
166
+ } else if (!this.config.haSigningEnabled && this.haSigner) {
167
+ 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.');
168
+ }
169
+ const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
170
+ if (this.haSigner) {
171
+ this.keyStore = new HAKeyStore(newAdapter, this.haSigner);
172
+ } else {
173
+ this.keyStore = newAdapter;
174
+ }
175
+ this.validationService = new ValidationService(this.keyStore, this.log.createChild('validation-service'));
176
+ }
157
177
  async start() {
158
178
  if (this.epochCacheUpdateLoop.isRunning()) {
159
179
  this.log.warn(`Validator client already started`);
@@ -186,6 +206,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
186
206
  // and processed separately via the block handler above.
187
207
  const checkpointHandler = (checkpoint, proposalSender)=>this.attestToCheckpointProposal(checkpoint, proposalSender);
188
208
  this.p2pClient.registerCheckpointProposalHandler(checkpointHandler);
209
+ // Duplicate proposal handler - triggers slashing for equivocation
210
+ this.p2pClient.registerDuplicateProposalCallback((info)=>{
211
+ this.handleDuplicateProposal(info);
212
+ });
213
+ // Duplicate attestation handler - triggers slashing for attestation equivocation
214
+ this.p2pClient.registerDuplicateAttestationCallback((info)=>{
215
+ this.handleDuplicateAttestation(info);
216
+ });
189
217
  const myAddresses = this.getValidatorAddresses();
190
218
  this.p2pClient.registerThisValidatorAddresses(myAddresses);
191
219
  await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
@@ -206,6 +234,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
206
234
  this.log.warn(`Received block proposal with invalid signature for slot ${slotNumber}`);
207
235
  return false;
208
236
  }
237
+ // Ignore proposals from ourselves (may happen in HA setups)
238
+ if (this.getValidatorAddresses().some((addr)=>addr.equals(proposer))) {
239
+ this.log.warn(`Ignoring block proposal from self for slot ${slotNumber}`, {
240
+ proposer: proposer.toString(),
241
+ slotNumber
242
+ });
243
+ return false;
244
+ }
209
245
  // Check if we're in the committee (for metrics purposes)
210
246
  const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
211
247
  const partOfCommittee = inCommittee.length > 0;
@@ -257,9 +293,6 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
257
293
  this.log.warn(`Escape hatch open for slot ${slotNumber}, rejecting block proposal`, proposalInfo);
258
294
  return false;
259
295
  }
260
- // TODO(palla/mbps): Remove this once checkpoint validation is stable.
261
- // Track that we successfully validated a block for this slot, so we can attest to checkpoint proposals for it.
262
- this.validatedBlockSlots.add(slotNumber);
263
296
  return true;
264
297
  }
265
298
  /**
@@ -280,6 +313,19 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
280
313
  this.log.warn(`Received checkpoint proposal with invalid signature for slot ${slotNumber}`);
281
314
  return undefined;
282
315
  }
316
+ // Ignore proposals from ourselves (may happen in HA setups)
317
+ if (this.getValidatorAddresses().some((addr)=>addr.equals(proposer))) {
318
+ this.log.warn(`Ignoring block proposal from self for slot ${slotNumber}`, {
319
+ proposer: proposer.toString(),
320
+ slotNumber
321
+ });
322
+ return undefined;
323
+ }
324
+ // Validate fee asset price modifier is within allowed range
325
+ if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
326
+ this.log.warn(`Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${slotNumber}`);
327
+ return undefined;
328
+ }
283
329
  // Check that I have any address in current committee before attesting
284
330
  const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
285
331
  const partOfCommittee = inCommittee.length > 0;
@@ -294,16 +340,9 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
294
340
  txHashes: proposal.txHashes.map((t)=>t.toString()),
295
341
  fishermanMode: this.config.fishermanMode || false
296
342
  });
297
- // TODO(palla/mbps): Remove this once checkpoint validation is stable.
298
- // Check that we have successfully validated a block for this slot before attesting to the checkpoint.
299
- if (!this.validatedBlockSlots.has(slotNumber)) {
300
- this.log.warn(`No validated block found for slot ${slotNumber}, refusing to attest to checkpoint`, proposalInfo);
301
- return undefined;
302
- }
303
343
  // Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
304
- // TODO(palla/mbps): Change default to false once checkpoint validation is stable.
305
- if (this.config.skipCheckpointProposalValidation !== false) {
306
- this.log.verbose(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
344
+ if (this.config.skipCheckpointProposalValidation) {
345
+ this.log.warn(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
307
346
  } else {
308
347
  const validationResult = await this.validateCheckpointProposal(proposal, proposalInfo);
309
348
  if (!validationResult.isValid) {
@@ -350,11 +389,32 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
350
389
  });
351
390
  return undefined;
352
391
  }
353
- return this.createCheckpointAttestationsFromProposal(proposal, attestors);
392
+ return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
393
+ }
394
+ /**
395
+ * Checks if we should attest to a slot based on equivocation prevention rules.
396
+ * @returns true if we should attest, false if we should skip
397
+ */ shouldAttestToSlot(slotNumber) {
398
+ // If attestToEquivocatedProposals is true, always allow
399
+ if (this.config.attestToEquivocatedProposals) {
400
+ return true;
401
+ }
402
+ // Check if incoming slot is strictly greater than last attested
403
+ if (this.lastAttestedProposal && slotNumber <= this.lastAttestedProposal.slotNumber) {
404
+ this.log.warn(`Refusing to process a proposal for slot ${slotNumber} given we already attested to a proposal for slot ${this.lastAttestedProposal.slotNumber}`);
405
+ return false;
406
+ }
407
+ return true;
354
408
  }
355
409
  async createCheckpointAttestationsFromProposal(proposal, attestors = []) {
410
+ // Equivocation check: must happen right before signing to minimize the race window
411
+ if (!this.shouldAttestToSlot(proposal.slotNumber)) {
412
+ return undefined;
413
+ }
356
414
  const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
357
- await this.p2pClient.addCheckpointAttestations(attestations);
415
+ // Track the proposal we attested to (to prevent equivocation)
416
+ this.lastAttestedProposal = proposal;
417
+ await this.p2pClient.addOwnCheckpointAttestations(attestations);
358
418
  return attestations;
359
419
  }
360
420
  /**
@@ -362,7 +422,10 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
362
422
  * @returns Validation result with isValid flag and reason if invalid.
363
423
  */ async validateCheckpointProposal(proposal, proposalInfo) {
364
424
  const slot = proposal.slotNumber;
365
- const timeoutSeconds = 10;
425
+ // Timeout block syncing at the start of the next slot
426
+ const config = this.checkpointsBuilder.getConfig();
427
+ const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
428
+ const timeoutSeconds = Math.max(1, nextSlotTimestampSeconds - Math.floor(this.dateProvider.now() / 1000));
366
429
  // Wait for last block to sync by archive
367
430
  let lastBlockHeader;
368
431
  try {
@@ -400,6 +463,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
400
463
  reason: 'no_blocks_for_slot'
401
464
  };
402
465
  }
466
+ // Ensure the last block for this slot matches the archive in the checkpoint proposal
467
+ if (!blocks.at(-1)?.archive.root.equals(proposal.archive)) {
468
+ this.log.warn(`Last block archive mismatch for checkpoint proposal`, proposalInfo);
469
+ return {
470
+ isValid: false,
471
+ reason: 'last_block_archive_mismatch'
472
+ };
473
+ }
403
474
  this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
404
475
  ...proposalInfo,
405
476
  blockNumbers: blocks.map((b)=>b.number)
@@ -410,18 +481,15 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
410
481
  const checkpointNumber = firstBlock.checkpointNumber;
411
482
  // Get L1-to-L2 messages for this checkpoint
412
483
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
413
- // Compute the previous checkpoint out hashes for the epoch.
414
- // TODO: There can be a more efficient way to get the previous checkpoint out hashes without having to fetch the
415
- // actual checkpoints and the blocks/txs in them.
484
+ // Collect the out hashes of all the checkpoints before this one in the same epoch
416
485
  const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
417
- const previousCheckpoints = (await this.blockSource.getCheckpointsForEpoch(epoch)).filter((b)=>b.number < checkpointNumber).sort((a, b)=>a.number - b.number);
418
- const previousCheckpointOutHashes = previousCheckpoints.map((c)=>c.getCheckpointOutHash());
486
+ const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch)).filter((c)=>c.checkpointNumber < checkpointNumber).map((c)=>c.checkpointOutHash);
419
487
  // Fork world state at the block before the first block
420
488
  const parentBlockNumber = BlockNumber(firstBlock.number - 1);
421
489
  const fork = await this.worldState.fork(parentBlockNumber);
422
490
  try {
423
491
  // Create checkpoint builder with all existing blocks
424
- const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, l1ToL2Messages, previousCheckpointOutHashes, fork, blocks);
492
+ const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, proposal.feeAssetPriceModifier, l1ToL2Messages, previousCheckpointOutHashes, fork, blocks, this.log.getBindings());
425
493
  // Complete the checkpoint to get computed values
426
494
  const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
427
495
  // Compare checkpoint header with proposal
@@ -448,13 +516,20 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
448
516
  reason: 'archive_mismatch'
449
517
  };
450
518
  }
451
- // Check that the accumulated out hash matches the value in the proposal.
452
- const computedOutHash = computedCheckpoint.getCheckpointOutHash();
453
- const proposalOutHash = proposal.checkpointHeader.epochOutHash;
454
- if (!computedOutHash.equals(proposalOutHash)) {
519
+ // Check that the accumulated epoch out hash matches the value in the proposal.
520
+ // The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
521
+ const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
522
+ const computedEpochOutHash = accumulateCheckpointOutHashes([
523
+ ...previousCheckpointOutHashes,
524
+ checkpointOutHash
525
+ ]);
526
+ const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
527
+ if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
455
528
  this.log.warn(`Epoch out hash mismatch`, {
456
- proposalOutHash: proposalOutHash.toString(),
457
- computedOutHash: computedOutHash.toString(),
529
+ proposalEpochOutHash: proposalEpochOutHash.toString(),
530
+ computedEpochOutHash: computedEpochOutHash.toString(),
531
+ checkpointOutHash: checkpointOutHash.toString(),
532
+ previousCheckpointOutHashes: previousCheckpointOutHashes.map((h)=>h.toString()),
458
533
  ...proposalInfo
459
534
  });
460
535
  return {
@@ -478,6 +553,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
478
553
  chainId: gv.chainId,
479
554
  version: gv.version,
480
555
  slotNumber: gv.slotNumber,
556
+ timestamp: gv.timestamp,
481
557
  coinbase: gv.coinbase,
482
558
  feeRecipient: gv.feeRecipient,
483
559
  gasFees: gv.gasFees
@@ -498,7 +574,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
498
574
  return;
499
575
  }
500
576
  const blobFields = blocks.flatMap((b)=>b.toBlobFields());
501
- const blobs = getBlobsPerL1Block(blobFields);
577
+ const blobs = await getBlobsPerL1Block(blobFields);
502
578
  await this.blobClient.sendBlobsToFilestore(blobs);
503
579
  this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
504
580
  ...proposalInfo,
@@ -530,23 +606,75 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
530
606
  }
531
607
  ]);
532
608
  }
533
- async createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, options) {
534
- // TODO(palla/mbps): Prevent double proposals properly
535
- // if (this.previousProposal?.slotNumber === blockHeader.globalVariables.slotNumber) {
536
- // this.log.verbose(`Already made a proposal for the same slot, skipping proposal`);
537
- // return Promise.resolve(undefined);
538
- // }
609
+ /**
610
+ * Handle detection of a duplicate proposal (equivocation).
611
+ * Emits a slash event when a proposer sends multiple proposals for the same position.
612
+ */ handleDuplicateProposal(info) {
613
+ const { slot, proposer, type } = info;
614
+ this.log.warn(`Triggering slash event for duplicate ${type} proposal from ${proposer.toString()} at slot ${slot}`, {
615
+ proposer: proposer.toString(),
616
+ slot,
617
+ type
618
+ });
619
+ // Emit slash event
620
+ this.emit(WANT_TO_SLASH_EVENT, [
621
+ {
622
+ validator: proposer,
623
+ amount: this.config.slashDuplicateProposalPenalty,
624
+ offenseType: OffenseType.DUPLICATE_PROPOSAL,
625
+ epochOrSlot: BigInt(slot)
626
+ }
627
+ ]);
628
+ }
629
+ /**
630
+ * Handle detection of a duplicate attestation (equivocation).
631
+ * Emits a slash event when an attester signs attestations for different proposals at the same slot.
632
+ */ handleDuplicateAttestation(info) {
633
+ const { slot, attester } = info;
634
+ this.log.warn(`Triggering slash event for duplicate attestation from ${attester.toString()} at slot ${slot}`, {
635
+ attester: attester.toString(),
636
+ slot
637
+ });
638
+ this.emit(WANT_TO_SLASH_EVENT, [
639
+ {
640
+ validator: attester,
641
+ amount: this.config.slashDuplicateAttestationPenalty,
642
+ offenseType: OffenseType.DUPLICATE_ATTESTATION,
643
+ epochOrSlot: BigInt(slot)
644
+ }
645
+ ]);
646
+ }
647
+ async createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, options = {}) {
648
+ // Validate that we're not creating a proposal for an older or equal position
649
+ if (this.lastProposedBlock) {
650
+ const lastSlot = this.lastProposedBlock.slotNumber;
651
+ const lastIndex = this.lastProposedBlock.indexWithinCheckpoint;
652
+ const newSlot = blockHeader.globalVariables.slotNumber;
653
+ if (newSlot < lastSlot || newSlot === lastSlot && indexWithinCheckpoint <= lastIndex) {
654
+ throw new Error(`Cannot create block proposal for slot ${newSlot} index ${indexWithinCheckpoint}: ` + `already proposed block for slot ${lastSlot} index ${lastIndex}`);
655
+ }
656
+ }
539
657
  this.log.info(`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`);
540
658
  const newProposal = await this.validationService.createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, {
541
659
  ...options,
542
660
  broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal
543
661
  });
544
- this.previousProposal = newProposal;
662
+ this.lastProposedBlock = newProposal;
545
663
  return newProposal;
546
664
  }
547
- async createCheckpointProposal(checkpointHeader, archive, lastBlockInfo, proposerAddress, options) {
665
+ async createCheckpointProposal(checkpointHeader, archive, feeAssetPriceModifier, lastBlockInfo, proposerAddress, options = {}) {
666
+ // Validate that we're not creating a proposal for an older or equal slot
667
+ if (this.lastProposedCheckpoint) {
668
+ const lastSlot = this.lastProposedCheckpoint.slotNumber;
669
+ const newSlot = checkpointHeader.slotNumber;
670
+ if (newSlot <= lastSlot) {
671
+ throw new Error(`Cannot create checkpoint proposal for slot ${newSlot}: ` + `already proposed checkpoint for slot ${lastSlot}`);
672
+ }
673
+ }
548
674
  this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
549
- return await this.validationService.createCheckpointProposal(checkpointHeader, archive, lastBlockInfo, proposerAddress, options);
675
+ const newProposal = await this.validationService.createCheckpointProposal(checkpointHeader, archive, feeAssetPriceModifier, lastBlockInfo, proposerAddress, options);
676
+ this.lastProposedCheckpoint = newProposal;
677
+ return newProposal;
550
678
  }
551
679
  async broadcastBlockProposal(proposal) {
552
680
  await this.p2pClient.broadcastProposal(proposal);
@@ -561,6 +689,9 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
561
689
  inCommittee
562
690
  });
563
691
  const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
692
+ if (!attestations) {
693
+ return [];
694
+ }
564
695
  // We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
565
696
  // other nodes can see that our validators did attest to this block proposal, and do not slash us
566
697
  // due to inactivity for missed attestations.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/validator-client",
3
- "version": "0.0.1-commit.e6bd8901",
3
+ "version": "0.0.1-commit.ec5f612",
4
4
  "main": "dest/index.js",
5
5
  "type": "module",
6
6
  "exports": {
@@ -64,28 +64,30 @@
64
64
  ]
65
65
  },
66
66
  "dependencies": {
67
- "@aztec/blob-client": "0.0.1-commit.e6bd8901",
68
- "@aztec/blob-lib": "0.0.1-commit.e6bd8901",
69
- "@aztec/constants": "0.0.1-commit.e6bd8901",
70
- "@aztec/epoch-cache": "0.0.1-commit.e6bd8901",
71
- "@aztec/ethereum": "0.0.1-commit.e6bd8901",
72
- "@aztec/foundation": "0.0.1-commit.e6bd8901",
73
- "@aztec/node-keystore": "0.0.1-commit.e6bd8901",
74
- "@aztec/noir-protocol-circuits-types": "0.0.1-commit.e6bd8901",
75
- "@aztec/p2p": "0.0.1-commit.e6bd8901",
76
- "@aztec/protocol-contracts": "0.0.1-commit.e6bd8901",
77
- "@aztec/prover-client": "0.0.1-commit.e6bd8901",
78
- "@aztec/simulator": "0.0.1-commit.e6bd8901",
79
- "@aztec/slasher": "0.0.1-commit.e6bd8901",
80
- "@aztec/stdlib": "0.0.1-commit.e6bd8901",
81
- "@aztec/telemetry-client": "0.0.1-commit.e6bd8901",
82
- "@aztec/validator-ha-signer": "0.0.1-commit.e6bd8901",
67
+ "@aztec/blob-client": "0.0.1-commit.ec5f612",
68
+ "@aztec/blob-lib": "0.0.1-commit.ec5f612",
69
+ "@aztec/constants": "0.0.1-commit.ec5f612",
70
+ "@aztec/epoch-cache": "0.0.1-commit.ec5f612",
71
+ "@aztec/ethereum": "0.0.1-commit.ec5f612",
72
+ "@aztec/foundation": "0.0.1-commit.ec5f612",
73
+ "@aztec/node-keystore": "0.0.1-commit.ec5f612",
74
+ "@aztec/noir-protocol-circuits-types": "0.0.1-commit.ec5f612",
75
+ "@aztec/p2p": "0.0.1-commit.ec5f612",
76
+ "@aztec/protocol-contracts": "0.0.1-commit.ec5f612",
77
+ "@aztec/prover-client": "0.0.1-commit.ec5f612",
78
+ "@aztec/simulator": "0.0.1-commit.ec5f612",
79
+ "@aztec/slasher": "0.0.1-commit.ec5f612",
80
+ "@aztec/stdlib": "0.0.1-commit.ec5f612",
81
+ "@aztec/telemetry-client": "0.0.1-commit.ec5f612",
82
+ "@aztec/validator-ha-signer": "0.0.1-commit.ec5f612",
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.ec5f612",
90
+ "@aztec/world-state": "0.0.1-commit.ec5f612",
89
91
  "@electric-sql/pglite": "^0.3.14",
90
92
  "@jest/globals": "^30.0.0",
91
93
  "@types/jest": "^30.0.0",