@aztec/validator-client 0.0.1-commit.8afd444 → 0.0.1-commit.8ee97c858
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/README.md +62 -18
- package/dest/block_proposal_handler.d.ts +5 -4
- package/dest/block_proposal_handler.d.ts.map +1 -1
- package/dest/block_proposal_handler.js +130 -62
- package/dest/checkpoint_builder.d.ts +21 -8
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +124 -46
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +26 -1
- package/dest/duties/validation_service.d.ts +2 -2
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +6 -12
- package/dest/factory.d.ts +3 -1
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +3 -2
- package/dest/index.d.ts +1 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +0 -1
- package/dest/key_store/ha_key_store.js +1 -1
- package/dest/metrics.d.ts +9 -1
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +12 -0
- package/dest/validator.d.ts +37 -10
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +214 -47
- package/package.json +19 -19
- package/src/block_proposal_handler.ts +157 -80
- package/src/checkpoint_builder.ts +142 -39
- package/src/config.ts +26 -1
- package/src/duties/validation_service.ts +12 -11
- package/src/factory.ts +4 -0
- package/src/index.ts +0 -1
- package/src/key_store/ha_key_store.ts +1 -1
- package/src/metrics.ts +18 -0
- package/src/validator.ts +276 -57
- package/dest/tx_validator/index.d.ts +0 -3
- package/dest/tx_validator/index.d.ts.map +0 -1
- package/dest/tx_validator/index.js +0 -2
- package/dest/tx_validator/nullifier_cache.d.ts +0 -14
- package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
- package/dest/tx_validator/nullifier_cache.js +0 -24
- package/dest/tx_validator/tx_validator_factory.d.ts +0 -19
- package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
- package/dest/tx_validator/tx_validator_factory.js +0 -54
- package/src/tx_validator/index.ts +0 -2
- package/src/tx_validator/nullifier_cache.ts +0 -30
- package/src/tx_validator/tx_validator_factory.ts +0 -154
package/src/validator.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BlobClientInterface } from '@aztec/blob-client/client';
|
|
2
2
|
import { type Blob, getBlobsPerL1Block } from '@aztec/blob-lib';
|
|
3
3
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
4
|
+
import { validateFeeAssetPriceModifier } from '@aztec/ethereum/contracts';
|
|
4
5
|
import {
|
|
5
6
|
BlockNumber,
|
|
6
7
|
CheckpointNumber,
|
|
@@ -18,12 +19,13 @@ import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
|
18
19
|
import { sleep } from '@aztec/foundation/sleep';
|
|
19
20
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
20
21
|
import type { KeystoreManager } from '@aztec/node-keystore';
|
|
21
|
-
import type { P2P, PeerId } from '@aztec/p2p';
|
|
22
|
+
import type { DuplicateAttestationInfo, DuplicateProposalInfo, P2P, PeerId } from '@aztec/p2p';
|
|
22
23
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
23
24
|
import { OffenseType, WANT_TO_SLASH_EVENT, type Watcher, type WatcherEmitter } from '@aztec/slasher';
|
|
24
25
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
25
26
|
import type { CommitteeAttestationsAndSigners, L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
26
|
-
import {
|
|
27
|
+
import { validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
28
|
+
import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
27
29
|
import type {
|
|
28
30
|
CreateCheckpointProposalLastBlockData,
|
|
29
31
|
ITxProvider,
|
|
@@ -32,20 +34,25 @@ import type {
|
|
|
32
34
|
WorldStateSynchronizer,
|
|
33
35
|
} from '@aztec/stdlib/interfaces/server';
|
|
34
36
|
import { type L1ToL2MessageSource, accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
|
|
35
|
-
import
|
|
36
|
-
BlockProposal,
|
|
37
|
-
BlockProposalOptions,
|
|
38
|
-
CheckpointAttestation,
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
import {
|
|
38
|
+
type BlockProposal,
|
|
39
|
+
type BlockProposalOptions,
|
|
40
|
+
type CheckpointAttestation,
|
|
41
|
+
CheckpointProposal,
|
|
42
|
+
type CheckpointProposalCore,
|
|
43
|
+
type CheckpointProposalOptions,
|
|
41
44
|
} from '@aztec/stdlib/p2p';
|
|
42
|
-
import { CheckpointProposal } from '@aztec/stdlib/p2p';
|
|
43
45
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
44
46
|
import type { BlockHeader, CheckpointGlobalVariables, Tx } from '@aztec/stdlib/tx';
|
|
45
47
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
46
48
|
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
47
|
-
import {
|
|
48
|
-
|
|
49
|
+
import {
|
|
50
|
+
createHASigner,
|
|
51
|
+
createLocalSignerWithProtection,
|
|
52
|
+
createSignerFromSharedDb,
|
|
53
|
+
} from '@aztec/validator-ha-signer/factory';
|
|
54
|
+
import { DutyType, type SigningContext, type SlashingProtectionDatabase } from '@aztec/validator-ha-signer/types';
|
|
55
|
+
import type { ValidatorHASigner } from '@aztec/validator-ha-signer/validator-ha-signer';
|
|
49
56
|
|
|
50
57
|
import { EventEmitter } from 'events';
|
|
51
58
|
import type { TypedDataDefinition } from 'viem';
|
|
@@ -76,18 +83,25 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
76
83
|
private validationService: ValidationService;
|
|
77
84
|
private metrics: ValidatorMetrics;
|
|
78
85
|
private log: Logger;
|
|
79
|
-
|
|
80
86
|
// Whether it has already registered handlers on the p2p client
|
|
81
87
|
private hasRegisteredHandlers = false;
|
|
82
88
|
|
|
83
|
-
|
|
84
|
-
private
|
|
89
|
+
/** Tracks the last block proposal we created, to detect duplicate proposal attempts. */
|
|
90
|
+
private lastProposedBlock?: BlockProposal;
|
|
91
|
+
|
|
92
|
+
/** Tracks the last checkpoint proposal we created. */
|
|
93
|
+
private lastProposedCheckpoint?: CheckpointProposal;
|
|
85
94
|
|
|
86
95
|
private lastEpochForCommitteeUpdateLoop: EpochNumber | undefined;
|
|
87
96
|
private epochCacheUpdateLoop: RunningPromise;
|
|
97
|
+
/** Tracks the last epoch in which each attester successfully submitted at least one attestation. */
|
|
98
|
+
private lastAttestedEpochByAttester: Map<string, EpochNumber> = new Map();
|
|
88
99
|
|
|
89
100
|
private proposersOfInvalidBlocks: Set<string> = new Set();
|
|
90
101
|
|
|
102
|
+
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
|
|
103
|
+
private lastAttestedProposal?: CheckpointProposalCore;
|
|
104
|
+
|
|
91
105
|
protected constructor(
|
|
92
106
|
private keyStore: ExtendedValidatorKeyStore,
|
|
93
107
|
private epochCache: EpochCache,
|
|
@@ -99,6 +113,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
99
113
|
private l1ToL2MessageSource: L1ToL2MessageSource,
|
|
100
114
|
private config: ValidatorClientFullConfig,
|
|
101
115
|
private blobClient: BlobClientInterface,
|
|
116
|
+
private slashingProtectionSigner: ValidatorHASigner,
|
|
102
117
|
private dateProvider: DateProvider = new DateProvider(),
|
|
103
118
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
104
119
|
log = createLogger('validator'),
|
|
@@ -152,6 +167,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
152
167
|
this.log.trace(`No committee found for slot`);
|
|
153
168
|
return;
|
|
154
169
|
}
|
|
170
|
+
this.metrics.setCurrentEpoch(epoch);
|
|
155
171
|
if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
|
|
156
172
|
const me = this.getValidatorAddresses();
|
|
157
173
|
const committeeSet = new Set(committee.map(v => v.toString()));
|
|
@@ -185,10 +201,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
185
201
|
blobClient: BlobClientInterface,
|
|
186
202
|
dateProvider: DateProvider = new DateProvider(),
|
|
187
203
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
204
|
+
slashingProtectionDb?: SlashingProtectionDatabase,
|
|
188
205
|
) {
|
|
189
206
|
const metrics = new ValidatorMetrics(telemetry);
|
|
190
207
|
const blockProposalValidator = new BlockProposalValidator(epochCache, {
|
|
191
208
|
txsPermitted: !config.disableTransactions,
|
|
209
|
+
maxTxsPerBlock: config.validateMaxTxsPerBlock,
|
|
192
210
|
});
|
|
193
211
|
const blockProposalHandler = new BlockProposalHandler(
|
|
194
212
|
checkpointsBuilder,
|
|
@@ -204,16 +222,34 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
204
222
|
telemetry,
|
|
205
223
|
);
|
|
206
224
|
|
|
207
|
-
|
|
208
|
-
|
|
225
|
+
const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
226
|
+
let slashingProtectionSigner: ValidatorHASigner;
|
|
227
|
+
if (slashingProtectionDb) {
|
|
228
|
+
// Shared database mode: use a pre-existing database (e.g. for testing HA setups).
|
|
229
|
+
({ signer: slashingProtectionSigner } = createSignerFromSharedDb(slashingProtectionDb, config, {
|
|
230
|
+
telemetryClient: telemetry,
|
|
231
|
+
dateProvider,
|
|
232
|
+
}));
|
|
233
|
+
} else if (config.haSigningEnabled) {
|
|
234
|
+
// Multi-node HA mode: use PostgreSQL-backed distributed locking.
|
|
209
235
|
// If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
|
|
210
236
|
const haConfig = {
|
|
211
237
|
...config,
|
|
212
238
|
maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000,
|
|
213
239
|
};
|
|
214
|
-
|
|
215
|
-
|
|
240
|
+
({ signer: slashingProtectionSigner } = await createHASigner(haConfig, {
|
|
241
|
+
telemetryClient: telemetry,
|
|
242
|
+
dateProvider,
|
|
243
|
+
}));
|
|
244
|
+
} else {
|
|
245
|
+
// Single-node mode: use LMDB-backed local signing protection.
|
|
246
|
+
// This prevents double-signing if the node crashes and restarts mid-proposal.
|
|
247
|
+
({ signer: slashingProtectionSigner } = await createLocalSignerWithProtection(config, {
|
|
248
|
+
telemetryClient: telemetry,
|
|
249
|
+
dateProvider,
|
|
250
|
+
}));
|
|
216
251
|
}
|
|
252
|
+
const validatorKeyStore: ExtendedValidatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, slashingProtectionSigner);
|
|
217
253
|
|
|
218
254
|
const validator = new ValidatorClient(
|
|
219
255
|
validatorKeyStore,
|
|
@@ -226,6 +262,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
226
262
|
l1ToL2MessageSource,
|
|
227
263
|
config,
|
|
228
264
|
blobClient,
|
|
265
|
+
slashingProtectionSigner,
|
|
229
266
|
dateProvider,
|
|
230
267
|
telemetry,
|
|
231
268
|
);
|
|
@@ -263,6 +300,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
263
300
|
this.config = { ...this.config, ...config };
|
|
264
301
|
}
|
|
265
302
|
|
|
303
|
+
public reloadKeystore(newManager: KeystoreManager): void {
|
|
304
|
+
const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
|
|
305
|
+
this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
|
|
306
|
+
this.validationService = new ValidationService(this.keyStore, this.log.createChild('validation-service'));
|
|
307
|
+
}
|
|
308
|
+
|
|
266
309
|
public async start() {
|
|
267
310
|
if (this.epochCacheUpdateLoop.isRunning()) {
|
|
268
311
|
this.log.warn(`Validator client already started`);
|
|
@@ -309,6 +352,16 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
309
352
|
): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
|
|
310
353
|
this.p2pClient.registerCheckpointProposalHandler(checkpointHandler);
|
|
311
354
|
|
|
355
|
+
// Duplicate proposal handler - triggers slashing for equivocation
|
|
356
|
+
this.p2pClient.registerDuplicateProposalCallback((info: DuplicateProposalInfo) => {
|
|
357
|
+
this.handleDuplicateProposal(info);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Duplicate attestation handler - triggers slashing for attestation equivocation
|
|
361
|
+
this.p2pClient.registerDuplicateAttestationCallback((info: DuplicateAttestationInfo) => {
|
|
362
|
+
this.handleDuplicateAttestation(info);
|
|
363
|
+
});
|
|
364
|
+
|
|
312
365
|
const myAddresses = this.getValidatorAddresses();
|
|
313
366
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
314
367
|
|
|
@@ -336,6 +389,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
336
389
|
return false;
|
|
337
390
|
}
|
|
338
391
|
|
|
392
|
+
// Log self-proposals from HA peers (same validator key on different nodes)
|
|
393
|
+
if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
394
|
+
this.log.verbose(`Processing block proposal from HA peer for slot ${slotNumber}`, {
|
|
395
|
+
proposer: proposer.toString(),
|
|
396
|
+
slotNumber,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
339
400
|
// Check if we're in the committee (for metrics purposes)
|
|
340
401
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
341
402
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -365,9 +426,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
365
426
|
);
|
|
366
427
|
|
|
367
428
|
if (!validationResult.isValid) {
|
|
368
|
-
this.log.warn(`Block proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
369
|
-
|
|
370
429
|
const reason = validationResult.reason || 'unknown';
|
|
430
|
+
|
|
431
|
+
this.log.warn(`Block proposal validation failed: ${reason}`, proposalInfo);
|
|
432
|
+
|
|
371
433
|
// Classify failure reason: bad proposal vs node issue
|
|
372
434
|
const badProposalReasons: BlockProposalValidationFailureReason[] = [
|
|
373
435
|
'invalid_proposal',
|
|
@@ -422,40 +484,55 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
422
484
|
proposal: CheckpointProposalCore,
|
|
423
485
|
_proposalSender: PeerId,
|
|
424
486
|
): Promise<CheckpointAttestation[] | undefined> {
|
|
425
|
-
const
|
|
487
|
+
const proposalSlotNumber = proposal.slotNumber;
|
|
426
488
|
const proposer = proposal.getSender();
|
|
427
489
|
|
|
428
490
|
// If escape hatch is open for this slot's epoch, do not attest.
|
|
429
|
-
if (await this.epochCache.isEscapeHatchOpenAtSlot(
|
|
430
|
-
this.log.warn(`Escape hatch open for slot ${
|
|
491
|
+
if (await this.epochCache.isEscapeHatchOpenAtSlot(proposalSlotNumber)) {
|
|
492
|
+
this.log.warn(`Escape hatch open for slot ${proposalSlotNumber}, skipping checkpoint attestation handling`);
|
|
431
493
|
return undefined;
|
|
432
494
|
}
|
|
433
495
|
|
|
434
496
|
// Reject proposals with invalid signatures
|
|
435
497
|
if (!proposer) {
|
|
436
|
-
this.log.warn(`Received checkpoint proposal with invalid signature for slot ${
|
|
498
|
+
this.log.warn(`Received checkpoint proposal with invalid signature for proposal slot ${proposalSlotNumber}`);
|
|
437
499
|
return undefined;
|
|
438
500
|
}
|
|
439
501
|
|
|
440
|
-
//
|
|
441
|
-
|
|
502
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
503
|
+
if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
504
|
+
this.log.debug(`Ignoring block proposal from self for slot ${proposalSlotNumber}`, {
|
|
505
|
+
proposer: proposer.toString(),
|
|
506
|
+
proposalSlotNumber,
|
|
507
|
+
});
|
|
508
|
+
return undefined;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Validate fee asset price modifier is within allowed range
|
|
512
|
+
if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
|
|
513
|
+
this.log.warn(
|
|
514
|
+
`Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${proposalSlotNumber}`,
|
|
515
|
+
);
|
|
516
|
+
return undefined;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Check that I have any address in the committee where this checkpoint will land before attesting
|
|
520
|
+
const inCommittee = await this.epochCache.filterInCommittee(proposalSlotNumber, this.getValidatorAddresses());
|
|
442
521
|
const partOfCommittee = inCommittee.length > 0;
|
|
443
522
|
|
|
444
523
|
const proposalInfo = {
|
|
445
|
-
|
|
524
|
+
proposalSlotNumber,
|
|
446
525
|
archive: proposal.archive.toString(),
|
|
447
526
|
proposer: proposer.toString(),
|
|
448
|
-
txCount: proposal.txHashes.length,
|
|
449
527
|
};
|
|
450
|
-
this.log.info(`Received checkpoint proposal for slot ${
|
|
528
|
+
this.log.info(`Received checkpoint proposal for slot ${proposalSlotNumber}`, {
|
|
451
529
|
...proposalInfo,
|
|
452
|
-
txHashes: proposal.txHashes.map(t => t.toString()),
|
|
453
530
|
fishermanMode: this.config.fishermanMode || false,
|
|
454
531
|
});
|
|
455
532
|
|
|
456
533
|
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
|
|
457
534
|
if (this.config.skipCheckpointProposalValidation) {
|
|
458
|
-
this.log.warn(`Skipping checkpoint proposal validation for slot ${
|
|
535
|
+
this.log.warn(`Skipping checkpoint proposal validation for slot ${proposalSlotNumber}`, proposalInfo);
|
|
459
536
|
} else {
|
|
460
537
|
const validationResult = await this.validateCheckpointProposal(proposal, proposalInfo);
|
|
461
538
|
if (!validationResult.isValid) {
|
|
@@ -477,14 +554,28 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
477
554
|
}
|
|
478
555
|
|
|
479
556
|
// Provided all of the above checks pass, we can attest to the proposal
|
|
480
|
-
this.log.info(
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
557
|
+
this.log.info(
|
|
558
|
+
`${partOfCommittee ? 'Attesting to' : 'Validated'} checkpoint proposal for slot ${proposalSlotNumber}`,
|
|
559
|
+
{
|
|
560
|
+
...proposalInfo,
|
|
561
|
+
inCommittee: partOfCommittee,
|
|
562
|
+
fishermanMode: this.config.fishermanMode || false,
|
|
563
|
+
},
|
|
564
|
+
);
|
|
485
565
|
|
|
486
566
|
this.metrics.incSuccessfulAttestations(inCommittee.length);
|
|
487
567
|
|
|
568
|
+
// Track epoch participation per attester: count each (attester, epoch) pair at most once
|
|
569
|
+
const proposalEpoch = getEpochAtSlot(proposalSlotNumber, this.epochCache.getL1Constants());
|
|
570
|
+
for (const attester of inCommittee) {
|
|
571
|
+
const key = attester.toString();
|
|
572
|
+
const lastEpoch = this.lastAttestedEpochByAttester.get(key);
|
|
573
|
+
if (lastEpoch === undefined || proposalEpoch > lastEpoch) {
|
|
574
|
+
this.lastAttestedEpochByAttester.set(key, proposalEpoch);
|
|
575
|
+
this.metrics.incAttestedEpochCount(attester);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
488
579
|
// Determine which validators should attest
|
|
489
580
|
let attestors: EthAddress[];
|
|
490
581
|
if (partOfCommittee) {
|
|
@@ -503,22 +594,52 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
503
594
|
|
|
504
595
|
if (this.config.fishermanMode) {
|
|
505
596
|
// bail out early and don't save attestations to the pool in fisherman mode
|
|
506
|
-
this.log.info(`Creating checkpoint attestations for slot ${
|
|
597
|
+
this.log.info(`Creating checkpoint attestations for slot ${proposalSlotNumber}`, {
|
|
507
598
|
...proposalInfo,
|
|
508
599
|
attestors: attestors.map(a => a.toString()),
|
|
509
600
|
});
|
|
510
601
|
return undefined;
|
|
511
602
|
}
|
|
512
603
|
|
|
513
|
-
return this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
604
|
+
return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Checks if we should attest to a slot based on equivocation prevention rules.
|
|
609
|
+
* @returns true if we should attest, false if we should skip
|
|
610
|
+
*/
|
|
611
|
+
private shouldAttestToSlot(slotNumber: SlotNumber): boolean {
|
|
612
|
+
// If attestToEquivocatedProposals is true, always allow
|
|
613
|
+
if (this.config.attestToEquivocatedProposals) {
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Check if incoming slot is strictly greater than last attested
|
|
618
|
+
if (this.lastAttestedProposal && slotNumber <= this.lastAttestedProposal.slotNumber) {
|
|
619
|
+
this.log.warn(
|
|
620
|
+
`Refusing to process a proposal for slot ${slotNumber} given we already attested to a proposal for slot ${this.lastAttestedProposal.slotNumber}`,
|
|
621
|
+
);
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return true;
|
|
514
626
|
}
|
|
515
627
|
|
|
516
628
|
private async createCheckpointAttestationsFromProposal(
|
|
517
629
|
proposal: CheckpointProposalCore,
|
|
518
630
|
attestors: EthAddress[] = [],
|
|
519
|
-
): Promise<CheckpointAttestation[]> {
|
|
631
|
+
): Promise<CheckpointAttestation[] | undefined> {
|
|
632
|
+
// Equivocation check: must happen right before signing to minimize the race window
|
|
633
|
+
if (!this.shouldAttestToSlot(proposal.slotNumber)) {
|
|
634
|
+
return undefined;
|
|
635
|
+
}
|
|
636
|
+
|
|
520
637
|
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
521
|
-
|
|
638
|
+
|
|
639
|
+
// Track the proposal we attested to (to prevent equivocation)
|
|
640
|
+
this.lastAttestedProposal = proposal;
|
|
641
|
+
|
|
642
|
+
await this.p2pClient.addOwnCheckpointAttestations(attestations);
|
|
522
643
|
return attestations;
|
|
523
644
|
}
|
|
524
645
|
|
|
@@ -531,7 +652,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
531
652
|
proposalInfo: LogData,
|
|
532
653
|
): Promise<{ isValid: true } | { isValid: false; reason: string }> {
|
|
533
654
|
const slot = proposal.slotNumber;
|
|
534
|
-
|
|
655
|
+
|
|
656
|
+
// Timeout block syncing at the start of the next slot
|
|
657
|
+
const config = this.checkpointsBuilder.getConfig();
|
|
658
|
+
const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
|
|
659
|
+
const timeoutSeconds = Math.max(1, nextSlotTimestampSeconds - Math.floor(this.dateProvider.now() / 1000));
|
|
535
660
|
|
|
536
661
|
// Wait for last block to sync by archive
|
|
537
662
|
let lastBlockHeader: BlockHeader | undefined;
|
|
@@ -566,6 +691,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
566
691
|
return { isValid: false, reason: 'no_blocks_for_slot' };
|
|
567
692
|
}
|
|
568
693
|
|
|
694
|
+
// Ensure the last block for this slot matches the archive in the checkpoint proposal
|
|
695
|
+
if (!blocks.at(-1)?.archive.root.equals(proposal.archive)) {
|
|
696
|
+
this.log.warn(`Last block archive mismatch for checkpoint proposal`, proposalInfo);
|
|
697
|
+
return { isValid: false, reason: 'last_block_archive_mismatch' };
|
|
698
|
+
}
|
|
699
|
+
|
|
569
700
|
this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
|
|
570
701
|
...proposalInfo,
|
|
571
702
|
blockNumbers: blocks.map(b => b.number),
|
|
@@ -579,14 +710,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
579
710
|
// Get L1-to-L2 messages for this checkpoint
|
|
580
711
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
581
712
|
|
|
582
|
-
//
|
|
583
|
-
// TODO: There can be a more efficient way to get the previous checkpoint out hashes without having to fetch the
|
|
584
|
-
// actual checkpoints and the blocks/txs in them.
|
|
713
|
+
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
585
714
|
const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
|
|
586
|
-
const
|
|
587
|
-
.filter(
|
|
588
|
-
.
|
|
589
|
-
const previousCheckpointOutHashes = previousCheckpoints.map(c => c.getCheckpointOutHash());
|
|
715
|
+
const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch))
|
|
716
|
+
.filter(c => c.checkpointNumber < checkpointNumber)
|
|
717
|
+
.map(c => c.checkpointOutHash);
|
|
590
718
|
|
|
591
719
|
// Fork world state at the block before the first block
|
|
592
720
|
const parentBlockNumber = BlockNumber(firstBlock.number - 1);
|
|
@@ -597,6 +725,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
597
725
|
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(
|
|
598
726
|
checkpointNumber,
|
|
599
727
|
constants,
|
|
728
|
+
proposal.feeAssetPriceModifier,
|
|
600
729
|
l1ToL2Messages,
|
|
601
730
|
previousCheckpointOutHashes,
|
|
602
731
|
fork,
|
|
@@ -643,6 +772,20 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
643
772
|
return { isValid: false, reason: 'out_hash_mismatch' };
|
|
644
773
|
}
|
|
645
774
|
|
|
775
|
+
// Final round of validations on the checkpoint, just in case.
|
|
776
|
+
try {
|
|
777
|
+
validateCheckpoint(computedCheckpoint, {
|
|
778
|
+
rollupManaLimit: this.checkpointsBuilder.getConfig().rollupManaLimit,
|
|
779
|
+
maxDABlockGas: this.config.validateMaxDABlockGas,
|
|
780
|
+
maxL2BlockGas: this.config.validateMaxL2BlockGas,
|
|
781
|
+
maxTxsPerBlock: this.config.validateMaxTxsPerBlock,
|
|
782
|
+
maxTxsPerCheckpoint: this.config.validateMaxTxsPerCheckpoint,
|
|
783
|
+
});
|
|
784
|
+
} catch (err) {
|
|
785
|
+
this.log.warn(`Checkpoint validation failed: ${err}`, proposalInfo);
|
|
786
|
+
return { isValid: false, reason: 'checkpoint_validation_failed' };
|
|
787
|
+
}
|
|
788
|
+
|
|
646
789
|
this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
|
|
647
790
|
return { isValid: true };
|
|
648
791
|
} finally {
|
|
@@ -659,6 +802,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
659
802
|
chainId: gv.chainId,
|
|
660
803
|
version: gv.version,
|
|
661
804
|
slotNumber: gv.slotNumber,
|
|
805
|
+
timestamp: gv.timestamp,
|
|
662
806
|
coinbase: gv.coinbase,
|
|
663
807
|
feeRecipient: gv.feeRecipient,
|
|
664
808
|
gasFees: gv.gasFees,
|
|
@@ -668,7 +812,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
668
812
|
/**
|
|
669
813
|
* Uploads blobs for a checkpoint to the filestore (fire and forget).
|
|
670
814
|
*/
|
|
671
|
-
|
|
815
|
+
protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
|
|
672
816
|
try {
|
|
673
817
|
const lastBlockHeader = await this.blockSource.getBlockHeaderByArchive(proposal.archive);
|
|
674
818
|
if (!lastBlockHeader) {
|
|
@@ -683,7 +827,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
683
827
|
}
|
|
684
828
|
|
|
685
829
|
const blobFields = blocks.flatMap(b => b.toBlobFields());
|
|
686
|
-
const blobs: Blob[] = getBlobsPerL1Block(blobFields);
|
|
830
|
+
const blobs: Blob[] = await getBlobsPerL1Block(blobFields);
|
|
687
831
|
await this.blobClient.sendBlobsToFilestore(blobs);
|
|
688
832
|
this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
|
|
689
833
|
...proposalInfo,
|
|
@@ -721,6 +865,52 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
721
865
|
]);
|
|
722
866
|
}
|
|
723
867
|
|
|
868
|
+
/**
|
|
869
|
+
* Handle detection of a duplicate proposal (equivocation).
|
|
870
|
+
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
871
|
+
*/
|
|
872
|
+
private handleDuplicateProposal(info: DuplicateProposalInfo): void {
|
|
873
|
+
const { slot, proposer, type } = info;
|
|
874
|
+
|
|
875
|
+
this.log.warn(`Triggering slash event for duplicate ${type} proposal from ${proposer.toString()} at slot ${slot}`, {
|
|
876
|
+
proposer: proposer.toString(),
|
|
877
|
+
slot,
|
|
878
|
+
type,
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
// Emit slash event
|
|
882
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
883
|
+
{
|
|
884
|
+
validator: proposer,
|
|
885
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
886
|
+
offenseType: OffenseType.DUPLICATE_PROPOSAL,
|
|
887
|
+
epochOrSlot: BigInt(slot),
|
|
888
|
+
},
|
|
889
|
+
]);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Handle detection of a duplicate attestation (equivocation).
|
|
894
|
+
* Emits a slash event when an attester signs attestations for different proposals at the same slot.
|
|
895
|
+
*/
|
|
896
|
+
private handleDuplicateAttestation(info: DuplicateAttestationInfo): void {
|
|
897
|
+
const { slot, attester } = info;
|
|
898
|
+
|
|
899
|
+
this.log.warn(`Triggering slash event for duplicate attestation from ${attester.toString()} at slot ${slot}`, {
|
|
900
|
+
attester: attester.toString(),
|
|
901
|
+
slot,
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
905
|
+
{
|
|
906
|
+
validator: attester,
|
|
907
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
908
|
+
offenseType: OffenseType.DUPLICATE_ATTESTATION,
|
|
909
|
+
epochOrSlot: BigInt(slot),
|
|
910
|
+
},
|
|
911
|
+
]);
|
|
912
|
+
}
|
|
913
|
+
|
|
724
914
|
async createBlockProposal(
|
|
725
915
|
blockHeader: BlockHeader,
|
|
726
916
|
indexWithinCheckpoint: IndexWithinCheckpoint,
|
|
@@ -730,11 +920,19 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
730
920
|
proposerAddress: EthAddress | undefined,
|
|
731
921
|
options: BlockProposalOptions = {},
|
|
732
922
|
): Promise<BlockProposal> {
|
|
733
|
-
//
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
923
|
+
// Validate that we're not creating a proposal for an older or equal position
|
|
924
|
+
if (this.lastProposedBlock) {
|
|
925
|
+
const lastSlot = this.lastProposedBlock.slotNumber;
|
|
926
|
+
const lastIndex = this.lastProposedBlock.indexWithinCheckpoint;
|
|
927
|
+
const newSlot = blockHeader.globalVariables.slotNumber;
|
|
928
|
+
|
|
929
|
+
if (newSlot < lastSlot || (newSlot === lastSlot && indexWithinCheckpoint <= lastIndex)) {
|
|
930
|
+
throw new Error(
|
|
931
|
+
`Cannot create block proposal for slot ${newSlot} index ${indexWithinCheckpoint}: ` +
|
|
932
|
+
`already proposed block for slot ${lastSlot} index ${lastIndex}`,
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
738
936
|
|
|
739
937
|
this.log.info(
|
|
740
938
|
`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`,
|
|
@@ -751,25 +949,42 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
751
949
|
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
|
|
752
950
|
},
|
|
753
951
|
);
|
|
754
|
-
this.
|
|
952
|
+
this.lastProposedBlock = newProposal;
|
|
755
953
|
return newProposal;
|
|
756
954
|
}
|
|
757
955
|
|
|
758
956
|
async createCheckpointProposal(
|
|
759
957
|
checkpointHeader: CheckpointHeader,
|
|
760
958
|
archive: Fr,
|
|
959
|
+
feeAssetPriceModifier: bigint,
|
|
761
960
|
lastBlockInfo: CreateCheckpointProposalLastBlockData | undefined,
|
|
762
961
|
proposerAddress: EthAddress | undefined,
|
|
763
962
|
options: CheckpointProposalOptions = {},
|
|
764
963
|
): Promise<CheckpointProposal> {
|
|
964
|
+
// Validate that we're not creating a proposal for an older or equal slot
|
|
965
|
+
if (this.lastProposedCheckpoint) {
|
|
966
|
+
const lastSlot = this.lastProposedCheckpoint.slotNumber;
|
|
967
|
+
const newSlot = checkpointHeader.slotNumber;
|
|
968
|
+
|
|
969
|
+
if (newSlot <= lastSlot) {
|
|
970
|
+
throw new Error(
|
|
971
|
+
`Cannot create checkpoint proposal for slot ${newSlot}: ` +
|
|
972
|
+
`already proposed checkpoint for slot ${lastSlot}`,
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
765
977
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
766
|
-
|
|
978
|
+
const newProposal = await this.validationService.createCheckpointProposal(
|
|
767
979
|
checkpointHeader,
|
|
768
980
|
archive,
|
|
981
|
+
feeAssetPriceModifier,
|
|
769
982
|
lastBlockInfo,
|
|
770
983
|
proposerAddress,
|
|
771
984
|
options,
|
|
772
985
|
);
|
|
986
|
+
this.lastProposedCheckpoint = newProposal;
|
|
987
|
+
return newProposal;
|
|
773
988
|
}
|
|
774
989
|
|
|
775
990
|
async broadcastBlockProposal(proposal: BlockProposal): Promise<void> {
|
|
@@ -791,6 +1006,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
791
1006
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
|
|
792
1007
|
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
793
1008
|
|
|
1009
|
+
if (!attestations) {
|
|
1010
|
+
return [];
|
|
1011
|
+
}
|
|
1012
|
+
|
|
794
1013
|
// We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
|
|
795
1014
|
// other nodes can see that our validators did attest to this block proposal, and do not slash us
|
|
796
1015
|
// due to inactivity for missed attestations.
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
export * from './nullifier_cache.js';
|
|
2
|
-
export * from './tx_validator_factory.js';
|
|
3
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy90eF92YWxpZGF0b3IvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxzQkFBc0IsQ0FBQztBQUNyQyxjQUFjLDJCQUEyQixDQUFDIn0=
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tx_validator/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,2BAA2B,CAAC"}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { NullifierSource } from '@aztec/p2p';
|
|
2
|
-
import type { MerkleTreeReadOperations } from '@aztec/stdlib/interfaces/server';
|
|
3
|
-
/**
|
|
4
|
-
* Implements a nullifier source by checking a DB and an in-memory collection.
|
|
5
|
-
* Intended for validating transactions as they are added to a block.
|
|
6
|
-
*/
|
|
7
|
-
export declare class NullifierCache implements NullifierSource {
|
|
8
|
-
private db;
|
|
9
|
-
nullifiers: Set<string>;
|
|
10
|
-
constructor(db: MerkleTreeReadOperations);
|
|
11
|
-
nullifiersExist(nullifiers: Buffer[]): Promise<boolean[]>;
|
|
12
|
-
addNullifiers(nullifiers: Buffer[]): void;
|
|
13
|
-
}
|
|
14
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibnVsbGlmaWVyX2NhY2hlLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvdHhfdmFsaWRhdG9yL251bGxpZmllcl9jYWNoZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssRUFBRSxlQUFlLEVBQUUsTUFBTSxZQUFZLENBQUM7QUFDbEQsT0FBTyxLQUFLLEVBQUUsd0JBQXdCLEVBQUUsTUFBTSxpQ0FBaUMsQ0FBQztBQUdoRjs7O0dBR0c7QUFDSCxxQkFBYSxjQUFlLFlBQVcsZUFBZTtJQUd4QyxPQUFPLENBQUMsRUFBRTtJQUZ0QixVQUFVLEVBQUUsR0FBRyxDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBRXhCLFlBQW9CLEVBQUUsRUFBRSx3QkFBd0IsRUFFL0M7SUFFWSxlQUFlLENBQUMsVUFBVSxFQUFFLE1BQU0sRUFBRSxHQUFHLE9BQU8sQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQU9yRTtJQUVNLGFBQWEsQ0FBQyxVQUFVLEVBQUUsTUFBTSxFQUFFLFFBSXhDO0NBQ0YifQ==
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"nullifier_cache.d.ts","sourceRoot":"","sources":["../../src/tx_validator/nullifier_cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAGhF;;;GAGG;AACH,qBAAa,cAAe,YAAW,eAAe;IAGxC,OAAO,CAAC,EAAE;IAFtB,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAExB,YAAoB,EAAE,EAAE,wBAAwB,EAE/C;IAEY,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAOrE;IAEM,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,QAIxC;CACF"}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { MerkleTreeId } from '@aztec/stdlib/trees';
|
|
2
|
-
/**
|
|
3
|
-
* Implements a nullifier source by checking a DB and an in-memory collection.
|
|
4
|
-
* Intended for validating transactions as they are added to a block.
|
|
5
|
-
*/ export class NullifierCache {
|
|
6
|
-
db;
|
|
7
|
-
nullifiers;
|
|
8
|
-
constructor(db){
|
|
9
|
-
this.db = db;
|
|
10
|
-
this.nullifiers = new Set();
|
|
11
|
-
}
|
|
12
|
-
async nullifiersExist(nullifiers) {
|
|
13
|
-
const cacheResults = nullifiers.map((n)=>this.nullifiers.has(n.toString()));
|
|
14
|
-
const toCheckDb = nullifiers.filter((_n, index)=>!cacheResults[index]);
|
|
15
|
-
const dbHits = await this.db.findLeafIndices(MerkleTreeId.NULLIFIER_TREE, toCheckDb);
|
|
16
|
-
let dbIndex = 0;
|
|
17
|
-
return nullifiers.map((_n, index)=>cacheResults[index] || dbHits[dbIndex++] !== undefined);
|
|
18
|
-
}
|
|
19
|
-
addNullifiers(nullifiers) {
|
|
20
|
-
for (const nullifier of nullifiers){
|
|
21
|
-
this.nullifiers.add(nullifier.toString());
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|