@aztec/validator-client 1.2.0 → 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,11 @@ 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
17
  type SlasherConfig,
18
18
  WANT_TO_SLASH_EVENT,
19
19
  type WantToSlashArgs,
@@ -22,7 +22,7 @@ import {
22
22
  } from '@aztec/slasher/config';
23
23
  import type { L2BlockSource } from '@aztec/stdlib/block';
24
24
  import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
25
- import type { IFullNodeBlockBuilder, ITxCollector, SequencerConfig } from '@aztec/stdlib/interfaces/server';
25
+ import type { IFullNodeBlockBuilder, SequencerConfig } from '@aztec/stdlib/interfaces/server';
26
26
  import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
27
27
  import type { BlockAttestation, BlockProposal, BlockProposalOptions } from '@aztec/stdlib/p2p';
28
28
  import { GlobalVariables, type ProposedBlockHeader, type StateReference, type Tx } from '@aztec/stdlib/tx';
@@ -37,11 +37,13 @@ import {
37
37
  import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
38
38
 
39
39
  import { EventEmitter } from 'events';
40
+ import type { TypedDataDefinition } from 'viem';
40
41
 
41
42
  import type { ValidatorClientConfig } from './config.js';
42
43
  import { ValidationService } from './duties/validation_service.js';
43
44
  import type { ValidatorKeyStore } from './key_store/interface.js';
44
45
  import { LocalKeyStore } from './key_store/local_key_store.js';
46
+ import { Web3SignerKeyStore } from './key_store/web3signer_key_store.js';
45
47
  import { ValidatorMetrics } from './metrics.js';
46
48
 
47
49
  // We maintain a set of proposers who have proposed invalid blocks.
@@ -80,20 +82,21 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
80
82
  private previousProposal?: BlockProposal;
81
83
 
82
84
  private myAddresses: EthAddress[];
83
- private lastEpoch: bigint | undefined;
85
+ private lastEpochForCommitteeUpdateLoop: bigint | undefined;
84
86
  private epochCacheUpdateLoop: RunningPromise;
85
87
 
86
88
  private blockProposalValidator: BlockProposalValidator;
87
- private txCollector: ITxCollector;
88
- private proposersOfInvalidBlocks: Set<EthAddress> = new Set();
89
89
 
90
- constructor(
90
+ private proposersOfInvalidBlocks: Set<string> = new Set();
91
+
92
+ protected constructor(
91
93
  private blockBuilder: IFullNodeBlockBuilder,
92
94
  private keyStore: ValidatorKeyStore,
93
95
  private epochCache: EpochCache,
94
96
  private p2pClient: P2P,
95
97
  private blockSource: L2BlockSource,
96
98
  private l1ToL2MessageSource: L1ToL2MessageSource,
99
+ private txProvider: TxProvider,
97
100
  private config: ValidatorClientConfig &
98
101
  Pick<SequencerConfig, 'txPublicSetupAllowList'> &
99
102
  Pick<SlasherConfig, 'slashInvalidBlockEnabled' | 'slashInvalidBlockPenalty' | 'slashInvalidBlockMaxPenalty'>,
@@ -109,8 +112,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
109
112
 
110
113
  this.blockProposalValidator = new BlockProposalValidator(epochCache);
111
114
 
112
- this.txCollector = new TxCollector(p2pClient, this.log);
113
-
114
115
  // Refresh epoch cache every second to trigger alert if participation in committee changes
115
116
  this.myAddresses = this.keyStore.getAddresses();
116
117
  this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), log, 1000);
@@ -120,12 +121,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
120
121
 
121
122
  private async handleEpochCommitteeUpdate() {
122
123
  try {
123
- const { committee, epoch } = await this.epochCache.getCommittee('now');
124
+ const { committee, epoch } = await this.epochCache.getCommittee('next');
124
125
  if (!committee) {
125
126
  this.log.trace(`No committee found for slot`);
126
127
  return;
127
128
  }
128
- if (epoch !== this.lastEpoch) {
129
+ if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
129
130
  const me = this.myAddresses;
130
131
  const committeeSet = new Set(committee.map(v => v.toString()));
131
132
  const inCommittee = me.filter(a => committeeSet.has(a.toString()));
@@ -138,7 +139,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
138
139
  `Validators ${me.map(a => a.toString()).join(', ')} are not on the validator committee for epoch ${epoch}`,
139
140
  );
140
141
  }
141
- this.lastEpoch = epoch;
142
+ this.lastEpochForCommitteeUpdateLoop = epoch;
142
143
  }
143
144
  } catch (err) {
144
145
  this.log.error(`Error updating epoch committee`, err);
@@ -153,27 +154,40 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
153
154
  p2pClient: P2P,
154
155
  blockSource: L2BlockSource,
155
156
  l1ToL2MessageSource: L1ToL2MessageSource,
157
+ txProvider: TxProvider,
156
158
  dateProvider: DateProvider = new DateProvider(),
157
159
  telemetry: TelemetryClient = getTelemetryClient(),
158
160
  ) {
159
- if (!config.validatorPrivateKeys.getValue().length) {
160
- throw new InvalidValidatorPrivateKeyError();
161
- }
161
+ let keyStore: ValidatorKeyStore;
162
162
 
163
- const privateKeys = config.validatorPrivateKeys.getValue().map(validatePrivateKey);
164
- 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
+ }
165
176
 
166
177
  const validator = new ValidatorClient(
167
178
  blockBuilder,
168
- localKeyStore,
179
+ keyStore,
169
180
  epochCache,
170
181
  p2pClient,
171
182
  blockSource,
172
183
  l1ToL2MessageSource,
184
+ txProvider,
173
185
  config,
174
186
  dateProvider,
175
187
  telemetry,
176
188
  );
189
+
190
+ // TODO(PhilWindle): This seems like it could/should be done inside start()
177
191
  validator.registerBlockProposalHandler();
178
192
  return validator;
179
193
  }
@@ -182,8 +196,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
182
196
  return this.keyStore.getAddresses();
183
197
  }
184
198
 
185
- public signWithAddress(addr: EthAddress, msg: Buffer32) {
186
- return this.keyStore.signWithAddress(addr, msg);
199
+ public signWithAddress(addr: EthAddress, msg: TypedDataDefinition) {
200
+ return this.keyStore.signTypedDataWithAddress(addr, msg);
187
201
  }
188
202
 
189
203
  public configureSlashing(
@@ -203,15 +217,21 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
203
217
 
204
218
  const myAddresses = this.keyStore.getAddresses();
205
219
 
206
- const inCommittee = await this.epochCache.filterInCommittee(myAddresses);
220
+ const inCommittee = await this.epochCache.filterInCommittee('now', myAddresses);
207
221
  if (inCommittee.length > 0) {
208
222
  this.log.info(
209
- `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(', ')}`,
210
226
  );
211
227
  } else {
212
228
  this.log.info(`Started validator with addresses: ${myAddresses.map(a => a.toString()).join(', ')}`);
213
229
  }
214
230
  this.epochCacheUpdateLoop.start();
231
+
232
+ this.p2pClient.registerThisValidatorAddresses(myAddresses);
233
+ await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
234
+
215
235
  return Promise.resolve();
216
236
  }
217
237
 
@@ -220,36 +240,48 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
220
240
  }
221
241
 
222
242
  public registerBlockProposalHandler() {
223
- const handler = (block: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> => {
224
- return this.attestToProposal(block, proposalSender);
225
- };
243
+ const handler = (block: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> =>
244
+ this.attestToProposal(block, proposalSender);
226
245
  this.p2pClient.registerBlockProposalHandler(handler);
227
246
  }
228
247
 
229
248
  async attestToProposal(proposal: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> {
230
- const slotNumber = proposal.slotNumber.toNumber();
249
+ const slotNumber = proposal.slotNumber.toBigInt();
231
250
  const blockNumber = proposal.blockNumber;
232
251
  const proposer = proposal.getSender();
233
252
 
234
253
  // Check that I have any address in current committee before attesting
235
- const inCommittee = await this.epochCache.filterInCommittee(this.keyStore.getAddresses());
254
+ const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.keyStore.getAddresses());
236
255
  const partOfCommittee = inCommittee.length > 0;
237
256
 
238
257
  const proposalInfo = {
239
- slotNumber,
240
- blockNumber,
258
+ ...proposal.toBlockInfo(),
241
259
  proposer: proposer.toString(),
242
- archive: proposal.payload.archive.toString(),
243
- txCount: proposal.payload.txHashes.length,
244
- txHashes: proposal.payload.txHashes.map(txHash => txHash.toString()),
245
260
  };
246
- 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
+ }
247
279
 
248
280
  // Check that the proposal is from the current proposer, or the next proposer.
249
281
  // Q: Should this be moved to the block proposal validator, so we disregard proposals from anyone?
250
282
  const invalidProposal = await this.blockProposalValidator.validate(proposal);
251
283
  if (invalidProposal) {
252
- this.log.warn(`Proposal is not valid, skipping attestation`);
284
+ this.log.warn(`Proposal is not valid, skipping attestation`, proposalInfo);
253
285
  if (partOfCommittee) {
254
286
  this.metrics.incFailedAttestations(1, 'invalid_proposal');
255
287
  }
@@ -284,14 +316,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
284
316
  );
285
317
 
286
318
  if (parentBlock === undefined) {
287
- this.log.warn(`Parent block for ${blockNumber} not found, skipping attestation`);
288
-
319
+ this.log.warn(`Parent block for ${blockNumber} not found, skipping attestation`, proposalInfo);
289
320
  if (partOfCommittee) {
290
321
  this.metrics.incFailedAttestations(1, 'parent_block_not_found');
291
322
  }
292
-
293
323
  return undefined;
294
324
  }
325
+
295
326
  if (!proposal.payload.header.lastArchiveRoot.equals(parentBlock.archive.root)) {
296
327
  this.log.warn(`Parent block archive root for proposal does not match, skipping attestation`, {
297
328
  proposalLastArchiveRoot: proposal.payload.header.lastArchiveRoot.toString(),
@@ -305,22 +336,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
305
336
  }
306
337
  }
307
338
 
308
- // Collect txs from the proposal
309
- const { missing, txs } = await this.txCollector.collectForBlockProposal(proposal, proposalSender);
310
-
311
- // Check that all of the transactions in the proposal are available in the tx pool before attesting
312
- if (missing && missing.length > 0) {
313
- this.log.warn(`Missing ${missing.length}/${proposal.payload.txHashes.length} txs to attest to proposal`, {
314
- ...proposalInfo,
315
- missing,
316
- });
317
- if (partOfCommittee) {
318
- this.metrics.incFailedAttestations(1, 'tx_not_available');
319
- }
320
- return undefined;
321
- }
322
-
323
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?
324
341
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockNumber);
325
342
  const computedInHash = await computeInHashFromL1ToL2Messages(l1ToL2Messages);
326
343
  const proposalInHash = proposal.payload.header.contentCommitment.inHash;
@@ -336,8 +353,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
336
353
  return undefined;
337
354
  }
338
355
 
339
- if (!partOfCommittee) {
340
- 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
+ }
341
362
  return undefined;
342
363
  }
343
364
 
@@ -380,11 +401,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
380
401
  * @param proposal - The proposal to re-execute
381
402
  */
382
403
  async reExecuteTransactions(proposal: BlockProposal, txs: Tx[], l1ToL2Messages: Fr[]): Promise<void> {
383
- const { header, txHashes } = proposal.payload;
404
+ const { header } = proposal.payload;
405
+ const { txHashes } = proposal;
384
406
 
385
407
  // If we do not have all of the transactions, then we should fail
386
408
  if (txs.length !== txHashes.length) {
387
- const foundTxHashes = await Promise.all(txs.map(async tx => await tx.getTxHash()));
409
+ const foundTxHashes = txs.map(tx => tx.getTxHash());
388
410
  const missingTxHashes = txHashes.filter(txHash => !foundTxHashes.includes(txHash));
389
411
  throw new TransactionsNotAvailableError(missingTxHashes);
390
412
  }
@@ -440,13 +462,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
440
462
  this.proposersOfInvalidBlocks.delete(this.proposersOfInvalidBlocks.values().next().value!);
441
463
  }
442
464
 
443
- this.proposersOfInvalidBlocks.add(proposer);
465
+ this.proposersOfInvalidBlocks.add(proposer.toString());
444
466
 
445
467
  this.emit(WANT_TO_SLASH_EVENT, [
446
468
  {
447
469
  validator: proposer,
448
470
  amount: this.config.slashInvalidBlockPenalty,
449
- offense: Offense.INVALID_BLOCK,
471
+ offense: Offense.BROADCASTED_INVALID_BLOCK_PROPOSAL,
450
472
  },
451
473
  ]);
452
474
  }
@@ -466,7 +488,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
466
488
  public shouldSlash(args: WantToSlashArgs): Promise<boolean> {
467
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.
468
490
  return Promise.resolve(
469
- args.amount <= this.config.slashInvalidBlockMaxPenalty && this.proposersOfInvalidBlocks.has(args.validator),
491
+ args.amount <= this.config.slashInvalidBlockMaxPenalty &&
492
+ this.proposersOfInvalidBlocks.has(args.validator.toString()),
470
493
  );
471
494
  }
472
495
 
@@ -501,6 +524,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
501
524
  await this.p2pClient.broadcastProposal(proposal);
502
525
  }
503
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
+
504
534
  async collectAttestations(proposal: BlockProposal, required: number, deadline: Date): Promise<BlockAttestation[]> {
505
535
  // Wait and poll the p2pClient's attestation pool for this block until we have enough attestations
506
536
  const slot = proposal.payload.header.slotNumber.toBigInt();
@@ -513,11 +543,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
513
543
  throw new AttestationTimeoutError(0, required, slot);
514
544
  }
515
545
 
516
- const proposalId = proposal.archive.toString();
517
- // adds attestations for all of my addresses locally
518
- const inCommittee = await this.epochCache.filterInCommittee(this.keyStore.getAddresses());
519
- await this.doAttestToProposal(proposal, inCommittee);
546
+ await this.collectOwnAttestations(proposal);
520
547
 
548
+ const proposalId = proposal.archive.toString();
521
549
  const myAddresses = this.keyStore.getAddresses();
522
550
 
523
551
  let attestations: BlockAttestation[] = [];
@@ -555,6 +583,29 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
555
583
  await this.p2pClient.addAttestations(attestations);
556
584
  return attestations;
557
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
+ }
558
609
  }
559
610
 
560
611
  function validatePrivateKey(privateKey: string): Buffer32 {