@aztec/validator-client 0.87.7 → 1.0.0-nightly.20250605
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/src/validator.ts
CHANGED
|
@@ -6,12 +6,15 @@ import { Fr } from '@aztec/foundation/fields';
|
|
|
6
6
|
import { createLogger } from '@aztec/foundation/log';
|
|
7
7
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
8
8
|
import { sleep } from '@aztec/foundation/sleep';
|
|
9
|
-
import { DateProvider
|
|
10
|
-
import {
|
|
9
|
+
import { DateProvider } from '@aztec/foundation/timer';
|
|
10
|
+
import type { P2P, PeerId } from '@aztec/p2p';
|
|
11
|
+
import { TxCollector } from '@aztec/p2p';
|
|
11
12
|
import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
|
|
12
|
-
import type {
|
|
13
|
+
import type { L2BlockSource } from '@aztec/stdlib/block';
|
|
14
|
+
import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
15
|
+
import type { IFullNodeBlockBuilder } from '@aztec/stdlib/interfaces/server';
|
|
13
16
|
import type { BlockAttestation, BlockProposal, BlockProposalOptions } from '@aztec/stdlib/p2p';
|
|
14
|
-
import type
|
|
17
|
+
import { GlobalVariables, type ProposedBlockHeader, type StateReference, type Tx } from '@aztec/stdlib/tx';
|
|
15
18
|
import { type TelemetryClient, WithTracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
16
19
|
|
|
17
20
|
import type { ValidatorClientConfig } from './config.js';
|
|
@@ -29,28 +32,9 @@ import type { ValidatorKeyStore } from './key_store/interface.js';
|
|
|
29
32
|
import { LocalKeyStore } from './key_store/local_key_store.js';
|
|
30
33
|
import { ValidatorMetrics } from './metrics.js';
|
|
31
34
|
|
|
32
|
-
/**
|
|
33
|
-
* Callback function for building a block
|
|
34
|
-
*
|
|
35
|
-
* We reuse the sequencer's block building functionality for re-execution
|
|
36
|
-
*/
|
|
37
|
-
type BlockBuilderCallback = (
|
|
38
|
-
blockNumber: Fr,
|
|
39
|
-
header: ProposedBlockHeader,
|
|
40
|
-
txs: Iterable<Tx> | AsyncIterableIterator<Tx>,
|
|
41
|
-
opts?: { validateOnly?: boolean },
|
|
42
|
-
) => Promise<{
|
|
43
|
-
block: L2Block;
|
|
44
|
-
publicProcessorDuration: number;
|
|
45
|
-
numTxs: number;
|
|
46
|
-
numFailedTxs: number;
|
|
47
|
-
blockBuildingTimer: Timer;
|
|
48
|
-
}>;
|
|
49
|
-
|
|
50
35
|
export interface Validator {
|
|
51
36
|
start(): Promise<void>;
|
|
52
37
|
registerBlockProposalHandler(): void;
|
|
53
|
-
registerBlockBuilder(blockBuilder: BlockBuilderCallback): void;
|
|
54
38
|
|
|
55
39
|
// Block validation responsibilities
|
|
56
40
|
createBlockProposal(
|
|
@@ -59,9 +43,10 @@ export interface Validator {
|
|
|
59
43
|
archive: Fr,
|
|
60
44
|
stateReference: StateReference,
|
|
61
45
|
txs: Tx[],
|
|
46
|
+
proposerAddress: EthAddress,
|
|
62
47
|
options: BlockProposalOptions,
|
|
63
48
|
): Promise<BlockProposal | undefined>;
|
|
64
|
-
attestToProposal(proposal: BlockProposal, sender: PeerId): Promise<BlockAttestation | undefined>;
|
|
49
|
+
attestToProposal(proposal: BlockProposal, sender: PeerId): Promise<BlockAttestation[] | undefined>;
|
|
65
50
|
|
|
66
51
|
broadcastBlockProposal(proposal: BlockProposal): Promise<void>;
|
|
67
52
|
collectAttestations(proposal: BlockProposal, required: number, deadline: Date): Promise<BlockAttestation[]>;
|
|
@@ -77,10 +62,7 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
77
62
|
// Used to check if we are sending the same proposal twice
|
|
78
63
|
private previousProposal?: BlockProposal;
|
|
79
64
|
|
|
80
|
-
|
|
81
|
-
private blockBuilder?: BlockBuilderCallback = undefined;
|
|
82
|
-
|
|
83
|
-
private myAddress: EthAddress;
|
|
65
|
+
private myAddresses: EthAddress[];
|
|
84
66
|
private lastEpoch: bigint | undefined;
|
|
85
67
|
private epochCacheUpdateLoop: RunningPromise;
|
|
86
68
|
|
|
@@ -88,6 +70,7 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
88
70
|
private txCollector: TxCollector;
|
|
89
71
|
|
|
90
72
|
constructor(
|
|
73
|
+
private blockBuilder: IFullNodeBlockBuilder,
|
|
91
74
|
private keyStore: ValidatorKeyStore,
|
|
92
75
|
private epochCache: EpochCache,
|
|
93
76
|
private p2pClient: P2P,
|
|
@@ -108,21 +91,27 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
108
91
|
this.txCollector = new TxCollector(p2pClient, this.log);
|
|
109
92
|
|
|
110
93
|
// Refresh epoch cache every second to trigger alert if participation in committee changes
|
|
111
|
-
this.
|
|
94
|
+
this.myAddresses = this.keyStore.getAddresses();
|
|
112
95
|
this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), log, 1000);
|
|
113
96
|
|
|
114
|
-
this.log.verbose(`Initialized validator with
|
|
97
|
+
this.log.verbose(`Initialized validator with addresses: ${this.myAddresses.map(a => a.toString()).join(', ')}`);
|
|
115
98
|
}
|
|
116
99
|
|
|
117
100
|
private async handleEpochCommitteeUpdate() {
|
|
118
101
|
try {
|
|
119
102
|
const { committee, epoch } = await this.epochCache.getCommittee('now');
|
|
120
103
|
if (epoch !== this.lastEpoch) {
|
|
121
|
-
const me = this.
|
|
122
|
-
|
|
123
|
-
|
|
104
|
+
const me = this.myAddresses;
|
|
105
|
+
const committeeSet = new Set(committee.map(v => v.toString()));
|
|
106
|
+
const inCommittee = me.filter(a => committeeSet.has(a.toString()));
|
|
107
|
+
if (inCommittee.length > 0) {
|
|
108
|
+
inCommittee.forEach(a =>
|
|
109
|
+
this.log.info(`Validator ${a.toString()} is on the validator committee for epoch ${epoch}`),
|
|
110
|
+
);
|
|
124
111
|
} else {
|
|
125
|
-
this.log.verbose(
|
|
112
|
+
this.log.verbose(
|
|
113
|
+
`Validators ${me.map(a => a.toString()).join(', ')} are not on the validator committee for epoch ${epoch}`,
|
|
114
|
+
);
|
|
126
115
|
}
|
|
127
116
|
this.lastEpoch = epoch;
|
|
128
117
|
}
|
|
@@ -133,20 +122,22 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
133
122
|
|
|
134
123
|
static new(
|
|
135
124
|
config: ValidatorClientConfig,
|
|
125
|
+
blockBuilder: IFullNodeBlockBuilder,
|
|
136
126
|
epochCache: EpochCache,
|
|
137
127
|
p2pClient: P2P,
|
|
138
128
|
blockSource: L2BlockSource,
|
|
139
129
|
dateProvider: DateProvider = new DateProvider(),
|
|
140
130
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
141
131
|
) {
|
|
142
|
-
if (!config.
|
|
132
|
+
if (!config.validatorPrivateKeys?.length) {
|
|
143
133
|
throw new InvalidValidatorPrivateKeyError();
|
|
144
134
|
}
|
|
145
135
|
|
|
146
|
-
const
|
|
147
|
-
const localKeyStore = new LocalKeyStore(
|
|
136
|
+
const privateKeys = config.validatorPrivateKeys.map(validatePrivateKey);
|
|
137
|
+
const localKeyStore = new LocalKeyStore(privateKeys);
|
|
148
138
|
|
|
149
139
|
const validator = new ValidatorClient(
|
|
140
|
+
blockBuilder,
|
|
150
141
|
localKeyStore,
|
|
151
142
|
epochCache,
|
|
152
143
|
p2pClient,
|
|
@@ -159,20 +150,25 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
159
150
|
return validator;
|
|
160
151
|
}
|
|
161
152
|
|
|
162
|
-
public
|
|
163
|
-
return this.keyStore.
|
|
153
|
+
public getValidatorAddresses() {
|
|
154
|
+
return this.keyStore.getAddresses();
|
|
164
155
|
}
|
|
165
156
|
|
|
166
157
|
public async start() {
|
|
167
158
|
// Sync the committee from the smart contract
|
|
168
159
|
// https://github.com/AztecProtocol/aztec-packages/issues/7962
|
|
169
160
|
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
161
|
+
const myAddresses = this.keyStore.getAddresses();
|
|
162
|
+
|
|
163
|
+
const inCommittee = await this.epochCache.filterInCommittee(myAddresses);
|
|
164
|
+
if (inCommittee.length > 0) {
|
|
165
|
+
this.log.info(
|
|
166
|
+
`Started validator with addresses in current validator committee:
|
|
167
|
+
${inCommittee.map(a => a.toString()).join(', ')}`,
|
|
168
|
+
);
|
|
174
169
|
} else {
|
|
175
|
-
this.log.info(`Started validator with
|
|
170
|
+
this.log.info(`Started validator with addresses:
|
|
171
|
+
${myAddresses.map(a => a.toString()).join(', ')}`);
|
|
176
172
|
}
|
|
177
173
|
this.epochCacheUpdateLoop.start();
|
|
178
174
|
return Promise.resolve();
|
|
@@ -183,22 +179,13 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
183
179
|
}
|
|
184
180
|
|
|
185
181
|
public registerBlockProposalHandler() {
|
|
186
|
-
const handler = (block: BlockProposal, proposalSender:
|
|
182
|
+
const handler = (block: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> => {
|
|
187
183
|
return this.attestToProposal(block, proposalSender);
|
|
188
184
|
};
|
|
189
185
|
this.p2pClient.registerBlockProposalHandler(handler);
|
|
190
186
|
}
|
|
191
187
|
|
|
192
|
-
|
|
193
|
-
* Register a callback function for building a block
|
|
194
|
-
*
|
|
195
|
-
* We reuse the sequencer's block building functionality for re-execution
|
|
196
|
-
*/
|
|
197
|
-
public registerBlockBuilder(blockBuilder: BlockBuilderCallback) {
|
|
198
|
-
this.blockBuilder = blockBuilder;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async attestToProposal(proposal: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation | undefined> {
|
|
188
|
+
async attestToProposal(proposal: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> {
|
|
202
189
|
const slotNumber = proposal.slotNumber.toNumber();
|
|
203
190
|
const blockNumber = proposal.blockNumber.toNumber();
|
|
204
191
|
const proposalInfo = {
|
|
@@ -215,20 +202,20 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
215
202
|
const invalidProposal = await this.blockProposalValidator.validate(proposal);
|
|
216
203
|
if (invalidProposal) {
|
|
217
204
|
this.log.verbose(`Proposal is not valid, skipping attestation`);
|
|
218
|
-
this.metrics.incFailedAttestations('invalid_proposal');
|
|
205
|
+
this.metrics.incFailedAttestations(1, 'invalid_proposal');
|
|
219
206
|
return undefined;
|
|
220
207
|
}
|
|
221
208
|
|
|
222
209
|
// Check that the parent proposal is a block we know, otherwise reexecution would fail.
|
|
223
210
|
// Q: Should we move this to the block proposal validator? If there, then p2p would check it
|
|
224
|
-
// before re-broadcasting it. This means that proposals built on top of an L1-
|
|
211
|
+
// before re-broadcasting it. This means that proposals built on top of an L1-reorg'ed-out block
|
|
225
212
|
// would not be rebroadcasted. But it also means that nodes that have not fully synced would
|
|
226
213
|
// not rebroadcast the proposal.
|
|
227
214
|
if (blockNumber > INITIAL_L2_BLOCK_NUM) {
|
|
228
215
|
const parentBlock = await this.blockSource.getBlock(blockNumber - 1);
|
|
229
216
|
if (parentBlock === undefined) {
|
|
230
217
|
this.log.verbose(`Parent block for ${blockNumber} not found, skipping attestation`);
|
|
231
|
-
this.metrics.incFailedAttestations('parent_block_not_found');
|
|
218
|
+
this.metrics.incFailedAttestations(1, 'parent_block_not_found');
|
|
232
219
|
return undefined;
|
|
233
220
|
}
|
|
234
221
|
if (!proposal.payload.header.lastArchiveRoot.equals(parentBlock.archive.root)) {
|
|
@@ -237,7 +224,7 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
237
224
|
parentBlockArchiveRoot: parentBlock.archive.root.toString(),
|
|
238
225
|
...proposalInfo,
|
|
239
226
|
});
|
|
240
|
-
this.metrics.incFailedAttestations('parent_block_does_not_match');
|
|
227
|
+
this.metrics.incFailedAttestations(1, 'parent_block_does_not_match');
|
|
241
228
|
return undefined;
|
|
242
229
|
}
|
|
243
230
|
}
|
|
@@ -245,9 +232,10 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
245
232
|
// Collect txs from the proposal
|
|
246
233
|
const { missing, txs } = await this.txCollector.collectForBlockProposal(proposal, proposalSender);
|
|
247
234
|
|
|
248
|
-
// Check that I
|
|
249
|
-
|
|
250
|
-
|
|
235
|
+
// Check that I have any address in current committee before attesting
|
|
236
|
+
const inCommittee = await this.epochCache.filterInCommittee(this.keyStore.getAddresses());
|
|
237
|
+
if (inCommittee.length === 0) {
|
|
238
|
+
this.log.verbose(`No validator in the committee, skipping attestation`);
|
|
251
239
|
return undefined;
|
|
252
240
|
}
|
|
253
241
|
|
|
@@ -258,7 +246,7 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
258
246
|
undefined,
|
|
259
247
|
{ proposalInfo, missing },
|
|
260
248
|
);
|
|
261
|
-
this.metrics.incFailedAttestations('TransactionsNotAvailableError');
|
|
249
|
+
this.metrics.incFailedAttestations(1, 'TransactionsNotAvailableError');
|
|
262
250
|
return undefined;
|
|
263
251
|
}
|
|
264
252
|
|
|
@@ -270,15 +258,26 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
270
258
|
await this.reExecuteTransactions(proposal, txs);
|
|
271
259
|
}
|
|
272
260
|
} catch (error: any) {
|
|
273
|
-
this.metrics.incFailedAttestations(error instanceof Error ? error.name : 'unknown');
|
|
261
|
+
this.metrics.incFailedAttestations(1, error instanceof Error ? error.name : 'unknown');
|
|
274
262
|
this.log.error(`Failed to attest to proposal`, error, proposalInfo);
|
|
275
263
|
return undefined;
|
|
276
264
|
}
|
|
277
265
|
|
|
278
266
|
// Provided all of the above checks pass, we can attest to the proposal
|
|
279
267
|
this.log.info(`Attesting to proposal for slot ${slotNumber}`, proposalInfo);
|
|
280
|
-
this.metrics.incAttestations();
|
|
281
|
-
|
|
268
|
+
this.metrics.incAttestations(inCommittee.length);
|
|
269
|
+
|
|
270
|
+
// If the above function does not throw an error, then we can attest to the proposal
|
|
271
|
+
return this.doAttestToProposal(proposal, inCommittee);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private getReexecutionDeadline(
|
|
275
|
+
proposal: BlockProposal,
|
|
276
|
+
config: { l1GenesisTime: bigint; slotDuration: number },
|
|
277
|
+
): Date {
|
|
278
|
+
const nextSlotTimestampSeconds = Number(getTimestampForSlot(proposal.slotNumber.toBigInt() + 1n, config));
|
|
279
|
+
const msNeededForPropagationAndPublishing = this.config.validatorReexecuteDeadlineMs;
|
|
280
|
+
return new Date(nextSlotTimestampSeconds * 1000 - msNeededForPropagationAndPublishing);
|
|
282
281
|
}
|
|
283
282
|
|
|
284
283
|
/**
|
|
@@ -302,12 +301,22 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
302
301
|
|
|
303
302
|
// Use the sequencer's block building logic to re-execute the transactions
|
|
304
303
|
const stopTimer = this.metrics.reExecutionTimer();
|
|
305
|
-
const
|
|
306
|
-
|
|
304
|
+
const config = this.blockBuilder.getConfig();
|
|
305
|
+
const globalVariables = GlobalVariables.from({
|
|
306
|
+
...proposal.payload.header,
|
|
307
|
+
blockNumber: proposal.blockNumber,
|
|
308
|
+
timestamp: new Fr(header.timestamp),
|
|
309
|
+
chainId: new Fr(config.l1ChainId),
|
|
310
|
+
version: new Fr(config.rollupVersion),
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const { block, failedTxs } = await this.blockBuilder.buildBlock(txs, globalVariables, {
|
|
314
|
+
deadline: this.getReexecutionDeadline(proposal, config),
|
|
307
315
|
});
|
|
308
316
|
stopTimer();
|
|
309
317
|
|
|
310
318
|
this.log.verbose(`Transaction re-execution complete`);
|
|
319
|
+
const numFailedTxs = failedTxs.length;
|
|
311
320
|
|
|
312
321
|
if (numFailedTxs > 0) {
|
|
313
322
|
this.metrics.recordFailedReexecution(proposal);
|
|
@@ -332,6 +341,7 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
332
341
|
archive: Fr,
|
|
333
342
|
stateReference: StateReference,
|
|
334
343
|
txs: Tx[],
|
|
344
|
+
proposerAddress: EthAddress,
|
|
335
345
|
options: BlockProposalOptions,
|
|
336
346
|
): Promise<BlockProposal | undefined> {
|
|
337
347
|
if (this.previousProposal?.slotNumber.equals(header.slotNumber)) {
|
|
@@ -345,6 +355,7 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
345
355
|
archive,
|
|
346
356
|
stateReference,
|
|
347
357
|
txs,
|
|
358
|
+
proposerAddress,
|
|
348
359
|
options,
|
|
349
360
|
);
|
|
350
361
|
this.previousProposal = newProposal;
|
|
@@ -355,7 +366,6 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
355
366
|
await this.p2pClient.broadcastProposal(proposal);
|
|
356
367
|
}
|
|
357
368
|
|
|
358
|
-
// TODO(https://github.com/AztecProtocol/aztec-packages/issues/7962)
|
|
359
369
|
async collectAttestations(proposal: BlockProposal, required: number, deadline: Date): Promise<BlockAttestation[]> {
|
|
360
370
|
// Wait and poll the p2pClient's attestation pool for this block until we have enough attestations
|
|
361
371
|
const slot = proposal.payload.header.slotNumber.toBigInt();
|
|
@@ -369,8 +379,11 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
369
379
|
}
|
|
370
380
|
|
|
371
381
|
const proposalId = proposal.archive.toString();
|
|
372
|
-
|
|
373
|
-
const
|
|
382
|
+
// adds attestations for all of my addresses locally
|
|
383
|
+
const inCommittee = await this.epochCache.filterInCommittee(this.keyStore.getAddresses());
|
|
384
|
+
await this.doAttestToProposal(proposal, inCommittee);
|
|
385
|
+
|
|
386
|
+
const myAddresses = this.keyStore.getAddresses();
|
|
374
387
|
|
|
375
388
|
let attestations: BlockAttestation[] = [];
|
|
376
389
|
while (true) {
|
|
@@ -378,7 +391,10 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
378
391
|
const oldSenders = attestations.map(attestation => attestation.getSender());
|
|
379
392
|
for (const collected of collectedAttestations) {
|
|
380
393
|
const collectedSender = collected.getSender();
|
|
381
|
-
if (
|
|
394
|
+
if (
|
|
395
|
+
!myAddresses.some(address => address.equals(collectedSender)) &&
|
|
396
|
+
!oldSenders.some(sender => sender.equals(collectedSender))
|
|
397
|
+
) {
|
|
382
398
|
this.log.debug(`Received attestation for slot ${slot} from ${collectedSender.toString()}`);
|
|
383
399
|
}
|
|
384
400
|
}
|
|
@@ -399,10 +415,10 @@ export class ValidatorClient extends WithTracer implements Validator {
|
|
|
399
415
|
}
|
|
400
416
|
}
|
|
401
417
|
|
|
402
|
-
private async doAttestToProposal(proposal: BlockProposal): Promise<BlockAttestation> {
|
|
403
|
-
const
|
|
404
|
-
await this.p2pClient.
|
|
405
|
-
return
|
|
418
|
+
private async doAttestToProposal(proposal: BlockProposal, attestors: EthAddress[] = []): Promise<BlockAttestation[]> {
|
|
419
|
+
const attestations = await this.validationService.attestToProposal(proposal, attestors);
|
|
420
|
+
await this.p2pClient.addAttestations(attestations);
|
|
421
|
+
return attestations;
|
|
406
422
|
}
|
|
407
423
|
}
|
|
408
424
|
|