Merge pull request #509 from sylvesterkaczmarek/fix/dispute-nonfinite-values

fix(infonet): reject non-finite dispute numerics
This commit is contained in:
Shadowbroker
2026-08-15 21:42:33 -06:00
committed by GitHub
2 changed files with 134 additions and 13 deletions
+24 -13
View File
@@ -28,6 +28,7 @@ Two effects:
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field
from math import isfinite
from typing import Any, Iterable from typing import Any, Iterable
from services.infonet.config import CONFIG from services.infonet.config import CONFIG
@@ -38,6 +39,15 @@ def _payload(event: dict[str, Any]) -> dict[str, Any]:
return p if isinstance(p, dict) else {} return p if isinstance(p, dict) else {}
def _finite_float(value: Any) -> float | None:
"""Parse a chain numeric, rejecting malformed and non-finite values."""
try:
parsed = float(value)
except (TypeError, ValueError, OverflowError):
return None
return parsed if isfinite(parsed) else None
@dataclass @dataclass
class DisputeView: class DisputeView:
"""Chain-derived view of a single dispute.""" """Chain-derived view of a single dispute."""
@@ -87,18 +97,18 @@ def collect_disputes(
if not open_events: if not open_events:
return [] return []
# Build by dispute_id keyed off the open event. # Build by dispute_id keyed off the open event. A malformed
# authoritative open must not be normalized into active state.
disputes: dict[str, DisputeView] = {} disputes: dict[str, DisputeView] = {}
open_id_by_market_event: dict[str, str] = {} open_id_by_market_event: dict[str, str] = {}
for ev in open_events: for ev in open_events:
p = _payload(ev) p = _payload(ev)
cstake = _finite_float(p.get("challenger_stake"))
opened_at = _finite_float(ev.get("timestamp"))
if cstake is None or opened_at is None:
continue
did = _dispute_id(ev) did = _dispute_id(ev)
challenger = ev.get("node_id") or "" challenger = ev.get("node_id") or ""
try:
cstake = float(p.get("challenger_stake") or 0.0)
except (TypeError, ValueError):
cstake = 0.0
opened_at = float(ev.get("timestamp") or 0.0)
disputes[did] = DisputeView( disputes[did] = DisputeView(
dispute_id=did, market_id=str(market_id), dispute_id=did, market_id=str(market_id),
challenger_id=str(challenger), challenger_stake=cstake, challenger_id=str(challenger), challenger_stake=cstake,
@@ -120,11 +130,8 @@ def collect_disputes(
rep_type = p.get("rep_type") rep_type = p.get("rep_type")
if rep_type not in ("oracle", "common"): if rep_type not in ("oracle", "common"):
continue continue
try: amount = _finite_float(p.get("amount"))
amount = float(p.get("amount") or 0.0) if amount is None or amount <= 0:
except (TypeError, ValueError):
continue
if amount <= 0:
continue continue
record = { record = {
"node_id": ev.get("node_id") or "", "node_id": ev.get("node_id") or "",
@@ -134,7 +141,8 @@ def collect_disputes(
target = disputes[did].confirm_stakes if side == "confirm" else disputes[did].reverse_stakes target = disputes[did].confirm_stakes if side == "confirm" else disputes[did].reverse_stakes
target.append(record) target.append(record)
# Resolution events. # Resolution events. Invalid resolution timestamps fail closed:
# they cannot exert consensus authority through resolved_outcome.
for ev in chain_list: for ev in chain_list:
if ev.get("event_type") != "dispute_resolve": if ev.get("event_type") != "dispute_resolve":
continue continue
@@ -145,8 +153,11 @@ def collect_disputes(
outcome = p.get("outcome") outcome = p.get("outcome")
if outcome not in ("upheld", "reversed", "tie"): if outcome not in ("upheld", "reversed", "tie"):
continue continue
resolved_at = _finite_float(ev.get("timestamp"))
if resolved_at is None:
continue
disputes[did].resolved_outcome = outcome disputes[did].resolved_outcome = outcome
disputes[did].resolved_at = float(ev.get("timestamp") or 0.0) disputes[did].resolved_at = resolved_at
return sorted(disputes.values(), key=lambda d: (d.opened_at, d.dispute_id)) return sorted(disputes.values(), key=lambda d: (d.opened_at, d.dispute_id))
@@ -0,0 +1,110 @@
"""Regression coverage for malformed and non-finite dispute numerics."""
from services.infonet.markets.dispute import collect_disputes
def _open_event(
*, dispute_id: str, market_id: str, timestamp: object, challenger_stake: object
) -> dict:
return {
"event_type": "dispute_open",
"event_id": dispute_id,
"node_id": "challenger",
"timestamp": timestamp,
"payload": {
"market_id": market_id,
"challenger_stake": challenger_stake,
},
}
def test_malformed_authoritative_open_events_are_excluded() -> None:
chain = [
_open_event(
dispute_id="bad-time",
market_id="market-1",
timestamp="not-a-timestamp",
challenger_stake=3.0,
),
_open_event(
dispute_id="bad-stake",
market_id="market-1",
timestamp=10.0,
challenger_stake=float("nan"),
),
]
assert collect_disputes("market-1", chain) == []
def test_invalid_economic_and_resolution_values_fail_closed() -> None:
chain = [
_open_event(
dispute_id="dispute-1",
market_id="market-1",
timestamp=10.0,
challenger_stake=3.0,
),
{
"event_type": "dispute_stake",
"node_id": "oracle-1",
"timestamp": 11.0,
"payload": {
"dispute_id": "dispute-1",
"side": "confirm",
"rep_type": "oracle",
"amount": float("nan"),
},
},
{
"event_type": "dispute_resolve",
"timestamp": float("inf"),
"payload": {
"dispute_id": "dispute-1",
"outcome": "reversed",
},
},
]
dispute = collect_disputes("market-1", chain)[0]
assert dispute.confirm_stakes == []
assert dispute.resolved_outcome is None
assert dispute.resolved_at is None
def test_collect_disputes_preserves_finite_numeric_strings() -> None:
chain = [
_open_event(
dispute_id="dispute-2",
market_id="market-2",
timestamp="12.5",
challenger_stake="3.5",
),
{
"event_type": "dispute_stake",
"node_id": "oracle-2",
"payload": {
"dispute_id": "dispute-2",
"side": "reverse",
"rep_type": "oracle",
"amount": "2.25",
},
},
{
"event_type": "dispute_resolve",
"timestamp": "13.5",
"payload": {
"dispute_id": "dispute-2",
"outcome": "reversed",
},
},
]
dispute = collect_disputes("market-2", chain)[0]
assert dispute.challenger_stake == 3.5
assert dispute.opened_at == 12.5
assert dispute.reverse_stakes[0]["amount"] == 2.25
assert dispute.resolved_outcome == "reversed"
assert dispute.resolved_at == 13.5