@aztec/validator-client 1.2.0 → 2.0.0-nightly.20250813

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
@@ -6,10 +6,11 @@ import { retryUntil } from '@aztec/foundation/retry';
6
6
  import { RunningPromise } from '@aztec/foundation/running-promise';
7
7
  import { sleep } from '@aztec/foundation/sleep';
8
8
  import { DateProvider, Timer } from '@aztec/foundation/timer';
9
- import { TxCollector } from '@aztec/p2p';
9
+ import { AuthRequest, AuthResponse, ReqRespSubProtocol } from '@aztec/p2p';
10
10
  import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
11
11
  import { computeInHashFromL1ToL2Messages } from '@aztec/prover-client/helpers';
12
- import { Offense, WANT_TO_SLASH_EVENT } from '@aztec/slasher/config';
12
+ import { Offense } from '@aztec/slasher';
13
+ import { WANT_TO_SLASH_EVENT } from '@aztec/slasher/config';
13
14
  import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
14
15
  import { GlobalVariables } from '@aztec/stdlib/tx';
15
16
  import { AttestationTimeoutError, InvalidValidatorPrivateKeyError, ReExFailedTxsError, ReExStateMismatchError, ReExTimeoutError, TransactionsNotAvailableError } from '@aztec/stdlib/validators';
@@ -17,6 +18,7 @@ import { getTelemetryClient } from '@aztec/telemetry-client';
17
18
  import { EventEmitter } from 'events';
18
19
  import { ValidationService } from './duties/validation_service.js';
19
20
  import { LocalKeyStore } from './key_store/local_key_store.js';
21
+ import { Web3SignerKeyStore } from './key_store/web3signer_key_store.js';
20
22
  import { ValidatorMetrics } from './metrics.js';
21
23
  // We maintain a set of proposers who have proposed invalid blocks.
22
24
  // Just cap the set to avoid unbounded growth.
@@ -30,6 +32,7 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
30
32
  p2pClient;
31
33
  blockSource;
32
34
  l1ToL2MessageSource;
35
+ txProvider;
33
36
  config;
34
37
  dateProvider;
35
38
  log;
@@ -39,18 +42,16 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
39
42
  // Used to check if we are sending the same proposal twice
40
43
  previousProposal;
41
44
  myAddresses;
42
- lastEpoch;
45
+ lastEpochForCommitteeUpdateLoop;
43
46
  epochCacheUpdateLoop;
44
47
  blockProposalValidator;
45
- txCollector;
46
48
  proposersOfInvalidBlocks;
47
- constructor(blockBuilder, keyStore, epochCache, p2pClient, blockSource, l1ToL2MessageSource, config, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
48
- super(), this.blockBuilder = blockBuilder, this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.blockSource = blockSource, this.l1ToL2MessageSource = l1ToL2MessageSource, this.config = config, this.dateProvider = dateProvider, this.log = log, this.proposersOfInvalidBlocks = new Set();
49
+ constructor(blockBuilder, keyStore, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, config, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
50
+ 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();
49
51
  this.tracer = telemetry.getTracer('Validator');
50
52
  this.metrics = new ValidatorMetrics(telemetry);
51
53
  this.validationService = new ValidationService(keyStore);
52
54
  this.blockProposalValidator = new BlockProposalValidator(epochCache);
53
- this.txCollector = new TxCollector(p2pClient, this.log);
54
55
  // Refresh epoch cache every second to trigger alert if participation in committee changes
55
56
  this.myAddresses = this.keyStore.getAddresses();
56
57
  this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), log, 1000);
@@ -58,12 +59,12 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
58
59
  }
59
60
  async handleEpochCommitteeUpdate() {
60
61
  try {
61
- const { committee, epoch } = await this.epochCache.getCommittee('now');
62
+ const { committee, epoch } = await this.epochCache.getCommittee('next');
62
63
  if (!committee) {
63
64
  this.log.trace(`No committee found for slot`);
64
65
  return;
65
66
  }
66
- if (epoch !== this.lastEpoch) {
67
+ if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
67
68
  const me = this.myAddresses;
68
69
  const committeeSet = new Set(committee.map((v)=>v.toString()));
69
70
  const inCommittee = me.filter((a)=>committeeSet.has(a.toString()));
@@ -72,19 +73,29 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
72
73
  } else {
73
74
  this.log.verbose(`Validators ${me.map((a)=>a.toString()).join(', ')} are not on the validator committee for epoch ${epoch}`);
74
75
  }
75
- this.lastEpoch = epoch;
76
+ this.lastEpochForCommitteeUpdateLoop = epoch;
76
77
  }
77
78
  } catch (err) {
78
79
  this.log.error(`Error updating epoch committee`, err);
79
80
  }
80
81
  }
81
- static new(config, blockBuilder, epochCache, p2pClient, blockSource, l1ToL2MessageSource, dateProvider = new DateProvider(), telemetry = getTelemetryClient()) {
82
- if (!config.validatorPrivateKeys.getValue().length) {
83
- throw new InvalidValidatorPrivateKeyError();
82
+ static new(config, blockBuilder, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, dateProvider = new DateProvider(), telemetry = getTelemetryClient()) {
83
+ let keyStore;
84
+ if (config.web3SignerUrl) {
85
+ const addresses = config.web3SignerAddresses;
86
+ if (!addresses?.length) {
87
+ throw new Error('web3SignerAddresses is required when web3SignerUrl is provided');
88
+ }
89
+ keyStore = new Web3SignerKeyStore(addresses, config.web3SignerUrl);
90
+ } else {
91
+ const privateKeys = config.validatorPrivateKeys?.getValue().map(validatePrivateKey);
92
+ if (!privateKeys?.length) {
93
+ throw new InvalidValidatorPrivateKeyError();
94
+ }
95
+ keyStore = new LocalKeyStore(privateKeys);
84
96
  }
85
- const privateKeys = config.validatorPrivateKeys.getValue().map(validatePrivateKey);
86
- const localKeyStore = new LocalKeyStore(privateKeys);
87
- const validator = new ValidatorClient(blockBuilder, localKeyStore, epochCache, p2pClient, blockSource, l1ToL2MessageSource, config, dateProvider, telemetry);
97
+ const validator = new ValidatorClient(blockBuilder, keyStore, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, config, dateProvider, telemetry);
98
+ // TODO(PhilWindle): This seems like it could/should be done inside start()
88
99
  validator.registerBlockProposalHandler();
89
100
  return validator;
90
101
  }
@@ -92,7 +103,7 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
92
103
  return this.keyStore.getAddresses();
93
104
  }
94
105
  signWithAddress(addr, msg) {
95
- return this.keyStore.signWithAddress(addr, msg);
106
+ return this.keyStore.signTypedDataWithAddress(addr, msg);
96
107
  }
97
108
  configureSlashing(config) {
98
109
  this.config.slashInvalidBlockEnabled = config.slashInvalidBlockEnabled ?? this.config.slashInvalidBlockEnabled;
@@ -103,45 +114,55 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
103
114
  // Sync the committee from the smart contract
104
115
  // https://github.com/AztecProtocol/aztec-packages/issues/7962
105
116
  const myAddresses = this.keyStore.getAddresses();
106
- const inCommittee = await this.epochCache.filterInCommittee(myAddresses);
117
+ const inCommittee = await this.epochCache.filterInCommittee('now', myAddresses);
107
118
  if (inCommittee.length > 0) {
108
119
  this.log.info(`Started validator with addresses in current validator committee: ${inCommittee.map((a)=>a.toString()).join(', ')}`);
109
120
  } else {
110
121
  this.log.info(`Started validator with addresses: ${myAddresses.map((a)=>a.toString()).join(', ')}`);
111
122
  }
112
123
  this.epochCacheUpdateLoop.start();
124
+ this.p2pClient.registerThisValidatorAddresses(myAddresses);
125
+ await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
113
126
  return Promise.resolve();
114
127
  }
115
128
  async stop() {
116
129
  await this.epochCacheUpdateLoop.stop();
117
130
  }
118
131
  registerBlockProposalHandler() {
119
- const handler = (block, proposalSender)=>{
120
- return this.attestToProposal(block, proposalSender);
121
- };
132
+ const handler = (block, proposalSender)=>this.attestToProposal(block, proposalSender);
122
133
  this.p2pClient.registerBlockProposalHandler(handler);
123
134
  }
124
135
  async attestToProposal(proposal, proposalSender) {
125
- const slotNumber = proposal.slotNumber.toNumber();
136
+ const slotNumber = proposal.slotNumber.toBigInt();
126
137
  const blockNumber = proposal.blockNumber;
127
138
  const proposer = proposal.getSender();
128
139
  // Check that I have any address in current committee before attesting
129
- const inCommittee = await this.epochCache.filterInCommittee(this.keyStore.getAddresses());
140
+ const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.keyStore.getAddresses());
130
141
  const partOfCommittee = inCommittee.length > 0;
131
142
  const proposalInfo = {
132
- slotNumber,
133
- blockNumber,
134
- proposer: proposer.toString(),
135
- archive: proposal.payload.archive.toString(),
136
- txCount: proposal.payload.txHashes.length,
137
- txHashes: proposal.payload.txHashes.map((txHash)=>txHash.toString())
143
+ ...proposal.toBlockInfo(),
144
+ proposer: proposer.toString()
138
145
  };
139
- this.log.info(`Received request to attest for slot ${slotNumber}`, proposalInfo);
146
+ this.log.info(`Received proposal for slot ${slotNumber}`, {
147
+ ...proposalInfo,
148
+ txHashes: proposal.txHashes.map((txHash)=>txHash.toString())
149
+ });
150
+ // Collect txs from the proposal. Note that we do this before checking if we have an address in the
151
+ // current committee, since we want to collect txs anyway to facilitate propagation.
152
+ const { txs, missingTxs } = await this.txProvider.getTxsForBlockProposal(proposal, {
153
+ pinnedPeer: proposalSender,
154
+ deadline: this.getReexecutionDeadline(proposal, this.blockBuilder.getConfig())
155
+ });
156
+ // Check that I have any address in current committee before attesting
157
+ if (!partOfCommittee) {
158
+ this.log.verbose(`No validator in the current committee, skipping attestation`, proposalInfo);
159
+ return undefined;
160
+ }
140
161
  // Check that the proposal is from the current proposer, or the next proposer.
141
162
  // Q: Should this be moved to the block proposal validator, so we disregard proposals from anyone?
142
163
  const invalidProposal = await this.blockProposalValidator.validate(proposal);
143
164
  if (invalidProposal) {
144
- this.log.warn(`Proposal is not valid, skipping attestation`);
165
+ this.log.warn(`Proposal is not valid, skipping attestation`, proposalInfo);
145
166
  if (partOfCommittee) {
146
167
  this.metrics.incFailedAttestations(1, 'invalid_proposal');
147
168
  }
@@ -166,7 +187,7 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
166
187
  return await this.blockSource.getBlock(blockNumber - 1);
167
188
  }, 'Force Archiver Sync', timeoutDurationMs / 1000, 0.5);
168
189
  if (parentBlock === undefined) {
169
- this.log.warn(`Parent block for ${blockNumber} not found, skipping attestation`);
190
+ this.log.warn(`Parent block for ${blockNumber} not found, skipping attestation`, proposalInfo);
170
191
  if (partOfCommittee) {
171
192
  this.metrics.incFailedAttestations(1, 'parent_block_not_found');
172
193
  }
@@ -184,20 +205,8 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
184
205
  return undefined;
185
206
  }
186
207
  }
187
- // Collect txs from the proposal
188
- const { missing, txs } = await this.txCollector.collectForBlockProposal(proposal, proposalSender);
189
- // Check that all of the transactions in the proposal are available in the tx pool before attesting
190
- if (missing && missing.length > 0) {
191
- this.log.warn(`Missing ${missing.length}/${proposal.payload.txHashes.length} txs to attest to proposal`, {
192
- ...proposalInfo,
193
- missing
194
- });
195
- if (partOfCommittee) {
196
- this.metrics.incFailedAttestations(1, 'tx_not_available');
197
- }
198
- return undefined;
199
- }
200
208
  // Check that I have the same set of l1ToL2Messages as the proposal
209
+ // Q: Same as above, should this be part of p2p validation?
201
210
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockNumber);
202
211
  const computedInHash = await computeInHashFromL1ToL2Messages(l1ToL2Messages);
203
212
  const proposalInHash = proposal.payload.header.contentCommitment.inHash;
@@ -212,8 +221,15 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
212
221
  }
213
222
  return undefined;
214
223
  }
215
- if (!partOfCommittee) {
216
- this.log.verbose(`No validator in the committee, skipping attestation`);
224
+ // Check that all of the transactions in the proposal are available in the tx pool before attesting
225
+ if (missingTxs.length > 0) {
226
+ this.log.warn(`Missing ${missingTxs.length} txs to attest to proposal`, {
227
+ ...proposalInfo,
228
+ missingTxs
229
+ });
230
+ if (partOfCommittee) {
231
+ this.metrics.incFailedAttestations(1, 'TransactionsNotAvailableError');
232
+ }
217
233
  return undefined;
218
234
  }
219
235
  // Try re-executing the transactions in the proposal
@@ -247,10 +263,11 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
247
263
  * Re-execute the transactions in the proposal and check that the state updates match the header state
248
264
  * @param proposal - The proposal to re-execute
249
265
  */ async reExecuteTransactions(proposal, txs, l1ToL2Messages) {
250
- const { header, txHashes } = proposal.payload;
266
+ const { header } = proposal.payload;
267
+ const { txHashes } = proposal;
251
268
  // If we do not have all of the transactions, then we should fail
252
269
  if (txs.length !== txHashes.length) {
253
- const foundTxHashes = await Promise.all(txs.map(async (tx)=>await tx.getTxHash()));
270
+ const foundTxHashes = txs.map((tx)=>tx.getTxHash());
254
271
  const missingTxHashes = txHashes.filter((txHash)=>!foundTxHashes.includes(txHash));
255
272
  throw new TransactionsNotAvailableError(missingTxHashes);
256
273
  }
@@ -291,12 +308,12 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
291
308
  // remove oldest proposer. `values` is guaranteed to be in insertion order.
292
309
  this.proposersOfInvalidBlocks.delete(this.proposersOfInvalidBlocks.values().next().value);
293
310
  }
294
- this.proposersOfInvalidBlocks.add(proposer);
311
+ this.proposersOfInvalidBlocks.add(proposer.toString());
295
312
  this.emit(WANT_TO_SLASH_EVENT, [
296
313
  {
297
314
  validator: proposer,
298
315
  amount: this.config.slashInvalidBlockPenalty,
299
- offense: Offense.INVALID_BLOCK
316
+ offense: Offense.BROADCASTED_INVALID_BLOCK_PROPOSAL
300
317
  }
301
318
  ]);
302
319
  }
@@ -313,7 +330,7 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
313
330
  * i.e. either we just created the slashing payload, or someone else did and we saw the event on L1.
314
331
  */ shouldSlash(args) {
315
332
  // note we don't check the offence here: we know this person is bad and we're willing to slash up to the max penalty.
316
- return Promise.resolve(args.amount <= this.config.slashInvalidBlockMaxPenalty && this.proposersOfInvalidBlocks.has(args.validator));
333
+ return Promise.resolve(args.amount <= this.config.slashInvalidBlockMaxPenalty && this.proposersOfInvalidBlocks.has(args.validator.toString()));
317
334
  }
318
335
  async createBlockProposal(blockNumber, header, archive, stateReference, txs, proposerAddress, options) {
319
336
  if (this.previousProposal?.slotNumber.equals(header.slotNumber)) {
@@ -327,6 +344,14 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
327
344
  async broadcastBlockProposal(proposal) {
328
345
  await this.p2pClient.broadcastProposal(proposal);
329
346
  }
347
+ async collectOwnAttestations(proposal) {
348
+ const slot = proposal.payload.header.slotNumber.toBigInt();
349
+ const inCommittee = await this.epochCache.filterInCommittee(slot, this.keyStore.getAddresses());
350
+ this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, {
351
+ inCommittee
352
+ });
353
+ return this.doAttestToProposal(proposal, inCommittee);
354
+ }
330
355
  async collectAttestations(proposal, required, deadline) {
331
356
  // Wait and poll the p2pClient's attestation pool for this block until we have enough attestations
332
357
  const slot = proposal.payload.header.slotNumber.toBigInt();
@@ -335,10 +360,8 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
335
360
  this.log.error(`Deadline ${deadline.toISOString()} for collecting ${required} attestations for slot ${slot} is in the past`);
336
361
  throw new AttestationTimeoutError(0, required, slot);
337
362
  }
363
+ await this.collectOwnAttestations(proposal);
338
364
  const proposalId = proposal.archive.toString();
339
- // adds attestations for all of my addresses locally
340
- const inCommittee = await this.epochCache.filterInCommittee(this.keyStore.getAddresses());
341
- await this.doAttestToProposal(proposal, inCommittee);
342
365
  const myAddresses = this.keyStore.getAddresses();
343
366
  let attestations = [];
344
367
  while(true){
@@ -368,6 +391,24 @@ const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
368
391
  await this.p2pClient.addAttestations(attestations);
369
392
  return attestations;
370
393
  }
394
+ async handleAuthRequest(peer, msg) {
395
+ const authRequest = AuthRequest.fromBuffer(msg);
396
+ const statusMessage = await this.p2pClient.handleAuthRequestFromPeer(authRequest, peer).catch((_)=>undefined);
397
+ if (statusMessage === undefined) {
398
+ return Buffer.alloc(0);
399
+ }
400
+ // Find a validator address that is in the set
401
+ const allRegisteredValidators = await this.epochCache.getRegisteredValidators();
402
+ const addressToUse = this.getValidatorAddresses().find((address)=>allRegisteredValidators.find((v)=>v.equals(address)) !== undefined);
403
+ if (addressToUse === undefined) {
404
+ // We don't have a registered address
405
+ return Buffer.alloc(0);
406
+ }
407
+ const payloadToSign = authRequest.getPayloadToSign();
408
+ const signature = await this.keyStore.signMessageWithAddress(addressToUse, payloadToSign);
409
+ const authResponse = new AuthResponse(statusMessage, signature);
410
+ return authResponse.toBuffer();
411
+ }
371
412
  }
372
413
  function validatePrivateKey(privateKey) {
373
414
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/validator-client",
3
- "version": "1.2.0",
3
+ "version": "2.0.0-nightly.20250813",
4
4
  "main": "dest/index.js",
5
5
  "type": "module",
6
6
  "exports": {
@@ -64,15 +64,15 @@
64
64
  ]
65
65
  },
66
66
  "dependencies": {
67
- "@aztec/constants": "1.2.0",
68
- "@aztec/epoch-cache": "1.2.0",
69
- "@aztec/ethereum": "1.2.0",
70
- "@aztec/foundation": "1.2.0",
71
- "@aztec/p2p": "1.2.0",
72
- "@aztec/prover-client": "1.2.0",
73
- "@aztec/slasher": "1.2.0",
74
- "@aztec/stdlib": "1.2.0",
75
- "@aztec/telemetry-client": "1.2.0",
67
+ "@aztec/constants": "2.0.0-nightly.20250813",
68
+ "@aztec/epoch-cache": "2.0.0-nightly.20250813",
69
+ "@aztec/ethereum": "2.0.0-nightly.20250813",
70
+ "@aztec/foundation": "2.0.0-nightly.20250813",
71
+ "@aztec/p2p": "2.0.0-nightly.20250813",
72
+ "@aztec/prover-client": "2.0.0-nightly.20250813",
73
+ "@aztec/slasher": "2.0.0-nightly.20250813",
74
+ "@aztec/stdlib": "2.0.0-nightly.20250813",
75
+ "@aztec/telemetry-client": "2.0.0-nightly.20250813",
76
76
  "koa": "^2.16.1",
77
77
  "koa-router": "^12.0.0",
78
78
  "tslib": "^2.4.0",
package/src/config.ts CHANGED
@@ -6,13 +6,14 @@ import {
6
6
  numberConfigHelper,
7
7
  secretValueConfigHelper,
8
8
  } from '@aztec/foundation/config';
9
+ import { EthAddress } from '@aztec/foundation/eth-address';
9
10
 
10
11
  /**
11
12
  * The Validator Configuration
12
13
  */
13
14
  export interface ValidatorClientConfig {
14
15
  /** The private keys of the validators participating in attestation duties */
15
- validatorPrivateKeys: SecretValue<`0x${string}`[]>;
16
+ validatorPrivateKeys?: SecretValue<`0x${string}`[]>;
16
17
 
17
18
  /** Do not run the validator */
18
19
  disableValidator: boolean;
@@ -25,6 +26,12 @@ export interface ValidatorClientConfig {
25
26
 
26
27
  /** Will re-execute until this many milliseconds are left in the slot */
27
28
  validatorReexecuteDeadlineMs: number;
29
+
30
+ /** URL of the Web3Signer instance */
31
+ web3SignerUrl?: string;
32
+
33
+ /** List of addresses of remote signers */
34
+ web3SignerAddresses?: EthAddress[];
28
35
  }
29
36
 
30
37
  export const validatorClientConfigMappings: ConfigMappingsType<ValidatorClientConfig> = {
@@ -56,6 +63,16 @@ export const validatorClientConfigMappings: ConfigMappingsType<ValidatorClientCo
56
63
  description: 'Will re-execute until this many milliseconds are left in the slot',
57
64
  ...numberConfigHelper(6000),
58
65
  },
66
+ web3SignerUrl: {
67
+ env: 'WEB3_SIGNER_URL',
68
+ description: 'URL of the Web3Signer instance',
69
+ parseEnv: (val: string) => val.trim(),
70
+ },
71
+ web3SignerAddresses: {
72
+ env: 'WEB3_SIGNER_ADDRESSES',
73
+ description: 'List of addresses of remote signers',
74
+ parseEnv: (val: string) => val.split(',').map(address => EthAddress.fromString(address)),
75
+ },
59
76
  };
60
77
 
61
78
  /**
@@ -49,7 +49,8 @@ export class ValidationService {
49
49
 
50
50
  return BlockProposal.createProposalFromSigner(
51
51
  blockNumber,
52
- new ConsensusPayload(header, archive, stateReference, txHashes),
52
+ new ConsensusPayload(header, archive, stateReference),
53
+ txHashes,
53
54
  options.publishFullTxs ? txs : undefined,
54
55
  payloadSigner,
55
56
  );
package/src/factory.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  import type { EpochCache } from '@aztec/epoch-cache';
2
2
  import { SecretValue } from '@aztec/foundation/config';
3
3
  import type { DateProvider } from '@aztec/foundation/timer';
4
- import type { P2P } from '@aztec/p2p';
5
- import type { SlasherConfig } from '@aztec/slasher/config';
4
+ import type { P2PClient } from '@aztec/p2p';
6
5
  import type { L2BlockSource } from '@aztec/stdlib/block';
7
- import type { IFullNodeBlockBuilder } from '@aztec/stdlib/interfaces/server';
6
+ import type { IFullNodeBlockBuilder, SlasherConfig } from '@aztec/stdlib/interfaces/server';
8
7
  import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
9
8
  import type { TelemetryClient } from '@aztec/telemetry-client';
10
9
 
@@ -18,7 +17,7 @@ export function createValidatorClient(
18
17
  Pick<SlasherConfig, 'slashInvalidBlockEnabled' | 'slashInvalidBlockPenalty' | 'slashInvalidBlockMaxPenalty'>,
19
18
  deps: {
20
19
  blockBuilder: IFullNodeBlockBuilder;
21
- p2pClient: P2P;
20
+ p2pClient: P2PClient;
22
21
  blockSource: L2BlockSource;
23
22
  l1ToL2MessageSource: L1ToL2MessageSource;
24
23
  telemetry: TelemetryClient;
@@ -29,10 +28,14 @@ export function createValidatorClient(
29
28
  if (config.disableValidator) {
30
29
  return undefined;
31
30
  }
32
- if (config.validatorPrivateKeys === undefined || !config.validatorPrivateKeys.getValue().length) {
31
+ if (
32
+ (config.validatorPrivateKeys === undefined || !config.validatorPrivateKeys.getValue().length) &&
33
+ !config.web3SignerUrl
34
+ ) {
33
35
  config.validatorPrivateKeys = new SecretValue([generatePrivateKey()]);
34
36
  }
35
37
 
38
+ const txProvider = deps.p2pClient.getTxProvider();
36
39
  return ValidatorClient.new(
37
40
  config,
38
41
  deps.blockBuilder,
@@ -40,6 +43,7 @@ export function createValidatorClient(
40
43
  deps.p2pClient,
41
44
  deps.blockSource,
42
45
  deps.l1ToL2MessageSource,
46
+ txProvider,
43
47
  deps.dateProvider,
44
48
  deps.telemetry,
45
49
  );
@@ -1,2 +1,3 @@
1
1
  export * from './interface.js';
2
2
  export * from './local_key_store.js';
3
+ export * from './web3signer_key_store.js';
@@ -2,6 +2,8 @@ import type { Buffer32 } from '@aztec/foundation/buffer';
2
2
  import type { EthAddress } from '@aztec/foundation/eth-address';
3
3
  import type { Signature } from '@aztec/foundation/eth-signature';
4
4
 
5
+ import type { TypedDataDefinition } from 'viem';
6
+
5
7
  /** Key Store
6
8
  *
7
9
  * A keystore interface that can be replaced with a local keystore / remote signer service
@@ -22,8 +24,8 @@ export interface ValidatorKeyStore {
22
24
  */
23
25
  getAddresses(): EthAddress[];
24
26
 
25
- sign(message: Buffer32): Promise<Signature[]>;
26
- signWithAddress(address: EthAddress, message: Buffer32): Promise<Signature>;
27
+ signTypedData(typedData: TypedDataDefinition): Promise<Signature[]>;
28
+ signTypedDataWithAddress(address: EthAddress, typedData: TypedDataDefinition): Promise<Signature>;
27
29
  /**
28
30
  * Flavor of sign message that followed EIP-712 eth signed message prefix
29
31
  * Note: this is only required when we are using ecdsa signatures over secp256k1
@@ -1,8 +1,10 @@
1
- import type { Buffer32 } from '@aztec/foundation/buffer';
1
+ import { Buffer32 } from '@aztec/foundation/buffer';
2
2
  import { Secp256k1Signer } from '@aztec/foundation/crypto';
3
3
  import type { EthAddress } from '@aztec/foundation/eth-address';
4
4
  import type { Signature } from '@aztec/foundation/eth-signature';
5
5
 
6
+ import { type TypedDataDefinition, hashTypedData } from 'viem';
7
+
6
8
  import type { ValidatorKeyStore } from './interface.js';
7
9
 
8
10
  /**
@@ -43,30 +45,32 @@ export class LocalKeyStore implements ValidatorKeyStore {
43
45
 
44
46
  /**
45
47
  * Sign a message with all keystore private keys
46
- * @param digest - The message buffer to sign
48
+ * @param typedData - The complete EIP-712 typed data structure (domain, types, primaryType, message)
47
49
  * @return signature
48
50
  */
49
- public sign(digest: Buffer32): Promise<Signature[]> {
50
- return Promise.all(this.signers.map(signer => signer.sign(digest)));
51
+ public signTypedData(typedData: TypedDataDefinition): Promise<Signature[]> {
52
+ const digest = hashTypedData(typedData);
53
+ return Promise.all(this.signers.map(signer => signer.sign(Buffer32.fromString(digest))));
51
54
  }
52
55
 
53
56
  /**
54
57
  * Sign a message with a specific address's private key
55
58
  * @param address - The address of the signer to use
56
- * @param digest - The message buffer to sign
59
+ * @param typedData - The complete EIP-712 typed data structure (domain, types, primaryType, message)
57
60
  * @returns signature for the specified address
58
61
  * @throws Error if the address is not found in the keystore
59
62
  */
60
- public signWithAddress(address: EthAddress, digest: Buffer32): Promise<Signature> {
63
+ public signTypedDataWithAddress(address: EthAddress, typedData: TypedDataDefinition): Promise<Signature> {
61
64
  const signer = this.signersByAddress.get(address.toString());
62
65
  if (!signer) {
63
66
  throw new Error(`No signer found for address ${address.toString()}`);
64
67
  }
65
- return Promise.resolve(signer.sign(digest));
68
+ const digest = hashTypedData(typedData);
69
+ return Promise.resolve(signer.sign(Buffer32.fromString(digest)));
66
70
  }
67
71
 
68
72
  /**
69
- * Sign a message with all keystore private keys
73
+ * Sign a message using eth_sign with all keystore private keys
70
74
  *
71
75
  * @param message - The message to sign
72
76
  * @return signatures
@@ -76,7 +80,7 @@ export class LocalKeyStore implements ValidatorKeyStore {
76
80
  }
77
81
 
78
82
  /**
79
- * Sign a message with a specific address's private key
83
+ * Sign a message using eth_sign with a specific address's private key
80
84
  * @param address - The address of the signer to use
81
85
  * @param message - The message to sign
82
86
  * @returns signature for the specified address