@aztec/validator-client 0.0.1-commit.7d4e6cd → 0.0.1-commit.86469d5

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 +41 -15
  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 +13 -13
  41. package/dest/validator.d.ts.map +1 -1
  42. package/dest/validator.js +82 -80
  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 +98 -93
@@ -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 { 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 */
@@ -301,6 +323,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
301
323
  */
302
324
  async validateBlockProposal(proposal: BlockProposal, proposalSender: PeerId): Promise<boolean> {
303
325
  const slotNumber = proposal.slotNumber;
326
+
327
+ // Note: During escape hatch, we still want to "validate" proposals for observability,
328
+ // but we intentionally reject them and disable slashing invalid block and attestation flow.
329
+ const escapeHatchOpen = await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber);
330
+
304
331
  const proposer = proposal.getSender();
305
332
 
306
333
  // Reject proposals with invalid signatures
@@ -334,7 +361,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
334
361
  const validationResult = await this.blockProposalHandler.handleBlockProposal(
335
362
  proposal,
336
363
  proposalSender,
337
- !!shouldReexecute,
364
+ !!shouldReexecute && !escapeHatchOpen,
338
365
  );
339
366
 
340
367
  if (!validationResult.isValid) {
@@ -359,6 +386,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
359
386
 
360
387
  // Slash invalid block proposals (can happen even when not in committee)
361
388
  if (
389
+ !escapeHatchOpen &&
362
390
  validationResult.reason &&
363
391
  SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) &&
364
392
  slashBroadcastedInvalidBlockPenalty > 0n
@@ -373,11 +401,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
373
401
  ...proposalInfo,
374
402
  inCommittee: partOfCommittee,
375
403
  fishermanMode: this.config.fishermanMode || false,
404
+ escapeHatchOpen,
376
405
  });
377
406
 
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);
407
+ if (escapeHatchOpen) {
408
+ this.log.warn(`Escape hatch open for slot ${slotNumber}, rejecting block proposal`, proposalInfo);
409
+ return false;
410
+ }
381
411
 
382
412
  return true;
383
413
  }
@@ -395,6 +425,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
395
425
  const slotNumber = proposal.slotNumber;
396
426
  const proposer = proposal.getSender();
397
427
 
428
+ // If escape hatch is open for this slot's epoch, do not attest.
429
+ if (await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber)) {
430
+ this.log.warn(`Escape hatch open for slot ${slotNumber}, skipping checkpoint attestation handling`);
431
+ return undefined;
432
+ }
433
+
398
434
  // Reject proposals with invalid signatures
399
435
  if (!proposer) {
400
436
  this.log.warn(`Received checkpoint proposal with invalid signature for slot ${slotNumber}`);
@@ -417,17 +453,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
417
453
  fishermanMode: this.config.fishermanMode || false,
418
454
  });
419
455
 
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
456
  // 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);
457
+ if (this.config.skipCheckpointProposalValidation) {
458
+ this.log.warn(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
431
459
  } else {
432
460
  const validationResult = await this.validateCheckpointProposal(proposal, proposalInfo);
433
461
  if (!validationResult.isValid) {
@@ -503,7 +531,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
503
531
  proposalInfo: LogData,
504
532
  ): Promise<{ isValid: true } | { isValid: false; reason: string }> {
505
533
  const slot = proposal.slotNumber;
506
- const timeoutSeconds = 10;
534
+ const timeoutSeconds = 10; // TODO(palla/mbps): This should map to the timetable settings
507
535
 
508
536
  // Wait for last block to sync by archive
509
537
  let lastBlockHeader: BlockHeader | undefined;
@@ -531,16 +559,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
531
559
  return { isValid: false, reason: 'last_block_not_found' };
532
560
  }
533
561
 
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
562
  // Get all full blocks for the slot and checkpoint
543
- const blocks = await this.getBlocksForSlot(slot, lastBlockHeader, checkpointNumber);
563
+ const blocks = await this.blockSource.getBlocksForSlot(slot);
544
564
  if (blocks.length === 0) {
545
565
  this.log.warn(`No blocks found for slot ${slot}`, proposalInfo);
546
566
  return { isValid: false, reason: 'no_blocks_for_slot' };
@@ -554,10 +574,20 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
554
574
  // Get checkpoint constants from first block
555
575
  const firstBlock = blocks[0];
556
576
  const constants = this.extractCheckpointConstants(firstBlock);
577
+ const checkpointNumber = firstBlock.checkpointNumber;
557
578
 
558
579
  // Get L1-to-L2 messages for this checkpoint
559
580
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
560
581
 
582
+ // Compute the previous checkpoint out hashes for the epoch.
583
+ // TODO: There can be a more efficient way to get the previous checkpoint out hashes without having to fetch the
584
+ // actual checkpoints and the blocks/txs in them.
585
+ const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
586
+ const previousCheckpoints = (await this.blockSource.getCheckpointsForEpoch(epoch))
587
+ .filter(b => b.number < checkpointNumber)
588
+ .sort((a, b) => a.number - b.number);
589
+ const previousCheckpointOutHashes = previousCheckpoints.map(c => c.getCheckpointOutHash());
590
+
561
591
  // Fork world state at the block before the first block
562
592
  const parentBlockNumber = BlockNumber(firstBlock.number - 1);
563
593
  const fork = await this.worldState.fork(parentBlockNumber);
@@ -568,8 +598,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
568
598
  checkpointNumber,
569
599
  constants,
570
600
  l1ToL2Messages,
601
+ previousCheckpointOutHashes,
571
602
  fork,
572
603
  blocks,
604
+ this.log.getBindings(),
573
605
  );
574
606
 
575
607
  // Complete the checkpoint to get computed values
@@ -595,6 +627,22 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
595
627
  return { isValid: false, reason: 'archive_mismatch' };
596
628
  }
597
629
 
630
+ // Check that the accumulated epoch out hash matches the value in the proposal.
631
+ // The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
632
+ const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
633
+ const computedEpochOutHash = accumulateCheckpointOutHashes([...previousCheckpointOutHashes, checkpointOutHash]);
634
+ const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
635
+ if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
636
+ this.log.warn(`Epoch out hash mismatch`, {
637
+ proposalEpochOutHash: proposalEpochOutHash.toString(),
638
+ computedEpochOutHash: computedEpochOutHash.toString(),
639
+ checkpointOutHash: checkpointOutHash.toString(),
640
+ previousCheckpointOutHashes: previousCheckpointOutHashes.map(h => h.toString()),
641
+ ...proposalInfo,
642
+ });
643
+ return { isValid: false, reason: 'out_hash_mismatch' };
644
+ }
645
+
598
646
  this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
599
647
  return { isValid: true };
600
648
  } finally {
@@ -602,50 +650,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
602
650
  }
603
651
  }
604
652
 
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
653
  /**
646
654
  * Extract checkpoint global variables from a block.
647
655
  */
648
- private extractCheckpointConstants(block: L2BlockNew): CheckpointGlobalVariables {
656
+ private extractCheckpointConstants(block: L2Block): CheckpointGlobalVariables {
649
657
  const gv = block.header.globalVariables;
650
658
  return {
651
659
  chainId: gv.chainId,
@@ -668,14 +676,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
668
676
  return;
669
677
  }
670
678
 
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);
679
+ const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
679
680
  if (blocks.length === 0) {
680
681
  this.log.warn(`No blocks found for blob upload`, proposalInfo);
681
682
  return;
@@ -722,12 +723,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
722
723
 
723
724
  async createBlockProposal(
724
725
  blockHeader: BlockHeader,
725
- indexWithinCheckpoint: number,
726
+ indexWithinCheckpoint: IndexWithinCheckpoint,
726
727
  inHash: Fr,
727
728
  archive: Fr,
728
729
  txs: Tx[],
729
730
  proposerAddress: EthAddress | undefined,
730
- options: BlockProposalOptions,
731
+ options: BlockProposalOptions = {},
731
732
  ): Promise<BlockProposal> {
732
733
  // TODO(palla/mbps): Prevent double proposals properly
733
734
  // if (this.previousProposal?.slotNumber === blockHeader.globalVariables.slotNumber) {
@@ -759,7 +760,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
759
760
  archive: Fr,
760
761
  lastBlockInfo: CreateCheckpointProposalLastBlockData | undefined,
761
762
  proposerAddress: EthAddress | undefined,
762
- options: CheckpointProposalOptions,
763
+ options: CheckpointProposalOptions = {},
763
764
  ): Promise<CheckpointProposal> {
764
765
  this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
765
766
  return await this.validationService.createCheckpointProposal(
@@ -778,8 +779,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
778
779
  async signAttestationsAndSigners(
779
780
  attestationsAndSigners: CommitteeAttestationsAndSigners,
780
781
  proposer: EthAddress,
782
+ slot: SlotNumber,
783
+ blockNumber: BlockNumber | CheckpointNumber,
781
784
  ): Promise<Signature> {
782
- return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer);
785
+ return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer, slot, blockNumber);
783
786
  }
784
787
 
785
788
  async collectOwnAttestations(proposal: CheckpointProposal): Promise<CheckpointAttestation[]> {
@@ -886,7 +889,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
886
889
  }
887
890
 
888
891
  const payloadToSign = authRequest.getPayloadToSign();
889
- const signature = await this.keyStore.signMessageWithAddress(addressToUse, payloadToSign);
892
+ // AUTH_REQUEST doesn't require HA protection - multiple signatures are safe
893
+ const context: SigningContext = { dutyType: DutyType.AUTH_REQUEST };
894
+ const signature = await this.keyStore.signMessageWithAddress(addressToUse, payloadToSign, context);
890
895
  const authResponse = new AuthResponse(statusMessage, signature);
891
896
  return authResponse.toBuffer();
892
897
  }