Files
neuron-tai/tests/test_reputation_scoring.py
Dobromir Popov 7cf8d9bcf3 test descriptions
2026-07-11 22:25:30 +03:00

239 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Issue 08 (FRAUD: reputation model): scoring rules on top of the persisted
reputation/strike/ban fields from issue 05 (ADR-0018 §6).
Score derives only from tracker audit outcomes + uptime/latency: slow build,
instant loss, inactivity decay. Strikes apply a ×0.8 routing multiplier per
strike, separate from the (full) forfeiture penalty owned by issue 10.
"""
import json
import urllib.request
import pytest
from meshnet_contracts import (
REPUTATION_CLEAN_AUDIT_DELTA,
REPUTATION_FAILED_AUDIT_DELTA,
LocalSolanaContracts,
)
from meshnet_node.server import StubNodeServer
from meshnet_validator import ValidatorProcess
MODEL = "stub-model"
# ---- documented deltas, applied via the persisted issue-05 fields ----
def test_clean_audit_increases_reputation_by_documented_delta():
"Clean audit increases reputation by documented delta\n\nTags: general"
contracts = LocalSolanaContracts()
contracts.registry.record_audit_outcome("wallet-a", passed=False) # 1.0 -> 0.7
before = contracts.registry.get_wallet("wallet-a").reputation
contracts.registry.record_audit_outcome("wallet-a", passed=True)
after = contracts.registry.get_wallet("wallet-a").reputation
assert after == pytest.approx(before + REPUTATION_CLEAN_AUDIT_DELTA)
def test_failed_audit_decreases_reputation_by_documented_delta():
"Failed audit decreases reputation by documented delta\n\nTags: general"
contracts = LocalSolanaContracts()
contracts.registry.record_audit_outcome("wallet-b", passed=False)
assert contracts.registry.get_wallet("wallet-b").reputation == pytest.approx(
1.0 + REPUTATION_FAILED_AUDIT_DELTA
)
def test_reputation_clamps_to_documented_bounds():
"Reputation clamps to documented bounds\n\nTags: general"
contracts = LocalSolanaContracts()
for _ in range(10):
contracts.registry.record_audit_outcome("wallet-c", passed=False)
assert contracts.registry.get_wallet("wallet-c").reputation == 0.0
for _ in range(30):
contracts.registry.record_audit_outcome("wallet-c", passed=True)
assert contracts.registry.get_wallet("wallet-c").reputation == 1.0
def test_reputation_uses_persisted_fields_not_a_second_schema(tmp_path):
"Red (issue 08 test-first item 1): reputation deltas must persist and reload through the same event log issue 05 already wired — no parallel storage path.\n\nTags: persistence"
db = str(tmp_path / "registry.sqlite")
contracts = LocalSolanaContracts(registry_db=db)
contracts.registry.record_audit_outcome("wallet-d", passed=False)
contracts.save_to_db()
reopened = LocalSolanaContracts(registry_db=db)
wallet = reopened.registry.get_wallet("wallet-d")
assert wallet.reputation == pytest.approx(1.0 + REPUTATION_FAILED_AUDIT_DELTA)
assert wallet.strike_count == 0 # audit_outcome never strikes on its own
# ---- strike -> routing multiplier, separate from forfeiture ----
def test_strike_applies_graduated_routing_multiplier_not_full_penalty():
"Strike applies graduated routing multiplier not full penalty\n\nTags: general"
contracts = LocalSolanaContracts()
contracts.registry.submit_stake("wallet-e", 500)
assert contracts.registry.routing_multiplier("wallet-e") == pytest.approx(1.0)
contracts.registry.record_strike("wallet-e", ban_threshold=10)
assert contracts.registry.routing_multiplier("wallet-e") == pytest.approx(0.8)
contracts.registry.record_strike("wallet-e", ban_threshold=10)
assert contracts.registry.routing_multiplier("wallet-e") == pytest.approx(0.8 * 0.8)
# stake (forfeiture surface) is untouched by strikes alone
assert contracts.registry.get_wallet("wallet-e").stake_balance == 500
def test_three_strikes_still_bans_and_probation_still_enforced():
"Three strikes still bans and probation still enforced\n\nTags: general"
contracts = LocalSolanaContracts(probationary_job_count=2)
for _ in range(3):
contracts.registry.record_strike("wallet-f")
wallet = contracts.registry.get_wallet("wallet-f")
assert wallet.strike_count == 3
assert wallet.banned is True
contracts.registry.record_completed_job("wallet-g")
assert contracts.registry.probationary_jobs_remaining("wallet-g") == 1
# ---- inactivity decay ----
def test_inactivity_decay_after_idle_days_without_completed_jobs():
"Inactivity decay after idle days without completed jobs\n\nTags: general"
contracts = LocalSolanaContracts()
contracts.registry.record_completed_jobs("wallet-h", 1, ts=0.0)
idle_seconds = 30 * 86400.0
decayed = contracts.registry.apply_inactivity_decay(
idle_seconds=idle_seconds, decay_amount=0.05, now=idle_seconds + 1.0,
)
assert "wallet-h" in decayed
assert contracts.registry.get_wallet("wallet-h").reputation == pytest.approx(0.95)
def test_inactivity_decay_skips_wallets_active_within_the_window():
"Inactivity decay skips wallets active within the window\n\nTags: security, wallet"
contracts = LocalSolanaContracts()
contracts.registry.record_completed_jobs("wallet-i", 1, ts=1000.0)
decayed = contracts.registry.apply_inactivity_decay(
idle_seconds=30 * 86400.0, now=1000.0 + 60.0,
)
assert decayed == {}
assert contracts.registry.get_wallet("wallet-i").reputation == pytest.approx(1.0)
def test_inactivity_decay_applies_at_most_once_per_idle_window():
"Inactivity decay applies at most once per idle window\n\nTags: general"
contracts = LocalSolanaContracts()
contracts.registry.record_completed_jobs("wallet-j", 1, ts=0.0)
idle_seconds = 30 * 86400.0
contracts.registry.apply_inactivity_decay(idle_seconds=idle_seconds, now=idle_seconds + 1.0)
again = contracts.registry.apply_inactivity_decay(idle_seconds=idle_seconds, now=idle_seconds + 2.0)
assert again == {}
assert contracts.registry.get_wallet("wallet-j").reputation == pytest.approx(0.95)
def test_inactivity_decay_skips_banned_wallets():
"Inactivity decay skips banned wallets\n\nTags: security, wallet"
contracts = LocalSolanaContracts()
contracts.registry.record_completed_jobs("wallet-k", 1, ts=0.0)
contracts.registry.ban_wallet("wallet-k")
decayed = contracts.registry.apply_inactivity_decay(now=30 * 86400.0 + 1.0)
assert decayed == {}
# ---- no peer-to-peer reputation inputs ----
def test_registry_contract_has_no_peer_rating_api():
"Registry contract has no peer rating api\n\nTags: general"
contracts = LocalSolanaContracts()
for forbidden in ("rate_peer", "submit_peer_rating", "peer_reputation"):
assert not hasattr(contracts.registry, forbidden)
# ---- validator wiring: audit outcomes feed reputation end-to-end ----
def _record_event(contracts, session_id: str, output: str, wallet: str) -> None:
contracts.validation.record_completed_inference(
session_id=session_id,
model=MODEL,
messages=[{"role": "user", "content": "2+2"}],
observed_output=output,
route_nodes=[{"wallet_address": wallet, "shard_end": 31}],
)
@pytest.fixture
def reference_node():
node = StubNodeServer(shard_start=0, shard_end=31, is_last_shard=True)
port = node.start()
yield f"http://127.0.0.1:{port}"
node.stop()
def _reference_output(reference_url: str) -> str:
req = urllib.request.Request(
f"{reference_url}/v1/infer",
data=json.dumps({"messages": [{"role": "user", "content": "2+2"}]}).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req) as r:
return json.loads(r.read())["text"]
def test_validator_credits_clean_audit_via_persisted_reputation(reference_node):
"Validator credits clean audit via persisted reputation\n\nTags: persistence"
contracts = LocalSolanaContracts()
contracts.registry.record_audit_outcome("wallet-good", passed=False) # 1.0 -> 0.7
validator = ValidatorProcess(
contracts=contracts, reference_node_url=reference_node, sample_rate=1.0, random_seed=7,
)
_record_event(contracts, "sess-clean", _reference_output(reference_node), "wallet-good")
validator.validate_once()
assert contracts.registry.get_wallet("wallet-good").reputation == pytest.approx(
0.7 + REPUTATION_CLEAN_AUDIT_DELTA
)
def test_validator_docks_reputation_on_failed_audit_without_double_striking(reference_node):
"Validator docks reputation on failed audit without double striking\n\nTags: general"
contracts = LocalSolanaContracts()
contracts.registry.submit_stake("wallet-bad", 500)
validator = ValidatorProcess(
contracts=contracts,
reference_node_url=reference_node,
sample_rate=1.0,
slash_amount=100,
strike_threshold=3,
random_seed=7,
)
_record_event(contracts, "sess-fraud", "fraudulent nonsense", "wallet-bad")
validator.validate_once()
wallet = contracts.registry.get_wallet("wallet-bad")
assert wallet.reputation == pytest.approx(1.0 + REPUTATION_FAILED_AUDIT_DELTA)
# exactly one strike from submit_slash_proof — record_audit_outcome must
# not add a second
assert wallet.strike_count == 1