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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions