From 7cb315dbb0fcd2c5f4ae72eca9bff4f8ed41bfe6 Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Fri, 14 Aug 2026 10:01:27 +0100 Subject: [PATCH 1/4] fix(infonet): reject non-finite dispute numerics --- backend/services/infonet/markets/dispute.py | 24 ++++++++++++--------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/backend/services/infonet/markets/dispute.py b/backend/services/infonet/markets/dispute.py index be1a51a..5121e07 100644 --- a/backend/services/infonet/markets/dispute.py +++ b/backend/services/infonet/markets/dispute.py @@ -28,6 +28,7 @@ Two effects: from __future__ import annotations from dataclasses import dataclass, field +from math import isfinite from typing import Any, Iterable 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 {} +def _finite_float(value: Any, default: float = 0.0) -> float: + """Parse a chain numeric without allowing NaN/inf to poison views.""" + try: + parsed = float(value or default) + except (TypeError, ValueError): + return float(default) + return parsed if isfinite(parsed) else float(default) + + @dataclass class DisputeView: """Chain-derived view of a single dispute.""" @@ -94,11 +104,8 @@ def collect_disputes( p = _payload(ev) did = _dispute_id(ev) 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) + cstake = _finite_float(p.get("challenger_stake")) + opened_at = _finite_float(ev.get("timestamp")) disputes[did] = DisputeView( dispute_id=did, market_id=str(market_id), challenger_id=str(challenger), challenger_stake=cstake, @@ -120,10 +127,7 @@ def collect_disputes( rep_type = p.get("rep_type") if rep_type not in ("oracle", "common"): continue - try: - amount = float(p.get("amount") or 0.0) - except (TypeError, ValueError): - continue + amount = _finite_float(p.get("amount")) if amount <= 0: continue record = { @@ -146,7 +150,7 @@ def collect_disputes( if outcome not in ("upheld", "reversed", "tie"): continue disputes[did].resolved_outcome = outcome - disputes[did].resolved_at = float(ev.get("timestamp") or 0.0) + disputes[did].resolved_at = _finite_float(ev.get("timestamp")) return sorted(disputes.values(), key=lambda d: (d.opened_at, d.dispute_id)) From 02b3d734d8b88c5f13b7e7f1973dc0bca590ced4 Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Fri, 14 Aug 2026 10:01:39 +0100 Subject: [PATCH 2/4] test(infonet): cover non-finite dispute numerics --- .../tests/test_dispute_nonfinite_values.py | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 backend/services/infonet/tests/test_dispute_nonfinite_values.py diff --git a/backend/services/infonet/tests/test_dispute_nonfinite_values.py b/backend/services/infonet/tests/test_dispute_nonfinite_values.py new file mode 100644 index 0000000..5375654 --- /dev/null +++ b/backend/services/infonet/tests/test_dispute_nonfinite_values.py @@ -0,0 +1,78 @@ +"""Regression coverage for malformed and non-finite dispute numerics.""" + +from services.infonet.markets.dispute import collect_disputes + + +def test_collect_disputes_normalizes_invalid_numeric_values(): + chain = [ + { + "event_type": "dispute_open", + "event_id": "dispute-1", + "node_id": "challenger", + "timestamp": "not-a-timestamp", + "payload": { + "market_id": "market-1", + "challenger_stake": "nan", + }, + }, + { + "event_type": "dispute_stake", + "node_id": "oracle-1", + "timestamp": 1.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": "upheld", + }, + }, + ] + + disputes = collect_disputes("market-1", chain) + + assert len(disputes) == 1 + dispute = disputes[0] + assert dispute.challenger_stake == 0.0 + assert dispute.opened_at == 0.0 + assert dispute.confirm_stakes == [] + assert dispute.resolved_outcome == "upheld" + assert dispute.resolved_at == 0.0 + + +def test_collect_disputes_preserves_finite_numeric_strings(): + chain = [ + { + "event_type": "dispute_open", + "event_id": "dispute-2", + "node_id": "challenger", + "timestamp": "12.5", + "payload": { + "market_id": "market-2", + "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", + }, + }, + ] + + 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 From 110aef505a51c3888418a305adeb9592618c5d7a Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Sat, 15 Aug 2026 23:32:20 +0100 Subject: [PATCH 3/4] Fail closed on malformed dispute authority --- backend/services/infonet/markets/dispute.py | 31 +++++++++++++-------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/backend/services/infonet/markets/dispute.py b/backend/services/infonet/markets/dispute.py index 5121e07..dc0de2d 100644 --- a/backend/services/infonet/markets/dispute.py +++ b/backend/services/infonet/markets/dispute.py @@ -39,13 +39,13 @@ def _payload(event: dict[str, Any]) -> dict[str, Any]: return p if isinstance(p, dict) else {} -def _finite_float(value: Any, default: float = 0.0) -> float: - """Parse a chain numeric without allowing NaN/inf to poison views.""" +def _finite_float(value: Any) -> float | None: + """Parse a chain numeric, rejecting malformed and non-finite values.""" try: - parsed = float(value or default) - except (TypeError, ValueError): - return float(default) - return parsed if isfinite(parsed) else float(default) + parsed = float(value) + except (TypeError, ValueError, OverflowError): + return None + return parsed if isfinite(parsed) else None @dataclass @@ -97,15 +97,18 @@ def collect_disputes( if not open_events: 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] = {} open_id_by_market_event: dict[str, str] = {} for ev in open_events: p = _payload(ev) - did = _dispute_id(ev) - challenger = ev.get("node_id") or "" 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) + challenger = ev.get("node_id") or "" disputes[did] = DisputeView( dispute_id=did, market_id=str(market_id), challenger_id=str(challenger), challenger_stake=cstake, @@ -128,7 +131,7 @@ def collect_disputes( if rep_type not in ("oracle", "common"): continue amount = _finite_float(p.get("amount")) - if amount <= 0: + if amount is None or amount <= 0: continue record = { "node_id": ev.get("node_id") or "", @@ -138,7 +141,8 @@ def collect_disputes( target = disputes[did].confirm_stakes if side == "confirm" else disputes[did].reverse_stakes 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: if ev.get("event_type") != "dispute_resolve": continue @@ -149,8 +153,11 @@ def collect_disputes( outcome = p.get("outcome") if outcome not in ("upheld", "reversed", "tie"): continue + resolved_at = _finite_float(ev.get("timestamp")) + if resolved_at is None: + continue disputes[did].resolved_outcome = outcome - disputes[did].resolved_at = _finite_float(ev.get("timestamp")) + disputes[did].resolved_at = resolved_at return sorted(disputes.values(), key=lambda d: (d.opened_at, d.dispute_id)) From 8b16af0c93fdeb3d779a798755571d85f0614e39 Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Sat, 15 Aug 2026 23:32:35 +0100 Subject: [PATCH 4/4] Cover fail-closed dispute authority --- .../tests/test_dispute_nonfinite_values.py | 94 +++++++++++++------ 1 file changed, 63 insertions(+), 31 deletions(-) diff --git a/backend/services/infonet/tests/test_dispute_nonfinite_values.py b/backend/services/infonet/tests/test_dispute_nonfinite_values.py index 5375654..1c82416 100644 --- a/backend/services/infonet/tests/test_dispute_nonfinite_values.py +++ b/backend/services/infonet/tests/test_dispute_nonfinite_values.py @@ -3,22 +3,52 @@ from services.infonet.markets.dispute import collect_disputes -def test_collect_disputes_normalizes_invalid_numeric_values(): - chain = [ - { - "event_type": "dispute_open", - "event_id": "dispute-1", - "node_id": "challenger", - "timestamp": "not-a-timestamp", - "payload": { - "market_id": "market-1", - "challenger_stake": "nan", - }, +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": 1.0, + "timestamp": 11.0, "payload": { "dispute_id": "dispute-1", "side": "confirm", @@ -31,34 +61,26 @@ def test_collect_disputes_normalizes_invalid_numeric_values(): "timestamp": float("inf"), "payload": { "dispute_id": "dispute-1", - "outcome": "upheld", + "outcome": "reversed", }, }, ] - disputes = collect_disputes("market-1", chain) + dispute = collect_disputes("market-1", chain)[0] - assert len(disputes) == 1 - dispute = disputes[0] - assert dispute.challenger_stake == 0.0 - assert dispute.opened_at == 0.0 assert dispute.confirm_stakes == [] - assert dispute.resolved_outcome == "upheld" - assert dispute.resolved_at == 0.0 + assert dispute.resolved_outcome is None + assert dispute.resolved_at is None -def test_collect_disputes_preserves_finite_numeric_strings(): +def test_collect_disputes_preserves_finite_numeric_strings() -> None: chain = [ - { - "event_type": "dispute_open", - "event_id": "dispute-2", - "node_id": "challenger", - "timestamp": "12.5", - "payload": { - "market_id": "market-2", - "challenger_stake": "3.5", - }, - }, + _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", @@ -69,6 +91,14 @@ def test_collect_disputes_preserves_finite_numeric_strings(): "amount": "2.25", }, }, + { + "event_type": "dispute_resolve", + "timestamp": "13.5", + "payload": { + "dispute_id": "dispute-2", + "outcome": "reversed", + }, + }, ] dispute = collect_disputes("market-2", chain)[0] @@ -76,3 +106,5 @@ def test_collect_disputes_preserves_finite_numeric_strings(): 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