@aztec/validator-client 0.0.1-commit.3469e52 → 0.0.1-commit.3895657bc

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 +64 -19
  2. package/dest/block_proposal_handler.d.ts +7 -9
  3. package/dest/block_proposal_handler.d.ts.map +1 -1
  4. package/dest/block_proposal_handler.js +71 -81
  5. package/dest/checkpoint_builder.d.ts +22 -13
  6. package/dest/checkpoint_builder.d.ts.map +1 -1
  7. package/dest/checkpoint_builder.js +107 -39
  8. package/dest/config.d.ts +1 -1
  9. package/dest/config.d.ts.map +1 -1
  10. package/dest/config.js +30 -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 +5 -11
  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 +212 -56
  29. package/package.json +19 -17
  30. package/src/block_proposal_handler.ts +87 -109
  31. package/src/checkpoint_builder.ts +146 -40
  32. package/src/config.ts +30 -7
  33. package/src/duties/validation_service.ts +11 -10
  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 +262 -68
  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 -54
  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 -135
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,36 @@ 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 { validateCheckpoint } from '@aztec/stdlib/checkpoint';
28
+ import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
27
29
  import type {
28
30
  CreateCheckpointProposalLastBlockData,
31
+ ITxProvider,
29
32
  Validator,
30
33
  ValidatorClientFullConfig,
31
34
  WorldStateSynchronizer,
32
35
  } 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,
36
+ import { type L1ToL2MessageSource, accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
37
+ import {
38
+ type BlockProposal,
39
+ type BlockProposalOptions,
40
+ type CheckpointAttestation,
41
+ CheckpointProposal,
42
+ type CheckpointProposalCore,
43
+ type CheckpointProposalOptions,
40
44
  } from '@aztec/stdlib/p2p';
41
- import { CheckpointProposal } from '@aztec/stdlib/p2p';
42
45
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
43
46
  import type { BlockHeader, CheckpointGlobalVariables, Tx } from '@aztec/stdlib/tx';
44
47
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
45
48
  import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
46
- import { createHASigner } from '@aztec/validator-ha-signer/factory';
49
+ import { createHASigner, createLocalSignerWithProtection } from '@aztec/validator-ha-signer/factory';
47
50
  import { DutyType, type SigningContext } from '@aztec/validator-ha-signer/types';
51
+ import type { ValidatorHASigner } from '@aztec/validator-ha-signer/validator-ha-signer';
48
52
 
49
53
  import { EventEmitter } from 'events';
50
54
  import type { TypedDataDefinition } from 'viem';
@@ -75,22 +79,24 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
75
79
  private validationService: ValidationService;
76
80
  private metrics: ValidatorMetrics;
77
81
  private log: Logger;
78
-
79
82
  // Whether it has already registered handlers on the p2p client
80
83
  private hasRegisteredHandlers = false;
81
84
 
82
- // Used to check if we are sending the same proposal twice
83
- private previousProposal?: BlockProposal;
85
+ /** Tracks the last block proposal we created, to detect duplicate proposal attempts. */
86
+ private lastProposedBlock?: BlockProposal;
87
+
88
+ /** Tracks the last checkpoint proposal we created. */
89
+ private lastProposedCheckpoint?: CheckpointProposal;
84
90
 
85
91
  private lastEpochForCommitteeUpdateLoop: EpochNumber | undefined;
86
92
  private epochCacheUpdateLoop: RunningPromise;
93
+ /** Tracks the last epoch in which each attester successfully submitted at least one attestation. */
94
+ private lastAttestedEpochByAttester: Map<string, EpochNumber> = new Map();
87
95
 
88
96
  private proposersOfInvalidBlocks: Set<string> = new Set();
89
97
 
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();
98
+ /** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
99
+ private lastAttestedProposal?: CheckpointProposalCore;
94
100
 
95
101
  protected constructor(
96
102
  private keyStore: ExtendedValidatorKeyStore,
@@ -103,6 +109,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
103
109
  private l1ToL2MessageSource: L1ToL2MessageSource,
104
110
  private config: ValidatorClientFullConfig,
105
111
  private blobClient: BlobClientInterface,
112
+ private slashingProtectionSigner: ValidatorHASigner,
106
113
  private dateProvider: DateProvider = new DateProvider(),
107
114
  telemetry: TelemetryClient = getTelemetryClient(),
108
115
  log = createLogger('validator'),
@@ -156,6 +163,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
156
163
  this.log.trace(`No committee found for slot`);
157
164
  return;
158
165
  }
166
+ this.metrics.setCurrentEpoch(epoch);
159
167
  if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
160
168
  const me = this.getValidatorAddresses();
161
169
  const committeeSet = new Set(committee.map(v => v.toString()));
@@ -184,7 +192,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
184
192
  p2pClient: P2P,
185
193
  blockSource: L2BlockSource & L2BlockSink,
186
194
  l1ToL2MessageSource: L1ToL2MessageSource,
187
- txProvider: TxProvider,
195
+ txProvider: ITxProvider,
188
196
  keyStoreManager: KeystoreManager,
189
197
  blobClient: BlobClientInterface,
190
198
  dateProvider: DateProvider = new DateProvider(),
@@ -193,6 +201,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
193
201
  const metrics = new ValidatorMetrics(telemetry);
194
202
  const blockProposalValidator = new BlockProposalValidator(epochCache, {
195
203
  txsPermitted: !config.disableTransactions,
204
+ maxTxsPerBlock: config.validateMaxTxsPerBlock,
196
205
  });
197
206
  const blockProposalHandler = new BlockProposalHandler(
198
207
  checkpointsBuilder,
@@ -208,16 +217,28 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
208
217
  telemetry,
209
218
  );
210
219
 
211
- let validatorKeyStore: ExtendedValidatorKeyStore = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
220
+ const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
221
+ let slashingProtectionSigner: ValidatorHASigner;
212
222
  if (config.haSigningEnabled) {
223
+ // Multi-node HA mode: use PostgreSQL-backed distributed locking.
213
224
  // If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
214
225
  const haConfig = {
215
226
  ...config,
216
227
  maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000,
217
228
  };
218
- const { signer } = await createHASigner(haConfig);
219
- validatorKeyStore = new HAKeyStore(validatorKeyStore, signer);
229
+ ({ signer: slashingProtectionSigner } = await createHASigner(haConfig, {
230
+ telemetryClient: telemetry,
231
+ dateProvider,
232
+ }));
233
+ } else {
234
+ // Single-node mode: use LMDB-backed local signing protection.
235
+ // This prevents double-signing if the node crashes and restarts mid-proposal.
236
+ ({ signer: slashingProtectionSigner } = await createLocalSignerWithProtection(config, {
237
+ telemetryClient: telemetry,
238
+ dateProvider,
239
+ }));
220
240
  }
241
+ const validatorKeyStore: ExtendedValidatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, slashingProtectionSigner);
221
242
 
222
243
  const validator = new ValidatorClient(
223
244
  validatorKeyStore,
@@ -230,6 +251,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
230
251
  l1ToL2MessageSource,
231
252
  config,
232
253
  blobClient,
254
+ slashingProtectionSigner,
233
255
  dateProvider,
234
256
  telemetry,
235
257
  );
@@ -267,6 +289,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
267
289
  this.config = { ...this.config, ...config };
268
290
  }
269
291
 
292
+ public reloadKeystore(newManager: KeystoreManager): void {
293
+ const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
294
+ this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
295
+ this.validationService = new ValidationService(this.keyStore, this.log.createChild('validation-service'));
296
+ }
297
+
270
298
  public async start() {
271
299
  if (this.epochCacheUpdateLoop.isRunning()) {
272
300
  this.log.warn(`Validator client already started`);
@@ -313,6 +341,16 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
313
341
  ): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
314
342
  this.p2pClient.registerCheckpointProposalHandler(checkpointHandler);
315
343
 
344
+ // Duplicate proposal handler - triggers slashing for equivocation
345
+ this.p2pClient.registerDuplicateProposalCallback((info: DuplicateProposalInfo) => {
346
+ this.handleDuplicateProposal(info);
347
+ });
348
+
349
+ // Duplicate attestation handler - triggers slashing for attestation equivocation
350
+ this.p2pClient.registerDuplicateAttestationCallback((info: DuplicateAttestationInfo) => {
351
+ this.handleDuplicateAttestation(info);
352
+ });
353
+
316
354
  const myAddresses = this.getValidatorAddresses();
317
355
  this.p2pClient.registerThisValidatorAddresses(myAddresses);
318
356
 
@@ -340,6 +378,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
340
378
  return false;
341
379
  }
342
380
 
381
+ // Ignore proposals from ourselves (may happen in HA setups)
382
+ if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
383
+ this.log.warn(`Ignoring block proposal from self for slot ${slotNumber}`, {
384
+ proposer: proposer.toString(),
385
+ slotNumber,
386
+ });
387
+ return false;
388
+ }
389
+
343
390
  // Check if we're in the committee (for metrics purposes)
344
391
  const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
345
392
  const partOfCommittee = inCommittee.length > 0;
@@ -413,10 +460,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
413
460
  return false;
414
461
  }
415
462
 
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
463
  return true;
421
464
  }
422
465
 
@@ -445,6 +488,23 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
445
488
  return undefined;
446
489
  }
447
490
 
491
+ // Ignore proposals from ourselves (may happen in HA setups)
492
+ if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
493
+ this.log.warn(`Ignoring block proposal from self for slot ${slotNumber}`, {
494
+ proposer: proposer.toString(),
495
+ slotNumber,
496
+ });
497
+ return undefined;
498
+ }
499
+
500
+ // Validate fee asset price modifier is within allowed range
501
+ if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
502
+ this.log.warn(
503
+ `Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${slotNumber}`,
504
+ );
505
+ return undefined;
506
+ }
507
+
448
508
  // Check that I have any address in current committee before attesting
449
509
  const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
450
510
  const partOfCommittee = inCommittee.length > 0;
@@ -453,25 +513,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
453
513
  slotNumber,
454
514
  archive: proposal.archive.toString(),
455
515
  proposer: proposer.toString(),
456
- txCount: proposal.txHashes.length,
457
516
  };
458
517
  this.log.info(`Received checkpoint proposal for slot ${slotNumber}`, {
459
518
  ...proposalInfo,
460
- txHashes: proposal.txHashes.map(t => t.toString()),
461
519
  fishermanMode: this.config.fishermanMode || false,
462
520
  });
463
521
 
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
522
  // 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);
523
+ if (this.config.skipCheckpointProposalValidation) {
524
+ this.log.warn(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
475
525
  } else {
476
526
  const validationResult = await this.validateCheckpointProposal(proposal, proposalInfo);
477
527
  if (!validationResult.isValid) {
@@ -501,6 +551,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
501
551
 
502
552
  this.metrics.incSuccessfulAttestations(inCommittee.length);
503
553
 
554
+ // Track epoch participation per attester: count each (attester, epoch) pair at most once
555
+ const proposalEpoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
556
+ for (const attester of inCommittee) {
557
+ const key = attester.toString();
558
+ const lastEpoch = this.lastAttestedEpochByAttester.get(key);
559
+ if (lastEpoch === undefined || proposalEpoch > lastEpoch) {
560
+ this.lastAttestedEpochByAttester.set(key, proposalEpoch);
561
+ this.metrics.incAttestedEpochCount(attester);
562
+ }
563
+ }
564
+
504
565
  // Determine which validators should attest
505
566
  let attestors: EthAddress[];
506
567
  if (partOfCommittee) {
@@ -526,15 +587,45 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
526
587
  return undefined;
527
588
  }
528
589
 
529
- return this.createCheckpointAttestationsFromProposal(proposal, attestors);
590
+ return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
591
+ }
592
+
593
+ /**
594
+ * Checks if we should attest to a slot based on equivocation prevention rules.
595
+ * @returns true if we should attest, false if we should skip
596
+ */
597
+ private shouldAttestToSlot(slotNumber: SlotNumber): boolean {
598
+ // If attestToEquivocatedProposals is true, always allow
599
+ if (this.config.attestToEquivocatedProposals) {
600
+ return true;
601
+ }
602
+
603
+ // Check if incoming slot is strictly greater than last attested
604
+ if (this.lastAttestedProposal && slotNumber <= this.lastAttestedProposal.slotNumber) {
605
+ this.log.warn(
606
+ `Refusing to process a proposal for slot ${slotNumber} given we already attested to a proposal for slot ${this.lastAttestedProposal.slotNumber}`,
607
+ );
608
+ return false;
609
+ }
610
+
611
+ return true;
530
612
  }
531
613
 
532
614
  private async createCheckpointAttestationsFromProposal(
533
615
  proposal: CheckpointProposalCore,
534
616
  attestors: EthAddress[] = [],
535
- ): Promise<CheckpointAttestation[]> {
617
+ ): Promise<CheckpointAttestation[] | undefined> {
618
+ // Equivocation check: must happen right before signing to minimize the race window
619
+ if (!this.shouldAttestToSlot(proposal.slotNumber)) {
620
+ return undefined;
621
+ }
622
+
536
623
  const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
537
- await this.p2pClient.addCheckpointAttestations(attestations);
624
+
625
+ // Track the proposal we attested to (to prevent equivocation)
626
+ this.lastAttestedProposal = proposal;
627
+
628
+ await this.p2pClient.addOwnCheckpointAttestations(attestations);
538
629
  return attestations;
539
630
  }
540
631
 
@@ -547,7 +638,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
547
638
  proposalInfo: LogData,
548
639
  ): Promise<{ isValid: true } | { isValid: false; reason: string }> {
549
640
  const slot = proposal.slotNumber;
550
- const timeoutSeconds = 10;
641
+
642
+ // Timeout block syncing at the start of the next slot
643
+ const config = this.checkpointsBuilder.getConfig();
644
+ const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
645
+ const timeoutSeconds = Math.max(1, nextSlotTimestampSeconds - Math.floor(this.dateProvider.now() / 1000));
551
646
 
552
647
  // Wait for last block to sync by archive
553
648
  let lastBlockHeader: BlockHeader | undefined;
@@ -582,6 +677,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
582
677
  return { isValid: false, reason: 'no_blocks_for_slot' };
583
678
  }
584
679
 
680
+ // Ensure the last block for this slot matches the archive in the checkpoint proposal
681
+ if (!blocks.at(-1)?.archive.root.equals(proposal.archive)) {
682
+ this.log.warn(`Last block archive mismatch for checkpoint proposal`, proposalInfo);
683
+ return { isValid: false, reason: 'last_block_archive_mismatch' };
684
+ }
685
+
585
686
  this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
586
687
  ...proposalInfo,
587
688
  blockNumbers: blocks.map(b => b.number),
@@ -595,14 +696,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
595
696
  // Get L1-to-L2 messages for this checkpoint
596
697
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
597
698
 
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.
699
+ // Collect the out hashes of all the checkpoints before this one in the same epoch
601
700
  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());
701
+ const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch))
702
+ .filter(c => c.checkpointNumber < checkpointNumber)
703
+ .map(c => c.checkpointOutHash);
606
704
 
607
705
  // Fork world state at the block before the first block
608
706
  const parentBlockNumber = BlockNumber(firstBlock.number - 1);
@@ -613,10 +711,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
613
711
  const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(
614
712
  checkpointNumber,
615
713
  constants,
714
+ proposal.feeAssetPriceModifier,
616
715
  l1ToL2Messages,
617
716
  previousCheckpointOutHashes,
618
717
  fork,
619
718
  blocks,
719
+ this.log.getBindings(),
620
720
  );
621
721
 
622
722
  // Complete the checkpoint to get computed values
@@ -642,18 +742,36 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
642
742
  return { isValid: false, reason: 'archive_mismatch' };
643
743
  }
644
744
 
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)) {
745
+ // Check that the accumulated epoch out hash matches the value in the proposal.
746
+ // The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
747
+ const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
748
+ const computedEpochOutHash = accumulateCheckpointOutHashes([...previousCheckpointOutHashes, checkpointOutHash]);
749
+ const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
750
+ if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
649
751
  this.log.warn(`Epoch out hash mismatch`, {
650
- proposalOutHash: proposalOutHash.toString(),
651
- computedOutHash: computedOutHash.toString(),
752
+ proposalEpochOutHash: proposalEpochOutHash.toString(),
753
+ computedEpochOutHash: computedEpochOutHash.toString(),
754
+ checkpointOutHash: checkpointOutHash.toString(),
755
+ previousCheckpointOutHashes: previousCheckpointOutHashes.map(h => h.toString()),
652
756
  ...proposalInfo,
653
757
  });
654
758
  return { isValid: false, reason: 'out_hash_mismatch' };
655
759
  }
656
760
 
761
+ // Final round of validations on the checkpoint, just in case.
762
+ try {
763
+ validateCheckpoint(computedCheckpoint, {
764
+ rollupManaLimit: this.checkpointsBuilder.getConfig().rollupManaLimit,
765
+ maxDABlockGas: this.config.validateMaxDABlockGas,
766
+ maxL2BlockGas: this.config.validateMaxL2BlockGas,
767
+ maxTxsPerBlock: this.config.validateMaxTxsPerBlock,
768
+ maxTxsPerCheckpoint: this.config.validateMaxTxsPerCheckpoint,
769
+ });
770
+ } catch (err) {
771
+ this.log.warn(`Checkpoint validation failed: ${err}`, proposalInfo);
772
+ return { isValid: false, reason: 'checkpoint_validation_failed' };
773
+ }
774
+
657
775
  this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
658
776
  return { isValid: true };
659
777
  } finally {
@@ -664,12 +782,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
664
782
  /**
665
783
  * Extract checkpoint global variables from a block.
666
784
  */
667
- private extractCheckpointConstants(block: L2BlockNew): CheckpointGlobalVariables {
785
+ private extractCheckpointConstants(block: L2Block): CheckpointGlobalVariables {
668
786
  const gv = block.header.globalVariables;
669
787
  return {
670
788
  chainId: gv.chainId,
671
789
  version: gv.version,
672
790
  slotNumber: gv.slotNumber,
791
+ timestamp: gv.timestamp,
673
792
  coinbase: gv.coinbase,
674
793
  feeRecipient: gv.feeRecipient,
675
794
  gasFees: gv.gasFees,
@@ -679,7 +798,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
679
798
  /**
680
799
  * Uploads blobs for a checkpoint to the filestore (fire and forget).
681
800
  */
682
- private async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
801
+ protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
683
802
  try {
684
803
  const lastBlockHeader = await this.blockSource.getBlockHeaderByArchive(proposal.archive);
685
804
  if (!lastBlockHeader) {
@@ -694,7 +813,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
694
813
  }
695
814
 
696
815
  const blobFields = blocks.flatMap(b => b.toBlobFields());
697
- const blobs: Blob[] = getBlobsPerL1Block(blobFields);
816
+ const blobs: Blob[] = await getBlobsPerL1Block(blobFields);
698
817
  await this.blobClient.sendBlobsToFilestore(blobs);
699
818
  this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
700
819
  ...proposalInfo,
@@ -732,6 +851,52 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
732
851
  ]);
733
852
  }
734
853
 
854
+ /**
855
+ * Handle detection of a duplicate proposal (equivocation).
856
+ * Emits a slash event when a proposer sends multiple proposals for the same position.
857
+ */
858
+ private handleDuplicateProposal(info: DuplicateProposalInfo): void {
859
+ const { slot, proposer, type } = info;
860
+
861
+ this.log.warn(`Triggering slash event for duplicate ${type} proposal from ${proposer.toString()} at slot ${slot}`, {
862
+ proposer: proposer.toString(),
863
+ slot,
864
+ type,
865
+ });
866
+
867
+ // Emit slash event
868
+ this.emit(WANT_TO_SLASH_EVENT, [
869
+ {
870
+ validator: proposer,
871
+ amount: this.config.slashDuplicateProposalPenalty,
872
+ offenseType: OffenseType.DUPLICATE_PROPOSAL,
873
+ epochOrSlot: BigInt(slot),
874
+ },
875
+ ]);
876
+ }
877
+
878
+ /**
879
+ * Handle detection of a duplicate attestation (equivocation).
880
+ * Emits a slash event when an attester signs attestations for different proposals at the same slot.
881
+ */
882
+ private handleDuplicateAttestation(info: DuplicateAttestationInfo): void {
883
+ const { slot, attester } = info;
884
+
885
+ this.log.warn(`Triggering slash event for duplicate attestation from ${attester.toString()} at slot ${slot}`, {
886
+ attester: attester.toString(),
887
+ slot,
888
+ });
889
+
890
+ this.emit(WANT_TO_SLASH_EVENT, [
891
+ {
892
+ validator: attester,
893
+ amount: this.config.slashDuplicateAttestationPenalty,
894
+ offenseType: OffenseType.DUPLICATE_ATTESTATION,
895
+ epochOrSlot: BigInt(slot),
896
+ },
897
+ ]);
898
+ }
899
+
735
900
  async createBlockProposal(
736
901
  blockHeader: BlockHeader,
737
902
  indexWithinCheckpoint: IndexWithinCheckpoint,
@@ -739,13 +904,21 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
739
904
  archive: Fr,
740
905
  txs: Tx[],
741
906
  proposerAddress: EthAddress | undefined,
742
- options: BlockProposalOptions,
907
+ options: BlockProposalOptions = {},
743
908
  ): 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
- // }
909
+ // Validate that we're not creating a proposal for an older or equal position
910
+ if (this.lastProposedBlock) {
911
+ const lastSlot = this.lastProposedBlock.slotNumber;
912
+ const lastIndex = this.lastProposedBlock.indexWithinCheckpoint;
913
+ const newSlot = blockHeader.globalVariables.slotNumber;
914
+
915
+ if (newSlot < lastSlot || (newSlot === lastSlot && indexWithinCheckpoint <= lastIndex)) {
916
+ throw new Error(
917
+ `Cannot create block proposal for slot ${newSlot} index ${indexWithinCheckpoint}: ` +
918
+ `already proposed block for slot ${lastSlot} index ${lastIndex}`,
919
+ );
920
+ }
921
+ }
749
922
 
750
923
  this.log.info(
751
924
  `Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`,
@@ -762,25 +935,42 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
762
935
  broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
763
936
  },
764
937
  );
765
- this.previousProposal = newProposal;
938
+ this.lastProposedBlock = newProposal;
766
939
  return newProposal;
767
940
  }
768
941
 
769
942
  async createCheckpointProposal(
770
943
  checkpointHeader: CheckpointHeader,
771
944
  archive: Fr,
945
+ feeAssetPriceModifier: bigint,
772
946
  lastBlockInfo: CreateCheckpointProposalLastBlockData | undefined,
773
947
  proposerAddress: EthAddress | undefined,
774
- options: CheckpointProposalOptions,
948
+ options: CheckpointProposalOptions = {},
775
949
  ): Promise<CheckpointProposal> {
950
+ // Validate that we're not creating a proposal for an older or equal slot
951
+ if (this.lastProposedCheckpoint) {
952
+ const lastSlot = this.lastProposedCheckpoint.slotNumber;
953
+ const newSlot = checkpointHeader.slotNumber;
954
+
955
+ if (newSlot <= lastSlot) {
956
+ throw new Error(
957
+ `Cannot create checkpoint proposal for slot ${newSlot}: ` +
958
+ `already proposed checkpoint for slot ${lastSlot}`,
959
+ );
960
+ }
961
+ }
962
+
776
963
  this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
777
- return await this.validationService.createCheckpointProposal(
964
+ const newProposal = await this.validationService.createCheckpointProposal(
778
965
  checkpointHeader,
779
966
  archive,
967
+ feeAssetPriceModifier,
780
968
  lastBlockInfo,
781
969
  proposerAddress,
782
970
  options,
783
971
  );
972
+ this.lastProposedCheckpoint = newProposal;
973
+ return newProposal;
784
974
  }
785
975
 
786
976
  async broadcastBlockProposal(proposal: BlockProposal): Promise<void> {
@@ -802,6 +992,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
802
992
  this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
803
993
  const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
804
994
 
995
+ if (!attestations) {
996
+ return [];
997
+ }
998
+
805
999
  // We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
806
1000
  // other nodes can see that our validators did attest to this block proposal, and do not slash us
807
1001
  // 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
- }