@aztec/validator-client 2.0.3-rc.16 → 2.0.3-rc.18

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,13 @@
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';
4
3
  import { Fr } from '@aztec/foundation/fields';
5
4
  import { createLogger } from '@aztec/foundation/log';
6
- import { retryUntil } from '@aztec/foundation/retry';
7
5
  import { RunningPromise } from '@aztec/foundation/running-promise';
8
6
  import { sleep } from '@aztec/foundation/sleep';
9
- import { DateProvider, Timer } from '@aztec/foundation/timer';
7
+ import { DateProvider } from '@aztec/foundation/timer';
10
8
  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';
9
+ import type { P2P, PeerId, TxProvider } from '@aztec/p2p';
10
+ import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
15
11
  import {
16
12
  OffenseType,
17
13
  type SlasherConfig,
@@ -21,23 +17,17 @@ import {
21
17
  } from '@aztec/slasher';
22
18
  import type { AztecAddress } from '@aztec/stdlib/aztec-address';
23
19
  import type { L2BlockSource } from '@aztec/stdlib/block';
24
- import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
25
20
  import type { IFullNodeBlockBuilder, Validator, ValidatorClientFullConfig } from '@aztec/stdlib/interfaces/server';
26
21
  import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
27
22
  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';
23
+ import type { ProposedBlockHeader, StateReference, Tx } from '@aztec/stdlib/tx';
24
+ import { AttestationTimeoutError } from '@aztec/stdlib/validators';
36
25
  import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
37
26
 
38
27
  import { EventEmitter } from 'events';
39
28
  import type { TypedDataDefinition } from 'viem';
40
29
 
30
+ import { BlockProposalHandler, type BlockProposalValidationFailureReason } from './block_proposal_handler.js';
41
31
  import type { ValidatorClientConfig } from './config.js';
42
32
  import { ValidationService } from './duties/validation_service.js';
43
33
  import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
@@ -47,6 +37,12 @@ import { ValidatorMetrics } from './metrics.js';
47
37
  // Just cap the set to avoid unbounded growth.
48
38
  const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
49
39
 
40
+ // What errors from the block proposal handler result in slashing
41
+ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT: BlockProposalValidationFailureReason[] = [
42
+ 'state_mismatch',
43
+ 'failed_txs',
44
+ ];
45
+
50
46
  /**
51
47
  * Validator Client
52
48
  */
@@ -55,24 +51,22 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
55
51
  private validationService: ValidationService;
56
52
  private metrics: ValidatorMetrics;
57
53
 
54
+ // Whether it has already registered handlers on the p2p client
55
+ private hasRegisteredHandlers = false;
56
+
58
57
  // Used to check if we are sending the same proposal twice
59
58
  private previousProposal?: BlockProposal;
60
59
 
61
60
  private lastEpochForCommitteeUpdateLoop: bigint | undefined;
62
61
  private epochCacheUpdateLoop: RunningPromise;
63
62
 
64
- private blockProposalValidator: BlockProposalValidator;
65
-
66
63
  private proposersOfInvalidBlocks: Set<string> = new Set();
67
64
 
68
65
  protected constructor(
69
- private blockBuilder: IFullNodeBlockBuilder,
70
66
  private keyStore: NodeKeystoreAdapter,
71
67
  private epochCache: EpochCache,
72
68
  private p2pClient: P2P,
73
- private blockSource: L2BlockSource,
74
- private l1ToL2MessageSource: L1ToL2MessageSource,
75
- private txProvider: TxProvider,
69
+ private blockProposalHandler: BlockProposalHandler,
76
70
  private config: ValidatorClientFullConfig,
77
71
  private dateProvider: DateProvider = new DateProvider(),
78
72
  telemetry: TelemetryClient = getTelemetryClient(),
@@ -84,8 +78,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
84
78
 
85
79
  this.validationService = new ValidationService(keyStore);
86
80
 
87
- this.blockProposalValidator = new BlockProposalValidator(epochCache);
88
-
89
81
  // Refresh epoch cache every second to trigger alert if participation in committee changes
90
82
  this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), log, 1000);
91
83
 
@@ -152,21 +144,30 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
152
144
  dateProvider: DateProvider = new DateProvider(),
153
145
  telemetry: TelemetryClient = getTelemetryClient(),
154
146
  ) {
155
- const validator = new ValidatorClient(
147
+ const metrics = new ValidatorMetrics(telemetry);
148
+ const blockProposalValidator = new BlockProposalValidator(epochCache);
149
+ const blockProposalHandler = new BlockProposalHandler(
156
150
  blockBuilder,
157
- NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager),
158
- epochCache,
159
- p2pClient,
160
151
  blockSource,
161
152
  l1ToL2MessageSource,
162
153
  txProvider,
154
+ blockProposalValidator,
155
+ config,
156
+ metrics,
157
+ dateProvider,
158
+ telemetry,
159
+ );
160
+
161
+ const validator = new ValidatorClient(
162
+ NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager),
163
+ epochCache,
164
+ p2pClient,
165
+ blockProposalHandler,
163
166
  config,
164
167
  dateProvider,
165
168
  telemetry,
166
169
  );
167
170
 
168
- // TODO(PhilWindle): This seems like it could/should be done inside start()
169
- validator.registerBlockProposalHandler();
170
171
  return validator;
171
172
  }
172
173
 
@@ -176,6 +177,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
176
177
  .filter(addr => !this.config.disabledValidators.some(disabled => disabled.equals(addr)));
177
178
  }
178
179
 
180
+ public getBlockProposalHandler() {
181
+ return this.blockProposalHandler;
182
+ }
183
+
184
+ // Proxy method for backwards compatibility with tests
185
+ public reExecuteTransactions(proposal: BlockProposal, txs: any[], l1ToL2Messages: Fr[]): Promise<any> {
186
+ return this.blockProposalHandler.reexecuteTransactions(proposal, txs, l1ToL2Messages);
187
+ }
188
+
179
189
  public signWithAddress(addr: EthAddress, msg: TypedDataDefinition) {
180
190
  return this.keyStore.signTypedDataWithAddress(addr, msg);
181
191
  }
@@ -197,11 +207,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
197
207
  }
198
208
 
199
209
  public async start() {
200
- // Sync the committee from the smart contract
201
- // https://github.com/AztecProtocol/aztec-packages/issues/7962
210
+ if (this.epochCacheUpdateLoop.isRunning()) {
211
+ this.log.warn(`Validator client already started`);
212
+ return;
213
+ }
202
214
 
203
- const myAddresses = this.getValidatorAddresses();
215
+ await this.registerHandlers();
204
216
 
217
+ const myAddresses = this.getValidatorAddresses();
205
218
  const inCommittee = await this.epochCache.filterInCommittee('now', myAddresses);
206
219
  if (inCommittee.length > 0) {
207
220
  this.log.info(
@@ -214,9 +227,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
214
227
  }
215
228
  this.epochCacheUpdateLoop.start();
216
229
 
217
- this.p2pClient.registerThisValidatorAddresses(myAddresses);
218
- await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
219
-
220
230
  return Promise.resolve();
221
231
  }
222
232
 
@@ -224,143 +234,71 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
224
234
  await this.epochCacheUpdateLoop.stop();
225
235
  }
226
236
 
227
- public registerBlockProposalHandler() {
228
- const handler = (block: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> =>
229
- this.attestToProposal(block, proposalSender);
230
- this.p2pClient.registerBlockProposalHandler(handler);
237
+ /** Register handlers on the p2p client */
238
+ public async registerHandlers() {
239
+ if (!this.hasRegisteredHandlers) {
240
+ this.hasRegisteredHandlers = true;
241
+ this.log.debug(`Registering validator handlers for p2p client`);
242
+
243
+ const handler = (block: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> =>
244
+ this.attestToProposal(block, proposalSender);
245
+ this.p2pClient.registerBlockProposalHandler(handler);
246
+
247
+ const myAddresses = this.getValidatorAddresses();
248
+ this.p2pClient.registerThisValidatorAddresses(myAddresses);
249
+
250
+ await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
251
+ }
231
252
  }
232
253
 
233
254
  async attestToProposal(proposal: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> {
234
255
  const slotNumber = proposal.slotNumber.toBigInt();
235
- const blockNumber = proposal.blockNumber;
236
256
  const proposer = proposal.getSender();
237
257
 
238
258
  // Check that I have any address in current committee before attesting
239
259
  const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
240
260
  const partOfCommittee = inCommittee.length > 0;
261
+ const incFailedAttestation = (reason: string) => this.metrics.incFailedAttestations(1, reason, partOfCommittee);
241
262
 
242
- const proposalInfo = {
243
- ...proposal.toBlockInfo(),
244
- proposer: proposer.toString(),
245
- };
246
-
263
+ const proposalInfo = { ...proposal.toBlockInfo(), proposer: proposer.toString() };
247
264
  this.log.info(`Received proposal for slot ${slotNumber}`, {
248
265
  ...proposalInfo,
249
- txHashes: proposal.txHashes.map(txHash => txHash.toString()),
266
+ txHashes: proposal.txHashes.map(t => t.toString()),
250
267
  });
251
268
 
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()),
257
- });
258
-
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
- }
269
+ // Reexecute txs if we are part of the committee so we can attest, or if slashing is enabled so we can slash
270
+ // invalid proposals even when not in the committee, or if we are configured to always reexecute for monitoring purposes.
271
+ const { validatorReexecute, slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals } = this.config;
272
+ const shouldReexecute =
273
+ (slashBroadcastedInvalidBlockPenalty > 0n && validatorReexecute) ||
274
+ (partOfCommittee && validatorReexecute) ||
275
+ alwaysReexecuteBlockProposals;
276
+
277
+ const validationResult = await this.blockProposalHandler.handleBlockProposal(
278
+ proposal,
279
+ proposalSender,
280
+ !!shouldReexecute,
281
+ );
323
282
 
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
- }
283
+ if (!validationResult.isValid) {
284
+ this.log.warn(`Proposal validation failed: ${validationResult.reason}`, proposalInfo);
285
+ incFailedAttestation(validationResult.reason || 'unknown');
340
286
 
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');
287
+ // Slash invalid block proposals
288
+ if (
289
+ validationResult.reason &&
290
+ SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) &&
291
+ slashBroadcastedInvalidBlockPenalty > 0n
292
+ ) {
293
+ this.log.warn(`Slashing proposer for invalid block proposal`, proposalInfo);
294
+ this.slashInvalidBlock(proposal);
346
295
  }
347
296
  return undefined;
348
297
  }
349
298
 
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
- }
299
+ // Check that I have any address in current committee before attesting
300
+ if (!partOfCommittee) {
301
+ this.log.verbose(`No validator in the current committee, skipping attestation`, proposalInfo);
364
302
  return undefined;
365
303
  }
366
304
 
@@ -369,73 +307,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
369
307
  this.metrics.incAttestations(inCommittee.length);
370
308
 
371
309
  // 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);
310
+ return this.createBlockAttestationsFromProposal(proposal, inCommittee);
439
311
  }
440
312
 
441
313
  private slashInvalidBlock(proposal: BlockProposal) {
@@ -494,7 +366,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
494
366
  const slot = proposal.payload.header.slotNumber.toBigInt();
495
367
  const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
496
368
  this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
497
- return this.doAttestToProposal(proposal, inCommittee);
369
+ return this.createBlockAttestationsFromProposal(proposal, inCommittee);
498
370
  }
499
371
 
500
372
  async collectAttestations(proposal: BlockProposal, required: number, deadline: Date): Promise<BlockAttestation[]> {
@@ -544,7 +416,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
544
416
  }
545
417
  }
546
418
 
547
- private async doAttestToProposal(proposal: BlockProposal, attestors: EthAddress[] = []): Promise<BlockAttestation[]> {
419
+ private async createBlockAttestationsFromProposal(
420
+ proposal: BlockProposal,
421
+ attestors: EthAddress[] = [],
422
+ ): Promise<BlockAttestation[]> {
548
423
  const attestations = await this.validationService.attestToProposal(proposal, attestors);
549
424
  await this.p2pClient.addAttestations(attestations);
550
425
  return attestations;
@@ -573,5 +448,3 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
573
448
  return authResponse.toBuffer();
574
449
  }
575
450
  }
576
-
577
- // Conversion helpers moved into NodeKeystoreAdapter.