Skip to content

Node Reputation Score Update Race in Concurrent Attestation Processing #3

Description

@JamesEjembi

Node Reputation Score Update Race in Concurrent Attestation Processing

Problem Statement

The src/core/reputation/score.rs module maintains a reputation score per node (reputation: Map<NodeId, i64>). The update_reputation() function at line 55 adjusts a node's score based on attestation outcomes: +10 for successful attestation, -50 for failed attestation, and -500 for slashing event. When two attestations for the same node are processed concurrently (e.g., one successful attestation from validators and one slashing event from the slashing condition monitor), both read the current reputation score (e.g., 750), compute their respective adjustments (+10 → 760, -500 → 250), and write back. The final stored score depends on which write completes last. If the slashing event writes last, the score is 250 (correct — the node should be slashed). If the attestation writes last, the score is 760 (incorrect — the slash was lost). The slashing event is supposed to be authoritative: a slashed node's reputation should be reduced regardless of concurrent attestations.

State Invariants & Parameters

  • Attestation reward: +10 points
  • Attestation failure penalty: -50 points
  • Slashing penalty: -500 points
  • Reputation range: [-1000, 1000]
  • Slashing authority: absolute (overrides concurrent attestations)
  • Invariant: ∀ slashing_event: reputation[node] == reputation_prior - 500 (slash always applied)

Affected Code Paths

  • src/core/reputation/score.rs:50-85 — Read-compute-write race
  • src/core/reputation/storage.rs:30-50 — Non-atomic reputation write
  • src/core/slashing/executor.rs:60-90 — Slashing event processing path
  • src/core/reputation/tests/score_tests.rs — No concurrent attestation-and-slash test

Resolution Blueprint

  1. Use an atomic increment via storage_update() pattern: instead of read-compute-write, use storage_set(&score_key, &(current_score + adjustment)) where the current_score is read inside a Soroban try_run context that retries on concurrent modification.
  2. Implement a write priority system: slashing events use a PRIORITY_LOCK key. When update_reputation() is called from the slashing path, it acquires the lock. Other reputation updates check the lock and, if held, queue for the next ledger.
  3. Store reputation as a per-event log: instead of a single score, store a Vec<ReputationEvent> that records every adjustment with its source (attestation, slashing, reward). The current score is computed as Σ(events.adjustment). Since events are appended (not overwritten), race conditions are eliminated — both events are recorded, and the score correctly reflects both.
  4. Add a slashing monotonic counter: each node has a slash_count that increments on every slashing event. update_reputation() from the slashing path uses slash_count as a version: require!(new_slash_count == current_slash_count + 1, "slash race"). If another slash already incremented the count, this is a duplicate — skip it.
  5. Add a concurrent test that starts a successful attestation update and a slashing event simultaneously for the same node, verifying the final reputation is 750 + 10 - 500 = 260 (both applied) and never 760 or 250.

Labels

  • Complexity: Hardcore
  • Layer: Core-Engine
  • Type: Race-Condition

Metadata

Metadata

Assignees

Labels

Complexity: HardcoreIssues requiring deep systems-level engineering rigorGrantFox 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
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