@aztec/validator-client 1.2.1 → 2.0.0-nightly.20250813

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.
@@ -0,0 +1,195 @@
1
+ import type { Buffer32 } from '@aztec/foundation/buffer';
2
+ import { EthAddress } from '@aztec/foundation/eth-address';
3
+ import { Signature } from '@aztec/foundation/eth-signature';
4
+
5
+ import type { TypedDataDefinition } from 'viem';
6
+
7
+ import type { ValidatorKeyStore } from './interface.js';
8
+
9
+ /**
10
+ * Web3Signer Key Store
11
+ *
12
+ * An implementation of the Key store using Web3Signer remote signing service.
13
+ * This implementation uses the Web3Signer JSON-RPC API for secp256k1 signatures.
14
+ */
15
+ export class Web3SignerKeyStore implements ValidatorKeyStore {
16
+ constructor(
17
+ private addresses: EthAddress[],
18
+ private baseUrl: string,
19
+ ) {}
20
+
21
+ /**
22
+ * Get the address of a signer by index
23
+ *
24
+ * @param index - The index of the signer
25
+ * @returns the address
26
+ */
27
+ public getAddress(index: number): EthAddress {
28
+ if (index >= this.addresses.length) {
29
+ throw new Error(`Index ${index} is out of bounds.`);
30
+ }
31
+ return this.addresses[index];
32
+ }
33
+
34
+ /**
35
+ * Get all addresses
36
+ *
37
+ * @returns all addresses
38
+ */
39
+ public getAddresses(): EthAddress[] {
40
+ return this.addresses;
41
+ }
42
+
43
+ /**
44
+ * Sign EIP-712 typed data with all keystore addresses
45
+ * @param typedData - The complete EIP-712 typed data structure (domain, types, primaryType, message)
46
+ * @return signatures
47
+ */
48
+ public async signTypedData(typedData: TypedDataDefinition): Promise<Signature[]> {
49
+ const signatures = await Promise.all(
50
+ this.addresses.map(address => this.makeJsonRpcSignTypedDataRequest(address, typedData)),
51
+ );
52
+ return signatures;
53
+ }
54
+
55
+ /**
56
+ * Sign EIP-712 typed data with a specific address
57
+ * @param address - The address of the signer to use
58
+ * @param typedData - The complete EIP-712 typed data structure (domain, types, primaryType, message)
59
+ * @returns signature for the specified address
60
+ * @throws Error if the address is not found in the keystore or signing fails
61
+ */
62
+ public async signTypedDataWithAddress(address: EthAddress, typedData: TypedDataDefinition): Promise<Signature> {
63
+ if (!this.addresses.some(addr => addr.equals(address))) {
64
+ throw new Error(`Address ${address.toString()} not found in keystore`);
65
+ }
66
+
67
+ return await this.makeJsonRpcSignTypedDataRequest(address, typedData);
68
+ }
69
+
70
+ /**
71
+ * Sign a message with all keystore addresses using EIP-191 prefix
72
+ *
73
+ * @param message - The message to sign
74
+ * @return signatures
75
+ */
76
+ public async signMessage(message: Buffer32): Promise<Signature[]> {
77
+ const signatures = await Promise.all(this.addresses.map(address => this.makeJsonRpcSignRequest(address, message)));
78
+ return signatures;
79
+ }
80
+
81
+ /**
82
+ * Sign a message with a specific address using EIP-191 prefix
83
+ * @param address - The address of the signer to use
84
+ * @param message - The message to sign
85
+ * @returns signature for the specified address
86
+ * @throws Error if the address is not found in the keystore or signing fails
87
+ */
88
+ public async signMessageWithAddress(address: EthAddress, message: Buffer32): Promise<Signature> {
89
+ if (!this.addresses.some(addr => addr.equals(address))) {
90
+ throw new Error(`Address ${address.toString()} not found in keystore`);
91
+ }
92
+ return await this.makeJsonRpcSignRequest(address, message);
93
+ }
94
+
95
+ /**
96
+ * Make a JSON-RPC sign request to Web3Signer using eth_sign
97
+ * @param address - The Ethereum address to sign with
98
+ * @param data - The data to sign
99
+ * @returns The signature
100
+ */
101
+ private async makeJsonRpcSignRequest(address: EthAddress, data: Buffer32): Promise<Signature> {
102
+ const url = this.baseUrl;
103
+
104
+ // Use JSON-RPC eth_sign method which automatically applies Ethereum message prefixing
105
+ const body = {
106
+ jsonrpc: '2.0',
107
+ method: 'eth_sign',
108
+ params: [
109
+ address.toString(), // Ethereum address as identifier
110
+ data.toString(), // Raw data to sign (eth_sign will apply Ethereum message prefix)
111
+ ],
112
+ id: 1,
113
+ };
114
+
115
+ const response = await fetch(url, {
116
+ method: 'POST',
117
+ headers: {
118
+ 'Content-Type': 'application/json',
119
+ },
120
+ body: JSON.stringify(body),
121
+ });
122
+
123
+ if (!response.ok) {
124
+ const errorText = await response.text();
125
+ throw new Error(`Web3Signer request failed: ${response.status} ${response.statusText} - ${errorText}`);
126
+ }
127
+
128
+ const result = await response.json();
129
+
130
+ // Handle JSON-RPC response format
131
+ if (result.error) {
132
+ throw new Error(`Web3Signer JSON-RPC error: ${result.error.code} - ${result.error.message}`);
133
+ }
134
+
135
+ if (!result.result) {
136
+ throw new Error('Invalid response from Web3Signer: no result found');
137
+ }
138
+
139
+ let signatureHex = result.result;
140
+
141
+ // Ensure the signature has the 0x prefix
142
+ if (!signatureHex.startsWith('0x')) {
143
+ signatureHex = '0x' + signatureHex;
144
+ }
145
+
146
+ // Parse the signature from the hex string
147
+ return Signature.fromString(signatureHex as `0x${string}`);
148
+ }
149
+
150
+ private async makeJsonRpcSignTypedDataRequest(
151
+ address: EthAddress,
152
+ typedData: TypedDataDefinition,
153
+ ): Promise<Signature> {
154
+ const url = this.baseUrl;
155
+
156
+ const body = {
157
+ jsonrpc: '2.0',
158
+ method: 'eth_signTypedData',
159
+ params: [address.toString(), JSON.stringify(typedData)],
160
+ id: 1,
161
+ };
162
+
163
+ const response = await fetch(url, {
164
+ method: 'POST',
165
+ headers: {
166
+ 'Content-Type': 'application/json',
167
+ },
168
+ body: JSON.stringify(body),
169
+ });
170
+
171
+ if (!response.ok) {
172
+ const errorText = await response.text();
173
+ throw new Error(`Web3Signer request failed: ${response.status} ${response.statusText} - ${errorText}`);
174
+ }
175
+
176
+ const result = await response.json();
177
+
178
+ if (result.error) {
179
+ throw new Error(`Web3Signer JSON-RPC error: ${result.error.code} - ${result.error.message}`);
180
+ }
181
+
182
+ if (!result.result) {
183
+ throw new Error('Invalid response from Web3Signer: no result found');
184
+ }
185
+
186
+ let signatureHex = result.result;
187
+
188
+ // Ensure the signature has the 0x prefix
189
+ if (!signatureHex.startsWith('0x')) {
190
+ signatureHex = '0x' + signatureHex;
191
+ }
192
+
193
+ return Signature.fromString(signatureHex as `0x${string}`);
194
+ }
195
+ }
package/src/validator.ts CHANGED
@@ -9,11 +9,12 @@ import { RunningPromise } from '@aztec/foundation/running-promise';
9
9
  import { sleep } from '@aztec/foundation/sleep';
10
10
  import { DateProvider, Timer } from '@aztec/foundation/timer';
11
11
  import type { P2P, PeerId } from '@aztec/p2p';
12
- import { TxCollector } from '@aztec/p2p';
12
+ import { AuthRequest, AuthResponse, ReqRespSubProtocol, TxProvider } from '@aztec/p2p';
13
13
  import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
14
14
  import { computeInHashFromL1ToL2Messages } from '@aztec/prover-client/helpers';
15
+ import { Offense } from '@aztec/slasher';
15
16
  import {
16
- Offense,
17
+ type SlasherConfig,
17
18
  WANT_TO_SLASH_EVENT,
18
19
  type WantToSlashArgs,
19
20
  type Watcher,
@@ -21,12 +22,7 @@ import {
21
22
  } from '@aztec/slasher/config';
22
23
  import type { L2BlockSource } from '@aztec/stdlib/block';
23
24
  import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
24
- import type {
25
- IFullNodeBlockBuilder,
26
- ITxCollector,
27
- SequencerConfig,
28
- SlasherConfig,
29
- } from '@aztec/stdlib/interfaces/server';
25
+ import type { IFullNodeBlockBuilder, SequencerConfig } from '@aztec/stdlib/interfaces/server';
30
26
  import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
31
27
  import type { BlockAttestation, BlockProposal, BlockProposalOptions } from '@aztec/stdlib/p2p';
32
28
  import { GlobalVariables, type ProposedBlockHeader, type StateReference, type Tx } from '@aztec/stdlib/tx';
@@ -41,11 +37,13 @@ import {
41
37
  import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
42
38
 
43
39
  import { EventEmitter } from 'events';
40
+ import type { TypedDataDefinition } from 'viem';
44
41
 
45
42
  import type { ValidatorClientConfig } from './config.js';
46
43
  import { ValidationService } from './duties/validation_service.js';
47
44
  import type { ValidatorKeyStore } from './key_store/interface.js';
48
45
  import { LocalKeyStore } from './key_store/local_key_store.js';
46
+ import { Web3SignerKeyStore } from './key_store/web3signer_key_store.js';
49
47
  import { ValidatorMetrics } from './metrics.js';
50
48
 
51
49
  // We maintain a set of proposers who have proposed invalid blocks.
@@ -84,20 +82,21 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
84
82
  private previousProposal?: BlockProposal;
85
83
 
86
84
  private myAddresses: EthAddress[];
87
- private lastEpoch: bigint | undefined;
85
+ private lastEpochForCommitteeUpdateLoop: bigint | undefined;
88
86
  private epochCacheUpdateLoop: RunningPromise;
89
87
 
90
88
  private blockProposalValidator: BlockProposalValidator;
91
- private txCollector: ITxCollector;
92
- private proposersOfInvalidBlocks: Set<EthAddress> = new Set();
93
89
 
94
- constructor(
90
+ private proposersOfInvalidBlocks: Set<string> = new Set();
91
+
92
+ protected constructor(
95
93
  private blockBuilder: IFullNodeBlockBuilder,
96
94
  private keyStore: ValidatorKeyStore,
97
95
  private epochCache: EpochCache,
98
96
  private p2pClient: P2P,
99
97
  private blockSource: L2BlockSource,
100
98
  private l1ToL2MessageSource: L1ToL2MessageSource,
99
+ private txProvider: TxProvider,
101
100
  private config: ValidatorClientConfig &
102
101
  Pick<SequencerConfig, 'txPublicSetupAllowList'> &
103
102
  Pick<SlasherConfig, 'slashInvalidBlockEnabled' | 'slashInvalidBlockPenalty' | 'slashInvalidBlockMaxPenalty'>,
@@ -113,8 +112,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
113
112
 
114
113
  this.blockProposalValidator = new BlockProposalValidator(epochCache);
115
114
 
116
- this.txCollector = new TxCollector(p2pClient, this.log);
117
-
118
115
  // Refresh epoch cache every second to trigger alert if participation in committee changes
119
116
  this.myAddresses = this.keyStore.getAddresses();
120
117
  this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), log, 1000);
@@ -124,12 +121,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
124
121
 
125
122
  private async handleEpochCommitteeUpdate() {
126
123
  try {
127
- const { committee, epoch } = await this.epochCache.getCommittee('now');
124
+ const { committee, epoch } = await this.epochCache.getCommittee('next');
128
125
  if (!committee) {
129
126
  this.log.trace(`No committee found for slot`);
130
127
  return;
131
128
  }
132
- if (epoch !== this.lastEpoch) {
129
+ if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
133
130
  const me = this.myAddresses;
134
131
  const committeeSet = new Set(committee.map(v => v.toString()));
135
132
  const inCommittee = me.filter(a => committeeSet.has(a.toString()));
@@ -142,7 +139,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
142
139
  `Validators ${me.map(a => a.toString()).join(', ')} are not on the validator committee for epoch ${epoch}`,
143
140
  );
144
141
  }
145
- this.lastEpoch = epoch;
142
+ this.lastEpochForCommitteeUpdateLoop = epoch;
146
143
  }
147
144
  } catch (err) {
148
145
  this.log.error(`Error updating epoch committee`, err);
@@ -157,27 +154,40 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
157
154
  p2pClient: P2P,
158
155
  blockSource: L2BlockSource,
159
156
  l1ToL2MessageSource: L1ToL2MessageSource,
157
+ txProvider: TxProvider,
160
158
  dateProvider: DateProvider = new DateProvider(),
161
159
  telemetry: TelemetryClient = getTelemetryClient(),
162
160
  ) {
163
- if (!config.validatorPrivateKeys.getValue().length) {
164
- throw new InvalidValidatorPrivateKeyError();
165
- }
161
+ let keyStore: ValidatorKeyStore;
166
162
 
167
- const privateKeys = config.validatorPrivateKeys.getValue().map(validatePrivateKey);
168
- const localKeyStore = new LocalKeyStore(privateKeys);
163
+ if (config.web3SignerUrl) {
164
+ const addresses = config.web3SignerAddresses;
165
+ if (!addresses?.length) {
166
+ throw new Error('web3SignerAddresses is required when web3SignerUrl is provided');
167
+ }
168
+ keyStore = new Web3SignerKeyStore(addresses, config.web3SignerUrl);
169
+ } else {
170
+ const privateKeys = config.validatorPrivateKeys?.getValue().map(validatePrivateKey);
171
+ if (!privateKeys?.length) {
172
+ throw new InvalidValidatorPrivateKeyError();
173
+ }
174
+ keyStore = new LocalKeyStore(privateKeys);
175
+ }
169
176
 
170
177
  const validator = new ValidatorClient(
171
178
  blockBuilder,
172
- localKeyStore,
179
+ keyStore,
173
180
  epochCache,
174
181
  p2pClient,
175
182
  blockSource,
176
183
  l1ToL2MessageSource,
184
+ txProvider,
177
185
  config,
178
186
  dateProvider,
179
187
  telemetry,
180
188
  );
189
+
190
+ // TODO(PhilWindle): This seems like it could/should be done inside start()
181
191
  validator.registerBlockProposalHandler();
182
192
  return validator;
183
193
  }
@@ -186,8 +196,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
186
196
  return this.keyStore.getAddresses();
187
197
  }
188
198
 
189
- public signWithAddress(addr: EthAddress, msg: Buffer32) {
190
- return this.keyStore.signWithAddress(addr, msg);
199
+ public signWithAddress(addr: EthAddress, msg: TypedDataDefinition) {
200
+ return this.keyStore.signTypedDataWithAddress(addr, msg);
191
201
  }
192
202
 
193
203
  public configureSlashing(
@@ -207,15 +217,21 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
207
217
 
208
218
  const myAddresses = this.keyStore.getAddresses();
209
219
 
210
- const inCommittee = await this.epochCache.filterInCommittee(myAddresses);
220
+ const inCommittee = await this.epochCache.filterInCommittee('now', myAddresses);
211
221
  if (inCommittee.length > 0) {
212
222
  this.log.info(
213
- `Started validator with addresses in current validator committee: ${inCommittee.map(a => a.toString()).join(', ')}`,
223
+ `Started validator with addresses in current validator committee: ${inCommittee
224
+ .map(a => a.toString())
225
+ .join(', ')}`,
214
226
  );
215
227
  } else {
216
228
  this.log.info(`Started validator with addresses: ${myAddresses.map(a => a.toString()).join(', ')}`);
217
229
  }
218
230
  this.epochCacheUpdateLoop.start();
231
+
232
+ this.p2pClient.registerThisValidatorAddresses(myAddresses);
233
+ await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
234
+
219
235
  return Promise.resolve();
220
236
  }
221
237
 
@@ -224,36 +240,48 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
224
240
  }
225
241
 
226
242
  public registerBlockProposalHandler() {
227
- const handler = (block: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> => {
228
- return this.attestToProposal(block, proposalSender);
229
- };
243
+ const handler = (block: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> =>
244
+ this.attestToProposal(block, proposalSender);
230
245
  this.p2pClient.registerBlockProposalHandler(handler);
231
246
  }
232
247
 
233
248
  async attestToProposal(proposal: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> {
234
- const slotNumber = proposal.slotNumber.toNumber();
249
+ const slotNumber = proposal.slotNumber.toBigInt();
235
250
  const blockNumber = proposal.blockNumber;
236
251
  const proposer = proposal.getSender();
237
252
 
238
253
  // Check that I have any address in current committee before attesting
239
- const inCommittee = await this.epochCache.filterInCommittee(this.keyStore.getAddresses());
254
+ const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.keyStore.getAddresses());
240
255
  const partOfCommittee = inCommittee.length > 0;
241
256
 
242
257
  const proposalInfo = {
243
- slotNumber,
244
- blockNumber,
258
+ ...proposal.toBlockInfo(),
245
259
  proposer: proposer.toString(),
246
- archive: proposal.payload.archive.toString(),
247
- txCount: proposal.payload.txHashes.length,
248
- txHashes: proposal.payload.txHashes.map(txHash => txHash.toString()),
249
260
  };
250
- this.log.info(`Received request to attest for slot ${slotNumber}`, proposalInfo);
261
+
262
+ this.log.info(`Received proposal for slot ${slotNumber}`, {
263
+ ...proposalInfo,
264
+ txHashes: proposal.txHashes.map(txHash => txHash.toString()),
265
+ });
266
+
267
+ // Collect txs from the proposal. Note that we do this before checking if we have an address in the
268
+ // current committee, since we want to collect txs anyway to facilitate propagation.
269
+ const { txs, missingTxs } = await this.txProvider.getTxsForBlockProposal(proposal, {
270
+ pinnedPeer: proposalSender,
271
+ deadline: this.getReexecutionDeadline(proposal, this.blockBuilder.getConfig()),
272
+ });
273
+
274
+ // Check that I have any address in current committee before attesting
275
+ if (!partOfCommittee) {
276
+ this.log.verbose(`No validator in the current committee, skipping attestation`, proposalInfo);
277
+ return undefined;
278
+ }
251
279
 
252
280
  // Check that the proposal is from the current proposer, or the next proposer.
253
281
  // Q: Should this be moved to the block proposal validator, so we disregard proposals from anyone?
254
282
  const invalidProposal = await this.blockProposalValidator.validate(proposal);
255
283
  if (invalidProposal) {
256
- this.log.warn(`Proposal is not valid, skipping attestation`);
284
+ this.log.warn(`Proposal is not valid, skipping attestation`, proposalInfo);
257
285
  if (partOfCommittee) {
258
286
  this.metrics.incFailedAttestations(1, 'invalid_proposal');
259
287
  }
@@ -288,14 +316,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
288
316
  );
289
317
 
290
318
  if (parentBlock === undefined) {
291
- this.log.warn(`Parent block for ${blockNumber} not found, skipping attestation`);
292
-
319
+ this.log.warn(`Parent block for ${blockNumber} not found, skipping attestation`, proposalInfo);
293
320
  if (partOfCommittee) {
294
321
  this.metrics.incFailedAttestations(1, 'parent_block_not_found');
295
322
  }
296
-
297
323
  return undefined;
298
324
  }
325
+
299
326
  if (!proposal.payload.header.lastArchiveRoot.equals(parentBlock.archive.root)) {
300
327
  this.log.warn(`Parent block archive root for proposal does not match, skipping attestation`, {
301
328
  proposalLastArchiveRoot: proposal.payload.header.lastArchiveRoot.toString(),
@@ -309,22 +336,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
309
336
  }
310
337
  }
311
338
 
312
- // Collect txs from the proposal
313
- const { missing, txs } = await this.txCollector.collectForBlockProposal(proposal, proposalSender);
314
-
315
- // Check that all of the transactions in the proposal are available in the tx pool before attesting
316
- if (missing && missing.length > 0) {
317
- this.log.warn(`Missing ${missing.length}/${proposal.payload.txHashes.length} txs to attest to proposal`, {
318
- ...proposalInfo,
319
- missing,
320
- });
321
- if (partOfCommittee) {
322
- this.metrics.incFailedAttestations(1, 'tx_not_available');
323
- }
324
- return undefined;
325
- }
326
-
327
339
  // Check that I have the same set of l1ToL2Messages as the proposal
340
+ // Q: Same as above, should this be part of p2p validation?
328
341
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockNumber);
329
342
  const computedInHash = await computeInHashFromL1ToL2Messages(l1ToL2Messages);
330
343
  const proposalInHash = proposal.payload.header.contentCommitment.inHash;
@@ -340,8 +353,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
340
353
  return undefined;
341
354
  }
342
355
 
343
- if (!partOfCommittee) {
344
- this.log.verbose(`No validator in the committee, skipping attestation`);
356
+ // Check that all of the transactions in the proposal are available in the tx pool before attesting
357
+ if (missingTxs.length > 0) {
358
+ this.log.warn(`Missing ${missingTxs.length} txs to attest to proposal`, { ...proposalInfo, missingTxs });
359
+ if (partOfCommittee) {
360
+ this.metrics.incFailedAttestations(1, 'TransactionsNotAvailableError');
361
+ }
345
362
  return undefined;
346
363
  }
347
364
 
@@ -384,11 +401,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
384
401
  * @param proposal - The proposal to re-execute
385
402
  */
386
403
  async reExecuteTransactions(proposal: BlockProposal, txs: Tx[], l1ToL2Messages: Fr[]): Promise<void> {
387
- const { header, txHashes } = proposal.payload;
404
+ const { header } = proposal.payload;
405
+ const { txHashes } = proposal;
388
406
 
389
407
  // If we do not have all of the transactions, then we should fail
390
408
  if (txs.length !== txHashes.length) {
391
- const foundTxHashes = await Promise.all(txs.map(async tx => await tx.getTxHash()));
409
+ const foundTxHashes = txs.map(tx => tx.getTxHash());
392
410
  const missingTxHashes = txHashes.filter(txHash => !foundTxHashes.includes(txHash));
393
411
  throw new TransactionsNotAvailableError(missingTxHashes);
394
412
  }
@@ -444,13 +462,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
444
462
  this.proposersOfInvalidBlocks.delete(this.proposersOfInvalidBlocks.values().next().value!);
445
463
  }
446
464
 
447
- this.proposersOfInvalidBlocks.add(proposer);
465
+ this.proposersOfInvalidBlocks.add(proposer.toString());
448
466
 
449
467
  this.emit(WANT_TO_SLASH_EVENT, [
450
468
  {
451
469
  validator: proposer,
452
470
  amount: this.config.slashInvalidBlockPenalty,
453
- offense: Offense.INVALID_BLOCK,
471
+ offense: Offense.BROADCASTED_INVALID_BLOCK_PROPOSAL,
454
472
  },
455
473
  ]);
456
474
  }
@@ -470,7 +488,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
470
488
  public shouldSlash(args: WantToSlashArgs): Promise<boolean> {
471
489
  // note we don't check the offence here: we know this person is bad and we're willing to slash up to the max penalty.
472
490
  return Promise.resolve(
473
- args.amount <= this.config.slashInvalidBlockMaxPenalty && this.proposersOfInvalidBlocks.has(args.validator),
491
+ args.amount <= this.config.slashInvalidBlockMaxPenalty &&
492
+ this.proposersOfInvalidBlocks.has(args.validator.toString()),
474
493
  );
475
494
  }
476
495
 
@@ -505,6 +524,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
505
524
  await this.p2pClient.broadcastProposal(proposal);
506
525
  }
507
526
 
527
+ async collectOwnAttestations(proposal: BlockProposal): Promise<BlockAttestation[]> {
528
+ const slot = proposal.payload.header.slotNumber.toBigInt();
529
+ const inCommittee = await this.epochCache.filterInCommittee(slot, this.keyStore.getAddresses());
530
+ this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
531
+ return this.doAttestToProposal(proposal, inCommittee);
532
+ }
533
+
508
534
  async collectAttestations(proposal: BlockProposal, required: number, deadline: Date): Promise<BlockAttestation[]> {
509
535
  // Wait and poll the p2pClient's attestation pool for this block until we have enough attestations
510
536
  const slot = proposal.payload.header.slotNumber.toBigInt();
@@ -517,11 +543,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
517
543
  throw new AttestationTimeoutError(0, required, slot);
518
544
  }
519
545
 
520
- const proposalId = proposal.archive.toString();
521
- // adds attestations for all of my addresses locally
522
- const inCommittee = await this.epochCache.filterInCommittee(this.keyStore.getAddresses());
523
- await this.doAttestToProposal(proposal, inCommittee);
546
+ await this.collectOwnAttestations(proposal);
524
547
 
548
+ const proposalId = proposal.archive.toString();
525
549
  const myAddresses = this.keyStore.getAddresses();
526
550
 
527
551
  let attestations: BlockAttestation[] = [];
@@ -559,6 +583,29 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
559
583
  await this.p2pClient.addAttestations(attestations);
560
584
  return attestations;
561
585
  }
586
+
587
+ private async handleAuthRequest(peer: PeerId, msg: Buffer): Promise<Buffer> {
588
+ const authRequest = AuthRequest.fromBuffer(msg);
589
+ const statusMessage = await this.p2pClient.handleAuthRequestFromPeer(authRequest, peer).catch(_ => undefined);
590
+ if (statusMessage === undefined) {
591
+ return Buffer.alloc(0);
592
+ }
593
+
594
+ // Find a validator address that is in the set
595
+ const allRegisteredValidators = await this.epochCache.getRegisteredValidators();
596
+ const addressToUse = this.getValidatorAddresses().find(
597
+ address => allRegisteredValidators.find(v => v.equals(address)) !== undefined,
598
+ );
599
+ if (addressToUse === undefined) {
600
+ // We don't have a registered address
601
+ return Buffer.alloc(0);
602
+ }
603
+
604
+ const payloadToSign = authRequest.getPayloadToSign();
605
+ const signature = await this.keyStore.signMessageWithAddress(addressToUse, payloadToSign);
606
+ const authResponse = new AuthResponse(statusMessage, signature);
607
+ return authResponse.toBuffer();
608
+ }
562
609
  }
563
610
 
564
611
  function validatePrivateKey(privateKey: string): Buffer32 {