@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/config.d.ts +6 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +11 -0
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +1 -1
- package/dest/factory.d.ts +3 -4
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +3 -2
- package/dest/key_store/index.d.ts +1 -0
- package/dest/key_store/index.d.ts.map +1 -1
- package/dest/key_store/index.js +1 -0
- package/dest/key_store/interface.d.ts +3 -2
- package/dest/key_store/interface.d.ts.map +1 -1
- package/dest/key_store/local_key_store.d.ts +8 -7
- package/dest/key_store/local_key_store.d.ts.map +1 -1
- package/dest/key_store/local_key_store.js +12 -8
- package/dest/key_store/web3signer_key_store.d.ts +67 -0
- package/dest/key_store/web3signer_key_store.d.ts.map +1 -0
- package/dest/key_store/web3signer_key_store.js +153 -0
- package/dest/validator.d.ts +9 -6
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +96 -55
- package/package.json +10 -10
- package/src/config.ts +18 -1
- package/src/duties/validation_service.ts +2 -1
- package/src/factory.ts +9 -5
- package/src/key_store/index.ts +1 -0
- package/src/key_store/interface.ts +4 -2
- package/src/key_store/local_key_store.ts +13 -9
- package/src/key_store/web3signer_key_store.ts +195 -0
- package/src/validator.ts +114 -63
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 {
|
|
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
|
|
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
|
-
|
|
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('
|
|
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.
|
|
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.
|
|
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
|
-
|
|
83
|
-
|
|
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
|
|
86
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
133
|
-
|
|
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
|
|
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
|
-
|
|
216
|
-
|
|
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
|
|
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 =
|
|
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.
|
|
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": "
|
|
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": "
|
|
68
|
-
"@aztec/epoch-cache": "
|
|
69
|
-
"@aztec/ethereum": "
|
|
70
|
-
"@aztec/foundation": "
|
|
71
|
-
"@aztec/p2p": "
|
|
72
|
-
"@aztec/prover-client": "
|
|
73
|
-
"@aztec/slasher": "
|
|
74
|
-
"@aztec/stdlib": "
|
|
75
|
-
"@aztec/telemetry-client": "
|
|
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
|
|
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
|
|
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 {
|
|
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:
|
|
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 (
|
|
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
|
);
|
package/src/key_store/index.ts
CHANGED
|
@@ -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
|
-
|
|
26
|
-
|
|
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
|
|
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
|
|
48
|
+
* @param typedData - The complete EIP-712 typed data structure (domain, types, primaryType, message)
|
|
47
49
|
* @return signature
|
|
48
50
|
*/
|
|
49
|
-
public
|
|
50
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|