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

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
@@ -6,12 +6,15 @@ import { Fr } from '@aztec/foundation/fields';
6
6
  import { createLogger } from '@aztec/foundation/log';
7
7
  import { RunningPromise } from '@aztec/foundation/running-promise';
8
8
  import { sleep } from '@aztec/foundation/sleep';
9
- import { DateProvider, type Timer } from '@aztec/foundation/timer';
10
- import { type P2P, type PeerId, TxCollector } from '@aztec/p2p';
9
+ import { DateProvider } from '@aztec/foundation/timer';
10
+ import type { P2P, PeerId } from '@aztec/p2p';
11
+ import { TxCollector } from '@aztec/p2p';
11
12
  import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
12
- import type { L2Block, L2BlockSource } from '@aztec/stdlib/block';
13
+ import type { L2BlockSource } from '@aztec/stdlib/block';
14
+ import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
15
+ import type { IFullNodeBlockBuilder } from '@aztec/stdlib/interfaces/server';
13
16
  import type { BlockAttestation, BlockProposal, BlockProposalOptions } from '@aztec/stdlib/p2p';
14
- import type { ProposedBlockHeader, StateReference, Tx } from '@aztec/stdlib/tx';
17
+ import { GlobalVariables, type ProposedBlockHeader, type StateReference, type Tx } from '@aztec/stdlib/tx';
15
18
  import { type TelemetryClient, WithTracer, getTelemetryClient } from '@aztec/telemetry-client';
16
19
 
17
20
  import type { ValidatorClientConfig } from './config.js';
@@ -29,28 +32,9 @@ import type { ValidatorKeyStore } from './key_store/interface.js';
29
32
  import { LocalKeyStore } from './key_store/local_key_store.js';
30
33
  import { ValidatorMetrics } from './metrics.js';
31
34
 
32
- /**
33
- * Callback function for building a block
34
- *
35
- * We reuse the sequencer's block building functionality for re-execution
36
- */
37
- type BlockBuilderCallback = (
38
- blockNumber: Fr,
39
- header: ProposedBlockHeader,
40
- txs: Iterable<Tx> | AsyncIterableIterator<Tx>,
41
- opts?: { validateOnly?: boolean },
42
- ) => Promise<{
43
- block: L2Block;
44
- publicProcessorDuration: number;
45
- numTxs: number;
46
- numFailedTxs: number;
47
- blockBuildingTimer: Timer;
48
- }>;
49
-
50
35
  export interface Validator {
51
36
  start(): Promise<void>;
52
37
  registerBlockProposalHandler(): void;
53
- registerBlockBuilder(blockBuilder: BlockBuilderCallback): void;
54
38
 
55
39
  // Block validation responsibilities
56
40
  createBlockProposal(
@@ -59,9 +43,10 @@ export interface Validator {
59
43
  archive: Fr,
60
44
  stateReference: StateReference,
61
45
  txs: Tx[],
46
+ proposerAddress: EthAddress,
62
47
  options: BlockProposalOptions,
63
48
  ): Promise<BlockProposal | undefined>;
64
- attestToProposal(proposal: BlockProposal, sender: PeerId): Promise<BlockAttestation | undefined>;
49
+ attestToProposal(proposal: BlockProposal, sender: PeerId): Promise<BlockAttestation[] | undefined>;
65
50
 
66
51
  broadcastBlockProposal(proposal: BlockProposal): Promise<void>;
67
52
  collectAttestations(proposal: BlockProposal, required: number, deadline: Date): Promise<BlockAttestation[]>;
@@ -77,10 +62,7 @@ export class ValidatorClient extends WithTracer implements Validator {
77
62
  // Used to check if we are sending the same proposal twice
78
63
  private previousProposal?: BlockProposal;
79
64
 
80
- // Callback registered to: sequencer.buildBlock
81
- private blockBuilder?: BlockBuilderCallback = undefined;
82
-
83
- private myAddress: EthAddress;
65
+ private myAddresses: EthAddress[];
84
66
  private lastEpoch: bigint | undefined;
85
67
  private epochCacheUpdateLoop: RunningPromise;
86
68
 
@@ -88,6 +70,7 @@ export class ValidatorClient extends WithTracer implements Validator {
88
70
  private txCollector: TxCollector;
89
71
 
90
72
  constructor(
73
+ private blockBuilder: IFullNodeBlockBuilder,
91
74
  private keyStore: ValidatorKeyStore,
92
75
  private epochCache: EpochCache,
93
76
  private p2pClient: P2P,
@@ -108,21 +91,27 @@ export class ValidatorClient extends WithTracer implements Validator {
108
91
  this.txCollector = new TxCollector(p2pClient, this.log);
109
92
 
110
93
  // Refresh epoch cache every second to trigger alert if participation in committee changes
111
- this.myAddress = this.keyStore.getAddress();
94
+ this.myAddresses = this.keyStore.getAddresses();
112
95
  this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), log, 1000);
113
96
 
114
- this.log.verbose(`Initialized validator with address ${this.keyStore.getAddress().toString()}`);
97
+ this.log.verbose(`Initialized validator with addresses: ${this.myAddresses.map(a => a.toString()).join(', ')}`);
115
98
  }
116
99
 
117
100
  private async handleEpochCommitteeUpdate() {
118
101
  try {
119
102
  const { committee, epoch } = await this.epochCache.getCommittee('now');
120
103
  if (epoch !== this.lastEpoch) {
121
- const me = this.myAddress;
122
- if (committee.some(addr => addr.equals(me))) {
123
- this.log.info(`Validator ${me.toString()} is on the validator committee for epoch ${epoch}`);
104
+ const me = this.myAddresses;
105
+ const committeeSet = new Set(committee.map(v => v.toString()));
106
+ const inCommittee = me.filter(a => committeeSet.has(a.toString()));
107
+ if (inCommittee.length > 0) {
108
+ inCommittee.forEach(a =>
109
+ this.log.info(`Validator ${a.toString()} is on the validator committee for epoch ${epoch}`),
110
+ );
124
111
  } else {
125
- this.log.verbose(`Validator ${me.toString()} not on the validator committee for epoch ${epoch}`);
112
+ this.log.verbose(
113
+ `Validators ${me.map(a => a.toString()).join(', ')} are not on the validator committee for epoch ${epoch}`,
114
+ );
126
115
  }
127
116
  this.lastEpoch = epoch;
128
117
  }
@@ -133,20 +122,22 @@ export class ValidatorClient extends WithTracer implements Validator {
133
122
 
134
123
  static new(
135
124
  config: ValidatorClientConfig,
125
+ blockBuilder: IFullNodeBlockBuilder,
136
126
  epochCache: EpochCache,
137
127
  p2pClient: P2P,
138
128
  blockSource: L2BlockSource,
139
129
  dateProvider: DateProvider = new DateProvider(),
140
130
  telemetry: TelemetryClient = getTelemetryClient(),
141
131
  ) {
142
- if (!config.validatorPrivateKey) {
132
+ if (!config.validatorPrivateKeys?.length) {
143
133
  throw new InvalidValidatorPrivateKeyError();
144
134
  }
145
135
 
146
- const privateKey = validatePrivateKey(config.validatorPrivateKey);
147
- const localKeyStore = new LocalKeyStore(privateKey);
136
+ const privateKeys = config.validatorPrivateKeys.map(validatePrivateKey);
137
+ const localKeyStore = new LocalKeyStore(privateKeys);
148
138
 
149
139
  const validator = new ValidatorClient(
140
+ blockBuilder,
150
141
  localKeyStore,
151
142
  epochCache,
152
143
  p2pClient,
@@ -159,20 +150,25 @@ export class ValidatorClient extends WithTracer implements Validator {
159
150
  return validator;
160
151
  }
161
152
 
162
- public getValidatorAddress() {
163
- return this.keyStore.getAddress();
153
+ public getValidatorAddresses() {
154
+ return this.keyStore.getAddresses();
164
155
  }
165
156
 
166
157
  public async start() {
167
158
  // Sync the committee from the smart contract
168
159
  // https://github.com/AztecProtocol/aztec-packages/issues/7962
169
160
 
170
- const me = this.keyStore.getAddress();
171
- const inCommittee = await this.epochCache.isInCommittee(me);
172
- if (inCommittee) {
173
- this.log.info(`Started validator with address ${me.toString()} in current validator committee`);
161
+ const myAddresses = this.keyStore.getAddresses();
162
+
163
+ const inCommittee = await this.epochCache.filterInCommittee(myAddresses);
164
+ if (inCommittee.length > 0) {
165
+ this.log.info(
166
+ `Started validator with addresses in current validator committee:
167
+ ${inCommittee.map(a => a.toString()).join(', ')}`,
168
+ );
174
169
  } else {
175
- this.log.info(`Started validator with address ${me.toString()}`);
170
+ this.log.info(`Started validator with addresses:
171
+ ${myAddresses.map(a => a.toString()).join(', ')}`);
176
172
  }
177
173
  this.epochCacheUpdateLoop.start();
178
174
  return Promise.resolve();
@@ -183,22 +179,13 @@ export class ValidatorClient extends WithTracer implements Validator {
183
179
  }
184
180
 
185
181
  public registerBlockProposalHandler() {
186
- const handler = (block: BlockProposal, proposalSender: any): Promise<BlockAttestation | undefined> => {
182
+ const handler = (block: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> => {
187
183
  return this.attestToProposal(block, proposalSender);
188
184
  };
189
185
  this.p2pClient.registerBlockProposalHandler(handler);
190
186
  }
191
187
 
192
- /**
193
- * Register a callback function for building a block
194
- *
195
- * We reuse the sequencer's block building functionality for re-execution
196
- */
197
- public registerBlockBuilder(blockBuilder: BlockBuilderCallback) {
198
- this.blockBuilder = blockBuilder;
199
- }
200
-
201
- async attestToProposal(proposal: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation | undefined> {
188
+ async attestToProposal(proposal: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> {
202
189
  const slotNumber = proposal.slotNumber.toNumber();
203
190
  const blockNumber = proposal.blockNumber.toNumber();
204
191
  const proposalInfo = {
@@ -215,20 +202,20 @@ export class ValidatorClient extends WithTracer implements Validator {
215
202
  const invalidProposal = await this.blockProposalValidator.validate(proposal);
216
203
  if (invalidProposal) {
217
204
  this.log.verbose(`Proposal is not valid, skipping attestation`);
218
- this.metrics.incFailedAttestations('invalid_proposal');
205
+ this.metrics.incFailedAttestations(1, 'invalid_proposal');
219
206
  return undefined;
220
207
  }
221
208
 
222
209
  // Check that the parent proposal is a block we know, otherwise reexecution would fail.
223
210
  // Q: Should we move this to the block proposal validator? If there, then p2p would check it
224
- // before re-broadcasting it. This means that proposals built on top of an L1-reorgd-out block
211
+ // before re-broadcasting it. This means that proposals built on top of an L1-reorg'ed-out block
225
212
  // would not be rebroadcasted. But it also means that nodes that have not fully synced would
226
213
  // not rebroadcast the proposal.
227
214
  if (blockNumber > INITIAL_L2_BLOCK_NUM) {
228
215
  const parentBlock = await this.blockSource.getBlock(blockNumber - 1);
229
216
  if (parentBlock === undefined) {
230
217
  this.log.verbose(`Parent block for ${blockNumber} not found, skipping attestation`);
231
- this.metrics.incFailedAttestations('parent_block_not_found');
218
+ this.metrics.incFailedAttestations(1, 'parent_block_not_found');
232
219
  return undefined;
233
220
  }
234
221
  if (!proposal.payload.header.lastArchiveRoot.equals(parentBlock.archive.root)) {
@@ -237,7 +224,7 @@ export class ValidatorClient extends WithTracer implements Validator {
237
224
  parentBlockArchiveRoot: parentBlock.archive.root.toString(),
238
225
  ...proposalInfo,
239
226
  });
240
- this.metrics.incFailedAttestations('parent_block_does_not_match');
227
+ this.metrics.incFailedAttestations(1, 'parent_block_does_not_match');
241
228
  return undefined;
242
229
  }
243
230
  }
@@ -245,9 +232,10 @@ export class ValidatorClient extends WithTracer implements Validator {
245
232
  // Collect txs from the proposal
246
233
  const { missing, txs } = await this.txCollector.collectForBlockProposal(proposal, proposalSender);
247
234
 
248
- // Check that I am in the committee before attesting
249
- if (!(await this.epochCache.isInCommittee(this.keyStore.getAddress()))) {
250
- this.log.verbose(`Not in the committee, skipping attestation`);
235
+ // Check that I have any address in current committee before attesting
236
+ const inCommittee = await this.epochCache.filterInCommittee(this.keyStore.getAddresses());
237
+ if (inCommittee.length === 0) {
238
+ this.log.verbose(`No validator in the committee, skipping attestation`);
251
239
  return undefined;
252
240
  }
253
241
 
@@ -258,7 +246,7 @@ export class ValidatorClient extends WithTracer implements Validator {
258
246
  undefined,
259
247
  { proposalInfo, missing },
260
248
  );
261
- this.metrics.incFailedAttestations('TransactionsNotAvailableError');
249
+ this.metrics.incFailedAttestations(1, 'TransactionsNotAvailableError');
262
250
  return undefined;
263
251
  }
264
252
 
@@ -270,15 +258,26 @@ export class ValidatorClient extends WithTracer implements Validator {
270
258
  await this.reExecuteTransactions(proposal, txs);
271
259
  }
272
260
  } catch (error: any) {
273
- this.metrics.incFailedAttestations(error instanceof Error ? error.name : 'unknown');
261
+ this.metrics.incFailedAttestations(1, error instanceof Error ? error.name : 'unknown');
274
262
  this.log.error(`Failed to attest to proposal`, error, proposalInfo);
275
263
  return undefined;
276
264
  }
277
265
 
278
266
  // Provided all of the above checks pass, we can attest to the proposal
279
267
  this.log.info(`Attesting to proposal for slot ${slotNumber}`, proposalInfo);
280
- this.metrics.incAttestations();
281
- return this.doAttestToProposal(proposal);
268
+ this.metrics.incAttestations(inCommittee.length);
269
+
270
+ // If the above function does not throw an error, then we can attest to the proposal
271
+ return this.doAttestToProposal(proposal, inCommittee);
272
+ }
273
+
274
+ private getReexecutionDeadline(
275
+ proposal: BlockProposal,
276
+ config: { l1GenesisTime: bigint; slotDuration: number },
277
+ ): Date {
278
+ const nextSlotTimestampSeconds = Number(getTimestampForSlot(proposal.slotNumber.toBigInt() + 1n, config));
279
+ const msNeededForPropagationAndPublishing = this.config.validatorReexecuteDeadlineMs;
280
+ return new Date(nextSlotTimestampSeconds * 1000 - msNeededForPropagationAndPublishing);
282
281
  }
283
282
 
284
283
  /**
@@ -302,12 +301,22 @@ export class ValidatorClient extends WithTracer implements Validator {
302
301
 
303
302
  // Use the sequencer's block building logic to re-execute the transactions
304
303
  const stopTimer = this.metrics.reExecutionTimer();
305
- const { block, numFailedTxs } = await this.blockBuilder(proposal.blockNumber, header, txs, {
306
- validateOnly: true,
304
+ const config = this.blockBuilder.getConfig();
305
+ const globalVariables = GlobalVariables.from({
306
+ ...proposal.payload.header,
307
+ blockNumber: proposal.blockNumber,
308
+ timestamp: new Fr(header.timestamp),
309
+ chainId: new Fr(config.l1ChainId),
310
+ version: new Fr(config.rollupVersion),
311
+ });
312
+
313
+ const { block, failedTxs } = await this.blockBuilder.buildBlock(txs, globalVariables, {
314
+ deadline: this.getReexecutionDeadline(proposal, config),
307
315
  });
308
316
  stopTimer();
309
317
 
310
318
  this.log.verbose(`Transaction re-execution complete`);
319
+ const numFailedTxs = failedTxs.length;
311
320
 
312
321
  if (numFailedTxs > 0) {
313
322
  this.metrics.recordFailedReexecution(proposal);
@@ -332,6 +341,7 @@ export class ValidatorClient extends WithTracer implements Validator {
332
341
  archive: Fr,
333
342
  stateReference: StateReference,
334
343
  txs: Tx[],
344
+ proposerAddress: EthAddress,
335
345
  options: BlockProposalOptions,
336
346
  ): Promise<BlockProposal | undefined> {
337
347
  if (this.previousProposal?.slotNumber.equals(header.slotNumber)) {
@@ -345,6 +355,7 @@ export class ValidatorClient extends WithTracer implements Validator {
345
355
  archive,
346
356
  stateReference,
347
357
  txs,
358
+ proposerAddress,
348
359
  options,
349
360
  );
350
361
  this.previousProposal = newProposal;
@@ -355,7 +366,6 @@ export class ValidatorClient extends WithTracer implements Validator {
355
366
  await this.p2pClient.broadcastProposal(proposal);
356
367
  }
357
368
 
358
- // TODO(https://github.com/AztecProtocol/aztec-packages/issues/7962)
359
369
  async collectAttestations(proposal: BlockProposal, required: number, deadline: Date): Promise<BlockAttestation[]> {
360
370
  // Wait and poll the p2pClient's attestation pool for this block until we have enough attestations
361
371
  const slot = proposal.payload.header.slotNumber.toBigInt();
@@ -369,8 +379,11 @@ export class ValidatorClient extends WithTracer implements Validator {
369
379
  }
370
380
 
371
381
  const proposalId = proposal.archive.toString();
372
- await this.doAttestToProposal(proposal);
373
- const me = this.keyStore.getAddress();
382
+ // adds attestations for all of my addresses locally
383
+ const inCommittee = await this.epochCache.filterInCommittee(this.keyStore.getAddresses());
384
+ await this.doAttestToProposal(proposal, inCommittee);
385
+
386
+ const myAddresses = this.keyStore.getAddresses();
374
387
 
375
388
  let attestations: BlockAttestation[] = [];
376
389
  while (true) {
@@ -378,7 +391,10 @@ export class ValidatorClient extends WithTracer implements Validator {
378
391
  const oldSenders = attestations.map(attestation => attestation.getSender());
379
392
  for (const collected of collectedAttestations) {
380
393
  const collectedSender = collected.getSender();
381
- if (!collectedSender.equals(me) && !oldSenders.some(sender => sender.equals(collectedSender))) {
394
+ if (
395
+ !myAddresses.some(address => address.equals(collectedSender)) &&
396
+ !oldSenders.some(sender => sender.equals(collectedSender))
397
+ ) {
382
398
  this.log.debug(`Received attestation for slot ${slot} from ${collectedSender.toString()}`);
383
399
  }
384
400
  }
@@ -399,10 +415,10 @@ export class ValidatorClient extends WithTracer implements Validator {
399
415
  }
400
416
  }
401
417
 
402
- private async doAttestToProposal(proposal: BlockProposal): Promise<BlockAttestation> {
403
- const attestation = await this.validationService.attestToProposal(proposal);
404
- await this.p2pClient.addAttestation(attestation);
405
- return attestation;
418
+ private async doAttestToProposal(proposal: BlockProposal, attestors: EthAddress[] = []): Promise<BlockAttestation[]> {
419
+ const attestations = await this.validationService.attestToProposal(proposal, attestors);
420
+ await this.p2pClient.addAttestations(attestations);
421
+ return attestations;
406
422
  }
407
423
  }
408
424