Skip to content

feat: add slashing penalty core#6674

Open
yyswhsccc wants to merge 2 commits into
Scottcjn:mainfrom
yyswhsccc:bounty-radar/issue-2327-slashing-penalties
Open

feat: add slashing penalty core#6674
yyswhsccc wants to merge 2 commits into
Scottcjn:mainfrom
yyswhsccc:bounty-radar/issue-2327-slashing-penalties

Conversation

@yyswhsccc
Copy link
Copy Markdown
Contributor

@yyswhsccc yyswhsccc commented May 31, 2026

BCOS Checklist (Required For Non-Doc PRs)

  • Add a tier label: BCOS-L1
  • If adding new code files, include SPDX header near the top
  • Provide test evidence

What Changed

  • Added a slashing penalty core for validator equivocation evidence.
  • Records slash evidence idempotently by evidence hash so replayed proof cannot burn twice.
  • Burns a configurable portion of the validator balance from supported legacy balance schemas.
  • Marks validators as slashed through a future epoch and removes matching future epoch_enroll rows.
  • Added focused unit coverage and a short demo note with sample input/output.

Why It Matters

This gives #2327 a concrete penalty/exclusion mechanism once double-vote, double-proposal, surround-vote, or equivocation evidence has been verified. It keeps the first slice side-effect-bounded: no networking changes, no consensus rewiring, and no automatic evidence submission path.

Validation

  • .venv-bounty-validation/bin/python -m py_compile node/slashing_penalties.py node/tests/test_slashing_penalties.py -> passed
  • .venv-bounty-validation/bin/python -m pytest -q node/tests/test_slashing_penalties.py --tb=short -> 7 passed
  • .venv-bounty-validation/bin/python -m ruff check node/slashing_penalties.py node/tests/test_slashing_penalties.py -> passed
  • demo snippet in docs/slashing-penalty-demo.md -> matched expected output
  • git diff --check upstream/main...HEAD -> passed
  • hidden Unicode scan over upstream/main...HEAD changed files -> passed

I also attempted the repo-default .venv-bounty-validation/bin/python -m pytest -q. After installing the immediate missing test dependencies available for Python 3.9, collection still stopped on unrelated optional dependencies such as yaml and matplotlib; the focused slashing penalty tests above are isolated and pass.

Scope / Risk Boundary

  • BCOS-L1
  • This PR applies already-verified slashing evidence to local penalty state and epoch eligibility.
  • It does not add peer evidence gossip, validator signature verification, consensus fork choice changes, or automatic reward claiming.

Closes #2327

wallet: RTC47bc28896a1a4bf240d1fd780f4559b242bcd945

@github-actions github-actions Bot added documentation Improvements or additions to documentation BCOS-L1 Beacon Certified Open Source tier BCOS-L1 (required for non-doc PRs) node Node server related tests Test suite changes size/L PR: 201-500 lines labels May 31, 2026
Copy link
Copy Markdown
Contributor

@zqleslie zqleslie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of PR #6674feat: add slashing penalty core

Reviewed head 93b70b77ae1c170347e8e73d72e8e73d72e8e73d (3 files, 395 LOC + 103 LOC test).

✅ Strengths

Idempotency by evidence hash (line 184-196): The validator_slashes table uses evidence_hash as the PK, and the apply_slashing_evidence() function returns early with {"duplicate": True} if the hash already exists. This prevents replay attacks that could drain a validator balance — critical for a slashing module.

Schema-agnostic balance lookup (line 300-328): _get_balance_urtc() probes for both amount_i64 and balance_rtc columns, and even checks alternative key columns (miner_id / miner_pk / wallet). This is a pragmatic approach for a codebase with legacy schemas.

Epoch enrollment cleanup (line 356-372): _remove_future_enrollments() deletes rows where epoch > current_epoch AND epoch <= slashed_until_epoch, which correctly prevents a slashed validator from being selected in future epochs during the exclusion window.

⚠️ Issues

1. SQL injection risk in PRAGMA query (line 385)

def _table_columns(conn, table_name):
    return tuple(row[1] for row in conn.execute(f"PRAGMA table_info({table_name})").fetchall())

table_name is interpolated directly into the SQL string. While callers currently pass hardcoded values ("balances", "epoch_enroll"), this is a footgun — if any caller ever passed user-influenced input, it would be injectable. Recommend using parameterized query or at minimum validating against a whitelist of known table names:

VALID_TABLES = {"balances", "epoch_enroll", "validator_slashes", "slashed_validators"}
if table_name not in VALID_TABLES:
    raise SlashingError("unknown_table")

2. Balance underflow edge case in _debit_balance (line 330-354)

When using balance_rtc (REAL column), the penalty_rtc = penalty_urtc / URTC_PER_RTC division can introduce floating-point rounding. If the balance is very close to the penalty (e.g., 0.10000001 RTC with a 0.10 RTC penalty), floating-point comparison balance_rtc >= penalty_rtc could be unreliable. The CASE WHEN ... ELSE 0 provides a floor, but the subtraction could produce -1e-15 due to IEEE 754, which is then set to 0. This is safe in practice but worth documenting, or using integer math throughout.

3. No transaction boundary (line 183-234)

apply_slashing_evidence() performs multiple writes (INSERT into validator_slashes, INSERT/UPDATE slashed_validators, DELETE from epoch_enroll) without wrapping them in a single transaction. If the process crashes between the balance debit and the enrollment cleanup, the ledger state would be inconsistent (funds burned but validator still enrolled). Consider:

with conn:  # auto-commits on success, rolls back on exception
    ...

📝 Minor

  • The test suite mocks rustchain_crypto at import time — this is fragile if the module gains new dependencies. Consider using pytest fixtures or unittest.mock.patch for more resilient test isolation.
  • _table_columns uses PRAGMA table_info which returns results in column-definition order, not sorted. The _first_present logic works correctly regardless, but this is worth a comment.

Verdict: Approve with suggestions. Solid first slice of slashing infrastructure.

I received RTC compensation for this review.

@autochamchikim-pixel
Copy link
Copy Markdown

Reviewing under the open PR-review bounty (#2782). I ran the focused test suite locally:

PYTHONPATH=node python -m pytest -q node/tests/test_slashing_penalties.py --tb=short -> 7 passed

Two things look worth tightening before this becomes reachable from a route or settlement pipeline:

  1. evidence_hash is accepted as a caller-controlled idempotency key. Because apply_slashing_evidence() checks validator_slashes by evidence_hash before looking at the rest of the offense, two distinct offenses can be collapsed if the submitter reuses a previous hash. I reproduced this locally by applying epoch 10 double_vote evidence with evidence_hash='reused-proof-id', then applying a different epoch 11 double_vote with different vote roots but the same hash. The second call returned duplicate=True, inserted no second slash row, and left the balance after only one penalty. I would either always derive the hash from canonical evidence, or reject a supplied hash unless it equals _derive_evidence_hash(...) for the submitted payload.

  2. The normalization path validates only the offense type, not the offense-specific proof shape. For example, apply_slashing_evidence(..., {'offense_type': 'double_vote', 'details': {}}) applies a penalty, and {'vote_a': 'same', 'vote_b': 'same'} also applies. If this module intentionally receives only already-verified evidence, that trust boundary should be explicit in tests/docs and the public route should not pass raw user payloads through this function. If the module is meant to be the final guardrail, it should require the minimal fields for each SLASHABLE_OFFENSES variant and reject non-evidence before debiting balances.

These are not style issues; they affect whether the slashing record is an auditable proof ledger versus a caller-selected dedupe key plus a generic penalty switch.

@autochamchikim-pixel
Copy link
Copy Markdown

Correction on the bounty reference above: I meant the tracker issue in Scottcjn/rustchain-bounties#2782, not Scottcjn/Rustchain#2782. Submission link for that review-bounty context: #6674 (comment)

@yyswhsccc
Copy link
Copy Markdown
Contributor Author

@autochamchikim-pixel Thanks for reviewing this. GitHub currently shows this as a comment-only review rather than a formal approval.

Could you re-review when you have a chance? If this looks good, a formal approval would help close out the review.

@yyswhsccc
Copy link
Copy Markdown
Contributor Author

@zqleslie Thanks for reviewing this. GitHub currently shows this as a comment-only review rather than a formal approval.

Could you re-review when you have a chance? If this looks good, a formal approval would help close out the review.

@yyswhsccc
Copy link
Copy Markdown
Contributor Author

@Scottcjn This PR is ready for maintainer review.

Validation evidence is listed in the PR body. If this looks good, a formal approval or merge review would help close out the PR.

@yyswhsccc
Copy link
Copy Markdown
Contributor Author

PR summary

What changed

  • Added a slashing penalty core for validator equivocation evidence.
  • Records slash evidence idempotently by evidence hash so replayed proof cannot burn twice.
  • Burns a configurable portion of the validator balance from supported legacy balance schemas.
  • Marks validators as slashed through a future epoch and removes matching future epoch_enroll rows.

Touched files

  • docs/slashing-penalty-demo.md, node/slashing_penalties.py, node/tests/test_slashing_penalties.py

Validation

  • .venv-bounty-validation/bin/python -m py_compile node/slashing_penalties.py node/tests/test_slashing_penalties.py -> passed
  • .venv-bounty-validation/bin/python -m pytest -q node/tests/test_slashing_penalties.py --tb=short -> 7 passed
  • .venv-bounty-validation/bin/python -m ruff check node/slashing_penalties.py node/tests/test_slashing_penalties.py -> passed
  • git diff --check upstream/main...HEAD -> passed

This summarizes the PR body so reviewers can see the change and validation from the timeline.

@github-actions github-actions Bot added size/XL PR: 500+ lines and removed size/L PR: 201-500 lines labels May 31, 2026
@yyswhsccc
Copy link
Copy Markdown
Contributor Author

@autochamchikim-pixel Thanks for reviewing this. GitHub currently shows this as a comment-only review rather than a formal approval.

Could you re-review when you have a chance? If this looks good, a formal approval would help close out the review.

@yyswhsccc
Copy link
Copy Markdown
Contributor Author

Maintenance update

Review follow-up addressed

  • Actionable technical review comment.

Commit

  • 9752593 — fix: validate slashing evidence before penalties

Validation

  • python3 -m py_compile node/slashing_penalties.py node/tests/test_slashing_penalties.pypassed
  • PYTHONPATH=node uv run --no-project --with pytest python -m pytest -q node/tests/test_slashing_penalties.py --tb=short --noconftest -o addopts=''14 passed
  • git diff --check origin/main...HEAD -- node/slashing_penalties.py node/tests/test_slashing_penalties.py docs/slashing-penalty-demo.mdclean

Reviewer recheck

Scope
This update is limited to the reviewer-directed maintenance items above.

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) documentation Improvements or additions to documentation node Node server related size/XL PR: 500+ lines tests Test suite changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[SEC] No slashing mechanism for malicious validators

3 participants