@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/dest/validator.js CHANGED
@@ -1,53 +1,49 @@
1
- import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
2
- import { Fr } from '@aztec/foundation/fields';
3
1
  import { createLogger } from '@aztec/foundation/log';
4
- import { retryUntil } from '@aztec/foundation/retry';
5
2
  import { RunningPromise } from '@aztec/foundation/running-promise';
6
3
  import { sleep } from '@aztec/foundation/sleep';
7
- import { DateProvider, Timer } from '@aztec/foundation/timer';
8
- import { AuthRequest, AuthResponse, ReqRespSubProtocol } from '@aztec/p2p';
9
- import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
10
- import { computeInHashFromL1ToL2Messages } from '@aztec/prover-client/helpers';
4
+ import { DateProvider } from '@aztec/foundation/timer';
5
+ import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
11
6
  import { OffenseType, WANT_TO_SLASH_EVENT } from '@aztec/slasher';
12
- import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
13
- import { GlobalVariables } from '@aztec/stdlib/tx';
14
- import { AttestationTimeoutError, ReExFailedTxsError, ReExStateMismatchError, ReExTimeoutError, TransactionsNotAvailableError } from '@aztec/stdlib/validators';
7
+ import { AttestationTimeoutError } from '@aztec/stdlib/validators';
15
8
  import { getTelemetryClient } from '@aztec/telemetry-client';
16
9
  import { EventEmitter } from 'events';
10
+ import { BlockProposalHandler } from './block_proposal_handler.js';
17
11
  import { ValidationService } from './duties/validation_service.js';
18
12
  import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
19
13
  import { ValidatorMetrics } from './metrics.js';
20
14
  // We maintain a set of proposers who have proposed invalid blocks.
21
15
  // Just cap the set to avoid unbounded growth.
22
16
  const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
17
+ // What errors from the block proposal handler result in slashing
18
+ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
19
+ 'state_mismatch',
20
+ 'failed_txs'
21
+ ];
23
22
  /**
24
23
  * Validator Client
25
24
  */ export class ValidatorClient extends EventEmitter {
26
- blockBuilder;
27
25
  keyStore;
28
26
  epochCache;
29
27
  p2pClient;
30
- blockSource;
31
- l1ToL2MessageSource;
32
- txProvider;
28
+ blockProposalHandler;
33
29
  config;
34
30
  dateProvider;
35
31
  log;
36
32
  tracer;
37
33
  validationService;
38
34
  metrics;
35
+ // Whether it has already registered handlers on the p2p client
36
+ hasRegisteredHandlers;
39
37
  // Used to check if we are sending the same proposal twice
40
38
  previousProposal;
41
39
  lastEpochForCommitteeUpdateLoop;
42
40
  epochCacheUpdateLoop;
43
- blockProposalValidator;
44
41
  proposersOfInvalidBlocks;
45
- constructor(blockBuilder, keyStore, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, config, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
46
- super(), this.blockBuilder = blockBuilder, this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.blockSource = blockSource, this.l1ToL2MessageSource = l1ToL2MessageSource, this.txProvider = txProvider, this.config = config, this.dateProvider = dateProvider, this.log = log, this.proposersOfInvalidBlocks = new Set();
42
+ constructor(keyStore, epochCache, p2pClient, blockProposalHandler, config, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
43
+ super(), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.blockProposalHandler = blockProposalHandler, this.config = config, this.dateProvider = dateProvider, this.log = log, this.hasRegisteredHandlers = false, this.proposersOfInvalidBlocks = new Set();
47
44
  this.tracer = telemetry.getTracer('Validator');
48
45
  this.metrics = new ValidatorMetrics(telemetry);
49
46
  this.validationService = new ValidationService(keyStore);
50
- this.blockProposalValidator = new BlockProposalValidator(epochCache);
51
47
  // Refresh epoch cache every second to trigger alert if participation in committee changes
52
48
  this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), log, 1000);
53
49
  const myAddresses = this.getValidatorAddresses();
@@ -94,14 +90,22 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
94
90
  }
95
91
  }
96
92
  static new(config, blockBuilder, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, keyStoreManager, dateProvider = new DateProvider(), telemetry = getTelemetryClient()) {
97
- const validator = new ValidatorClient(blockBuilder, NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager), epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, config, dateProvider, telemetry);
98
- // TODO(PhilWindle): This seems like it could/should be done inside start()
99
- validator.registerBlockProposalHandler();
93
+ const metrics = new ValidatorMetrics(telemetry);
94
+ const blockProposalValidator = new BlockProposalValidator(epochCache);
95
+ const blockProposalHandler = new BlockProposalHandler(blockBuilder, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, config, metrics, dateProvider, telemetry);
96
+ const validator = new ValidatorClient(NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager), epochCache, p2pClient, blockProposalHandler, config, dateProvider, telemetry);
100
97
  return validator;
101
98
  }
102
99
  getValidatorAddresses() {
103
100
  return this.keyStore.getAddresses().filter((addr)=>!this.config.disabledValidators.some((disabled)=>disabled.equals(addr)));
104
101
  }
102
+ getBlockProposalHandler() {
103
+ return this.blockProposalHandler;
104
+ }
105
+ // Proxy method for backwards compatibility with tests
106
+ reExecuteTransactions(proposal, txs, l1ToL2Messages) {
107
+ return this.blockProposalHandler.reexecuteTransactions(proposal, txs, l1ToL2Messages);
108
+ }
105
109
  signWithAddress(addr, msg) {
106
110
  return this.keyStore.signTypedDataWithAddress(addr, msg);
107
111
  }
@@ -121,8 +125,11 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
121
125
  };
122
126
  }
123
127
  async start() {
124
- // Sync the committee from the smart contract
125
- // https://github.com/AztecProtocol/aztec-packages/issues/7962
128
+ if (this.epochCacheUpdateLoop.isRunning()) {
129
+ this.log.warn(`Validator client already started`);
130
+ return;
131
+ }
132
+ await this.registerHandlers();
126
133
  const myAddresses = this.getValidatorAddresses();
127
134
  const inCommittee = await this.epochCache.filterInCommittee('now', myAddresses);
128
135
  if (inCommittee.length > 0) {
@@ -131,185 +138,62 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
131
138
  this.log.info(`Started validator with addresses: ${myAddresses.map((a)=>a.toString()).join(', ')}`);
132
139
  }
133
140
  this.epochCacheUpdateLoop.start();
134
- this.p2pClient.registerThisValidatorAddresses(myAddresses);
135
- await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
136
141
  return Promise.resolve();
137
142
  }
138
143
  async stop() {
139
144
  await this.epochCacheUpdateLoop.stop();
140
145
  }
141
- registerBlockProposalHandler() {
142
- const handler = (block, proposalSender)=>this.attestToProposal(block, proposalSender);
143
- this.p2pClient.registerBlockProposalHandler(handler);
146
+ /** Register handlers on the p2p client */ async registerHandlers() {
147
+ if (!this.hasRegisteredHandlers) {
148
+ this.hasRegisteredHandlers = true;
149
+ this.log.debug(`Registering validator handlers for p2p client`);
150
+ const handler = (block, proposalSender)=>this.attestToProposal(block, proposalSender);
151
+ this.p2pClient.registerBlockProposalHandler(handler);
152
+ const myAddresses = this.getValidatorAddresses();
153
+ this.p2pClient.registerThisValidatorAddresses(myAddresses);
154
+ await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
155
+ }
144
156
  }
145
157
  async attestToProposal(proposal, proposalSender) {
146
158
  const slotNumber = proposal.slotNumber.toBigInt();
147
- const blockNumber = proposal.blockNumber;
148
159
  const proposer = proposal.getSender();
149
160
  // Check that I have any address in current committee before attesting
150
161
  const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
151
162
  const partOfCommittee = inCommittee.length > 0;
163
+ const incFailedAttestation = (reason)=>this.metrics.incFailedAttestations(1, reason, partOfCommittee);
152
164
  const proposalInfo = {
153
165
  ...proposal.toBlockInfo(),
154
166
  proposer: proposer.toString()
155
167
  };
156
- this.log.info(`Received proposal for slot ${slotNumber}`, {
168
+ this.log.info(`Received proposal for block ${proposal.blockNumber} at slot ${slotNumber}`, {
157
169
  ...proposalInfo,
158
- txHashes: proposal.txHashes.map((txHash)=>txHash.toString())
159
- });
160
- // Collect txs from the proposal. Note that we do this before checking if we have an address in the
161
- // current committee, since we want to collect txs anyway to facilitate propagation.
162
- const { txs, missingTxs } = await this.txProvider.getTxsForBlockProposal(proposal, {
163
- pinnedPeer: proposalSender,
164
- deadline: this.getReexecutionDeadline(proposal, this.blockBuilder.getConfig())
170
+ txHashes: proposal.txHashes.map((t)=>t.toString())
165
171
  });
166
- // Check that I have any address in current committee before attesting
167
- if (!partOfCommittee) {
168
- this.log.verbose(`No validator in the current committee, skipping attestation`, proposalInfo);
169
- return undefined;
170
- }
171
- // Check that the proposal is from the current proposer, or the next proposer.
172
- // Q: Should this be moved to the block proposal validator, so we disregard proposals from anyone?
173
- const invalidProposal = await this.blockProposalValidator.validate(proposal);
174
- if (invalidProposal) {
175
- this.log.warn(`Proposal is not valid, skipping attestation`, proposalInfo);
176
- if (partOfCommittee) {
177
- this.metrics.incFailedAttestations(1, 'invalid_proposal');
178
- }
179
- return undefined;
180
- }
181
- // Check that the parent proposal is a block we know, otherwise reexecution would fail.
182
- // Q: Should we move this to the block proposal validator? If there, then p2p would check it
183
- // before re-broadcasting it. This means that proposals built on top of an L1-reorg'ed-out block
184
- // would not be rebroadcasted. But it also means that nodes that have not fully synced would
185
- // not rebroadcast the proposal.
186
- if (blockNumber > INITIAL_L2_BLOCK_NUM) {
187
- const config = this.blockBuilder.getConfig();
188
- const deadline = this.getReexecutionDeadline(proposal, config);
189
- const currentTime = this.dateProvider.now();
190
- const timeoutDurationMs = deadline.getTime() - currentTime;
191
- const parentBlock = timeoutDurationMs <= 0 ? undefined : await retryUntil(async ()=>{
192
- const block = await this.blockSource.getBlock(blockNumber - 1);
193
- if (block) {
194
- return block;
195
- }
196
- await this.blockSource.syncImmediate();
197
- return await this.blockSource.getBlock(blockNumber - 1);
198
- }, 'Force Archiver Sync', timeoutDurationMs / 1000, 0.5);
199
- if (parentBlock === undefined) {
200
- this.log.warn(`Parent block for ${blockNumber} not found, skipping attestation`, proposalInfo);
201
- if (partOfCommittee) {
202
- this.metrics.incFailedAttestations(1, 'parent_block_not_found');
203
- }
204
- return undefined;
205
- }
206
- if (!proposal.payload.header.lastArchiveRoot.equals(parentBlock.archive.root)) {
207
- this.log.warn(`Parent block archive root for proposal does not match, skipping attestation`, {
208
- proposalLastArchiveRoot: proposal.payload.header.lastArchiveRoot.toString(),
209
- parentBlockArchiveRoot: parentBlock.archive.root.toString(),
210
- ...proposalInfo
211
- });
212
- if (partOfCommittee) {
213
- this.metrics.incFailedAttestations(1, 'parent_block_does_not_match');
214
- }
215
- return undefined;
216
- }
217
- }
218
- // Check that I have the same set of l1ToL2Messages as the proposal
219
- // Q: Same as above, should this be part of p2p validation?
220
- const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockNumber);
221
- const computedInHash = await computeInHashFromL1ToL2Messages(l1ToL2Messages);
222
- const proposalInHash = proposal.payload.header.contentCommitment.inHash;
223
- if (!computedInHash.equals(proposalInHash)) {
224
- this.log.warn(`L1 to L2 messages in hash mismatch, skipping attestation`, {
225
- proposalInHash: proposalInHash.toString(),
226
- computedInHash: computedInHash.toString(),
227
- ...proposalInfo
228
- });
229
- if (partOfCommittee) {
230
- this.metrics.incFailedAttestations(1, 'in_hash_mismatch');
231
- }
232
- return undefined;
233
- }
234
- // Check that all of the transactions in the proposal are available in the tx pool before attesting
235
- if (missingTxs.length > 0) {
236
- this.log.warn(`Missing ${missingTxs.length} txs to attest to proposal`, {
237
- ...proposalInfo,
238
- missingTxs
239
- });
240
- if (partOfCommittee) {
241
- this.metrics.incFailedAttestations(1, 'TransactionsNotAvailableError');
242
- }
243
- return undefined;
244
- }
245
- // Try re-executing the transactions in the proposal
246
- try {
247
- this.log.verbose(`Processing attestation for slot ${slotNumber}`, proposalInfo);
248
- if (this.config.validatorReexecute) {
249
- this.log.verbose(`Re-executing transactions in the proposal before attesting`);
250
- await this.reExecuteTransactions(proposal, txs, l1ToL2Messages);
251
- }
252
- } catch (error) {
253
- this.metrics.incFailedAttestations(1, error instanceof Error ? error.name : 'unknown');
254
- this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo);
255
- if (error instanceof ReExStateMismatchError && this.config.slashBroadcastedInvalidBlockPenalty > 0n) {
172
+ // Reexecute txs if we are part of the committee so we can attest, or if slashing is enabled so we can slash
173
+ // invalid proposals even when not in the committee, or if we are configured to always reexecute for monitoring purposes.
174
+ const { validatorReexecute, slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals } = this.config;
175
+ const shouldReexecute = slashBroadcastedInvalidBlockPenalty > 0n && validatorReexecute || partOfCommittee && validatorReexecute || alwaysReexecuteBlockProposals;
176
+ const validationResult = await this.blockProposalHandler.handleBlockProposal(proposal, proposalSender, !!shouldReexecute);
177
+ if (!validationResult.isValid) {
178
+ this.log.warn(`Proposal validation failed: ${validationResult.reason}`, proposalInfo);
179
+ incFailedAttestation(validationResult.reason || 'unknown');
180
+ // Slash invalid block proposals
181
+ if (validationResult.reason && SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) && slashBroadcastedInvalidBlockPenalty > 0n) {
256
182
  this.log.warn(`Slashing proposer for invalid block proposal`, proposalInfo);
257
183
  this.slashInvalidBlock(proposal);
258
184
  }
259
185
  return undefined;
260
186
  }
187
+ // Check that I have any address in current committee before attesting
188
+ if (!partOfCommittee) {
189
+ this.log.verbose(`No validator in the current committee, skipping attestation`, proposalInfo);
190
+ return undefined;
191
+ }
261
192
  // Provided all of the above checks pass, we can attest to the proposal
262
- this.log.info(`Attesting to proposal for slot ${slotNumber}`, proposalInfo);
193
+ this.log.info(`Attesting to proposal for block ${proposal.blockNumber} at slot ${slotNumber}`, proposalInfo);
263
194
  this.metrics.incAttestations(inCommittee.length);
264
195
  // If the above function does not throw an error, then we can attest to the proposal
265
- return this.doAttestToProposal(proposal, inCommittee);
266
- }
267
- getReexecutionDeadline(proposal, config) {
268
- const nextSlotTimestampSeconds = Number(getTimestampForSlot(proposal.slotNumber.toBigInt() + 1n, config));
269
- const msNeededForPropagationAndPublishing = this.config.validatorReexecuteDeadlineMs;
270
- return new Date(nextSlotTimestampSeconds * 1000 - msNeededForPropagationAndPublishing);
271
- }
272
- /**
273
- * Re-execute the transactions in the proposal and check that the state updates match the header state
274
- * @param proposal - The proposal to re-execute
275
- */ async reExecuteTransactions(proposal, txs, l1ToL2Messages) {
276
- const { header } = proposal.payload;
277
- const { txHashes } = proposal;
278
- // If we do not have all of the transactions, then we should fail
279
- if (txs.length !== txHashes.length) {
280
- const foundTxHashes = txs.map((tx)=>tx.getTxHash());
281
- const missingTxHashes = txHashes.filter((txHash)=>!foundTxHashes.includes(txHash));
282
- throw new TransactionsNotAvailableError(missingTxHashes);
283
- }
284
- // Use the sequencer's block building logic to re-execute the transactions
285
- const timer = new Timer();
286
- const config = this.blockBuilder.getConfig();
287
- const globalVariables = GlobalVariables.from({
288
- ...proposal.payload.header,
289
- blockNumber: proposal.blockNumber,
290
- timestamp: header.timestamp,
291
- chainId: new Fr(config.l1ChainId),
292
- version: new Fr(config.rollupVersion)
293
- });
294
- const { block, failedTxs } = await this.blockBuilder.buildBlock(txs, l1ToL2Messages, globalVariables, {
295
- deadline: this.getReexecutionDeadline(proposal, config)
296
- });
297
- this.log.verbose(`Transaction re-execution complete`);
298
- const numFailedTxs = failedTxs.length;
299
- if (numFailedTxs > 0) {
300
- this.metrics.recordFailedReexecution(proposal);
301
- throw new ReExFailedTxsError(numFailedTxs);
302
- }
303
- if (block.body.txEffects.length !== txHashes.length) {
304
- this.metrics.recordFailedReexecution(proposal);
305
- throw new ReExTimeoutError();
306
- }
307
- // This function will throw an error if state updates do not match
308
- if (!block.archive.root.equals(proposal.archive)) {
309
- this.metrics.recordFailedReexecution(proposal);
310
- throw new ReExStateMismatchError(proposal.archive, block.archive.root, proposal.payload.stateReference, block.header.state);
311
- }
312
- this.metrics.recordReex(timer.ms(), txs.length, block.header.totalManaUsed.toNumber() / 1e6);
196
+ return this.createBlockAttestationsFromProposal(proposal, inCommittee);
313
197
  }
314
198
  slashInvalidBlock(proposal) {
315
199
  const proposer = proposal.getSender();
@@ -340,13 +224,16 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
340
224
  async broadcastBlockProposal(proposal) {
341
225
  await this.p2pClient.broadcastProposal(proposal);
342
226
  }
227
+ async signAttestationsAndSigners(attestationsAndSigners, proposer) {
228
+ return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer);
229
+ }
343
230
  async collectOwnAttestations(proposal) {
344
231
  const slot = proposal.payload.header.slotNumber.toBigInt();
345
232
  const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
346
233
  this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, {
347
234
  inCommittee
348
235
  });
349
- return this.doAttestToProposal(proposal, inCommittee);
236
+ return this.createBlockAttestationsFromProposal(proposal, inCommittee);
350
237
  }
351
238
  async collectAttestations(proposal, required, deadline) {
352
239
  // Wait and poll the p2pClient's attestation pool for this block until we have enough attestations
@@ -382,7 +269,7 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
382
269
  await sleep(this.config.attestationPollingIntervalMs);
383
270
  }
384
271
  }
385
- async doAttestToProposal(proposal, attestors = []) {
272
+ async createBlockAttestationsFromProposal(proposal, attestors = []) {
386
273
  const attestations = await this.validationService.attestToProposal(proposal, attestors);
387
274
  await this.p2pClient.addAttestations(attestations);
388
275
  return attestations;
@@ -405,4 +292,4 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
405
292
  const authResponse = new AuthResponse(statusMessage, signature);
406
293
  return authResponse.toBuffer();
407
294
  }
408
- } // Conversion helpers moved into NodeKeystoreAdapter.
295
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/validator-client",
3
- "version": "2.0.3",
3
+ "version": "2.1.0-rc.2",
4
4
  "main": "dest/index.js",
5
5
  "type": "module",
6
6
  "exports": {
@@ -64,18 +64,18 @@
64
64
  ]
65
65
  },
66
66
  "dependencies": {
67
- "@aztec/constants": "2.0.3",
68
- "@aztec/epoch-cache": "2.0.3",
69
- "@aztec/ethereum": "2.0.3",
70
- "@aztec/foundation": "2.0.3",
71
- "@aztec/node-keystore": "2.0.3",
72
- "@aztec/p2p": "2.0.3",
73
- "@aztec/prover-client": "2.0.3",
74
- "@aztec/slasher": "2.0.3",
75
- "@aztec/stdlib": "2.0.3",
76
- "@aztec/telemetry-client": "2.0.3",
67
+ "@aztec/constants": "2.1.0-rc.2",
68
+ "@aztec/epoch-cache": "2.1.0-rc.2",
69
+ "@aztec/ethereum": "2.1.0-rc.2",
70
+ "@aztec/foundation": "2.1.0-rc.2",
71
+ "@aztec/node-keystore": "2.1.0-rc.2",
72
+ "@aztec/p2p": "2.1.0-rc.2",
73
+ "@aztec/prover-client": "2.1.0-rc.2",
74
+ "@aztec/slasher": "2.1.0-rc.2",
75
+ "@aztec/stdlib": "2.1.0-rc.2",
76
+ "@aztec/telemetry-client": "2.1.0-rc.2",
77
77
  "koa": "^2.16.1",
78
- "koa-router": "^12.0.0",
78
+ "koa-router": "^13.1.1",
79
79
  "tslib": "^2.4.0",
80
80
  "viem": "2.23.7"
81
81
  },