@aztec/validator-client 0.0.1-commit.1142ef1 → 0.0.1-commit.18ccd8f0

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 +23 -23
  6. package/dest/checkpoint_builder.d.ts.map +1 -1
  7. package/dest/checkpoint_builder.js +38 -26
  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 +74 -31
  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,7 +1,7 @@
1
1
  import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
2
2
  import { merge, pick } from '@aztec/foundation/collection';
3
3
  import { Fr } from '@aztec/foundation/curves/bn254';
4
- import { createLogger } from '@aztec/foundation/log';
4
+ import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
5
5
  import { bufferToHex } from '@aztec/foundation/string';
6
6
  import { DateProvider, Timer, elapsed } from '@aztec/foundation/timer';
7
7
  import { getDefaultAllowedSetupFunctions } from '@aztec/p2p/msg_validators';
@@ -12,39 +12,42 @@ import {
12
12
  PublicProcessor,
13
13
  createPublicTxSimulatorForBlockBuilding,
14
14
  } from '@aztec/simulator/server';
15
- import { L2BlockNew } from '@aztec/stdlib/block';
15
+ import { L2Block } from '@aztec/stdlib/block';
16
16
  import { Checkpoint } from '@aztec/stdlib/checkpoint';
17
17
  import type { ContractDataSource } from '@aztec/stdlib/contract';
18
+ import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
18
19
  import { Gas } from '@aztec/stdlib/gas';
19
20
  import {
21
+ type BuildBlockInCheckpointResult,
20
22
  type FullNodeBlockBuilderConfig,
21
23
  FullNodeBlockBuilderConfigKeys,
24
+ type ICheckpointBlockBuilder,
25
+ type ICheckpointsBuilder,
22
26
  type MerkleTreeWriteOperations,
23
27
  type PublicProcessorLimits,
28
+ type WorldStateSynchronizer,
24
29
  } from '@aztec/stdlib/interfaces/server';
25
30
  import { MerkleTreeId } from '@aztec/stdlib/trees';
26
- import { type CheckpointGlobalVariables, type FailedTx, GlobalVariables, StateReference, Tx } from '@aztec/stdlib/tx';
31
+ import { type CheckpointGlobalVariables, GlobalVariables, StateReference, Tx } from '@aztec/stdlib/tx';
27
32
  import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
28
33
 
29
34
  import { createValidatorForBlockBuilding } from './tx_validator/tx_validator_factory.js';
30
35
 
31
- const log = createLogger('checkpoint-builder');
36
+ // Re-export for backward compatibility
37
+ export type { BuildBlockInCheckpointResult } from '@aztec/stdlib/interfaces/server';
32
38
 
33
- export interface BuildBlockInCheckpointResult {
34
- block: L2BlockNew;
35
- publicGas: Gas;
36
- publicProcessorDuration: number;
37
- numTxs: number;
38
- failedTxs: FailedTx[];
39
+ /** Result of building a block within a checkpoint. Extends the base interface with timer. */
40
+ export interface BuildBlockInCheckpointResultWithTimer extends BuildBlockInCheckpointResult {
39
41
  blockBuildingTimer: Timer;
40
- usedTxs: Tx[];
41
42
  }
42
43
 
43
44
  /**
44
45
  * Builder for a single checkpoint. Handles building blocks within the checkpoint
45
46
  * and completing it.
46
47
  */
47
- export class CheckpointBuilder {
48
+ export class CheckpointBuilder implements ICheckpointBlockBuilder {
49
+ private log: Logger;
50
+
48
51
  constructor(
49
52
  private checkpointBuilder: LightweightCheckpointBuilder,
50
53
  private fork: MerkleTreeWriteOperations,
@@ -52,7 +55,13 @@ export class CheckpointBuilder {
52
55
  private contractDataSource: ContractDataSource,
53
56
  private dateProvider: DateProvider,
54
57
  private telemetryClient: TelemetryClient,
55
- ) {}
58
+ bindings?: LoggerBindings,
59
+ ) {
60
+ this.log = createLogger('checkpoint-builder', {
61
+ ...bindings,
62
+ instanceId: `checkpoint-${checkpointBuilder.checkpointNumber}`,
63
+ });
64
+ }
56
65
 
57
66
  getConstantData(): CheckpointGlobalVariables {
58
67
  return this.checkpointBuilder.constants;
@@ -65,12 +74,17 @@ export class CheckpointBuilder {
65
74
  pendingTxs: Iterable<Tx> | AsyncIterable<Tx>,
66
75
  blockNumber: BlockNumber,
67
76
  timestamp: bigint,
68
- opts: PublicProcessorLimits & { expectedEndState?: StateReference },
69
- ): Promise<BuildBlockInCheckpointResult> {
77
+ opts: PublicProcessorLimits & { expectedEndState?: StateReference } = {},
78
+ ): Promise<BuildBlockInCheckpointResultWithTimer> {
70
79
  const blockBuildingTimer = new Timer();
71
80
  const slot = this.checkpointBuilder.constants.slotNumber;
72
81
 
73
- log.verbose(`Building block ${blockNumber} for slot ${slot} within checkpoint`, { slot, blockNumber, ...opts });
82
+ this.log.verbose(`Building block ${blockNumber} for slot ${slot} within checkpoint`, {
83
+ slot,
84
+ blockNumber,
85
+ ...opts,
86
+ currentTime: new Date(this.dateProvider.now()),
87
+ });
74
88
 
75
89
  const constants = this.checkpointBuilder.constants;
76
90
  const globalVariables = GlobalVariables.from({
@@ -85,7 +99,7 @@ export class CheckpointBuilder {
85
99
  });
86
100
  const { processor, validator } = await this.makeBlockBuilderDeps(globalVariables, this.fork);
87
101
 
88
- const [publicProcessorDuration, [processedTxs, failedTxs, usedTxs]] = await elapsed(() =>
102
+ const [publicProcessorDuration, [processedTxs, failedTxs, usedTxs, _, usedTxBlobFields]] = await elapsed(() =>
89
103
  processor.process(pendingTxs, opts, validator),
90
104
  );
91
105
 
@@ -105,8 +119,9 @@ export class CheckpointBuilder {
105
119
  failedTxs,
106
120
  blockBuildingTimer,
107
121
  usedTxs,
122
+ usedTxBlobFields,
108
123
  };
109
- log.debug('Built block within checkpoint', res.block.header);
124
+ this.log.debug('Built block within checkpoint', res.block.header);
110
125
  return res;
111
126
  }
112
127
 
@@ -114,7 +129,7 @@ export class CheckpointBuilder {
114
129
  async completeCheckpoint(): Promise<Checkpoint> {
115
130
  const checkpoint = await this.checkpointBuilder.completeCheckpoint();
116
131
 
117
- log.verbose(`Completed checkpoint ${checkpoint.number}`, {
132
+ this.log.verbose(`Completed checkpoint ${checkpoint.number}`, {
118
133
  checkpointNumber: checkpoint.number,
119
134
  numBlocks: checkpoint.blocks.length,
120
135
  archiveRoot: checkpoint.archive.root.toString(),
@@ -130,14 +145,16 @@ export class CheckpointBuilder {
130
145
 
131
146
  protected async makeBlockBuilderDeps(globalVariables: GlobalVariables, fork: MerkleTreeWriteOperations) {
132
147
  const txPublicSetupAllowList = this.config.txPublicSetupAllowList ?? (await getDefaultAllowedSetupFunctions());
133
- const contractsDB = new PublicContractsDB(this.contractDataSource);
148
+ const contractsDB = new PublicContractsDB(this.contractDataSource, this.log.getBindings());
134
149
  const guardedFork = new GuardedMerkleTreeOperations(fork);
135
150
 
151
+ const bindings = this.log.getBindings();
136
152
  const publicTxSimulator = createPublicTxSimulatorForBlockBuilding(
137
153
  guardedFork,
138
154
  contractsDB,
139
155
  globalVariables,
140
156
  this.telemetryClient,
157
+ bindings,
141
158
  );
142
159
 
143
160
  const processor = new PublicProcessor(
@@ -147,7 +164,7 @@ export class CheckpointBuilder {
147
164
  publicTxSimulator,
148
165
  this.dateProvider,
149
166
  this.telemetryClient,
150
- undefined,
167
+ createLogger('simulator:public-processor', bindings),
151
168
  this.config,
152
169
  );
153
170
 
@@ -156,6 +173,7 @@ export class CheckpointBuilder {
156
173
  this.contractDataSource,
157
174
  globalVariables,
158
175
  txPublicSetupAllowList,
176
+ this.log.getBindings(),
159
177
  );
160
178
 
161
179
  return {
@@ -165,16 +183,19 @@ export class CheckpointBuilder {
165
183
  }
166
184
  }
167
185
 
168
- /**
169
- * Factory for creating checkpoint builders.
170
- */
171
- export class FullNodeCheckpointsBuilder {
186
+ /** Factory for creating checkpoint builders. */
187
+ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder {
188
+ private log: Logger;
189
+
172
190
  constructor(
173
- private config: FullNodeBlockBuilderConfig,
191
+ private config: FullNodeBlockBuilderConfig & Pick<L1RollupConstants, 'l1GenesisTime' | 'slotDuration'>,
192
+ private worldState: WorldStateSynchronizer,
174
193
  private contractDataSource: ContractDataSource,
175
194
  private dateProvider: DateProvider,
176
195
  private telemetryClient: TelemetryClient = getTelemetryClient(),
177
- ) {}
196
+ ) {
197
+ this.log = createLogger('checkpoint-builder');
198
+ }
178
199
 
179
200
  public getConfig(): FullNodeBlockBuilderConfig {
180
201
  return this.config;
@@ -191,12 +212,14 @@ export class FullNodeCheckpointsBuilder {
191
212
  checkpointNumber: CheckpointNumber,
192
213
  constants: CheckpointGlobalVariables,
193
214
  l1ToL2Messages: Fr[],
215
+ previousCheckpointOutHashes: Fr[],
194
216
  fork: MerkleTreeWriteOperations,
217
+ bindings?: LoggerBindings,
195
218
  ): Promise<CheckpointBuilder> {
196
219
  const stateReference = await fork.getStateReference();
197
220
  const archiveTree = await fork.getTreeInfo(MerkleTreeId.ARCHIVE);
198
221
 
199
- log.verbose(`Building new checkpoint ${checkpointNumber}`, {
222
+ this.log.verbose(`Building new checkpoint ${checkpointNumber}`, {
200
223
  checkpointNumber,
201
224
  msgCount: l1ToL2Messages.length,
202
225
  initialStateReference: stateReference.toInspect(),
@@ -208,7 +231,9 @@ export class FullNodeCheckpointsBuilder {
208
231
  checkpointNumber,
209
232
  constants,
210
233
  l1ToL2Messages,
234
+ previousCheckpointOutHashes,
211
235
  fork,
236
+ bindings,
212
237
  );
213
238
 
214
239
  return new CheckpointBuilder(
@@ -218,6 +243,7 @@ export class FullNodeCheckpointsBuilder {
218
243
  this.contractDataSource,
219
244
  this.dateProvider,
220
245
  this.telemetryClient,
246
+ bindings,
221
247
  );
222
248
  }
223
249
 
@@ -228,17 +254,26 @@ export class FullNodeCheckpointsBuilder {
228
254
  checkpointNumber: CheckpointNumber,
229
255
  constants: CheckpointGlobalVariables,
230
256
  l1ToL2Messages: Fr[],
257
+ previousCheckpointOutHashes: Fr[],
231
258
  fork: MerkleTreeWriteOperations,
232
- existingBlocks: L2BlockNew[] = [],
259
+ existingBlocks: L2Block[] = [],
260
+ bindings?: LoggerBindings,
233
261
  ): Promise<CheckpointBuilder> {
234
262
  const stateReference = await fork.getStateReference();
235
263
  const archiveTree = await fork.getTreeInfo(MerkleTreeId.ARCHIVE);
236
264
 
237
265
  if (existingBlocks.length === 0) {
238
- return this.startCheckpoint(checkpointNumber, constants, l1ToL2Messages, fork);
266
+ return this.startCheckpoint(
267
+ checkpointNumber,
268
+ constants,
269
+ l1ToL2Messages,
270
+ previousCheckpointOutHashes,
271
+ fork,
272
+ bindings,
273
+ );
239
274
  }
240
275
 
241
- log.verbose(`Resuming checkpoint ${checkpointNumber} with ${existingBlocks.length} existing blocks`, {
276
+ this.log.verbose(`Resuming checkpoint ${checkpointNumber} with ${existingBlocks.length} existing blocks`, {
242
277
  checkpointNumber,
243
278
  msgCount: l1ToL2Messages.length,
244
279
  existingBlockCount: existingBlocks.length,
@@ -251,8 +286,10 @@ export class FullNodeCheckpointsBuilder {
251
286
  checkpointNumber,
252
287
  constants,
253
288
  l1ToL2Messages,
289
+ previousCheckpointOutHashes,
254
290
  fork,
255
291
  existingBlocks,
292
+ bindings,
256
293
  );
257
294
 
258
295
  return new CheckpointBuilder(
@@ -262,6 +299,12 @@ export class FullNodeCheckpointsBuilder {
262
299
  this.contractDataSource,
263
300
  this.dateProvider,
264
301
  this.telemetryClient,
302
+ bindings,
265
303
  );
266
304
  }
305
+
306
+ /** Returns a fork of the world state at the given block number. */
307
+ getFork(blockNumber: BlockNumber): Promise<MerkleTreeWriteOperations> {
308
+ return this.worldState.fork(blockNumber);
309
+ }
267
310
  }
package/src/config.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  } from '@aztec/foundation/config';
8
8
  import { EthAddress } from '@aztec/foundation/eth-address';
9
9
  import type { ValidatorClientConfig } from '@aztec/stdlib/interfaces/server';
10
+ import { validatorHASignerConfigMappings } from '@aztec/validator-ha-signer/config';
10
11
 
11
12
  export type { ValidatorClientConfig };
12
13
 
@@ -53,16 +54,10 @@ export const validatorClientConfigMappings: ConfigMappingsType<ValidatorClientCo
53
54
  description: 'Re-execute transactions before attesting',
54
55
  ...booleanConfigHelper(true),
55
56
  },
56
- validatorReexecuteDeadlineMs: {
57
- env: 'VALIDATOR_REEXECUTE_DEADLINE_MS',
58
- description: 'Will re-execute until this many milliseconds are left in the slot',
59
- ...numberConfigHelper(6000),
60
- },
61
57
  alwaysReexecuteBlockProposals: {
62
- env: 'ALWAYS_REEXECUTE_BLOCK_PROPOSALS',
63
58
  description:
64
59
  'Whether to always reexecute block proposals, even for non-validator nodes (useful for monitoring network status).',
65
- ...booleanConfigHelper(false),
60
+ defaultValue: true,
66
61
  },
67
62
  fishermanMode: {
68
63
  env: 'FISHERMAN_MODE',
@@ -70,16 +65,15 @@ export const validatorClientConfigMappings: ConfigMappingsType<ValidatorClientCo
70
65
  'Whether to run in fisherman mode: validates all proposals and attestations but does not broadcast attestations or participate in consensus.',
71
66
  ...booleanConfigHelper(false),
72
67
  },
73
- // TODO(palla/mbps): Change default to false once checkpoint validation is stable
74
68
  skipCheckpointProposalValidation: {
75
- description: 'Skip checkpoint proposal validation and always attest (default: true)',
76
- defaultValue: true,
69
+ description: 'Skip checkpoint proposal validation and always attest (default: false)',
70
+ defaultValue: false,
77
71
  },
78
- // TODO(palla/mbps): Change default to false once block sync is stable
79
72
  skipPushProposedBlocksToArchiver: {
80
- description: 'Skip pushing re-executed blocks to archiver (default: true)',
81
- defaultValue: true,
73
+ description: 'Skip pushing re-executed blocks to archiver (default: false)',
74
+ defaultValue: false,
82
75
  },
76
+ ...validatorHASignerConfigMappings,
83
77
  };
84
78
 
85
79
  /**
@@ -1,3 +1,9 @@
1
+ import {
2
+ BlockNumber,
3
+ type CheckpointNumber,
4
+ IndexWithinCheckpoint,
5
+ type SlotNumber,
6
+ } from '@aztec/foundation/branded-types';
1
7
  import { Buffer32 } from '@aztec/foundation/buffer';
2
8
  import { keccak256 } from '@aztec/foundation/crypto/keccak';
3
9
  import { Fr } from '@aztec/foundation/curves/bn254';
@@ -18,6 +24,8 @@ import {
18
24
  } from '@aztec/stdlib/p2p';
19
25
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
20
26
  import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
27
+ import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validator-ha-signer/errors';
28
+ import { DutyType, type SigningContext } from '@aztec/validator-ha-signer/types';
21
29
 
22
30
  import type { ValidatorKeyStore } from '../key_store/interface.js';
23
31
 
@@ -31,34 +39,40 @@ export class ValidationService {
31
39
  * Create a block proposal with the given header, archive, and transactions
32
40
  *
33
41
  * @param blockHeader - The block header
34
- * @param indexWithinCheckpoint - Index of this block within the checkpoint (0-indexed)
42
+ * @param blockIndexWithinCheckpoint - The block index within checkpoint for HA signing context
35
43
  * @param inHash - Hash of L1 to L2 messages for this checkpoint
36
44
  * @param archive - The archive of the current block
37
- * @param txs - TxHash[] ordered list of transactions
45
+ * @param txs - Ordered list of transactions (Tx[])
46
+ * @param proposerAttesterAddress - The address of the proposer/attester, or undefined
38
47
  * @param options - Block proposal options (including broadcastInvalidBlockProposal for testing)
39
48
  *
40
49
  * @returns A block proposal signing the above information
50
+ * @throws DutyAlreadySignedError if HA signer indicates duty already signed by another node
51
+ * @throws SlashingProtectionError if attempting to sign different data for same slot
41
52
  */
42
53
  public createBlockProposal(
43
54
  blockHeader: BlockHeader,
44
- indexWithinCheckpoint: number,
55
+ blockIndexWithinCheckpoint: IndexWithinCheckpoint,
45
56
  inHash: Fr,
46
57
  archive: Fr,
47
58
  txs: Tx[],
48
59
  proposerAttesterAddress: EthAddress | undefined,
49
60
  options: BlockProposalOptions,
50
61
  ): Promise<BlockProposal> {
51
- const payloadSigner = this.getPayloadSigner(proposerAttesterAddress);
52
-
53
62
  // For testing: change the new archive to trigger state_mismatch validation failure
54
63
  if (options.broadcastInvalidBlockProposal) {
55
64
  archive = Fr.random();
56
65
  this.log.warn(`Creating INVALID block proposal for slot ${blockHeader.globalVariables.slotNumber}`);
57
66
  }
58
67
 
68
+ // Create a signer that uses the appropriate address
69
+ const address = proposerAttesterAddress ?? this.keyStore.getAddress(0);
70
+ const payloadSigner = (payload: Buffer32, context: SigningContext) =>
71
+ this.keyStore.signMessageWithAddress(address, payload, context);
72
+
59
73
  return BlockProposal.createProposalFromSigner(
60
74
  blockHeader,
61
- indexWithinCheckpoint,
75
+ blockIndexWithinCheckpoint,
62
76
  inHash,
63
77
  archive,
64
78
  txs.map(tx => tx.getTxHash()),
@@ -85,14 +99,18 @@ export class ValidationService {
85
99
  proposerAttesterAddress: EthAddress | undefined,
86
100
  options: CheckpointProposalOptions,
87
101
  ): Promise<CheckpointProposal> {
88
- const payloadSigner = this.getPayloadSigner(proposerAttesterAddress);
89
-
90
102
  // For testing: change the archive to trigger state_mismatch validation failure
91
103
  if (options.broadcastInvalidCheckpointProposal) {
92
104
  archive = Fr.random();
93
105
  this.log.warn(`Creating INVALID checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
94
106
  }
95
107
 
108
+ // Create a signer that takes payload and context, and uses the appropriate address
109
+ const payloadSigner = (payload: Buffer32, context: SigningContext) => {
110
+ const address = proposerAttesterAddress ?? this.keyStore.getAddress(0);
111
+ return this.keyStore.signMessageWithAddress(address, payload, context);
112
+ };
113
+
96
114
  // Last block to include in the proposal
97
115
  const lastBlock = lastBlockInfo && {
98
116
  blockHeader: lastBlockInfo.blockHeader,
@@ -104,16 +122,6 @@ export class ValidationService {
104
122
  return CheckpointProposal.createProposalFromSigner(checkpointHeader, archive, lastBlock, payloadSigner);
105
123
  }
106
124
 
107
- private getPayloadSigner(proposerAttesterAddress: EthAddress | undefined): (payload: Buffer32) => Promise<Signature> {
108
- if (proposerAttesterAddress !== undefined) {
109
- return (payload: Buffer32) => this.keyStore.signMessageWithAddress(proposerAttesterAddress, payload);
110
- } else {
111
- // if there is no proposer attester address, just use the first signer
112
- const signer = this.keyStore.getAddress(0);
113
- return (payload: Buffer32) => this.keyStore.signMessageWithAddress(signer, payload);
114
- }
115
- }
116
-
117
125
  /**
118
126
  * Attest with selection of validators to the given checkpoint proposal
119
127
  *
@@ -133,19 +141,79 @@ export class ValidationService {
133
141
  const buf = Buffer32.fromBuffer(
134
142
  keccak256(payload.getPayloadToSign(SignatureDomainSeparator.checkpointAttestation)),
135
143
  );
136
- const signatures = await Promise.all(
137
- attestors.map(attestor => this.keyStore.signMessageWithAddress(attestor, buf)),
144
+
145
+ // TODO(spy/ha): Use checkpointNumber instead of blockNumber once CheckpointHeader includes it.
146
+ // Currently using lastBlock.blockNumber as a proxy for checkpoint identification in HA signing.
147
+ // blockNumber is NOT used for the primary key so it's safe to use here.
148
+ // See CheckpointHeader TODO and SigningContext types documentation.
149
+ let blockNumber: BlockNumber;
150
+ try {
151
+ blockNumber = proposal.blockNumber;
152
+ } catch {
153
+ // Checkpoint proposal may not have lastBlock, use 0 as fallback
154
+ blockNumber = BlockNumber(0);
155
+ }
156
+ const context: SigningContext = {
157
+ slot: proposal.slotNumber,
158
+ blockNumber,
159
+ dutyType: DutyType.ATTESTATION,
160
+ };
161
+
162
+ // Sign each attestor in parallel, catching HA errors per-attestor
163
+ const results = await Promise.allSettled(
164
+ attestors.map(async attestor => {
165
+ const sig = await this.keyStore.signMessageWithAddress(attestor, buf, context);
166
+ // return new BlockAttestation(proposal.payload, sig, proposal.signature);
167
+ return new CheckpointAttestation(payload, sig, proposal.signature);
168
+ }),
138
169
  );
139
- return signatures.map(sig => new CheckpointAttestation(payload, sig, proposal.signature));
170
+
171
+ const attestations: CheckpointAttestation[] = [];
172
+ for (let i = 0; i < results.length; i++) {
173
+ const result = results[i];
174
+ if (result.status === 'fulfilled') {
175
+ attestations.push(result.value);
176
+ } else {
177
+ const error = result.reason;
178
+ if (error instanceof DutyAlreadySignedError || error instanceof SlashingProtectionError) {
179
+ this.log.info(
180
+ `Attestation for slot ${proposal.slotNumber} by ${attestors[i]} already signed by another High-Availability node`,
181
+ );
182
+ // Continue with remaining attestors
183
+ } else {
184
+ throw error;
185
+ }
186
+ }
187
+ }
188
+
189
+ return attestations;
140
190
  }
141
191
 
142
- async signAttestationsAndSigners(
192
+ /**
193
+ * Sign attestations and signers payload
194
+ * @param attestationsAndSigners - The attestations and signers to sign
195
+ * @param proposer - The proposer address to sign with
196
+ * @param slot - The slot number for HA signing context
197
+ * @param blockNumber - The block or checkpoint number for HA signing context
198
+ * @returns signature
199
+ * @throws DutyAlreadySignedError if already signed by another HA node
200
+ * @throws SlashingProtectionError if attempting to sign different data for same slot
201
+ */
202
+ signAttestationsAndSigners(
143
203
  attestationsAndSigners: CommitteeAttestationsAndSigners,
144
204
  proposer: EthAddress,
205
+ slot: SlotNumber,
206
+ blockNumber: BlockNumber | CheckpointNumber,
145
207
  ): Promise<Signature> {
208
+ const context: SigningContext = {
209
+ slot,
210
+ blockNumber,
211
+ dutyType: DutyType.ATTESTATIONS_AND_SIGNERS,
212
+ };
213
+
146
214
  const buf = Buffer32.fromBuffer(
147
215
  keccak256(attestationsAndSigners.getPayloadToSign(SignatureDomainSeparator.attestationsAndSigners)),
148
216
  );
149
- return await this.keyStore.signMessageWithAddress(proposer, buf);
217
+ return this.keyStore.signMessageWithAddress(proposer, buf, context);
150
218
  }
151
219
  }
package/src/factory.ts CHANGED
@@ -37,6 +37,7 @@ export function createBlockProposalHandler(
37
37
  deps.l1ToL2MessageSource,
38
38
  deps.p2pClient.getTxProvider(),
39
39
  blockProposalValidator,
40
+ deps.epochCache,
40
41
  config,
41
42
  metrics,
42
43
  deps.dateProvider,