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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +51 -24
  2. package/dest/block_proposal_handler.d.ts +8 -8
  3. package/dest/block_proposal_handler.d.ts.map +1 -1
  4. package/dest/block_proposal_handler.js +27 -32
  5. package/dest/checkpoint_builder.d.ts +21 -25
  6. package/dest/checkpoint_builder.d.ts.map +1 -1
  7. package/dest/checkpoint_builder.js +50 -32
  8. package/dest/config.d.ts +1 -1
  9. package/dest/config.d.ts.map +1 -1
  10. package/dest/config.js +8 -14
  11. package/dest/duties/validation_service.d.ts +19 -6
  12. package/dest/duties/validation_service.d.ts.map +1 -1
  13. package/dest/duties/validation_service.js +72 -19
  14. package/dest/factory.d.ts +2 -2
  15. package/dest/factory.d.ts.map +1 -1
  16. package/dest/factory.js +1 -1
  17. package/dest/key_store/ha_key_store.d.ts +99 -0
  18. package/dest/key_store/ha_key_store.d.ts.map +1 -0
  19. package/dest/key_store/ha_key_store.js +208 -0
  20. package/dest/key_store/index.d.ts +2 -1
  21. package/dest/key_store/index.d.ts.map +1 -1
  22. package/dest/key_store/index.js +1 -0
  23. package/dest/key_store/interface.d.ts +36 -6
  24. package/dest/key_store/interface.d.ts.map +1 -1
  25. package/dest/key_store/local_key_store.d.ts +10 -5
  26. package/dest/key_store/local_key_store.d.ts.map +1 -1
  27. package/dest/key_store/local_key_store.js +8 -4
  28. package/dest/key_store/node_keystore_adapter.d.ts +18 -5
  29. package/dest/key_store/node_keystore_adapter.d.ts.map +1 -1
  30. package/dest/key_store/node_keystore_adapter.js +18 -4
  31. package/dest/key_store/web3signer_key_store.d.ts +10 -5
  32. package/dest/key_store/web3signer_key_store.d.ts.map +1 -1
  33. package/dest/key_store/web3signer_key_store.js +8 -4
  34. package/dest/metrics.d.ts +4 -3
  35. package/dest/metrics.d.ts.map +1 -1
  36. package/dest/metrics.js +34 -5
  37. package/dest/tx_validator/tx_validator_factory.d.ts +4 -3
  38. package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
  39. package/dest/tx_validator/tx_validator_factory.js +17 -16
  40. package/dest/validator.d.ts +18 -13
  41. package/dest/validator.d.ts.map +1 -1
  42. package/dest/validator.js +107 -81
  43. package/package.json +21 -17
  44. package/src/block_proposal_handler.ts +41 -42
  45. package/src/checkpoint_builder.ts +85 -38
  46. package/src/config.ts +7 -13
  47. package/src/duties/validation_service.ts +91 -23
  48. package/src/factory.ts +1 -0
  49. package/src/key_store/ha_key_store.ts +269 -0
  50. package/src/key_store/index.ts +1 -0
  51. package/src/key_store/interface.ts +44 -5
  52. package/src/key_store/local_key_store.ts +13 -4
  53. package/src/key_store/node_keystore_adapter.ts +27 -4
  54. package/src/key_store/web3signer_key_store.ts +17 -4
  55. package/src/metrics.ts +45 -6
  56. package/src/tx_validator/tx_validator_factory.ts +52 -31
  57. package/src/validator.ts +128 -94
@@ -1,5 +1,6 @@
1
1
  import { BlockNumber } from '@aztec/foundation/branded-types';
2
2
  import { Fr } from '@aztec/foundation/curves/bn254';
3
+ import type { LoggerBindings } from '@aztec/foundation/log';
3
4
  import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree';
4
5
  import {
5
6
  AggregateTxValidator,
@@ -10,6 +11,7 @@ import {
10
11
  GasTxValidator,
11
12
  MetadataTxValidator,
12
13
  PhasesTxValidator,
14
+ SizeTxValidator,
13
15
  TimestampTxValidator,
14
16
  TxPermittedValidator,
15
17
  TxProofValidator,
@@ -52,31 +54,41 @@ export function createValidatorForAcceptingTxs(
52
54
  blockNumber: BlockNumber;
53
55
  txsPermitted: boolean;
54
56
  },
57
+ bindings?: LoggerBindings,
55
58
  ): TxValidator<Tx> {
56
59
  const validators: TxValidator<Tx>[] = [
57
- new TxPermittedValidator(txsPermitted),
58
- new DataTxValidator(),
59
- new MetadataTxValidator({
60
- l1ChainId: new Fr(l1ChainId),
61
- rollupVersion: new Fr(rollupVersion),
62
- protocolContractsHash,
63
- vkTreeRoot: getVKTreeRoot(),
64
- }),
65
- new TimestampTxValidator({
66
- timestamp,
67
- blockNumber,
68
- }),
69
- new DoubleSpendTxValidator(new NullifierCache(db)),
70
- new PhasesTxValidator(contractDataSource, setupAllowList, timestamp),
71
- new BlockHeaderTxValidator(new ArchiveCache(db)),
60
+ new TxPermittedValidator(txsPermitted, bindings),
61
+ new SizeTxValidator(bindings),
62
+ new DataTxValidator(bindings),
63
+ new MetadataTxValidator(
64
+ {
65
+ l1ChainId: new Fr(l1ChainId),
66
+ rollupVersion: new Fr(rollupVersion),
67
+ protocolContractsHash,
68
+ vkTreeRoot: getVKTreeRoot(),
69
+ },
70
+ bindings,
71
+ ),
72
+ new TimestampTxValidator(
73
+ {
74
+ timestamp,
75
+ blockNumber,
76
+ },
77
+ bindings,
78
+ ),
79
+ new DoubleSpendTxValidator(new NullifierCache(db), bindings),
80
+ new PhasesTxValidator(contractDataSource, setupAllowList, timestamp, bindings),
81
+ new BlockHeaderTxValidator(new ArchiveCache(db), bindings),
72
82
  ];
73
83
 
74
84
  if (!skipFeeEnforcement) {
75
- validators.push(new GasTxValidator(new DatabasePublicStateSource(db), ProtocolContractAddress.FeeJuice, gasFees));
85
+ validators.push(
86
+ new GasTxValidator(new DatabasePublicStateSource(db), ProtocolContractAddress.FeeJuice, gasFees, bindings),
87
+ );
76
88
  }
77
89
 
78
90
  if (verifier) {
79
- validators.push(new TxProofValidator(verifier));
91
+ validators.push(new TxProofValidator(verifier, bindings));
80
92
  }
81
93
 
82
94
  return new AggregateTxValidator(...validators);
@@ -87,6 +99,7 @@ export function createValidatorForBlockBuilding(
87
99
  contractDataSource: ContractDataSource,
88
100
  globalVariables: GlobalVariables,
89
101
  setupAllowList: AllowedElement[],
102
+ bindings?: LoggerBindings,
90
103
  ): PublicProcessorValidator {
91
104
  const nullifierCache = new NullifierCache(db);
92
105
  const archiveCache = new ArchiveCache(db);
@@ -100,6 +113,7 @@ export function createValidatorForBlockBuilding(
100
113
  contractDataSource,
101
114
  globalVariables,
102
115
  setupAllowList,
116
+ bindings,
103
117
  ),
104
118
  nullifierCache,
105
119
  };
@@ -112,22 +126,29 @@ function preprocessValidator(
112
126
  contractDataSource: ContractDataSource,
113
127
  globalVariables: GlobalVariables,
114
128
  setupAllowList: AllowedElement[],
129
+ bindings?: LoggerBindings,
115
130
  ): TxValidator<Tx> {
116
131
  // We don't include the TxProofValidator nor the DataTxValidator here because they are already checked by the time we get to block building.
117
132
  return new AggregateTxValidator(
118
- new MetadataTxValidator({
119
- l1ChainId: globalVariables.chainId,
120
- rollupVersion: globalVariables.version,
121
- protocolContractsHash,
122
- vkTreeRoot: getVKTreeRoot(),
123
- }),
124
- new TimestampTxValidator({
125
- timestamp: globalVariables.timestamp,
126
- blockNumber: globalVariables.blockNumber,
127
- }),
128
- new DoubleSpendTxValidator(nullifierCache),
129
- new PhasesTxValidator(contractDataSource, setupAllowList, globalVariables.timestamp),
130
- new GasTxValidator(publicStateSource, ProtocolContractAddress.FeeJuice, globalVariables.gasFees),
131
- new BlockHeaderTxValidator(archiveCache),
133
+ new MetadataTxValidator(
134
+ {
135
+ l1ChainId: globalVariables.chainId,
136
+ rollupVersion: globalVariables.version,
137
+ protocolContractsHash,
138
+ vkTreeRoot: getVKTreeRoot(),
139
+ },
140
+ bindings,
141
+ ),
142
+ new TimestampTxValidator(
143
+ {
144
+ timestamp: globalVariables.timestamp,
145
+ blockNumber: globalVariables.blockNumber,
146
+ },
147
+ bindings,
148
+ ),
149
+ new DoubleSpendTxValidator(nullifierCache, bindings),
150
+ new PhasesTxValidator(contractDataSource, setupAllowList, globalVariables.timestamp, bindings),
151
+ new GasTxValidator(publicStateSource, ProtocolContractAddress.FeeJuice, globalVariables.gasFees, bindings),
152
+ new BlockHeaderTxValidator(archiveCache, bindings),
132
153
  );
133
154
  }
package/src/validator.ts CHANGED
@@ -1,7 +1,13 @@
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 { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
4
+ import {
5
+ BlockNumber,
6
+ CheckpointNumber,
7
+ EpochNumber,
8
+ IndexWithinCheckpoint,
9
+ SlotNumber,
10
+ } from '@aztec/foundation/branded-types';
5
11
  import { Fr } from '@aztec/foundation/curves/bn254';
6
12
  import { TimeoutError } from '@aztec/foundation/error';
7
13
  import type { EthAddress } from '@aztec/foundation/eth-address';
@@ -12,18 +18,20 @@ import { RunningPromise } from '@aztec/foundation/running-promise';
12
18
  import { sleep } from '@aztec/foundation/sleep';
13
19
  import { DateProvider } from '@aztec/foundation/timer';
14
20
  import type { KeystoreManager } from '@aztec/node-keystore';
15
- import type { P2P, PeerId, TxProvider } from '@aztec/p2p';
21
+ import type { DuplicateProposalInfo, P2P, PeerId } from '@aztec/p2p';
16
22
  import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
17
23
  import { OffenseType, WANT_TO_SLASH_EVENT, type Watcher, type WatcherEmitter } from '@aztec/slasher';
18
24
  import type { AztecAddress } from '@aztec/stdlib/aztec-address';
19
- import type { CommitteeAttestationsAndSigners, L2BlockNew, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
25
+ import type { CommitteeAttestationsAndSigners, L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
26
+ import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
20
27
  import type {
21
28
  CreateCheckpointProposalLastBlockData,
29
+ ITxProvider,
22
30
  Validator,
23
31
  ValidatorClientFullConfig,
24
32
  WorldStateSynchronizer,
25
33
  } from '@aztec/stdlib/interfaces/server';
26
- import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
34
+ import { type L1ToL2MessageSource, accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
27
35
  import type {
28
36
  BlockProposal,
29
37
  BlockProposalOptions,
@@ -36,6 +44,8 @@ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
36
44
  import type { BlockHeader, CheckpointGlobalVariables, Tx } from '@aztec/stdlib/tx';
37
45
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
38
46
  import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
47
+ import { createHASigner } from '@aztec/validator-ha-signer/factory';
48
+ import { DutyType, type SigningContext } from '@aztec/validator-ha-signer/types';
39
49
 
40
50
  import { EventEmitter } from 'events';
41
51
  import type { TypedDataDefinition } from 'viem';
@@ -43,6 +53,8 @@ import type { TypedDataDefinition } from 'viem';
43
53
  import { BlockProposalHandler, type BlockProposalValidationFailureReason } from './block_proposal_handler.js';
44
54
  import type { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
45
55
  import { ValidationService } from './duties/validation_service.js';
56
+ import { HAKeyStore } from './key_store/ha_key_store.js';
57
+ import type { ExtendedValidatorKeyStore } from './key_store/interface.js';
46
58
  import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
47
59
  import { ValidatorMetrics } from './metrics.js';
48
60
 
@@ -76,13 +88,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
76
88
 
77
89
  private proposersOfInvalidBlocks: Set<string> = new Set();
78
90
 
79
- // TODO(palla/mbps): Remove this once checkpoint validation is stable and we can validate all blocks properly.
80
- // Tracks slots for which we have successfully validated a block proposal, so we can attest to checkpoint proposals for those slots.
81
- // eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
82
- private validatedBlockSlots: Set<SlotNumber> = new Set();
83
-
84
91
  protected constructor(
85
- private keyStore: NodeKeystoreAdapter,
92
+ private keyStore: ExtendedValidatorKeyStore,
86
93
  private epochCache: EpochCache,
87
94
  private p2pClient: P2P,
88
95
  private blockProposalHandler: BlockProposalHandler,
@@ -165,7 +172,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
165
172
  }
166
173
  }
167
174
 
168
- static new(
175
+ static async new(
169
176
  config: ValidatorClientFullConfig,
170
177
  checkpointsBuilder: FullNodeCheckpointsBuilder,
171
178
  worldState: WorldStateSynchronizer,
@@ -173,7 +180,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
173
180
  p2pClient: P2P,
174
181
  blockSource: L2BlockSource & L2BlockSink,
175
182
  l1ToL2MessageSource: L1ToL2MessageSource,
176
- txProvider: TxProvider,
183
+ txProvider: ITxProvider,
177
184
  keyStoreManager: KeystoreManager,
178
185
  blobClient: BlobClientInterface,
179
186
  dateProvider: DateProvider = new DateProvider(),
@@ -190,14 +197,26 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
190
197
  l1ToL2MessageSource,
191
198
  txProvider,
192
199
  blockProposalValidator,
200
+ epochCache,
193
201
  config,
194
202
  metrics,
195
203
  dateProvider,
196
204
  telemetry,
197
205
  );
198
206
 
207
+ let validatorKeyStore: ExtendedValidatorKeyStore = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
208
+ if (config.haSigningEnabled) {
209
+ // If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
210
+ const haConfig = {
211
+ ...config,
212
+ maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000,
213
+ };
214
+ const { signer } = await createHASigner(haConfig);
215
+ validatorKeyStore = new HAKeyStore(validatorKeyStore, signer);
216
+ }
217
+
199
218
  const validator = new ValidatorClient(
200
- NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager),
219
+ validatorKeyStore,
201
220
  epochCache,
202
221
  p2pClient,
203
222
  blockProposalHandler,
@@ -224,8 +243,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
224
243
  return this.blockProposalHandler;
225
244
  }
226
245
 
227
- public signWithAddress(addr: EthAddress, msg: TypedDataDefinition) {
228
- return this.keyStore.signTypedDataWithAddress(addr, msg);
246
+ public signWithAddress(addr: EthAddress, msg: TypedDataDefinition, context: SigningContext) {
247
+ return this.keyStore.signTypedDataWithAddress(addr, msg, context);
229
248
  }
230
249
 
231
250
  public getCoinbaseForAttestor(attestor: EthAddress): EthAddress {
@@ -250,6 +269,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
250
269
  return;
251
270
  }
252
271
 
272
+ await this.keyStore.start();
273
+
253
274
  await this.registerHandlers();
254
275
 
255
276
  const myAddresses = this.getValidatorAddresses();
@@ -265,6 +286,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
265
286
 
266
287
  public async stop() {
267
288
  await this.epochCacheUpdateLoop.stop();
289
+ await this.keyStore.stop();
268
290
  }
269
291
 
270
292
  /** Register handlers on the p2p client */
@@ -287,6 +309,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
287
309
  ): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
288
310
  this.p2pClient.registerCheckpointProposalHandler(checkpointHandler);
289
311
 
312
+ // Duplicate proposal handler - triggers slashing for equivocation
313
+ this.p2pClient.registerDuplicateProposalCallback((info: DuplicateProposalInfo) => {
314
+ this.handleDuplicateProposal(info);
315
+ });
316
+
290
317
  const myAddresses = this.getValidatorAddresses();
291
318
  this.p2pClient.registerThisValidatorAddresses(myAddresses);
292
319
 
@@ -301,6 +328,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
301
328
  */
302
329
  async validateBlockProposal(proposal: BlockProposal, proposalSender: PeerId): Promise<boolean> {
303
330
  const slotNumber = proposal.slotNumber;
331
+
332
+ // Note: During escape hatch, we still want to "validate" proposals for observability,
333
+ // but we intentionally reject them and disable slashing invalid block and attestation flow.
334
+ const escapeHatchOpen = await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber);
335
+
304
336
  const proposer = proposal.getSender();
305
337
 
306
338
  // Reject proposals with invalid signatures
@@ -334,7 +366,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
334
366
  const validationResult = await this.blockProposalHandler.handleBlockProposal(
335
367
  proposal,
336
368
  proposalSender,
337
- !!shouldReexecute,
369
+ !!shouldReexecute && !escapeHatchOpen,
338
370
  );
339
371
 
340
372
  if (!validationResult.isValid) {
@@ -359,6 +391,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
359
391
 
360
392
  // Slash invalid block proposals (can happen even when not in committee)
361
393
  if (
394
+ !escapeHatchOpen &&
362
395
  validationResult.reason &&
363
396
  SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) &&
364
397
  slashBroadcastedInvalidBlockPenalty > 0n
@@ -373,11 +406,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
373
406
  ...proposalInfo,
374
407
  inCommittee: partOfCommittee,
375
408
  fishermanMode: this.config.fishermanMode || false,
409
+ escapeHatchOpen,
376
410
  });
377
411
 
378
- // TODO(palla/mbps): Remove this once checkpoint validation is stable.
379
- // Track that we successfully validated a block for this slot, so we can attest to checkpoint proposals for it.
380
- this.validatedBlockSlots.add(slotNumber);
412
+ if (escapeHatchOpen) {
413
+ this.log.warn(`Escape hatch open for slot ${slotNumber}, rejecting block proposal`, proposalInfo);
414
+ return false;
415
+ }
381
416
 
382
417
  return true;
383
418
  }
@@ -395,6 +430,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
395
430
  const slotNumber = proposal.slotNumber;
396
431
  const proposer = proposal.getSender();
397
432
 
433
+ // If escape hatch is open for this slot's epoch, do not attest.
434
+ if (await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber)) {
435
+ this.log.warn(`Escape hatch open for slot ${slotNumber}, skipping checkpoint attestation handling`);
436
+ return undefined;
437
+ }
438
+
398
439
  // Reject proposals with invalid signatures
399
440
  if (!proposer) {
400
441
  this.log.warn(`Received checkpoint proposal with invalid signature for slot ${slotNumber}`);
@@ -417,17 +458,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
417
458
  fishermanMode: this.config.fishermanMode || false,
418
459
  });
419
460
 
420
- // TODO(palla/mbps): Remove this once checkpoint validation is stable.
421
- // Check that we have successfully validated a block for this slot before attesting to the checkpoint.
422
- if (!this.validatedBlockSlots.has(slotNumber)) {
423
- this.log.warn(`No validated block found for slot ${slotNumber}, refusing to attest to checkpoint`, proposalInfo);
424
- return undefined;
425
- }
426
-
427
461
  // Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
428
- // TODO(palla/mbps): Change default to false once checkpoint validation is stable.
429
- if (this.config.skipCheckpointProposalValidation !== false) {
430
- this.log.verbose(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
462
+ if (this.config.skipCheckpointProposalValidation) {
463
+ this.log.warn(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
431
464
  } else {
432
465
  const validationResult = await this.validateCheckpointProposal(proposal, proposalInfo);
433
466
  if (!validationResult.isValid) {
@@ -490,7 +523,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
490
523
  attestors: EthAddress[] = [],
491
524
  ): Promise<CheckpointAttestation[]> {
492
525
  const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
493
- await this.p2pClient.addCheckpointAttestations(attestations);
526
+ await this.p2pClient.addOwnCheckpointAttestations(attestations);
494
527
  return attestations;
495
528
  }
496
529
 
@@ -503,7 +536,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
503
536
  proposalInfo: LogData,
504
537
  ): Promise<{ isValid: true } | { isValid: false; reason: string }> {
505
538
  const slot = proposal.slotNumber;
506
- const timeoutSeconds = 10;
539
+ const timeoutSeconds = 10; // TODO(palla/mbps): This should map to the timetable settings
507
540
 
508
541
  // Wait for last block to sync by archive
509
542
  let lastBlockHeader: BlockHeader | undefined;
@@ -531,16 +564,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
531
564
  return { isValid: false, reason: 'last_block_not_found' };
532
565
  }
533
566
 
534
- // Get the last full block to determine checkpoint number
535
- const lastBlock = await this.blockSource.getL2BlockNew(lastBlockHeader.getBlockNumber());
536
- if (!lastBlock) {
537
- this.log.warn(`Last block ${lastBlockHeader.getBlockNumber()} not found`, proposalInfo);
538
- return { isValid: false, reason: 'last_block_not_found' };
539
- }
540
- const checkpointNumber = lastBlock.checkpointNumber;
541
-
542
567
  // Get all full blocks for the slot and checkpoint
543
- const blocks = await this.getBlocksForSlot(slot, lastBlockHeader, checkpointNumber);
568
+ const blocks = await this.blockSource.getBlocksForSlot(slot);
544
569
  if (blocks.length === 0) {
545
570
  this.log.warn(`No blocks found for slot ${slot}`, proposalInfo);
546
571
  return { isValid: false, reason: 'no_blocks_for_slot' };
@@ -554,10 +579,20 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
554
579
  // Get checkpoint constants from first block
555
580
  const firstBlock = blocks[0];
556
581
  const constants = this.extractCheckpointConstants(firstBlock);
582
+ const checkpointNumber = firstBlock.checkpointNumber;
557
583
 
558
584
  // Get L1-to-L2 messages for this checkpoint
559
585
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
560
586
 
587
+ // Compute the previous checkpoint out hashes for the epoch.
588
+ // TODO: There can be a more efficient way to get the previous checkpoint out hashes without having to fetch the
589
+ // actual checkpoints and the blocks/txs in them.
590
+ const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
591
+ const previousCheckpoints = (await this.blockSource.getCheckpointsForEpoch(epoch))
592
+ .filter(b => b.number < checkpointNumber)
593
+ .sort((a, b) => a.number - b.number);
594
+ const previousCheckpointOutHashes = previousCheckpoints.map(c => c.getCheckpointOutHash());
595
+
561
596
  // Fork world state at the block before the first block
562
597
  const parentBlockNumber = BlockNumber(firstBlock.number - 1);
563
598
  const fork = await this.worldState.fork(parentBlockNumber);
@@ -568,8 +603,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
568
603
  checkpointNumber,
569
604
  constants,
570
605
  l1ToL2Messages,
606
+ previousCheckpointOutHashes,
571
607
  fork,
572
608
  blocks,
609
+ this.log.getBindings(),
573
610
  );
574
611
 
575
612
  // Complete the checkpoint to get computed values
@@ -595,6 +632,22 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
595
632
  return { isValid: false, reason: 'archive_mismatch' };
596
633
  }
597
634
 
635
+ // Check that the accumulated epoch out hash matches the value in the proposal.
636
+ // The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
637
+ const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
638
+ const computedEpochOutHash = accumulateCheckpointOutHashes([...previousCheckpointOutHashes, checkpointOutHash]);
639
+ const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
640
+ if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
641
+ this.log.warn(`Epoch out hash mismatch`, {
642
+ proposalEpochOutHash: proposalEpochOutHash.toString(),
643
+ computedEpochOutHash: computedEpochOutHash.toString(),
644
+ checkpointOutHash: checkpointOutHash.toString(),
645
+ previousCheckpointOutHashes: previousCheckpointOutHashes.map(h => h.toString()),
646
+ ...proposalInfo,
647
+ });
648
+ return { isValid: false, reason: 'out_hash_mismatch' };
649
+ }
650
+
598
651
  this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
599
652
  return { isValid: true };
600
653
  } finally {
@@ -602,50 +655,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
602
655
  }
603
656
  }
604
657
 
605
- /**
606
- * Get all full blocks for a given slot and checkpoint by walking backwards from the last block.
607
- * Returns blocks in ascending order (earliest to latest).
608
- * TODO(palla/mbps): Add getL2BlocksForSlot() to L2BlockSource interface for efficiency.
609
- */
610
- private async getBlocksForSlot(
611
- slot: SlotNumber,
612
- lastBlockHeader: BlockHeader,
613
- checkpointNumber: CheckpointNumber,
614
- ): Promise<L2BlockNew[]> {
615
- const blocks: L2BlockNew[] = [];
616
- let currentHeader = lastBlockHeader;
617
- const { genesisArchiveRoot } = await this.blockSource.getGenesisValues();
618
-
619
- while (currentHeader.getSlot() === slot) {
620
- const block = await this.blockSource.getL2BlockNew(currentHeader.getBlockNumber());
621
- if (!block) {
622
- this.log.warn(`Block ${currentHeader.getBlockNumber()} not found while getting blocks for slot ${slot}`);
623
- break;
624
- }
625
- if (block.checkpointNumber !== checkpointNumber) {
626
- break;
627
- }
628
- blocks.unshift(block);
629
-
630
- const prevArchive = currentHeader.lastArchive.root;
631
- if (prevArchive.equals(genesisArchiveRoot)) {
632
- break;
633
- }
634
-
635
- const prevHeader = await this.blockSource.getBlockHeaderByArchive(prevArchive);
636
- if (!prevHeader || prevHeader.getSlot() !== slot) {
637
- break;
638
- }
639
- currentHeader = prevHeader;
640
- }
641
-
642
- return blocks;
643
- }
644
-
645
658
  /**
646
659
  * Extract checkpoint global variables from a block.
647
660
  */
648
- private extractCheckpointConstants(block: L2BlockNew): CheckpointGlobalVariables {
661
+ private extractCheckpointConstants(block: L2Block): CheckpointGlobalVariables {
649
662
  const gv = block.header.globalVariables;
650
663
  return {
651
664
  chainId: gv.chainId,
@@ -668,14 +681,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
668
681
  return;
669
682
  }
670
683
 
671
- // Get the last full block to determine checkpoint number
672
- const lastBlock = await this.blockSource.getL2BlockNew(lastBlockHeader.getBlockNumber());
673
- if (!lastBlock) {
674
- this.log.warn(`Failed to get last block for blob upload`, proposalInfo);
675
- return;
676
- }
677
-
678
- const blocks = await this.getBlocksForSlot(proposal.slotNumber, lastBlockHeader, lastBlock.checkpointNumber);
684
+ const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
679
685
  if (blocks.length === 0) {
680
686
  this.log.warn(`No blocks found for blob upload`, proposalInfo);
681
687
  return;
@@ -720,14 +726,38 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
720
726
  ]);
721
727
  }
722
728
 
729
+ /**
730
+ * Handle detection of a duplicate proposal (equivocation).
731
+ * Emits a slash event when a proposer sends multiple proposals for the same position.
732
+ */
733
+ private handleDuplicateProposal(info: DuplicateProposalInfo): void {
734
+ const { slot, proposer, type } = info;
735
+
736
+ this.log.warn(`Triggering slash event for duplicate ${type} proposal from ${proposer.toString()} at slot ${slot}`, {
737
+ proposer: proposer.toString(),
738
+ slot,
739
+ type,
740
+ });
741
+
742
+ // Emit slash event
743
+ this.emit(WANT_TO_SLASH_EVENT, [
744
+ {
745
+ validator: proposer,
746
+ amount: this.config.slashDuplicateProposalPenalty,
747
+ offenseType: OffenseType.DUPLICATE_PROPOSAL,
748
+ epochOrSlot: BigInt(slot),
749
+ },
750
+ ]);
751
+ }
752
+
723
753
  async createBlockProposal(
724
754
  blockHeader: BlockHeader,
725
- indexWithinCheckpoint: number,
755
+ indexWithinCheckpoint: IndexWithinCheckpoint,
726
756
  inHash: Fr,
727
757
  archive: Fr,
728
758
  txs: Tx[],
729
759
  proposerAddress: EthAddress | undefined,
730
- options: BlockProposalOptions,
760
+ options: BlockProposalOptions = {},
731
761
  ): Promise<BlockProposal> {
732
762
  // TODO(palla/mbps): Prevent double proposals properly
733
763
  // if (this.previousProposal?.slotNumber === blockHeader.globalVariables.slotNumber) {
@@ -759,7 +789,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
759
789
  archive: Fr,
760
790
  lastBlockInfo: CreateCheckpointProposalLastBlockData | undefined,
761
791
  proposerAddress: EthAddress | undefined,
762
- options: CheckpointProposalOptions,
792
+ options: CheckpointProposalOptions = {},
763
793
  ): Promise<CheckpointProposal> {
764
794
  this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
765
795
  return await this.validationService.createCheckpointProposal(
@@ -778,8 +808,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
778
808
  async signAttestationsAndSigners(
779
809
  attestationsAndSigners: CommitteeAttestationsAndSigners,
780
810
  proposer: EthAddress,
811
+ slot: SlotNumber,
812
+ blockNumber: BlockNumber | CheckpointNumber,
781
813
  ): Promise<Signature> {
782
- return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer);
814
+ return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer, slot, blockNumber);
783
815
  }
784
816
 
785
817
  async collectOwnAttestations(proposal: CheckpointProposal): Promise<CheckpointAttestation[]> {
@@ -886,7 +918,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
886
918
  }
887
919
 
888
920
  const payloadToSign = authRequest.getPayloadToSign();
889
- const signature = await this.keyStore.signMessageWithAddress(addressToUse, payloadToSign);
921
+ // AUTH_REQUEST doesn't require HA protection - multiple signatures are safe
922
+ const context: SigningContext = { dutyType: DutyType.AUTH_REQUEST };
923
+ const signature = await this.keyStore.signMessageWithAddress(addressToUse, payloadToSign, context);
890
924
  const authResponse = new AuthResponse(statusMessage, signature);
891
925
  return authResponse.toBuffer();
892
926
  }