@aztec/validator-client 0.0.1-commit.f295ac2 → 0.0.1-commit.f8ca9b2f3
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 +22 -19
- package/dest/block_proposal_handler.d.ts +5 -7
- package/dest/block_proposal_handler.d.ts.map +1 -1
- package/dest/block_proposal_handler.js +17 -32
- package/dest/checkpoint_builder.d.ts +11 -12
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +38 -25
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +8 -6
- 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 +2 -2
- package/dest/metrics.d.ts +4 -3
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +34 -5
- package/dest/tx_validator/tx_validator_factory.d.ts +4 -3
- package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
- package/dest/tx_validator/tx_validator_factory.js +17 -16
- package/dest/validator.d.ts +29 -11
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +133 -38
- package/package.json +19 -17
- package/src/block_proposal_handler.ts +25 -46
- package/src/checkpoint_builder.ts +57 -27
- package/src/config.ts +8 -6
- package/src/key_store/ha_key_store.ts +2 -2
- package/src/metrics.ts +45 -6
- package/src/tx_validator/tx_validator_factory.ts +52 -31
- package/src/validator.ts +177 -51
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BlockNumber } from '@aztec/foundation/branded-types';
|
|
2
2
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
3
|
+
import type { LoggerBindings } from '@aztec/foundation/log';
|
|
3
4
|
import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree';
|
|
4
5
|
import {
|
|
5
6
|
AggregateTxValidator,
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
GasTxValidator,
|
|
11
12
|
MetadataTxValidator,
|
|
12
13
|
PhasesTxValidator,
|
|
14
|
+
SizeTxValidator,
|
|
13
15
|
TimestampTxValidator,
|
|
14
16
|
TxPermittedValidator,
|
|
15
17
|
TxProofValidator,
|
|
@@ -52,31 +54,41 @@ export function createValidatorForAcceptingTxs(
|
|
|
52
54
|
blockNumber: BlockNumber;
|
|
53
55
|
txsPermitted: boolean;
|
|
54
56
|
},
|
|
57
|
+
bindings?: LoggerBindings,
|
|
55
58
|
): TxValidator<Tx> {
|
|
56
59
|
const validators: TxValidator<Tx>[] = [
|
|
57
|
-
new TxPermittedValidator(txsPermitted),
|
|
58
|
-
new
|
|
59
|
-
new
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
new
|
|
70
|
-
|
|
71
|
-
|
|
60
|
+
new TxPermittedValidator(txsPermitted, bindings),
|
|
61
|
+
new SizeTxValidator(bindings),
|
|
62
|
+
new DataTxValidator(bindings),
|
|
63
|
+
new MetadataTxValidator(
|
|
64
|
+
{
|
|
65
|
+
l1ChainId: new Fr(l1ChainId),
|
|
66
|
+
rollupVersion: new Fr(rollupVersion),
|
|
67
|
+
protocolContractsHash,
|
|
68
|
+
vkTreeRoot: getVKTreeRoot(),
|
|
69
|
+
},
|
|
70
|
+
bindings,
|
|
71
|
+
),
|
|
72
|
+
new TimestampTxValidator(
|
|
73
|
+
{
|
|
74
|
+
timestamp,
|
|
75
|
+
blockNumber,
|
|
76
|
+
},
|
|
77
|
+
bindings,
|
|
78
|
+
),
|
|
79
|
+
new DoubleSpendTxValidator(new NullifierCache(db), bindings),
|
|
80
|
+
new PhasesTxValidator(contractDataSource, setupAllowList, timestamp, bindings),
|
|
81
|
+
new BlockHeaderTxValidator(new ArchiveCache(db), bindings),
|
|
72
82
|
];
|
|
73
83
|
|
|
74
84
|
if (!skipFeeEnforcement) {
|
|
75
|
-
validators.push(
|
|
85
|
+
validators.push(
|
|
86
|
+
new GasTxValidator(new DatabasePublicStateSource(db), ProtocolContractAddress.FeeJuice, gasFees, bindings),
|
|
87
|
+
);
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
if (verifier) {
|
|
79
|
-
validators.push(new TxProofValidator(verifier));
|
|
91
|
+
validators.push(new TxProofValidator(verifier, bindings));
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
return new AggregateTxValidator(...validators);
|
|
@@ -87,6 +99,7 @@ export function createValidatorForBlockBuilding(
|
|
|
87
99
|
contractDataSource: ContractDataSource,
|
|
88
100
|
globalVariables: GlobalVariables,
|
|
89
101
|
setupAllowList: AllowedElement[],
|
|
102
|
+
bindings?: LoggerBindings,
|
|
90
103
|
): PublicProcessorValidator {
|
|
91
104
|
const nullifierCache = new NullifierCache(db);
|
|
92
105
|
const archiveCache = new ArchiveCache(db);
|
|
@@ -100,6 +113,7 @@ export function createValidatorForBlockBuilding(
|
|
|
100
113
|
contractDataSource,
|
|
101
114
|
globalVariables,
|
|
102
115
|
setupAllowList,
|
|
116
|
+
bindings,
|
|
103
117
|
),
|
|
104
118
|
nullifierCache,
|
|
105
119
|
};
|
|
@@ -112,22 +126,29 @@ function preprocessValidator(
|
|
|
112
126
|
contractDataSource: ContractDataSource,
|
|
113
127
|
globalVariables: GlobalVariables,
|
|
114
128
|
setupAllowList: AllowedElement[],
|
|
129
|
+
bindings?: LoggerBindings,
|
|
115
130
|
): TxValidator<Tx> {
|
|
116
131
|
// We don't include the TxProofValidator nor the DataTxValidator here because they are already checked by the time we get to block building.
|
|
117
132
|
return new AggregateTxValidator(
|
|
118
|
-
new MetadataTxValidator(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
133
|
+
new MetadataTxValidator(
|
|
134
|
+
{
|
|
135
|
+
l1ChainId: globalVariables.chainId,
|
|
136
|
+
rollupVersion: globalVariables.version,
|
|
137
|
+
protocolContractsHash,
|
|
138
|
+
vkTreeRoot: getVKTreeRoot(),
|
|
139
|
+
},
|
|
140
|
+
bindings,
|
|
141
|
+
),
|
|
142
|
+
new TimestampTxValidator(
|
|
143
|
+
{
|
|
144
|
+
timestamp: globalVariables.timestamp,
|
|
145
|
+
blockNumber: globalVariables.blockNumber,
|
|
146
|
+
},
|
|
147
|
+
bindings,
|
|
148
|
+
),
|
|
149
|
+
new DoubleSpendTxValidator(nullifierCache, bindings),
|
|
150
|
+
new PhasesTxValidator(contractDataSource, setupAllowList, globalVariables.timestamp, bindings),
|
|
151
|
+
new GasTxValidator(publicStateSource, ProtocolContractAddress.FeeJuice, globalVariables.gasFees, bindings),
|
|
152
|
+
new BlockHeaderTxValidator(archiveCache, bindings),
|
|
132
153
|
);
|
|
133
154
|
}
|
package/src/validator.ts
CHANGED
|
@@ -18,27 +18,28 @@ import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
|
18
18
|
import { sleep } from '@aztec/foundation/sleep';
|
|
19
19
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
20
20
|
import type { KeystoreManager } from '@aztec/node-keystore';
|
|
21
|
-
import type { P2P, PeerId
|
|
21
|
+
import type { DuplicateAttestationInfo, DuplicateProposalInfo, P2P, PeerId } from '@aztec/p2p';
|
|
22
22
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
23
23
|
import { OffenseType, WANT_TO_SLASH_EVENT, type Watcher, type WatcherEmitter } from '@aztec/slasher';
|
|
24
24
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
25
|
-
import type { CommitteeAttestationsAndSigners,
|
|
25
|
+
import type { CommitteeAttestationsAndSigners, L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
26
26
|
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
27
27
|
import type {
|
|
28
28
|
CreateCheckpointProposalLastBlockData,
|
|
29
|
+
ITxProvider,
|
|
29
30
|
Validator,
|
|
30
31
|
ValidatorClientFullConfig,
|
|
31
32
|
WorldStateSynchronizer,
|
|
32
33
|
} from '@aztec/stdlib/interfaces/server';
|
|
33
|
-
import type
|
|
34
|
-
import
|
|
35
|
-
BlockProposal,
|
|
36
|
-
BlockProposalOptions,
|
|
37
|
-
CheckpointAttestation,
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
import { type L1ToL2MessageSource, accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
|
|
35
|
+
import {
|
|
36
|
+
type BlockProposal,
|
|
37
|
+
type BlockProposalOptions,
|
|
38
|
+
type CheckpointAttestation,
|
|
39
|
+
CheckpointProposal,
|
|
40
|
+
type CheckpointProposalCore,
|
|
41
|
+
type CheckpointProposalOptions,
|
|
40
42
|
} from '@aztec/stdlib/p2p';
|
|
41
|
-
import { CheckpointProposal } from '@aztec/stdlib/p2p';
|
|
42
43
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
43
44
|
import type { BlockHeader, CheckpointGlobalVariables, Tx } from '@aztec/stdlib/tx';
|
|
44
45
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
@@ -79,18 +80,19 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
79
80
|
// Whether it has already registered handlers on the p2p client
|
|
80
81
|
private hasRegisteredHandlers = false;
|
|
81
82
|
|
|
82
|
-
|
|
83
|
-
private
|
|
83
|
+
/** Tracks the last block proposal we created, to detect duplicate proposal attempts. */
|
|
84
|
+
private lastProposedBlock?: BlockProposal;
|
|
85
|
+
|
|
86
|
+
/** Tracks the last checkpoint proposal we created. */
|
|
87
|
+
private lastProposedCheckpoint?: CheckpointProposal;
|
|
84
88
|
|
|
85
89
|
private lastEpochForCommitteeUpdateLoop: EpochNumber | undefined;
|
|
86
90
|
private epochCacheUpdateLoop: RunningPromise;
|
|
87
91
|
|
|
88
92
|
private proposersOfInvalidBlocks: Set<string> = new Set();
|
|
89
93
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
|
|
93
|
-
private validatedBlockSlots: Set<SlotNumber> = new Set();
|
|
94
|
+
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
|
|
95
|
+
private lastAttestedProposal?: CheckpointProposalCore;
|
|
94
96
|
|
|
95
97
|
protected constructor(
|
|
96
98
|
private keyStore: ExtendedValidatorKeyStore,
|
|
@@ -184,7 +186,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
184
186
|
p2pClient: P2P,
|
|
185
187
|
blockSource: L2BlockSource & L2BlockSink,
|
|
186
188
|
l1ToL2MessageSource: L1ToL2MessageSource,
|
|
187
|
-
txProvider:
|
|
189
|
+
txProvider: ITxProvider,
|
|
188
190
|
keyStoreManager: KeystoreManager,
|
|
189
191
|
blobClient: BlobClientInterface,
|
|
190
192
|
dateProvider: DateProvider = new DateProvider(),
|
|
@@ -313,6 +315,16 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
313
315
|
): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
|
|
314
316
|
this.p2pClient.registerCheckpointProposalHandler(checkpointHandler);
|
|
315
317
|
|
|
318
|
+
// Duplicate proposal handler - triggers slashing for equivocation
|
|
319
|
+
this.p2pClient.registerDuplicateProposalCallback((info: DuplicateProposalInfo) => {
|
|
320
|
+
this.handleDuplicateProposal(info);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Duplicate attestation handler - triggers slashing for attestation equivocation
|
|
324
|
+
this.p2pClient.registerDuplicateAttestationCallback((info: DuplicateAttestationInfo) => {
|
|
325
|
+
this.handleDuplicateAttestation(info);
|
|
326
|
+
});
|
|
327
|
+
|
|
316
328
|
const myAddresses = this.getValidatorAddresses();
|
|
317
329
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
318
330
|
|
|
@@ -340,6 +352,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
340
352
|
return false;
|
|
341
353
|
}
|
|
342
354
|
|
|
355
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
356
|
+
if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
357
|
+
this.log.warn(`Ignoring block proposal from self for slot ${slotNumber}`, {
|
|
358
|
+
proposer: proposer.toString(),
|
|
359
|
+
slotNumber,
|
|
360
|
+
});
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
|
|
343
364
|
// Check if we're in the committee (for metrics purposes)
|
|
344
365
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
345
366
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -413,10 +434,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
413
434
|
return false;
|
|
414
435
|
}
|
|
415
436
|
|
|
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
437
|
return true;
|
|
421
438
|
}
|
|
422
439
|
|
|
@@ -445,6 +462,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
445
462
|
return undefined;
|
|
446
463
|
}
|
|
447
464
|
|
|
465
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
466
|
+
if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
467
|
+
this.log.warn(`Ignoring block proposal from self for slot ${slotNumber}`, {
|
|
468
|
+
proposer: proposer.toString(),
|
|
469
|
+
slotNumber,
|
|
470
|
+
});
|
|
471
|
+
return undefined;
|
|
472
|
+
}
|
|
473
|
+
|
|
448
474
|
// Check that I have any address in current committee before attesting
|
|
449
475
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
450
476
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -461,17 +487,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
461
487
|
fishermanMode: this.config.fishermanMode || false,
|
|
462
488
|
});
|
|
463
489
|
|
|
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
490
|
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
this.log.verbose(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
|
|
491
|
+
if (this.config.skipCheckpointProposalValidation) {
|
|
492
|
+
this.log.warn(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
|
|
475
493
|
} else {
|
|
476
494
|
const validationResult = await this.validateCheckpointProposal(proposal, proposalInfo);
|
|
477
495
|
if (!validationResult.isValid) {
|
|
@@ -526,15 +544,45 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
526
544
|
return undefined;
|
|
527
545
|
}
|
|
528
546
|
|
|
529
|
-
return this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
547
|
+
return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Checks if we should attest to a slot based on equivocation prevention rules.
|
|
552
|
+
* @returns true if we should attest, false if we should skip
|
|
553
|
+
*/
|
|
554
|
+
private shouldAttestToSlot(slotNumber: SlotNumber): boolean {
|
|
555
|
+
// If attestToEquivocatedProposals is true, always allow
|
|
556
|
+
if (this.config.attestToEquivocatedProposals) {
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Check if incoming slot is strictly greater than last attested
|
|
561
|
+
if (this.lastAttestedProposal && slotNumber <= this.lastAttestedProposal.slotNumber) {
|
|
562
|
+
this.log.warn(
|
|
563
|
+
`Refusing to process a proposal for slot ${slotNumber} given we already attested to a proposal for slot ${this.lastAttestedProposal.slotNumber}`,
|
|
564
|
+
);
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return true;
|
|
530
569
|
}
|
|
531
570
|
|
|
532
571
|
private async createCheckpointAttestationsFromProposal(
|
|
533
572
|
proposal: CheckpointProposalCore,
|
|
534
573
|
attestors: EthAddress[] = [],
|
|
535
|
-
): Promise<CheckpointAttestation[]> {
|
|
574
|
+
): Promise<CheckpointAttestation[] | undefined> {
|
|
575
|
+
// Equivocation check: must happen right before signing to minimize the race window
|
|
576
|
+
if (!this.shouldAttestToSlot(proposal.slotNumber)) {
|
|
577
|
+
return undefined;
|
|
578
|
+
}
|
|
579
|
+
|
|
536
580
|
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
537
|
-
|
|
581
|
+
|
|
582
|
+
// Track the proposal we attested to (to prevent equivocation)
|
|
583
|
+
this.lastAttestedProposal = proposal;
|
|
584
|
+
|
|
585
|
+
await this.p2pClient.addOwnCheckpointAttestations(attestations);
|
|
538
586
|
return attestations;
|
|
539
587
|
}
|
|
540
588
|
|
|
@@ -547,7 +595,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
547
595
|
proposalInfo: LogData,
|
|
548
596
|
): Promise<{ isValid: true } | { isValid: false; reason: string }> {
|
|
549
597
|
const slot = proposal.slotNumber;
|
|
550
|
-
const timeoutSeconds = 10;
|
|
598
|
+
const timeoutSeconds = 10; // TODO(palla/mbps): This should map to the timetable settings
|
|
551
599
|
|
|
552
600
|
// Wait for last block to sync by archive
|
|
553
601
|
let lastBlockHeader: BlockHeader | undefined;
|
|
@@ -617,6 +665,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
617
665
|
previousCheckpointOutHashes,
|
|
618
666
|
fork,
|
|
619
667
|
blocks,
|
|
668
|
+
this.log.getBindings(),
|
|
620
669
|
);
|
|
621
670
|
|
|
622
671
|
// Complete the checkpoint to get computed values
|
|
@@ -642,13 +691,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
642
691
|
return { isValid: false, reason: 'archive_mismatch' };
|
|
643
692
|
}
|
|
644
693
|
|
|
645
|
-
// Check that the accumulated out hash matches the value in the proposal.
|
|
646
|
-
|
|
647
|
-
const
|
|
648
|
-
|
|
694
|
+
// Check that the accumulated epoch out hash matches the value in the proposal.
|
|
695
|
+
// The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
|
|
696
|
+
const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
|
|
697
|
+
const computedEpochOutHash = accumulateCheckpointOutHashes([...previousCheckpointOutHashes, checkpointOutHash]);
|
|
698
|
+
const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
|
|
699
|
+
if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
|
|
649
700
|
this.log.warn(`Epoch out hash mismatch`, {
|
|
650
|
-
|
|
651
|
-
|
|
701
|
+
proposalEpochOutHash: proposalEpochOutHash.toString(),
|
|
702
|
+
computedEpochOutHash: computedEpochOutHash.toString(),
|
|
703
|
+
checkpointOutHash: checkpointOutHash.toString(),
|
|
704
|
+
previousCheckpointOutHashes: previousCheckpointOutHashes.map(h => h.toString()),
|
|
652
705
|
...proposalInfo,
|
|
653
706
|
});
|
|
654
707
|
return { isValid: false, reason: 'out_hash_mismatch' };
|
|
@@ -664,7 +717,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
664
717
|
/**
|
|
665
718
|
* Extract checkpoint global variables from a block.
|
|
666
719
|
*/
|
|
667
|
-
private extractCheckpointConstants(block:
|
|
720
|
+
private extractCheckpointConstants(block: L2Block): CheckpointGlobalVariables {
|
|
668
721
|
const gv = block.header.globalVariables;
|
|
669
722
|
return {
|
|
670
723
|
chainId: gv.chainId,
|
|
@@ -732,6 +785,52 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
732
785
|
]);
|
|
733
786
|
}
|
|
734
787
|
|
|
788
|
+
/**
|
|
789
|
+
* Handle detection of a duplicate proposal (equivocation).
|
|
790
|
+
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
791
|
+
*/
|
|
792
|
+
private handleDuplicateProposal(info: DuplicateProposalInfo): void {
|
|
793
|
+
const { slot, proposer, type } = info;
|
|
794
|
+
|
|
795
|
+
this.log.warn(`Triggering slash event for duplicate ${type} proposal from ${proposer.toString()} at slot ${slot}`, {
|
|
796
|
+
proposer: proposer.toString(),
|
|
797
|
+
slot,
|
|
798
|
+
type,
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
// Emit slash event
|
|
802
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
803
|
+
{
|
|
804
|
+
validator: proposer,
|
|
805
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
806
|
+
offenseType: OffenseType.DUPLICATE_PROPOSAL,
|
|
807
|
+
epochOrSlot: BigInt(slot),
|
|
808
|
+
},
|
|
809
|
+
]);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Handle detection of a duplicate attestation (equivocation).
|
|
814
|
+
* Emits a slash event when an attester signs attestations for different proposals at the same slot.
|
|
815
|
+
*/
|
|
816
|
+
private handleDuplicateAttestation(info: DuplicateAttestationInfo): void {
|
|
817
|
+
const { slot, attester } = info;
|
|
818
|
+
|
|
819
|
+
this.log.warn(`Triggering slash event for duplicate attestation from ${attester.toString()} at slot ${slot}`, {
|
|
820
|
+
attester: attester.toString(),
|
|
821
|
+
slot,
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
825
|
+
{
|
|
826
|
+
validator: attester,
|
|
827
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
828
|
+
offenseType: OffenseType.DUPLICATE_ATTESTATION,
|
|
829
|
+
epochOrSlot: BigInt(slot),
|
|
830
|
+
},
|
|
831
|
+
]);
|
|
832
|
+
}
|
|
833
|
+
|
|
735
834
|
async createBlockProposal(
|
|
736
835
|
blockHeader: BlockHeader,
|
|
737
836
|
indexWithinCheckpoint: IndexWithinCheckpoint,
|
|
@@ -739,13 +838,21 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
739
838
|
archive: Fr,
|
|
740
839
|
txs: Tx[],
|
|
741
840
|
proposerAddress: EthAddress | undefined,
|
|
742
|
-
options: BlockProposalOptions,
|
|
841
|
+
options: BlockProposalOptions = {},
|
|
743
842
|
): Promise<BlockProposal> {
|
|
744
|
-
//
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
843
|
+
// Validate that we're not creating a proposal for an older or equal position
|
|
844
|
+
if (this.lastProposedBlock) {
|
|
845
|
+
const lastSlot = this.lastProposedBlock.slotNumber;
|
|
846
|
+
const lastIndex = this.lastProposedBlock.indexWithinCheckpoint;
|
|
847
|
+
const newSlot = blockHeader.globalVariables.slotNumber;
|
|
848
|
+
|
|
849
|
+
if (newSlot < lastSlot || (newSlot === lastSlot && indexWithinCheckpoint <= lastIndex)) {
|
|
850
|
+
throw new Error(
|
|
851
|
+
`Cannot create block proposal for slot ${newSlot} index ${indexWithinCheckpoint}: ` +
|
|
852
|
+
`already proposed block for slot ${lastSlot} index ${lastIndex}`,
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
749
856
|
|
|
750
857
|
this.log.info(
|
|
751
858
|
`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`,
|
|
@@ -762,7 +869,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
762
869
|
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
|
|
763
870
|
},
|
|
764
871
|
);
|
|
765
|
-
this.
|
|
872
|
+
this.lastProposedBlock = newProposal;
|
|
766
873
|
return newProposal;
|
|
767
874
|
}
|
|
768
875
|
|
|
@@ -771,16 +878,31 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
771
878
|
archive: Fr,
|
|
772
879
|
lastBlockInfo: CreateCheckpointProposalLastBlockData | undefined,
|
|
773
880
|
proposerAddress: EthAddress | undefined,
|
|
774
|
-
options: CheckpointProposalOptions,
|
|
881
|
+
options: CheckpointProposalOptions = {},
|
|
775
882
|
): Promise<CheckpointProposal> {
|
|
883
|
+
// Validate that we're not creating a proposal for an older or equal slot
|
|
884
|
+
if (this.lastProposedCheckpoint) {
|
|
885
|
+
const lastSlot = this.lastProposedCheckpoint.slotNumber;
|
|
886
|
+
const newSlot = checkpointHeader.slotNumber;
|
|
887
|
+
|
|
888
|
+
if (newSlot <= lastSlot) {
|
|
889
|
+
throw new Error(
|
|
890
|
+
`Cannot create checkpoint proposal for slot ${newSlot}: ` +
|
|
891
|
+
`already proposed checkpoint for slot ${lastSlot}`,
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
776
896
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
777
|
-
|
|
897
|
+
const newProposal = await this.validationService.createCheckpointProposal(
|
|
778
898
|
checkpointHeader,
|
|
779
899
|
archive,
|
|
780
900
|
lastBlockInfo,
|
|
781
901
|
proposerAddress,
|
|
782
902
|
options,
|
|
783
903
|
);
|
|
904
|
+
this.lastProposedCheckpoint = newProposal;
|
|
905
|
+
return newProposal;
|
|
784
906
|
}
|
|
785
907
|
|
|
786
908
|
async broadcastBlockProposal(proposal: BlockProposal): Promise<void> {
|
|
@@ -802,6 +924,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
802
924
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
|
|
803
925
|
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
804
926
|
|
|
927
|
+
if (!attestations) {
|
|
928
|
+
return [];
|
|
929
|
+
}
|
|
930
|
+
|
|
805
931
|
// We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
|
|
806
932
|
// other nodes can see that our validators did attest to this block proposal, and do not slash us
|
|
807
933
|
// due to inactivity for missed attestations.
|