@aztec/validator-client 2.0.3 → 2.1.0-rc.2

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/src/validator.ts CHANGED
@@ -1,17 +1,14 @@
1
- import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
2
1
  import type { EpochCache } from '@aztec/epoch-cache';
3
2
  import type { EthAddress } from '@aztec/foundation/eth-address';
3
+ import type { Signature } from '@aztec/foundation/eth-signature';
4
4
  import { Fr } from '@aztec/foundation/fields';
5
5
  import { createLogger } from '@aztec/foundation/log';
6
- import { retryUntil } from '@aztec/foundation/retry';
7
6
  import { RunningPromise } from '@aztec/foundation/running-promise';
8
7
  import { sleep } from '@aztec/foundation/sleep';
9
- import { DateProvider, Timer } from '@aztec/foundation/timer';
8
+ import { DateProvider } from '@aztec/foundation/timer';
10
9
  import type { KeystoreManager } from '@aztec/node-keystore';
11
- import type { P2P, PeerId } from '@aztec/p2p';
12
- import { AuthRequest, AuthResponse, ReqRespSubProtocol, TxProvider } from '@aztec/p2p';
13
- import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
14
- import { computeInHashFromL1ToL2Messages } from '@aztec/prover-client/helpers';
10
+ import type { P2P, PeerId, TxProvider } from '@aztec/p2p';
11
+ import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
15
12
  import {
16
13
  OffenseType,
17
14
  type SlasherConfig,
@@ -20,24 +17,18 @@ import {
20
17
  type WatcherEmitter,
21
18
  } from '@aztec/slasher';
22
19
  import type { AztecAddress } from '@aztec/stdlib/aztec-address';
23
- import type { L2BlockSource } from '@aztec/stdlib/block';
24
- import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
20
+ import type { CommitteeAttestationsAndSigners, L2BlockSource } from '@aztec/stdlib/block';
25
21
  import type { IFullNodeBlockBuilder, Validator, ValidatorClientFullConfig } from '@aztec/stdlib/interfaces/server';
26
22
  import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
27
23
  import type { BlockAttestation, BlockProposal, BlockProposalOptions } from '@aztec/stdlib/p2p';
28
- import { GlobalVariables, type ProposedBlockHeader, type StateReference, type Tx } from '@aztec/stdlib/tx';
29
- import {
30
- AttestationTimeoutError,
31
- ReExFailedTxsError,
32
- ReExStateMismatchError,
33
- ReExTimeoutError,
34
- TransactionsNotAvailableError,
35
- } from '@aztec/stdlib/validators';
24
+ import type { ProposedBlockHeader, StateReference, Tx } from '@aztec/stdlib/tx';
25
+ import { AttestationTimeoutError } from '@aztec/stdlib/validators';
36
26
  import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
37
27
 
38
28
  import { EventEmitter } from 'events';
39
29
  import type { TypedDataDefinition } from 'viem';
40
30
 
31
+ import { BlockProposalHandler, type BlockProposalValidationFailureReason } from './block_proposal_handler.js';
41
32
  import type { ValidatorClientConfig } from './config.js';
42
33
  import { ValidationService } from './duties/validation_service.js';
43
34
  import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
@@ -47,6 +38,12 @@ import { ValidatorMetrics } from './metrics.js';
47
38
  // Just cap the set to avoid unbounded growth.
48
39
  const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
49
40
 
41
+ // What errors from the block proposal handler result in slashing
42
+ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT: BlockProposalValidationFailureReason[] = [
43
+ 'state_mismatch',
44
+ 'failed_txs',
45
+ ];
46
+
50
47
  /**
51
48
  * Validator Client
52
49
  */
@@ -55,24 +52,22 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
55
52
  private validationService: ValidationService;
56
53
  private metrics: ValidatorMetrics;
57
54
 
55
+ // Whether it has already registered handlers on the p2p client
56
+ private hasRegisteredHandlers = false;
57
+
58
58
  // Used to check if we are sending the same proposal twice
59
59
  private previousProposal?: BlockProposal;
60
60
 
61
61
  private lastEpochForCommitteeUpdateLoop: bigint | undefined;
62
62
  private epochCacheUpdateLoop: RunningPromise;
63
63
 
64
- private blockProposalValidator: BlockProposalValidator;
65
-
66
64
  private proposersOfInvalidBlocks: Set<string> = new Set();
67
65
 
68
66
  protected constructor(
69
- private blockBuilder: IFullNodeBlockBuilder,
70
67
  private keyStore: NodeKeystoreAdapter,
71
68
  private epochCache: EpochCache,
72
69
  private p2pClient: P2P,
73
- private blockSource: L2BlockSource,
74
- private l1ToL2MessageSource: L1ToL2MessageSource,
75
- private txProvider: TxProvider,
70
+ private blockProposalHandler: BlockProposalHandler,
76
71
  private config: ValidatorClientFullConfig,
77
72
  private dateProvider: DateProvider = new DateProvider(),
78
73
  telemetry: TelemetryClient = getTelemetryClient(),
@@ -84,8 +79,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
84
79
 
85
80
  this.validationService = new ValidationService(keyStore);
86
81
 
87
- this.blockProposalValidator = new BlockProposalValidator(epochCache);
88
-
89
82
  // Refresh epoch cache every second to trigger alert if participation in committee changes
90
83
  this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), log, 1000);
91
84
 
@@ -152,21 +145,30 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
152
145
  dateProvider: DateProvider = new DateProvider(),
153
146
  telemetry: TelemetryClient = getTelemetryClient(),
154
147
  ) {
155
- const validator = new ValidatorClient(
148
+ const metrics = new ValidatorMetrics(telemetry);
149
+ const blockProposalValidator = new BlockProposalValidator(epochCache);
150
+ const blockProposalHandler = new BlockProposalHandler(
156
151
  blockBuilder,
157
- NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager),
158
- epochCache,
159
- p2pClient,
160
152
  blockSource,
161
153
  l1ToL2MessageSource,
162
154
  txProvider,
155
+ blockProposalValidator,
156
+ config,
157
+ metrics,
158
+ dateProvider,
159
+ telemetry,
160
+ );
161
+
162
+ const validator = new ValidatorClient(
163
+ NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager),
164
+ epochCache,
165
+ p2pClient,
166
+ blockProposalHandler,
163
167
  config,
164
168
  dateProvider,
165
169
  telemetry,
166
170
  );
167
171
 
168
- // TODO(PhilWindle): This seems like it could/should be done inside start()
169
- validator.registerBlockProposalHandler();
170
172
  return validator;
171
173
  }
172
174
 
@@ -176,6 +178,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
176
178
  .filter(addr => !this.config.disabledValidators.some(disabled => disabled.equals(addr)));
177
179
  }
178
180
 
181
+ public getBlockProposalHandler() {
182
+ return this.blockProposalHandler;
183
+ }
184
+
185
+ // Proxy method for backwards compatibility with tests
186
+ public reExecuteTransactions(proposal: BlockProposal, txs: any[], l1ToL2Messages: Fr[]): Promise<any> {
187
+ return this.blockProposalHandler.reexecuteTransactions(proposal, txs, l1ToL2Messages);
188
+ }
189
+
179
190
  public signWithAddress(addr: EthAddress, msg: TypedDataDefinition) {
180
191
  return this.keyStore.signTypedDataWithAddress(addr, msg);
181
192
  }
@@ -197,11 +208,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
197
208
  }
198
209
 
199
210
  public async start() {
200
- // Sync the committee from the smart contract
201
- // https://github.com/AztecProtocol/aztec-packages/issues/7962
211
+ if (this.epochCacheUpdateLoop.isRunning()) {
212
+ this.log.warn(`Validator client already started`);
213
+ return;
214
+ }
202
215
 
203
- const myAddresses = this.getValidatorAddresses();
216
+ await this.registerHandlers();
204
217
 
218
+ const myAddresses = this.getValidatorAddresses();
205
219
  const inCommittee = await this.epochCache.filterInCommittee('now', myAddresses);
206
220
  if (inCommittee.length > 0) {
207
221
  this.log.info(
@@ -214,9 +228,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
214
228
  }
215
229
  this.epochCacheUpdateLoop.start();
216
230
 
217
- this.p2pClient.registerThisValidatorAddresses(myAddresses);
218
- await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
219
-
220
231
  return Promise.resolve();
221
232
  }
222
233
 
@@ -224,218 +235,80 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
224
235
  await this.epochCacheUpdateLoop.stop();
225
236
  }
226
237
 
227
- public registerBlockProposalHandler() {
228
- const handler = (block: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> =>
229
- this.attestToProposal(block, proposalSender);
230
- this.p2pClient.registerBlockProposalHandler(handler);
238
+ /** Register handlers on the p2p client */
239
+ public async registerHandlers() {
240
+ if (!this.hasRegisteredHandlers) {
241
+ this.hasRegisteredHandlers = true;
242
+ this.log.debug(`Registering validator handlers for p2p client`);
243
+
244
+ const handler = (block: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> =>
245
+ this.attestToProposal(block, proposalSender);
246
+ this.p2pClient.registerBlockProposalHandler(handler);
247
+
248
+ const myAddresses = this.getValidatorAddresses();
249
+ this.p2pClient.registerThisValidatorAddresses(myAddresses);
250
+
251
+ await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
252
+ }
231
253
  }
232
254
 
233
255
  async attestToProposal(proposal: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> {
234
256
  const slotNumber = proposal.slotNumber.toBigInt();
235
- const blockNumber = proposal.blockNumber;
236
257
  const proposer = proposal.getSender();
237
258
 
238
259
  // Check that I have any address in current committee before attesting
239
260
  const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
240
261
  const partOfCommittee = inCommittee.length > 0;
262
+ const incFailedAttestation = (reason: string) => this.metrics.incFailedAttestations(1, reason, partOfCommittee);
241
263
 
242
- const proposalInfo = {
243
- ...proposal.toBlockInfo(),
244
- proposer: proposer.toString(),
245
- };
246
-
247
- this.log.info(`Received proposal for slot ${slotNumber}`, {
264
+ const proposalInfo = { ...proposal.toBlockInfo(), proposer: proposer.toString() };
265
+ this.log.info(`Received proposal for block ${proposal.blockNumber} at slot ${slotNumber}`, {
248
266
  ...proposalInfo,
249
- txHashes: proposal.txHashes.map(txHash => txHash.toString()),
250
- });
251
-
252
- // Collect txs from the proposal. Note that we do this before checking if we have an address in the
253
- // current committee, since we want to collect txs anyway to facilitate propagation.
254
- const { txs, missingTxs } = await this.txProvider.getTxsForBlockProposal(proposal, {
255
- pinnedPeer: proposalSender,
256
- deadline: this.getReexecutionDeadline(proposal, this.blockBuilder.getConfig()),
267
+ txHashes: proposal.txHashes.map(t => t.toString()),
257
268
  });
258
269
 
259
- // Check that I have any address in current committee before attesting
260
- if (!partOfCommittee) {
261
- this.log.verbose(`No validator in the current committee, skipping attestation`, proposalInfo);
262
- return undefined;
263
- }
264
-
265
- // Check that the proposal is from the current proposer, or the next proposer.
266
- // Q: Should this be moved to the block proposal validator, so we disregard proposals from anyone?
267
- const invalidProposal = await this.blockProposalValidator.validate(proposal);
268
- if (invalidProposal) {
269
- this.log.warn(`Proposal is not valid, skipping attestation`, proposalInfo);
270
- if (partOfCommittee) {
271
- this.metrics.incFailedAttestations(1, 'invalid_proposal');
272
- }
273
- return undefined;
274
- }
275
-
276
- // Check that the parent proposal is a block we know, otherwise reexecution would fail.
277
- // Q: Should we move this to the block proposal validator? If there, then p2p would check it
278
- // before re-broadcasting it. This means that proposals built on top of an L1-reorg'ed-out block
279
- // would not be rebroadcasted. But it also means that nodes that have not fully synced would
280
- // not rebroadcast the proposal.
281
- if (blockNumber > INITIAL_L2_BLOCK_NUM) {
282
- const config = this.blockBuilder.getConfig();
283
- const deadline = this.getReexecutionDeadline(proposal, config);
284
- const currentTime = this.dateProvider.now();
285
- const timeoutDurationMs = deadline.getTime() - currentTime;
286
- const parentBlock =
287
- timeoutDurationMs <= 0
288
- ? undefined
289
- : await retryUntil(
290
- async () => {
291
- const block = await this.blockSource.getBlock(blockNumber - 1);
292
- if (block) {
293
- return block;
294
- }
295
- await this.blockSource.syncImmediate();
296
- return await this.blockSource.getBlock(blockNumber - 1);
297
- },
298
- 'Force Archiver Sync',
299
- timeoutDurationMs / 1000, // Continue retrying until the deadline
300
- 0.5, // Retry every 500ms
301
- );
302
-
303
- if (parentBlock === undefined) {
304
- this.log.warn(`Parent block for ${blockNumber} not found, skipping attestation`, proposalInfo);
305
- if (partOfCommittee) {
306
- this.metrics.incFailedAttestations(1, 'parent_block_not_found');
307
- }
308
- return undefined;
309
- }
310
-
311
- if (!proposal.payload.header.lastArchiveRoot.equals(parentBlock.archive.root)) {
312
- this.log.warn(`Parent block archive root for proposal does not match, skipping attestation`, {
313
- proposalLastArchiveRoot: proposal.payload.header.lastArchiveRoot.toString(),
314
- parentBlockArchiveRoot: parentBlock.archive.root.toString(),
315
- ...proposalInfo,
316
- });
317
- if (partOfCommittee) {
318
- this.metrics.incFailedAttestations(1, 'parent_block_does_not_match');
319
- }
320
- return undefined;
321
- }
322
- }
270
+ // Reexecute txs if we are part of the committee so we can attest, or if slashing is enabled so we can slash
271
+ // invalid proposals even when not in the committee, or if we are configured to always reexecute for monitoring purposes.
272
+ const { validatorReexecute, slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals } = this.config;
273
+ const shouldReexecute =
274
+ (slashBroadcastedInvalidBlockPenalty > 0n && validatorReexecute) ||
275
+ (partOfCommittee && validatorReexecute) ||
276
+ alwaysReexecuteBlockProposals;
277
+
278
+ const validationResult = await this.blockProposalHandler.handleBlockProposal(
279
+ proposal,
280
+ proposalSender,
281
+ !!shouldReexecute,
282
+ );
323
283
 
324
- // Check that I have the same set of l1ToL2Messages as the proposal
325
- // Q: Same as above, should this be part of p2p validation?
326
- const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockNumber);
327
- const computedInHash = await computeInHashFromL1ToL2Messages(l1ToL2Messages);
328
- const proposalInHash = proposal.payload.header.contentCommitment.inHash;
329
- if (!computedInHash.equals(proposalInHash)) {
330
- this.log.warn(`L1 to L2 messages in hash mismatch, skipping attestation`, {
331
- proposalInHash: proposalInHash.toString(),
332
- computedInHash: computedInHash.toString(),
333
- ...proposalInfo,
334
- });
335
- if (partOfCommittee) {
336
- this.metrics.incFailedAttestations(1, 'in_hash_mismatch');
337
- }
338
- return undefined;
339
- }
284
+ if (!validationResult.isValid) {
285
+ this.log.warn(`Proposal validation failed: ${validationResult.reason}`, proposalInfo);
286
+ incFailedAttestation(validationResult.reason || 'unknown');
340
287
 
341
- // Check that all of the transactions in the proposal are available in the tx pool before attesting
342
- if (missingTxs.length > 0) {
343
- this.log.warn(`Missing ${missingTxs.length} txs to attest to proposal`, { ...proposalInfo, missingTxs });
344
- if (partOfCommittee) {
345
- this.metrics.incFailedAttestations(1, 'TransactionsNotAvailableError');
288
+ // Slash invalid block proposals
289
+ if (
290
+ validationResult.reason &&
291
+ SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) &&
292
+ slashBroadcastedInvalidBlockPenalty > 0n
293
+ ) {
294
+ this.log.warn(`Slashing proposer for invalid block proposal`, proposalInfo);
295
+ this.slashInvalidBlock(proposal);
346
296
  }
347
297
  return undefined;
348
298
  }
349
299
 
350
- // Try re-executing the transactions in the proposal
351
- try {
352
- this.log.verbose(`Processing attestation for slot ${slotNumber}`, proposalInfo);
353
- if (this.config.validatorReexecute) {
354
- this.log.verbose(`Re-executing transactions in the proposal before attesting`);
355
- await this.reExecuteTransactions(proposal, txs, l1ToL2Messages);
356
- }
357
- } catch (error: any) {
358
- this.metrics.incFailedAttestations(1, error instanceof Error ? error.name : 'unknown');
359
- this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo);
360
- if (error instanceof ReExStateMismatchError && this.config.slashBroadcastedInvalidBlockPenalty > 0n) {
361
- this.log.warn(`Slashing proposer for invalid block proposal`, proposalInfo);
362
- this.slashInvalidBlock(proposal);
363
- }
300
+ // Check that I have any address in current committee before attesting
301
+ if (!partOfCommittee) {
302
+ this.log.verbose(`No validator in the current committee, skipping attestation`, proposalInfo);
364
303
  return undefined;
365
304
  }
366
305
 
367
306
  // Provided all of the above checks pass, we can attest to the proposal
368
- this.log.info(`Attesting to proposal for slot ${slotNumber}`, proposalInfo);
307
+ this.log.info(`Attesting to proposal for block ${proposal.blockNumber} at slot ${slotNumber}`, proposalInfo);
369
308
  this.metrics.incAttestations(inCommittee.length);
370
309
 
371
310
  // If the above function does not throw an error, then we can attest to the proposal
372
- return this.doAttestToProposal(proposal, inCommittee);
373
- }
374
-
375
- private getReexecutionDeadline(
376
- proposal: BlockProposal,
377
- config: { l1GenesisTime: bigint; slotDuration: number },
378
- ): Date {
379
- const nextSlotTimestampSeconds = Number(getTimestampForSlot(proposal.slotNumber.toBigInt() + 1n, config));
380
- const msNeededForPropagationAndPublishing = this.config.validatorReexecuteDeadlineMs;
381
- return new Date(nextSlotTimestampSeconds * 1000 - msNeededForPropagationAndPublishing);
382
- }
383
-
384
- /**
385
- * Re-execute the transactions in the proposal and check that the state updates match the header state
386
- * @param proposal - The proposal to re-execute
387
- */
388
- async reExecuteTransactions(proposal: BlockProposal, txs: Tx[], l1ToL2Messages: Fr[]): Promise<void> {
389
- const { header } = proposal.payload;
390
- const { txHashes } = proposal;
391
-
392
- // If we do not have all of the transactions, then we should fail
393
- if (txs.length !== txHashes.length) {
394
- const foundTxHashes = txs.map(tx => tx.getTxHash());
395
- const missingTxHashes = txHashes.filter(txHash => !foundTxHashes.includes(txHash));
396
- throw new TransactionsNotAvailableError(missingTxHashes);
397
- }
398
-
399
- // Use the sequencer's block building logic to re-execute the transactions
400
- const timer = new Timer();
401
- const config = this.blockBuilder.getConfig();
402
- const globalVariables = GlobalVariables.from({
403
- ...proposal.payload.header,
404
- blockNumber: proposal.blockNumber,
405
- timestamp: header.timestamp,
406
- chainId: new Fr(config.l1ChainId),
407
- version: new Fr(config.rollupVersion),
408
- });
409
-
410
- const { block, failedTxs } = await this.blockBuilder.buildBlock(txs, l1ToL2Messages, globalVariables, {
411
- deadline: this.getReexecutionDeadline(proposal, config),
412
- });
413
-
414
- this.log.verbose(`Transaction re-execution complete`);
415
- const numFailedTxs = failedTxs.length;
416
-
417
- if (numFailedTxs > 0) {
418
- this.metrics.recordFailedReexecution(proposal);
419
- throw new ReExFailedTxsError(numFailedTxs);
420
- }
421
-
422
- if (block.body.txEffects.length !== txHashes.length) {
423
- this.metrics.recordFailedReexecution(proposal);
424
- throw new ReExTimeoutError();
425
- }
426
-
427
- // This function will throw an error if state updates do not match
428
- if (!block.archive.root.equals(proposal.archive)) {
429
- this.metrics.recordFailedReexecution(proposal);
430
- throw new ReExStateMismatchError(
431
- proposal.archive,
432
- block.archive.root,
433
- proposal.payload.stateReference,
434
- block.header.state,
435
- );
436
- }
437
-
438
- this.metrics.recordReex(timer.ms(), txs.length, block.header.totalManaUsed.toNumber() / 1e6);
311
+ return this.createBlockAttestationsFromProposal(proposal, inCommittee);
439
312
  }
440
313
 
441
314
  private slashInvalidBlock(proposal: BlockProposal) {
@@ -490,11 +363,18 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
490
363
  await this.p2pClient.broadcastProposal(proposal);
491
364
  }
492
365
 
366
+ async signAttestationsAndSigners(
367
+ attestationsAndSigners: CommitteeAttestationsAndSigners,
368
+ proposer: EthAddress | undefined,
369
+ ): Promise<Signature> {
370
+ return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer);
371
+ }
372
+
493
373
  async collectOwnAttestations(proposal: BlockProposal): Promise<BlockAttestation[]> {
494
374
  const slot = proposal.payload.header.slotNumber.toBigInt();
495
375
  const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
496
376
  this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
497
- return this.doAttestToProposal(proposal, inCommittee);
377
+ return this.createBlockAttestationsFromProposal(proposal, inCommittee);
498
378
  }
499
379
 
500
380
  async collectAttestations(proposal: BlockProposal, required: number, deadline: Date): Promise<BlockAttestation[]> {
@@ -544,7 +424,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
544
424
  }
545
425
  }
546
426
 
547
- private async doAttestToProposal(proposal: BlockProposal, attestors: EthAddress[] = []): Promise<BlockAttestation[]> {
427
+ private async createBlockAttestationsFromProposal(
428
+ proposal: BlockProposal,
429
+ attestors: EthAddress[] = [],
430
+ ): Promise<BlockAttestation[]> {
548
431
  const attestations = await this.validationService.attestToProposal(proposal, attestors);
549
432
  await this.p2pClient.addAttestations(attestations);
550
433
  return attestations;
@@ -573,5 +456,3 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
573
456
  return authResponse.toBuffer();
574
457
  }
575
458
  }
576
-
577
- // Conversion helpers moved into NodeKeystoreAdapter.