feat(alpha): complete hardening backlog

Complete the alpha-hardening Ralph task set, including tracker billing/accounting guards, validator fraud-audit primitives, wallet binding proof support, documentation runbooks, and updated tests.

Verification: .venv/bin/python -m compileall -q packages tests; .venv/bin/python -m pytest -q --tb=short (313 passed, 3 skipped, 1 failed: tests/test_mining_cli.py::test_legacy_start_without_port_uses_next_available_port because meshnet-node pid 1263451 is already listening on port 7000).
This commit is contained in:
Dobromir Popov
2026-07-05 21:47:23 +03:00
parent c967e5cfc4
commit 9abe83b5f4
45 changed files with 4095 additions and 774 deletions

View File

@@ -124,8 +124,8 @@ def _call(url, method="GET", body=None, token=None):
@pytest.fixture
def account_tracker():
ledger = BillingLedger(starting_credit=0.5, default_price_per_1k=0.02)
def account_tracker():
ledger = BillingLedger(starting_credit=0.0, default_price_per_1k=0.02)
tracker = TrackerServer(billing=ledger, accounts=AccountStore(), hive_secret=HIVE_SECRET)
port = tracker.start()
yield f"http://127.0.0.1:{port}", ledger
@@ -145,8 +145,7 @@ def test_register_login_and_account_view(account_tracker):
me = _call(f"{url}/v1/account", token=login["session_token"])
assert me["account"]["email"] == "admin@example.com"
assert me["api_keys"] == [reg["api_key"]]
# registration granted Caller Credit on the ledger
assert me["total_balance"] == pytest.approx(0.5)
assert me["total_balance"] == pytest.approx(0.0)
assert me["usage"]["requests"] == 0

View File

@@ -0,0 +1,193 @@
"""AH-009 (FRAUD: reputation-weighted routing + adaptive audit rate).
Covers the audit-sampling half of the issue: per-wallet audit probability as
a function of tenure/reputation, a fleet-wide budget balancer that keeps the
realized audit rate anchored to a configured target, and passive tripwire
escalation (ADR-0018 §1, §6-7).
"""
import random
import pytest
from meshnet_contracts import LocalSolanaContracts
from meshnet_node.server import StubNodeServer
from meshnet_validator import AdaptiveAuditSampler, AuditRateConfig, ValidatorProcess, detect_output_tripwire
MODEL = "stub-model"
# ---- per-wallet base rate: newcomer high, veteran low, floor >= 2% ----
def test_newcomer_gets_elevated_audit_rate():
sampler = AdaptiveAuditSampler()
rate = sampler.wallet_base_rate(completed_job_count=0, reputation=1.0)
assert 0.20 <= rate <= 0.30
def test_veteran_in_good_standing_floors_near_target():
sampler = AdaptiveAuditSampler()
rate = sampler.wallet_base_rate(completed_job_count=1000, reputation=1.0)
assert rate == pytest.approx(0.02)
def test_veteran_rate_never_drops_below_floor():
sampler = AdaptiveAuditSampler(AuditRateConfig(veteran_floor=0.02))
rate = sampler.wallet_base_rate(completed_job_count=10_000, reputation=1.0)
assert rate >= 0.02
def test_low_reputation_wallet_sampled_more_than_high_reputation_wallet():
"""Red (test-first item 1): a uniform sampler ignores reputation. A
low-reputation wallet must get a higher rate than a high-reputation one
with the same tenure."""
sampler = AdaptiveAuditSampler()
low_rep_rate = sampler.sample_rate_for(completed_job_count=200, reputation=0.1)
high_rep_rate = sampler.sample_rate_for(completed_job_count=200, reputation=1.0)
assert low_rep_rate > high_rep_rate
def test_low_reputation_escalates_even_for_a_tenured_wallet():
sampler = AdaptiveAuditSampler()
rate = sampler.wallet_base_rate(completed_job_count=1000, reputation=0.0)
assert rate == pytest.approx(sampler.config.newcomer_rate)
# ---- fleet-wide budget balance ----
def test_fleet_wide_audit_rate_tracks_configured_target_within_one_point():
"""Test-first item 2: over >=1000 requests with a fixed seed and a mixed
wallet population, the measured fleet audit rate lands within +-1.0
percentage point of the configured 5% target."""
sampler = AdaptiveAuditSampler(random_seed=1234)
rng = random.Random(99)
audited = 0
total = 6000
for _ in range(total):
roll = rng.random()
if roll < 0.7:
wallet = dict(completed_job_count=800, reputation=1.0) # veteran
elif roll < 0.9:
wallet = dict(completed_job_count=150, reputation=0.9) # mid-tenure
else:
wallet = dict(completed_job_count=0, reputation=1.0) # newcomer
if sampler.should_audit(**wallet):
audited += 1
measured_rate = audited / total
assert abs(measured_rate - sampler.config.target_rate) <= 0.01
def test_fleet_wide_audit_rate_respects_custom_target():
sampler = AdaptiveAuditSampler(AuditRateConfig(target_rate=0.10), random_seed=42)
audited = sum(
1
for _ in range(2000)
if sampler.should_audit(completed_job_count=800, reputation=1.0)
)
measured_rate = audited / 2000
assert abs(measured_rate - 0.10) <= 0.01
def test_sampling_is_deterministic_for_a_fixed_seed():
sampler_a = AdaptiveAuditSampler(random_seed=7)
sampler_b = AdaptiveAuditSampler(random_seed=7)
decisions_a = [sampler_a.should_audit(completed_job_count=0, reputation=1.0) for _ in range(200)]
decisions_b = [sampler_b.should_audit(completed_job_count=0, reputation=1.0) for _ in range(200)]
assert decisions_a == decisions_b
# ---- passive tripwires bump rate only ----
def test_tripwire_flag_bumps_audit_rate_for_that_wallet():
sampler = AdaptiveAuditSampler()
normal_rate = sampler.sample_rate_for(completed_job_count=800, reputation=1.0, tripwire=False)
flagged_rate = sampler.sample_rate_for(completed_job_count=800, reputation=1.0, tripwire=True)
assert flagged_rate > normal_rate
def test_tripwire_does_not_change_other_wallets_rate():
"""A tripwire hit must never leak the multiplier into the shared
budget-balance history -- only the wallet's un-boosted base rate is
recorded, so a flagged decision affects the running budget scale exactly
like a plain decision for the same wallet would, and never inflates or
depresses everyone else's rate on top of that."""
flagged = AdaptiveAuditSampler(random_seed=5)
flagged.should_audit(completed_job_count=800, reputation=1.0, tripwire=True)
plain = AdaptiveAuditSampler(random_seed=5)
plain.should_audit(completed_job_count=800, reputation=1.0, tripwire=False)
after_flagged = flagged.sample_rate_for(completed_job_count=800, reputation=1.0)
after_plain = plain.sample_rate_for(completed_job_count=800, reputation=1.0)
assert after_flagged == pytest.approx(after_plain)
def test_detect_output_tripwire_flags_repetition_loop():
degenerate = " ".join(["loop"] * 20)
assert detect_output_tripwire(degenerate) is True
def test_detect_output_tripwire_flags_empty_output():
assert detect_output_tripwire("") is True
def test_detect_output_tripwire_passes_normal_prose():
normal = "The quick brown fox jumps over the lazy dog near the riverbank."
assert detect_output_tripwire(normal) is False
# ---- ValidatorProcess wiring: reputation-weighted sampling end to end ----
@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 _record_event(contracts, session_id: str, wallet: str) -> None:
contracts.validation.record_completed_inference(
session_id=session_id,
model=MODEL,
messages=[{"role": "user", "content": "2+2"}],
observed_output="4",
route_nodes=[{"wallet_address": wallet, "shard_end": 31}],
)
def test_validator_uses_audit_sampler_when_configured(reference_node):
"""A flagged low-reputation wallet gets audited far more often than a
veteran in good standing when routed through the same validator."""
contracts = LocalSolanaContracts()
contracts.registry.record_completed_jobs("wallet-veteran", 800)
for _ in range(9):
contracts.registry.record_audit_outcome("wallet-shady", passed=False) # reputation -> 0.0
trials = 400
shady_audits = 0
veteran_audits = 0
for i in range(trials):
sampler = AdaptiveAuditSampler(random_seed=i)
validator = ValidatorProcess(
contracts=contracts,
reference_node_url=reference_node,
audit_sampler=sampler,
random_seed=i,
)
shady_audits += int(validator._should_sample(_FakeEvent("wallet-shady")))
veteran_audits += int(validator._should_sample(_FakeEvent("wallet-veteran")))
assert shady_audits > veteran_audits
class _FakeEvent:
def __init__(self, wallet: str) -> None:
self.route_nodes = [{"wallet_address": wallet, "shard_end": 31}]
self.observed_output = "4"

View File

@@ -1,8 +1,8 @@
"""US-031: billing ledger — per-token pricing, 90/10 split, pending balances.
Unit tests for BillingLedger math/persistence/replication, plus HTTP
integration: 401 without an API key, billed 200 with one, 402 once the
Caller Credit is exhausted (rejected before routing — no free work).
integration: 401 without an API key, 402 for unfunded keys, billed 200 after
explicit credit, and 402 once the balance is exhausted.
"""
import http.server
@@ -15,8 +15,14 @@ import urllib.request
import pytest
from meshnet_tracker.auth import sign_hive_request
from meshnet_tracker.billing import BillingLedger
from meshnet_tracker.server import TrackerServer
from meshnet_tracker.billing import DEFAULT_STARTING_CREDIT, BillingLedger
from meshnet_tracker.server import (
TrackerServer,
_billable_non_stream_tokens,
_billable_stream_tokens,
_estimate_prompt_tokens,
_observed_stream_tokens,
)
MODEL = "openai-community/gpt2"
HIVE_SECRET = "test-hive-secret"
@@ -35,6 +41,16 @@ def test_charge_single_node_gets_90_percent():
assert ledger.snapshot()["protocol_cut"] == pytest.approx(0.02 * 0.10)
def test_default_starting_credit_is_zero_and_fresh_key_is_unfunded():
ledger = BillingLedger(default_price_per_1k=0.02)
assert DEFAULT_STARTING_CREDIT == 0.0
assert ledger.ensure_client("fresh-key") == pytest.approx(0.0)
assert ledger.get_client_balance("fresh-key") == pytest.approx(0.0)
assert ledger.has_funds("fresh-key") is False
assert ledger.events_since(0)[0] == []
def test_charge_three_node_split_by_work_units():
ledger = BillingLedger(starting_credit=1.0, default_price_per_1k=0.02)
ledger.ensure_client("key-a")
@@ -75,6 +91,28 @@ def test_per_model_price_override():
assert event["cost"] == pytest.approx(0.02 * 500 / 1000)
def test_non_stream_billable_tokens_cap_usage_by_request_bound():
payload = {"usage": {"total_tokens": 1_000_000}}
request = {"messages": [{"role": "user", "content": "hello tracker"}], "max_tokens": 3}
assert _estimate_prompt_tokens(request) == 2
assert _billable_non_stream_tokens(payload, request) == 5
def test_stream_billable_tokens_allow_usage_to_lower_observed_count_only():
observed_payload = {
"choices": [{
"index": 0,
"delta": {"content": "hello tracker"},
"finish_reason": None,
}]
}
assert _observed_stream_tokens(observed_payload) == 2
assert _billable_stream_tokens(observed_tokens=2, reported_tokens=1_000_000) == 2
assert _billable_stream_tokens(observed_tokens=2, reported_tokens=1) == 1
def test_payout_and_forfeit_hooks():
ledger = BillingLedger(starting_credit=1.0, default_price_per_1k=0.02)
ledger.charge_request("key-a", MODEL, 1000, [("wallet-1", 1)])
@@ -155,8 +193,17 @@ def test_event_replication_converges_and_dedupes():
class _UsageStubNode:
"""Minimal head node returning a chat completion with real usage numbers."""
def __init__(self, total_tokens: int = 1000):
def __init__(
self,
total_tokens: int = 1000,
*,
stream_chunks: list[str] | None = None,
stream_usage_total: int | None = None,
):
self.total_tokens = total_tokens
self.stream_chunks = stream_chunks or []
self.stream_usage_total = stream_usage_total
self.request_count = 0
outer = self
class Handler(http.server.BaseHTTPRequestHandler):
@@ -164,8 +211,44 @@ class _UsageStubNode:
pass
def do_POST(self):
outer.request_count += 1
length = int(self.headers.get("Content-Length", 0))
self.rfile.read(length)
raw_body = self.rfile.read(length)
try:
request_body = json.loads(raw_body)
except json.JSONDecodeError:
request_body = {}
if request_body.get("stream"):
self.send_response(200)
self.send_header("Content-Type", "text/event-stream; charset=utf-8")
self.end_headers()
for chunk in outer.stream_chunks:
payload = json.dumps({
"id": "chatcmpl-stub",
"object": "chat.completion.chunk",
"model": MODEL,
"choices": [{
"index": 0,
"delta": {"content": chunk},
"finish_reason": None,
}],
}).encode()
self.wfile.write(b"data: " + payload + b"\n\n")
if outer.stream_usage_total is not None:
usage_payload = json.dumps({
"id": "chatcmpl-stub",
"object": "chat.completion.chunk",
"model": MODEL,
"choices": [],
"usage": {
"prompt_tokens": 0,
"completion_tokens": outer.stream_usage_total,
"total_tokens": outer.stream_usage_total,
},
}).encode()
self.wfile.write(b"data: " + usage_payload + b"\n\n")
self.wfile.write(b"data: [DONE]\n\n")
return
body = json.dumps({
"id": "chatcmpl-stub",
"object": "chat.completion",
@@ -203,7 +286,7 @@ class _UsageStubNode:
@pytest.fixture
def billed_tracker():
ledger = BillingLedger(starting_credit=0.03, default_price_per_1k=0.02)
ledger = BillingLedger(starting_credit=0.0, default_price_per_1k=0.02)
tracker = TrackerServer(
model_presets={
MODEL: {
@@ -239,17 +322,19 @@ def billed_tracker():
with urllib.request.urlopen(req) as r:
r.read()
yield tracker_url, ledger
yield tracker_url, ledger, stub
stub.stop()
tracker.stop()
def _chat(tracker_url: str, api_key: str | None):
data = json.dumps({
def _chat(tracker_url: str, api_key: str | None, **body_overrides):
body = {
"model": MODEL,
"messages": [{"role": "user", "content": "hi"}],
}).encode()
}
body.update(body_overrides)
data = json.dumps(body).encode()
headers = {"Content-Type": "application/json"}
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
@@ -260,20 +345,32 @@ def _chat(tracker_url: str, api_key: str | None):
method="POST",
)
with urllib.request.urlopen(req) as r:
if body.get("stream"):
return r.read().decode()
return json.loads(r.read())
def test_proxy_chat_requires_api_key_when_billing_enabled(billed_tracker):
tracker_url, _ = billed_tracker
tracker_url, _, _ = billed_tracker
with pytest.raises(urllib.error.HTTPError) as exc_info:
_chat(tracker_url, api_key=None)
assert exc_info.value.code == 401
def test_proxy_chat_bills_client_and_credits_node(billed_tracker):
tracker_url, ledger = billed_tracker
def test_proxy_chat_402_for_fresh_key_before_routing(billed_tracker):
tracker_url, ledger, stub = billed_tracker
with pytest.raises(urllib.error.HTTPError) as exc_info:
_chat(tracker_url, api_key="fresh-client")
assert exc_info.value.code == 402
assert ledger.get_client_balance("fresh-client") == pytest.approx(0.0)
assert stub.request_count == 0
def test_proxy_chat_bills_credited_client_and_credits_node(billed_tracker):
tracker_url, ledger, _ = billed_tracker
ledger.credit_client("client-1", 0.03, note="admin-credit")
_chat(tracker_url, api_key="client-1")
# 1000 tokens at 0.02/1K = 0.02 USDT; starting credit 0.03
# 1000 tokens at 0.02/1K = 0.02 USDT; explicit credit 0.03
assert ledger.get_client_balance("client-1") == pytest.approx(0.03 - 0.02)
assert ledger.get_node_pending("wallet-head") == pytest.approx(0.02 * 0.90)
@@ -289,8 +386,128 @@ def test_proxy_chat_bills_client_and_credits_node(billed_tracker):
assert node_stats["sample_count_last_hour"] == 1
def test_proxy_chat_caps_inflated_non_streaming_usage_by_request_bounds(billed_tracker):
tracker_url, ledger, stub = billed_tracker
stub.total_tokens = 1_000_000
ledger.credit_client("bounded-client", 100.0, note="admin-credit")
_chat(tracker_url, api_key="bounded-client", max_tokens=2)
# messages=[{"content": "hi"}] has a local prompt estimate of one token, so
# billable total is capped at max_tokens + prompt estimate, not node usage.
expected_tokens = 3
assert ledger.get_client_balance("bounded-client") == pytest.approx(100.0 - 0.02 * expected_tokens / 1000)
events, _ = ledger.events_since(0)
charge = next(event for event in events if event["type"] == "charge")
assert charge["total_tokens"] == expected_tokens
def test_proxy_chat_caps_inflated_streaming_usage_by_observed_chunks():
ledger = BillingLedger(starting_credit=0.0, default_price_per_1k=0.02)
ledger.credit_client("stream-client", 1.0, note="admin-credit")
tracker = TrackerServer(
model_presets={
MODEL: {
"layers_start": 0,
"layers_end": 11,
"bytes_per_layer": {"bfloat16": 30 * 1024 * 1024},
}
},
billing=ledger,
)
port = tracker.start()
tracker_url = f"http://127.0.0.1:{port}"
stub = _UsageStubNode(stream_chunks=["one", "two"], stream_usage_total=1_000_000)
stub_port = stub.start()
try:
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-head",
}).encode()
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(tracker_url, api_key="stream-client", stream=True, max_tokens=2)
events, _ = ledger.events_since(0)
charge = next(event for event in events if event["type"] == "charge")
assert charge["total_tokens"] == 2
assert ledger.get_client_balance("stream-client") == pytest.approx(1.0 - 0.02 * 2 / 1000)
finally:
stub.stop()
tracker.stop()
def test_proxy_chat_splits_payout_by_tracker_assigned_route_span():
ledger = BillingLedger(starting_credit=0.0, default_price_per_1k=0.02)
ledger.credit_client("route-client", 1.0, note="admin-credit")
tracker = TrackerServer(
model_presets={
MODEL: {
"total_layers": 12,
"bytes_per_layer": {"bfloat16": 1_000},
}
},
billing=ledger,
)
port = tracker.start()
tracker_url = f"http://127.0.0.1:{port}"
head = _UsageStubNode(total_tokens=1000)
head_port = head.start()
try:
for endpoint, wallet, vram, bench in (
(f"http://127.0.0.1:{head_port}", "wallet-head", 5_000, 2.0),
("http://127.0.0.1:1", "wallet-tail", 10_000, 1.0),
):
reg = json.dumps({
"endpoint": endpoint,
"model": MODEL,
"vram_bytes": vram,
"ram_bytes": 10_000,
"quantizations": ["bfloat16"],
"benchmark_tokens_per_sec": bench,
"hardware_profile": {},
"score": 1.0,
"tracker_mode": endpoint.endswith(str(head_port)),
"wallet_address": wallet,
"managed_assignment": True,
"shard_start": 0,
"shard_end": 999,
}).encode()
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(tracker_url, api_key="route-client", max_tokens=1000)
pool = 0.02 * 0.90
assert ledger.get_node_pending("wallet-head") == pytest.approx(pool * 4 / 12)
assert ledger.get_node_pending("wallet-tail") == pytest.approx(pool * 8 / 12)
finally:
head.stop()
tracker.stop()
def test_proxy_chat_402_when_balance_exhausted(billed_tracker):
tracker_url, ledger = billed_tracker
tracker_url, ledger, _ = billed_tracker
ledger.credit_client("client-2", 0.03, note="admin-credit")
_chat(tracker_url, api_key="client-2") # 0.03 -> 0.01
_chat(tracker_url, api_key="client-2") # 0.01 -> -0.01 (post-pay drift)
assert ledger.get_client_balance("client-2") == pytest.approx(-0.01)
@@ -301,8 +518,59 @@ def test_proxy_chat_402_when_balance_exhausted(billed_tracker):
assert ledger.get_client_balance("client-2") == pytest.approx(-0.01)
def test_proxy_chat_rejects_request_above_spend_cap_before_routing():
ledger = BillingLedger(starting_credit=0.0, default_price_per_1k=0.02)
ledger.credit_client("capped-client", 10.0, note="admin-credit")
tracker = TrackerServer(
model_presets={
MODEL: {
"layers_start": 0,
"layers_end": 11,
"bytes_per_layer": {"bfloat16": 30 * 1024 * 1024},
}
},
billing=ledger,
max_charge_per_request=0.01,
)
port = tracker.start()
tracker_url = f"http://127.0.0.1:{port}"
stub = _UsageStubNode(total_tokens=1000)
stub_port = stub.start()
try:
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-head",
}).encode()
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()
with pytest.raises(urllib.error.HTTPError) as exc_info:
_chat(tracker_url, api_key="capped-client", max_tokens=1000)
body = json.loads(exc_info.value.read())
assert exc_info.value.code == 402
assert body["error"]["code"] == "spend_cap_exceeded"
assert "max_charge_per_request" in body["error"]["message"]
assert ledger.get_client_balance("capped-client") == pytest.approx(10.0)
assert stub.request_count == 0
finally:
stub.stop()
tracker.stop()
def test_billing_gossip_endpoint_applies_events(billed_tracker):
tracker_url, ledger = billed_tracker
tracker_url, ledger, _ = billed_tracker
peer = BillingLedger(starting_credit=0.03, default_price_per_1k=0.02)
peer.credit_client("remote-client", 7.0)
events, _ = peer.events_since(0)
@@ -315,6 +583,5 @@ def test_billing_gossip_endpoint_applies_events(billed_tracker):
)
with urllib.request.urlopen(req) as r:
assert json.loads(r.read())["applied"] == len(events)
# only the replicated credit event lands here — Caller Credit was granted
# on the peer that first saw the key, and its event replicates separately
# only the replicated credit event lands here.
assert ledger.get_client_balance("remote-client") == pytest.approx(7.0)

View File

@@ -13,9 +13,24 @@ import urllib.error
import urllib.request
import pytest
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from meshnet_tracker.billing import BillingLedger
from meshnet_tracker.server import TrackerServer
from meshnet_tracker.wallet_proof import binding_message
def _keypair():
"""Deterministic-enough local ed25519 keypair + base58 address for tests."""
priv = Ed25519PrivateKey.generate()
from meshnet_node.wallet import _b58encode
public = priv.public_key().public_bytes_raw()
return priv, _b58encode(public)
def _sign(priv: Ed25519PrivateKey, api_key: str, wallet: str) -> str:
return priv.sign(binding_message(api_key, wallet)).hex()
class _FakeDeposit:
@@ -78,14 +93,15 @@ def test_wallet_register_requires_auth(watched_tracker):
def test_deposit_credits_bound_api_key_exactly_once(watched_tracker):
tracker_url, ledger, treasury = watched_tracker
priv, wallet = _keypair()
reply = _post_json(
f"{tracker_url}/v1/wallet/register",
{"wallet": "So1anaWa11et111"},
{"wallet": wallet, "signature": _sign(priv, "client-key-1", wallet)},
headers={"Authorization": "Bearer client-key-1"},
)
assert reply["bound"] is True
treasury.deposits.append(_FakeDeposit("sig-1", "So1anaWa11et111", 25.0))
treasury.deposits.append(_FakeDeposit("sig-1", wallet, 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)

View File

@@ -15,7 +15,8 @@ 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
from meshnet_validator import ToplocAuditConfig, ValidatorProcess
from meshnet_validator.audit import build_activation_proofs
MODEL = "stub-model"
@@ -261,3 +262,142 @@ def test_probation_earns_nothing_then_earning_begins():
finally:
stub.stop()
tracker.stop()
class _FakeToploc:
"""Fingerprint = the activations themselves; verify is exact equality
(same contract as tests/test_hop_bisection.py's fake backend)."""
def build_proofs_base64(self, activations, *, decode_batching_size, topk, skip_prefill):
return {
"activation_fingerprint": tuple(tuple(row) for row in activations),
"decode_batching_size": decode_batching_size,
"topk": topk,
"skip_prefill": skip_prefill,
}
def verify_proofs_base64(self, activations, proofs, *, decode_batching_size, topk, skip_prefill):
return proofs == {
"activation_fingerprint": tuple(tuple(row) for row in activations),
"decode_batching_size": decode_batching_size,
"topk": topk,
"skip_prefill": skip_prefill,
}
class _HopReferenceValidator(ValidatorProcess):
"""Stands in for the reference node: returns canned ground-truth
activations at each hop cut-point instead of making an HTTP call."""
def __init__(self, *args, reference_activations_by_hop, **kwargs) -> None:
super().__init__(*args, **kwargs)
self._reference_activations_by_hop = reference_activations_by_hop
def _run_teacher_forced_prefill_hops(self, **kwargs):
return self._reference_activations_by_hop
def _record_two_hop_event(
contracts, session_id: str, *, hop0_activations, hop1_activations, config, backend,
) -> None:
"""Two-hop route (wallet-hop0 upstream, wallet-hop1 downstream), each hop
carrying its own on-demand TOPLOC commitment (AH-007 bisection format)."""
hop0_claim = build_activation_proofs(hop0_activations, config=config, backend=backend)
hop1_claim = build_activation_proofs(hop1_activations, config=config, backend=backend)
token_ids = [101, 202, 303]
contracts.validation.record_completed_inference(
session_id=session_id,
model=MODEL,
messages=[{"role": "user", "content": "2+2"}],
observed_output="two-hop pipeline output",
route_nodes=[
{
"wallet_address": "wallet-hop0",
"shard_start": 0,
"shard_end": 15,
"toploc_proof": hop0_claim.as_mapping(),
"claimed_token_ids": token_ids,
},
{
"wallet_address": "wallet-hop1",
"shard_start": 16,
"shard_end": 31,
"toploc_proof": hop1_claim.as_mapping(),
"claimed_token_ids": token_ids,
},
],
)
def test_60_request_stream_bans_intermittent_first_hop_cheater_not_last_hop():
"""Integration (AH-010): a 60-request stream through a two-hop route where
the *first* hop (not the last) cheats on 3 of the jobs. TOPLOC bisection
(issue 07) must blame wallet-hop0 specifically -- the old last-hop-only
heuristic would have blamed the innocent wallet-hop1 instead. Each catch
forfeits wallet-hop0's pending balance and strikes it in the same
validation cycle; the third strike bans it within the 60-request stream,
and the settlement loop's payables() then excludes only the banned wallet
while the honest downstream hop keeps earning (ADR-0015/ADR-0018).
"""
config = ToplocAuditConfig(topk=2, decode_batching_size=16)
backend = _FakeToploc()
reference_hop0 = [[1.0, 2.0], [3.0, 4.0]]
reference_hop1 = [[5.0, 6.0], [7.0, 8.0]]
corrupted_hop0 = [[9.0, 9.0], [9.0, 9.0]]
cheat_at = {20, 40, 60}
contracts = LocalSolanaContracts()
contracts.registry.submit_stake("wallet-hop0", 500)
contracts.registry.submit_stake("wallet-hop1", 500)
ledger = BillingLedger(starting_credit=1000.0, default_price_per_1k=0.02)
validator = _HopReferenceValidator(
contracts=contracts,
billing=ledger,
reference_node_url="http://reference.invalid",
sample_rate=1.0,
strike_threshold=3,
random_seed=7,
toploc_config=config,
toploc_backend=backend,
reference_activations_by_hop=[reference_hop0, reference_hop1],
)
banned_at: int | None = None
for i in range(1, 61):
ledger.charge_request(
"client", MODEL, 1000, [("wallet-hop0", 6), ("wallet-hop1", 6)],
)
_record_two_hop_event(
contracts,
f"sess-{i}",
hop0_activations=corrupted_hop0 if i in cheat_at else reference_hop0,
hop1_activations=reference_hop1,
config=config,
backend=backend,
)
validator.validate_once()
if banned_at is None and contracts.registry.get_wallet("wallet-hop0").banned:
banned_at = i
assert banned_at is not None and banned_at <= 60
cheater = contracts.registry.get_wallet("wallet-hop0")
honest = contracts.registry.get_wallet("wallet-hop1")
assert cheater.strike_count == 3
assert cheater.banned
assert not honest.banned
# Every catch blamed the true (first, non-last) culprit hop -- never the
# innocent downstream wallet.
assert ledger.get_node_pending("wallet-hop0") == pytest.approx(0.0)
assert ledger.get_node_pending("wallet-hop1") > 0.0
# Settlement-loop wiring (server.py `_settlement_loop`): banned wallets
# are excluded from payables even though their (forfeited-to-zero)
# balance would fail the dust floor anyway -- the honest hop is still due.
banned_wallets = {
wallet for wallet in ("wallet-hop0", "wallet-hop1")
if contracts.registry.get_wallet(wallet).banned
}
due = dict(ledger.payables(threshold=0.0, max_period=0.0, dust_floor=0.0, exclude=banned_wallets))
assert "wallet-hop0" not in due
assert due.get("wallet-hop1", 0.0) > 0.0

279
tests/test_hop_bisection.py Normal file
View File

@@ -0,0 +1,279 @@
"""AH-007: on-demand hop-boundary commitments + first-divergent-hop bisection.
`_final_text_node` (blame `max(shard_end)`) is wrong for multi-hop pipelines:
a corrupt early hop still produces a route whose *last* hop reported perfectly
honest activations. These tests build a two-hop route, corrupt one hop's
committed fingerprint, and assert the referee blames that hop specifically —
not always the last one.
"""
from __future__ import annotations
from types import SimpleNamespace
from meshnet_validator import ToplocAuditConfig, ValidatorProcess
from meshnet_validator.audit import build_activation_proofs
class FakeToploc:
"""Fingerprint = the activations themselves; verify is exact equality,
matching the encode/verify contract exercised in test_toploc_audit.py."""
def build_proofs_base64(self, activations, *, decode_batching_size, topk, skip_prefill):
return {
"activation_fingerprint": tuple(tuple(row) for row in activations),
"decode_batching_size": decode_batching_size,
"topk": topk,
"skip_prefill": skip_prefill,
}
def verify_proofs_base64(self, activations, proofs, *, decode_batching_size, topk, skip_prefill):
return proofs == {
"activation_fingerprint": tuple(tuple(row) for row in activations),
"decode_batching_size": decode_batching_size,
"topk": topk,
"skip_prefill": skip_prefill,
}
class FakeValidationLog:
def __init__(self, events) -> None:
self._events = events
def list_completed_inferences(self, *, after_index: int = -1):
return [event for event in self._events if event.index > after_index]
class FakeRegistry:
def __init__(self) -> None:
self.slashes: list[dict] = []
self.audit_outcomes: list[dict] = []
def get_wallet(self, wallet_address: str):
return SimpleNamespace(banned=False)
def submit_slash_proof(self, **kwargs):
self.slashes.append(kwargs)
return kwargs
def record_audit_outcome(self, wallet_address: str, *, passed: bool):
self.audit_outcomes.append({"wallet_address": wallet_address, "passed": passed})
class FakeContracts:
def __init__(self, events) -> None:
self.validation = FakeValidationLog(events)
self.registry = FakeRegistry()
def _hop_route_event(*, index, hop0_claim_activations, hop1_claim_activations, config, backend, ts=None):
"""Two-hop route where each hop carries its own TOPLOC commitment built
from the given (possibly corrupted) activations."""
hop0_claim = build_activation_proofs(hop0_claim_activations, config=config, backend=backend)
hop1_claim = build_activation_proofs(hop1_claim_activations, config=config, backend=backend)
route_nodes = [
{
"wallet_address": "wallet-hop0",
"shard_start": 0,
"shard_end": 15,
"toploc_proof": hop0_claim.as_mapping(),
},
{
"wallet_address": "wallet-hop1",
"shard_start": 16,
"shard_end": 31,
"toploc_proof": hop1_claim.as_mapping(),
},
]
event = SimpleNamespace(
index=index,
session_id=f"session-hop-{index}",
model="Qwen2.5-0.5B-Instruct",
messages=[{"role": "user", "content": "hello"}],
observed_output="two-hop pipeline output",
route_nodes=route_nodes,
claimed_token_ids=[101, 202, 303],
)
if ts is not None:
event.ts = ts
return event
class HopReferenceValidator(ValidatorProcess):
"""Stands in for the reference node: returns canned ground-truth
activations at each hop cut-point instead of making an HTTP call."""
def __init__(self, *args, reference_activations_by_hop, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.reference_activations_by_hop = reference_activations_by_hop
self.hops_calls: list[dict] = []
self.text_reference_calls: list[list[dict]] = []
def _run_teacher_forced_prefill_hops(self, **kwargs):
self.hops_calls.append(kwargs)
return self.reference_activations_by_hop
def _run_reference(self, messages: list[dict]) -> str:
self.text_reference_calls.append(messages)
return "two-hop pipeline output"
def test_bisection_blames_first_divergent_hop_not_last_hop():
"""Red: corrupt hop-0 only. The old `_final_text_node` bug blames
max(shard_end) == hop-1 (wallet-hop1), which is innocent here."""
config = ToplocAuditConfig(topk=2, decode_batching_size=16)
backend = FakeToploc()
reference_hop0 = [[1.0, 2.0], [3.0, 4.0]]
reference_hop1 = [[5.0, 6.0], [7.0, 8.0]]
corrupted_hop0_claim_activations = [[9.0, 9.0], [9.0, 9.0]] # diverges from reference_hop0
event = _hop_route_event(
index=0,
hop0_claim_activations=corrupted_hop0_claim_activations,
hop1_claim_activations=reference_hop1,
config=config,
backend=backend,
)
contracts = FakeContracts([event])
validator = HopReferenceValidator(
contracts=contracts,
reference_node_url="http://reference.invalid",
sample_rate=1.0,
random_seed=7,
toploc_config=config,
toploc_backend=backend,
reference_activations_by_hop=[reference_hop0, reference_hop1],
)
receipts = validator.validate_once()
assert len(receipts) == 1
assert contracts.registry.slashes[0]["wallet_address"] == "wallet-hop0"
assert "hop 0" in contracts.registry.slashes[0]["reason"]
def test_bisection_blames_the_actual_faulty_hop_when_it_is_the_second_hop():
"""Integration test: multi-hop pipeline, fault injected at a known
(non-first) hop — proves blame follows the real culprit, not a fixed index."""
config = ToplocAuditConfig(topk=2, decode_batching_size=16)
backend = FakeToploc()
reference_hop0 = [[1.0, 2.0], [3.0, 4.0]]
reference_hop1 = [[5.0, 6.0], [7.0, 8.0]]
corrupted_hop1_claim_activations = [[0.0, 0.0], [0.0, 0.0]]
event = _hop_route_event(
index=0,
hop0_claim_activations=reference_hop0,
hop1_claim_activations=corrupted_hop1_claim_activations,
config=config,
backend=backend,
)
contracts = FakeContracts([event])
validator = HopReferenceValidator(
contracts=contracts,
reference_node_url="http://reference.invalid",
sample_rate=1.0,
random_seed=7,
toploc_config=config,
toploc_backend=backend,
reference_activations_by_hop=[reference_hop0, reference_hop1],
)
receipts = validator.validate_once()
assert len(receipts) == 1
assert contracts.registry.slashes[0]["wallet_address"] == "wallet-hop1"
def test_honest_two_hop_route_is_not_slashed():
config = ToplocAuditConfig(topk=2, decode_batching_size=16)
backend = FakeToploc()
reference_hop0 = [[1.0, 2.0], [3.0, 4.0]]
reference_hop1 = [[5.0, 6.0], [7.0, 8.0]]
event = _hop_route_event(
index=0,
hop0_claim_activations=reference_hop0,
hop1_claim_activations=reference_hop1,
config=config,
backend=backend,
)
contracts = FakeContracts([event])
validator = HopReferenceValidator(
contracts=contracts,
reference_node_url="http://reference.invalid",
sample_rate=1.0,
random_seed=7,
toploc_config=config,
toploc_backend=backend,
reference_activations_by_hop=[reference_hop0, reference_hop1],
)
assert validator.validate_once() == []
assert contracts.registry.slashes == []
def test_expired_commitment_window_falls_back_to_text_only_audit():
"""ADR-0018 §3: nodes only retain boundary activations briefly. Once the
on-demand TTL has passed, bisection can't be verified — the validator must
fall back to the text-comparison path instead of erroring out."""
config = ToplocAuditConfig(topk=2, decode_batching_size=16, commitment_ttl_seconds=1.0)
backend = FakeToploc()
reference_hop0 = [[1.0, 2.0], [3.0, 4.0]]
reference_hop1 = [[5.0, 6.0], [7.0, 8.0]]
event = _hop_route_event(
index=0,
hop0_claim_activations=reference_hop0,
hop1_claim_activations=reference_hop1,
config=config,
backend=backend,
ts=0.0, # far outside any TTL relative to real wall-clock time
)
contracts = FakeContracts([event])
validator = HopReferenceValidator(
contracts=contracts,
reference_node_url="http://reference.invalid",
sample_rate=1.0,
random_seed=7,
toploc_config=config,
toploc_backend=backend,
reference_activations_by_hop=[reference_hop0, reference_hop1],
)
assert validator.validate_once() == []
assert validator.hops_calls == []
assert len(validator.text_reference_calls) == 1
def test_hop_commitments_are_not_requested_unless_the_event_is_audit_selected():
"""On-demand: the (expensive) per-hop commitment retrieval only happens
for events the tracker RNG actually selects for audit — sample_rate is
the selection gate, and a miss must not touch the reference node at all."""
config = ToplocAuditConfig(topk=2, decode_batching_size=16)
backend = FakeToploc()
reference_hop0 = [[1.0, 2.0], [3.0, 4.0]]
reference_hop1 = [[5.0, 6.0], [7.0, 8.0]]
event = _hop_route_event(
index=0,
hop0_claim_activations=reference_hop0,
hop1_claim_activations=reference_hop1,
config=config,
backend=backend,
)
contracts = FakeContracts([event])
validator = HopReferenceValidator(
contracts=contracts,
reference_node_url="http://reference.invalid",
sample_rate=0.0, # never selects any event for audit
random_seed=7,
toploc_config=config,
toploc_backend=backend,
reference_activations_by_hop=[reference_hop0, reference_hop1],
)
assert validator.validate_once() == []
assert validator.hops_calls == []
assert validator.text_reference_calls == []
assert validator.sampled_count == 0

View File

@@ -0,0 +1,227 @@
"""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():
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():
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():
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."""
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():
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():
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():
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():
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():
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():
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():
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):
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):
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

201
tests/test_toploc_audit.py Normal file
View File

@@ -0,0 +1,201 @@
"""AH-006: validator TOPLOC audit primitive."""
from __future__ import annotations
from types import SimpleNamespace
from meshnet_validator import ToplocAuditConfig, ValidatorProcess
from meshnet_validator.audit import build_activation_proofs, verify_activation_proofs
class FakeToploc:
def __init__(self) -> None:
self.build_calls: list[dict] = []
self.verify_calls: list[dict] = []
def build_proofs_base64(
self,
activations,
*,
decode_batching_size: int,
topk: int,
skip_prefill: bool,
):
self.build_calls.append({
"decode_batching_size": decode_batching_size,
"topk": topk,
"skip_prefill": skip_prefill,
})
return {
"activation_fingerprint": tuple(tuple(row) for row in activations),
"decode_batching_size": decode_batching_size,
"topk": topk,
"skip_prefill": skip_prefill,
}
def verify_proofs_base64(
self,
activations,
proofs,
*,
decode_batching_size: int,
topk: int,
skip_prefill: bool,
):
self.verify_calls.append({
"decode_batching_size": decode_batching_size,
"topk": topk,
"skip_prefill": skip_prefill,
})
return proofs == {
"activation_fingerprint": tuple(tuple(row) for row in activations),
"decode_batching_size": decode_batching_size,
"topk": topk,
"skip_prefill": skip_prefill,
}
class FakeValidationLog:
def __init__(self, events) -> None:
self._events = events
def list_completed_inferences(self, *, after_index: int = -1):
return [event for event in self._events if event.index > after_index]
class FakeRegistry:
def __init__(self) -> None:
self.slashes: list[dict] = []
self.audit_outcomes: list[dict] = []
def get_wallet(self, wallet_address: str):
return SimpleNamespace(banned=False)
def submit_slash_proof(self, **kwargs):
self.slashes.append(kwargs)
return kwargs
def record_audit_outcome(self, wallet_address: str, *, passed: bool):
self.audit_outcomes.append({"wallet_address": wallet_address, "passed": passed})
class FakeContracts:
def __init__(self, events) -> None:
self.validation = FakeValidationLog(events)
self.registry = FakeRegistry()
class TeacherForcedValidator(ValidatorProcess):
def __init__(self, *args, reference_activations, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.reference_activations = reference_activations
self.teacher_forced_calls: list[dict] = []
def _run_reference(self, messages: list[dict]) -> str:
raise AssertionError("TOPLOC audits must not free-generate reference text")
def _run_teacher_forced_prefill(self, **kwargs):
self.teacher_forced_calls.append(kwargs)
return self.reference_activations
def test_stub_activation_tensors_round_trip_through_toploc_proofs():
fake_toploc = FakeToploc()
activations = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]
config = ToplocAuditConfig(topk=2, decode_batching_size=16)
claim = build_activation_proofs(activations, config=config, backend=fake_toploc)
assert verify_activation_proofs(activations, claim, config=config, backend=fake_toploc) is True
assert fake_toploc.build_calls == [{
"decode_batching_size": 16,
"topk": 2,
"skip_prefill": True,
}]
assert fake_toploc.verify_calls == [{
"decode_batching_size": 16,
"topk": 2,
"skip_prefill": True,
}]
def test_validator_teacher_forces_claimed_tokens_for_toploc_audit():
fake_toploc = FakeToploc()
activations = [[0.25, 0.5], [0.75, 1.0]]
config = ToplocAuditConfig(topk=2, decode_batching_size=16)
claim = build_activation_proofs(activations, config=config, backend=fake_toploc)
event = SimpleNamespace(
index=0,
session_id="session-toploc-ok",
model="Qwen2.5-0.5B-Instruct",
messages=[{"role": "user", "content": "hello"}],
observed_output="honest but hardware-divergent text",
route_nodes=[{"wallet_address": "wallet-good", "shard_end": 31}],
claimed_token_ids=[101, 202, 303],
toploc_proof=claim.as_mapping(),
)
contracts = FakeContracts([event])
validator = TeacherForcedValidator(
contracts=contracts,
reference_node_url="http://reference.invalid",
sample_rate=1.0,
random_seed=7,
toploc_config=config,
toploc_backend=fake_toploc,
reference_activations=activations,
)
assert validator.validate_once() == []
assert contracts.registry.slashes == []
assert validator.teacher_forced_calls == [{
"model": "Qwen2.5-0.5B-Instruct",
"messages": [{"role": "user", "content": "hello"}],
"claimed_token_ids": [101, 202, 303],
"claim": claim,
}]
def test_validator_rejects_swapped_precision_toploc_claim():
fake_toploc = FakeToploc()
activations = [[0.25, 0.5], [0.75, 1.0]]
canonical = ToplocAuditConfig(
dtype="bfloat16",
quantization="bfloat16",
topk=2,
decode_batching_size=16,
)
swapped = ToplocAuditConfig(
dtype="bfloat16",
quantization="int8",
topk=2,
decode_batching_size=16,
)
claim = build_activation_proofs(activations, config=swapped, backend=fake_toploc)
event = SimpleNamespace(
index=0,
session_id="session-toploc-bad",
model="Qwen2.5-0.5B-Instruct",
messages=[{"role": "user", "content": "hello"}],
observed_output="looks plausible",
route_nodes=[{"wallet_address": "wallet-bad", "shard_end": 31}],
audit={
"claimed_token_ids": [101, 202, 303],
"toploc_proof": claim.as_mapping(),
},
)
contracts = FakeContracts([event])
validator = TeacherForcedValidator(
contracts=contracts,
reference_node_url="http://reference.invalid",
sample_rate=1.0,
random_seed=7,
toploc_config=canonical,
toploc_backend=fake_toploc,
reference_activations=activations,
)
receipts = validator.validate_once()
assert len(receipts) == 1
assert contracts.registry.slashes[0]["wallet_address"] == "wallet-bad"
assert "TOPLOC activation proof mismatch" in contracts.registry.slashes[0]["reason"]

View File

@@ -1592,14 +1592,14 @@ def test_stats_gossip_endpoint_merges_peer_slice():
# ---------------------------------------------------------------- US-027 / US-028 routing
def _make_node(node_id, shard_start, shard_end, bench=1.0, queue_depth=0):
def _make_node(node_id, shard_start, shard_end, bench=1.0, queue_depth=0, wallet_address=None):
"""Helper: build a _NodeEntry with minimal fields."""
from meshnet_tracker.server import _NodeEntry
n = _NodeEntry(
node_id=node_id, endpoint=f"http://fake:{9000 + shard_start}",
shard_start=shard_start, shard_end=shard_end,
model="m", shard_checksum=None, hardware_profile={},
wallet_address=None, score=1.0,
wallet_address=wallet_address, score=1.0,
benchmark_tokens_per_sec=bench,
)
n.queue_depth = queue_depth
@@ -1683,6 +1683,49 @@ def test_select_route_throughput_accounts_for_queue_depth():
assert route[0].node_id in ("busy", "idle")
# ---------------------------------------------------------------- AH-009 reputation-weighted routing
def test_select_route_prefers_higher_reputation_when_throughput_equal():
"""AH-009 test-first item 3: among candidates that advance coverage
equally with the same effective throughput, the higher-reputation
wallet's node wins the tiebreak (ADR-0018 §6 -- earnings scale with
tenure/standing)."""
from meshnet_contracts import LocalSolanaContracts
from meshnet_tracker.server import _select_route
contracts = LocalSolanaContracts()
contracts.registry.record_audit_outcome("wallet-veteran", passed=False) # 1.0 -> 0.7
# wallet-fresh keeps the default reputation of 1.0
veteran = _make_node("veteran", 0, 11, bench=10.0, wallet_address="wallet-veteran")
fresh = _make_node("fresh", 0, 11, bench=10.0, wallet_address="wallet-fresh")
route, err = _select_route([veteran, fresh], 0, 11, contracts=contracts)
assert err == ""
assert route[0].node_id == "fresh"
def test_select_route_reputation_never_overrides_coverage():
"""A lower-reputation node that covers more layers still wins -- the
reputation multiplier is a tiebreak among equal-coverage candidates,
never a substitute for coverage maximization."""
from meshnet_contracts import LocalSolanaContracts
from meshnet_tracker.server import _select_route
contracts = LocalSolanaContracts()
contracts.registry.record_audit_outcome("wallet-shady", passed=False)
for _ in range(9):
contracts.registry.record_audit_outcome("wallet-shady", passed=False) # reputation -> 0.0
wide_shady = _make_node("wide-shady", 0, 11, bench=1.0, wallet_address="wallet-shady")
narrow_good = _make_node("narrow-good", 0, 5, bench=1.0, wallet_address="wallet-good")
route, err = _select_route([wide_shady, narrow_good], 0, 11, contracts=contracts)
assert err == ""
assert [n.node_id for n in route] == ["wide-shady"]
def test_two_stub_nodes_complete_pipeline_via_tracker():
"""Integration: two StubNodeServer instances serving complementary shards
produce a full inference response through the tracker route."""

View File

@@ -0,0 +1,182 @@
"""C6: wallet binding requires proof of ownership, and rebinding is safe.
Any Bearer key could previously bind any wallet string with no proof of
ownership, and gossip `bind` events overwrote an existing binding directly.
These tests cover: signature-gated binding, rebind protection (with an
admin-override escape hatch), and gossip-safe conflict handling.
"""
import json
import urllib.error
import urllib.request
import pytest
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from meshnet_node.wallet import _b58encode
from meshnet_tracker.accounts import AccountStore
from meshnet_tracker.billing import BillingLedger
from meshnet_tracker.server import TrackerServer
from meshnet_tracker.wallet_proof import binding_message, verify_wallet_signature
HIVE_SECRET = "test-hive-secret"
def _keypair():
priv = Ed25519PrivateKey.generate()
return priv, _b58encode(priv.public_key().public_bytes_raw())
def _sign(priv, api_key: str, wallet: str) -> str:
return priv.sign(binding_message(api_key, wallet)).hex()
# ---------------------------------------------------------------- unit tests
def test_verify_wallet_signature_accepts_valid_and_rejects_forged():
priv, wallet = _keypair()
other_priv, _ = _keypair()
message = binding_message("client-key-1", wallet)
assert verify_wallet_signature(wallet, message, priv.sign(message).hex())
# signed by a different key entirely
assert not verify_wallet_signature(wallet, message, other_priv.sign(message).hex())
# signature for a different api_key can't be replayed
other_message = binding_message("client-key-2", wallet)
assert not verify_wallet_signature(wallet, other_message, priv.sign(message).hex())
assert not verify_wallet_signature(wallet, message, "not-hex")
def test_bind_wallet_rejects_conflicting_rebind_without_admin_override():
ledger = BillingLedger(starting_credit=0.0)
ledger.bind_wallet("key-1", "WalletA")
assert ledger.api_key_for_wallet("WalletA") == "key-1"
event = ledger.bind_wallet("key-2", "WalletA")
assert event.get("rejected") is True
assert ledger.api_key_for_wallet("WalletA") == "key-1" # unchanged
override = ledger.bind_wallet("key-2", "WalletA", admin_override=True)
assert not override.get("rejected")
assert ledger.api_key_for_wallet("WalletA") == "key-2"
def test_gossip_bind_event_cannot_overwrite_existing_binding():
"""A conflicting `bind` event applied via gossip must not clobber."""
leader = BillingLedger(starting_credit=0.0)
leader.bind_wallet("key-1", "WalletA")
follower = BillingLedger(starting_credit=0.0)
follower.bind_wallet("key-9", "WalletA") # follower already has its own binding
events, _ = leader.events_since(0)
applied = follower.apply_events(events)
assert applied == 1 # event was processed (and rejected), not dropped/ignored
# the follower's pre-existing binding is untouched by the conflicting gossip event
assert follower.api_key_for_wallet("WalletA") == "key-9"
# ---------------------------------------------------------- HTTP integration
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 tracker():
ledger = BillingLedger(starting_credit=0.0)
server = TrackerServer(billing=ledger, accounts=AccountStore(), hive_secret=HIVE_SECRET)
port = server.start()
yield f"http://127.0.0.1:{port}", ledger
server.stop()
def test_bind_without_signature_is_rejected(tracker):
url, ledger = tracker
_, wallet = _keypair()
with pytest.raises(urllib.error.HTTPError) as exc_info:
_post_json(
f"{url}/v1/wallet/register",
{"wallet": wallet},
headers={"Authorization": "Bearer client-key-1"},
)
assert exc_info.value.code == 400
assert ledger.api_key_for_wallet(wallet) is None
def test_bind_with_forged_signature_is_rejected(tracker):
url, ledger = tracker
priv, wallet = _keypair()
forger_priv, _ = _keypair()
with pytest.raises(urllib.error.HTTPError) as exc_info:
_post_json(
f"{url}/v1/wallet/register",
{"wallet": wallet, "signature": _sign(forger_priv, "client-key-1", wallet)},
headers={"Authorization": "Bearer client-key-1"},
)
assert exc_info.value.code == 401
assert ledger.api_key_for_wallet(wallet) is None
def test_valid_signature_binds_wallet(tracker):
url, ledger = tracker
priv, wallet = _keypair()
reply = _post_json(
f"{url}/v1/wallet/register",
{"wallet": wallet, "signature": _sign(priv, "client-key-1", wallet)},
headers={"Authorization": "Bearer client-key-1"},
)
assert reply == {"wallet": wallet, "bound": True}
assert ledger.api_key_for_wallet(wallet) == "client-key-1"
def test_second_key_cannot_steal_bound_wallet(tracker):
url, ledger = tracker
priv, wallet = _keypair()
_post_json(
f"{url}/v1/wallet/register",
{"wallet": wallet, "signature": _sign(priv, "client-key-1", wallet)},
headers={"Authorization": "Bearer client-key-1"},
)
# key-2 gets the wallet owner's genuine signature over *their* api_key,
# proving they control the private key — still must not steal the binding.
with pytest.raises(urllib.error.HTTPError) as exc_info:
_post_json(
f"{url}/v1/wallet/register",
{"wallet": wallet, "signature": _sign(priv, "client-key-2", wallet)},
headers={"Authorization": "Bearer client-key-2"},
)
assert exc_info.value.code == 409
assert ledger.api_key_for_wallet(wallet) == "client-key-1"
def test_admin_session_can_force_rebind(tracker):
url, ledger = tracker
priv, wallet = _keypair()
_post_json(
f"{url}/v1/wallet/register",
{"wallet": wallet, "signature": _sign(priv, "client-key-1", wallet)},
headers={"Authorization": "Bearer client-key-1"},
)
admin = _post_json(
f"{url}/v1/auth/register",
{"email": "admin@example.com", "password": "secret-123"},
)
reply = _post_json(
f"{url}/v1/wallet/register",
{"wallet": wallet, "api_key": "client-key-2"},
headers={"Authorization": f"Bearer {admin['session_token']}"},
)
assert reply == {"wallet": wallet, "bound": True}
assert ledger.api_key_for_wallet(wallet) == "client-key-2"