"""Optimistic fraud validator for completed inference requests.""" import json import math import random import threading import time import urllib.request from typing import Any __version__ = "0.1.0" class ValidatorProcess: """Separate validator loop that samples completed requests and submits slashes.""" def __init__( self, *, contracts: Any, reference_node_url: str, sample_rate: float = 0.05, tolerance: float = 1e-6, slash_amount: int = 100, strike_threshold: int = 3, random_seed: int | None = None, webhook_url: str | None = None, interval_seconds: float = 1.0, billing: Any | None = None, ) -> None: if not 0.0 <= sample_rate <= 1.0: raise ValueError("sample_rate must be between 0 and 1") if tolerance < 0: raise ValueError("tolerance must be non-negative") if slash_amount <= 0: raise ValueError("slash_amount must be positive") if strike_threshold <= 0: raise ValueError("strike_threshold must be positive") if interval_seconds <= 0: raise ValueError("interval_seconds must be positive") self._contracts = contracts self._billing = billing self._reference_node_url = reference_node_url.rstrip("/") self._sample_rate = sample_rate self._tolerance = tolerance self._slash_amount = slash_amount self._strike_threshold = strike_threshold self._webhook_url = webhook_url self._interval_seconds = interval_seconds self._random = random.Random(random_seed) self._last_event_index = -1 self._running = False self._thread: threading.Thread | None = None self.sampled_count = 0 def validate_once(self) -> list[Any]: """Run one validation cycle and return slash receipts submitted this cycle.""" receipts: list[Any] = [] events = self._contracts.validation.list_completed_inferences( after_index=self._last_event_index, ) for event in events: self._last_event_index = max(self._last_event_index, event.index) if self._random.random() >= self._sample_rate: continue self.sampled_count += 1 reference_output = self._run_reference(event.messages) if _outputs_match(event.observed_output, reference_output, self._tolerance): continue receipts.extend(self._slash_route(event.route_nodes, event.observed_output, reference_output)) return receipts def start(self) -> None: if self._running: raise RuntimeError("ValidatorProcess is already running") self._running = True self._thread = threading.Thread(target=self._run_loop, daemon=True) self._thread.start() def stop(self) -> None: self._running = False if self._thread is not None: self._thread.join(timeout=2) self._thread = None def _run_loop(self) -> None: while self._running: self.validate_once() time.sleep(self._interval_seconds) def _run_reference(self, messages: list[dict]) -> str: response = _post_json( f"{self._reference_node_url}/v1/infer", {"messages": messages}, ) text = response.get("text") if not isinstance(text, str): raise ValueError("reference node response did not contain text") return text def _slash_route( self, route_nodes: list[dict], observed_output: str, reference_output: str, ) -> list[Any]: receipts: list[Any] = [] node = _final_text_node(route_nodes) wallet_address = node.get("wallet_address") if node else None if not wallet_address: return receipts if self._contracts.registry.get_wallet(wallet_address).banned: return receipts receipts.append(self._contracts.registry.submit_slash_proof( wallet_address=wallet_address, slash_amount=self._slash_amount, strike_threshold=self._strike_threshold, reason=( "reference output diverged " f"(observed={observed_output!r}, reference={reference_output!r})" ), webhook_url=self._webhook_url, )) # ADR-0015: the pending balance is the collateral — forfeit it in the # same validation cycle as the strike. if self._billing is not None: forfeit = self._billing.forfeit_pending(wallet_address, reason="fraud-divergence") print( f"[validator] forfeited pending balance of {wallet_address}: " f"{forfeit['amount']:.6f} USDT (fraud-divergence)", flush=True, ) return receipts def _final_text_node(route_nodes: list[dict]) -> dict | None: if not route_nodes: return None return max(route_nodes, key=lambda node: int(node.get("shard_end", 0))) def _outputs_match(observed: str, reference: str, tolerance: float) -> bool: observed_float = _parse_float(observed) reference_float = _parse_float(reference) if observed_float is not None and reference_float is not None: return math.isclose(observed_float, reference_float, rel_tol=tolerance, abs_tol=tolerance) return observed == reference def _parse_float(value: str) -> float | None: try: return float(value) except ValueError: return None def _post_json(url: str, payload: dict, timeout: float = 5.0) -> dict: data = json.dumps(payload).encode() req = urllib.request.Request( url, data=data, headers={"Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(req, timeout=timeout) as response: return json.loads(response.read()) __all__ = ["ValidatorProcess"]