@aztec/validator-client 0.0.1-commit.1142ef1 → 0.0.1-commit.1bea0213

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 (53) hide show
  1. package/README.md +41 -15
  2. package/dest/block_proposal_handler.d.ts +7 -6
  3. package/dest/block_proposal_handler.d.ts.map +1 -1
  4. package/dest/block_proposal_handler.js +23 -29
  5. package/dest/checkpoint_builder.d.ts +18 -21
  6. package/dest/checkpoint_builder.d.ts.map +1 -1
  7. package/dest/checkpoint_builder.js +17 -12
  8. package/dest/config.d.ts +1 -1
  9. package/dest/config.d.ts.map +1 -1
  10. package/dest/config.js +6 -11
  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/tx_validator/tx_validator_factory.d.ts +1 -1
  35. package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
  36. package/dest/tx_validator/tx_validator_factory.js +2 -1
  37. package/dest/validator.d.ts +9 -8
  38. package/dest/validator.d.ts.map +1 -1
  39. package/dest/validator.js +68 -60
  40. package/package.json +19 -17
  41. package/src/block_proposal_handler.ts +34 -36
  42. package/src/checkpoint_builder.ts +37 -20
  43. package/src/config.ts +5 -10
  44. package/src/duties/validation_service.ts +91 -23
  45. package/src/factory.ts +1 -0
  46. package/src/key_store/ha_key_store.ts +269 -0
  47. package/src/key_store/index.ts +1 -0
  48. package/src/key_store/interface.ts +44 -5
  49. package/src/key_store/local_key_store.ts +13 -4
  50. package/src/key_store/node_keystore_adapter.ts +27 -4
  51. package/src/key_store/web3signer_key_store.ts +17 -4
  52. package/src/tx_validator/tx_validator_factory.ts +2 -0
  53. package/src/validator.ts +85 -69
@@ -6,6 +6,7 @@ import { KeystoreManager, loadKeystoreFile } from '@aztec/node-keystore';
6
6
  import type { EthRemoteSignerConfig } from '@aztec/node-keystore';
7
7
  import { AztecAddress } from '@aztec/stdlib/aztec-address';
8
8
  import { InvalidValidatorPrivateKeyError } from '@aztec/stdlib/validators';
9
+ import type { SigningContext } from '@aztec/validator-ha-signer/types';
9
10
 
10
11
  import type { TypedDataDefinition } from 'viem';
11
12
  import { privateKeyToAccount } from 'viem/accounts';
@@ -230,9 +231,10 @@ export class NodeKeystoreAdapter implements ExtendedValidatorKeyStore {
230
231
  /**
231
232
  * Sign typed data with all attester signers across validators.
232
233
  * @param typedData EIP-712 typed data
234
+ * @param _context Signing context (ignored by NodeKeystoreAdapter, used for HA protection)
233
235
  * @returns Array of signatures in validator order, flattened
234
236
  */
235
- async signTypedData(typedData: TypedDataDefinition): Promise<Signature[]> {
237
+ async signTypedData(typedData: TypedDataDefinition, _context: SigningContext): Promise<Signature[]> {
236
238
  const jobs: Promise<Signature>[] = [];
237
239
  for (const i of this.validatorIndices()) {
238
240
  const v = this.ensureValidator(i);
@@ -246,9 +248,10 @@ export class NodeKeystoreAdapter implements ExtendedValidatorKeyStore {
246
248
  /**
247
249
  * Sign a message with all attester signers across validators.
248
250
  * @param message 32-byte message (already hashed/padded as needed)
251
+ * @param _context Signing context (ignored by NodeKeystoreAdapter, used for HA protection)
249
252
  * @returns Array of signatures in validator order, flattened
250
253
  */
251
- async signMessage(message: Buffer32): Promise<Signature[]> {
254
+ async signMessage(message: Buffer32, _context: SigningContext): Promise<Signature[]> {
252
255
  const jobs: Promise<Signature>[] = [];
253
256
  for (const i of this.validatorIndices()) {
254
257
  const v = this.ensureValidator(i);
@@ -264,10 +267,15 @@ export class NodeKeystoreAdapter implements ExtendedValidatorKeyStore {
264
267
  * Hydrates caches on-demand when the address is first seen.
265
268
  * @param address Address to sign with
266
269
  * @param typedData EIP-712 typed data
270
+ * @param _context Signing context (ignored by NodeKeystoreAdapter, used for HA protection)
267
271
  * @returns Signature from the signer matching the address
268
272
  * @throws Error when no signer exists for the address
269
273
  */
270
- async signTypedDataWithAddress(address: EthAddress, typedData: TypedDataDefinition): Promise<Signature> {
274
+ async signTypedDataWithAddress(
275
+ address: EthAddress,
276
+ typedData: TypedDataDefinition,
277
+ _context: SigningContext,
278
+ ): Promise<Signature> {
271
279
  const entry = this.addressIndex.get(NodeKeystoreAdapter.key(address));
272
280
  if (entry) {
273
281
  return await this.keystoreManager.signTypedData(entry.signer, typedData);
@@ -290,10 +298,11 @@ export class NodeKeystoreAdapter implements ExtendedValidatorKeyStore {
290
298
  * Hydrates caches on-demand when the address is first seen.
291
299
  * @param address Address to sign with
292
300
  * @param message 32-byte message
301
+ * @param _context Signing context (ignored by NodeKeystoreAdapter, used for HA protection)
293
302
  * @returns Signature from the signer matching the address
294
303
  * @throws Error when no signer exists for the address
295
304
  */
296
- async signMessageWithAddress(address: EthAddress, message: Buffer32): Promise<Signature> {
305
+ async signMessageWithAddress(address: EthAddress, message: Buffer32, _context: SigningContext): Promise<Signature> {
297
306
  const entry = this.addressIndex.get(NodeKeystoreAdapter.key(address));
298
307
  if (entry) {
299
308
  return await this.keystoreManager.signMessage(entry.signer, message);
@@ -372,4 +381,18 @@ export class NodeKeystoreAdapter implements ExtendedValidatorKeyStore {
372
381
  const validatorIndex = this.findValidatorIndexForAttester(attesterAddress);
373
382
  return this.keystoreManager.getEffectiveRemoteSignerConfig(validatorIndex, attesterAddress);
374
383
  }
384
+
385
+ /**
386
+ * Start the key store - no-op
387
+ */
388
+ start(): Promise<void> {
389
+ return Promise.resolve();
390
+ }
391
+
392
+ /**
393
+ * Stop the key store - no-op
394
+ */
395
+ stop(): Promise<void> {
396
+ return Promise.resolve();
397
+ }
375
398
  }
@@ -2,6 +2,7 @@ import type { Buffer32 } from '@aztec/foundation/buffer';
2
2
  import { normalizeSignature } from '@aztec/foundation/crypto/secp256k1-signer';
3
3
  import { EthAddress } from '@aztec/foundation/eth-address';
4
4
  import { Signature } from '@aztec/foundation/eth-signature';
5
+ import type { SigningContext } from '@aztec/validator-ha-signer/types';
5
6
 
6
7
  import type { TypedDataDefinition } from 'viem';
7
8
 
@@ -44,9 +45,10 @@ export class Web3SignerKeyStore implements ValidatorKeyStore {
44
45
  /**
45
46
  * Sign EIP-712 typed data with all keystore addresses
46
47
  * @param typedData - The complete EIP-712 typed data structure (domain, types, primaryType, message)
48
+ * @param _context - Signing context (ignored by Web3SignerKeyStore, used for HA protection)
47
49
  * @return signatures
48
50
  */
49
- public signTypedData(typedData: TypedDataDefinition): Promise<Signature[]> {
51
+ public signTypedData(typedData: TypedDataDefinition, _context: SigningContext): Promise<Signature[]> {
50
52
  return Promise.all(this.addresses.map(address => this.makeJsonRpcSignTypedDataRequest(address, typedData)));
51
53
  }
52
54
 
@@ -54,10 +56,15 @@ export class Web3SignerKeyStore implements ValidatorKeyStore {
54
56
  * Sign EIP-712 typed data with a specific address
55
57
  * @param address - The address of the signer to use
56
58
  * @param typedData - The complete EIP-712 typed data structure (domain, types, primaryType, message)
59
+ * @param _context - Signing context (ignored by Web3SignerKeyStore, used for HA protection)
57
60
  * @returns signature for the specified address
58
61
  * @throws Error if the address is not found in the keystore or signing fails
59
62
  */
60
- public async signTypedDataWithAddress(address: EthAddress, typedData: TypedDataDefinition): Promise<Signature> {
63
+ public async signTypedDataWithAddress(
64
+ address: EthAddress,
65
+ typedData: TypedDataDefinition,
66
+ _context: SigningContext,
67
+ ): Promise<Signature> {
61
68
  if (!this.addresses.some(addr => addr.equals(address))) {
62
69
  throw new Error(`Address ${address.toString()} not found in keystore`);
63
70
  }
@@ -69,9 +76,10 @@ export class Web3SignerKeyStore implements ValidatorKeyStore {
69
76
  * Sign a message with all keystore addresses using EIP-191 prefix
70
77
  *
71
78
  * @param message - The message to sign
79
+ * @param _context - Signing context (ignored by Web3SignerKeyStore, used for HA protection)
72
80
  * @return signatures
73
81
  */
74
- public signMessage(message: Buffer32): Promise<Signature[]> {
82
+ public signMessage(message: Buffer32, _context: SigningContext): Promise<Signature[]> {
75
83
  return Promise.all(this.addresses.map(address => this.makeJsonRpcSignRequest(address, message)));
76
84
  }
77
85
 
@@ -79,10 +87,15 @@ export class Web3SignerKeyStore implements ValidatorKeyStore {
79
87
  * Sign a message with a specific address using EIP-191 prefix
80
88
  * @param address - The address of the signer to use
81
89
  * @param message - The message to sign
90
+ * @param _context - Signing context (ignored by Web3SignerKeyStore, used for HA protection)
82
91
  * @returns signature for the specified address
83
92
  * @throws Error if the address is not found in the keystore or signing fails
84
93
  */
85
- public async signMessageWithAddress(address: EthAddress, message: Buffer32): Promise<Signature> {
94
+ public async signMessageWithAddress(
95
+ address: EthAddress,
96
+ message: Buffer32,
97
+ _context: SigningContext,
98
+ ): Promise<Signature> {
86
99
  if (!this.addresses.some(addr => addr.equals(address))) {
87
100
  throw new Error(`Address ${address.toString()} not found in keystore`);
88
101
  }
@@ -10,6 +10,7 @@ import {
10
10
  GasTxValidator,
11
11
  MetadataTxValidator,
12
12
  PhasesTxValidator,
13
+ SizeTxValidator,
13
14
  TimestampTxValidator,
14
15
  TxPermittedValidator,
15
16
  TxProofValidator,
@@ -55,6 +56,7 @@ export function createValidatorForAcceptingTxs(
55
56
  ): TxValidator<Tx> {
56
57
  const validators: TxValidator<Tx>[] = [
57
58
  new TxPermittedValidator(txsPermitted),
59
+ new SizeTxValidator(),
58
60
  new DataTxValidator(),
59
61
  new MetadataTxValidator({
60
62
  l1ChainId: new Fr(l1ChainId),
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';
@@ -16,7 +22,8 @@ import type { P2P, PeerId, TxProvider } 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,
22
29
  Validator,
@@ -36,6 +43,8 @@ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
36
43
  import type { BlockHeader, CheckpointGlobalVariables, Tx } from '@aztec/stdlib/tx';
37
44
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
38
45
  import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
46
+ import { createHASigner } from '@aztec/validator-ha-signer/factory';
47
+ import { DutyType, type SigningContext } from '@aztec/validator-ha-signer/types';
39
48
 
40
49
  import { EventEmitter } from 'events';
41
50
  import type { TypedDataDefinition } from 'viem';
@@ -43,6 +52,8 @@ import type { TypedDataDefinition } from 'viem';
43
52
  import { BlockProposalHandler, type BlockProposalValidationFailureReason } from './block_proposal_handler.js';
44
53
  import type { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
45
54
  import { ValidationService } from './duties/validation_service.js';
55
+ import { HAKeyStore } from './key_store/ha_key_store.js';
56
+ import type { ExtendedValidatorKeyStore } from './key_store/interface.js';
46
57
  import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
47
58
  import { ValidatorMetrics } from './metrics.js';
48
59
 
@@ -82,7 +93,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
82
93
  private validatedBlockSlots: Set<SlotNumber> = new Set();
83
94
 
84
95
  protected constructor(
85
- private keyStore: NodeKeystoreAdapter,
96
+ private keyStore: ExtendedValidatorKeyStore,
86
97
  private epochCache: EpochCache,
87
98
  private p2pClient: P2P,
88
99
  private blockProposalHandler: BlockProposalHandler,
@@ -165,7 +176,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
165
176
  }
166
177
  }
167
178
 
168
- static new(
179
+ static async new(
169
180
  config: ValidatorClientFullConfig,
170
181
  checkpointsBuilder: FullNodeCheckpointsBuilder,
171
182
  worldState: WorldStateSynchronizer,
@@ -190,14 +201,26 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
190
201
  l1ToL2MessageSource,
191
202
  txProvider,
192
203
  blockProposalValidator,
204
+ epochCache,
193
205
  config,
194
206
  metrics,
195
207
  dateProvider,
196
208
  telemetry,
197
209
  );
198
210
 
211
+ let validatorKeyStore: ExtendedValidatorKeyStore = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
212
+ if (config.haSigningEnabled) {
213
+ // If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
214
+ const haConfig = {
215
+ ...config,
216
+ maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000,
217
+ };
218
+ const { signer } = await createHASigner(haConfig);
219
+ validatorKeyStore = new HAKeyStore(validatorKeyStore, signer);
220
+ }
221
+
199
222
  const validator = new ValidatorClient(
200
- NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager),
223
+ validatorKeyStore,
201
224
  epochCache,
202
225
  p2pClient,
203
226
  blockProposalHandler,
@@ -224,8 +247,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
224
247
  return this.blockProposalHandler;
225
248
  }
226
249
 
227
- public signWithAddress(addr: EthAddress, msg: TypedDataDefinition) {
228
- return this.keyStore.signTypedDataWithAddress(addr, msg);
250
+ public signWithAddress(addr: EthAddress, msg: TypedDataDefinition, context: SigningContext) {
251
+ return this.keyStore.signTypedDataWithAddress(addr, msg, context);
229
252
  }
230
253
 
231
254
  public getCoinbaseForAttestor(attestor: EthAddress): EthAddress {
@@ -250,6 +273,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
250
273
  return;
251
274
  }
252
275
 
276
+ await this.keyStore.start();
277
+
253
278
  await this.registerHandlers();
254
279
 
255
280
  const myAddresses = this.getValidatorAddresses();
@@ -265,6 +290,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
265
290
 
266
291
  public async stop() {
267
292
  await this.epochCacheUpdateLoop.stop();
293
+ await this.keyStore.stop();
268
294
  }
269
295
 
270
296
  /** Register handlers on the p2p client */
@@ -301,6 +327,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
301
327
  */
302
328
  async validateBlockProposal(proposal: BlockProposal, proposalSender: PeerId): Promise<boolean> {
303
329
  const slotNumber = proposal.slotNumber;
330
+
331
+ // Note: During escape hatch, we still want to "validate" proposals for observability,
332
+ // but we intentionally reject them and disable slashing invalid block and attestation flow.
333
+ const escapeHatchOpen = await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber);
334
+
304
335
  const proposer = proposal.getSender();
305
336
 
306
337
  // Reject proposals with invalid signatures
@@ -334,7 +365,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
334
365
  const validationResult = await this.blockProposalHandler.handleBlockProposal(
335
366
  proposal,
336
367
  proposalSender,
337
- !!shouldReexecute,
368
+ !!shouldReexecute && !escapeHatchOpen,
338
369
  );
339
370
 
340
371
  if (!validationResult.isValid) {
@@ -359,6 +390,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
359
390
 
360
391
  // Slash invalid block proposals (can happen even when not in committee)
361
392
  if (
393
+ !escapeHatchOpen &&
362
394
  validationResult.reason &&
363
395
  SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) &&
364
396
  slashBroadcastedInvalidBlockPenalty > 0n
@@ -373,8 +405,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
373
405
  ...proposalInfo,
374
406
  inCommittee: partOfCommittee,
375
407
  fishermanMode: this.config.fishermanMode || false,
408
+ escapeHatchOpen,
376
409
  });
377
410
 
411
+ if (escapeHatchOpen) {
412
+ this.log.warn(`Escape hatch open for slot ${slotNumber}, rejecting block proposal`, proposalInfo);
413
+ return false;
414
+ }
415
+
378
416
  // TODO(palla/mbps): Remove this once checkpoint validation is stable.
379
417
  // Track that we successfully validated a block for this slot, so we can attest to checkpoint proposals for it.
380
418
  this.validatedBlockSlots.add(slotNumber);
@@ -395,6 +433,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
395
433
  const slotNumber = proposal.slotNumber;
396
434
  const proposer = proposal.getSender();
397
435
 
436
+ // If escape hatch is open for this slot's epoch, do not attest.
437
+ if (await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber)) {
438
+ this.log.warn(`Escape hatch open for slot ${slotNumber}, skipping checkpoint attestation handling`);
439
+ return undefined;
440
+ }
441
+
398
442
  // Reject proposals with invalid signatures
399
443
  if (!proposer) {
400
444
  this.log.warn(`Received checkpoint proposal with invalid signature for slot ${slotNumber}`);
@@ -531,16 +575,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
531
575
  return { isValid: false, reason: 'last_block_not_found' };
532
576
  }
533
577
 
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
578
  // Get all full blocks for the slot and checkpoint
543
- const blocks = await this.getBlocksForSlot(slot, lastBlockHeader, checkpointNumber);
579
+ const blocks = await this.blockSource.getBlocksForSlot(slot);
544
580
  if (blocks.length === 0) {
545
581
  this.log.warn(`No blocks found for slot ${slot}`, proposalInfo);
546
582
  return { isValid: false, reason: 'no_blocks_for_slot' };
@@ -554,10 +590,20 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
554
590
  // Get checkpoint constants from first block
555
591
  const firstBlock = blocks[0];
556
592
  const constants = this.extractCheckpointConstants(firstBlock);
593
+ const checkpointNumber = firstBlock.checkpointNumber;
557
594
 
558
595
  // Get L1-to-L2 messages for this checkpoint
559
596
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
560
597
 
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.
601
+ 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());
606
+
561
607
  // Fork world state at the block before the first block
562
608
  const parentBlockNumber = BlockNumber(firstBlock.number - 1);
563
609
  const fork = await this.worldState.fork(parentBlockNumber);
@@ -568,6 +614,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
568
614
  checkpointNumber,
569
615
  constants,
570
616
  l1ToL2Messages,
617
+ previousCheckpointOutHashes,
571
618
  fork,
572
619
  blocks,
573
620
  );
@@ -595,6 +642,18 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
595
642
  return { isValid: false, reason: 'archive_mismatch' };
596
643
  }
597
644
 
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)) {
649
+ this.log.warn(`Epoch out hash mismatch`, {
650
+ proposalOutHash: proposalOutHash.toString(),
651
+ computedOutHash: computedOutHash.toString(),
652
+ ...proposalInfo,
653
+ });
654
+ return { isValid: false, reason: 'out_hash_mismatch' };
655
+ }
656
+
598
657
  this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
599
658
  return { isValid: true };
600
659
  } finally {
@@ -602,50 +661,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
602
661
  }
603
662
  }
604
663
 
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
664
  /**
646
665
  * Extract checkpoint global variables from a block.
647
666
  */
648
- private extractCheckpointConstants(block: L2BlockNew): CheckpointGlobalVariables {
667
+ private extractCheckpointConstants(block: L2Block): CheckpointGlobalVariables {
649
668
  const gv = block.header.globalVariables;
650
669
  return {
651
670
  chainId: gv.chainId,
@@ -668,14 +687,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
668
687
  return;
669
688
  }
670
689
 
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);
690
+ const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
679
691
  if (blocks.length === 0) {
680
692
  this.log.warn(`No blocks found for blob upload`, proposalInfo);
681
693
  return;
@@ -722,7 +734,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
722
734
 
723
735
  async createBlockProposal(
724
736
  blockHeader: BlockHeader,
725
- indexWithinCheckpoint: number,
737
+ indexWithinCheckpoint: IndexWithinCheckpoint,
726
738
  inHash: Fr,
727
739
  archive: Fr,
728
740
  txs: Tx[],
@@ -778,8 +790,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
778
790
  async signAttestationsAndSigners(
779
791
  attestationsAndSigners: CommitteeAttestationsAndSigners,
780
792
  proposer: EthAddress,
793
+ slot: SlotNumber,
794
+ blockNumber: BlockNumber | CheckpointNumber,
781
795
  ): Promise<Signature> {
782
- return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer);
796
+ return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer, slot, blockNumber);
783
797
  }
784
798
 
785
799
  async collectOwnAttestations(proposal: CheckpointProposal): Promise<CheckpointAttestation[]> {
@@ -886,7 +900,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
886
900
  }
887
901
 
888
902
  const payloadToSign = authRequest.getPayloadToSign();
889
- const signature = await this.keyStore.signMessageWithAddress(addressToUse, payloadToSign);
903
+ // AUTH_REQUEST doesn't require HA protection - multiple signatures are safe
904
+ const context: SigningContext = { dutyType: DutyType.AUTH_REQUEST };
905
+ const signature = await this.keyStore.signMessageWithAddress(addressToUse, payloadToSign, context);
890
906
  const authResponse = new AuthResponse(statusMessage, signature);
891
907
  return authResponse.toBuffer();
892
908
  }