"""Reputation-weighted, budget-balanced audit sampling (ADR-0018 §1, §6-7). The flat ``sample_rate`` on ``ValidatorProcess`` treats every wallet the same. This module scores each wallet's audit probability from its tenure (``completed_job_count``) and reputation -- newcomers and low-reputation wallets get sampled far more than veterans in good standing, who float down to a floor -- while a running budget balance keeps the *realized* fleet-wide average anchored to a configured target even as the wallet population mix drifts (research §6, §8 layers 2-4). """ from __future__ import annotations import random from dataclasses import dataclass @dataclass(frozen=True) class AuditRateConfig: """Tunable audit-rate policy. Defaults match ADR-0018 §1/§6.""" target_rate: float = 0.05 newcomer_rate: float = 0.25 veteran_floor: float = 0.02 probation_jobs: int = 50 veteran_jobs: int = 500 tripwire_multiplier: float = 3.0 # Bounds the correction the budget balancer can apply in one step so a # short burst of skewed traffic can't swing everyone's rate wildly. max_budget_scale: float = 20.0 def __post_init__(self) -> None: if not 0.0 <= self.target_rate <= 1.0: raise ValueError("target_rate must be between 0 and 1") if not 0.0 <= self.veteran_floor <= self.newcomer_rate <= 1.0: raise ValueError("veteran_floor must be <= newcomer_rate, both within [0, 1]") if self.probation_jobs < 0 or self.veteran_jobs <= self.probation_jobs: raise ValueError("veteran_jobs must be greater than probation_jobs") if self.tripwire_multiplier < 1.0: raise ValueError("tripwire_multiplier must be >= 1.0") class AdaptiveAuditSampler: """Per-wallet audit probability + fleet-wide budget balancer. ``wallet_base_rate`` scores tenure and reputation into a rate between ``veteran_floor`` and ``newcomer_rate``. ``should_audit`` scales that rate by a running budget-balance factor (target_rate / historical mean base rate) so the fleet-wide realized audit rate tracks ``target_rate`` even when the wallet mix is skewed toward veterans or newcomers, then applies the tripwire multiplier to *that single decision only* -- a tripwire flag never touches the shared budget-balance history, so it can't punish other wallets' audit rate. """ def __init__(self, config: AuditRateConfig | None = None, *, random_seed: int | None = None) -> None: self.config = config or AuditRateConfig() self._random = random.Random(random_seed) self._base_rate_total = 0.0 self._decisions = 0 def wallet_base_rate(self, *, completed_job_count: int, reputation: float) -> float: cfg = self.config if completed_job_count <= cfg.probation_jobs: tenure_rate = cfg.newcomer_rate elif completed_job_count >= cfg.veteran_jobs: tenure_rate = cfg.veteran_floor else: span = cfg.veteran_jobs - cfg.probation_jobs frac = (completed_job_count - cfg.probation_jobs) / span tenure_rate = cfg.newcomer_rate + (cfg.veteran_floor - cfg.newcomer_rate) * frac reputation_gap = max(0.0, min(1.0, 1.0 - reputation)) reputation_rate = cfg.veteran_floor + reputation_gap * (cfg.newcomer_rate - cfg.veteran_floor) return max(tenure_rate, reputation_rate, cfg.veteran_floor) def sample_rate_for(self, *, completed_job_count: int, reputation: float, tripwire: bool = False) -> float: """Preview the effective audit probability without recording a decision.""" base_rate = self.wallet_base_rate(completed_job_count=completed_job_count, reputation=reputation) return self._effective_rate(base_rate, tripwire=tripwire) def should_audit(self, *, completed_job_count: int, reputation: float, tripwire: bool = False) -> bool: base_rate = self.wallet_base_rate(completed_job_count=completed_job_count, reputation=reputation) effective_rate = self._effective_rate(base_rate, tripwire=tripwire) self._base_rate_total += base_rate self._decisions += 1 return self._random.random() < effective_rate def _effective_rate(self, base_rate: float, *, tripwire: bool) -> float: rate = base_rate * self._budget_scale() if tripwire: rate *= self.config.tripwire_multiplier return max(self.config.veteran_floor, min(1.0, rate)) def _budget_scale(self) -> float: if self._decisions == 0: return 1.0 mean_base_rate = self._base_rate_total / self._decisions if mean_base_rate <= 0: return 1.0 return min(self.config.max_budget_scale, self.config.target_rate / mean_base_rate) __all__ = ["AuditRateConfig", "AdaptiveAuditSampler"]