@aztec/validator-client 0.0.1-commit.993d240 → 0.0.1-commit.9a89641
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 +2 -0
- package/dest/checkpoint_builder.d.ts +17 -7
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +26 -9
- package/dest/config.d.ts +9 -3
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +9 -1
- package/dest/factory.d.ts +1 -1
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +10 -3
- package/dest/key_store/web3signer_key_store.d.ts +10 -2
- package/dest/key_store/web3signer_key_store.d.ts.map +1 -1
- package/dest/key_store/web3signer_key_store.js +32 -41
- package/dest/metrics.d.ts +1 -1
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +3 -1
- package/dest/proposal_handler.d.ts +39 -8
- package/dest/proposal_handler.d.ts.map +1 -1
- package/dest/proposal_handler.js +290 -98
- package/dest/validator.d.ts +11 -5
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +54 -43
- package/package.json +19 -19
- package/src/checkpoint_builder.ts +28 -10
- package/src/config.ts +16 -3
- package/src/factory.ts +9 -1
- package/src/key_store/web3signer_key_store.ts +43 -59
- package/src/metrics.ts +2 -0
- package/src/proposal_handler.ts +309 -107
- package/src/validator.ts +63 -46
package/src/validator.ts
CHANGED
|
@@ -4,14 +4,14 @@ import type { EpochCache } from '@aztec/epoch-cache';
|
|
|
4
4
|
import { CheckpointNumber, EpochNumber, IndexWithinCheckpoint, SlotNumber } from '@aztec/foundation/branded-types';
|
|
5
5
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
6
6
|
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
7
|
-
import
|
|
7
|
+
import { Signature } from '@aztec/foundation/eth-signature';
|
|
8
8
|
import { FifoSet } from '@aztec/foundation/fifo-set';
|
|
9
9
|
import { type LogData, type Logger, createLogger } from '@aztec/foundation/log';
|
|
10
10
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
11
11
|
import { sleep } from '@aztec/foundation/sleep';
|
|
12
12
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
13
13
|
import type { KeystoreManager } from '@aztec/node-keystore';
|
|
14
|
-
import type { DuplicateAttestationInfo, DuplicateProposalInfo, P2P, PeerId } from '@aztec/p2p';
|
|
14
|
+
import type { DuplicateAttestationInfo, DuplicateProposalInfo, OversizedProposalInfo, P2P, PeerId } from '@aztec/p2p';
|
|
15
15
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
16
16
|
import {
|
|
17
17
|
OffenseType,
|
|
@@ -35,13 +35,14 @@ import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
|
35
35
|
import {
|
|
36
36
|
type BlockProposal,
|
|
37
37
|
type BlockProposalOptions,
|
|
38
|
-
|
|
38
|
+
CheckpointAttestation,
|
|
39
39
|
CheckpointProposal,
|
|
40
40
|
type CheckpointProposalCore,
|
|
41
41
|
type CheckpointProposalOptions,
|
|
42
42
|
type CoordinationSignatureContext,
|
|
43
43
|
} from '@aztec/stdlib/p2p';
|
|
44
44
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
45
|
+
import { ConsensusTimetable } from '@aztec/stdlib/timetable';
|
|
45
46
|
import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
|
|
46
47
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
47
48
|
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
@@ -57,6 +58,7 @@ import { EventEmitter } from 'events';
|
|
|
57
58
|
import type { TypedDataDefinition } from 'viem';
|
|
58
59
|
|
|
59
60
|
import type { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
|
|
61
|
+
import { DEFAULT_MAX_GOSSIP_CLOCK_DISPARITY_MS } from './config.js';
|
|
60
62
|
import { ValidationService } from './duties/validation_service.js';
|
|
61
63
|
import { HAKeyStore } from './key_store/ha_key_store.js';
|
|
62
64
|
import type { ExtendedValidatorKeyStore } from './key_store/interface.js';
|
|
@@ -64,48 +66,18 @@ import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
|
|
|
64
66
|
import { ValidatorMetrics } from './metrics.js';
|
|
65
67
|
import {
|
|
66
68
|
type BlockProposalValidationFailureReason,
|
|
67
|
-
type CheckpointProposalValidationFailureReason,
|
|
68
69
|
type CheckpointProposalValidationFailureResult,
|
|
69
70
|
ProposalHandler,
|
|
71
|
+
SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT,
|
|
72
|
+
SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT,
|
|
70
73
|
} from './proposal_handler.js';
|
|
71
74
|
|
|
72
75
|
// We maintain a set of proposers who have proposed invalid blocks.
|
|
73
76
|
// Just cap the set to avoid unbounded growth.
|
|
74
77
|
const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
|
|
75
|
-
const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
|
|
76
78
|
const MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS = 1000;
|
|
77
79
|
const MAX_TRACKED_BAD_ATTESTATIONS = 10_000;
|
|
78
80
|
|
|
79
|
-
// What errors from the block proposal handler result in slashing
|
|
80
|
-
const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT: BlockProposalValidationFailureReason[] = [
|
|
81
|
-
'state_mismatch',
|
|
82
|
-
'failed_txs',
|
|
83
|
-
'global_variables_mismatch',
|
|
84
|
-
'invalid_proposal',
|
|
85
|
-
'parent_block_wrong_slot',
|
|
86
|
-
'in_hash_mismatch',
|
|
87
|
-
];
|
|
88
|
-
|
|
89
|
-
const SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT: Record<CheckpointProposalValidationFailureReason, boolean> = {
|
|
90
|
-
// enabled
|
|
91
|
-
['invalid_fee_asset_price_modifier']: true,
|
|
92
|
-
['checkpoint_header_mismatch']: true,
|
|
93
|
-
// These late mismatches should normally be caught by earlier checks, but if reached after validating the local
|
|
94
|
-
// checkpoint inputs, the proposer-signed payload disagrees with deterministic recomputation.
|
|
95
|
-
['archive_mismatch']: true,
|
|
96
|
-
['out_hash_mismatch']: true,
|
|
97
|
-
['no_blocks_for_slot']: true,
|
|
98
|
-
['too_many_blocks_in_checkpoint']: true,
|
|
99
|
-
['checkpoint_validation_failed']: true,
|
|
100
|
-
['last_block_archive_mismatch']: true,
|
|
101
|
-
|
|
102
|
-
// disabled
|
|
103
|
-
['invalid_signature']: false,
|
|
104
|
-
['last_block_not_found']: false,
|
|
105
|
-
['block_fetch_error']: false,
|
|
106
|
-
['checkpoint_already_published']: false,
|
|
107
|
-
};
|
|
108
|
-
|
|
109
81
|
/**
|
|
110
82
|
* Validator Client
|
|
111
83
|
*/
|
|
@@ -129,10 +101,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
129
101
|
private lastAttestedEpochByAttester: Map<string, EpochNumber> = new Map();
|
|
130
102
|
|
|
131
103
|
private proposersOfInvalidBlocks = FifoSet.withLimit<string>(MAX_PROPOSERS_OF_INVALID_BLOCKS);
|
|
132
|
-
private slotsWithInvalidProposals = FifoSet.withLimit<SlotNumber>(MAX_TRACKED_INVALID_PROPOSAL_SLOTS);
|
|
133
104
|
private invalidCheckpointProposalOffenseKeys = FifoSet.withLimit<string>(MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS);
|
|
105
|
+
private oversizedProposalOffenseKeys = FifoSet.withLimit<string>(MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS);
|
|
134
106
|
private badAttestationOffenseKeys = FifoSet.withLimit<string>(MAX_TRACKED_BAD_ATTESTATIONS);
|
|
135
|
-
private slotsWithProposalEquivocation = FifoSet.withLimit<SlotNumber>(MAX_TRACKED_INVALID_PROPOSAL_SLOTS);
|
|
136
107
|
|
|
137
108
|
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
|
|
138
109
|
private lastAttestedProposal?: CheckpointProposalCore;
|
|
@@ -246,7 +217,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
246
217
|
slashingProtectionDb?: SlashingProtectionDatabase,
|
|
247
218
|
) {
|
|
248
219
|
const metrics = new ValidatorMetrics(telemetry);
|
|
249
|
-
const
|
|
220
|
+
const consensusTimetable = new ConsensusTimetable({
|
|
221
|
+
l1Constants: epochCache.getL1Constants(),
|
|
222
|
+
blockDuration: config.blockDurationMs / 1000,
|
|
223
|
+
});
|
|
224
|
+
const blockProposalValidator = new BlockProposalValidator(epochCache, consensusTimetable, {
|
|
250
225
|
txsPermitted: !config.disableTransactions,
|
|
251
226
|
maxTxsPerBlock: config.validateMaxTxsPerBlock,
|
|
252
227
|
maxBlocksPerCheckpoint: config.maxBlocksPerCheckpoint,
|
|
@@ -255,6 +230,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
255
230
|
chainId: config.l1ChainId,
|
|
256
231
|
rollupAddress: config.rollupAddress,
|
|
257
232
|
},
|
|
233
|
+
clockDisparityMs: config.maxGossipClockDisparityMs ?? DEFAULT_MAX_GOSSIP_CLOCK_DISPARITY_MS,
|
|
258
234
|
});
|
|
259
235
|
const proposalHandler = new ProposalHandler(
|
|
260
236
|
checkpointsBuilder,
|
|
@@ -264,6 +240,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
264
240
|
txProvider,
|
|
265
241
|
blockProposalValidator,
|
|
266
242
|
epochCache,
|
|
243
|
+
consensusTimetable,
|
|
267
244
|
config,
|
|
268
245
|
blobClient,
|
|
269
246
|
reexecutionTracker,
|
|
@@ -355,11 +332,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
355
332
|
}
|
|
356
333
|
|
|
357
334
|
public hasProposalEquivocation(slotNumber: SlotNumber): boolean {
|
|
358
|
-
return this.
|
|
335
|
+
return this.proposalHandler.hasProposalEquivocation(slotNumber);
|
|
359
336
|
}
|
|
360
337
|
|
|
361
338
|
public hasInvalidProposals(slotNumber: SlotNumber): boolean {
|
|
362
|
-
return this.
|
|
339
|
+
return this.proposalHandler.hasInvalidProposals(slotNumber);
|
|
363
340
|
}
|
|
364
341
|
|
|
365
342
|
public updateConfig(config: Partial<ValidatorClientFullConfig>) {
|
|
@@ -428,6 +405,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
428
405
|
this.handleDuplicateProposal(info);
|
|
429
406
|
});
|
|
430
407
|
|
|
408
|
+
// Oversized proposal handler - triggers slashing for proposals beyond the per-checkpoint block limit
|
|
409
|
+
this.p2pClient.registerOversizedProposalCallback((info: OversizedProposalInfo) => {
|
|
410
|
+
this.handleOversizedProposal(info);
|
|
411
|
+
});
|
|
412
|
+
|
|
431
413
|
// Duplicate attestation handler - triggers slashing for attestation equivocation
|
|
432
414
|
this.p2pClient.registerDuplicateAttestationCallback((info: DuplicateAttestationInfo) => {
|
|
433
415
|
this.handleDuplicateAttestation(info);
|
|
@@ -498,6 +480,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
498
480
|
'failed_txs',
|
|
499
481
|
'in_hash_mismatch',
|
|
500
482
|
'parent_block_wrong_slot',
|
|
483
|
+
'duplicate_txs',
|
|
484
|
+
'invalid_embedded_txs',
|
|
501
485
|
];
|
|
502
486
|
|
|
503
487
|
if (badProposalReasons.includes(reason as BlockProposalValidationFailureReason)) {
|
|
@@ -564,7 +548,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
564
548
|
|
|
565
549
|
// Ignore proposals from ourselves (may happen in HA setups)
|
|
566
550
|
if (proposer && this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
567
|
-
this.log.debug(`
|
|
551
|
+
this.log.debug(`Not attesting to block proposal from self for slot ${proposalSlotNumber}`, {
|
|
568
552
|
proposer: proposer.toString(),
|
|
569
553
|
proposalSlotNumber,
|
|
570
554
|
});
|
|
@@ -757,8 +741,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
757
741
|
return;
|
|
758
742
|
}
|
|
759
743
|
|
|
760
|
-
this
|
|
761
|
-
|
|
744
|
+
// The slot is already marked invalid by the all-nodes checkpoint handler that invokes this callback,
|
|
745
|
+
// so we only emit the proposer slash event here.
|
|
762
746
|
if (this.slashInvalidCheckpointProposal(proposal)) {
|
|
763
747
|
this.log.info(`Detected invalid checkpoint proposal offense`, {
|
|
764
748
|
...proposalInfo,
|
|
@@ -797,12 +781,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
797
781
|
}
|
|
798
782
|
|
|
799
783
|
private markInvalidProposalSlot(slotNumber: SlotNumber): void {
|
|
800
|
-
this.
|
|
784
|
+
this.proposalHandler.markInvalidProposalSlot(slotNumber);
|
|
801
785
|
}
|
|
802
786
|
|
|
803
787
|
private handleCheckpointAttestation(attestation: CheckpointAttestation): void {
|
|
804
788
|
const slotNumber = attestation.slotNumber;
|
|
805
|
-
if (
|
|
789
|
+
if (
|
|
790
|
+
!this.proposalHandler.hasInvalidProposals(slotNumber) ||
|
|
791
|
+
this.proposalHandler.hasProposalEquivocation(slotNumber)
|
|
792
|
+
) {
|
|
806
793
|
return;
|
|
807
794
|
}
|
|
808
795
|
|
|
@@ -841,13 +828,43 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
841
828
|
]);
|
|
842
829
|
}
|
|
843
830
|
|
|
831
|
+
/**
|
|
832
|
+
* Handle detection of an oversized block proposal: one whose index within its checkpoint lands at or
|
|
833
|
+
* beyond the consensus per-checkpoint block limit. A single signed proposal at an illegal index is
|
|
834
|
+
* self-contained evidence, so emit an invalid-block-proposal slash event for the proposer, deduped per
|
|
835
|
+
* (proposer, slot) since the p2p layer reports every oversized proposal it stores.
|
|
836
|
+
*/
|
|
837
|
+
private handleOversizedProposal(info: OversizedProposalInfo): void {
|
|
838
|
+
const { slot, proposer } = info;
|
|
839
|
+
const offenseType = OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL;
|
|
840
|
+
if (!this.oversizedProposalOffenseKeys.addIfAbsent(`${proposer.toString()}:${offenseType}:${slot}`)) {
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
this.log.info(`Detected oversized block proposal offense from ${proposer.toString()} at slot ${slot}`, {
|
|
845
|
+
proposer: proposer.toString(),
|
|
846
|
+
slot,
|
|
847
|
+
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
848
|
+
offenseType: getOffenseTypeName(offenseType),
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
852
|
+
{
|
|
853
|
+
validator: proposer,
|
|
854
|
+
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
855
|
+
offenseType,
|
|
856
|
+
epochOrSlot: BigInt(slot),
|
|
857
|
+
},
|
|
858
|
+
]);
|
|
859
|
+
}
|
|
860
|
+
|
|
844
861
|
/**
|
|
845
862
|
* Handle detection of a duplicate proposal (equivocation).
|
|
846
863
|
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
847
864
|
*/
|
|
848
865
|
private handleDuplicateProposal(info: DuplicateProposalInfo): void {
|
|
849
866
|
const { slot, proposer, type } = info;
|
|
850
|
-
this.
|
|
867
|
+
this.proposalHandler.markProposalEquivocation(slot);
|
|
851
868
|
|
|
852
869
|
this.log.info(`Detected duplicate ${type} proposal offense from ${proposer.toString()} at slot ${slot}`, {
|
|
853
870
|
proposer: proposer.toString(),
|