@aztec/validator-client 0.0.1-commit.f295ac2 → 0.0.1-commit.f504929

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +22 -19
  2. package/dest/block_proposal_handler.d.ts +6 -8
  3. package/dest/block_proposal_handler.d.ts.map +1 -1
  4. package/dest/block_proposal_handler.js +28 -57
  5. package/dest/checkpoint_builder.d.ts +15 -13
  6. package/dest/checkpoint_builder.d.ts.map +1 -1
  7. package/dest/checkpoint_builder.js +55 -32
  8. package/dest/config.d.ts +1 -1
  9. package/dest/config.d.ts.map +1 -1
  10. package/dest/config.js +9 -7
  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/factory.d.ts +1 -1
  15. package/dest/factory.d.ts.map +1 -1
  16. package/dest/factory.js +2 -1
  17. package/dest/index.d.ts +1 -2
  18. package/dest/index.d.ts.map +1 -1
  19. package/dest/index.js +0 -1
  20. package/dest/key_store/ha_key_store.d.ts +1 -1
  21. package/dest/key_store/ha_key_store.d.ts.map +1 -1
  22. package/dest/key_store/ha_key_store.js +2 -2
  23. package/dest/metrics.d.ts +12 -3
  24. package/dest/metrics.d.ts.map +1 -1
  25. package/dest/metrics.js +46 -5
  26. package/dest/validator.d.ts +40 -14
  27. package/dest/validator.d.ts.map +1 -1
  28. package/dest/validator.js +196 -52
  29. package/package.json +19 -17
  30. package/src/block_proposal_handler.ts +40 -81
  31. package/src/checkpoint_builder.ts +80 -33
  32. package/src/config.ts +9 -7
  33. package/src/duties/validation_service.ts +9 -2
  34. package/src/factory.ts +1 -0
  35. package/src/index.ts +0 -1
  36. package/src/key_store/ha_key_store.ts +2 -2
  37. package/src/metrics.ts +63 -6
  38. package/src/validator.ts +253 -65
  39. package/dest/tx_validator/index.d.ts +0 -3
  40. package/dest/tx_validator/index.d.ts.map +0 -1
  41. package/dest/tx_validator/index.js +0 -2
  42. package/dest/tx_validator/nullifier_cache.d.ts +0 -14
  43. package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
  44. package/dest/tx_validator/nullifier_cache.js +0 -24
  45. package/dest/tx_validator/tx_validator_factory.d.ts +0 -18
  46. package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
  47. package/dest/tx_validator/tx_validator_factory.js +0 -53
  48. package/src/tx_validator/index.ts +0 -2
  49. package/src/tx_validator/nullifier_cache.ts +0 -30
  50. package/src/tx_validator/tx_validator_factory.ts +0 -133
package/src/validator.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { BlobClientInterface } from '@aztec/blob-client/client';
2
2
  import { type Blob, getBlobsPerL1Block } from '@aztec/blob-lib';
3
3
  import type { EpochCache } from '@aztec/epoch-cache';
4
+ import { validateFeeAssetPriceModifier } from '@aztec/ethereum/contracts';
4
5
  import {
5
6
  BlockNumber,
6
7
  CheckpointNumber,
@@ -18,33 +19,35 @@ import { RunningPromise } from '@aztec/foundation/running-promise';
18
19
  import { sleep } from '@aztec/foundation/sleep';
19
20
  import { DateProvider } from '@aztec/foundation/timer';
20
21
  import type { KeystoreManager } from '@aztec/node-keystore';
21
- import type { P2P, PeerId, TxProvider } from '@aztec/p2p';
22
+ import type { DuplicateAttestationInfo, DuplicateProposalInfo, P2P, PeerId } from '@aztec/p2p';
22
23
  import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
23
24
  import { OffenseType, WANT_TO_SLASH_EVENT, type Watcher, type WatcherEmitter } from '@aztec/slasher';
24
25
  import type { AztecAddress } from '@aztec/stdlib/aztec-address';
25
- import type { CommitteeAttestationsAndSigners, L2BlockNew, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
26
- import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
26
+ import type { CommitteeAttestationsAndSigners, L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
27
+ import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
27
28
  import type {
28
29
  CreateCheckpointProposalLastBlockData,
30
+ ITxProvider,
29
31
  Validator,
30
32
  ValidatorClientFullConfig,
31
33
  WorldStateSynchronizer,
32
34
  } from '@aztec/stdlib/interfaces/server';
33
- import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
34
- import type {
35
- BlockProposal,
36
- BlockProposalOptions,
37
- CheckpointAttestation,
38
- CheckpointProposalCore,
39
- CheckpointProposalOptions,
35
+ import { type L1ToL2MessageSource, accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
36
+ import {
37
+ type BlockProposal,
38
+ type BlockProposalOptions,
39
+ type CheckpointAttestation,
40
+ CheckpointProposal,
41
+ type CheckpointProposalCore,
42
+ type CheckpointProposalOptions,
40
43
  } from '@aztec/stdlib/p2p';
41
- import { CheckpointProposal } from '@aztec/stdlib/p2p';
42
44
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
43
45
  import type { BlockHeader, CheckpointGlobalVariables, Tx } from '@aztec/stdlib/tx';
44
46
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
45
47
  import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
46
48
  import { createHASigner } from '@aztec/validator-ha-signer/factory';
47
49
  import { DutyType, type SigningContext } from '@aztec/validator-ha-signer/types';
50
+ import type { ValidatorHASigner } from '@aztec/validator-ha-signer/validator-ha-signer';
48
51
 
49
52
  import { EventEmitter } from 'events';
50
53
  import type { TypedDataDefinition } from 'viem';
@@ -75,22 +78,24 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
75
78
  private validationService: ValidationService;
76
79
  private metrics: ValidatorMetrics;
77
80
  private log: Logger;
78
-
79
81
  // Whether it has already registered handlers on the p2p client
80
82
  private hasRegisteredHandlers = false;
81
83
 
82
- // Used to check if we are sending the same proposal twice
83
- private previousProposal?: BlockProposal;
84
+ /** Tracks the last block proposal we created, to detect duplicate proposal attempts. */
85
+ private lastProposedBlock?: BlockProposal;
86
+
87
+ /** Tracks the last checkpoint proposal we created. */
88
+ private lastProposedCheckpoint?: CheckpointProposal;
84
89
 
85
90
  private lastEpochForCommitteeUpdateLoop: EpochNumber | undefined;
86
91
  private epochCacheUpdateLoop: RunningPromise;
92
+ /** Tracks the last epoch in which each attester successfully submitted at least one attestation. */
93
+ private lastAttestedEpochByAttester: Map<string, EpochNumber> = new Map();
87
94
 
88
95
  private proposersOfInvalidBlocks: Set<string> = new Set();
89
96
 
90
- // TODO(palla/mbps): Remove this once checkpoint validation is stable and we can validate all blocks properly.
91
- // Tracks slots for which we have successfully validated a block proposal, so we can attest to checkpoint proposals for those slots.
92
- // eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
93
- private validatedBlockSlots: Set<SlotNumber> = new Set();
97
+ /** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
98
+ private lastAttestedProposal?: CheckpointProposalCore;
94
99
 
95
100
  protected constructor(
96
101
  private keyStore: ExtendedValidatorKeyStore,
@@ -103,6 +108,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
103
108
  private l1ToL2MessageSource: L1ToL2MessageSource,
104
109
  private config: ValidatorClientFullConfig,
105
110
  private blobClient: BlobClientInterface,
111
+ private haSigner: ValidatorHASigner | undefined,
106
112
  private dateProvider: DateProvider = new DateProvider(),
107
113
  telemetry: TelemetryClient = getTelemetryClient(),
108
114
  log = createLogger('validator'),
@@ -156,6 +162,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
156
162
  this.log.trace(`No committee found for slot`);
157
163
  return;
158
164
  }
165
+ this.metrics.setCurrentEpoch(epoch);
159
166
  if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
160
167
  const me = this.getValidatorAddresses();
161
168
  const committeeSet = new Set(committee.map(v => v.toString()));
@@ -184,7 +191,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
184
191
  p2pClient: P2P,
185
192
  blockSource: L2BlockSource & L2BlockSink,
186
193
  l1ToL2MessageSource: L1ToL2MessageSource,
187
- txProvider: TxProvider,
194
+ txProvider: ITxProvider,
188
195
  keyStoreManager: KeystoreManager,
189
196
  blobClient: BlobClientInterface,
190
197
  dateProvider: DateProvider = new DateProvider(),
@@ -193,6 +200,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
193
200
  const metrics = new ValidatorMetrics(telemetry);
194
201
  const blockProposalValidator = new BlockProposalValidator(epochCache, {
195
202
  txsPermitted: !config.disableTransactions,
203
+ maxTxsPerBlock: config.maxTxsPerBlock,
196
204
  });
197
205
  const blockProposalHandler = new BlockProposalHandler(
198
206
  checkpointsBuilder,
@@ -208,15 +216,18 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
208
216
  telemetry,
209
217
  );
210
218
 
211
- let validatorKeyStore: ExtendedValidatorKeyStore = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
219
+ const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
220
+ let validatorKeyStore: ExtendedValidatorKeyStore = nodeKeystoreAdapter;
221
+ let haSigner: ValidatorHASigner | undefined;
212
222
  if (config.haSigningEnabled) {
213
223
  // If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
214
224
  const haConfig = {
215
225
  ...config,
216
226
  maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000,
217
227
  };
218
- const { signer } = await createHASigner(haConfig);
219
- validatorKeyStore = new HAKeyStore(validatorKeyStore, signer);
228
+ const { signer } = await createHASigner(haConfig, { telemetryClient: telemetry, dateProvider });
229
+ haSigner = signer;
230
+ validatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, signer);
220
231
  }
221
232
 
222
233
  const validator = new ValidatorClient(
@@ -230,6 +241,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
230
241
  l1ToL2MessageSource,
231
242
  config,
232
243
  blobClient,
244
+ haSigner,
233
245
  dateProvider,
234
246
  telemetry,
235
247
  );
@@ -267,6 +279,28 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
267
279
  this.config = { ...this.config, ...config };
268
280
  }
269
281
 
282
+ public reloadKeystore(newManager: KeystoreManager): void {
283
+ if (this.config.haSigningEnabled && !this.haSigner) {
284
+ this.log.warn(
285
+ 'HA signing is enabled in config but was not initialized at startup. ' +
286
+ 'Restart the node to enable HA signing.',
287
+ );
288
+ } else if (!this.config.haSigningEnabled && this.haSigner) {
289
+ this.log.warn(
290
+ 'HA signing was disabled via config update but the HA signer is still active. ' +
291
+ 'Restart the node to fully disable HA signing.',
292
+ );
293
+ }
294
+
295
+ const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
296
+ if (this.haSigner) {
297
+ this.keyStore = new HAKeyStore(newAdapter, this.haSigner);
298
+ } else {
299
+ this.keyStore = newAdapter;
300
+ }
301
+ this.validationService = new ValidationService(this.keyStore, this.log.createChild('validation-service'));
302
+ }
303
+
270
304
  public async start() {
271
305
  if (this.epochCacheUpdateLoop.isRunning()) {
272
306
  this.log.warn(`Validator client already started`);
@@ -313,6 +347,16 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
313
347
  ): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
314
348
  this.p2pClient.registerCheckpointProposalHandler(checkpointHandler);
315
349
 
350
+ // Duplicate proposal handler - triggers slashing for equivocation
351
+ this.p2pClient.registerDuplicateProposalCallback((info: DuplicateProposalInfo) => {
352
+ this.handleDuplicateProposal(info);
353
+ });
354
+
355
+ // Duplicate attestation handler - triggers slashing for attestation equivocation
356
+ this.p2pClient.registerDuplicateAttestationCallback((info: DuplicateAttestationInfo) => {
357
+ this.handleDuplicateAttestation(info);
358
+ });
359
+
316
360
  const myAddresses = this.getValidatorAddresses();
317
361
  this.p2pClient.registerThisValidatorAddresses(myAddresses);
318
362
 
@@ -340,6 +384,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
340
384
  return false;
341
385
  }
342
386
 
387
+ // Ignore proposals from ourselves (may happen in HA setups)
388
+ if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
389
+ this.log.warn(`Ignoring block proposal from self for slot ${slotNumber}`, {
390
+ proposer: proposer.toString(),
391
+ slotNumber,
392
+ });
393
+ return false;
394
+ }
395
+
343
396
  // Check if we're in the committee (for metrics purposes)
344
397
  const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
345
398
  const partOfCommittee = inCommittee.length > 0;
@@ -413,10 +466,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
413
466
  return false;
414
467
  }
415
468
 
416
- // TODO(palla/mbps): Remove this once checkpoint validation is stable.
417
- // Track that we successfully validated a block for this slot, so we can attest to checkpoint proposals for it.
418
- this.validatedBlockSlots.add(slotNumber);
419
-
420
469
  return true;
421
470
  }
422
471
 
@@ -445,6 +494,23 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
445
494
  return undefined;
446
495
  }
447
496
 
497
+ // Ignore proposals from ourselves (may happen in HA setups)
498
+ if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
499
+ this.log.warn(`Ignoring block proposal from self for slot ${slotNumber}`, {
500
+ proposer: proposer.toString(),
501
+ slotNumber,
502
+ });
503
+ return undefined;
504
+ }
505
+
506
+ // Validate fee asset price modifier is within allowed range
507
+ if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
508
+ this.log.warn(
509
+ `Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${slotNumber}`,
510
+ );
511
+ return undefined;
512
+ }
513
+
448
514
  // Check that I have any address in current committee before attesting
449
515
  const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
450
516
  const partOfCommittee = inCommittee.length > 0;
@@ -461,17 +527,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
461
527
  fishermanMode: this.config.fishermanMode || false,
462
528
  });
463
529
 
464
- // TODO(palla/mbps): Remove this once checkpoint validation is stable.
465
- // Check that we have successfully validated a block for this slot before attesting to the checkpoint.
466
- if (!this.validatedBlockSlots.has(slotNumber)) {
467
- this.log.warn(`No validated block found for slot ${slotNumber}, refusing to attest to checkpoint`, proposalInfo);
468
- return undefined;
469
- }
470
-
471
530
  // Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
472
- // TODO(palla/mbps): Change default to false once checkpoint validation is stable.
473
- if (this.config.skipCheckpointProposalValidation !== false) {
474
- this.log.verbose(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
531
+ if (this.config.skipCheckpointProposalValidation) {
532
+ this.log.warn(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
475
533
  } else {
476
534
  const validationResult = await this.validateCheckpointProposal(proposal, proposalInfo);
477
535
  if (!validationResult.isValid) {
@@ -501,6 +559,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
501
559
 
502
560
  this.metrics.incSuccessfulAttestations(inCommittee.length);
503
561
 
562
+ // Track epoch participation per attester: count each (attester, epoch) pair at most once
563
+ const proposalEpoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
564
+ for (const attester of inCommittee) {
565
+ const key = attester.toString();
566
+ const lastEpoch = this.lastAttestedEpochByAttester.get(key);
567
+ if (lastEpoch === undefined || proposalEpoch > lastEpoch) {
568
+ this.lastAttestedEpochByAttester.set(key, proposalEpoch);
569
+ this.metrics.incAttestedEpochCount(attester);
570
+ }
571
+ }
572
+
504
573
  // Determine which validators should attest
505
574
  let attestors: EthAddress[];
506
575
  if (partOfCommittee) {
@@ -526,15 +595,45 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
526
595
  return undefined;
527
596
  }
528
597
 
529
- return this.createCheckpointAttestationsFromProposal(proposal, attestors);
598
+ return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
599
+ }
600
+
601
+ /**
602
+ * Checks if we should attest to a slot based on equivocation prevention rules.
603
+ * @returns true if we should attest, false if we should skip
604
+ */
605
+ private shouldAttestToSlot(slotNumber: SlotNumber): boolean {
606
+ // If attestToEquivocatedProposals is true, always allow
607
+ if (this.config.attestToEquivocatedProposals) {
608
+ return true;
609
+ }
610
+
611
+ // Check if incoming slot is strictly greater than last attested
612
+ if (this.lastAttestedProposal && slotNumber <= this.lastAttestedProposal.slotNumber) {
613
+ this.log.warn(
614
+ `Refusing to process a proposal for slot ${slotNumber} given we already attested to a proposal for slot ${this.lastAttestedProposal.slotNumber}`,
615
+ );
616
+ return false;
617
+ }
618
+
619
+ return true;
530
620
  }
531
621
 
532
622
  private async createCheckpointAttestationsFromProposal(
533
623
  proposal: CheckpointProposalCore,
534
624
  attestors: EthAddress[] = [],
535
- ): Promise<CheckpointAttestation[]> {
625
+ ): Promise<CheckpointAttestation[] | undefined> {
626
+ // Equivocation check: must happen right before signing to minimize the race window
627
+ if (!this.shouldAttestToSlot(proposal.slotNumber)) {
628
+ return undefined;
629
+ }
630
+
536
631
  const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
537
- await this.p2pClient.addCheckpointAttestations(attestations);
632
+
633
+ // Track the proposal we attested to (to prevent equivocation)
634
+ this.lastAttestedProposal = proposal;
635
+
636
+ await this.p2pClient.addOwnCheckpointAttestations(attestations);
538
637
  return attestations;
539
638
  }
540
639
 
@@ -547,7 +646,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
547
646
  proposalInfo: LogData,
548
647
  ): Promise<{ isValid: true } | { isValid: false; reason: string }> {
549
648
  const slot = proposal.slotNumber;
550
- const timeoutSeconds = 10;
649
+
650
+ // Timeout block syncing at the start of the next slot
651
+ const config = this.checkpointsBuilder.getConfig();
652
+ const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
653
+ const timeoutSeconds = Math.max(1, nextSlotTimestampSeconds - Math.floor(this.dateProvider.now() / 1000));
551
654
 
552
655
  // Wait for last block to sync by archive
553
656
  let lastBlockHeader: BlockHeader | undefined;
@@ -582,6 +685,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
582
685
  return { isValid: false, reason: 'no_blocks_for_slot' };
583
686
  }
584
687
 
688
+ // Ensure the last block for this slot matches the archive in the checkpoint proposal
689
+ if (!blocks.at(-1)?.archive.root.equals(proposal.archive)) {
690
+ this.log.warn(`Last block archive mismatch for checkpoint proposal`, proposalInfo);
691
+ return { isValid: false, reason: 'last_block_archive_mismatch' };
692
+ }
693
+
585
694
  this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
586
695
  ...proposalInfo,
587
696
  blockNumbers: blocks.map(b => b.number),
@@ -595,14 +704,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
595
704
  // Get L1-to-L2 messages for this checkpoint
596
705
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
597
706
 
598
- // Compute the previous checkpoint out hashes for the epoch.
599
- // TODO: There can be a more efficient way to get the previous checkpoint out hashes without having to fetch the
600
- // actual checkpoints and the blocks/txs in them.
707
+ // Collect the out hashes of all the checkpoints before this one in the same epoch
601
708
  const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
602
- const previousCheckpoints = (await this.blockSource.getCheckpointsForEpoch(epoch))
603
- .filter(b => b.number < checkpointNumber)
604
- .sort((a, b) => a.number - b.number);
605
- const previousCheckpointOutHashes = previousCheckpoints.map(c => c.getCheckpointOutHash());
709
+ const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch))
710
+ .filter(c => c.checkpointNumber < checkpointNumber)
711
+ .map(c => c.checkpointOutHash);
606
712
 
607
713
  // Fork world state at the block before the first block
608
714
  const parentBlockNumber = BlockNumber(firstBlock.number - 1);
@@ -613,10 +719,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
613
719
  const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(
614
720
  checkpointNumber,
615
721
  constants,
722
+ proposal.feeAssetPriceModifier,
616
723
  l1ToL2Messages,
617
724
  previousCheckpointOutHashes,
618
725
  fork,
619
726
  blocks,
727
+ this.log.getBindings(),
620
728
  );
621
729
 
622
730
  // Complete the checkpoint to get computed values
@@ -642,13 +750,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
642
750
  return { isValid: false, reason: 'archive_mismatch' };
643
751
  }
644
752
 
645
- // Check that the accumulated out hash matches the value in the proposal.
646
- const computedOutHash = computedCheckpoint.getCheckpointOutHash();
647
- const proposalOutHash = proposal.checkpointHeader.epochOutHash;
648
- if (!computedOutHash.equals(proposalOutHash)) {
753
+ // Check that the accumulated epoch out hash matches the value in the proposal.
754
+ // The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
755
+ const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
756
+ const computedEpochOutHash = accumulateCheckpointOutHashes([...previousCheckpointOutHashes, checkpointOutHash]);
757
+ const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
758
+ if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
649
759
  this.log.warn(`Epoch out hash mismatch`, {
650
- proposalOutHash: proposalOutHash.toString(),
651
- computedOutHash: computedOutHash.toString(),
760
+ proposalEpochOutHash: proposalEpochOutHash.toString(),
761
+ computedEpochOutHash: computedEpochOutHash.toString(),
762
+ checkpointOutHash: checkpointOutHash.toString(),
763
+ previousCheckpointOutHashes: previousCheckpointOutHashes.map(h => h.toString()),
652
764
  ...proposalInfo,
653
765
  });
654
766
  return { isValid: false, reason: 'out_hash_mismatch' };
@@ -664,12 +776,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
664
776
  /**
665
777
  * Extract checkpoint global variables from a block.
666
778
  */
667
- private extractCheckpointConstants(block: L2BlockNew): CheckpointGlobalVariables {
779
+ private extractCheckpointConstants(block: L2Block): CheckpointGlobalVariables {
668
780
  const gv = block.header.globalVariables;
669
781
  return {
670
782
  chainId: gv.chainId,
671
783
  version: gv.version,
672
784
  slotNumber: gv.slotNumber,
785
+ timestamp: gv.timestamp,
673
786
  coinbase: gv.coinbase,
674
787
  feeRecipient: gv.feeRecipient,
675
788
  gasFees: gv.gasFees,
@@ -679,7 +792,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
679
792
  /**
680
793
  * Uploads blobs for a checkpoint to the filestore (fire and forget).
681
794
  */
682
- private async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
795
+ protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
683
796
  try {
684
797
  const lastBlockHeader = await this.blockSource.getBlockHeaderByArchive(proposal.archive);
685
798
  if (!lastBlockHeader) {
@@ -694,7 +807,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
694
807
  }
695
808
 
696
809
  const blobFields = blocks.flatMap(b => b.toBlobFields());
697
- const blobs: Blob[] = getBlobsPerL1Block(blobFields);
810
+ const blobs: Blob[] = await getBlobsPerL1Block(blobFields);
698
811
  await this.blobClient.sendBlobsToFilestore(blobs);
699
812
  this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
700
813
  ...proposalInfo,
@@ -732,6 +845,52 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
732
845
  ]);
733
846
  }
734
847
 
848
+ /**
849
+ * Handle detection of a duplicate proposal (equivocation).
850
+ * Emits a slash event when a proposer sends multiple proposals for the same position.
851
+ */
852
+ private handleDuplicateProposal(info: DuplicateProposalInfo): void {
853
+ const { slot, proposer, type } = info;
854
+
855
+ this.log.warn(`Triggering slash event for duplicate ${type} proposal from ${proposer.toString()} at slot ${slot}`, {
856
+ proposer: proposer.toString(),
857
+ slot,
858
+ type,
859
+ });
860
+
861
+ // Emit slash event
862
+ this.emit(WANT_TO_SLASH_EVENT, [
863
+ {
864
+ validator: proposer,
865
+ amount: this.config.slashDuplicateProposalPenalty,
866
+ offenseType: OffenseType.DUPLICATE_PROPOSAL,
867
+ epochOrSlot: BigInt(slot),
868
+ },
869
+ ]);
870
+ }
871
+
872
+ /**
873
+ * Handle detection of a duplicate attestation (equivocation).
874
+ * Emits a slash event when an attester signs attestations for different proposals at the same slot.
875
+ */
876
+ private handleDuplicateAttestation(info: DuplicateAttestationInfo): void {
877
+ const { slot, attester } = info;
878
+
879
+ this.log.warn(`Triggering slash event for duplicate attestation from ${attester.toString()} at slot ${slot}`, {
880
+ attester: attester.toString(),
881
+ slot,
882
+ });
883
+
884
+ this.emit(WANT_TO_SLASH_EVENT, [
885
+ {
886
+ validator: attester,
887
+ amount: this.config.slashDuplicateAttestationPenalty,
888
+ offenseType: OffenseType.DUPLICATE_ATTESTATION,
889
+ epochOrSlot: BigInt(slot),
890
+ },
891
+ ]);
892
+ }
893
+
735
894
  async createBlockProposal(
736
895
  blockHeader: BlockHeader,
737
896
  indexWithinCheckpoint: IndexWithinCheckpoint,
@@ -739,13 +898,21 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
739
898
  archive: Fr,
740
899
  txs: Tx[],
741
900
  proposerAddress: EthAddress | undefined,
742
- options: BlockProposalOptions,
901
+ options: BlockProposalOptions = {},
743
902
  ): Promise<BlockProposal> {
744
- // TODO(palla/mbps): Prevent double proposals properly
745
- // if (this.previousProposal?.slotNumber === blockHeader.globalVariables.slotNumber) {
746
- // this.log.verbose(`Already made a proposal for the same slot, skipping proposal`);
747
- // return Promise.resolve(undefined);
748
- // }
903
+ // Validate that we're not creating a proposal for an older or equal position
904
+ if (this.lastProposedBlock) {
905
+ const lastSlot = this.lastProposedBlock.slotNumber;
906
+ const lastIndex = this.lastProposedBlock.indexWithinCheckpoint;
907
+ const newSlot = blockHeader.globalVariables.slotNumber;
908
+
909
+ if (newSlot < lastSlot || (newSlot === lastSlot && indexWithinCheckpoint <= lastIndex)) {
910
+ throw new Error(
911
+ `Cannot create block proposal for slot ${newSlot} index ${indexWithinCheckpoint}: ` +
912
+ `already proposed block for slot ${lastSlot} index ${lastIndex}`,
913
+ );
914
+ }
915
+ }
749
916
 
750
917
  this.log.info(
751
918
  `Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`,
@@ -762,25 +929,42 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
762
929
  broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
763
930
  },
764
931
  );
765
- this.previousProposal = newProposal;
932
+ this.lastProposedBlock = newProposal;
766
933
  return newProposal;
767
934
  }
768
935
 
769
936
  async createCheckpointProposal(
770
937
  checkpointHeader: CheckpointHeader,
771
938
  archive: Fr,
939
+ feeAssetPriceModifier: bigint,
772
940
  lastBlockInfo: CreateCheckpointProposalLastBlockData | undefined,
773
941
  proposerAddress: EthAddress | undefined,
774
- options: CheckpointProposalOptions,
942
+ options: CheckpointProposalOptions = {},
775
943
  ): Promise<CheckpointProposal> {
944
+ // Validate that we're not creating a proposal for an older or equal slot
945
+ if (this.lastProposedCheckpoint) {
946
+ const lastSlot = this.lastProposedCheckpoint.slotNumber;
947
+ const newSlot = checkpointHeader.slotNumber;
948
+
949
+ if (newSlot <= lastSlot) {
950
+ throw new Error(
951
+ `Cannot create checkpoint proposal for slot ${newSlot}: ` +
952
+ `already proposed checkpoint for slot ${lastSlot}`,
953
+ );
954
+ }
955
+ }
956
+
776
957
  this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
777
- return await this.validationService.createCheckpointProposal(
958
+ const newProposal = await this.validationService.createCheckpointProposal(
778
959
  checkpointHeader,
779
960
  archive,
961
+ feeAssetPriceModifier,
780
962
  lastBlockInfo,
781
963
  proposerAddress,
782
964
  options,
783
965
  );
966
+ this.lastProposedCheckpoint = newProposal;
967
+ return newProposal;
784
968
  }
785
969
 
786
970
  async broadcastBlockProposal(proposal: BlockProposal): Promise<void> {
@@ -802,6 +986,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
802
986
  this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
803
987
  const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
804
988
 
989
+ if (!attestations) {
990
+ return [];
991
+ }
992
+
805
993
  // We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
806
994
  // other nodes can see that our validators did attest to this block proposal, and do not slash us
807
995
  // due to inactivity for missed attestations.
@@ -1,3 +0,0 @@
1
- export * from './nullifier_cache.js';
2
- export * from './tx_validator_factory.js';
3
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy90eF92YWxpZGF0b3IvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxzQkFBc0IsQ0FBQztBQUNyQyxjQUFjLDJCQUEyQixDQUFDIn0=
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tx_validator/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,2BAA2B,CAAC"}
@@ -1,2 +0,0 @@
1
- export * from './nullifier_cache.js';
2
- export * from './tx_validator_factory.js';
@@ -1,14 +0,0 @@
1
- import type { NullifierSource } from '@aztec/p2p';
2
- import type { MerkleTreeReadOperations } from '@aztec/stdlib/interfaces/server';
3
- /**
4
- * Implements a nullifier source by checking a DB and an in-memory collection.
5
- * Intended for validating transactions as they are added to a block.
6
- */
7
- export declare class NullifierCache implements NullifierSource {
8
- private db;
9
- nullifiers: Set<string>;
10
- constructor(db: MerkleTreeReadOperations);
11
- nullifiersExist(nullifiers: Buffer[]): Promise<boolean[]>;
12
- addNullifiers(nullifiers: Buffer[]): void;
13
- }
14
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibnVsbGlmaWVyX2NhY2hlLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvdHhfdmFsaWRhdG9yL251bGxpZmllcl9jYWNoZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssRUFBRSxlQUFlLEVBQUUsTUFBTSxZQUFZLENBQUM7QUFDbEQsT0FBTyxLQUFLLEVBQUUsd0JBQXdCLEVBQUUsTUFBTSxpQ0FBaUMsQ0FBQztBQUdoRjs7O0dBR0c7QUFDSCxxQkFBYSxjQUFlLFlBQVcsZUFBZTtJQUd4QyxPQUFPLENBQUMsRUFBRTtJQUZ0QixVQUFVLEVBQUUsR0FBRyxDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBRXhCLFlBQW9CLEVBQUUsRUFBRSx3QkFBd0IsRUFFL0M7SUFFWSxlQUFlLENBQUMsVUFBVSxFQUFFLE1BQU0sRUFBRSxHQUFHLE9BQU8sQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQU9yRTtJQUVNLGFBQWEsQ0FBQyxVQUFVLEVBQUUsTUFBTSxFQUFFLFFBSXhDO0NBQ0YifQ==
@@ -1 +0,0 @@
1
- {"version":3,"file":"nullifier_cache.d.ts","sourceRoot":"","sources":["../../src/tx_validator/nullifier_cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAGhF;;;GAGG;AACH,qBAAa,cAAe,YAAW,eAAe;IAGxC,OAAO,CAAC,EAAE;IAFtB,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAExB,YAAoB,EAAE,EAAE,wBAAwB,EAE/C;IAEY,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAOrE;IAEM,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,QAIxC;CACF"}
@@ -1,24 +0,0 @@
1
- import { MerkleTreeId } from '@aztec/stdlib/trees';
2
- /**
3
- * Implements a nullifier source by checking a DB and an in-memory collection.
4
- * Intended for validating transactions as they are added to a block.
5
- */ export class NullifierCache {
6
- db;
7
- nullifiers;
8
- constructor(db){
9
- this.db = db;
10
- this.nullifiers = new Set();
11
- }
12
- async nullifiersExist(nullifiers) {
13
- const cacheResults = nullifiers.map((n)=>this.nullifiers.has(n.toString()));
14
- const toCheckDb = nullifiers.filter((_n, index)=>!cacheResults[index]);
15
- const dbHits = await this.db.findLeafIndices(MerkleTreeId.NULLIFIER_TREE, toCheckDb);
16
- let dbIndex = 0;
17
- return nullifiers.map((_n, index)=>cacheResults[index] || dbHits[dbIndex++] !== undefined);
18
- }
19
- addNullifiers(nullifiers) {
20
- for (const nullifier of nullifiers){
21
- this.nullifiers.add(nullifier.toString());
22
- }
23
- }
24
- }
@@ -1,18 +0,0 @@
1
- import { BlockNumber } from '@aztec/foundation/branded-types';
2
- import type { ContractDataSource } from '@aztec/stdlib/contract';
3
- import type { GasFees } from '@aztec/stdlib/gas';
4
- import type { AllowedElement, ClientProtocolCircuitVerifier, MerkleTreeReadOperations, PublicProcessorValidator } from '@aztec/stdlib/interfaces/server';
5
- import { GlobalVariables, type Tx, type TxValidator } from '@aztec/stdlib/tx';
6
- import type { UInt64 } from '@aztec/stdlib/types';
7
- export declare function createValidatorForAcceptingTxs(db: MerkleTreeReadOperations, contractDataSource: ContractDataSource, verifier: ClientProtocolCircuitVerifier | undefined, { l1ChainId, rollupVersion, setupAllowList, gasFees, skipFeeEnforcement, timestamp, blockNumber, txsPermitted }: {
8
- l1ChainId: number;
9
- rollupVersion: number;
10
- setupAllowList: AllowedElement[];
11
- gasFees: GasFees;
12
- skipFeeEnforcement?: boolean;
13
- timestamp: UInt64;
14
- blockNumber: BlockNumber;
15
- txsPermitted: boolean;
16
- }): TxValidator<Tx>;
17
- export declare function createValidatorForBlockBuilding(db: MerkleTreeReadOperations, contractDataSource: ContractDataSource, globalVariables: GlobalVariables, setupAllowList: AllowedElement[]): PublicProcessorValidator;
18
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHhfdmFsaWRhdG9yX2ZhY3RvcnkuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy90eF92YWxpZGF0b3IvdHhfdmFsaWRhdG9yX2ZhY3RvcnkudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFdBQVcsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBaUI5RCxPQUFPLEtBQUssRUFBRSxrQkFBa0IsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBQ2pFLE9BQU8sS0FBSyxFQUFFLE9BQU8sRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBQ2pELE9BQU8sS0FBSyxFQUNWLGNBQWMsRUFDZCw2QkFBNkIsRUFDN0Isd0JBQXdCLEVBQ3hCLHdCQUF3QixFQUN6QixNQUFNLGlDQUFpQyxDQUFDO0FBRXpDLE9BQU8sRUFBRSxlQUFlLEVBQUUsS0FBSyxFQUFFLEVBQUUsS0FBSyxXQUFXLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUM5RSxPQUFPLEtBQUssRUFBRSxNQUFNLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUlsRCx3QkFBZ0IsOEJBQThCLENBQzVDLEVBQUUsRUFBRSx3QkFBd0IsRUFDNUIsa0JBQWtCLEVBQUUsa0JBQWtCLEVBQ3RDLFFBQVEsRUFBRSw2QkFBNkIsR0FBRyxTQUFTLEVBQ25ELEVBQ0UsU0FBUyxFQUNULGFBQWEsRUFDYixjQUFjLEVBQ2QsT0FBTyxFQUNQLGtCQUFrQixFQUNsQixTQUFTLEVBQ1QsV0FBVyxFQUNYLFlBQVksRUFDYixFQUFFO0lBQ0QsU0FBUyxFQUFFLE1BQU0sQ0FBQztJQUNsQixhQUFhLEVBQUUsTUFBTSxDQUFDO0lBQ3RCLGNBQWMsRUFBRSxjQUFjLEVBQUUsQ0FBQztJQUNqQyxPQUFPLEVBQUUsT0FBTyxDQUFDO0lBQ2pCLGtCQUFrQixDQUFDLEVBQUUsT0FBTyxDQUFDO0lBQzdCLFNBQVMsRUFBRSxNQUFNLENBQUM7SUFDbEIsV0FBVyxFQUFFLFdBQVcsQ0FBQztJQUN6QixZQUFZLEVBQUUsT0FBTyxDQUFDO0NBQ3ZCLEdBQ0EsV0FBVyxDQUFDLEVBQUUsQ0FBQyxDQTRCakI7QUFFRCx3QkFBZ0IsK0JBQStCLENBQzdDLEVBQUUsRUFBRSx3QkFBd0IsRUFDNUIsa0JBQWtCLEVBQUUsa0JBQWtCLEVBQ3RDLGVBQWUsRUFBRSxlQUFlLEVBQ2hDLGNBQWMsRUFBRSxjQUFjLEVBQUUsR0FDL0Isd0JBQXdCLENBZ0IxQiJ9
@@ -1 +0,0 @@
1
- {"version":3,"file":"tx_validator_factory.d.ts","sourceRoot":"","sources":["../../src/tx_validator/tx_validator_factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAiB9D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,KAAK,EACV,cAAc,EACd,6BAA6B,EAC7B,wBAAwB,EACxB,wBAAwB,EACzB,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC9E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAIlD,wBAAgB,8BAA8B,CAC5C,EAAE,EAAE,wBAAwB,EAC5B,kBAAkB,EAAE,kBAAkB,EACtC,QAAQ,EAAE,6BAA6B,GAAG,SAAS,EACnD,EACE,SAAS,EACT,aAAa,EACb,cAAc,EACd,OAAO,EACP,kBAAkB,EAClB,SAAS,EACT,WAAW,EACX,YAAY,EACb,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,cAAc,EAAE,CAAC;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,WAAW,CAAC;IACzB,YAAY,EAAE,OAAO,CAAC;CACvB,GACA,WAAW,CAAC,EAAE,CAAC,CA4BjB;AAED,wBAAgB,+BAA+B,CAC7C,EAAE,EAAE,wBAAwB,EAC5B,kBAAkB,EAAE,kBAAkB,EACtC,eAAe,EAAE,eAAe,EAChC,cAAc,EAAE,cAAc,EAAE,GAC/B,wBAAwB,CAgB1B"}