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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user