@aztec/validator-client 0.87.6 → 1.0.0-nightly.20250604
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 +4 -2
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +9 -5
- package/dest/duties/validation_service.d.ts +6 -4
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +9 -8
- package/dest/factory.d.ts +2 -0
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +5 -3
- package/dest/key_store/interface.d.ts +14 -5
- package/dest/key_store/interface.d.ts.map +1 -1
- package/dest/key_store/local_key_store.d.ts +38 -9
- package/dest/key_store/local_key_store.d.ts.map +1 -1
- package/dest/key_store/local_key_store.js +58 -15
- package/dest/metrics.d.ts +2 -2
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +4 -4
- package/dest/validator.d.ts +15 -35
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +69 -53
- package/package.json +8 -8
- package/src/config.ts +14 -7
- package/src/duties/validation_service.ts +12 -8
- package/src/factory.ts +5 -2
- package/src/key_store/interface.ts +15 -5
- package/src/key_store/local_key_store.ts +62 -16
- package/src/metrics.ts +4 -4
- package/src/validator.ts +92 -76
package/dest/validator.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
|
|
2
2
|
import { Buffer32 } from '@aztec/foundation/buffer';
|
|
3
|
+
import { Fr } from '@aztec/foundation/fields';
|
|
3
4
|
import { createLogger } from '@aztec/foundation/log';
|
|
4
5
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
5
6
|
import { sleep } from '@aztec/foundation/sleep';
|
|
6
7
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
7
8
|
import { TxCollector } from '@aztec/p2p';
|
|
8
9
|
import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
|
|
10
|
+
import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
11
|
+
import { GlobalVariables } from '@aztec/stdlib/tx';
|
|
9
12
|
import { WithTracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
10
13
|
import { ValidationService } from './duties/validation_service.js';
|
|
11
14
|
import { AttestationTimeoutError, BlockBuilderNotProvidedError, InvalidValidatorPrivateKeyError, ReExFailedTxsError, ReExStateMismatchError, ReExTimeoutError, TransactionsNotAvailableError } from './errors/validator.error.js';
|
|
@@ -14,6 +17,7 @@ import { ValidatorMetrics } from './metrics.js';
|
|
|
14
17
|
/**
|
|
15
18
|
* Validator Client
|
|
16
19
|
*/ export class ValidatorClient extends WithTracer {
|
|
20
|
+
blockBuilder;
|
|
17
21
|
keyStore;
|
|
18
22
|
epochCache;
|
|
19
23
|
p2pClient;
|
|
@@ -25,34 +29,34 @@ import { ValidatorMetrics } from './metrics.js';
|
|
|
25
29
|
metrics;
|
|
26
30
|
// Used to check if we are sending the same proposal twice
|
|
27
31
|
previousProposal;
|
|
28
|
-
|
|
29
|
-
blockBuilder;
|
|
30
|
-
myAddress;
|
|
32
|
+
myAddresses;
|
|
31
33
|
lastEpoch;
|
|
32
34
|
epochCacheUpdateLoop;
|
|
33
35
|
blockProposalValidator;
|
|
34
36
|
txCollector;
|
|
35
|
-
constructor(keyStore, epochCache, p2pClient, blockSource, config, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
|
|
37
|
+
constructor(blockBuilder, keyStore, epochCache, p2pClient, blockSource, config, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
|
|
36
38
|
// Instantiate tracer
|
|
37
|
-
super(telemetry, 'Validator'), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.blockSource = blockSource, this.config = config, this.dateProvider = dateProvider, this.log = log
|
|
39
|
+
super(telemetry, 'Validator'), this.blockBuilder = blockBuilder, this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.blockSource = blockSource, this.config = config, this.dateProvider = dateProvider, this.log = log;
|
|
38
40
|
this.metrics = new ValidatorMetrics(telemetry);
|
|
39
41
|
this.validationService = new ValidationService(keyStore);
|
|
40
42
|
this.blockProposalValidator = new BlockProposalValidator(epochCache);
|
|
41
43
|
this.txCollector = new TxCollector(p2pClient, this.log);
|
|
42
44
|
// Refresh epoch cache every second to trigger alert if participation in committee changes
|
|
43
|
-
this.
|
|
45
|
+
this.myAddresses = this.keyStore.getAddresses();
|
|
44
46
|
this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), log, 1000);
|
|
45
|
-
this.log.verbose(`Initialized validator with
|
|
47
|
+
this.log.verbose(`Initialized validator with addresses: ${this.myAddresses.map((a)=>a.toString()).join(', ')}`);
|
|
46
48
|
}
|
|
47
49
|
async handleEpochCommitteeUpdate() {
|
|
48
50
|
try {
|
|
49
51
|
const { committee, epoch } = await this.epochCache.getCommittee('now');
|
|
50
52
|
if (epoch !== this.lastEpoch) {
|
|
51
|
-
const me = this.
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
const me = this.myAddresses;
|
|
54
|
+
const committeeSet = new Set(committee.map((v)=>v.toString()));
|
|
55
|
+
const inCommittee = me.filter((a)=>committeeSet.has(a.toString()));
|
|
56
|
+
if (inCommittee.length > 0) {
|
|
57
|
+
inCommittee.forEach((a)=>this.log.info(`Validator ${a.toString()} is on the validator committee for epoch ${epoch}`));
|
|
54
58
|
} else {
|
|
55
|
-
this.log.verbose(`
|
|
59
|
+
this.log.verbose(`Validators ${me.map((a)=>a.toString()).join(', ')} are not on the validator committee for epoch ${epoch}`);
|
|
56
60
|
}
|
|
57
61
|
this.lastEpoch = epoch;
|
|
58
62
|
}
|
|
@@ -60,28 +64,30 @@ import { ValidatorMetrics } from './metrics.js';
|
|
|
60
64
|
this.log.error(`Error updating epoch committee`, err);
|
|
61
65
|
}
|
|
62
66
|
}
|
|
63
|
-
static new(config, epochCache, p2pClient, blockSource, dateProvider = new DateProvider(), telemetry = getTelemetryClient()) {
|
|
64
|
-
if (!config.
|
|
67
|
+
static new(config, blockBuilder, epochCache, p2pClient, blockSource, dateProvider = new DateProvider(), telemetry = getTelemetryClient()) {
|
|
68
|
+
if (!config.validatorPrivateKeys?.length) {
|
|
65
69
|
throw new InvalidValidatorPrivateKeyError();
|
|
66
70
|
}
|
|
67
|
-
const
|
|
68
|
-
const localKeyStore = new LocalKeyStore(
|
|
69
|
-
const validator = new ValidatorClient(localKeyStore, epochCache, p2pClient, blockSource, config, dateProvider, telemetry);
|
|
71
|
+
const privateKeys = config.validatorPrivateKeys.map(validatePrivateKey);
|
|
72
|
+
const localKeyStore = new LocalKeyStore(privateKeys);
|
|
73
|
+
const validator = new ValidatorClient(blockBuilder, localKeyStore, epochCache, p2pClient, blockSource, config, dateProvider, telemetry);
|
|
70
74
|
validator.registerBlockProposalHandler();
|
|
71
75
|
return validator;
|
|
72
76
|
}
|
|
73
|
-
|
|
74
|
-
return this.keyStore.
|
|
77
|
+
getValidatorAddresses() {
|
|
78
|
+
return this.keyStore.getAddresses();
|
|
75
79
|
}
|
|
76
80
|
async start() {
|
|
77
81
|
// Sync the committee from the smart contract
|
|
78
82
|
// https://github.com/AztecProtocol/aztec-packages/issues/7962
|
|
79
|
-
const
|
|
80
|
-
const inCommittee = await this.epochCache.
|
|
81
|
-
if (inCommittee) {
|
|
82
|
-
this.log.info(`Started validator with
|
|
83
|
+
const myAddresses = this.keyStore.getAddresses();
|
|
84
|
+
const inCommittee = await this.epochCache.filterInCommittee(myAddresses);
|
|
85
|
+
if (inCommittee.length > 0) {
|
|
86
|
+
this.log.info(`Started validator with addresses in current validator committee:
|
|
87
|
+
${inCommittee.map((a)=>a.toString()).join(', ')}`);
|
|
83
88
|
} else {
|
|
84
|
-
this.log.info(`Started validator with
|
|
89
|
+
this.log.info(`Started validator with addresses:
|
|
90
|
+
${myAddresses.map((a)=>a.toString()).join(', ')}`);
|
|
85
91
|
}
|
|
86
92
|
this.epochCacheUpdateLoop.start();
|
|
87
93
|
return Promise.resolve();
|
|
@@ -95,13 +101,6 @@ import { ValidatorMetrics } from './metrics.js';
|
|
|
95
101
|
};
|
|
96
102
|
this.p2pClient.registerBlockProposalHandler(handler);
|
|
97
103
|
}
|
|
98
|
-
/**
|
|
99
|
-
* Register a callback function for building a block
|
|
100
|
-
*
|
|
101
|
-
* We reuse the sequencer's block building functionality for re-execution
|
|
102
|
-
*/ registerBlockBuilder(blockBuilder) {
|
|
103
|
-
this.blockBuilder = blockBuilder;
|
|
104
|
-
}
|
|
105
104
|
async attestToProposal(proposal, proposalSender) {
|
|
106
105
|
const slotNumber = proposal.slotNumber.toNumber();
|
|
107
106
|
const blockNumber = proposal.blockNumber.toNumber();
|
|
@@ -118,19 +117,19 @@ import { ValidatorMetrics } from './metrics.js';
|
|
|
118
117
|
const invalidProposal = await this.blockProposalValidator.validate(proposal);
|
|
119
118
|
if (invalidProposal) {
|
|
120
119
|
this.log.verbose(`Proposal is not valid, skipping attestation`);
|
|
121
|
-
this.metrics.incFailedAttestations('invalid_proposal');
|
|
120
|
+
this.metrics.incFailedAttestations(1, 'invalid_proposal');
|
|
122
121
|
return undefined;
|
|
123
122
|
}
|
|
124
123
|
// Check that the parent proposal is a block we know, otherwise reexecution would fail.
|
|
125
124
|
// Q: Should we move this to the block proposal validator? If there, then p2p would check it
|
|
126
|
-
// before re-broadcasting it. This means that proposals built on top of an L1-
|
|
125
|
+
// before re-broadcasting it. This means that proposals built on top of an L1-reorg'ed-out block
|
|
127
126
|
// would not be rebroadcasted. But it also means that nodes that have not fully synced would
|
|
128
127
|
// not rebroadcast the proposal.
|
|
129
128
|
if (blockNumber > INITIAL_L2_BLOCK_NUM) {
|
|
130
129
|
const parentBlock = await this.blockSource.getBlock(blockNumber - 1);
|
|
131
130
|
if (parentBlock === undefined) {
|
|
132
131
|
this.log.verbose(`Parent block for ${blockNumber} not found, skipping attestation`);
|
|
133
|
-
this.metrics.incFailedAttestations('parent_block_not_found');
|
|
132
|
+
this.metrics.incFailedAttestations(1, 'parent_block_not_found');
|
|
134
133
|
return undefined;
|
|
135
134
|
}
|
|
136
135
|
if (!proposal.payload.header.lastArchiveRoot.equals(parentBlock.archive.root)) {
|
|
@@ -139,15 +138,16 @@ import { ValidatorMetrics } from './metrics.js';
|
|
|
139
138
|
parentBlockArchiveRoot: parentBlock.archive.root.toString(),
|
|
140
139
|
...proposalInfo
|
|
141
140
|
});
|
|
142
|
-
this.metrics.incFailedAttestations('parent_block_does_not_match');
|
|
141
|
+
this.metrics.incFailedAttestations(1, 'parent_block_does_not_match');
|
|
143
142
|
return undefined;
|
|
144
143
|
}
|
|
145
144
|
}
|
|
146
145
|
// Collect txs from the proposal
|
|
147
146
|
const { missing, txs } = await this.txCollector.collectForBlockProposal(proposal, proposalSender);
|
|
148
|
-
// Check that I
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
// Check that I have any address in current committee before attesting
|
|
148
|
+
const inCommittee = await this.epochCache.filterInCommittee(this.keyStore.getAddresses());
|
|
149
|
+
if (inCommittee.length === 0) {
|
|
150
|
+
this.log.verbose(`No validator in the committee, skipping attestation`);
|
|
151
151
|
return undefined;
|
|
152
152
|
}
|
|
153
153
|
// Check that all of the transactions in the proposal are available in the tx pool before attesting
|
|
@@ -156,7 +156,7 @@ import { ValidatorMetrics } from './metrics.js';
|
|
|
156
156
|
proposalInfo,
|
|
157
157
|
missing
|
|
158
158
|
});
|
|
159
|
-
this.metrics.incFailedAttestations('TransactionsNotAvailableError');
|
|
159
|
+
this.metrics.incFailedAttestations(1, 'TransactionsNotAvailableError');
|
|
160
160
|
return undefined;
|
|
161
161
|
}
|
|
162
162
|
// Try re-executing the transactions in the proposal
|
|
@@ -167,14 +167,20 @@ import { ValidatorMetrics } from './metrics.js';
|
|
|
167
167
|
await this.reExecuteTransactions(proposal, txs);
|
|
168
168
|
}
|
|
169
169
|
} catch (error) {
|
|
170
|
-
this.metrics.incFailedAttestations(error instanceof Error ? error.name : 'unknown');
|
|
170
|
+
this.metrics.incFailedAttestations(1, error instanceof Error ? error.name : 'unknown');
|
|
171
171
|
this.log.error(`Failed to attest to proposal`, error, proposalInfo);
|
|
172
172
|
return undefined;
|
|
173
173
|
}
|
|
174
174
|
// Provided all of the above checks pass, we can attest to the proposal
|
|
175
175
|
this.log.info(`Attesting to proposal for slot ${slotNumber}`, proposalInfo);
|
|
176
|
-
this.metrics.incAttestations();
|
|
177
|
-
|
|
176
|
+
this.metrics.incAttestations(inCommittee.length);
|
|
177
|
+
// If the above function does not throw an error, then we can attest to the proposal
|
|
178
|
+
return this.doAttestToProposal(proposal, inCommittee);
|
|
179
|
+
}
|
|
180
|
+
getReexecutionDeadline(proposal, config) {
|
|
181
|
+
const nextSlotTimestampSeconds = Number(getTimestampForSlot(proposal.slotNumber.toBigInt() + 1n, config));
|
|
182
|
+
const msNeededForPropagationAndPublishing = this.config.validatorReexecuteDeadlineMs;
|
|
183
|
+
return new Date(nextSlotTimestampSeconds * 1000 - msNeededForPropagationAndPublishing);
|
|
178
184
|
}
|
|
179
185
|
/**
|
|
180
186
|
* Re-execute the transactions in the proposal and check that the state updates match the header state
|
|
@@ -193,11 +199,20 @@ import { ValidatorMetrics } from './metrics.js';
|
|
|
193
199
|
}
|
|
194
200
|
// Use the sequencer's block building logic to re-execute the transactions
|
|
195
201
|
const stopTimer = this.metrics.reExecutionTimer();
|
|
196
|
-
const
|
|
197
|
-
|
|
202
|
+
const config = this.blockBuilder.getConfig();
|
|
203
|
+
const globalVariables = GlobalVariables.from({
|
|
204
|
+
...proposal.payload.header,
|
|
205
|
+
blockNumber: proposal.blockNumber,
|
|
206
|
+
timestamp: new Fr(header.timestamp),
|
|
207
|
+
chainId: new Fr(config.l1ChainId),
|
|
208
|
+
version: new Fr(config.rollupVersion)
|
|
209
|
+
});
|
|
210
|
+
const { block, failedTxs } = await this.blockBuilder.buildBlock(txs, globalVariables, {
|
|
211
|
+
deadline: this.getReexecutionDeadline(proposal, config)
|
|
198
212
|
});
|
|
199
213
|
stopTimer();
|
|
200
214
|
this.log.verbose(`Transaction re-execution complete`);
|
|
215
|
+
const numFailedTxs = failedTxs.length;
|
|
201
216
|
if (numFailedTxs > 0) {
|
|
202
217
|
this.metrics.recordFailedReexecution(proposal);
|
|
203
218
|
throw new ReExFailedTxsError(numFailedTxs);
|
|
@@ -212,19 +227,18 @@ import { ValidatorMetrics } from './metrics.js';
|
|
|
212
227
|
throw new ReExStateMismatchError();
|
|
213
228
|
}
|
|
214
229
|
}
|
|
215
|
-
async createBlockProposal(blockNumber, header, archive, stateReference, txs, options) {
|
|
230
|
+
async createBlockProposal(blockNumber, header, archive, stateReference, txs, proposerAddress, options) {
|
|
216
231
|
if (this.previousProposal?.slotNumber.equals(header.slotNumber)) {
|
|
217
232
|
this.log.verbose(`Already made a proposal for the same slot, skipping proposal`);
|
|
218
233
|
return Promise.resolve(undefined);
|
|
219
234
|
}
|
|
220
|
-
const newProposal = await this.validationService.createBlockProposal(blockNumber, header, archive, stateReference, txs, options);
|
|
235
|
+
const newProposal = await this.validationService.createBlockProposal(blockNumber, header, archive, stateReference, txs, proposerAddress, options);
|
|
221
236
|
this.previousProposal = newProposal;
|
|
222
237
|
return newProposal;
|
|
223
238
|
}
|
|
224
239
|
async broadcastBlockProposal(proposal) {
|
|
225
240
|
await this.p2pClient.broadcastProposal(proposal);
|
|
226
241
|
}
|
|
227
|
-
// TODO(https://github.com/AztecProtocol/aztec-packages/issues/7962)
|
|
228
242
|
async collectAttestations(proposal, required, deadline) {
|
|
229
243
|
// Wait and poll the p2pClient's attestation pool for this block until we have enough attestations
|
|
230
244
|
const slot = proposal.payload.header.slotNumber.toBigInt();
|
|
@@ -234,15 +248,17 @@ import { ValidatorMetrics } from './metrics.js';
|
|
|
234
248
|
throw new AttestationTimeoutError(required, slot);
|
|
235
249
|
}
|
|
236
250
|
const proposalId = proposal.archive.toString();
|
|
237
|
-
|
|
238
|
-
const
|
|
251
|
+
// adds attestations for all of my addresses locally
|
|
252
|
+
const inCommittee = await this.epochCache.filterInCommittee(this.keyStore.getAddresses());
|
|
253
|
+
await this.doAttestToProposal(proposal, inCommittee);
|
|
254
|
+
const myAddresses = this.keyStore.getAddresses();
|
|
239
255
|
let attestations = [];
|
|
240
256
|
while(true){
|
|
241
257
|
const collectedAttestations = await this.p2pClient.getAttestationsForSlot(slot, proposalId);
|
|
242
258
|
const oldSenders = attestations.map((attestation)=>attestation.getSender());
|
|
243
259
|
for (const collected of collectedAttestations){
|
|
244
260
|
const collectedSender = collected.getSender();
|
|
245
|
-
if (!
|
|
261
|
+
if (!myAddresses.some((address)=>address.equals(collectedSender)) && !oldSenders.some((sender)=>sender.equals(collectedSender))) {
|
|
246
262
|
this.log.debug(`Received attestation for slot ${slot} from ${collectedSender.toString()}`);
|
|
247
263
|
}
|
|
248
264
|
}
|
|
@@ -259,10 +275,10 @@ import { ValidatorMetrics } from './metrics.js';
|
|
|
259
275
|
await sleep(this.config.attestationPollingIntervalMs);
|
|
260
276
|
}
|
|
261
277
|
}
|
|
262
|
-
async doAttestToProposal(proposal) {
|
|
263
|
-
const
|
|
264
|
-
await this.p2pClient.
|
|
265
|
-
return
|
|
278
|
+
async doAttestToProposal(proposal, attestors = []) {
|
|
279
|
+
const attestations = await this.validationService.attestToProposal(proposal, attestors);
|
|
280
|
+
await this.p2pClient.addAttestations(attestations);
|
|
281
|
+
return attestations;
|
|
266
282
|
}
|
|
267
283
|
}
|
|
268
284
|
function validatePrivateKey(privateKey) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/validator-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-nightly.20250604",
|
|
4
4
|
"main": "dest/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -60,13 +60,13 @@
|
|
|
60
60
|
]
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@aztec/constants": "0.
|
|
64
|
-
"@aztec/epoch-cache": "0.
|
|
65
|
-
"@aztec/ethereum": "0.
|
|
66
|
-
"@aztec/foundation": "0.
|
|
67
|
-
"@aztec/p2p": "0.
|
|
68
|
-
"@aztec/stdlib": "0.
|
|
69
|
-
"@aztec/telemetry-client": "0.
|
|
63
|
+
"@aztec/constants": "1.0.0-nightly.20250604",
|
|
64
|
+
"@aztec/epoch-cache": "1.0.0-nightly.20250604",
|
|
65
|
+
"@aztec/ethereum": "1.0.0-nightly.20250604",
|
|
66
|
+
"@aztec/foundation": "1.0.0-nightly.20250604",
|
|
67
|
+
"@aztec/p2p": "1.0.0-nightly.20250604",
|
|
68
|
+
"@aztec/stdlib": "1.0.0-nightly.20250604",
|
|
69
|
+
"@aztec/telemetry-client": "1.0.0-nightly.20250604",
|
|
70
70
|
"koa": "^2.16.1",
|
|
71
71
|
"koa-router": "^12.0.0",
|
|
72
72
|
"tslib": "^2.4.0",
|
package/src/config.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { NULL_KEY } from '@aztec/ethereum';
|
|
2
1
|
import {
|
|
3
2
|
type ConfigMappingsType,
|
|
4
3
|
booleanConfigHelper,
|
|
@@ -10,8 +9,8 @@ import {
|
|
|
10
9
|
* The Validator Configuration
|
|
11
10
|
*/
|
|
12
11
|
export interface ValidatorClientConfig {
|
|
13
|
-
/** The private
|
|
14
|
-
|
|
12
|
+
/** The private keys of the validators participating in attestation duties */
|
|
13
|
+
validatorPrivateKeys?: `0x${string}`[];
|
|
15
14
|
|
|
16
15
|
/** Do not run the validator */
|
|
17
16
|
disableValidator: boolean;
|
|
@@ -21,13 +20,16 @@ export interface ValidatorClientConfig {
|
|
|
21
20
|
|
|
22
21
|
/** Re-execute transactions before attesting */
|
|
23
22
|
validatorReexecute: boolean;
|
|
23
|
+
|
|
24
|
+
/** Will re-execute until this many milliseconds are left in the slot */
|
|
25
|
+
validatorReexecuteDeadlineMs: number;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export const validatorClientConfigMappings: ConfigMappingsType<ValidatorClientConfig> = {
|
|
27
|
-
|
|
28
|
-
env: '
|
|
29
|
-
parseEnv: (val: string) => (
|
|
30
|
-
description: '
|
|
29
|
+
validatorPrivateKeys: {
|
|
30
|
+
env: 'VALIDATOR_PRIVATE_KEYS',
|
|
31
|
+
parseEnv: (val: string) => val.split(',').map(key => `0x${key.replace('0x', '')}`),
|
|
32
|
+
description: 'List of private keys of the validators participating in attestation duties',
|
|
31
33
|
},
|
|
32
34
|
disableValidator: {
|
|
33
35
|
env: 'VALIDATOR_DISABLED',
|
|
@@ -44,6 +46,11 @@ export const validatorClientConfigMappings: ConfigMappingsType<ValidatorClientCo
|
|
|
44
46
|
description: 'Re-execute transactions before attesting',
|
|
45
47
|
...booleanConfigHelper(true),
|
|
46
48
|
},
|
|
49
|
+
validatorReexecuteDeadlineMs: {
|
|
50
|
+
env: 'VALIDATOR_REEXECUTE_DEADLINE_MS',
|
|
51
|
+
description: 'Will re-execute until this many milliseconds are left in the slot',
|
|
52
|
+
...numberConfigHelper(6000),
|
|
53
|
+
},
|
|
47
54
|
};
|
|
48
55
|
|
|
49
56
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Buffer32 } from '@aztec/foundation/buffer';
|
|
2
2
|
import { keccak256 } from '@aztec/foundation/crypto';
|
|
3
|
+
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
3
4
|
import type { Fr } from '@aztec/foundation/fields';
|
|
4
5
|
import {
|
|
5
6
|
BlockAttestation,
|
|
@@ -31,9 +32,10 @@ export class ValidationService {
|
|
|
31
32
|
archive: Fr,
|
|
32
33
|
stateReference: StateReference,
|
|
33
34
|
txs: Tx[],
|
|
35
|
+
proposerAttesterAddress: EthAddress,
|
|
34
36
|
options: BlockProposalOptions,
|
|
35
37
|
): Promise<BlockProposal> {
|
|
36
|
-
const payloadSigner = (payload: Buffer32) => this.keyStore.
|
|
38
|
+
const payloadSigner = (payload: Buffer32) => this.keyStore.signMessageWithAddress(proposerAttesterAddress, payload);
|
|
37
39
|
// TODO: check if this is calculated earlier / can not be recomputed
|
|
38
40
|
const txHashes = await Promise.all(txs.map(tx => tx.getTxHash()));
|
|
39
41
|
|
|
@@ -46,21 +48,23 @@ export class ValidationService {
|
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
/**
|
|
49
|
-
* Attest to the given block proposal constructed by the current sequencer
|
|
51
|
+
* Attest with selection of validators to the given block proposal, constructed by the current sequencer
|
|
50
52
|
*
|
|
51
53
|
* NOTE: This is just a blind signing.
|
|
52
54
|
* We assume that the proposal is valid and DA guarantees have been checked previously.
|
|
53
55
|
*
|
|
54
56
|
* @param proposal - The proposal to attest to
|
|
55
|
-
* @
|
|
57
|
+
* @param attestors - The validators to attest with
|
|
58
|
+
* @returns attestations
|
|
56
59
|
*/
|
|
57
|
-
async attestToProposal(proposal: BlockProposal): Promise<BlockAttestation> {
|
|
58
|
-
// TODO(https://github.com/AztecProtocol/aztec-packages/issues/7961): check that the current validator is correct
|
|
59
|
-
|
|
60
|
+
async attestToProposal(proposal: BlockProposal, attestors: EthAddress[]): Promise<BlockAttestation[]> {
|
|
60
61
|
const buf = Buffer32.fromBuffer(
|
|
61
62
|
keccak256(proposal.payload.getPayloadToSign(SignatureDomainSeparator.blockAttestation)),
|
|
62
63
|
);
|
|
63
|
-
const
|
|
64
|
-
|
|
64
|
+
const signatures = await Promise.all(
|
|
65
|
+
attestors.map(attestor => this.keyStore.signMessageWithAddress(attestor, buf)),
|
|
66
|
+
);
|
|
67
|
+
//await this.keyStore.signMessage(buf);
|
|
68
|
+
return signatures.map(sig => new BlockAttestation(proposal.blockNumber, proposal.payload, sig));
|
|
65
69
|
}
|
|
66
70
|
}
|
package/src/factory.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { EpochCache } from '@aztec/epoch-cache';
|
|
|
2
2
|
import type { DateProvider } from '@aztec/foundation/timer';
|
|
3
3
|
import type { P2P } from '@aztec/p2p';
|
|
4
4
|
import type { L2BlockSource } from '@aztec/stdlib/block';
|
|
5
|
+
import type { IFullNodeBlockBuilder } from '@aztec/stdlib/interfaces/server';
|
|
5
6
|
import type { TelemetryClient } from '@aztec/telemetry-client';
|
|
6
7
|
|
|
7
8
|
import { generatePrivateKey } from 'viem/accounts';
|
|
@@ -12,6 +13,7 @@ import { ValidatorClient } from './validator.js';
|
|
|
12
13
|
export function createValidatorClient(
|
|
13
14
|
config: ValidatorClientConfig,
|
|
14
15
|
deps: {
|
|
16
|
+
blockBuilder: IFullNodeBlockBuilder;
|
|
15
17
|
p2pClient: P2P;
|
|
16
18
|
blockSource: L2BlockSource;
|
|
17
19
|
telemetry: TelemetryClient;
|
|
@@ -22,12 +24,13 @@ export function createValidatorClient(
|
|
|
22
24
|
if (config.disableValidator) {
|
|
23
25
|
return undefined;
|
|
24
26
|
}
|
|
25
|
-
if (config.
|
|
26
|
-
config.
|
|
27
|
+
if (config.validatorPrivateKeys === undefined || !config.validatorPrivateKeys?.length) {
|
|
28
|
+
config.validatorPrivateKeys = [generatePrivateKey()];
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
return ValidatorClient.new(
|
|
30
32
|
config,
|
|
33
|
+
deps.blockBuilder,
|
|
31
34
|
deps.epochCache,
|
|
32
35
|
deps.p2pClient,
|
|
33
36
|
deps.blockSource,
|
|
@@ -8,19 +8,29 @@ import type { Signature } from '@aztec/foundation/eth-signature';
|
|
|
8
8
|
*/
|
|
9
9
|
export interface ValidatorKeyStore {
|
|
10
10
|
/**
|
|
11
|
-
* Get the address of
|
|
11
|
+
* Get the address of a signer by index
|
|
12
12
|
*
|
|
13
|
+
* @param index - The index of the signer
|
|
13
14
|
* @returns the address
|
|
14
15
|
*/
|
|
15
|
-
getAddress(): EthAddress;
|
|
16
|
+
getAddress(index: number): EthAddress;
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Get all addresses
|
|
20
|
+
*
|
|
21
|
+
* @returns all addresses
|
|
22
|
+
*/
|
|
23
|
+
getAddresses(): EthAddress[];
|
|
24
|
+
|
|
25
|
+
sign(message: Buffer32): Promise<Signature[]>;
|
|
26
|
+
signWithAddress(address: EthAddress, message: Buffer32): Promise<Signature>;
|
|
18
27
|
/**
|
|
19
28
|
* Flavor of sign message that followed EIP-712 eth signed message prefix
|
|
20
29
|
* Note: this is only required when we are using ecdsa signatures over secp256k1
|
|
21
30
|
*
|
|
22
31
|
* @param message - The message to sign.
|
|
23
|
-
* @returns The
|
|
32
|
+
* @returns The signatures.
|
|
24
33
|
*/
|
|
25
|
-
signMessage(message: Buffer32): Promise<Signature>;
|
|
34
|
+
signMessage(message: Buffer32): Promise<Signature[]>;
|
|
35
|
+
signMessageWithAddress(address: EthAddress, message: Buffer32): Promise<Signature>;
|
|
26
36
|
}
|
|
@@ -8,39 +8,85 @@ import type { ValidatorKeyStore } from './interface.js';
|
|
|
8
8
|
/**
|
|
9
9
|
* Local Key Store
|
|
10
10
|
*
|
|
11
|
-
* An implementation of the Key store using
|
|
11
|
+
* An implementation of the Key store using in memory private keys.
|
|
12
12
|
*/
|
|
13
13
|
export class LocalKeyStore implements ValidatorKeyStore {
|
|
14
|
-
private
|
|
14
|
+
private signers: Secp256k1Signer[];
|
|
15
|
+
private signersByAddress: Map<`0x${string}`, Secp256k1Signer>;
|
|
15
16
|
|
|
16
|
-
constructor(
|
|
17
|
-
this.
|
|
17
|
+
constructor(privateKeys: Buffer32[]) {
|
|
18
|
+
this.signers = privateKeys.map(privateKey => new Secp256k1Signer(privateKey));
|
|
19
|
+
this.signersByAddress = new Map(this.signers.map(signer => [signer.address.toString(), signer]));
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
|
-
* Get the address of
|
|
23
|
+
* Get the address of a signer by index
|
|
22
24
|
*
|
|
25
|
+
* @param index - The index of the signer
|
|
23
26
|
* @returns the address
|
|
24
27
|
*/
|
|
25
|
-
public getAddress(): EthAddress {
|
|
26
|
-
|
|
28
|
+
public getAddress(index: number): EthAddress {
|
|
29
|
+
if (index >= this.signers.length) {
|
|
30
|
+
throw new Error(`Index ${index} is out of bounds.`);
|
|
31
|
+
}
|
|
32
|
+
return this.signers[index].address;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
/**
|
|
30
|
-
*
|
|
36
|
+
* Get the addresses of all signers
|
|
31
37
|
*
|
|
32
|
-
* @
|
|
38
|
+
* @returns the addresses
|
|
39
|
+
*/
|
|
40
|
+
public getAddresses(): EthAddress[] {
|
|
41
|
+
return this.signers.map(signer => signer.address);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Sign a message with all keystore private keys
|
|
46
|
+
* @param digest - The message buffer to sign
|
|
33
47
|
* @return signature
|
|
34
48
|
*/
|
|
35
|
-
public sign(digest: Buffer32): Promise<Signature> {
|
|
36
|
-
|
|
49
|
+
public sign(digest: Buffer32): Promise<Signature[]> {
|
|
50
|
+
return Promise.all(this.signers.map(signer => signer.sign(digest)));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Sign a message with a specific address's private key
|
|
55
|
+
* @param address - The address of the signer to use
|
|
56
|
+
* @param digest - The message buffer to sign
|
|
57
|
+
* @returns signature for the specified address
|
|
58
|
+
* @throws Error if the address is not found in the keystore
|
|
59
|
+
*/
|
|
60
|
+
public signWithAddress(address: EthAddress, digest: Buffer32): Promise<Signature> {
|
|
61
|
+
const signer = this.signersByAddress.get(address.toString());
|
|
62
|
+
if (!signer) {
|
|
63
|
+
throw new Error(`No signer found for address ${address.toString()}`);
|
|
64
|
+
}
|
|
65
|
+
return Promise.resolve(signer.sign(digest));
|
|
66
|
+
}
|
|
37
67
|
|
|
38
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Sign a message with all keystore private keys
|
|
70
|
+
*
|
|
71
|
+
* @param message - The message to sign
|
|
72
|
+
* @return signatures
|
|
73
|
+
*/
|
|
74
|
+
public signMessage(message: Buffer32): Promise<Signature[]> {
|
|
75
|
+
return Promise.all(this.signers.map(signer => signer.signMessage(message)));
|
|
39
76
|
}
|
|
40
77
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Sign a message with a specific address's private key
|
|
80
|
+
* @param address - The address of the signer to use
|
|
81
|
+
* @param message - The message to sign
|
|
82
|
+
* @returns signature for the specified address
|
|
83
|
+
* @throws Error if the address is not found in the keystore
|
|
84
|
+
*/
|
|
85
|
+
public signMessageWithAddress(address: EthAddress, message: Buffer32): Promise<Signature> {
|
|
86
|
+
const signer = this.signersByAddress.get(address.toString());
|
|
87
|
+
if (!signer) {
|
|
88
|
+
throw new Error(`No signer found for address ${address.toString()}`);
|
|
89
|
+
}
|
|
90
|
+
return Promise.resolve(signer.signMessage(message));
|
|
45
91
|
}
|
|
46
92
|
}
|
package/src/metrics.ts
CHANGED
|
@@ -59,12 +59,12 @@ export class ValidatorMetrics {
|
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
public incAttestations() {
|
|
63
|
-
this.attestationsCount.add(
|
|
62
|
+
public incAttestations(num: number) {
|
|
63
|
+
this.attestationsCount.add(num);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
public incFailedAttestations(reason: string) {
|
|
67
|
-
this.failedAttestationsCount.add(
|
|
66
|
+
public incFailedAttestations(num: number, reason: string) {
|
|
67
|
+
this.failedAttestationsCount.add(num, {
|
|
68
68
|
[Attributes.ERROR_TYPE]: reason,
|
|
69
69
|
});
|
|
70
70
|
}
|