@aztec/validator-client 0.87.7 → 1.0.0-nightly.20250604

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.
package/dest/validator.js CHANGED
@@ -1,11 +1,14 @@
1
1
  import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
2
2
  import { Buffer32 } from '@aztec/foundation/buffer';
3
+ import { Fr } from '@aztec/foundation/fields';
3
4
  import { createLogger } from '@aztec/foundation/log';
4
5
  import { RunningPromise } from '@aztec/foundation/running-promise';
5
6
  import { sleep } from '@aztec/foundation/sleep';
6
7
  import { DateProvider } from '@aztec/foundation/timer';
7
8
  import { TxCollector } from '@aztec/p2p';
8
9
  import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
10
+ import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
11
+ import { GlobalVariables } from '@aztec/stdlib/tx';
9
12
  import { WithTracer, getTelemetryClient } from '@aztec/telemetry-client';
10
13
  import { ValidationService } from './duties/validation_service.js';
11
14
  import { AttestationTimeoutError, BlockBuilderNotProvidedError, InvalidValidatorPrivateKeyError, ReExFailedTxsError, ReExStateMismatchError, ReExTimeoutError, TransactionsNotAvailableError } from './errors/validator.error.js';
@@ -14,6 +17,7 @@ import { ValidatorMetrics } from './metrics.js';
14
17
  /**
15
18
  * Validator Client
16
19
  */ export class ValidatorClient extends WithTracer {
20
+ blockBuilder;
17
21
  keyStore;
18
22
  epochCache;
19
23
  p2pClient;
@@ -25,34 +29,34 @@ import { ValidatorMetrics } from './metrics.js';
25
29
  metrics;
26
30
  // Used to check if we are sending the same proposal twice
27
31
  previousProposal;
28
- // Callback registered to: sequencer.buildBlock
29
- blockBuilder;
30
- myAddress;
32
+ myAddresses;
31
33
  lastEpoch;
32
34
  epochCacheUpdateLoop;
33
35
  blockProposalValidator;
34
36
  txCollector;
35
- constructor(keyStore, epochCache, p2pClient, blockSource, config, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
37
+ constructor(blockBuilder, keyStore, epochCache, p2pClient, blockSource, config, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
36
38
  // Instantiate tracer
37
- super(telemetry, 'Validator'), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.blockSource = blockSource, this.config = config, this.dateProvider = dateProvider, this.log = log, this.blockBuilder = undefined;
39
+ super(telemetry, 'Validator'), this.blockBuilder = blockBuilder, this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.blockSource = blockSource, this.config = config, this.dateProvider = dateProvider, this.log = log;
38
40
  this.metrics = new ValidatorMetrics(telemetry);
39
41
  this.validationService = new ValidationService(keyStore);
40
42
  this.blockProposalValidator = new BlockProposalValidator(epochCache);
41
43
  this.txCollector = new TxCollector(p2pClient, this.log);
42
44
  // Refresh epoch cache every second to trigger alert if participation in committee changes
43
- this.myAddress = this.keyStore.getAddress();
45
+ this.myAddresses = this.keyStore.getAddresses();
44
46
  this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), log, 1000);
45
- this.log.verbose(`Initialized validator with address ${this.keyStore.getAddress().toString()}`);
47
+ this.log.verbose(`Initialized validator with addresses: ${this.myAddresses.map((a)=>a.toString()).join(', ')}`);
46
48
  }
47
49
  async handleEpochCommitteeUpdate() {
48
50
  try {
49
51
  const { committee, epoch } = await this.epochCache.getCommittee('now');
50
52
  if (epoch !== this.lastEpoch) {
51
- const me = this.myAddress;
52
- if (committee.some((addr)=>addr.equals(me))) {
53
- this.log.info(`Validator ${me.toString()} is on the validator committee for epoch ${epoch}`);
53
+ const me = this.myAddresses;
54
+ const committeeSet = new Set(committee.map((v)=>v.toString()));
55
+ const inCommittee = me.filter((a)=>committeeSet.has(a.toString()));
56
+ if (inCommittee.length > 0) {
57
+ inCommittee.forEach((a)=>this.log.info(`Validator ${a.toString()} is on the validator committee for epoch ${epoch}`));
54
58
  } else {
55
- this.log.verbose(`Validator ${me.toString()} not on the validator committee for epoch ${epoch}`);
59
+ this.log.verbose(`Validators ${me.map((a)=>a.toString()).join(', ')} are not on the validator committee for epoch ${epoch}`);
56
60
  }
57
61
  this.lastEpoch = epoch;
58
62
  }
@@ -60,28 +64,30 @@ import { ValidatorMetrics } from './metrics.js';
60
64
  this.log.error(`Error updating epoch committee`, err);
61
65
  }
62
66
  }
63
- static new(config, epochCache, p2pClient, blockSource, dateProvider = new DateProvider(), telemetry = getTelemetryClient()) {
64
- if (!config.validatorPrivateKey) {
67
+ static new(config, blockBuilder, epochCache, p2pClient, blockSource, dateProvider = new DateProvider(), telemetry = getTelemetryClient()) {
68
+ if (!config.validatorPrivateKeys?.length) {
65
69
  throw new InvalidValidatorPrivateKeyError();
66
70
  }
67
- const privateKey = validatePrivateKey(config.validatorPrivateKey);
68
- const localKeyStore = new LocalKeyStore(privateKey);
69
- const validator = new ValidatorClient(localKeyStore, epochCache, p2pClient, blockSource, config, dateProvider, telemetry);
71
+ const privateKeys = config.validatorPrivateKeys.map(validatePrivateKey);
72
+ const localKeyStore = new LocalKeyStore(privateKeys);
73
+ const validator = new ValidatorClient(blockBuilder, localKeyStore, epochCache, p2pClient, blockSource, config, dateProvider, telemetry);
70
74
  validator.registerBlockProposalHandler();
71
75
  return validator;
72
76
  }
73
- getValidatorAddress() {
74
- return this.keyStore.getAddress();
77
+ getValidatorAddresses() {
78
+ return this.keyStore.getAddresses();
75
79
  }
76
80
  async start() {
77
81
  // Sync the committee from the smart contract
78
82
  // https://github.com/AztecProtocol/aztec-packages/issues/7962
79
- const me = this.keyStore.getAddress();
80
- const inCommittee = await this.epochCache.isInCommittee(me);
81
- if (inCommittee) {
82
- this.log.info(`Started validator with address ${me.toString()} in current validator committee`);
83
+ const myAddresses = this.keyStore.getAddresses();
84
+ const inCommittee = await this.epochCache.filterInCommittee(myAddresses);
85
+ if (inCommittee.length > 0) {
86
+ this.log.info(`Started validator with addresses in current validator committee:
87
+ ${inCommittee.map((a)=>a.toString()).join(', ')}`);
83
88
  } else {
84
- this.log.info(`Started validator with address ${me.toString()}`);
89
+ this.log.info(`Started validator with addresses:
90
+ ${myAddresses.map((a)=>a.toString()).join(', ')}`);
85
91
  }
86
92
  this.epochCacheUpdateLoop.start();
87
93
  return Promise.resolve();
@@ -95,13 +101,6 @@ import { ValidatorMetrics } from './metrics.js';
95
101
  };
96
102
  this.p2pClient.registerBlockProposalHandler(handler);
97
103
  }
98
- /**
99
- * Register a callback function for building a block
100
- *
101
- * We reuse the sequencer's block building functionality for re-execution
102
- */ registerBlockBuilder(blockBuilder) {
103
- this.blockBuilder = blockBuilder;
104
- }
105
104
  async attestToProposal(proposal, proposalSender) {
106
105
  const slotNumber = proposal.slotNumber.toNumber();
107
106
  const blockNumber = proposal.blockNumber.toNumber();
@@ -118,19 +117,19 @@ import { ValidatorMetrics } from './metrics.js';
118
117
  const invalidProposal = await this.blockProposalValidator.validate(proposal);
119
118
  if (invalidProposal) {
120
119
  this.log.verbose(`Proposal is not valid, skipping attestation`);
121
- this.metrics.incFailedAttestations('invalid_proposal');
120
+ this.metrics.incFailedAttestations(1, 'invalid_proposal');
122
121
  return undefined;
123
122
  }
124
123
  // Check that the parent proposal is a block we know, otherwise reexecution would fail.
125
124
  // Q: Should we move this to the block proposal validator? If there, then p2p would check it
126
- // before re-broadcasting it. This means that proposals built on top of an L1-reorgd-out block
125
+ // before re-broadcasting it. This means that proposals built on top of an L1-reorg'ed-out block
127
126
  // would not be rebroadcasted. But it also means that nodes that have not fully synced would
128
127
  // not rebroadcast the proposal.
129
128
  if (blockNumber > INITIAL_L2_BLOCK_NUM) {
130
129
  const parentBlock = await this.blockSource.getBlock(blockNumber - 1);
131
130
  if (parentBlock === undefined) {
132
131
  this.log.verbose(`Parent block for ${blockNumber} not found, skipping attestation`);
133
- this.metrics.incFailedAttestations('parent_block_not_found');
132
+ this.metrics.incFailedAttestations(1, 'parent_block_not_found');
134
133
  return undefined;
135
134
  }
136
135
  if (!proposal.payload.header.lastArchiveRoot.equals(parentBlock.archive.root)) {
@@ -139,15 +138,16 @@ import { ValidatorMetrics } from './metrics.js';
139
138
  parentBlockArchiveRoot: parentBlock.archive.root.toString(),
140
139
  ...proposalInfo
141
140
  });
142
- this.metrics.incFailedAttestations('parent_block_does_not_match');
141
+ this.metrics.incFailedAttestations(1, 'parent_block_does_not_match');
143
142
  return undefined;
144
143
  }
145
144
  }
146
145
  // Collect txs from the proposal
147
146
  const { missing, txs } = await this.txCollector.collectForBlockProposal(proposal, proposalSender);
148
- // Check that I am in the committee before attesting
149
- if (!await this.epochCache.isInCommittee(this.keyStore.getAddress())) {
150
- this.log.verbose(`Not in the committee, skipping attestation`);
147
+ // Check that I have any address in current committee before attesting
148
+ const inCommittee = await this.epochCache.filterInCommittee(this.keyStore.getAddresses());
149
+ if (inCommittee.length === 0) {
150
+ this.log.verbose(`No validator in the committee, skipping attestation`);
151
151
  return undefined;
152
152
  }
153
153
  // Check that all of the transactions in the proposal are available in the tx pool before attesting
@@ -156,7 +156,7 @@ import { ValidatorMetrics } from './metrics.js';
156
156
  proposalInfo,
157
157
  missing
158
158
  });
159
- this.metrics.incFailedAttestations('TransactionsNotAvailableError');
159
+ this.metrics.incFailedAttestations(1, 'TransactionsNotAvailableError');
160
160
  return undefined;
161
161
  }
162
162
  // Try re-executing the transactions in the proposal
@@ -167,14 +167,20 @@ import { ValidatorMetrics } from './metrics.js';
167
167
  await this.reExecuteTransactions(proposal, txs);
168
168
  }
169
169
  } catch (error) {
170
- this.metrics.incFailedAttestations(error instanceof Error ? error.name : 'unknown');
170
+ this.metrics.incFailedAttestations(1, error instanceof Error ? error.name : 'unknown');
171
171
  this.log.error(`Failed to attest to proposal`, error, proposalInfo);
172
172
  return undefined;
173
173
  }
174
174
  // Provided all of the above checks pass, we can attest to the proposal
175
175
  this.log.info(`Attesting to proposal for slot ${slotNumber}`, proposalInfo);
176
- this.metrics.incAttestations();
177
- return this.doAttestToProposal(proposal);
176
+ this.metrics.incAttestations(inCommittee.length);
177
+ // If the above function does not throw an error, then we can attest to the proposal
178
+ return this.doAttestToProposal(proposal, inCommittee);
179
+ }
180
+ getReexecutionDeadline(proposal, config) {
181
+ const nextSlotTimestampSeconds = Number(getTimestampForSlot(proposal.slotNumber.toBigInt() + 1n, config));
182
+ const msNeededForPropagationAndPublishing = this.config.validatorReexecuteDeadlineMs;
183
+ return new Date(nextSlotTimestampSeconds * 1000 - msNeededForPropagationAndPublishing);
178
184
  }
179
185
  /**
180
186
  * Re-execute the transactions in the proposal and check that the state updates match the header state
@@ -193,11 +199,20 @@ import { ValidatorMetrics } from './metrics.js';
193
199
  }
194
200
  // Use the sequencer's block building logic to re-execute the transactions
195
201
  const stopTimer = this.metrics.reExecutionTimer();
196
- const { block, numFailedTxs } = await this.blockBuilder(proposal.blockNumber, header, txs, {
197
- validateOnly: true
202
+ const config = this.blockBuilder.getConfig();
203
+ const globalVariables = GlobalVariables.from({
204
+ ...proposal.payload.header,
205
+ blockNumber: proposal.blockNumber,
206
+ timestamp: new Fr(header.timestamp),
207
+ chainId: new Fr(config.l1ChainId),
208
+ version: new Fr(config.rollupVersion)
209
+ });
210
+ const { block, failedTxs } = await this.blockBuilder.buildBlock(txs, globalVariables, {
211
+ deadline: this.getReexecutionDeadline(proposal, config)
198
212
  });
199
213
  stopTimer();
200
214
  this.log.verbose(`Transaction re-execution complete`);
215
+ const numFailedTxs = failedTxs.length;
201
216
  if (numFailedTxs > 0) {
202
217
  this.metrics.recordFailedReexecution(proposal);
203
218
  throw new ReExFailedTxsError(numFailedTxs);
@@ -212,19 +227,18 @@ import { ValidatorMetrics } from './metrics.js';
212
227
  throw new ReExStateMismatchError();
213
228
  }
214
229
  }
215
- async createBlockProposal(blockNumber, header, archive, stateReference, txs, options) {
230
+ async createBlockProposal(blockNumber, header, archive, stateReference, txs, proposerAddress, options) {
216
231
  if (this.previousProposal?.slotNumber.equals(header.slotNumber)) {
217
232
  this.log.verbose(`Already made a proposal for the same slot, skipping proposal`);
218
233
  return Promise.resolve(undefined);
219
234
  }
220
- const newProposal = await this.validationService.createBlockProposal(blockNumber, header, archive, stateReference, txs, options);
235
+ const newProposal = await this.validationService.createBlockProposal(blockNumber, header, archive, stateReference, txs, proposerAddress, options);
221
236
  this.previousProposal = newProposal;
222
237
  return newProposal;
223
238
  }
224
239
  async broadcastBlockProposal(proposal) {
225
240
  await this.p2pClient.broadcastProposal(proposal);
226
241
  }
227
- // TODO(https://github.com/AztecProtocol/aztec-packages/issues/7962)
228
242
  async collectAttestations(proposal, required, deadline) {
229
243
  // Wait and poll the p2pClient's attestation pool for this block until we have enough attestations
230
244
  const slot = proposal.payload.header.slotNumber.toBigInt();
@@ -234,15 +248,17 @@ import { ValidatorMetrics } from './metrics.js';
234
248
  throw new AttestationTimeoutError(required, slot);
235
249
  }
236
250
  const proposalId = proposal.archive.toString();
237
- await this.doAttestToProposal(proposal);
238
- const me = this.keyStore.getAddress();
251
+ // adds attestations for all of my addresses locally
252
+ const inCommittee = await this.epochCache.filterInCommittee(this.keyStore.getAddresses());
253
+ await this.doAttestToProposal(proposal, inCommittee);
254
+ const myAddresses = this.keyStore.getAddresses();
239
255
  let attestations = [];
240
256
  while(true){
241
257
  const collectedAttestations = await this.p2pClient.getAttestationsForSlot(slot, proposalId);
242
258
  const oldSenders = attestations.map((attestation)=>attestation.getSender());
243
259
  for (const collected of collectedAttestations){
244
260
  const collectedSender = collected.getSender();
245
- if (!collectedSender.equals(me) && !oldSenders.some((sender)=>sender.equals(collectedSender))) {
261
+ if (!myAddresses.some((address)=>address.equals(collectedSender)) && !oldSenders.some((sender)=>sender.equals(collectedSender))) {
246
262
  this.log.debug(`Received attestation for slot ${slot} from ${collectedSender.toString()}`);
247
263
  }
248
264
  }
@@ -259,10 +275,10 @@ import { ValidatorMetrics } from './metrics.js';
259
275
  await sleep(this.config.attestationPollingIntervalMs);
260
276
  }
261
277
  }
262
- async doAttestToProposal(proposal) {
263
- const attestation = await this.validationService.attestToProposal(proposal);
264
- await this.p2pClient.addAttestation(attestation);
265
- return attestation;
278
+ async doAttestToProposal(proposal, attestors = []) {
279
+ const attestations = await this.validationService.attestToProposal(proposal, attestors);
280
+ await this.p2pClient.addAttestations(attestations);
281
+ return attestations;
266
282
  }
267
283
  }
268
284
  function validatePrivateKey(privateKey) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/validator-client",
3
- "version": "0.87.7",
3
+ "version": "1.0.0-nightly.20250604",
4
4
  "main": "dest/index.js",
5
5
  "type": "module",
6
6
  "exports": {
@@ -60,13 +60,13 @@
60
60
  ]
61
61
  },
62
62
  "dependencies": {
63
- "@aztec/constants": "0.87.7",
64
- "@aztec/epoch-cache": "0.87.7",
65
- "@aztec/ethereum": "0.87.7",
66
- "@aztec/foundation": "0.87.7",
67
- "@aztec/p2p": "0.87.7",
68
- "@aztec/stdlib": "0.87.7",
69
- "@aztec/telemetry-client": "0.87.7",
63
+ "@aztec/constants": "1.0.0-nightly.20250604",
64
+ "@aztec/epoch-cache": "1.0.0-nightly.20250604",
65
+ "@aztec/ethereum": "1.0.0-nightly.20250604",
66
+ "@aztec/foundation": "1.0.0-nightly.20250604",
67
+ "@aztec/p2p": "1.0.0-nightly.20250604",
68
+ "@aztec/stdlib": "1.0.0-nightly.20250604",
69
+ "@aztec/telemetry-client": "1.0.0-nightly.20250604",
70
70
  "koa": "^2.16.1",
71
71
  "koa-router": "^12.0.0",
72
72
  "tslib": "^2.4.0",
package/src/config.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { NULL_KEY } from '@aztec/ethereum';
2
1
  import {
3
2
  type ConfigMappingsType,
4
3
  booleanConfigHelper,
@@ -10,8 +9,8 @@ import {
10
9
  * The Validator Configuration
11
10
  */
12
11
  export interface ValidatorClientConfig {
13
- /** The private key of the validator participating in attestation duties */
14
- validatorPrivateKey?: string;
12
+ /** The private keys of the validators participating in attestation duties */
13
+ validatorPrivateKeys?: `0x${string}`[];
15
14
 
16
15
  /** Do not run the validator */
17
16
  disableValidator: boolean;
@@ -21,13 +20,16 @@ export interface ValidatorClientConfig {
21
20
 
22
21
  /** Re-execute transactions before attesting */
23
22
  validatorReexecute: boolean;
23
+
24
+ /** Will re-execute until this many milliseconds are left in the slot */
25
+ validatorReexecuteDeadlineMs: number;
24
26
  }
25
27
 
26
28
  export const validatorClientConfigMappings: ConfigMappingsType<ValidatorClientConfig> = {
27
- validatorPrivateKey: {
28
- env: 'VALIDATOR_PRIVATE_KEY',
29
- parseEnv: (val: string) => (val ? `0x${val.replace('0x', '')}` : NULL_KEY),
30
- description: 'The private key of the validator participating in attestation duties',
29
+ validatorPrivateKeys: {
30
+ env: 'VALIDATOR_PRIVATE_KEYS',
31
+ parseEnv: (val: string) => val.split(',').map(key => `0x${key.replace('0x', '')}`),
32
+ description: 'List of private keys of the validators participating in attestation duties',
31
33
  },
32
34
  disableValidator: {
33
35
  env: 'VALIDATOR_DISABLED',
@@ -44,6 +46,11 @@ export const validatorClientConfigMappings: ConfigMappingsType<ValidatorClientCo
44
46
  description: 'Re-execute transactions before attesting',
45
47
  ...booleanConfigHelper(true),
46
48
  },
49
+ validatorReexecuteDeadlineMs: {
50
+ env: 'VALIDATOR_REEXECUTE_DEADLINE_MS',
51
+ description: 'Will re-execute until this many milliseconds are left in the slot',
52
+ ...numberConfigHelper(6000),
53
+ },
47
54
  };
48
55
 
49
56
  /**
@@ -1,5 +1,6 @@
1
1
  import { Buffer32 } from '@aztec/foundation/buffer';
2
2
  import { keccak256 } from '@aztec/foundation/crypto';
3
+ import type { EthAddress } from '@aztec/foundation/eth-address';
3
4
  import type { Fr } from '@aztec/foundation/fields';
4
5
  import {
5
6
  BlockAttestation,
@@ -31,9 +32,10 @@ export class ValidationService {
31
32
  archive: Fr,
32
33
  stateReference: StateReference,
33
34
  txs: Tx[],
35
+ proposerAttesterAddress: EthAddress,
34
36
  options: BlockProposalOptions,
35
37
  ): Promise<BlockProposal> {
36
- const payloadSigner = (payload: Buffer32) => this.keyStore.signMessage(payload);
38
+ const payloadSigner = (payload: Buffer32) => this.keyStore.signMessageWithAddress(proposerAttesterAddress, payload);
37
39
  // TODO: check if this is calculated earlier / can not be recomputed
38
40
  const txHashes = await Promise.all(txs.map(tx => tx.getTxHash()));
39
41
 
@@ -46,21 +48,23 @@ export class ValidationService {
46
48
  }
47
49
 
48
50
  /**
49
- * Attest to the given block proposal constructed by the current sequencer
51
+ * Attest with selection of validators to the given block proposal, constructed by the current sequencer
50
52
  *
51
53
  * NOTE: This is just a blind signing.
52
54
  * We assume that the proposal is valid and DA guarantees have been checked previously.
53
55
  *
54
56
  * @param proposal - The proposal to attest to
55
- * @returns attestation
57
+ * @param attestors - The validators to attest with
58
+ * @returns attestations
56
59
  */
57
- async attestToProposal(proposal: BlockProposal): Promise<BlockAttestation> {
58
- // TODO(https://github.com/AztecProtocol/aztec-packages/issues/7961): check that the current validator is correct
59
-
60
+ async attestToProposal(proposal: BlockProposal, attestors: EthAddress[]): Promise<BlockAttestation[]> {
60
61
  const buf = Buffer32.fromBuffer(
61
62
  keccak256(proposal.payload.getPayloadToSign(SignatureDomainSeparator.blockAttestation)),
62
63
  );
63
- const sig = await this.keyStore.signMessage(buf);
64
- return new BlockAttestation(proposal.blockNumber, proposal.payload, sig);
64
+ const signatures = await Promise.all(
65
+ attestors.map(attestor => this.keyStore.signMessageWithAddress(attestor, buf)),
66
+ );
67
+ //await this.keyStore.signMessage(buf);
68
+ return signatures.map(sig => new BlockAttestation(proposal.blockNumber, proposal.payload, sig));
65
69
  }
66
70
  }
package/src/factory.ts CHANGED
@@ -2,6 +2,7 @@ import type { EpochCache } from '@aztec/epoch-cache';
2
2
  import type { DateProvider } from '@aztec/foundation/timer';
3
3
  import type { P2P } from '@aztec/p2p';
4
4
  import type { L2BlockSource } from '@aztec/stdlib/block';
5
+ import type { IFullNodeBlockBuilder } from '@aztec/stdlib/interfaces/server';
5
6
  import type { TelemetryClient } from '@aztec/telemetry-client';
6
7
 
7
8
  import { generatePrivateKey } from 'viem/accounts';
@@ -12,6 +13,7 @@ import { ValidatorClient } from './validator.js';
12
13
  export function createValidatorClient(
13
14
  config: ValidatorClientConfig,
14
15
  deps: {
16
+ blockBuilder: IFullNodeBlockBuilder;
15
17
  p2pClient: P2P;
16
18
  blockSource: L2BlockSource;
17
19
  telemetry: TelemetryClient;
@@ -22,12 +24,13 @@ export function createValidatorClient(
22
24
  if (config.disableValidator) {
23
25
  return undefined;
24
26
  }
25
- if (config.validatorPrivateKey === undefined || config.validatorPrivateKey === '') {
26
- config.validatorPrivateKey = generatePrivateKey();
27
+ if (config.validatorPrivateKeys === undefined || !config.validatorPrivateKeys?.length) {
28
+ config.validatorPrivateKeys = [generatePrivateKey()];
27
29
  }
28
30
 
29
31
  return ValidatorClient.new(
30
32
  config,
33
+ deps.blockBuilder,
31
34
  deps.epochCache,
32
35
  deps.p2pClient,
33
36
  deps.blockSource,
@@ -8,19 +8,29 @@ import type { Signature } from '@aztec/foundation/eth-signature';
8
8
  */
9
9
  export interface ValidatorKeyStore {
10
10
  /**
11
- * Get the address of the signer
11
+ * Get the address of a signer by index
12
12
  *
13
+ * @param index - The index of the signer
13
14
  * @returns the address
14
15
  */
15
- getAddress(): EthAddress;
16
+ getAddress(index: number): EthAddress;
16
17
 
17
- sign(message: Buffer32): Promise<Signature>;
18
+ /**
19
+ * Get all addresses
20
+ *
21
+ * @returns all addresses
22
+ */
23
+ getAddresses(): EthAddress[];
24
+
25
+ sign(message: Buffer32): Promise<Signature[]>;
26
+ signWithAddress(address: EthAddress, message: Buffer32): Promise<Signature>;
18
27
  /**
19
28
  * Flavor of sign message that followed EIP-712 eth signed message prefix
20
29
  * Note: this is only required when we are using ecdsa signatures over secp256k1
21
30
  *
22
31
  * @param message - The message to sign.
23
- * @returns The signature.
32
+ * @returns The signatures.
24
33
  */
25
- signMessage(message: Buffer32): Promise<Signature>;
34
+ signMessage(message: Buffer32): Promise<Signature[]>;
35
+ signMessageWithAddress(address: EthAddress, message: Buffer32): Promise<Signature>;
26
36
  }
@@ -8,39 +8,85 @@ import type { ValidatorKeyStore } from './interface.js';
8
8
  /**
9
9
  * Local Key Store
10
10
  *
11
- * An implementation of the Key store using an in memory private key.
11
+ * An implementation of the Key store using in memory private keys.
12
12
  */
13
13
  export class LocalKeyStore implements ValidatorKeyStore {
14
- private signer: Secp256k1Signer;
14
+ private signers: Secp256k1Signer[];
15
+ private signersByAddress: Map<`0x${string}`, Secp256k1Signer>;
15
16
 
16
- constructor(privateKey: Buffer32) {
17
- this.signer = new Secp256k1Signer(privateKey);
17
+ constructor(privateKeys: Buffer32[]) {
18
+ this.signers = privateKeys.map(privateKey => new Secp256k1Signer(privateKey));
19
+ this.signersByAddress = new Map(this.signers.map(signer => [signer.address.toString(), signer]));
18
20
  }
19
21
 
20
22
  /**
21
- * Get the address of the signer
23
+ * Get the address of a signer by index
22
24
  *
25
+ * @param index - The index of the signer
23
26
  * @returns the address
24
27
  */
25
- public getAddress(): EthAddress {
26
- return this.signer.address;
28
+ public getAddress(index: number): EthAddress {
29
+ if (index >= this.signers.length) {
30
+ throw new Error(`Index ${index} is out of bounds.`);
31
+ }
32
+ return this.signers[index].address;
27
33
  }
28
34
 
29
35
  /**
30
- * Sign a message with the keystore private key
36
+ * Get the addresses of all signers
31
37
  *
32
- * @param messageBuffer - The message buffer to sign
38
+ * @returns the addresses
39
+ */
40
+ public getAddresses(): EthAddress[] {
41
+ return this.signers.map(signer => signer.address);
42
+ }
43
+
44
+ /**
45
+ * Sign a message with all keystore private keys
46
+ * @param digest - The message buffer to sign
33
47
  * @return signature
34
48
  */
35
- public sign(digest: Buffer32): Promise<Signature> {
36
- const signature = this.signer.sign(digest);
49
+ public sign(digest: Buffer32): Promise<Signature[]> {
50
+ return Promise.all(this.signers.map(signer => signer.sign(digest)));
51
+ }
52
+
53
+ /**
54
+ * Sign a message with a specific address's private key
55
+ * @param address - The address of the signer to use
56
+ * @param digest - The message buffer to sign
57
+ * @returns signature for the specified address
58
+ * @throws Error if the address is not found in the keystore
59
+ */
60
+ public signWithAddress(address: EthAddress, digest: Buffer32): Promise<Signature> {
61
+ const signer = this.signersByAddress.get(address.toString());
62
+ if (!signer) {
63
+ throw new Error(`No signer found for address ${address.toString()}`);
64
+ }
65
+ return Promise.resolve(signer.sign(digest));
66
+ }
37
67
 
38
- return Promise.resolve(signature);
68
+ /**
69
+ * Sign a message with all keystore private keys
70
+ *
71
+ * @param message - The message to sign
72
+ * @return signatures
73
+ */
74
+ public signMessage(message: Buffer32): Promise<Signature[]> {
75
+ return Promise.all(this.signers.map(signer => signer.signMessage(message)));
39
76
  }
40
77
 
41
- public signMessage(message: Buffer32): Promise<Signature> {
42
- // Sign message adds eth sign prefix and hashes before signing
43
- const signature = this.signer.signMessage(message);
44
- return Promise.resolve(signature);
78
+ /**
79
+ * Sign a message with a specific address's private key
80
+ * @param address - The address of the signer to use
81
+ * @param message - The message to sign
82
+ * @returns signature for the specified address
83
+ * @throws Error if the address is not found in the keystore
84
+ */
85
+ public signMessageWithAddress(address: EthAddress, message: Buffer32): Promise<Signature> {
86
+ const signer = this.signersByAddress.get(address.toString());
87
+ if (!signer) {
88
+ throw new Error(`No signer found for address ${address.toString()}`);
89
+ }
90
+ return Promise.resolve(signer.signMessage(message));
45
91
  }
46
92
  }
package/src/metrics.ts CHANGED
@@ -59,12 +59,12 @@ export class ValidatorMetrics {
59
59
  });
60
60
  }
61
61
 
62
- public incAttestations() {
63
- this.attestationsCount.add(1);
62
+ public incAttestations(num: number) {
63
+ this.attestationsCount.add(num);
64
64
  }
65
65
 
66
- public incFailedAttestations(reason: string) {
67
- this.failedAttestationsCount.add(1, {
66
+ public incFailedAttestations(num: number, reason: string) {
67
+ this.failedAttestationsCount.add(num, {
68
68
  [Attributes.ERROR_TYPE]: reason,
69
69
  });
70
70
  }