@aztec/validator-client 0.0.1-commit.b655e406 → 0.0.1-commit.c7c42ec
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/block_proposal_handler.d.ts +7 -6
- package/dest/block_proposal_handler.d.ts.map +1 -1
- package/dest/block_proposal_handler.js +17 -13
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +5 -0
- package/dest/duties/validation_service.d.ts +4 -4
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +7 -8
- package/dest/factory.d.ts +3 -1
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +1 -1
- package/dest/index.d.ts +1 -1
- package/dest/key_store/index.d.ts +1 -1
- package/dest/key_store/interface.d.ts +1 -1
- package/dest/key_store/local_key_store.d.ts +1 -1
- package/dest/key_store/local_key_store.d.ts.map +1 -1
- package/dest/key_store/local_key_store.js +1 -1
- package/dest/key_store/node_keystore_adapter.d.ts +1 -1
- package/dest/key_store/node_keystore_adapter.d.ts.map +1 -1
- package/dest/key_store/web3signer_key_store.d.ts +1 -7
- package/dest/key_store/web3signer_key_store.d.ts.map +1 -1
- package/dest/key_store/web3signer_key_store.js +1 -1
- package/dest/metrics.d.ts +1 -1
- package/dest/metrics.d.ts.map +1 -1
- package/dest/validator.d.ts +12 -8
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +100 -28
- package/package.json +16 -14
- package/src/block_proposal_handler.ts +26 -23
- package/src/config.ts +6 -0
- package/src/duties/validation_service.ts +7 -10
- package/src/factory.ts +3 -0
- package/src/key_store/local_key_store.ts +1 -1
- package/src/key_store/node_keystore_adapter.ts +1 -1
- package/src/key_store/web3signer_key_store.ts +1 -1
- package/src/validator.ts +118 -41
package/src/validator.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import type { FileStoreBlobClient } from '@aztec/blob-client/filestore';
|
|
2
|
+
import { getBlobsPerL1Block } from '@aztec/blob-lib';
|
|
1
3
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
4
|
+
import { BlockNumber, EpochNumber } from '@aztec/foundation/branded-types';
|
|
5
|
+
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
2
6
|
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
3
7
|
import type { Signature } from '@aztec/foundation/eth-signature';
|
|
4
|
-
import { Fr } from '@aztec/foundation/fields';
|
|
5
8
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
6
9
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
7
10
|
import { sleep } from '@aztec/foundation/sleep';
|
|
@@ -16,9 +19,9 @@ import type { IFullNodeBlockBuilder, Validator, ValidatorClientFullConfig } from
|
|
|
16
19
|
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
17
20
|
import type { BlockAttestation, BlockProposal, BlockProposalOptions } from '@aztec/stdlib/p2p';
|
|
18
21
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
19
|
-
import type {
|
|
22
|
+
import type { Tx } from '@aztec/stdlib/tx';
|
|
20
23
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
21
|
-
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
24
|
+
import { Attributes, type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
22
25
|
|
|
23
26
|
import { EventEmitter } from 'events';
|
|
24
27
|
import type { TypedDataDefinition } from 'viem';
|
|
@@ -45,6 +48,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
45
48
|
public readonly tracer: Tracer;
|
|
46
49
|
private validationService: ValidationService;
|
|
47
50
|
private metrics: ValidatorMetrics;
|
|
51
|
+
private log: Logger;
|
|
48
52
|
|
|
49
53
|
// Whether it has already registered handlers on the p2p client
|
|
50
54
|
private hasRegisteredHandlers = false;
|
|
@@ -52,7 +56,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
52
56
|
// Used to check if we are sending the same proposal twice
|
|
53
57
|
private previousProposal?: BlockProposal;
|
|
54
58
|
|
|
55
|
-
private lastEpochForCommitteeUpdateLoop:
|
|
59
|
+
private lastEpochForCommitteeUpdateLoop: EpochNumber | undefined;
|
|
56
60
|
private epochCacheUpdateLoop: RunningPromise;
|
|
57
61
|
|
|
58
62
|
private proposersOfInvalidBlocks: Set<string> = new Set();
|
|
@@ -63,18 +67,23 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
63
67
|
private p2pClient: P2P,
|
|
64
68
|
private blockProposalHandler: BlockProposalHandler,
|
|
65
69
|
private config: ValidatorClientFullConfig,
|
|
70
|
+
private fileStoreBlobUploadClient: FileStoreBlobClient | undefined,
|
|
66
71
|
private dateProvider: DateProvider = new DateProvider(),
|
|
67
72
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
68
|
-
|
|
73
|
+
log = createLogger('validator'),
|
|
69
74
|
) {
|
|
70
75
|
super();
|
|
76
|
+
|
|
77
|
+
// Create child logger with fisherman prefix if in fisherman mode
|
|
78
|
+
this.log = config.fishermanMode ? log.createChild('[FISHERMAN]') : log;
|
|
79
|
+
|
|
71
80
|
this.tracer = telemetry.getTracer('Validator');
|
|
72
81
|
this.metrics = new ValidatorMetrics(telemetry);
|
|
73
82
|
|
|
74
|
-
this.validationService = new ValidationService(keyStore, log.createChild('validation-service'));
|
|
83
|
+
this.validationService = new ValidationService(keyStore, this.log.createChild('validation-service'));
|
|
75
84
|
|
|
76
85
|
// Refresh epoch cache every second to trigger alert if participation in committee changes
|
|
77
|
-
this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), log, 1000);
|
|
86
|
+
this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), this.log, 1000);
|
|
78
87
|
|
|
79
88
|
const myAddresses = this.getValidatorAddresses();
|
|
80
89
|
this.log.verbose(`Initialized validator with addresses: ${myAddresses.map(a => a.toString()).join(', ')}`);
|
|
@@ -141,6 +150,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
141
150
|
l1ToL2MessageSource: L1ToL2MessageSource,
|
|
142
151
|
txProvider: TxProvider,
|
|
143
152
|
keyStoreManager: KeystoreManager,
|
|
153
|
+
fileStoreBlobUploadClient?: FileStoreBlobClient,
|
|
144
154
|
dateProvider: DateProvider = new DateProvider(),
|
|
145
155
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
146
156
|
) {
|
|
@@ -166,6 +176,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
166
176
|
p2pClient,
|
|
167
177
|
blockProposalHandler,
|
|
168
178
|
config,
|
|
179
|
+
fileStoreBlobUploadClient,
|
|
169
180
|
dateProvider,
|
|
170
181
|
telemetry,
|
|
171
182
|
);
|
|
@@ -186,7 +197,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
186
197
|
// Proxy method for backwards compatibility with tests
|
|
187
198
|
public reExecuteTransactions(
|
|
188
199
|
proposal: BlockProposal,
|
|
189
|
-
blockNumber:
|
|
200
|
+
blockNumber: BlockNumber,
|
|
190
201
|
txs: any[],
|
|
191
202
|
l1ToL2Messages: Fr[],
|
|
192
203
|
): Promise<any> {
|
|
@@ -223,14 +234,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
223
234
|
|
|
224
235
|
const myAddresses = this.getValidatorAddresses();
|
|
225
236
|
const inCommittee = await this.epochCache.filterInCommittee('now', myAddresses);
|
|
237
|
+
this.log.info(`Started validator with addresses: ${myAddresses.map(a => a.toString()).join(', ')}`);
|
|
226
238
|
if (inCommittee.length > 0) {
|
|
227
|
-
this.log.info(
|
|
228
|
-
`Started validator with addresses in current validator committee: ${inCommittee
|
|
229
|
-
.map(a => a.toString())
|
|
230
|
-
.join(', ')}`,
|
|
231
|
-
);
|
|
232
|
-
} else {
|
|
233
|
-
this.log.info(`Started validator with addresses: ${myAddresses.map(a => a.toString()).join(', ')}`);
|
|
239
|
+
this.log.info(`Addresses in current validator committee: ${inCommittee.map(a => a.toString()).join(', ')}`);
|
|
234
240
|
}
|
|
235
241
|
this.epochCacheUpdateLoop.start();
|
|
236
242
|
|
|
@@ -258,8 +264,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
258
264
|
}
|
|
259
265
|
}
|
|
260
266
|
|
|
267
|
+
@trackSpan('validator.attestToProposal', (proposal, proposalSender) => ({
|
|
268
|
+
[Attributes.BLOCK_HASH]: proposal.payload.header.hash.toString(),
|
|
269
|
+
[Attributes.PEER_ID]: proposalSender.toString(),
|
|
270
|
+
}))
|
|
261
271
|
async attestToProposal(proposal: BlockProposal, proposalSender: PeerId): Promise<BlockAttestation[] | undefined> {
|
|
262
|
-
const slotNumber = proposal.slotNumber
|
|
272
|
+
const slotNumber = proposal.slotNumber;
|
|
263
273
|
const proposer = proposal.getSender();
|
|
264
274
|
|
|
265
275
|
// Reject proposals with invalid signatures
|
|
@@ -276,15 +286,20 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
276
286
|
this.log.info(`Received proposal for slot ${slotNumber}`, {
|
|
277
287
|
...proposalInfo,
|
|
278
288
|
txHashes: proposal.txHashes.map(t => t.toString()),
|
|
289
|
+
fishermanMode: this.config.fishermanMode || false,
|
|
279
290
|
});
|
|
280
291
|
|
|
281
292
|
// Reexecute txs if we are part of the committee so we can attest, or if slashing is enabled so we can slash
|
|
282
293
|
// invalid proposals even when not in the committee, or if we are configured to always reexecute for monitoring purposes.
|
|
283
|
-
|
|
294
|
+
// In fisherman mode, we always reexecute to validate proposals.
|
|
295
|
+
const { validatorReexecute, slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } =
|
|
296
|
+
this.config;
|
|
284
297
|
const shouldReexecute =
|
|
298
|
+
fishermanMode ||
|
|
285
299
|
(slashBroadcastedInvalidBlockPenalty > 0n && validatorReexecute) ||
|
|
286
300
|
(partOfCommittee && validatorReexecute) ||
|
|
287
|
-
alwaysReexecuteBlockProposals
|
|
301
|
+
alwaysReexecuteBlockProposals ||
|
|
302
|
+
this.fileStoreBlobUploadClient;
|
|
288
303
|
|
|
289
304
|
const validationResult = await this.blockProposalHandler.handleBlockProposal(
|
|
290
305
|
proposal,
|
|
@@ -325,17 +340,61 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
325
340
|
}
|
|
326
341
|
|
|
327
342
|
// Check that I have any address in current committee before attesting
|
|
328
|
-
if
|
|
343
|
+
// In fisherman mode, we still create attestations for validation even if not in committee
|
|
344
|
+
if (!partOfCommittee && !this.config.fishermanMode) {
|
|
329
345
|
this.log.verbose(`No validator in the current committee, skipping attestation`, proposalInfo);
|
|
330
346
|
return undefined;
|
|
331
347
|
}
|
|
332
348
|
|
|
333
349
|
// Provided all of the above checks pass, we can attest to the proposal
|
|
334
|
-
this.log.info(
|
|
350
|
+
this.log.info(`${partOfCommittee ? 'Attesting to' : 'Validated'} proposal for slot ${slotNumber}`, {
|
|
351
|
+
...proposalInfo,
|
|
352
|
+
inCommittee: partOfCommittee,
|
|
353
|
+
fishermanMode: this.config.fishermanMode || false,
|
|
354
|
+
});
|
|
355
|
+
|
|
335
356
|
this.metrics.incSuccessfulAttestations(inCommittee.length);
|
|
336
357
|
|
|
358
|
+
// Upload blobs to filestore after successful re-execution (fire-and-forget)
|
|
359
|
+
if (validationResult.reexecutionResult?.block && this.fileStoreBlobUploadClient) {
|
|
360
|
+
void Promise.resolve().then(async () => {
|
|
361
|
+
try {
|
|
362
|
+
const blobFields = validationResult.reexecutionResult!.block.getCheckpointBlobFields();
|
|
363
|
+
const blobs = getBlobsPerL1Block(blobFields);
|
|
364
|
+
await this.fileStoreBlobUploadClient!.saveBlobs(blobs, true);
|
|
365
|
+
this.log.debug(`Uploaded ${blobs.length} blobs to filestore from re-execution`, proposalInfo);
|
|
366
|
+
} catch (err) {
|
|
367
|
+
this.log.warn(`Failed to upload blobs from re-execution`, err);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
337
372
|
// If the above function does not throw an error, then we can attest to the proposal
|
|
338
|
-
|
|
373
|
+
// Determine which validators should attest
|
|
374
|
+
let attestors: EthAddress[];
|
|
375
|
+
if (partOfCommittee) {
|
|
376
|
+
attestors = inCommittee;
|
|
377
|
+
} else if (this.config.fishermanMode) {
|
|
378
|
+
// In fisherman mode, create attestations for validation purposes even if not in committee. These won't be broadcast.
|
|
379
|
+
attestors = this.getValidatorAddresses();
|
|
380
|
+
} else {
|
|
381
|
+
attestors = [];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Only create attestations if we have attestors
|
|
385
|
+
if (attestors.length === 0) {
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (this.config.fishermanMode) {
|
|
390
|
+
// bail out early and don't save attestations to the pool in fisherman mode
|
|
391
|
+
this.log.info(`Creating attestations for proposal for slot ${slotNumber}`, {
|
|
392
|
+
...proposalInfo,
|
|
393
|
+
attestors: attestors.map(a => a.toString()),
|
|
394
|
+
});
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
return this.createBlockAttestationsFromProposal(proposal, attestors);
|
|
339
398
|
}
|
|
340
399
|
|
|
341
400
|
private slashInvalidBlock(proposal: BlockProposal) {
|
|
@@ -360,37 +419,47 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
360
419
|
validator: proposer,
|
|
361
420
|
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
362
421
|
offenseType: OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL,
|
|
363
|
-
epochOrSlot: proposal.slotNumber
|
|
422
|
+
epochOrSlot: BigInt(proposal.slotNumber),
|
|
364
423
|
},
|
|
365
424
|
]);
|
|
366
425
|
}
|
|
367
426
|
|
|
427
|
+
// TODO(palla/mbps): Block proposal should not require a checkpoint proposal
|
|
368
428
|
async createBlockProposal(
|
|
369
|
-
blockNumber:
|
|
429
|
+
blockNumber: BlockNumber,
|
|
370
430
|
header: CheckpointHeader,
|
|
371
431
|
archive: Fr,
|
|
372
|
-
stateReference: StateReference,
|
|
373
432
|
txs: Tx[],
|
|
374
433
|
proposerAddress: EthAddress | undefined,
|
|
375
434
|
options: BlockProposalOptions,
|
|
376
|
-
): Promise<BlockProposal
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
{ ...options, broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal },
|
|
389
|
-
);
|
|
435
|
+
): Promise<BlockProposal> {
|
|
436
|
+
// TODO(palla/mbps): Prevent double proposals properly
|
|
437
|
+
// if (this.previousProposal?.slotNumber === header.slotNumber) {
|
|
438
|
+
// this.log.verbose(`Already made a proposal for the same slot, skipping proposal`);
|
|
439
|
+
// return Promise.resolve(undefined);
|
|
440
|
+
// }
|
|
441
|
+
|
|
442
|
+
this.log.info(`Assembling block proposal for block ${blockNumber} slot ${header.slotNumber}`);
|
|
443
|
+
const newProposal = await this.validationService.createBlockProposal(header, archive, txs, proposerAddress, {
|
|
444
|
+
...options,
|
|
445
|
+
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
|
|
446
|
+
});
|
|
390
447
|
this.previousProposal = newProposal;
|
|
391
448
|
return newProposal;
|
|
392
449
|
}
|
|
393
450
|
|
|
451
|
+
// TODO(palla/mbps): Effectively create a checkpoint proposal different from a block proposal
|
|
452
|
+
createCheckpointProposal(
|
|
453
|
+
header: CheckpointHeader,
|
|
454
|
+
archive: Fr,
|
|
455
|
+
txs: Tx[],
|
|
456
|
+
proposerAddress: EthAddress | undefined,
|
|
457
|
+
options: BlockProposalOptions,
|
|
458
|
+
): Promise<BlockProposal> {
|
|
459
|
+
this.log.info(`Assembling checkpoint proposal for slot ${header.slotNumber}`);
|
|
460
|
+
return this.createBlockProposal(0 as BlockNumber, header, archive, txs, proposerAddress, options);
|
|
461
|
+
}
|
|
462
|
+
|
|
394
463
|
async broadcastBlockProposal(proposal: BlockProposal): Promise<void> {
|
|
395
464
|
await this.p2pClient.broadcastProposal(proposal);
|
|
396
465
|
}
|
|
@@ -403,15 +472,23 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
403
472
|
}
|
|
404
473
|
|
|
405
474
|
async collectOwnAttestations(proposal: BlockProposal): Promise<BlockAttestation[]> {
|
|
406
|
-
const slot = proposal.payload.header.slotNumber
|
|
475
|
+
const slot = proposal.payload.header.slotNumber;
|
|
407
476
|
const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
|
|
408
477
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
|
|
409
|
-
|
|
478
|
+
const attestations = await this.createBlockAttestationsFromProposal(proposal, inCommittee);
|
|
479
|
+
|
|
480
|
+
// We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
|
|
481
|
+
// other nodes can see that our validators did attest to this block proposal, and do not slash us
|
|
482
|
+
// due to inactivity for missed attestations.
|
|
483
|
+
void this.p2pClient.broadcastAttestations(attestations).catch(err => {
|
|
484
|
+
this.log.error(`Failed to broadcast self-attestations for slot ${slot}`, err);
|
|
485
|
+
});
|
|
486
|
+
return attestations;
|
|
410
487
|
}
|
|
411
488
|
|
|
412
489
|
async collectAttestations(proposal: BlockProposal, required: number, deadline: Date): Promise<BlockAttestation[]> {
|
|
413
490
|
// Wait and poll the p2pClient's attestation pool for this block until we have enough attestations
|
|
414
|
-
const slot = proposal.payload.header.slotNumber
|
|
491
|
+
const slot = proposal.payload.header.slotNumber;
|
|
415
492
|
this.log.debug(`Collecting ${required} attestations for slot ${slot} with deadline ${deadline.toISOString()}`);
|
|
416
493
|
|
|
417
494
|
if (+deadline < this.dateProvider.now()) {
|