@aztec/validator-client 0.0.1-commit.6d3c34e → 0.0.1-commit.9372f48

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,9 +1,9 @@
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
- import { DateProvider, Timer, elapsed } from '@aztec/foundation/timer';
6
+ import { DateProvider, elapsed } from '@aztec/foundation/timer';
7
7
  import { getDefaultAllowedSetupFunctions } from '@aztec/p2p/msg_validators';
8
8
  import { LightweightCheckpointBuilder } from '@aztec/prover-client/light';
9
9
  import {
@@ -12,39 +12,38 @@ 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,
27
+ NoValidTxsError,
23
28
  type PublicProcessorLimits,
29
+ type WorldStateSynchronizer,
24
30
  } from '@aztec/stdlib/interfaces/server';
25
31
  import { MerkleTreeId } from '@aztec/stdlib/trees';
26
- import { type CheckpointGlobalVariables, type FailedTx, GlobalVariables, StateReference, Tx } from '@aztec/stdlib/tx';
32
+ import { type CheckpointGlobalVariables, GlobalVariables, StateReference, Tx } from '@aztec/stdlib/tx';
27
33
  import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
28
34
 
29
35
  import { createValidatorForBlockBuilding } from './tx_validator/tx_validator_factory.js';
30
36
 
31
- const log = createLogger('checkpoint-builder');
32
-
33
- export interface BuildBlockInCheckpointResult {
34
- block: L2BlockNew;
35
- publicGas: Gas;
36
- publicProcessorDuration: number;
37
- numTxs: number;
38
- failedTxs: FailedTx[];
39
- blockBuildingTimer: Timer;
40
- usedTxs: Tx[];
41
- }
37
+ // Re-export for backward compatibility
38
+ export type { BuildBlockInCheckpointResult } from '@aztec/stdlib/interfaces/server';
42
39
 
43
40
  /**
44
41
  * Builder for a single checkpoint. Handles building blocks within the checkpoint
45
42
  * and completing it.
46
43
  */
47
- export class CheckpointBuilder {
44
+ export class CheckpointBuilder implements ICheckpointBlockBuilder {
45
+ private log: Logger;
46
+
48
47
  constructor(
49
48
  private checkpointBuilder: LightweightCheckpointBuilder,
50
49
  private fork: MerkleTreeWriteOperations,
@@ -52,7 +51,13 @@ export class CheckpointBuilder {
52
51
  private contractDataSource: ContractDataSource,
53
52
  private dateProvider: DateProvider,
54
53
  private telemetryClient: TelemetryClient,
55
- ) {}
54
+ bindings?: LoggerBindings,
55
+ ) {
56
+ this.log = createLogger('checkpoint-builder', {
57
+ ...bindings,
58
+ instanceId: `checkpoint-${checkpointBuilder.checkpointNumber}`,
59
+ });
60
+ }
56
61
 
57
62
  getConstantData(): CheckpointGlobalVariables {
58
63
  return this.checkpointBuilder.constants;
@@ -65,12 +70,16 @@ export class CheckpointBuilder {
65
70
  pendingTxs: Iterable<Tx> | AsyncIterable<Tx>,
66
71
  blockNumber: BlockNumber,
67
72
  timestamp: bigint,
68
- opts: PublicProcessorLimits & { expectedEndState?: StateReference },
73
+ opts: PublicProcessorLimits & { expectedEndState?: StateReference } = {},
69
74
  ): Promise<BuildBlockInCheckpointResult> {
70
- const blockBuildingTimer = new Timer();
71
75
  const slot = this.checkpointBuilder.constants.slotNumber;
72
76
 
73
- log.verbose(`Building block ${blockNumber} for slot ${slot} within checkpoint`, { slot, blockNumber, ...opts });
77
+ this.log.verbose(`Building block ${blockNumber} for slot ${slot} within checkpoint`, {
78
+ slot,
79
+ blockNumber,
80
+ ...opts,
81
+ currentTime: new Date(this.dateProvider.now()),
82
+ });
74
83
 
75
84
  const constants = this.checkpointBuilder.constants;
76
85
  const globalVariables = GlobalVariables.from({
@@ -85,10 +94,16 @@ export class CheckpointBuilder {
85
94
  });
86
95
  const { processor, validator } = await this.makeBlockBuilderDeps(globalVariables, this.fork);
87
96
 
88
- const [publicProcessorDuration, [processedTxs, failedTxs, usedTxs]] = await elapsed(() =>
97
+ const [publicProcessorDuration, [processedTxs, failedTxs, usedTxs, _, usedTxBlobFields]] = await elapsed(() =>
89
98
  processor.process(pendingTxs, opts, validator),
90
99
  );
91
100
 
101
+ // Throw if we didn't collect a single valid tx and we're not allowed to build empty blocks
102
+ // (only the first block in a checkpoint can be empty)
103
+ if (processedTxs.length === 0 && this.checkpointBuilder.getBlockCount() > 0) {
104
+ throw new NoValidTxsError(failedTxs);
105
+ }
106
+
92
107
  // Add block to checkpoint
93
108
  const block = await this.checkpointBuilder.addBlock(globalVariables, processedTxs, {
94
109
  expectedEndState: opts.expectedEndState,
@@ -97,24 +112,28 @@ export class CheckpointBuilder {
97
112
  // How much public gas was processed
98
113
  const publicGas = processedTxs.reduce((acc, tx) => acc.add(tx.gasUsed.publicGas), Gas.empty());
99
114
 
100
- const res = {
115
+ this.log.debug('Built block within checkpoint', {
116
+ header: block.header.toInspect(),
117
+ processedTxs: processedTxs.map(tx => tx.hash.toString()),
118
+ failedTxs: failedTxs.map(tx => tx.tx.txHash.toString()),
119
+ });
120
+
121
+ return {
101
122
  block,
102
123
  publicGas,
103
124
  publicProcessorDuration,
104
125
  numTxs: processedTxs.length,
105
126
  failedTxs,
106
- blockBuildingTimer,
107
127
  usedTxs,
128
+ usedTxBlobFields,
108
129
  };
109
- log.debug('Built block within checkpoint', res.block.header);
110
- return res;
111
130
  }
112
131
 
113
132
  /** Completes the checkpoint and returns it. */
114
133
  async completeCheckpoint(): Promise<Checkpoint> {
115
134
  const checkpoint = await this.checkpointBuilder.completeCheckpoint();
116
135
 
117
- log.verbose(`Completed checkpoint ${checkpoint.number}`, {
136
+ this.log.verbose(`Completed checkpoint ${checkpoint.number}`, {
118
137
  checkpointNumber: checkpoint.number,
119
138
  numBlocks: checkpoint.blocks.length,
120
139
  archiveRoot: checkpoint.archive.root.toString(),
@@ -130,14 +149,16 @@ export class CheckpointBuilder {
130
149
 
131
150
  protected async makeBlockBuilderDeps(globalVariables: GlobalVariables, fork: MerkleTreeWriteOperations) {
132
151
  const txPublicSetupAllowList = this.config.txPublicSetupAllowList ?? (await getDefaultAllowedSetupFunctions());
133
- const contractsDB = new PublicContractsDB(this.contractDataSource);
152
+ const contractsDB = new PublicContractsDB(this.contractDataSource, this.log.getBindings());
134
153
  const guardedFork = new GuardedMerkleTreeOperations(fork);
135
154
 
155
+ const bindings = this.log.getBindings();
136
156
  const publicTxSimulator = createPublicTxSimulatorForBlockBuilding(
137
157
  guardedFork,
138
158
  contractsDB,
139
159
  globalVariables,
140
160
  this.telemetryClient,
161
+ bindings,
141
162
  );
142
163
 
143
164
  const processor = new PublicProcessor(
@@ -147,7 +168,7 @@ export class CheckpointBuilder {
147
168
  publicTxSimulator,
148
169
  this.dateProvider,
149
170
  this.telemetryClient,
150
- undefined,
171
+ createLogger('simulator:public-processor', bindings),
151
172
  this.config,
152
173
  );
153
174
 
@@ -156,6 +177,7 @@ export class CheckpointBuilder {
156
177
  this.contractDataSource,
157
178
  globalVariables,
158
179
  txPublicSetupAllowList,
180
+ this.log.getBindings(),
159
181
  );
160
182
 
161
183
  return {
@@ -165,16 +187,19 @@ export class CheckpointBuilder {
165
187
  }
166
188
  }
167
189
 
168
- /**
169
- * Factory for creating checkpoint builders.
170
- */
171
- export class FullNodeCheckpointsBuilder {
190
+ /** Factory for creating checkpoint builders. */
191
+ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder {
192
+ private log: Logger;
193
+
172
194
  constructor(
173
- private config: FullNodeBlockBuilderConfig,
195
+ private config: FullNodeBlockBuilderConfig & Pick<L1RollupConstants, 'l1GenesisTime' | 'slotDuration'>,
196
+ private worldState: WorldStateSynchronizer,
174
197
  private contractDataSource: ContractDataSource,
175
198
  private dateProvider: DateProvider,
176
199
  private telemetryClient: TelemetryClient = getTelemetryClient(),
177
- ) {}
200
+ ) {
201
+ this.log = createLogger('checkpoint-builder');
202
+ }
178
203
 
179
204
  public getConfig(): FullNodeBlockBuilderConfig {
180
205
  return this.config;
@@ -191,12 +216,14 @@ export class FullNodeCheckpointsBuilder {
191
216
  checkpointNumber: CheckpointNumber,
192
217
  constants: CheckpointGlobalVariables,
193
218
  l1ToL2Messages: Fr[],
219
+ previousCheckpointOutHashes: Fr[],
194
220
  fork: MerkleTreeWriteOperations,
221
+ bindings?: LoggerBindings,
195
222
  ): Promise<CheckpointBuilder> {
196
223
  const stateReference = await fork.getStateReference();
197
224
  const archiveTree = await fork.getTreeInfo(MerkleTreeId.ARCHIVE);
198
225
 
199
- log.verbose(`Building new checkpoint ${checkpointNumber}`, {
226
+ this.log.verbose(`Building new checkpoint ${checkpointNumber}`, {
200
227
  checkpointNumber,
201
228
  msgCount: l1ToL2Messages.length,
202
229
  initialStateReference: stateReference.toInspect(),
@@ -208,7 +235,9 @@ export class FullNodeCheckpointsBuilder {
208
235
  checkpointNumber,
209
236
  constants,
210
237
  l1ToL2Messages,
238
+ previousCheckpointOutHashes,
211
239
  fork,
240
+ bindings,
212
241
  );
213
242
 
214
243
  return new CheckpointBuilder(
@@ -218,6 +247,7 @@ export class FullNodeCheckpointsBuilder {
218
247
  this.contractDataSource,
219
248
  this.dateProvider,
220
249
  this.telemetryClient,
250
+ bindings,
221
251
  );
222
252
  }
223
253
 
@@ -228,17 +258,26 @@ export class FullNodeCheckpointsBuilder {
228
258
  checkpointNumber: CheckpointNumber,
229
259
  constants: CheckpointGlobalVariables,
230
260
  l1ToL2Messages: Fr[],
261
+ previousCheckpointOutHashes: Fr[],
231
262
  fork: MerkleTreeWriteOperations,
232
- existingBlocks: L2BlockNew[] = [],
263
+ existingBlocks: L2Block[] = [],
264
+ bindings?: LoggerBindings,
233
265
  ): Promise<CheckpointBuilder> {
234
266
  const stateReference = await fork.getStateReference();
235
267
  const archiveTree = await fork.getTreeInfo(MerkleTreeId.ARCHIVE);
236
268
 
237
269
  if (existingBlocks.length === 0) {
238
- return this.startCheckpoint(checkpointNumber, constants, l1ToL2Messages, fork);
270
+ return this.startCheckpoint(
271
+ checkpointNumber,
272
+ constants,
273
+ l1ToL2Messages,
274
+ previousCheckpointOutHashes,
275
+ fork,
276
+ bindings,
277
+ );
239
278
  }
240
279
 
241
- log.verbose(`Resuming checkpoint ${checkpointNumber} with ${existingBlocks.length} existing blocks`, {
280
+ this.log.verbose(`Resuming checkpoint ${checkpointNumber} with ${existingBlocks.length} existing blocks`, {
242
281
  checkpointNumber,
243
282
  msgCount: l1ToL2Messages.length,
244
283
  existingBlockCount: existingBlocks.length,
@@ -251,8 +290,10 @@ export class FullNodeCheckpointsBuilder {
251
290
  checkpointNumber,
252
291
  constants,
253
292
  l1ToL2Messages,
293
+ previousCheckpointOutHashes,
254
294
  fork,
255
295
  existingBlocks,
296
+ bindings,
256
297
  );
257
298
 
258
299
  return new CheckpointBuilder(
@@ -262,6 +303,12 @@ export class FullNodeCheckpointsBuilder {
262
303
  this.contractDataSource,
263
304
  this.dateProvider,
264
305
  this.telemetryClient,
306
+ bindings,
265
307
  );
266
308
  }
309
+
310
+ /** Returns a fork of the world state at the given block number. */
311
+ getFork(blockNumber: BlockNumber): Promise<MerkleTreeWriteOperations> {
312
+ return this.worldState.fork(blockNumber);
313
+ }
267
314
  }
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,