Skip to content

fix(sophia): reject negative enroll weights that amplify epoch payouts (#14583)#7871

Merged
Scottcjn merged 1 commit into
Scottcjn:mainfrom
Vyacheslav-Tomashevskiy:fix/sophia-negative-weight-14583
Jul 3, 2026
Merged

fix(sophia): reject negative enroll weights that amplify epoch payouts (#14583)#7871
Scottcjn merged 1 commit into
Scottcjn:mainfrom
Vyacheslav-Tomashevskiy:fix/sophia-negative-weight-14583

Conversation

@Vyacheslav-Tomashevskiy

Copy link
Copy Markdown
Contributor

Closes #14583 (Critical: Reward Amplification via Negative Weight in SOPHIA).

Bug

/epoch/enroll parses the temporal/rtc weight factors with _finite_float, which accepts any finite value, including negatives. The product total_weight = temporal * rtc * hw is stored verbatim by enroll_epoch, with no non-negativity check. A caller holding a valid ticket can therefore enroll a negative weight for any miner_pubkey.

At settlement, finalize_epoch computes sum_w = sum(weights) and pays each miner total_reward * (w / sum_w). A negative weight shrinks sum_w, inflating every other (attacker-controlled) miner's pro-rata share — reward theft / denial-of-weight.

This was the only enrollment path in the node that did not exclude non-positive weights. node/rip0202_enrollment.py ("negative weights canonicalise to 0 units (excluded)") and node/rustchain_block_producer.py (if enroll_weight <= 0) already guard against it.

Fix (defense in depth)

  • Endpoint /epoch/enroll: reject negative temporal/rtc and any total_weight <= 0 with invalid_weights, before consuming the ticket (so a rejected request never burns a ticket).
  • enroll_epoch(): backstop — non-finite / non-positive weights are never persisted, protecting any internal caller.
  • finalize_epoch(): exclude non-positive / non-finite weights from sum_w and payouts, so a poisoned legacy row (enrolled before this guard) cannot distort settlement.

Tests

Added to node/tests/test_sophia_elya_service.py:

  • endpoint rejects temporal: -5.0 with invalid_weights and preserves the ticket;
  • enroll_epoch drops negative and zero weights, keeps the positive one;
  • finalize_epoch excludes a poisoned -9.0 row so sum_w == 1.0 and the whole reward goes to the honest miner.

pytest node/tests/test_sophia_elya_service.py → 12 passed; adjacent epoch/settlement suites (reward-overflow, finalize-ledger, settlement-atomic, money-units, anti-double-mining-enroll, weight-fixedpoint, integrated-balance) → 39 passed.

/claim #14583

…s (#14583)

The /epoch/enroll endpoint parsed `temporal`/`rtc` weight factors with
`_finite_float`, which accepts any finite value including negatives. The
resulting `total_weight = temporal * rtc * hw` was stored verbatim, so a
caller with a valid ticket could enroll a negative weight. At settlement,
`finalize_epoch` computes `sum_w = sum(weights)`; a negative weight shrinks
sum_w and inflates every other miner's pro-rata share (`w / sum_w`),
enabling reward theft / denial-of-weight.

This was the only enrollment path in the node that did not exclude
non-positive weights — rip0202_enrollment and rustchain_block_producer
already do (`weight <= 0` / "negative weights canonicalise to 0 units").

Fix (defense in depth):
- /epoch/enroll rejects negative `temporal`/`rtc` and any `total_weight <= 0`
  with `invalid_weights`, before consuming the ticket.
- enroll_epoch() backstops direct callers: non-finite / non-positive weights
  are never persisted.
- finalize_epoch() excludes non-positive / non-finite weights from sum_w and
  payouts, so a poisoned legacy row cannot distort settlement.

Adds tests covering the endpoint rejection (ticket preserved), the
enroll_epoch backstop, and settlement exclusion of a poisoned negative row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Welcome to RustChain! Thanks for your first pull request.

Before we review, please make sure:

  • Non-doc PRs have a BCOS-L1 or BCOS-L2 label
  • Doc-only PRs are exempt from BCOS tier labels when they only touch docs/**, *.md, or common image/PDF files
  • New code files include an SPDX license header
  • You've tested your changes against the live node

Bounty tiers: Micro (1-10 RTC) | Standard (20-50) | Major (75-100) | Critical (100-150)

A maintainer will review your PR soon. Thanks for contributing!

@github-actions github-actions Bot added BCOS-L1 Beacon Certified Open Source tier BCOS-L1 (required for non-doc PRs) BCOS-L2 Beacon Certified Open Source tier BCOS-L2 (required for non-doc PRs) node Node server related tests Test suite changes size/M PR: 51-200 lines labels Jul 3, 2026
@Scottcjn Scottcjn merged commit 24aa145 into Scottcjn:main Jul 3, 2026
12 checks passed
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

RTC Reward

This merged PR earned 5 RTC — sent to Vyacheslav-Tomashevskiy.

RustChain Bounty Program

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

Labels

BCOS-L1 Beacon Certified Open Source tier BCOS-L1 (required for non-doc PRs) BCOS-L2 Beacon Certified Open Source tier BCOS-L2 (required for non-doc PRs) node Node server related size/M PR: 51-200 lines tests Test suite changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants