diff --git a/backend/services/infonet/markets/stalemate_burn.py b/backend/services/infonet/markets/stalemate_burn.py index 80c716a..2a85d5b 100644 --- a/backend/services/infonet/markets/stalemate_burn.py +++ b/backend/services/infonet/markets/stalemate_burn.py @@ -30,28 +30,43 @@ specific branches in ``resolution.py`` that match the spec's from __future__ import annotations -from typing import Iterable +import math +from typing import Any, Iterable from services.infonet.config import CONFIG +def _finite_float(value: Any) -> float | None: + try: + parsed = float(value) + except (TypeError, ValueError): + return None + return parsed if math.isfinite(parsed) else None + + def stalemate_burn_pct() -> float: """Current burn percentage from CONFIG. Helper so callers don't need to remember the key name.""" - return float(CONFIG["resolution_stalemate_burn_pct"]) + pct = _finite_float(CONFIG["resolution_stalemate_burn_pct"]) + if pct is None: + raise ValueError("resolution_stalemate_burn_pct must be finite") + return pct def split_burn_and_return(amount: float, burn_pct: float | None = None) -> tuple[float, float]: """Compute (burn_amount, returned_amount) for a single stake.""" - if amount <= 0: + amt = _finite_float(amount) + if amt is None or amt <= 0: return 0.0, 0.0 - pct = float(stalemate_burn_pct() if burn_pct is None else burn_pct) + pct = _finite_float(stalemate_burn_pct() if burn_pct is None else burn_pct) + if pct is None: + raise ValueError("burn_pct must be finite") if pct <= 0: - return 0.0, float(amount) + return 0.0, amt if pct >= 1: - return float(amount), 0.0 - burn = float(amount) * pct - returned = float(amount) - burn + return amt, 0.0 + burn = amt * pct + returned = amt - burn return burn, returned @@ -67,18 +82,17 @@ def apply_to_stakes( folds these into the larger ``ResolutionResult`` rather than mutating any state directly. """ - pct = float(stalemate_burn_pct() if burn_pct is None else burn_pct) + pct = _finite_float(stalemate_burn_pct() if burn_pct is None else burn_pct) + if pct is None: + raise ValueError("burn_pct must be finite") returns: dict[tuple[str, str], float] = {} total_burned = 0.0 for s in stakes: node_id = s.get("node_id") if isinstance(s, dict) else getattr(s, "node_id", None) rep_type = s.get("rep_type") if isinstance(s, dict) else getattr(s, "rep_type", None) amount = s.get("amount") if isinstance(s, dict) else getattr(s, "amount", None) - try: - amt = float(amount) if amount is not None else 0.0 - except (TypeError, ValueError): - amt = 0.0 - if amt <= 0 or not isinstance(node_id, str) or rep_type not in ("oracle", "common"): + amt = _finite_float(amount) + if amt is None or amt <= 0 or not isinstance(node_id, str) or rep_type not in ("oracle", "common"): continue burn, ret = split_burn_and_return(amt, pct) if ret > 0: diff --git a/backend/services/infonet/tests/test_stalemate_burn_nonfinite_values.py b/backend/services/infonet/tests/test_stalemate_burn_nonfinite_values.py new file mode 100644 index 0000000..c587cb7 --- /dev/null +++ b/backend/services/infonet/tests/test_stalemate_burn_nonfinite_values.py @@ -0,0 +1,35 @@ +"""Regression coverage for non-finite stalemate burn inputs.""" + +import math + +import pytest + +from services.infonet.markets.stalemate_burn import apply_to_stakes, split_burn_and_return + + +def test_split_rejects_nonfinite_amounts(): + assert split_burn_and_return(float("nan"), 0.1) == (0.0, 0.0) + assert split_burn_and_return(float("inf"), 0.1) == (0.0, 0.0) + + +def test_apply_skips_nonfinite_stakes_without_poisoning_totals(): + returns, burned = apply_to_stakes( + [ + {"node_id": "alice", "rep_type": "oracle", "amount": 10.0}, + {"node_id": "mallory", "rep_type": "oracle", "amount": float("nan")}, + {"node_id": "eve", "rep_type": "common", "amount": float("inf")}, + ], + burn_pct=0.1, + ) + + assert returns == {("alice", "oracle"): 9.0} + assert burned == 1.0 + assert math.isfinite(burned) + + +def test_nonfinite_burn_percentage_is_rejected(): + with pytest.raises(ValueError, match="burn_pct must be finite"): + split_burn_and_return(10.0, float("nan")) + + with pytest.raises(ValueError, match="burn_pct must be finite"): + apply_to_stakes([], burn_pct=float("inf"))