135 lines
4.6 KiB
Python
135 lines
4.6 KiB
Python
"""Issue 05 (A1/A5) tests: strike/ban/reputation state survives tracker restart.
|
|
|
|
Registry wallet state was RAM-only (`_LocalContractState.registry`) — a tracker
|
|
restart wiped strikes, bans, and job counts. These tests pin the event-sourced
|
|
SQLite persistence (same replication model as BillingLedger / AccountStore)
|
|
per ADR-0016 §4 and ADR-0018 §6.
|
|
"""
|
|
|
|
import json
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
import pytest
|
|
|
|
from meshnet_contracts import LocalSolanaContracts, RegistryWallet
|
|
from meshnet_tracker.server import TrackerServer
|
|
|
|
|
|
def _post_json(url: str, payload: dict) -> dict:
|
|
data = json.dumps(payload).encode()
|
|
req = urllib.request.Request(
|
|
url, data=data, headers={"Content-Type": "application/json"}, method="POST"
|
|
)
|
|
with urllib.request.urlopen(req) as r:
|
|
return json.loads(r.read())
|
|
|
|
|
|
# ---- schema: new reputation fields ----
|
|
|
|
|
|
def test_registry_wallet_has_reputation_fields():
|
|
"Registry wallet has reputation fields\n\nTags: security, wallet"
|
|
wallet = RegistryWallet()
|
|
assert wallet.reputation == 1.0
|
|
assert wallet.last_audit_ts is None
|
|
|
|
|
|
# ---- persistence across restart ----
|
|
|
|
|
|
def test_strike_survives_restart(tmp_path):
|
|
"Strike survives restart\n\nTags: general"
|
|
db = str(tmp_path / "registry.sqlite")
|
|
contracts = LocalSolanaContracts(registry_db=db)
|
|
contracts.registry.record_strike("wallet-a")
|
|
contracts.save_to_db()
|
|
|
|
reopened = LocalSolanaContracts(registry_db=db)
|
|
assert reopened.registry.get_wallet("wallet-a").strike_count == 1
|
|
|
|
|
|
def test_ban_stake_and_jobs_survive_restart(tmp_path):
|
|
"Ban stake and jobs survive restart\n\nTags: general"
|
|
db = str(tmp_path / "registry.sqlite")
|
|
contracts = LocalSolanaContracts(registry_db=db)
|
|
contracts.registry.submit_stake("wallet-b", 500)
|
|
contracts.registry.submit_slash_proof(
|
|
wallet_address="wallet-b", slash_amount=200, strike_threshold=1
|
|
)
|
|
contracts.registry.record_completed_job("wallet-b")
|
|
contracts.save_to_db()
|
|
|
|
reopened = LocalSolanaContracts(registry_db=db)
|
|
wallet = reopened.registry.get_wallet("wallet-b")
|
|
assert wallet.banned is True
|
|
assert wallet.strike_count == 1
|
|
assert wallet.stake_balance == 300
|
|
assert wallet.completed_job_count == 1
|
|
# list_wallets must reflect persisted state
|
|
assert "wallet-b" in reopened.registry.list_wallets()
|
|
|
|
|
|
def test_reputation_and_audit_ts_survive_restart(tmp_path):
|
|
"Reputation and audit ts survive restart\n\nTags: general"
|
|
db = str(tmp_path / "registry.sqlite")
|
|
contracts = LocalSolanaContracts(registry_db=db)
|
|
contracts.registry.set_reputation("wallet-c", 0.8)
|
|
contracts.registry.record_audit("wallet-c", ts=1234.5)
|
|
contracts.save_to_db()
|
|
|
|
reopened = LocalSolanaContracts(registry_db=db)
|
|
wallet = reopened.registry.get_wallet("wallet-c")
|
|
assert wallet.reputation == pytest.approx(0.8)
|
|
assert wallet.last_audit_ts == pytest.approx(1234.5)
|
|
|
|
|
|
# ---- event replication (multi-tracker gossip / future Raft) ----
|
|
|
|
|
|
def test_registry_events_replicate_between_instances():
|
|
"Registry events replicate between instances\n\nTags: general"
|
|
a = LocalSolanaContracts()
|
|
b = LocalSolanaContracts()
|
|
a.registry.record_strike("wallet-d")
|
|
a.registry.submit_stake("wallet-d", 100)
|
|
|
|
events, cursor = a.registry_log.events_since(0)
|
|
assert cursor == len(events) >= 2
|
|
applied = b.registry_log.apply_events(events)
|
|
assert applied == len(events)
|
|
assert b.registry.get_wallet("wallet-d") == a.registry.get_wallet("wallet-d")
|
|
|
|
# idempotent: re-applying the same events is a no-op
|
|
assert b.registry_log.apply_events(events) == 0
|
|
|
|
|
|
# ---- banned wallet rejected at registration after restart ----
|
|
|
|
|
|
def test_banned_wallet_rejected_at_registration_after_restart(tmp_path):
|
|
"Banned wallet rejected at registration after restart\n\nTags: security, wallet"
|
|
db = str(tmp_path / "registry.sqlite")
|
|
contracts = LocalSolanaContracts(registry_db=db)
|
|
contracts.registry.ban_wallet("wallet-evil")
|
|
contracts.save_to_db()
|
|
|
|
tracker = TrackerServer(contracts=LocalSolanaContracts(registry_db=db))
|
|
port = tracker.start()
|
|
try:
|
|
with pytest.raises(urllib.error.HTTPError) as excinfo:
|
|
_post_json(
|
|
f"http://127.0.0.1:{port}/v1/nodes/register",
|
|
{
|
|
"endpoint": "http://127.0.0.1:7900",
|
|
"model": "test-model",
|
|
"num_layers": 4,
|
|
"shard_start": 0,
|
|
"shard_end": 3,
|
|
"wallet_address": "wallet-evil",
|
|
},
|
|
)
|
|
assert excinfo.value.code == 403
|
|
finally:
|
|
tracker.stop()
|