@aztec/validator-client 6.0.0-nightly.20260604 → 6.0.0-nightly.20260721
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 +6 -4
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +16 -8
- 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/proposal_handler.d.ts +37 -6
- package/dest/proposal_handler.d.ts.map +1 -1
- package/dest/proposal_handler.js +182 -47
- package/dest/validator.d.ts +11 -5
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +51 -42
- package/package.json +19 -19
- package/src/checkpoint_builder.ts +16 -7
- package/src/config.ts +16 -3
- package/src/factory.ts +9 -1
- package/src/key_store/web3signer_key_store.ts +43 -59
- package/src/proposal_handler.ts +203 -50
- package/src/validator.ts +61 -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);
|
|
@@ -564,7 +546,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
564
546
|
|
|
565
547
|
// Ignore proposals from ourselves (may happen in HA setups)
|
|
566
548
|
if (proposer && this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
567
|
-
this.log.debug(`
|
|
549
|
+
this.log.debug(`Not attesting to block proposal from self for slot ${proposalSlotNumber}`, {
|
|
568
550
|
proposer: proposer.toString(),
|
|
569
551
|
proposalSlotNumber,
|
|
570
552
|
});
|
|
@@ -757,8 +739,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
757
739
|
return;
|
|
758
740
|
}
|
|
759
741
|
|
|
760
|
-
this
|
|
761
|
-
|
|
742
|
+
// The slot is already marked invalid by the all-nodes checkpoint handler that invokes this callback,
|
|
743
|
+
// so we only emit the proposer slash event here.
|
|
762
744
|
if (this.slashInvalidCheckpointProposal(proposal)) {
|
|
763
745
|
this.log.info(`Detected invalid checkpoint proposal offense`, {
|
|
764
746
|
...proposalInfo,
|
|
@@ -797,12 +779,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
797
779
|
}
|
|
798
780
|
|
|
799
781
|
private markInvalidProposalSlot(slotNumber: SlotNumber): void {
|
|
800
|
-
this.
|
|
782
|
+
this.proposalHandler.markInvalidProposalSlot(slotNumber);
|
|
801
783
|
}
|
|
802
784
|
|
|
803
785
|
private handleCheckpointAttestation(attestation: CheckpointAttestation): void {
|
|
804
786
|
const slotNumber = attestation.slotNumber;
|
|
805
|
-
if (
|
|
787
|
+
if (
|
|
788
|
+
!this.proposalHandler.hasInvalidProposals(slotNumber) ||
|
|
789
|
+
this.proposalHandler.hasProposalEquivocation(slotNumber)
|
|
790
|
+
) {
|
|
806
791
|
return;
|
|
807
792
|
}
|
|
808
793
|
|
|
@@ -841,13 +826,43 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
841
826
|
]);
|
|
842
827
|
}
|
|
843
828
|
|
|
829
|
+
/**
|
|
830
|
+
* Handle detection of an oversized block proposal: one whose index within its checkpoint lands at or
|
|
831
|
+
* beyond the consensus per-checkpoint block limit. A single signed proposal at an illegal index is
|
|
832
|
+
* self-contained evidence, so emit an invalid-block-proposal slash event for the proposer, deduped per
|
|
833
|
+
* (proposer, slot) since the p2p layer reports every oversized proposal it stores.
|
|
834
|
+
*/
|
|
835
|
+
private handleOversizedProposal(info: OversizedProposalInfo): void {
|
|
836
|
+
const { slot, proposer } = info;
|
|
837
|
+
const offenseType = OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL;
|
|
838
|
+
if (!this.oversizedProposalOffenseKeys.addIfAbsent(`${proposer.toString()}:${offenseType}:${slot}`)) {
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
this.log.info(`Detected oversized block proposal offense from ${proposer.toString()} at slot ${slot}`, {
|
|
843
|
+
proposer: proposer.toString(),
|
|
844
|
+
slot,
|
|
845
|
+
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
846
|
+
offenseType: getOffenseTypeName(offenseType),
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
850
|
+
{
|
|
851
|
+
validator: proposer,
|
|
852
|
+
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
853
|
+
offenseType,
|
|
854
|
+
epochOrSlot: BigInt(slot),
|
|
855
|
+
},
|
|
856
|
+
]);
|
|
857
|
+
}
|
|
858
|
+
|
|
844
859
|
/**
|
|
845
860
|
* Handle detection of a duplicate proposal (equivocation).
|
|
846
861
|
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
847
862
|
*/
|
|
848
863
|
private handleDuplicateProposal(info: DuplicateProposalInfo): void {
|
|
849
864
|
const { slot, proposer, type } = info;
|
|
850
|
-
this.
|
|
865
|
+
this.proposalHandler.markProposalEquivocation(slot);
|
|
851
866
|
|
|
852
867
|
this.log.info(`Detected duplicate ${type} proposal offense from ${proposer.toString()} at slot ${slot}`, {
|
|
853
868
|
proposer: proposer.toString(),
|