Skip to content

security: /epoch/enroll allows external reward-weight downgrade #2109

@createkr

Description

@createkr

Zero-weight miner reward distortion via explicit /epoch/enroll endpoint

Severity: High
Component: node/rustchain_v2_integrated_v2.2.1_rip200.py (/epoch/enroll), node/sophia_elya_service.py (enroll_epoch)
Labels: bug, rewards, enrollment, security

Summary

The explicit /epoch/enroll endpoint and the enroll_epoch() helper in sophia_elya_service.py both use INSERT OR REPLACE INTO epoch_enroll, allowing any caller to overwrite a legitimate miner's epoch weight with a near-zero value. This causes proportional reward loss for the victim miner for the entire epoch.

Distinction from prior submission

The prior submission (.tmp-issue-attest-overwrite-reward-loss.md) covered two self-inflicted downgrade paths:

  1. miner_attest_recent fingerprint downgrade via INSERT OR REPLACE → fixed with ON CONFLICT DO UPDATE + MAX(fingerprint_passed).
  2. Auto-enroll weight downgrade via INSERT OR REPLACE → fixed with INSERT OR IGNORE.
  3. P2P gossip path → fixed with ON CONFLICT DO UPDATE + MAX.

This finding is distinct: it covers the explicit /epoch/enroll endpoint which was NOT changed by the prior fix and still uses INSERT OR REPLACE. This creates an external downgrade vector — an attacker (not the victim miner) can call this endpoint to overwrite the victim's weight.

Root Cause

/epoch/enroll endpoint (line ~3075)

c.execute(
    "INSERT OR REPLACE INTO epoch_enroll (epoch, miner_pk, weight) VALUES (?, ?, ?)",
    (epoch, miner_pk, weight)
)

This endpoint accepts miner_pubkey and device from the request body. The weight is recalculated from the provided device data. INSERT OR REPLACE means any call overwrites the existing enrollment for (epoch, miner_pk).

sophia_elya_service.py enroll_epoch() (line ~73)

Same INSERT OR REPLACE pattern in a separate service module.

Exploit Path

  1. Victim miner attests successfully → auto-enrolled with weight=2.5 (e.g. G4 hardware).
  2. Attacker calls POST /epoch/enroll with:
    • miner_pubkey: victim's public key
    • device: {} (empty) or any device that resolves to low weight
    • Or: device data that triggers fingerprint_failed=Trueweight=1e-9
  3. INSERT OR REPLACE overwrites the victim's enrollment: weight goes from 2.51.0 (default x86) or 1e-9 (fingerprint failed).
  4. At epoch settlement, the victim receives ~0% of their expected rewards.

No authentication or ownership proof is required — anyone with a miner's pubkey can do this.

Impact

  • Any enrolled miner is vulnerable to weight downgrade by any third party.
  • Economic impact: proportional to the victim's weight and epoch pot. A G4 miner (weight 2.5) downgraded to 1e-9 loses ~100% of epoch rewards.
  • No exploit complexity: single HTTP POST, no signature or auth required.
  • Triggers under normal operation: a miner re-enrolling themselves with different device data also triggers this (self-inflicted variant).

Reproduction

See node/tests/test_attestation_overwrite_reward_loss.py:

  • test_external_enroll_downgrade_old_behaviour — demonstrates the bug
  • test_external_enroll_downgrade_fixed — verifies the fix

Fix

Change INSERT OR REPLACE to INSERT OR IGNORE in both enrollment paths. The first enrollment in an epoch wins; subsequent calls are no-ops.

node/rustchain_v2_integrated_v2.2.1_rip200.py/epoch/enroll

-        c.execute(
-            "INSERT OR REPLACE INTO epoch_enroll (epoch, miner_pk, weight) VALUES (?, ?, ?)",
-            (epoch, miner_pk, weight)
-        )
+        c.execute(
+            "INSERT OR IGNORE INTO epoch_enroll (epoch, miner_pk, weight) VALUES (?, ?, ?)",
+            (epoch, miner_pk, weight)
+        )

node/sophia_elya_service.pyenroll_epoch()

-        c.execute("INSERT OR REPLACE INTO epoch_enroll(epoch, miner_pk, weight) VALUES (?,?,?)", (epoch, miner_pk, float(weight)))
+        c.execute("INSERT OR IGNORE INTO epoch_enroll(epoch, miner_pk, weight) VALUES (?,?,?)", (epoch, miner_pk, float(weight)))

Trade-off: first-enrollment-wins

With INSERT OR IGNORE, the first enrollment in an epoch wins. If an attacker enrolls a victim's pubkey first with low weight, the victim's later legitimate enrollment is also blocked. This is acceptable because:

  • The attacker would need the victim's pubkey in advance.
  • The attacker sacrifices their own rewards by enrolling with low weight.
  • This is no worse than the attacker having mined with that pubkey from the start.
  • The alternative (allowing re-enrollment) enables the external downgrade attack described above.

A future improvement could add authorization (require the miner to sign the enrollment request) to allow safe re-enrollment.

Files Changed

  • node/rustchain_v2_integrated_v2.2.1_rip200.py/epoch/enroll endpoint
  • node/sophia_elya_service.pyenroll_epoch() helper
  • node/tests/test_attestation_overwrite_reward_loss.py — 3 new tests for external downgrade vector

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions