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).
106 lines
4.7 KiB
Python
106 lines
4.7 KiB
Python
"""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"]
|