Skip to content

Commit 42107cd

Browse files
committed
feat: slash for invalid checkpoint proposals
1 parent fe4792b commit 42107cd

9 files changed

Lines changed: 398 additions & 19 deletions

File tree

yarn-project/slasher/README.md

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

131131
### BROADCASTED_INVALID_CHECKPOINT_PROPOSAL
132-
**Description**: A proposer broadcast a checkpoint proposal that terminates before a higher-index block proposal signed by the same proposer in the same slot.
133-
**Detection**: BroadcastedInvalidCheckpointProposalWatcher scans retained P2P proposals and compares checkpoint archive roots to signed block proposals from the same slot and signer.
134-
**Target**: Proposer who broadcast the truncated checkpoint proposal.
132+
**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.
133+
**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.
134+
**Target**: Proposer who broadcast the invalid checkpoint proposal.
135135
**Time Unit**: Slot-based offense.
136136

137137
## Configuration

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export type ValidatorClientFullConfig = ValidatorClientConfig &
8787
Pick<
8888
SlasherConfig,
8989
| 'slashBroadcastedInvalidBlockPenalty'
90+
| 'slashBroadcastedInvalidCheckpointProposalPenalty'
9091
| 'slashDuplicateProposalPenalty'
9192
| 'slashDuplicateAttestationPenalty'
9293
| 'slashAttestInvalidCheckpointProposalPenalty'
@@ -124,6 +125,7 @@ export const ValidatorClientFullConfigSchema = zodFor<Omit<ValidatorClientFullCo
124125
broadcastInvalidBlockProposal: z.boolean().optional(),
125126
maxBlocksPerCheckpoint: z.number().positive().optional(),
126127
slashBroadcastedInvalidBlockPenalty: schemas.BigInt,
128+
slashBroadcastedInvalidCheckpointProposalPenalty: schemas.BigInt,
127129
slashDuplicateProposalPenalty: schemas.BigInt,
128130
slashDuplicateAttestationPenalty: schemas.BigInt,
129131
slashAttestInvalidCheckpointProposalPenalty: schemas.BigInt,

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

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

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: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,39 @@ export type BlockProposalValidationFailureResult = {
8282

8383
export type BlockProposalValidationResult = BlockProposalValidationSuccessResult | BlockProposalValidationFailureResult;
8484

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

89119
type CheckpointComputationResult =
90120
| { checkpointNumber: CheckpointNumber; reason?: undefined }
@@ -108,6 +138,8 @@ export class ProposalHandler {
108138
/** Returns current validator addresses for own-proposal detection. Set via register(). */
109139
private getOwnValidatorAddresses?: () => string[];
110140

141+
private checkpointProposalValidationFailureCallback?: CheckpointProposalValidationFailureCallback;
142+
111143
constructor(
112144
private checkpointsBuilder: FullNodeCheckpointsBuilder,
113145
private worldState: WorldStateSynchronizer,
@@ -130,6 +162,14 @@ export class ProposalHandler {
130162
this.tracer = telemetry.getTracer('ProposalHandler');
131163
}
132164

165+
public updateConfig(config: Partial<ValidatorClientFullConfig>): void {
166+
this.config = { ...this.config, ...config };
167+
}
168+
169+
public setCheckpointProposalValidationFailureCallback(callback?: CheckpointProposalValidationFailureCallback): void {
170+
this.checkpointProposalValidationFailureCallback = callback;
171+
}
172+
133173
/**
134174
* Registers handlers for block and checkpoint proposals on the p2p client.
135175
* Block proposals are registered for non-validator nodes (validators register their own enhanced handler).
@@ -192,6 +232,19 @@ export class ProposalHandler {
192232
proposer: proposal.getSender()?.toString(),
193233
};
194234

235+
if (this.config.skipCheckpointProposalValidation) {
236+
this.log.warn(`Skipping checkpoint proposal validation for slot ${proposal.slotNumber}`, proposalInfo);
237+
return undefined;
238+
}
239+
240+
if (await this.epochCache.isEscapeHatchOpenAtSlot(proposal.slotNumber)) {
241+
this.log.warn(
242+
`Escape hatch open for slot ${proposal.slotNumber}, skipping checkpoint proposal validation`,
243+
proposalInfo,
244+
);
245+
return undefined;
246+
}
247+
195248
// For own proposals, skip validation — the proposer already built and validated the checkpoint
196249
const proposer = proposal.getSender();
197250
const ownAddresses = this.getOwnValidatorAddresses?.();
@@ -214,7 +267,9 @@ export class ProposalHandler {
214267
}
215268

216269
const result = await this.handleCheckpointProposal(proposal, proposalInfo);
217-
if (result.isValid && this.archiver && this.epochCache.isProposerPipeliningEnabled()) {
270+
if (!result.isValid) {
271+
await this.checkpointProposalValidationFailureCallback?.(proposal, result, proposalInfo);
272+
} else if (this.archiver && this.epochCache.isProposerPipeliningEnabled()) {
218273
const set = await this.setProposedCheckpointFromValidation(proposal);
219274
if (set) {
220275
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
@@ -137,6 +137,7 @@ describe('ValidatorClient HA Integration', () => {
137137
Pick<
138138
SlasherConfig,
139139
| 'slashBroadcastedInvalidBlockPenalty'
140+
| 'slashBroadcastedInvalidCheckpointProposalPenalty'
140141
| 'slashDuplicateProposalPenalty'
141142
| 'slashDuplicateAttestationPenalty'
142143
| 'slashAttestInvalidCheckpointProposalPenalty'
@@ -146,6 +147,7 @@ describe('ValidatorClient HA Integration', () => {
146147
disableValidator: false,
147148
disabledValidators: [],
148149
slashBroadcastedInvalidBlockPenalty: 1n,
150+
slashBroadcastedInvalidCheckpointProposalPenalty: 1n,
149151
rollupAddress,
150152
l1ChainId: TEST_COORDINATION_SIGNATURE_CONTEXT.chainId,
151153
slashDuplicateProposalPenalty: 1n,
@@ -194,6 +196,7 @@ describe('ValidatorClient HA Integration', () => {
194196
Pick<
195197
SlasherConfig,
196198
| 'slashBroadcastedInvalidBlockPenalty'
199+
| 'slashBroadcastedInvalidCheckpointProposalPenalty'
197200
| 'slashDuplicateProposalPenalty'
198201
| 'slashDuplicateAttestationPenalty'
199202
| '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)