Skip to content

Commit b5658bd

Browse files
committed
feat: slash for invalid checkpoint proposals
1 parent 816aef3 commit b5658bd

9 files changed

Lines changed: 369 additions & 18 deletions

File tree

yarn-project/slasher/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,9 @@ List of all slashable offenses in the system:
135135
**Time Unit**: Slot-based offense.
136136

137137
### BROADCASTED_INVALID_CHECKPOINT_PROPOSAL
138-
**Description**: A proposer broadcast a checkpoint proposal that terminates before a higher-index block proposal signed by the same proposer in the same slot.
139-
**Detection**: BroadcastedInvalidCheckpointProposalWatcher scans retained P2P proposals and compares checkpoint archive roots to signed block proposals from the same slot and signer.
140-
**Target**: Proposer who broadcast the truncated checkpoint proposal.
138+
**Description**: A proposer broadcast an invalid checkpoint proposal, either one that terminates before a higher-index block proposal signed by the same proposer in the same slot, one whose signed header does not match deterministic validator recomputation, or one with a malformed fee asset price modifier.
139+
**Detection**: BroadcastedInvalidCheckpointProposalWatcher scans retained P2P proposal evidence and compares checkpoint archive roots to signed block proposals from the same slot and signer. ValidatorClient also validates checkpoint proposals during the all-nodes callback and emits this offense when checkpoint header recomputation fails or the signed fee asset price modifier is malformed.
140+
**Target**: Proposer who broadcast the invalid checkpoint proposal.
141141
**Time Unit**: Slot-based offense.
142142

143143
## Configuration

yarn-project/stdlib/src/interfaces/validator.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export type ValidatorClientFullConfig = ValidatorClientConfig &
8484
Pick<
8585
SlasherConfig,
8686
| 'slashBroadcastedInvalidBlockPenalty'
87+
| 'slashBroadcastedInvalidCheckpointProposalPenalty'
8788
| 'slashDuplicateProposalPenalty'
8889
| 'slashDuplicateAttestationPenalty'
8990
| 'slashAttestInvalidCheckpointProposalPenalty'
@@ -120,6 +121,7 @@ export const ValidatorClientFullConfigSchema = zodFor<Omit<ValidatorClientFullCo
120121
broadcastInvalidBlockProposal: z.boolean().optional(),
121122
maxBlocksPerCheckpoint: z.number().positive().optional(),
122123
slashBroadcastedInvalidBlockPenalty: schemas.BigInt,
124+
slashBroadcastedInvalidCheckpointProposalPenalty: schemas.BigInt,
123125
slashDuplicateProposalPenalty: schemas.BigInt,
124126
slashDuplicateAttestationPenalty: schemas.BigInt,
125127
slashAttestInvalidCheckpointProposalPenalty: schemas.BigInt,

yarn-project/stdlib/src/slashing/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export enum OffenseType {
2626
DUPLICATE_ATTESTATION = 9,
2727
/** A committee member attested to a checkpoint proposal in a slot with an invalid block proposal */
2828
ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL = 10,
29-
/** A proposer broadcast a checkpoint proposal truncated before a higher-index block proposal in the same slot */
29+
/** A proposer broadcast an invalid checkpoint proposal, detected by retained evidence or deterministic recomputation */
3030
BROADCASTED_INVALID_CHECKPOINT_PROPOSAL = 11,
3131
}
3232

yarn-project/validator-client/README.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -156,15 +156,16 @@ Time | Proposer | Validator
156156

157157
## Configuration
158158

159-
| Flag | Purpose |
160-
| ------------------------------------- | -------------------------------------------------------------------------------------- |
161-
| `fishermanMode` | Validate proposals but don't broadcast attestations (monitoring only) |
162-
| `alwaysReexecuteBlockProposals` | Force re-execution even when not in committee |
163-
| `slashBroadcastedInvalidBlockPenalty` | Penalty amount for invalid proposals (0 = disabled) |
164-
| `slashDuplicateProposalPenalty` | Penalty amount for duplicate proposals (0 = disabled) |
165-
| `slashDuplicateAttestationPenalty` | Penalty amount for duplicate attestations (0 = disabled) |
166-
| `attestationPollingIntervalMs` | How often to poll for attestations when collecting |
167-
| `disabledValidators` | Validator addresses to exclude from duties |
159+
| Flag | Purpose |
160+
| -------------------------------------------------- | -------------------------------------------------------------------- |
161+
| `fishermanMode` | Validate proposals but don't broadcast attestations (monitoring only) |
162+
| `alwaysReexecuteBlockProposals` | Force re-execution even when not in committee |
163+
| `slashBroadcastedInvalidBlockPenalty` | Penalty amount for invalid proposals (0 = disabled) |
164+
| `slashBroadcastedInvalidCheckpointProposalPenalty` | Penalty amount for invalid checkpoint proposals (0 = disabled) |
165+
| `slashDuplicateProposalPenalty` | Penalty amount for duplicate proposals (0 = disabled) |
166+
| `slashDuplicateAttestationPenalty` | Penalty amount for duplicate attestations (0 = disabled) |
167+
| `attestationPollingIntervalMs` | How often to poll for attestations when collecting |
168+
| `disabledValidators` | Validator addresses to exclude from duties |
168169

169170
### High Availability (HA) Keystore
170171

yarn-project/validator-client/src/proposal_handler.ts

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,38 @@ export type BlockProposalValidationFailureResult = {
8181

8282
export type BlockProposalValidationResult = BlockProposalValidationSuccessResult | BlockProposalValidationFailureResult;
8383

84+
export type CheckpointProposalValidationFailureReason =
85+
| 'invalid_signature'
86+
| 'invalid_fee_asset_price_modifier'
87+
| 'last_block_not_found'
88+
| 'block_fetch_error'
89+
| 'no_blocks_for_slot'
90+
| 'last_block_archive_mismatch'
91+
| 'too_many_blocks_in_checkpoint'
92+
| 'checkpoint_header_mismatch'
93+
| 'archive_mismatch'
94+
| 'out_hash_mismatch'
95+
| 'checkpoint_validation_failed';
96+
97+
export type CheckpointProposalValidationSuccessResult = {
98+
isValid: true;
99+
checkpointNumber: CheckpointNumber;
100+
};
101+
102+
export type CheckpointProposalValidationFailureResult = {
103+
isValid: false;
104+
reason: CheckpointProposalValidationFailureReason;
105+
};
106+
84107
export type CheckpointProposalValidationResult =
85-
| { isValid: true; checkpointNumber: CheckpointNumber }
86-
| { isValid: false; reason: string };
108+
| CheckpointProposalValidationSuccessResult
109+
| CheckpointProposalValidationFailureResult;
110+
111+
export type CheckpointProposalValidationFailureCallback = (
112+
proposal: CheckpointProposalCore,
113+
result: CheckpointProposalValidationFailureResult,
114+
proposalInfo: LogData,
115+
) => void | Promise<void>;
87116

88117
type CheckpointComputationResult =
89118
| { checkpointNumber: CheckpointNumber; reason?: undefined }
@@ -107,6 +136,8 @@ export class ProposalHandler {
107136
/** Returns current validator addresses for own-proposal detection. Set via register(). */
108137
private getOwnValidatorAddresses?: () => string[];
109138

139+
private checkpointProposalValidationFailureCallback?: CheckpointProposalValidationFailureCallback;
140+
110141
constructor(
111142
private checkpointsBuilder: FullNodeCheckpointsBuilder,
112143
private worldState: WorldStateSynchronizer,
@@ -128,6 +159,14 @@ export class ProposalHandler {
128159
this.tracer = telemetry.getTracer('ProposalHandler');
129160
}
130161

162+
public updateConfig(config: Partial<ValidatorClientFullConfig>): void {
163+
this.config = { ...this.config, ...config };
164+
}
165+
166+
public setCheckpointProposalValidationFailureCallback(callback?: CheckpointProposalValidationFailureCallback): void {
167+
this.checkpointProposalValidationFailureCallback = callback;
168+
}
169+
131170
/**
132171
* Registers handlers for block and checkpoint proposals on the p2p client.
133172
* Block proposals are registered for non-validator nodes (validators register their own enhanced handler).
@@ -190,6 +229,19 @@ export class ProposalHandler {
190229
proposer: proposal.getSender()?.toString(),
191230
};
192231

232+
if (this.config.skipCheckpointProposalValidation) {
233+
this.log.warn(`Skipping checkpoint proposal validation for slot ${proposal.slotNumber}`, proposalInfo);
234+
return undefined;
235+
}
236+
237+
if (await this.epochCache.isEscapeHatchOpenAtSlot(proposal.slotNumber)) {
238+
this.log.warn(
239+
`Escape hatch open for slot ${proposal.slotNumber}, skipping checkpoint proposal validation`,
240+
proposalInfo,
241+
);
242+
return undefined;
243+
}
244+
193245
// For own proposals, skip validation — the proposer already built and validated the checkpoint
194246
const proposer = proposal.getSender();
195247
const ownAddresses = this.getOwnValidatorAddresses?.();
@@ -204,7 +256,9 @@ export class ProposalHandler {
204256
}
205257

206258
const result = await this.handleCheckpointProposal(proposal, proposalInfo);
207-
if (result.isValid && this.archiver && this.epochCache.isProposerPipeliningEnabled()) {
259+
if (!result.isValid) {
260+
await this.checkpointProposalValidationFailureCallback?.(proposal, result, proposalInfo);
261+
} else if (this.archiver && this.epochCache.isProposerPipeliningEnabled()) {
208262
const set = await this.setProposedCheckpointFromValidation(proposal);
209263
if (set) {
210264
this.metrics?.recordCheckpointProposalToPipelinedStateDuration(pipeliningTimer.ms());

yarn-project/validator-client/src/validator.ha.integration.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ describe('ValidatorClient HA Integration', () => {
136136
Pick<
137137
SlasherConfig,
138138
| 'slashBroadcastedInvalidBlockPenalty'
139+
| 'slashBroadcastedInvalidCheckpointProposalPenalty'
139140
| 'slashDuplicateProposalPenalty'
140141
| 'slashDuplicateAttestationPenalty'
141142
| 'slashAttestInvalidCheckpointProposalPenalty'
@@ -145,6 +146,7 @@ describe('ValidatorClient HA Integration', () => {
145146
disableValidator: false,
146147
disabledValidators: [],
147148
slashBroadcastedInvalidBlockPenalty: 1n,
149+
slashBroadcastedInvalidCheckpointProposalPenalty: 1n,
148150
rollupAddress,
149151
l1ChainId: TEST_COORDINATION_SIGNATURE_CONTEXT.chainId,
150152
slashDuplicateProposalPenalty: 1n,
@@ -193,6 +195,7 @@ describe('ValidatorClient HA Integration', () => {
193195
Pick<
194196
SlasherConfig,
195197
| 'slashBroadcastedInvalidBlockPenalty'
198+
| 'slashBroadcastedInvalidCheckpointProposalPenalty'
196199
| 'slashDuplicateProposalPenalty'
197200
| 'slashDuplicateAttestationPenalty'
198201
| 'slashAttestInvalidCheckpointProposalPenalty'

yarn-project/validator-client/src/validator.integration.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ describe('ValidatorClient Integration', () => {
177177
disableValidator: false,
178178
disabledValidators: [],
179179
slashBroadcastedInvalidBlockPenalty: 10n,
180+
slashBroadcastedInvalidCheckpointProposalPenalty: 10n,
180181
slashDuplicateProposalPenalty: 10n,
181182
slashDuplicateAttestationPenalty: 10n,
182183
slashAttestInvalidCheckpointProposalPenalty: 10n,

0 commit comments

Comments
 (0)