feat: USDT reward system — billing ledger, devnet treasury, settlement loop, forfeiture PoW, tracker dashboard (US-030…US-035, ADR-0015)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
60
tests/test_dashboard.py
Normal file
60
tests/test_dashboard.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""US-035: tracker web dashboard — served from any tracker, embedded asset."""
|
||||
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
from meshnet_contracts import LocalSolanaContracts
|
||||
from meshnet_tracker.billing import BillingLedger
|
||||
from meshnet_tracker.server import TrackerServer
|
||||
|
||||
PANELS = [
|
||||
"Tracker hive", "Nodes & coverage", "Client balances",
|
||||
"Node pending payouts", "Settlement history",
|
||||
"Strikes / bans / forfeitures", "Model usage",
|
||||
]
|
||||
|
||||
|
||||
def test_dashboard_served_with_all_panels():
|
||||
tracker = TrackerServer(billing=BillingLedger())
|
||||
port = tracker.start()
|
||||
try:
|
||||
html = urllib.request.urlopen(
|
||||
f"http://127.0.0.1:{port}/dashboard"
|
||||
).read().decode()
|
||||
for panel in PANELS:
|
||||
assert panel in html
|
||||
assert "<script>" in html # polling client embedded, no build step
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_dashboard_served_by_follower():
|
||||
"""A tracker that is not the leader (unreachable peers → never elected)
|
||||
still serves the dashboard from its own replicated state."""
|
||||
tracker = TrackerServer(
|
||||
billing=BillingLedger(),
|
||||
cluster_peers=["http://127.0.0.1:1", "http://127.0.0.1:2"],
|
||||
)
|
||||
port = tracker.start()
|
||||
try:
|
||||
response = urllib.request.urlopen(f"http://127.0.0.1:{port}/dashboard")
|
||||
assert response.status == 200
|
||||
assert "meshnet tracker" in response.read().decode()
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_registry_wallets_endpoint():
|
||||
contracts = LocalSolanaContracts()
|
||||
contracts.registry.submit_stake("wallet-a", 100)
|
||||
contracts.registry.record_strike("wallet-a")
|
||||
tracker = TrackerServer(contracts=contracts)
|
||||
port = tracker.start()
|
||||
try:
|
||||
data = json.loads(urllib.request.urlopen(
|
||||
f"http://127.0.0.1:{port}/v1/registry/wallets"
|
||||
).read())
|
||||
assert data["wallets"]["wallet-a"]["strike_count"] == 1
|
||||
assert data["wallets"]["wallet-a"]["banned"] is False
|
||||
finally:
|
||||
tracker.stop()
|
||||
223
tests/test_devnet_treasury.py
Normal file
223
tests/test_devnet_treasury.py
Normal file
@@ -0,0 +1,223 @@
|
||||
"""US-032: devnet custodial treasury — wallet binding + deposit watcher.
|
||||
|
||||
The deposit watcher polls the treasury token account and credits the sending
|
||||
wallet's bound API key exactly once per transaction signature. The watcher is
|
||||
exercised against a fake treasury (hermetic CI, ADR-0007); the real
|
||||
solana-test-validator flow runs only when the binary is installed.
|
||||
"""
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
import pytest
|
||||
|
||||
from meshnet_tracker.billing import BillingLedger
|
||||
from meshnet_tracker.server import TrackerServer
|
||||
|
||||
|
||||
class _FakeDeposit:
|
||||
def __init__(self, signature, sender_wallet, amount_usdt):
|
||||
self.signature = signature
|
||||
self.sender_wallet = sender_wallet
|
||||
self.amount_usdt = amount_usdt
|
||||
|
||||
|
||||
class _FakeTreasury:
|
||||
"""Stands in for SolanaCustodialTreasury: a queue of confirmed deposits."""
|
||||
|
||||
def __init__(self):
|
||||
self.deposits: list[_FakeDeposit] = []
|
||||
|
||||
def list_new_deposits(self, is_seen, *, limit=200):
|
||||
return [d for d in self.deposits if not is_seen(d.signature)]
|
||||
|
||||
|
||||
def _post_json(url: str, payload: dict, headers: dict | None = None) -> dict:
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=json.dumps(payload).encode(),
|
||||
headers={"Content-Type": "application/json", **(headers or {})},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def watched_tracker():
|
||||
ledger = BillingLedger(starting_credit=0.0, default_price_per_1k=0.02)
|
||||
treasury = _FakeTreasury()
|
||||
tracker = TrackerServer(
|
||||
billing=ledger,
|
||||
treasury=treasury,
|
||||
deposit_poll_interval=0.1,
|
||||
)
|
||||
port = tracker.start()
|
||||
yield f"http://127.0.0.1:{port}", ledger, treasury
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def _wait_for(predicate, timeout=3.0):
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
if predicate():
|
||||
return True
|
||||
time.sleep(0.05)
|
||||
return False
|
||||
|
||||
|
||||
def test_wallet_register_requires_auth(watched_tracker):
|
||||
tracker_url, _, _ = watched_tracker
|
||||
with pytest.raises(urllib.error.HTTPError) as exc_info:
|
||||
_post_json(f"{tracker_url}/v1/wallet/register", {"wallet": "So1anaWa11et111"})
|
||||
assert exc_info.value.code == 401
|
||||
|
||||
|
||||
def test_deposit_credits_bound_api_key_exactly_once(watched_tracker):
|
||||
tracker_url, ledger, treasury = watched_tracker
|
||||
reply = _post_json(
|
||||
f"{tracker_url}/v1/wallet/register",
|
||||
{"wallet": "So1anaWa11et111"},
|
||||
headers={"Authorization": "Bearer client-key-1"},
|
||||
)
|
||||
assert reply["bound"] is True
|
||||
|
||||
treasury.deposits.append(_FakeDeposit("sig-1", "So1anaWa11et111", 25.0))
|
||||
assert _wait_for(lambda: ledger.get_client_balance("client-key-1") > 0)
|
||||
assert ledger.get_client_balance("client-key-1") == pytest.approx(25.0)
|
||||
|
||||
# the same signature keeps being reported by the RPC — credited once only
|
||||
time.sleep(0.4)
|
||||
assert ledger.get_client_balance("client-key-1") == pytest.approx(25.0)
|
||||
|
||||
# replay through the ledger API directly is also a no-op
|
||||
assert ledger.credit_deposit("client-key-1", 25.0, "sig-1") is None
|
||||
|
||||
|
||||
def test_unbound_wallet_deposit_is_not_credited(watched_tracker):
|
||||
_, ledger, treasury = watched_tracker
|
||||
treasury.deposits.append(_FakeDeposit("sig-2", "UnknownWallet999", 10.0))
|
||||
time.sleep(0.4)
|
||||
assert ledger.snapshot()["clients"] == {}
|
||||
|
||||
|
||||
def test_binding_replicates_via_events():
|
||||
a = BillingLedger(starting_credit=0.0)
|
||||
b = BillingLedger(starting_credit=0.0)
|
||||
a.bind_wallet("key-9", "WalletNine")
|
||||
a.credit_deposit("key-9", 3.0, "sig-9")
|
||||
events, _ = a.events_since(0)
|
||||
b.apply_events(events)
|
||||
assert b.api_key_for_wallet("WalletNine") == "key-9"
|
||||
assert b.get_client_balance("key-9") == pytest.approx(3.0)
|
||||
# deposit event id embeds the signature → replicated replay is a no-op
|
||||
assert b.apply_events(events) == 0
|
||||
|
||||
|
||||
def test_solana_adapter_derives_treasury_accounts():
|
||||
"""Adapter smoke test without any RPC round-trip."""
|
||||
pytest.importorskip("solders")
|
||||
token_instructions = pytest.importorskip("spl.token.instructions")
|
||||
if not hasattr(token_instructions, "InitializeMintParams"):
|
||||
pytest.skip("installed spl.token lacks InitializeMintParams")
|
||||
from solders.keypair import Keypair
|
||||
|
||||
from meshnet_contracts.solana_adapter import SolanaCustodialTreasury
|
||||
|
||||
keypair = Keypair()
|
||||
treasury = SolanaCustodialTreasury(
|
||||
"http://127.0.0.1:8899",
|
||||
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", # real USDT mint address shape
|
||||
keypair,
|
||||
)
|
||||
assert treasury.treasury_wallet == str(keypair.pubkey())
|
||||
assert treasury.treasury_token_account # deterministic ATA derivation
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
shutil.which("solana-test-validator") is None,
|
||||
reason="solana-test-validator not installed",
|
||||
)
|
||||
def test_mint_deposit_credit_flow_against_local_validator(tmp_path):
|
||||
"""Full mint → deposit → credit flow on a local validator (US-032)."""
|
||||
import subprocess
|
||||
|
||||
from solders.keypair import Keypair
|
||||
|
||||
from meshnet_contracts.solana_adapter import RpcClient, SolanaCustodialTreasury
|
||||
|
||||
rpc_url = "http://127.0.0.1:8899"
|
||||
ledger_dir = tmp_path / "ledger"
|
||||
validator = subprocess.Popen(
|
||||
["solana-test-validator", "--ledger", str(ledger_dir), "--quiet",
|
||||
"--rpc-port", "8899", "--reset"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
try:
|
||||
assert _wait_for(lambda: _rpc_alive(RpcClient(rpc_url)), timeout=60)
|
||||
|
||||
treasury_kp, client_kp = Keypair(), Keypair()
|
||||
bootstrap = SolanaCustodialTreasury(
|
||||
rpc_url, "11111111111111111111111111111111", treasury_kp,
|
||||
)
|
||||
client_bootstrap = SolanaCustodialTreasury(
|
||||
rpc_url, "11111111111111111111111111111111", client_kp,
|
||||
)
|
||||
bootstrap.request_airdrop(2.0)
|
||||
client_bootstrap.request_airdrop(2.0)
|
||||
assert _wait_for(lambda: bootstrap.get_sol_balance() > 0, timeout=30)
|
||||
assert _wait_for(lambda: client_bootstrap.get_sol_balance() > 0, timeout=30)
|
||||
|
||||
treasury, mint = bootstrap.create_mock_usdt_mint()
|
||||
assert _wait_for(
|
||||
lambda: treasury._account_exists( # noqa: SLF001 — test-only poke
|
||||
__import__("solders.pubkey", fromlist=["Pubkey"]).Pubkey.from_string(mint)
|
||||
),
|
||||
timeout=30,
|
||||
)
|
||||
treasury.ensure_token_account(treasury.treasury_wallet)
|
||||
treasury.mint_mock_usdt(str(client_kp.pubkey()), 50.0)
|
||||
|
||||
# the client depositing into the treasury is just a transfer the
|
||||
# client signs — reuse send_payouts from an adapter bound to their key
|
||||
client_side = SolanaCustodialTreasury(rpc_url, mint, client_kp)
|
||||
assert _wait_for(
|
||||
lambda: _try_send(client_side, treasury.treasury_wallet, 25.0), timeout=30,
|
||||
)
|
||||
|
||||
ledger = BillingLedger(starting_credit=0.0)
|
||||
ledger.bind_wallet("api-key-e2e", str(client_kp.pubkey()))
|
||||
|
||||
def _credited():
|
||||
for dep in treasury.list_new_deposits(
|
||||
lambda sig: ledger.has_event(f"deposit-{sig}")
|
||||
):
|
||||
key = ledger.api_key_for_wallet(dep.sender_wallet)
|
||||
if key:
|
||||
ledger.credit_deposit(key, dep.amount_usdt, dep.signature)
|
||||
return ledger.get_client_balance("api-key-e2e") > 0
|
||||
|
||||
assert _wait_for(_credited, timeout=60)
|
||||
assert ledger.get_client_balance("api-key-e2e") == pytest.approx(25.0)
|
||||
finally:
|
||||
validator.terminate()
|
||||
validator.wait(timeout=10)
|
||||
|
||||
|
||||
def _try_send(adapter, wallet: str, amount: float) -> bool:
|
||||
try:
|
||||
adapter.send_payouts([(wallet, amount)])
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _rpc_alive(rpc) -> bool:
|
||||
try:
|
||||
return rpc.call("getHealth") == "ok"
|
||||
except Exception:
|
||||
return False
|
||||
261
tests/test_forfeiture_penalty.py
Normal file
261
tests/test_forfeiture_penalty.py
Normal file
@@ -0,0 +1,261 @@
|
||||
"""US-034: hardened proof-of-work — pending-balance forfeiture penalty.
|
||||
|
||||
The pending balance is the fraud collateral (ADR-0015): a confirmed divergence
|
||||
forfeits it in full to the protocol cut alongside the strike; three strikes ban
|
||||
the wallet, whose remaining pending is never paid out.
|
||||
"""
|
||||
|
||||
import json
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
import pytest
|
||||
|
||||
from meshnet_contracts import LocalSolanaContracts
|
||||
from meshnet_node.server import StubNodeServer
|
||||
from meshnet_tracker.billing import BillingLedger
|
||||
from meshnet_tracker.server import TrackerServer
|
||||
from meshnet_validator import ValidatorProcess
|
||||
|
||||
MODEL = "stub-model"
|
||||
|
||||
|
||||
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:
|
||||
"""What the reference stub answers for the probe messages."""
|
||||
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_divergence_forfeits_pending_and_strikes(reference_node):
|
||||
contracts = LocalSolanaContracts()
|
||||
contracts.registry.submit_stake("wallet-bad", 500)
|
||||
ledger = BillingLedger(starting_credit=10.0, default_price_per_1k=0.02)
|
||||
ledger.charge_request("client", MODEL, 1000, [("wallet-bad", 12)])
|
||||
pending_before = ledger.get_node_pending("wallet-bad")
|
||||
assert pending_before > 0
|
||||
|
||||
validator = ValidatorProcess(
|
||||
contracts=contracts,
|
||||
billing=ledger,
|
||||
reference_node_url=reference_node,
|
||||
sample_rate=1.0,
|
||||
slash_amount=100,
|
||||
random_seed=7,
|
||||
)
|
||||
_record_event(contracts, "sess-1", "fraudulent nonsense", "wallet-bad")
|
||||
receipts = validator.validate_once()
|
||||
|
||||
assert len(receipts) == 1
|
||||
assert ledger.get_node_pending("wallet-bad") == pytest.approx(0.0)
|
||||
snapshot = ledger.snapshot()
|
||||
# forfeited amount landed in the protocol cut on top of the original 10%
|
||||
assert snapshot["protocol_cut"] == pytest.approx(0.02 * 0.10 + pending_before)
|
||||
assert contracts.registry.get_wallet("wallet-bad").strike_count == 1
|
||||
|
||||
|
||||
def test_matching_output_forfeits_nothing(reference_node):
|
||||
contracts = LocalSolanaContracts()
|
||||
contracts.registry.submit_stake("wallet-good", 500)
|
||||
ledger = BillingLedger(starting_credit=10.0, default_price_per_1k=0.02)
|
||||
ledger.charge_request("client", MODEL, 1000, [("wallet-good", 12)])
|
||||
pending_before = ledger.get_node_pending("wallet-good")
|
||||
|
||||
validator = ValidatorProcess(
|
||||
contracts=contracts,
|
||||
billing=ledger,
|
||||
reference_node_url=reference_node,
|
||||
sample_rate=1.0,
|
||||
random_seed=7,
|
||||
)
|
||||
_record_event(contracts, "sess-1", _reference_output(reference_node), "wallet-good")
|
||||
receipts = validator.validate_once()
|
||||
|
||||
assert receipts == []
|
||||
assert ledger.get_node_pending("wallet-good") == pytest.approx(pending_before)
|
||||
assert contracts.registry.get_wallet("wallet-good").strike_count == 0
|
||||
|
||||
|
||||
def test_three_strikes_bans_and_bad_node_loses_everything(reference_node):
|
||||
"""Deliberately-bad node: every job is fraudulent, checks always sample.
|
||||
|
||||
Earn → caught → forfeit, three times over; the third strike bans the
|
||||
wallet, and the tracker rejects its registration.
|
||||
"""
|
||||
contracts = LocalSolanaContracts()
|
||||
contracts.registry.submit_stake("wallet-bad", 500)
|
||||
ledger = BillingLedger(starting_credit=10.0, default_price_per_1k=0.02)
|
||||
validator = ValidatorProcess(
|
||||
contracts=contracts,
|
||||
billing=ledger,
|
||||
reference_node_url=reference_node,
|
||||
sample_rate=1.0,
|
||||
strike_threshold=3,
|
||||
random_seed=7,
|
||||
)
|
||||
|
||||
for i in range(3):
|
||||
ledger.charge_request("client", MODEL, 1000, [("wallet-bad", 12)])
|
||||
_record_event(contracts, f"sess-{i}", f"fraud-{i}", "wallet-bad")
|
||||
validator.validate_once()
|
||||
|
||||
wallet = contracts.registry.get_wallet("wallet-bad")
|
||||
assert wallet.strike_count == 3
|
||||
assert wallet.banned
|
||||
assert ledger.get_node_pending("wallet-bad") == pytest.approx(0.0)
|
||||
|
||||
# banned wallet cannot register with the tracker
|
||||
tracker = TrackerServer(contracts=contracts)
|
||||
port = tracker.start()
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
f"http://127.0.0.1:{port}/v1/nodes/register",
|
||||
data=json.dumps({
|
||||
"endpoint": "http://127.0.0.1:9",
|
||||
"shard_start": 0,
|
||||
"shard_end": 31,
|
||||
"hardware_profile": {},
|
||||
"wallet_address": "wallet-bad",
|
||||
"score": 1.0,
|
||||
}).encode(),
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
with pytest.raises(urllib.error.HTTPError) as exc_info:
|
||||
urllib.request.urlopen(req)
|
||||
assert exc_info.value.code == 403
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_forfeit_endpoint_requires_auth_and_forfeits():
|
||||
contracts = LocalSolanaContracts()
|
||||
ledger = BillingLedger(starting_credit=10.0, default_price_per_1k=0.02)
|
||||
ledger.charge_request("client", MODEL, 1000, [("wallet-x", 12)])
|
||||
pending = ledger.get_node_pending("wallet-x")
|
||||
|
||||
tracker = TrackerServer(contracts=contracts, billing=ledger)
|
||||
port = tracker.start()
|
||||
url = f"http://127.0.0.1:{port}/v1/billing/forfeit"
|
||||
body = json.dumps({"wallet": "wallet-x", "reason": "fraud"}).encode()
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
url, data=body, headers={"Content-Type": "application/json"}, method="POST",
|
||||
)
|
||||
with pytest.raises(urllib.error.HTTPError) as exc_info:
|
||||
urllib.request.urlopen(req)
|
||||
assert exc_info.value.code == 401
|
||||
|
||||
req = urllib.request.Request(
|
||||
url, data=body,
|
||||
headers={"Content-Type": "application/json", "Authorization": "Bearer validator"},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req) as r:
|
||||
result = json.loads(r.read())
|
||||
assert result["forfeited"] == pytest.approx(pending)
|
||||
assert result["strike_count"] == 1
|
||||
assert result["banned"] is False
|
||||
assert ledger.get_node_pending("wallet-x") == pytest.approx(0.0)
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_probation_earns_nothing_then_earning_begins():
|
||||
"""First N jobs accrue no pending balance; job N+1 earns (issue 08)."""
|
||||
contracts = LocalSolanaContracts(probationary_job_count=2)
|
||||
ledger = BillingLedger(starting_credit=10.0, default_price_per_1k=0.02)
|
||||
|
||||
tracker = TrackerServer(
|
||||
model_presets={
|
||||
MODEL: {
|
||||
"layers_start": 0,
|
||||
"layers_end": 11,
|
||||
"bytes_per_layer": {"bfloat16": 30 * 1024 * 1024},
|
||||
}
|
||||
},
|
||||
contracts=contracts,
|
||||
billing=ledger,
|
||||
)
|
||||
port = tracker.start()
|
||||
tracker_url = f"http://127.0.0.1:{port}"
|
||||
|
||||
stub = StubNodeServer(
|
||||
shard_start=0, shard_end=11, is_last_shard=True, model=MODEL, tracker_mode=True,
|
||||
)
|
||||
stub_port = stub.start()
|
||||
reg = json.dumps({
|
||||
"endpoint": f"http://127.0.0.1:{stub_port}",
|
||||
"shard_start": 0,
|
||||
"shard_end": 11,
|
||||
"model": MODEL,
|
||||
"hardware_profile": {},
|
||||
"score": 1.0,
|
||||
"tracker_mode": True,
|
||||
"wallet_address": "wallet-new",
|
||||
}).encode()
|
||||
|
||||
def _chat():
|
||||
req = urllib.request.Request(
|
||||
f"{tracker_url}/v1/chat/completions",
|
||||
data=json.dumps({
|
||||
"model": MODEL,
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
}).encode(),
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer probation-client",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req) as r:
|
||||
r.read()
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
f"{tracker_url}/v1/nodes/register",
|
||||
data=reg,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req) as r:
|
||||
r.read()
|
||||
|
||||
_chat() # job 1: probation
|
||||
_chat() # job 2: probation
|
||||
assert ledger.get_node_pending("wallet-new") == pytest.approx(0.0)
|
||||
assert contracts.registry.probationary_jobs_remaining("wallet-new") == 0
|
||||
|
||||
_chat() # job 3: earning begins (tokens are 0 in the stub, so the
|
||||
# charge event exists but the amount is 0 — assert via event log)
|
||||
events, _ = ledger.events_since(0)
|
||||
charges = [e for e in events if e["type"] == "charge"]
|
||||
assert len(charges) == 3
|
||||
assert charges[0]["shares"] == {} and charges[1]["shares"] == {}
|
||||
assert "wallet-new" in charges[2]["shares"]
|
||||
finally:
|
||||
stub.stop()
|
||||
tracker.stop()
|
||||
154
tests/test_manual_route_benchmark.py
Normal file
154
tests/test_manual_route_benchmark.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""US-030: manual route selection + hop-penalty benchmarking.
|
||||
|
||||
Pinned "route" in the chat body overrides auto-selection; the privileged
|
||||
benchmark endpoint fans the same prompt through 1/2/3-node routes and appends
|
||||
per-hop latency records to benchmark_results.json.
|
||||
"""
|
||||
|
||||
import json
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
import pytest
|
||||
|
||||
from meshnet_node.server import StubNodeServer
|
||||
from meshnet_tracker.server import TrackerServer
|
||||
|
||||
MODEL = "openai-community/gpt2"
|
||||
|
||||
|
||||
def _post_json(url: str, payload: dict, headers: dict | None = None) -> dict:
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=json.dumps(payload).encode(),
|
||||
headers={"Content-Type": "application/json", **(headers or {})},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def benchmark_setup(tmp_path):
|
||||
"""Tracker + one full-coverage node and one 0-5/6-11 pair, with node ids."""
|
||||
results_path = str(tmp_path / "benchmark_results.json")
|
||||
tracker = TrackerServer(
|
||||
model_presets={
|
||||
MODEL: {
|
||||
"layers_start": 0,
|
||||
"layers_end": 11,
|
||||
"bytes_per_layer": {"bfloat16": 30 * 1024 * 1024},
|
||||
}
|
||||
},
|
||||
benchmark_results_path=results_path,
|
||||
)
|
||||
port = tracker.start()
|
||||
tracker_url = f"http://127.0.0.1:{port}"
|
||||
|
||||
stubs = []
|
||||
node_ids = {}
|
||||
for name, (start, end, prefix, last) in {
|
||||
"full": (0, 11, "full:", True),
|
||||
"head": (0, 5, "head:", False),
|
||||
"tail": (6, 11, "tail:", True),
|
||||
}.items():
|
||||
stub = StubNodeServer(
|
||||
shard_start=start, shard_end=end, is_last_shard=last,
|
||||
response_prefix=prefix, model=MODEL, tracker_mode=(start == 0),
|
||||
)
|
||||
sport = stub.start()
|
||||
stubs.append(stub)
|
||||
reply = _post_json(f"{tracker_url}/v1/nodes/register", {
|
||||
"endpoint": f"http://127.0.0.1:{sport}",
|
||||
"shard_start": start,
|
||||
"shard_end": end,
|
||||
"model": MODEL,
|
||||
"hardware_profile": {},
|
||||
"score": 1.0,
|
||||
"tracker_mode": start == 0,
|
||||
"node_id": f"node-{name}",
|
||||
})
|
||||
node_ids[name] = reply.get("node_id", f"node-{name}")
|
||||
|
||||
yield tracker_url, node_ids, results_path
|
||||
|
||||
for stub in stubs:
|
||||
stub.stop()
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def _chat(tracker_url: str, route: list[str] | None = None) -> dict:
|
||||
body: dict = {
|
||||
"model": MODEL,
|
||||
"messages": [{"role": "user", "content": "ping"}],
|
||||
}
|
||||
if route is not None:
|
||||
body["route"] = route
|
||||
return _post_json(f"{tracker_url}/v1/chat/completions", body)
|
||||
|
||||
|
||||
def test_pinned_route_uses_named_node(benchmark_setup):
|
||||
tracker_url, node_ids, _ = benchmark_setup
|
||||
reply = _chat(tracker_url, route=[node_ids["full"]])
|
||||
content = reply["choices"][0]["message"]["content"]
|
||||
assert content.startswith("full:")
|
||||
|
||||
|
||||
def test_unknown_route_node_is_400(benchmark_setup):
|
||||
tracker_url, _, _ = benchmark_setup
|
||||
with pytest.raises(urllib.error.HTTPError) as exc_info:
|
||||
_chat(tracker_url, route=["no-such-node"])
|
||||
assert exc_info.value.code == 400
|
||||
detail = json.loads(exc_info.value.read())
|
||||
assert "no-such-node" in detail["error"]["message"]
|
||||
|
||||
|
||||
def test_invalid_route_shape_is_400(benchmark_setup):
|
||||
tracker_url, _, _ = benchmark_setup
|
||||
with pytest.raises(urllib.error.HTTPError) as exc_info:
|
||||
_chat(tracker_url, route=[])
|
||||
assert exc_info.value.code == 400
|
||||
|
||||
|
||||
def test_clients_without_route_are_unaffected(benchmark_setup):
|
||||
tracker_url, _, _ = benchmark_setup
|
||||
reply = _chat(tracker_url)
|
||||
assert reply["choices"][0]["message"]["content"]
|
||||
|
||||
|
||||
def test_benchmark_requires_auth(benchmark_setup):
|
||||
tracker_url, _, _ = benchmark_setup
|
||||
with pytest.raises(urllib.error.HTTPError) as exc_info:
|
||||
_post_json(f"{tracker_url}/v1/benchmark/hop-penalty", {"model": MODEL})
|
||||
assert exc_info.value.code == 401
|
||||
with pytest.raises(urllib.error.HTTPError) as exc_info:
|
||||
urllib.request.urlopen(f"{tracker_url}/v1/benchmark/results")
|
||||
assert exc_info.value.code == 401
|
||||
|
||||
|
||||
def test_benchmark_records_one_and_two_node_routes(benchmark_setup):
|
||||
tracker_url, _, results_path = benchmark_setup
|
||||
record = _post_json(
|
||||
f"{tracker_url}/v1/benchmark/hop-penalty",
|
||||
{"model": MODEL, "prompt": "2+2", "max_new_tokens": 8},
|
||||
headers={"Authorization": "Bearer bench"},
|
||||
)
|
||||
by_hops = {len(r["route"]): r for r in record["routes"] if "total_ms" in r}
|
||||
assert 1 in by_hops and 2 in by_hops # 3-node coverage impossible → skipped
|
||||
assert 3 not in by_hops
|
||||
assert len(by_hops[1]["per_hop_ms"]) == 1
|
||||
assert len(by_hops[2]["per_hop_ms"]) == 2
|
||||
assert by_hops[2]["total_ms"] > 0
|
||||
|
||||
stored = json.loads(open(results_path, encoding="utf-8").read())
|
||||
assert isinstance(stored, list) and len(stored) == 1
|
||||
assert stored[0]["model"] == MODEL
|
||||
assert stored[0]["prompt_hash"]
|
||||
|
||||
fetched = json.loads(
|
||||
urllib.request.urlopen(urllib.request.Request(
|
||||
f"{tracker_url}/v1/benchmark/results",
|
||||
headers={"Authorization": "Bearer bench"},
|
||||
)).read()
|
||||
)
|
||||
assert len(fetched["results"]) == 1
|
||||
169
tests/test_settlement_loop.py
Normal file
169
tests/test_settlement_loop.py
Normal file
@@ -0,0 +1,169 @@
|
||||
"""US-033: leader-only settlement loop with batched USDT payouts.
|
||||
|
||||
Mining-pool trigger (threshold OR max-period, dust floor), pending debited
|
||||
before the transaction is sent, unconfirmed batches resent by settlement id
|
||||
(never double-paying), banned wallets skipped, history queryable over HTTP.
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
import pytest
|
||||
|
||||
from meshnet_contracts import LocalSolanaContracts
|
||||
from meshnet_tracker.billing import BillingLedger
|
||||
from meshnet_tracker.server import TrackerServer
|
||||
|
||||
MODEL = "stub-model"
|
||||
|
||||
|
||||
class _FakePayoutTreasury:
|
||||
"""Records payout batches; can be told to fail the next N sends."""
|
||||
|
||||
def __init__(self):
|
||||
self.batches: list[list[tuple[str, float]]] = []
|
||||
self.fail_next = 0
|
||||
self._counter = 0
|
||||
|
||||
def list_new_deposits(self, is_seen, *, limit=200):
|
||||
return []
|
||||
|
||||
def send_payouts(self, payouts):
|
||||
if self.fail_next > 0:
|
||||
self.fail_next -= 1
|
||||
raise RuntimeError("rpc timeout (simulated)")
|
||||
self.batches.append(list(payouts))
|
||||
self._counter += 1
|
||||
return f"fake-tx-{self._counter}"
|
||||
|
||||
|
||||
def _wait_for(predicate, timeout=5.0):
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
if predicate():
|
||||
return True
|
||||
time.sleep(0.05)
|
||||
return False
|
||||
|
||||
|
||||
def _make_tracker(ledger, treasury, *, threshold=0.01, period=3600.0,
|
||||
dust=0.001, contracts=None, cluster_peers=None):
|
||||
return TrackerServer(
|
||||
billing=ledger,
|
||||
treasury=treasury,
|
||||
contracts=contracts,
|
||||
deposit_poll_interval=30.0,
|
||||
settle_period=period,
|
||||
payout_threshold=threshold,
|
||||
payout_dust_floor=dust,
|
||||
settlement_check_interval=0.05,
|
||||
cluster_peers=cluster_peers,
|
||||
)
|
||||
|
||||
|
||||
def test_threshold_triggers_payout_and_zeroes_pending():
|
||||
ledger = BillingLedger(starting_credit=10.0, default_price_per_1k=0.02)
|
||||
ledger.charge_request("client", MODEL, 1000, [("wallet-a", 12)]) # 0.018 pending
|
||||
treasury = _FakePayoutTreasury()
|
||||
tracker = _make_tracker(ledger, treasury, threshold=0.01)
|
||||
port = tracker.start()
|
||||
try:
|
||||
assert _wait_for(lambda: treasury.batches)
|
||||
assert treasury.batches[0] == [("wallet-a", pytest.approx(0.018))]
|
||||
assert ledger.get_node_pending("wallet-a") == pytest.approx(0.0)
|
||||
|
||||
history = json.loads(urllib.request.urlopen(
|
||||
f"http://127.0.0.1:{port}/v1/billing/settlements"
|
||||
).read())["settlements"]
|
||||
assert len(history) == 1
|
||||
assert history[0]["signature"] == "fake-tx-1"
|
||||
assert history[0]["payouts"] == [
|
||||
{"wallet": "wallet-a", "amount": pytest.approx(0.018)}
|
||||
]
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_period_triggers_payout_below_threshold():
|
||||
ledger = BillingLedger(starting_credit=10.0, default_price_per_1k=0.02)
|
||||
ledger.charge_request("client", MODEL, 1000, [("wallet-a", 12)])
|
||||
treasury = _FakePayoutTreasury()
|
||||
tracker = _make_tracker(ledger, treasury, threshold=100.0, period=0.2)
|
||||
tracker.start()
|
||||
try:
|
||||
assert _wait_for(lambda: treasury.batches)
|
||||
assert ledger.get_node_pending("wallet-a") == pytest.approx(0.0)
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_dust_floor_blocks_tiny_payouts():
|
||||
ledger = BillingLedger(starting_credit=10.0, default_price_per_1k=0.02)
|
||||
ledger.charge_request("client", MODEL, 10, [("wallet-a", 12)]) # 0.00018 pending
|
||||
treasury = _FakePayoutTreasury()
|
||||
tracker = _make_tracker(ledger, treasury, threshold=0.0, period=0.1, dust=0.001)
|
||||
tracker.start()
|
||||
try:
|
||||
time.sleep(0.5)
|
||||
assert treasury.batches == []
|
||||
assert ledger.get_node_pending("wallet-a") > 0
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_failed_transaction_retries_without_double_pay():
|
||||
ledger = BillingLedger(starting_credit=10.0, default_price_per_1k=0.02)
|
||||
ledger.charge_request("client", MODEL, 1000, [("wallet-a", 12)])
|
||||
treasury = _FakePayoutTreasury()
|
||||
treasury.fail_next = 3
|
||||
tracker = _make_tracker(ledger, treasury, threshold=0.01)
|
||||
tracker.start()
|
||||
try:
|
||||
assert _wait_for(lambda: treasury.batches)
|
||||
time.sleep(0.3) # give the loop room to (incorrectly) double-send
|
||||
assert len(treasury.batches) == 1
|
||||
assert treasury.batches[0] == [("wallet-a", pytest.approx(0.018))]
|
||||
assert ledger.get_node_pending("wallet-a") == pytest.approx(0.0)
|
||||
history = ledger.settlement_history()
|
||||
assert len(history) == 1 # one settlement id across all retries
|
||||
assert history[0]["signature"] == "fake-tx-1"
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_banned_wallet_is_never_paid():
|
||||
contracts = LocalSolanaContracts()
|
||||
contracts.registry.ban_wallet("wallet-banned")
|
||||
ledger = BillingLedger(starting_credit=10.0, default_price_per_1k=0.02)
|
||||
ledger.charge_request("client", MODEL, 1000, [("wallet-banned", 6), ("wallet-ok", 6)])
|
||||
treasury = _FakePayoutTreasury()
|
||||
tracker = _make_tracker(ledger, treasury, threshold=0.001, contracts=contracts)
|
||||
tracker.start()
|
||||
try:
|
||||
assert _wait_for(lambda: treasury.batches)
|
||||
time.sleep(0.3)
|
||||
paid_wallets = {w for batch in treasury.batches for w, _ in batch}
|
||||
assert paid_wallets == {"wallet-ok"}
|
||||
assert ledger.get_node_pending("wallet-banned") > 0 # held, never paid
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_non_leader_never_signs():
|
||||
"""A tracker in a cluster whose peers are unreachable never wins an
|
||||
election, so it must never send a payout transaction."""
|
||||
ledger = BillingLedger(starting_credit=10.0, default_price_per_1k=0.02)
|
||||
ledger.charge_request("client", MODEL, 1000, [("wallet-a", 12)])
|
||||
treasury = _FakePayoutTreasury()
|
||||
tracker = _make_tracker(
|
||||
ledger, treasury, threshold=0.001,
|
||||
cluster_peers=["http://127.0.0.1:1", "http://127.0.0.1:2"],
|
||||
)
|
||||
tracker.start()
|
||||
try:
|
||||
time.sleep(0.8)
|
||||
assert treasury.batches == []
|
||||
assert ledger.get_node_pending("wallet-a") > 0
|
||||
finally:
|
||||
tracker.stop()
|
||||
Reference in New Issue
Block a user