Slashing Penalty Calculation Race Under Concurrent Validator Set Changes
Problem Statement
The slashing penalty calculator in src/slashing/penaltyCalculator.ts computes the penalty amount for a slashed node based on the current active validator set size. The calculatePenalty() function at line 75 reads activeValidatorCount from the database and computes penalty = basePenalty * (1 + (totalValidators - activeValidators) / totalValidators). When a validator joins or leaves the active set concurrently with a slashing event (e.g., validator A joins while validator B is being slashed), the slashing event may read the validator count BEFORE the join (10 validators) while the join completes AFTER the penalty calculation writes. The penalty is computed for 10 validators but the actual active set is now 11. If the penalty formula penalizes nodes more heavily when fewer validators are active (to incentivize participation), the node is over-penalized by 10%.
State Invariants & Parameters
- Validator set size: variable (changes via staking/unstaking)
- Base penalty: 500 tokens
- Penalty multiplier:
1 + (total - active) / total (range [1.0, 2.0])
- Read-write gap: 50-100ms (DB round trip)
- Invariant:
penalty == f(active_validator_count_at_slashing_time)
Affected Code Paths
src/slashing/penaltyCalculator.ts:70-110 — Read-count-then-calculate race
src/staking/validatorRegistry.ts:45-75 — Validator set changes
src/slashing/executor.ts:55-85 — Slashing execution pipeline
Resolution Blueprint
- Read the validator count inside the same database transaction that writes the slashing event: use
SELECT COUNT(*) FROM active_validators and INSERT INTO slashing_events (...) VALUES (...) in a single SERIALIZABLE transaction. This ensures the count is consistent with the moment of slashing.
- Store the
validatorCountAtSlashing as a field in the slashing_events table, so the penalty is always computed from the count at the time of the event, not the current live count.
- Acquire a
SHARE ROW EXCLUSIVE lock on the validator_registry table before computing the penalty, preventing concurrent validator set changes during the calculation.
- Add a concurrent test that adds a validator while a slashing penalty is being calculated, verifying the penalty uses the validator count from the transaction's snapshot.
Labels
Complexity: Hardcore
Layer: Core-Engine
Type: Race-Condition
Slashing Penalty Calculation Race Under Concurrent Validator Set Changes
Problem Statement
The slashing penalty calculator in
src/slashing/penaltyCalculator.tscomputes the penalty amount for a slashed node based on the current active validator set size. ThecalculatePenalty()function at line 75 readsactiveValidatorCountfrom the database and computespenalty = basePenalty * (1 + (totalValidators - activeValidators) / totalValidators). When a validator joins or leaves the active set concurrently with a slashing event (e.g., validator A joins while validator B is being slashed), the slashing event may read the validator count BEFORE the join (10 validators) while the join completes AFTER the penalty calculation writes. The penalty is computed for 10 validators but the actual active set is now 11. If the penalty formula penalizes nodes more heavily when fewer validators are active (to incentivize participation), the node is over-penalized by 10%.State Invariants & Parameters
1 + (total - active) / total(range [1.0, 2.0])penalty == f(active_validator_count_at_slashing_time)Affected Code Paths
src/slashing/penaltyCalculator.ts:70-110— Read-count-then-calculate racesrc/staking/validatorRegistry.ts:45-75— Validator set changessrc/slashing/executor.ts:55-85— Slashing execution pipelineResolution Blueprint
SELECT COUNT(*) FROM active_validatorsandINSERT INTO slashing_events (...) VALUES (...)in a singleSERIALIZABLEtransaction. This ensures the count is consistent with the moment of slashing.validatorCountAtSlashingas a field in theslashing_eventstable, so the penalty is always computed from the count at the time of the event, not the current live count.SHARE ROW EXCLUSIVElock on thevalidator_registrytable before computing the penalty, preventing concurrent validator set changes during the calculation.Labels
Complexity: HardcoreLayer: Core-EngineType: Race-Condition