@aztec/validator-client 3.0.0-canary.a9708bd → 3.0.0-devnet.2-patch.1

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.
Files changed (43) hide show
  1. package/dest/block_proposal_handler.d.ts +53 -0
  2. package/dest/block_proposal_handler.d.ts.map +1 -0
  3. package/dest/block_proposal_handler.js +290 -0
  4. package/dest/config.d.ts +4 -20
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +18 -2
  7. package/dest/duties/validation_service.d.ts +11 -6
  8. package/dest/duties/validation_service.d.ts.map +1 -1
  9. package/dest/duties/validation_service.js +19 -7
  10. package/dest/factory.d.ts +14 -5
  11. package/dest/factory.d.ts.map +1 -1
  12. package/dest/factory.js +10 -0
  13. package/dest/index.d.ts +2 -1
  14. package/dest/index.d.ts.map +1 -1
  15. package/dest/index.js +1 -0
  16. package/dest/key_store/index.d.ts +1 -1
  17. package/dest/key_store/interface.d.ts +1 -1
  18. package/dest/key_store/local_key_store.d.ts +1 -1
  19. package/dest/key_store/local_key_store.d.ts.map +1 -1
  20. package/dest/key_store/local_key_store.js +1 -1
  21. package/dest/key_store/node_keystore_adapter.d.ts +1 -1
  22. package/dest/key_store/node_keystore_adapter.d.ts.map +1 -1
  23. package/dest/key_store/node_keystore_adapter.js +2 -4
  24. package/dest/key_store/web3signer_key_store.d.ts +1 -7
  25. package/dest/key_store/web3signer_key_store.d.ts.map +1 -1
  26. package/dest/key_store/web3signer_key_store.js +7 -8
  27. package/dest/metrics.d.ts +7 -5
  28. package/dest/metrics.d.ts.map +1 -1
  29. package/dest/metrics.js +25 -12
  30. package/dest/validator.d.ts +28 -38
  31. package/dest/validator.d.ts.map +1 -1
  32. package/dest/validator.js +182 -205
  33. package/package.json +15 -15
  34. package/src/block_proposal_handler.ts +346 -0
  35. package/src/config.ts +30 -25
  36. package/src/duties/validation_service.ts +30 -12
  37. package/src/factory.ts +34 -4
  38. package/src/index.ts +1 -0
  39. package/src/key_store/local_key_store.ts +1 -1
  40. package/src/key_store/node_keystore_adapter.ts +3 -4
  41. package/src/key_store/web3signer_key_store.ts +7 -10
  42. package/src/metrics.ts +34 -13
  43. package/src/validator.ts +252 -299
package/dest/validator.js CHANGED
@@ -1,68 +1,67 @@
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
- log;
36
31
  tracer;
37
32
  validationService;
38
33
  metrics;
34
+ log;
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
- myAddresses;
42
39
  lastEpochForCommitteeUpdateLoop;
43
40
  epochCacheUpdateLoop;
44
- blockProposalValidator;
45
41
  proposersOfInvalidBlocks;
46
- constructor(blockBuilder, keyStore, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, config, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
47
- 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.hasRegisteredHandlers = false, this.proposersOfInvalidBlocks = new Set();
44
+ // Create child logger with fisherman prefix if in fisherman mode
45
+ this.log = config.fishermanMode ? log.createChild('[FISHERMAN]') : log;
48
46
  this.tracer = telemetry.getTracer('Validator');
49
47
  this.metrics = new ValidatorMetrics(telemetry);
50
- this.validationService = new ValidationService(keyStore);
51
- this.blockProposalValidator = new BlockProposalValidator(epochCache);
48
+ this.validationService = new ValidationService(keyStore, this.log.createChild('validation-service'));
52
49
  // Refresh epoch cache every second to trigger alert if participation in committee changes
53
- this.myAddresses = this.keyStore.getAddresses();
54
- this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), log, 1000);
55
- this.log.verbose(`Initialized validator with addresses: ${this.myAddresses.map((a)=>a.toString()).join(', ')}`);
50
+ this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), this.log, 1000);
51
+ const myAddresses = this.getValidatorAddresses();
52
+ this.log.verbose(`Initialized validator with addresses: ${myAddresses.map((a)=>a.toString()).join(', ')}`);
56
53
  }
57
- static validateKeyStoreConfiguration(keyStoreManager) {
54
+ static validateKeyStoreConfiguration(keyStoreManager, logger) {
58
55
  const validatorKeyStore = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
59
56
  const validatorAddresses = validatorKeyStore.getAddresses();
60
57
  // Verify that we can retrieve all required data from the key store
61
58
  for (const address of validatorAddresses){
62
59
  // Functions throw if required data is not available
60
+ let coinbase;
61
+ let feeRecipient;
63
62
  try {
64
- validatorKeyStore.getCoinbaseAddress(address);
65
- validatorKeyStore.getFeeRecipient(address);
63
+ coinbase = validatorKeyStore.getCoinbaseAddress(address);
64
+ feeRecipient = validatorKeyStore.getFeeRecipient(address);
66
65
  } catch (error) {
67
66
  throw new Error(`Failed to retrieve required data for validator address ${address}, error: ${error}`);
68
67
  }
@@ -70,6 +69,7 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
70
69
  if (!publisherAddresses.length) {
71
70
  throw new Error(`No publisher addresses found for validator address ${address}`);
72
71
  }
72
+ logger?.debug(`Validator ${address.toString()} configured with coinbase ${coinbase.toString()}, feeRecipient ${feeRecipient.toString()} and publishers ${publisherAddresses.map((x)=>x.toString()).join()}`);
73
73
  }
74
74
  }
75
75
  async handleEpochCommitteeUpdate() {
@@ -80,7 +80,7 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
80
80
  return;
81
81
  }
82
82
  if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
83
- const me = this.myAddresses;
83
+ const me = this.getValidatorAddresses();
84
84
  const committeeSet = new Set(committee.map((v)=>v.toString()));
85
85
  const inCommittee = me.filter((a)=>committeeSet.has(a.toString()));
86
86
  if (inCommittee.length > 0) {
@@ -95,13 +95,23 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
95
95
  }
96
96
  }
97
97
  static new(config, blockBuilder, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, keyStoreManager, dateProvider = new DateProvider(), telemetry = getTelemetryClient()) {
98
- const validator = new ValidatorClient(blockBuilder, NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager), epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, config, dateProvider, telemetry);
99
- // TODO(PhilWindle): This seems like it could/should be done inside start()
100
- validator.registerBlockProposalHandler();
98
+ const metrics = new ValidatorMetrics(telemetry);
99
+ const blockProposalValidator = new BlockProposalValidator(epochCache, {
100
+ txsPermitted: !config.disableTransactions
101
+ });
102
+ const blockProposalHandler = new BlockProposalHandler(blockBuilder, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, config, metrics, dateProvider, telemetry);
103
+ const validator = new ValidatorClient(NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager), epochCache, p2pClient, blockProposalHandler, config, dateProvider, telemetry);
101
104
  return validator;
102
105
  }
103
106
  getValidatorAddresses() {
104
- return this.keyStore.getAddresses();
107
+ return this.keyStore.getAddresses().filter((addr)=>!this.config.disabledValidators.some((disabled)=>disabled.equals(addr)));
108
+ }
109
+ getBlockProposalHandler() {
110
+ return this.blockProposalHandler;
111
+ }
112
+ // Proxy method for backwards compatibility with tests
113
+ reExecuteTransactions(proposal, blockNumber, txs, l1ToL2Messages) {
114
+ return this.blockProposalHandler.reexecuteTransactions(proposal, blockNumber, txs, l1ToL2Messages);
105
115
  }
106
116
  signWithAddress(addr, msg) {
107
117
  return this.keyStore.signTypedDataWithAddress(addr, msg);
@@ -112,37 +122,54 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
112
122
  getFeeRecipientForAttestor(attestor) {
113
123
  return this.keyStore.getFeeRecipient(attestor);
114
124
  }
115
- configureSlashing(config) {
116
- this.config.slashBroadcastedInvalidBlockPenalty = config.slashBroadcastedInvalidBlockPenalty ?? this.config.slashBroadcastedInvalidBlockPenalty;
125
+ getConfig() {
126
+ return this.config;
127
+ }
128
+ updateConfig(config) {
129
+ this.config = {
130
+ ...this.config,
131
+ ...config
132
+ };
117
133
  }
118
134
  async start() {
119
- // Sync the committee from the smart contract
120
- // https://github.com/AztecProtocol/aztec-packages/issues/7962
121
- const myAddresses = this.keyStore.getAddresses();
135
+ if (this.epochCacheUpdateLoop.isRunning()) {
136
+ this.log.warn(`Validator client already started`);
137
+ return;
138
+ }
139
+ await this.registerHandlers();
140
+ const myAddresses = this.getValidatorAddresses();
122
141
  const inCommittee = await this.epochCache.filterInCommittee('now', myAddresses);
142
+ this.log.info(`Started validator with addresses: ${myAddresses.map((a)=>a.toString()).join(', ')}`);
123
143
  if (inCommittee.length > 0) {
124
- this.log.info(`Started validator with addresses in current validator committee: ${inCommittee.map((a)=>a.toString()).join(', ')}`);
125
- } else {
126
- this.log.info(`Started validator with addresses: ${myAddresses.map((a)=>a.toString()).join(', ')}`);
144
+ this.log.info(`Addresses in current validator committee: ${inCommittee.map((a)=>a.toString()).join(', ')}`);
127
145
  }
128
146
  this.epochCacheUpdateLoop.start();
129
- this.p2pClient.registerThisValidatorAddresses(myAddresses);
130
- await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
131
147
  return Promise.resolve();
132
148
  }
133
149
  async stop() {
134
150
  await this.epochCacheUpdateLoop.stop();
135
151
  }
136
- registerBlockProposalHandler() {
137
- const handler = (block, proposalSender)=>this.attestToProposal(block, proposalSender);
138
- this.p2pClient.registerBlockProposalHandler(handler);
152
+ /** Register handlers on the p2p client */ async registerHandlers() {
153
+ if (!this.hasRegisteredHandlers) {
154
+ this.hasRegisteredHandlers = true;
155
+ this.log.debug(`Registering validator handlers for p2p client`);
156
+ const handler = (block, proposalSender)=>this.attestToProposal(block, proposalSender);
157
+ this.p2pClient.registerBlockProposalHandler(handler);
158
+ const myAddresses = this.getValidatorAddresses();
159
+ this.p2pClient.registerThisValidatorAddresses(myAddresses);
160
+ await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
161
+ }
139
162
  }
140
163
  async attestToProposal(proposal, proposalSender) {
141
- const slotNumber = proposal.slotNumber.toBigInt();
142
- const blockNumber = proposal.blockNumber;
164
+ const slotNumber = proposal.slotNumber;
143
165
  const proposer = proposal.getSender();
166
+ // Reject proposals with invalid signatures
167
+ if (!proposer) {
168
+ this.log.warn(`Received proposal with invalid signature for slot ${slotNumber}`);
169
+ return undefined;
170
+ }
144
171
  // Check that I have any address in current committee before attesting
145
- const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.keyStore.getAddresses());
172
+ const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
146
173
  const partOfCommittee = inCommittee.length > 0;
147
174
  const proposalInfo = {
148
175
  ...proposal.toBlockInfo(),
@@ -150,164 +177,84 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
150
177
  };
151
178
  this.log.info(`Received proposal for slot ${slotNumber}`, {
152
179
  ...proposalInfo,
153
- txHashes: proposal.txHashes.map((txHash)=>txHash.toString())
180
+ txHashes: proposal.txHashes.map((t)=>t.toString()),
181
+ fishermanMode: this.config.fishermanMode || false
154
182
  });
155
- // Collect txs from the proposal. Note that we do this before checking if we have an address in the
156
- // current committee, since we want to collect txs anyway to facilitate propagation.
157
- const { txs, missingTxs } = await this.txProvider.getTxsForBlockProposal(proposal, {
158
- pinnedPeer: proposalSender,
159
- deadline: this.getReexecutionDeadline(proposal, this.blockBuilder.getConfig())
160
- });
161
- // Check that I have any address in current committee before attesting
162
- if (!partOfCommittee) {
163
- this.log.verbose(`No validator in the current committee, skipping attestation`, proposalInfo);
164
- return undefined;
165
- }
166
- // Check that the proposal is from the current proposer, or the next proposer.
167
- // Q: Should this be moved to the block proposal validator, so we disregard proposals from anyone?
168
- const invalidProposal = await this.blockProposalValidator.validate(proposal);
169
- if (invalidProposal) {
170
- this.log.warn(`Proposal is not valid, skipping attestation`, proposalInfo);
171
- if (partOfCommittee) {
172
- this.metrics.incFailedAttestations(1, 'invalid_proposal');
173
- }
174
- return undefined;
175
- }
176
- // Check that the parent proposal is a block we know, otherwise reexecution would fail.
177
- // Q: Should we move this to the block proposal validator? If there, then p2p would check it
178
- // before re-broadcasting it. This means that proposals built on top of an L1-reorg'ed-out block
179
- // would not be rebroadcasted. But it also means that nodes that have not fully synced would
180
- // not rebroadcast the proposal.
181
- if (blockNumber > INITIAL_L2_BLOCK_NUM) {
182
- const config = this.blockBuilder.getConfig();
183
- const deadline = this.getReexecutionDeadline(proposal, config);
184
- const currentTime = this.dateProvider.now();
185
- const timeoutDurationMs = deadline.getTime() - currentTime;
186
- const parentBlock = timeoutDurationMs <= 0 ? undefined : await retryUntil(async ()=>{
187
- const block = await this.blockSource.getBlock(blockNumber - 1);
188
- if (block) {
189
- return block;
190
- }
191
- await this.blockSource.syncImmediate();
192
- return await this.blockSource.getBlock(blockNumber - 1);
193
- }, 'Force Archiver Sync', timeoutDurationMs / 1000, 0.5);
194
- if (parentBlock === undefined) {
195
- this.log.warn(`Parent block for ${blockNumber} not found, skipping attestation`, proposalInfo);
196
- if (partOfCommittee) {
197
- this.metrics.incFailedAttestations(1, 'parent_block_not_found');
198
- }
199
- return undefined;
200
- }
201
- if (!proposal.payload.header.lastArchiveRoot.equals(parentBlock.archive.root)) {
202
- this.log.warn(`Parent block archive root for proposal does not match, skipping attestation`, {
203
- proposalLastArchiveRoot: proposal.payload.header.lastArchiveRoot.toString(),
204
- parentBlockArchiveRoot: parentBlock.archive.root.toString(),
205
- ...proposalInfo
206
- });
207
- if (partOfCommittee) {
208
- this.metrics.incFailedAttestations(1, 'parent_block_does_not_match');
209
- }
210
- return undefined;
211
- }
212
- }
213
- // Check that I have the same set of l1ToL2Messages as the proposal
214
- // Q: Same as above, should this be part of p2p validation?
215
- const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockNumber);
216
- const computedInHash = await computeInHashFromL1ToL2Messages(l1ToL2Messages);
217
- const proposalInHash = proposal.payload.header.contentCommitment.inHash;
218
- if (!computedInHash.equals(proposalInHash)) {
219
- this.log.warn(`L1 to L2 messages in hash mismatch, skipping attestation`, {
220
- proposalInHash: proposalInHash.toString(),
221
- computedInHash: computedInHash.toString(),
222
- ...proposalInfo
223
- });
224
- if (partOfCommittee) {
225
- this.metrics.incFailedAttestations(1, 'in_hash_mismatch');
226
- }
227
- return undefined;
228
- }
229
- // Check that all of the transactions in the proposal are available in the tx pool before attesting
230
- if (missingTxs.length > 0) {
231
- this.log.warn(`Missing ${missingTxs.length} txs to attest to proposal`, {
232
- ...proposalInfo,
233
- missingTxs
234
- });
235
- if (partOfCommittee) {
236
- this.metrics.incFailedAttestations(1, 'TransactionsNotAvailableError');
237
- }
238
- return undefined;
239
- }
240
- // Try re-executing the transactions in the proposal
241
- try {
242
- this.log.verbose(`Processing attestation for slot ${slotNumber}`, proposalInfo);
243
- if (this.config.validatorReexecute) {
244
- this.log.verbose(`Re-executing transactions in the proposal before attesting`);
245
- await this.reExecuteTransactions(proposal, txs, l1ToL2Messages);
183
+ // Reexecute txs if we are part of the committee so we can attest, or if slashing is enabled so we can slash
184
+ // invalid proposals even when not in the committee, or if we are configured to always reexecute for monitoring purposes.
185
+ // In fisherman mode, we always reexecute to validate proposals.
186
+ const { validatorReexecute, slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } = this.config;
187
+ const shouldReexecute = fishermanMode || slashBroadcastedInvalidBlockPenalty > 0n && validatorReexecute || partOfCommittee && validatorReexecute || alwaysReexecuteBlockProposals;
188
+ const validationResult = await this.blockProposalHandler.handleBlockProposal(proposal, proposalSender, !!shouldReexecute);
189
+ if (!validationResult.isValid) {
190
+ this.log.warn(`Proposal validation failed: ${validationResult.reason}`, proposalInfo);
191
+ const reason = validationResult.reason || 'unknown';
192
+ // Classify failure reason: bad proposal vs node issue
193
+ const badProposalReasons = [
194
+ 'invalid_proposal',
195
+ 'state_mismatch',
196
+ 'failed_txs',
197
+ 'in_hash_mismatch',
198
+ 'parent_block_wrong_slot'
199
+ ];
200
+ if (badProposalReasons.includes(reason)) {
201
+ this.metrics.incFailedAttestationsBadProposal(1, reason, partOfCommittee);
202
+ } else {
203
+ // Node issues so we can't attest
204
+ this.metrics.incFailedAttestationsNodeIssue(1, reason, partOfCommittee);
246
205
  }
247
- } catch (error) {
248
- this.metrics.incFailedAttestations(1, error instanceof Error ? error.name : 'unknown');
249
- this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo);
250
- if (error instanceof ReExStateMismatchError && this.config.slashBroadcastedInvalidBlockPenalty > 0n) {
206
+ // Slash invalid block proposals (can happen even when not in committee)
207
+ if (validationResult.reason && SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) && slashBroadcastedInvalidBlockPenalty > 0n) {
251
208
  this.log.warn(`Slashing proposer for invalid block proposal`, proposalInfo);
252
209
  this.slashInvalidBlock(proposal);
253
210
  }
254
211
  return undefined;
255
212
  }
256
- // Provided all of the above checks pass, we can attest to the proposal
257
- this.log.info(`Attesting to proposal for slot ${slotNumber}`, proposalInfo);
258
- this.metrics.incAttestations(inCommittee.length);
259
- // If the above function does not throw an error, then we can attest to the proposal
260
- return this.doAttestToProposal(proposal, inCommittee);
261
- }
262
- getReexecutionDeadline(proposal, config) {
263
- const nextSlotTimestampSeconds = Number(getTimestampForSlot(proposal.slotNumber.toBigInt() + 1n, config));
264
- const msNeededForPropagationAndPublishing = this.config.validatorReexecuteDeadlineMs;
265
- return new Date(nextSlotTimestampSeconds * 1000 - msNeededForPropagationAndPublishing);
266
- }
267
- /**
268
- * Re-execute the transactions in the proposal and check that the state updates match the header state
269
- * @param proposal - The proposal to re-execute
270
- */ async reExecuteTransactions(proposal, txs, l1ToL2Messages) {
271
- const { header } = proposal.payload;
272
- const { txHashes } = proposal;
273
- // If we do not have all of the transactions, then we should fail
274
- if (txs.length !== txHashes.length) {
275
- const foundTxHashes = txs.map((tx)=>tx.getTxHash());
276
- const missingTxHashes = txHashes.filter((txHash)=>!foundTxHashes.includes(txHash));
277
- throw new TransactionsNotAvailableError(missingTxHashes);
213
+ // Check that I have any address in current committee before attesting
214
+ // In fisherman mode, we still create attestations for validation even if not in committee
215
+ if (!partOfCommittee && !this.config.fishermanMode) {
216
+ this.log.verbose(`No validator in the current committee, skipping attestation`, proposalInfo);
217
+ return undefined;
278
218
  }
279
- // Use the sequencer's block building logic to re-execute the transactions
280
- const timer = new Timer();
281
- const config = this.blockBuilder.getConfig();
282
- const globalVariables = GlobalVariables.from({
283
- ...proposal.payload.header,
284
- blockNumber: proposal.blockNumber,
285
- timestamp: header.timestamp,
286
- chainId: new Fr(config.l1ChainId),
287
- version: new Fr(config.rollupVersion)
288
- });
289
- const { block, failedTxs } = await this.blockBuilder.buildBlock(txs, l1ToL2Messages, globalVariables, {
290
- deadline: this.getReexecutionDeadline(proposal, config)
219
+ // Provided all of the above checks pass, we can attest to the proposal
220
+ this.log.info(`${partOfCommittee ? 'Attesting to' : 'Validated'} proposal for slot ${slotNumber}`, {
221
+ ...proposalInfo,
222
+ inCommittee: partOfCommittee,
223
+ fishermanMode: this.config.fishermanMode || false
291
224
  });
292
- this.log.verbose(`Transaction re-execution complete`);
293
- const numFailedTxs = failedTxs.length;
294
- if (numFailedTxs > 0) {
295
- this.metrics.recordFailedReexecution(proposal);
296
- throw new ReExFailedTxsError(numFailedTxs);
225
+ this.metrics.incSuccessfulAttestations(inCommittee.length);
226
+ // If the above function does not throw an error, then we can attest to the proposal
227
+ // Determine which validators should attest
228
+ let attestors;
229
+ if (partOfCommittee) {
230
+ attestors = inCommittee;
231
+ } else if (this.config.fishermanMode) {
232
+ // In fisherman mode, create attestations for validation purposes even if not in committee. These won't be broadcast.
233
+ attestors = this.getValidatorAddresses();
234
+ } else {
235
+ attestors = [];
297
236
  }
298
- if (block.body.txEffects.length !== txHashes.length) {
299
- this.metrics.recordFailedReexecution(proposal);
300
- throw new ReExTimeoutError();
237
+ // Only create attestations if we have attestors
238
+ if (attestors.length === 0) {
239
+ return undefined;
301
240
  }
302
- // This function will throw an error if state updates do not match
303
- if (!block.archive.root.equals(proposal.archive)) {
304
- this.metrics.recordFailedReexecution(proposal);
305
- throw new ReExStateMismatchError(proposal.archive, block.archive.root, proposal.payload.stateReference, block.header.state);
241
+ if (this.config.fishermanMode) {
242
+ // bail out early and don't save attestations to the pool in fisherman mode
243
+ this.log.info(`Creating attestations for proposal for slot ${slotNumber}`, {
244
+ ...proposalInfo,
245
+ attestors: attestors.map((a)=>a.toString())
246
+ });
247
+ return undefined;
306
248
  }
307
- this.metrics.recordReex(timer.ms(), txs.length, block.header.totalManaUsed.toNumber() / 1e6);
249
+ return this.createBlockAttestationsFromProposal(proposal, attestors);
308
250
  }
309
251
  slashInvalidBlock(proposal) {
310
252
  const proposer = proposal.getSender();
253
+ // Skip if signature is invalid (shouldn't happen since we validate earlier)
254
+ if (!proposer) {
255
+ this.log.warn(`Cannot slash proposal with invalid signature`);
256
+ return;
257
+ }
311
258
  // Trim the set if it's too big.
312
259
  if (this.proposersOfInvalidBlocks.size > MAX_PROPOSERS_OF_INVALID_BLOCKS) {
313
260
  // remove oldest proposer. `values` is guaranteed to be in insertion order.
@@ -319,33 +266,46 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
319
266
  validator: proposer,
320
267
  amount: this.config.slashBroadcastedInvalidBlockPenalty,
321
268
  offenseType: OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL,
322
- epochOrSlot: proposal.slotNumber.toBigInt()
269
+ epochOrSlot: BigInt(proposal.slotNumber)
323
270
  }
324
271
  ]);
325
272
  }
326
- async createBlockProposal(blockNumber, header, archive, stateReference, txs, proposerAddress, options) {
327
- if (this.previousProposal?.slotNumber.equals(header.slotNumber)) {
273
+ async createBlockProposal(blockNumber, header, archive, txs, proposerAddress, options) {
274
+ if (this.previousProposal?.slotNumber === header.slotNumber) {
328
275
  this.log.verbose(`Already made a proposal for the same slot, skipping proposal`);
329
276
  return Promise.resolve(undefined);
330
277
  }
331
- const newProposal = await this.validationService.createBlockProposal(blockNumber, header, archive, stateReference, txs, proposerAddress, options);
278
+ const newProposal = await this.validationService.createBlockProposal(header, archive, txs, proposerAddress, {
279
+ ...options,
280
+ broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal
281
+ });
332
282
  this.previousProposal = newProposal;
333
283
  return newProposal;
334
284
  }
335
285
  async broadcastBlockProposal(proposal) {
336
286
  await this.p2pClient.broadcastProposal(proposal);
337
287
  }
288
+ async signAttestationsAndSigners(attestationsAndSigners, proposer) {
289
+ return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer);
290
+ }
338
291
  async collectOwnAttestations(proposal) {
339
- const slot = proposal.payload.header.slotNumber.toBigInt();
340
- const inCommittee = await this.epochCache.filterInCommittee(slot, this.keyStore.getAddresses());
292
+ const slot = proposal.payload.header.slotNumber;
293
+ const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
341
294
  this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, {
342
295
  inCommittee
343
296
  });
344
- return this.doAttestToProposal(proposal, inCommittee);
297
+ const attestations = await this.createBlockAttestationsFromProposal(proposal, inCommittee);
298
+ // We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
299
+ // other nodes can see that our validators did attest to this block proposal, and do not slash us
300
+ // due to inactivity for missed attestations.
301
+ void this.p2pClient.broadcastAttestations(attestations).catch((err)=>{
302
+ this.log.error(`Failed to broadcast self-attestations for slot ${slot}`, err);
303
+ });
304
+ return attestations;
345
305
  }
346
306
  async collectAttestations(proposal, required, deadline) {
347
307
  // Wait and poll the p2pClient's attestation pool for this block until we have enough attestations
348
- const slot = proposal.payload.header.slotNumber.toBigInt();
308
+ const slot = proposal.payload.header.slotNumber;
349
309
  this.log.debug(`Collecting ${required} attestations for slot ${slot} with deadline ${deadline.toISOString()}`);
350
310
  if (+deadline < this.dateProvider.now()) {
351
311
  this.log.error(`Deadline ${deadline.toISOString()} for collecting ${required} attestations for slot ${slot} is in the past`);
@@ -353,14 +313,31 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
353
313
  }
354
314
  await this.collectOwnAttestations(proposal);
355
315
  const proposalId = proposal.archive.toString();
356
- const myAddresses = this.keyStore.getAddresses();
316
+ const myAddresses = this.getValidatorAddresses();
357
317
  let attestations = [];
358
318
  while(true){
359
- const collectedAttestations = await this.p2pClient.getAttestationsForSlot(slot, proposalId);
319
+ // Filter out attestations with a mismatching payload. This should NOT happen since we have verified
320
+ // the proposer signature (ie our own) before accepting the attestation into the pool via the p2p client.
321
+ const collectedAttestations = (await this.p2pClient.getAttestationsForSlot(slot, proposalId)).filter((attestation)=>{
322
+ if (!attestation.payload.equals(proposal.payload)) {
323
+ this.log.warn(`Received attestation for slot ${slot} with mismatched payload from ${attestation.getSender()?.toString()}`, {
324
+ attestationPayload: attestation.payload,
325
+ proposalPayload: proposal.payload
326
+ });
327
+ return false;
328
+ }
329
+ return true;
330
+ });
331
+ // Log new attestations we collected
360
332
  const oldSenders = attestations.map((attestation)=>attestation.getSender());
361
333
  for (const collected of collectedAttestations){
362
334
  const collectedSender = collected.getSender();
363
- if (!myAddresses.some((address)=>address.equals(collectedSender)) && !oldSenders.some((sender)=>sender.equals(collectedSender))) {
335
+ // Skip attestations with invalid signatures
336
+ if (!collectedSender) {
337
+ this.log.warn(`Skipping attestation with invalid signature for slot ${slot}`);
338
+ continue;
339
+ }
340
+ if (!myAddresses.some((address)=>address.equals(collectedSender)) && !oldSenders.some((sender)=>sender?.equals(collectedSender))) {
364
341
  this.log.debug(`Received attestation for slot ${slot} from ${collectedSender.toString()}`);
365
342
  }
366
343
  }
@@ -373,11 +350,11 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
373
350
  this.log.error(`Timeout ${deadline.toISOString()} waiting for ${required} attestations for slot ${slot}`);
374
351
  throw new AttestationTimeoutError(attestations.length, required, slot);
375
352
  }
376
- this.log.debug(`Collected ${attestations.length} attestations so far`);
353
+ this.log.debug(`Collected ${attestations.length} of ${required} attestations so far`);
377
354
  await sleep(this.config.attestationPollingIntervalMs);
378
355
  }
379
356
  }
380
- async doAttestToProposal(proposal, attestors = []) {
357
+ async createBlockAttestationsFromProposal(proposal, attestors = []) {
381
358
  const attestations = await this.validationService.attestToProposal(proposal, attestors);
382
359
  await this.p2pClient.addAttestations(attestations);
383
360
  return attestations;
@@ -400,4 +377,4 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
400
377
  const authResponse = new AuthResponse(statusMessage, signature);
401
378
  return authResponse.toBuffer();
402
379
  }
403
- } // Conversion helpers moved into NodeKeystoreAdapter.
380
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/validator-client",
3
- "version": "3.0.0-canary.a9708bd",
3
+ "version": "3.0.0-devnet.2-patch.1",
4
4
  "main": "dest/index.js",
5
5
  "type": "module",
6
6
  "exports": {
@@ -18,8 +18,8 @@
18
18
  },
19
19
  "scripts": {
20
20
  "start": "node --no-warnings ./dest/bin",
21
- "build": "yarn clean && tsc -b",
22
- "build:dev": "tsc -b --watch",
21
+ "build": "yarn clean && ../scripts/tsc.sh",
22
+ "build:dev": "../scripts/tsc.sh --watch",
23
23
  "clean": "rm -rf ./dest .tsbuildinfo",
24
24
  "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests --maxWorkers=${JEST_MAX_WORKERS:-8}"
25
25
  },
@@ -64,25 +64,25 @@
64
64
  ]
65
65
  },
66
66
  "dependencies": {
67
- "@aztec/constants": "3.0.0-canary.a9708bd",
68
- "@aztec/epoch-cache": "3.0.0-canary.a9708bd",
69
- "@aztec/ethereum": "3.0.0-canary.a9708bd",
70
- "@aztec/foundation": "3.0.0-canary.a9708bd",
71
- "@aztec/node-keystore": "3.0.0-canary.a9708bd",
72
- "@aztec/p2p": "3.0.0-canary.a9708bd",
73
- "@aztec/prover-client": "3.0.0-canary.a9708bd",
74
- "@aztec/slasher": "3.0.0-canary.a9708bd",
75
- "@aztec/stdlib": "3.0.0-canary.a9708bd",
76
- "@aztec/telemetry-client": "3.0.0-canary.a9708bd",
67
+ "@aztec/constants": "3.0.0-devnet.2-patch.1",
68
+ "@aztec/epoch-cache": "3.0.0-devnet.2-patch.1",
69
+ "@aztec/ethereum": "3.0.0-devnet.2-patch.1",
70
+ "@aztec/foundation": "3.0.0-devnet.2-patch.1",
71
+ "@aztec/node-keystore": "3.0.0-devnet.2-patch.1",
72
+ "@aztec/p2p": "3.0.0-devnet.2-patch.1",
73
+ "@aztec/slasher": "3.0.0-devnet.2-patch.1",
74
+ "@aztec/stdlib": "3.0.0-devnet.2-patch.1",
75
+ "@aztec/telemetry-client": "3.0.0-devnet.2-patch.1",
77
76
  "koa": "^2.16.1",
78
- "koa-router": "^12.0.0",
77
+ "koa-router": "^13.1.1",
79
78
  "tslib": "^2.4.0",
80
- "viem": "2.23.7"
79
+ "viem": "npm:@aztec/viem@2.38.2"
81
80
  },
82
81
  "devDependencies": {
83
82
  "@jest/globals": "^30.0.0",
84
83
  "@types/jest": "^30.0.0",
85
84
  "@types/node": "^22.15.17",
85
+ "@typescript/native-preview": "7.0.0-dev.20251126.1",
86
86
  "jest": "^30.0.0",
87
87
  "jest-mock-extended": "^4.0.0",
88
88
  "ts-node": "^10.9.1",