@aztec/validator-client 0.0.1-commit.0b941701 → 0.0.1-commit.10bd49492
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 +63 -18
- package/dest/block_proposal_handler.d.ts +7 -7
- package/dest/block_proposal_handler.d.ts.map +1 -1
- package/dest/block_proposal_handler.js +125 -64
- package/dest/checkpoint_builder.d.ts +21 -12
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +107 -39
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +28 -4
- 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 +5 -11
- package/dest/factory.d.ts +1 -1
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +2 -1
- 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.d.ts +1 -1
- package/dest/key_store/ha_key_store.d.ts.map +1 -1
- package/dest/key_store/ha_key_store.js +3 -3
- package/dest/metrics.d.ts +12 -3
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +46 -5
- package/dest/validator.d.ts +40 -14
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +213 -57
- package/package.json +19 -17
- package/src/block_proposal_handler.ts +155 -85
- package/src/checkpoint_builder.ts +144 -38
- package/src/config.ts +28 -4
- package/src/duties/validation_service.ts +11 -10
- package/src/factory.ts +1 -0
- package/src/index.ts +0 -1
- package/src/key_store/ha_key_store.ts +3 -3
- package/src/metrics.ts +63 -6
- package/src/validator.ts +263 -68
- 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 -18
- 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 -135
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,33 +19,36 @@ 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
|
|
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,
|
|
31
|
+
ITxProvider,
|
|
29
32
|
Validator,
|
|
30
33
|
ValidatorClientFullConfig,
|
|
31
34
|
WorldStateSynchronizer,
|
|
32
35
|
} from '@aztec/stdlib/interfaces/server';
|
|
33
|
-
import type
|
|
34
|
-
import
|
|
35
|
-
BlockProposal,
|
|
36
|
-
BlockProposalOptions,
|
|
37
|
-
CheckpointAttestation,
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
import { type L1ToL2MessageSource, accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
|
|
37
|
+
import {
|
|
38
|
+
type BlockProposal,
|
|
39
|
+
type BlockProposalOptions,
|
|
40
|
+
type CheckpointAttestation,
|
|
41
|
+
CheckpointProposal,
|
|
42
|
+
type CheckpointProposalCore,
|
|
43
|
+
type CheckpointProposalOptions,
|
|
40
44
|
} from '@aztec/stdlib/p2p';
|
|
41
|
-
import { CheckpointProposal } from '@aztec/stdlib/p2p';
|
|
42
45
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
43
46
|
import type { BlockHeader, CheckpointGlobalVariables, Tx } from '@aztec/stdlib/tx';
|
|
44
47
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
45
48
|
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
46
|
-
import { createHASigner } from '@aztec/validator-ha-signer/factory';
|
|
49
|
+
import { createHASigner, createLocalSignerWithProtection } from '@aztec/validator-ha-signer/factory';
|
|
47
50
|
import { DutyType, type SigningContext } from '@aztec/validator-ha-signer/types';
|
|
51
|
+
import type { ValidatorHASigner } from '@aztec/validator-ha-signer/validator-ha-signer';
|
|
48
52
|
|
|
49
53
|
import { EventEmitter } from 'events';
|
|
50
54
|
import type { TypedDataDefinition } from 'viem';
|
|
@@ -75,22 +79,24 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
75
79
|
private validationService: ValidationService;
|
|
76
80
|
private metrics: ValidatorMetrics;
|
|
77
81
|
private log: Logger;
|
|
78
|
-
|
|
79
82
|
// Whether it has already registered handlers on the p2p client
|
|
80
83
|
private hasRegisteredHandlers = false;
|
|
81
84
|
|
|
82
|
-
|
|
83
|
-
private
|
|
85
|
+
/** Tracks the last block proposal we created, to detect duplicate proposal attempts. */
|
|
86
|
+
private lastProposedBlock?: BlockProposal;
|
|
87
|
+
|
|
88
|
+
/** Tracks the last checkpoint proposal we created. */
|
|
89
|
+
private lastProposedCheckpoint?: CheckpointProposal;
|
|
84
90
|
|
|
85
91
|
private lastEpochForCommitteeUpdateLoop: EpochNumber | undefined;
|
|
86
92
|
private epochCacheUpdateLoop: RunningPromise;
|
|
93
|
+
/** Tracks the last epoch in which each attester successfully submitted at least one attestation. */
|
|
94
|
+
private lastAttestedEpochByAttester: Map<string, EpochNumber> = new Map();
|
|
87
95
|
|
|
88
96
|
private proposersOfInvalidBlocks: Set<string> = new Set();
|
|
89
97
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
|
|
93
|
-
private validatedBlockSlots: Set<SlotNumber> = new Set();
|
|
98
|
+
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
|
|
99
|
+
private lastAttestedProposal?: CheckpointProposalCore;
|
|
94
100
|
|
|
95
101
|
protected constructor(
|
|
96
102
|
private keyStore: ExtendedValidatorKeyStore,
|
|
@@ -103,6 +109,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
103
109
|
private l1ToL2MessageSource: L1ToL2MessageSource,
|
|
104
110
|
private config: ValidatorClientFullConfig,
|
|
105
111
|
private blobClient: BlobClientInterface,
|
|
112
|
+
private slashingProtectionSigner: ValidatorHASigner,
|
|
106
113
|
private dateProvider: DateProvider = new DateProvider(),
|
|
107
114
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
108
115
|
log = createLogger('validator'),
|
|
@@ -156,6 +163,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
156
163
|
this.log.trace(`No committee found for slot`);
|
|
157
164
|
return;
|
|
158
165
|
}
|
|
166
|
+
this.metrics.setCurrentEpoch(epoch);
|
|
159
167
|
if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
|
|
160
168
|
const me = this.getValidatorAddresses();
|
|
161
169
|
const committeeSet = new Set(committee.map(v => v.toString()));
|
|
@@ -184,7 +192,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
184
192
|
p2pClient: P2P,
|
|
185
193
|
blockSource: L2BlockSource & L2BlockSink,
|
|
186
194
|
l1ToL2MessageSource: L1ToL2MessageSource,
|
|
187
|
-
txProvider:
|
|
195
|
+
txProvider: ITxProvider,
|
|
188
196
|
keyStoreManager: KeystoreManager,
|
|
189
197
|
blobClient: BlobClientInterface,
|
|
190
198
|
dateProvider: DateProvider = new DateProvider(),
|
|
@@ -193,6 +201,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
193
201
|
const metrics = new ValidatorMetrics(telemetry);
|
|
194
202
|
const blockProposalValidator = new BlockProposalValidator(epochCache, {
|
|
195
203
|
txsPermitted: !config.disableTransactions,
|
|
204
|
+
maxTxsPerBlock: config.validateMaxTxsPerBlock,
|
|
196
205
|
});
|
|
197
206
|
const blockProposalHandler = new BlockProposalHandler(
|
|
198
207
|
checkpointsBuilder,
|
|
@@ -208,16 +217,28 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
208
217
|
telemetry,
|
|
209
218
|
);
|
|
210
219
|
|
|
211
|
-
|
|
220
|
+
const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
221
|
+
let slashingProtectionSigner: ValidatorHASigner;
|
|
212
222
|
if (config.haSigningEnabled) {
|
|
223
|
+
// Multi-node HA mode: use PostgreSQL-backed distributed locking.
|
|
213
224
|
// If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
|
|
214
225
|
const haConfig = {
|
|
215
226
|
...config,
|
|
216
227
|
maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000,
|
|
217
228
|
};
|
|
218
|
-
|
|
219
|
-
|
|
229
|
+
({ signer: slashingProtectionSigner } = await createHASigner(haConfig, {
|
|
230
|
+
telemetryClient: telemetry,
|
|
231
|
+
dateProvider,
|
|
232
|
+
}));
|
|
233
|
+
} else {
|
|
234
|
+
// Single-node mode: use LMDB-backed local signing protection.
|
|
235
|
+
// This prevents double-signing if the node crashes and restarts mid-proposal.
|
|
236
|
+
({ signer: slashingProtectionSigner } = await createLocalSignerWithProtection(config, {
|
|
237
|
+
telemetryClient: telemetry,
|
|
238
|
+
dateProvider,
|
|
239
|
+
}));
|
|
220
240
|
}
|
|
241
|
+
const validatorKeyStore: ExtendedValidatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, slashingProtectionSigner);
|
|
221
242
|
|
|
222
243
|
const validator = new ValidatorClient(
|
|
223
244
|
validatorKeyStore,
|
|
@@ -230,6 +251,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
230
251
|
l1ToL2MessageSource,
|
|
231
252
|
config,
|
|
232
253
|
blobClient,
|
|
254
|
+
slashingProtectionSigner,
|
|
233
255
|
dateProvider,
|
|
234
256
|
telemetry,
|
|
235
257
|
);
|
|
@@ -267,6 +289,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
267
289
|
this.config = { ...this.config, ...config };
|
|
268
290
|
}
|
|
269
291
|
|
|
292
|
+
public reloadKeystore(newManager: KeystoreManager): void {
|
|
293
|
+
const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
|
|
294
|
+
this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
|
|
295
|
+
this.validationService = new ValidationService(this.keyStore, this.log.createChild('validation-service'));
|
|
296
|
+
}
|
|
297
|
+
|
|
270
298
|
public async start() {
|
|
271
299
|
if (this.epochCacheUpdateLoop.isRunning()) {
|
|
272
300
|
this.log.warn(`Validator client already started`);
|
|
@@ -313,6 +341,16 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
313
341
|
): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
|
|
314
342
|
this.p2pClient.registerCheckpointProposalHandler(checkpointHandler);
|
|
315
343
|
|
|
344
|
+
// Duplicate proposal handler - triggers slashing for equivocation
|
|
345
|
+
this.p2pClient.registerDuplicateProposalCallback((info: DuplicateProposalInfo) => {
|
|
346
|
+
this.handleDuplicateProposal(info);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Duplicate attestation handler - triggers slashing for attestation equivocation
|
|
350
|
+
this.p2pClient.registerDuplicateAttestationCallback((info: DuplicateAttestationInfo) => {
|
|
351
|
+
this.handleDuplicateAttestation(info);
|
|
352
|
+
});
|
|
353
|
+
|
|
316
354
|
const myAddresses = this.getValidatorAddresses();
|
|
317
355
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
318
356
|
|
|
@@ -340,6 +378,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
340
378
|
return false;
|
|
341
379
|
}
|
|
342
380
|
|
|
381
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
382
|
+
if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
383
|
+
this.log.debug(`Ignoring block proposal from self for slot ${slotNumber}`, {
|
|
384
|
+
proposer: proposer.toString(),
|
|
385
|
+
slotNumber,
|
|
386
|
+
});
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
|
|
343
390
|
// Check if we're in the committee (for metrics purposes)
|
|
344
391
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
345
392
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -369,9 +416,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
369
416
|
);
|
|
370
417
|
|
|
371
418
|
if (!validationResult.isValid) {
|
|
372
|
-
this.log.warn(`Block proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
373
|
-
|
|
374
419
|
const reason = validationResult.reason || 'unknown';
|
|
420
|
+
|
|
421
|
+
this.log.warn(`Block proposal validation failed: ${reason}`, proposalInfo);
|
|
422
|
+
|
|
375
423
|
// Classify failure reason: bad proposal vs node issue
|
|
376
424
|
const badProposalReasons: BlockProposalValidationFailureReason[] = [
|
|
377
425
|
'invalid_proposal',
|
|
@@ -413,10 +461,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
413
461
|
return false;
|
|
414
462
|
}
|
|
415
463
|
|
|
416
|
-
// TODO(palla/mbps): Remove this once checkpoint validation is stable.
|
|
417
|
-
// Track that we successfully validated a block for this slot, so we can attest to checkpoint proposals for it.
|
|
418
|
-
this.validatedBlockSlots.add(slotNumber);
|
|
419
|
-
|
|
420
464
|
return true;
|
|
421
465
|
}
|
|
422
466
|
|
|
@@ -445,6 +489,23 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
445
489
|
return undefined;
|
|
446
490
|
}
|
|
447
491
|
|
|
492
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
493
|
+
if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
494
|
+
this.log.debug(`Ignoring block proposal from self for slot ${slotNumber}`, {
|
|
495
|
+
proposer: proposer.toString(),
|
|
496
|
+
slotNumber,
|
|
497
|
+
});
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Validate fee asset price modifier is within allowed range
|
|
502
|
+
if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
|
|
503
|
+
this.log.warn(
|
|
504
|
+
`Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${slotNumber}`,
|
|
505
|
+
);
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
|
|
448
509
|
// Check that I have any address in current committee before attesting
|
|
449
510
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
450
511
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -453,25 +514,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
453
514
|
slotNumber,
|
|
454
515
|
archive: proposal.archive.toString(),
|
|
455
516
|
proposer: proposer.toString(),
|
|
456
|
-
txCount: proposal.txHashes.length,
|
|
457
517
|
};
|
|
458
518
|
this.log.info(`Received checkpoint proposal for slot ${slotNumber}`, {
|
|
459
519
|
...proposalInfo,
|
|
460
|
-
txHashes: proposal.txHashes.map(t => t.toString()),
|
|
461
520
|
fishermanMode: this.config.fishermanMode || false,
|
|
462
521
|
});
|
|
463
522
|
|
|
464
|
-
// TODO(palla/mbps): Remove this once checkpoint validation is stable.
|
|
465
|
-
// Check that we have successfully validated a block for this slot before attesting to the checkpoint.
|
|
466
|
-
if (!this.validatedBlockSlots.has(slotNumber)) {
|
|
467
|
-
this.log.warn(`No validated block found for slot ${slotNumber}, refusing to attest to checkpoint`, proposalInfo);
|
|
468
|
-
return undefined;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
523
|
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
this.log.verbose(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
|
|
524
|
+
if (this.config.skipCheckpointProposalValidation) {
|
|
525
|
+
this.log.warn(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
|
|
475
526
|
} else {
|
|
476
527
|
const validationResult = await this.validateCheckpointProposal(proposal, proposalInfo);
|
|
477
528
|
if (!validationResult.isValid) {
|
|
@@ -501,6 +552,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
501
552
|
|
|
502
553
|
this.metrics.incSuccessfulAttestations(inCommittee.length);
|
|
503
554
|
|
|
555
|
+
// Track epoch participation per attester: count each (attester, epoch) pair at most once
|
|
556
|
+
const proposalEpoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
|
|
557
|
+
for (const attester of inCommittee) {
|
|
558
|
+
const key = attester.toString();
|
|
559
|
+
const lastEpoch = this.lastAttestedEpochByAttester.get(key);
|
|
560
|
+
if (lastEpoch === undefined || proposalEpoch > lastEpoch) {
|
|
561
|
+
this.lastAttestedEpochByAttester.set(key, proposalEpoch);
|
|
562
|
+
this.metrics.incAttestedEpochCount(attester);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
504
566
|
// Determine which validators should attest
|
|
505
567
|
let attestors: EthAddress[];
|
|
506
568
|
if (partOfCommittee) {
|
|
@@ -526,15 +588,45 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
526
588
|
return undefined;
|
|
527
589
|
}
|
|
528
590
|
|
|
529
|
-
return this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
591
|
+
return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Checks if we should attest to a slot based on equivocation prevention rules.
|
|
596
|
+
* @returns true if we should attest, false if we should skip
|
|
597
|
+
*/
|
|
598
|
+
private shouldAttestToSlot(slotNumber: SlotNumber): boolean {
|
|
599
|
+
// If attestToEquivocatedProposals is true, always allow
|
|
600
|
+
if (this.config.attestToEquivocatedProposals) {
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Check if incoming slot is strictly greater than last attested
|
|
605
|
+
if (this.lastAttestedProposal && slotNumber <= this.lastAttestedProposal.slotNumber) {
|
|
606
|
+
this.log.warn(
|
|
607
|
+
`Refusing to process a proposal for slot ${slotNumber} given we already attested to a proposal for slot ${this.lastAttestedProposal.slotNumber}`,
|
|
608
|
+
);
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return true;
|
|
530
613
|
}
|
|
531
614
|
|
|
532
615
|
private async createCheckpointAttestationsFromProposal(
|
|
533
616
|
proposal: CheckpointProposalCore,
|
|
534
617
|
attestors: EthAddress[] = [],
|
|
535
|
-
): Promise<CheckpointAttestation[]> {
|
|
618
|
+
): Promise<CheckpointAttestation[] | undefined> {
|
|
619
|
+
// Equivocation check: must happen right before signing to minimize the race window
|
|
620
|
+
if (!this.shouldAttestToSlot(proposal.slotNumber)) {
|
|
621
|
+
return undefined;
|
|
622
|
+
}
|
|
623
|
+
|
|
536
624
|
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
537
|
-
|
|
625
|
+
|
|
626
|
+
// Track the proposal we attested to (to prevent equivocation)
|
|
627
|
+
this.lastAttestedProposal = proposal;
|
|
628
|
+
|
|
629
|
+
await this.p2pClient.addOwnCheckpointAttestations(attestations);
|
|
538
630
|
return attestations;
|
|
539
631
|
}
|
|
540
632
|
|
|
@@ -547,7 +639,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
547
639
|
proposalInfo: LogData,
|
|
548
640
|
): Promise<{ isValid: true } | { isValid: false; reason: string }> {
|
|
549
641
|
const slot = proposal.slotNumber;
|
|
550
|
-
|
|
642
|
+
|
|
643
|
+
// Timeout block syncing at the start of the next slot
|
|
644
|
+
const config = this.checkpointsBuilder.getConfig();
|
|
645
|
+
const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
|
|
646
|
+
const timeoutSeconds = Math.max(1, nextSlotTimestampSeconds - Math.floor(this.dateProvider.now() / 1000));
|
|
551
647
|
|
|
552
648
|
// Wait for last block to sync by archive
|
|
553
649
|
let lastBlockHeader: BlockHeader | undefined;
|
|
@@ -582,6 +678,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
582
678
|
return { isValid: false, reason: 'no_blocks_for_slot' };
|
|
583
679
|
}
|
|
584
680
|
|
|
681
|
+
// Ensure the last block for this slot matches the archive in the checkpoint proposal
|
|
682
|
+
if (!blocks.at(-1)?.archive.root.equals(proposal.archive)) {
|
|
683
|
+
this.log.warn(`Last block archive mismatch for checkpoint proposal`, proposalInfo);
|
|
684
|
+
return { isValid: false, reason: 'last_block_archive_mismatch' };
|
|
685
|
+
}
|
|
686
|
+
|
|
585
687
|
this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
|
|
586
688
|
...proposalInfo,
|
|
587
689
|
blockNumbers: blocks.map(b => b.number),
|
|
@@ -595,14 +697,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
595
697
|
// Get L1-to-L2 messages for this checkpoint
|
|
596
698
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
597
699
|
|
|
598
|
-
//
|
|
599
|
-
// TODO: There can be a more efficient way to get the previous checkpoint out hashes without having to fetch the
|
|
600
|
-
// actual checkpoints and the blocks/txs in them.
|
|
700
|
+
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
601
701
|
const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
|
|
602
|
-
const
|
|
603
|
-
.filter(
|
|
604
|
-
.
|
|
605
|
-
const previousCheckpointOutHashes = previousCheckpoints.map(c => c.getCheckpointOutHash());
|
|
702
|
+
const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch))
|
|
703
|
+
.filter(c => c.checkpointNumber < checkpointNumber)
|
|
704
|
+
.map(c => c.checkpointOutHash);
|
|
606
705
|
|
|
607
706
|
// Fork world state at the block before the first block
|
|
608
707
|
const parentBlockNumber = BlockNumber(firstBlock.number - 1);
|
|
@@ -613,10 +712,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
613
712
|
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(
|
|
614
713
|
checkpointNumber,
|
|
615
714
|
constants,
|
|
715
|
+
proposal.feeAssetPriceModifier,
|
|
616
716
|
l1ToL2Messages,
|
|
617
717
|
previousCheckpointOutHashes,
|
|
618
718
|
fork,
|
|
619
719
|
blocks,
|
|
720
|
+
this.log.getBindings(),
|
|
620
721
|
);
|
|
621
722
|
|
|
622
723
|
// Complete the checkpoint to get computed values
|
|
@@ -642,18 +743,36 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
642
743
|
return { isValid: false, reason: 'archive_mismatch' };
|
|
643
744
|
}
|
|
644
745
|
|
|
645
|
-
// Check that the accumulated out hash matches the value in the proposal.
|
|
646
|
-
|
|
647
|
-
const
|
|
648
|
-
|
|
746
|
+
// Check that the accumulated epoch out hash matches the value in the proposal.
|
|
747
|
+
// The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
|
|
748
|
+
const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
|
|
749
|
+
const computedEpochOutHash = accumulateCheckpointOutHashes([...previousCheckpointOutHashes, checkpointOutHash]);
|
|
750
|
+
const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
|
|
751
|
+
if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
|
|
649
752
|
this.log.warn(`Epoch out hash mismatch`, {
|
|
650
|
-
|
|
651
|
-
|
|
753
|
+
proposalEpochOutHash: proposalEpochOutHash.toString(),
|
|
754
|
+
computedEpochOutHash: computedEpochOutHash.toString(),
|
|
755
|
+
checkpointOutHash: checkpointOutHash.toString(),
|
|
756
|
+
previousCheckpointOutHashes: previousCheckpointOutHashes.map(h => h.toString()),
|
|
652
757
|
...proposalInfo,
|
|
653
758
|
});
|
|
654
759
|
return { isValid: false, reason: 'out_hash_mismatch' };
|
|
655
760
|
}
|
|
656
761
|
|
|
762
|
+
// Final round of validations on the checkpoint, just in case.
|
|
763
|
+
try {
|
|
764
|
+
validateCheckpoint(computedCheckpoint, {
|
|
765
|
+
rollupManaLimit: this.checkpointsBuilder.getConfig().rollupManaLimit,
|
|
766
|
+
maxDABlockGas: this.config.validateMaxDABlockGas,
|
|
767
|
+
maxL2BlockGas: this.config.validateMaxL2BlockGas,
|
|
768
|
+
maxTxsPerBlock: this.config.validateMaxTxsPerBlock,
|
|
769
|
+
maxTxsPerCheckpoint: this.config.validateMaxTxsPerCheckpoint,
|
|
770
|
+
});
|
|
771
|
+
} catch (err) {
|
|
772
|
+
this.log.warn(`Checkpoint validation failed: ${err}`, proposalInfo);
|
|
773
|
+
return { isValid: false, reason: 'checkpoint_validation_failed' };
|
|
774
|
+
}
|
|
775
|
+
|
|
657
776
|
this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
|
|
658
777
|
return { isValid: true };
|
|
659
778
|
} finally {
|
|
@@ -670,6 +789,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
670
789
|
chainId: gv.chainId,
|
|
671
790
|
version: gv.version,
|
|
672
791
|
slotNumber: gv.slotNumber,
|
|
792
|
+
timestamp: gv.timestamp,
|
|
673
793
|
coinbase: gv.coinbase,
|
|
674
794
|
feeRecipient: gv.feeRecipient,
|
|
675
795
|
gasFees: gv.gasFees,
|
|
@@ -679,7 +799,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
679
799
|
/**
|
|
680
800
|
* Uploads blobs for a checkpoint to the filestore (fire and forget).
|
|
681
801
|
*/
|
|
682
|
-
|
|
802
|
+
protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
|
|
683
803
|
try {
|
|
684
804
|
const lastBlockHeader = await this.blockSource.getBlockHeaderByArchive(proposal.archive);
|
|
685
805
|
if (!lastBlockHeader) {
|
|
@@ -694,7 +814,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
694
814
|
}
|
|
695
815
|
|
|
696
816
|
const blobFields = blocks.flatMap(b => b.toBlobFields());
|
|
697
|
-
const blobs: Blob[] = getBlobsPerL1Block(blobFields);
|
|
817
|
+
const blobs: Blob[] = await getBlobsPerL1Block(blobFields);
|
|
698
818
|
await this.blobClient.sendBlobsToFilestore(blobs);
|
|
699
819
|
this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
|
|
700
820
|
...proposalInfo,
|
|
@@ -732,6 +852,52 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
732
852
|
]);
|
|
733
853
|
}
|
|
734
854
|
|
|
855
|
+
/**
|
|
856
|
+
* Handle detection of a duplicate proposal (equivocation).
|
|
857
|
+
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
858
|
+
*/
|
|
859
|
+
private handleDuplicateProposal(info: DuplicateProposalInfo): void {
|
|
860
|
+
const { slot, proposer, type } = info;
|
|
861
|
+
|
|
862
|
+
this.log.warn(`Triggering slash event for duplicate ${type} proposal from ${proposer.toString()} at slot ${slot}`, {
|
|
863
|
+
proposer: proposer.toString(),
|
|
864
|
+
slot,
|
|
865
|
+
type,
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
// Emit slash event
|
|
869
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
870
|
+
{
|
|
871
|
+
validator: proposer,
|
|
872
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
873
|
+
offenseType: OffenseType.DUPLICATE_PROPOSAL,
|
|
874
|
+
epochOrSlot: BigInt(slot),
|
|
875
|
+
},
|
|
876
|
+
]);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Handle detection of a duplicate attestation (equivocation).
|
|
881
|
+
* Emits a slash event when an attester signs attestations for different proposals at the same slot.
|
|
882
|
+
*/
|
|
883
|
+
private handleDuplicateAttestation(info: DuplicateAttestationInfo): void {
|
|
884
|
+
const { slot, attester } = info;
|
|
885
|
+
|
|
886
|
+
this.log.warn(`Triggering slash event for duplicate attestation from ${attester.toString()} at slot ${slot}`, {
|
|
887
|
+
attester: attester.toString(),
|
|
888
|
+
slot,
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
892
|
+
{
|
|
893
|
+
validator: attester,
|
|
894
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
895
|
+
offenseType: OffenseType.DUPLICATE_ATTESTATION,
|
|
896
|
+
epochOrSlot: BigInt(slot),
|
|
897
|
+
},
|
|
898
|
+
]);
|
|
899
|
+
}
|
|
900
|
+
|
|
735
901
|
async createBlockProposal(
|
|
736
902
|
blockHeader: BlockHeader,
|
|
737
903
|
indexWithinCheckpoint: IndexWithinCheckpoint,
|
|
@@ -739,13 +905,21 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
739
905
|
archive: Fr,
|
|
740
906
|
txs: Tx[],
|
|
741
907
|
proposerAddress: EthAddress | undefined,
|
|
742
|
-
options: BlockProposalOptions,
|
|
908
|
+
options: BlockProposalOptions = {},
|
|
743
909
|
): Promise<BlockProposal> {
|
|
744
|
-
//
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
910
|
+
// Validate that we're not creating a proposal for an older or equal position
|
|
911
|
+
if (this.lastProposedBlock) {
|
|
912
|
+
const lastSlot = this.lastProposedBlock.slotNumber;
|
|
913
|
+
const lastIndex = this.lastProposedBlock.indexWithinCheckpoint;
|
|
914
|
+
const newSlot = blockHeader.globalVariables.slotNumber;
|
|
915
|
+
|
|
916
|
+
if (newSlot < lastSlot || (newSlot === lastSlot && indexWithinCheckpoint <= lastIndex)) {
|
|
917
|
+
throw new Error(
|
|
918
|
+
`Cannot create block proposal for slot ${newSlot} index ${indexWithinCheckpoint}: ` +
|
|
919
|
+
`already proposed block for slot ${lastSlot} index ${lastIndex}`,
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
749
923
|
|
|
750
924
|
this.log.info(
|
|
751
925
|
`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`,
|
|
@@ -762,25 +936,42 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
762
936
|
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
|
|
763
937
|
},
|
|
764
938
|
);
|
|
765
|
-
this.
|
|
939
|
+
this.lastProposedBlock = newProposal;
|
|
766
940
|
return newProposal;
|
|
767
941
|
}
|
|
768
942
|
|
|
769
943
|
async createCheckpointProposal(
|
|
770
944
|
checkpointHeader: CheckpointHeader,
|
|
771
945
|
archive: Fr,
|
|
946
|
+
feeAssetPriceModifier: bigint,
|
|
772
947
|
lastBlockInfo: CreateCheckpointProposalLastBlockData | undefined,
|
|
773
948
|
proposerAddress: EthAddress | undefined,
|
|
774
|
-
options: CheckpointProposalOptions,
|
|
949
|
+
options: CheckpointProposalOptions = {},
|
|
775
950
|
): Promise<CheckpointProposal> {
|
|
951
|
+
// Validate that we're not creating a proposal for an older or equal slot
|
|
952
|
+
if (this.lastProposedCheckpoint) {
|
|
953
|
+
const lastSlot = this.lastProposedCheckpoint.slotNumber;
|
|
954
|
+
const newSlot = checkpointHeader.slotNumber;
|
|
955
|
+
|
|
956
|
+
if (newSlot <= lastSlot) {
|
|
957
|
+
throw new Error(
|
|
958
|
+
`Cannot create checkpoint proposal for slot ${newSlot}: ` +
|
|
959
|
+
`already proposed checkpoint for slot ${lastSlot}`,
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
776
964
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
777
|
-
|
|
965
|
+
const newProposal = await this.validationService.createCheckpointProposal(
|
|
778
966
|
checkpointHeader,
|
|
779
967
|
archive,
|
|
968
|
+
feeAssetPriceModifier,
|
|
780
969
|
lastBlockInfo,
|
|
781
970
|
proposerAddress,
|
|
782
971
|
options,
|
|
783
972
|
);
|
|
973
|
+
this.lastProposedCheckpoint = newProposal;
|
|
974
|
+
return newProposal;
|
|
784
975
|
}
|
|
785
976
|
|
|
786
977
|
async broadcastBlockProposal(proposal: BlockProposal): Promise<void> {
|
|
@@ -802,6 +993,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
802
993
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
|
|
803
994
|
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
804
995
|
|
|
996
|
+
if (!attestations) {
|
|
997
|
+
return [];
|
|
998
|
+
}
|
|
999
|
+
|
|
805
1000
|
// We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
|
|
806
1001
|
// other nodes can see that our validators did attest to this block proposal, and do not slash us
|
|
807
1002
|
// 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
|
-
}
|