Skip to content

Slashing Penalty Calculation Race Under Concurrent Validator Set Changes #32

Description

@JamesEjembi

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

  1. 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.
  2. 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.
  3. Acquire a SHARE ROW EXCLUSIVE lock on the validator_registry table before computing the penalty, preventing concurrent validator set changes during the calculation.
  4. 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

Metadata

Metadata

Assignees

Labels

Complexity: HardcoreExtremely difficult, high-complexity engineering taskGrantFox OSSIssue tracked in GrantFox OSSLayer: Core-EngineCore engine layerMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial CampaignCampaign: Official CampaignType: Race-ConditionConcurrency and race condition related issues

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions