From 8cbffc8fcec128aa59f570f840676602fbab528d Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Fri, 14 Aug 2026 10:26:42 +0100 Subject: [PATCH 1/4] fix(infonet): harden market snapshot projection --- backend/services/infonet/markets/snapshot.py | 71 ++++++++++++++------ 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/backend/services/infonet/markets/snapshot.py b/backend/services/infonet/markets/snapshot.py index 5ab2208..1a3e2f1 100644 --- a/backend/services/infonet/markets/snapshot.py +++ b/backend/services/infonet/markets/snapshot.py @@ -24,6 +24,7 @@ from __future__ import annotations import hashlib import json +import math from typing import Any, Iterable @@ -32,6 +33,32 @@ def _payload(event: dict[str, Any]) -> dict[str, Any]: return p if isinstance(p, dict) else {} +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 _safe_int(value: Any) -> int | None: + try: + return int(value) + except (TypeError, ValueError, OverflowError): + return None + + +def _event_order_key(event: dict[str, Any]) -> tuple[int, float, int, int]: + timestamp = _finite_float(event.get("timestamp")) + sequence = _safe_int(event.get("sequence")) + return ( + 1 if timestamp is None else 0, + 0.0 if timestamp is None else timestamp, + 1 if sequence is None else 0, + 0 if sequence is None else sequence, + ) + + def _events_for_market(market_id: str, chain: Iterable[dict[str, Any]]) -> list[dict[str, Any]]: out: list[dict[str, Any]] = [] for ev in chain: @@ -39,7 +66,7 @@ def _events_for_market(market_id: str, chain: Iterable[dict[str, Any]]) -> list[ continue if _payload(ev).get("market_id") == market_id: out.append(ev) - out.sort(key=lambda e: (float(e.get("timestamp") or 0.0), int(e.get("sequence") or 0))) + out.sort(key=_event_order_key) return out @@ -61,6 +88,10 @@ def build_snapshot( to advance to EVIDENCE. Pass it explicitly so the function stays pure and deterministic. """ + frozen_at_value = _finite_float(frozen_at) + if frozen_at_value is None: + raise ValueError("frozen_at must be finite") + events = _events_for_market(market_id, chain) predictor_ids: list[str] = [] @@ -79,27 +110,29 @@ def build_snapshot( side = p.get("side") if side not in ("yes", "no"): continue + + stake = p.get("stake_amount") + if stake is None: + weight = 1.0 # Free pick = 1.0 virtual stake (RULES §5.2). + staked_amount = 0.0 + else: + parsed_stake = _finite_float(stake) + if parsed_stake is None or parsed_stake <= 0: + # Invalid paid predictions must not inflate participant + # counts or poison the frozen probability state. + continue + weight = parsed_stake + staked_amount = parsed_stake + if node not in seen_predictors: seen_predictors.add(node) predictor_ids.append(node) - stake = p.get("stake_amount") - if stake is not None: - try: - a = float(stake) - except (TypeError, ValueError): - a = 0.0 - if a > 0: - total_stake += a - if side == "yes": - yes_weight += a - else: - no_weight += a + + total_stake += staked_amount + if side == "yes": + yes_weight += weight else: - # Free pick = 1.0 virtual stake (RULES §5.2). - if side == "yes": - yes_weight += 1.0 - else: - no_weight += 1.0 + no_weight += weight pool = yes_weight + no_weight if pool > 0: @@ -114,7 +147,7 @@ def build_snapshot( "frozen_total_stake": total_stake, "frozen_predictor_ids": predictor_ids, "frozen_probability_state": {"yes": yes_p, "no": no_p}, - "frozen_at": float(frozen_at), + "frozen_at": frozen_at_value, } From e5e7be198260ba06f35e7e0cffa4c0bb5b6fadee Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Fri, 14 Aug 2026 10:26:56 +0100 Subject: [PATCH 2/4] test(infonet): cover invalid snapshot numerics --- .../tests/test_snapshot_invalid_numerics.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 backend/services/infonet/tests/test_snapshot_invalid_numerics.py diff --git a/backend/services/infonet/tests/test_snapshot_invalid_numerics.py b/backend/services/infonet/tests/test_snapshot_invalid_numerics.py new file mode 100644 index 0000000..d727e16 --- /dev/null +++ b/backend/services/infonet/tests/test_snapshot_invalid_numerics.py @@ -0,0 +1,70 @@ +"""Regression coverage for malformed/non-finite market snapshot inputs.""" + +import math + +import pytest + +from services.infonet.markets.snapshot import build_snapshot, find_snapshot + + +def _prediction(node, side, stake, *, timestamp, sequence): + payload = {"market_id": "m1", "side": side} + if stake is not None: + payload["stake_amount"] = stake + return { + "event_type": "prediction_place", + "node_id": node, + "timestamp": timestamp, + "sequence": sequence, + "payload": payload, + } + + +def test_nonfinite_paid_stake_does_not_poison_snapshot(): + chain = [ + _prediction("alice", "yes", None, timestamp=100.0, sequence=1), + _prediction("mallory", "no", float("inf"), timestamp=101.0, sequence=2), + ] + + snapshot = build_snapshot("m1", chain, frozen_at=200.0) + + assert snapshot["frozen_participant_count"] == 1 + assert snapshot["frozen_predictor_ids"] == ["alice"] + assert snapshot["frozen_total_stake"] == 0.0 + assert snapshot["frozen_probability_state"] == {"yes": 1.0, "no": 0.0} + assert all(math.isfinite(v) for v in snapshot["frozen_probability_state"].values()) + + +def test_malformed_ordering_metadata_does_not_break_snapshot_build(): + chain = [ + _prediction("alice", "yes", 5.0, timestamp="bad", sequence="bad"), + _prediction("bob", "no", 5.0, timestamp=100.0, sequence=1), + ] + + snapshot = build_snapshot("m1", chain, frozen_at=200.0) + + assert snapshot["frozen_participant_count"] == 2 + assert snapshot["frozen_total_stake"] == 10.0 + assert snapshot["frozen_probability_state"] == {"yes": 0.5, "no": 0.5} + + +def test_invalid_snapshot_ordering_does_not_outrank_valid_snapshot(): + invalid = { + "event_type": "market_snapshot", + "timestamp": "bad", + "sequence": "bad", + "payload": {"market_id": "m1", "marker": "invalid"}, + } + valid = { + "event_type": "market_snapshot", + "timestamp": 100.0, + "sequence": 1, + "payload": {"market_id": "m1", "marker": "valid"}, + } + + assert find_snapshot("m1", [invalid, valid])["marker"] == "valid" + + +def test_nonfinite_frozen_at_is_rejected(): + with pytest.raises(ValueError, match="frozen_at must be finite"): + build_snapshot("m1", [], frozen_at=float("nan")) From 6db964a9f56c46ce3b018d86c0101978870180b8 Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Sat, 15 Aug 2026 23:35:39 +0100 Subject: [PATCH 3/4] Preserve snapshot commitment ordering --- backend/services/infonet/markets/snapshot.py | 22 ++------------------ 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/backend/services/infonet/markets/snapshot.py b/backend/services/infonet/markets/snapshot.py index 1a3e2f1..5d8fa0d 100644 --- a/backend/services/infonet/markets/snapshot.py +++ b/backend/services/infonet/markets/snapshot.py @@ -36,27 +36,9 @@ def _payload(event: dict[str, Any]) -> dict[str, Any]: 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 _safe_int(value: Any) -> int | None: - try: - return int(value) except (TypeError, ValueError, OverflowError): return None - - -def _event_order_key(event: dict[str, Any]) -> tuple[int, float, int, int]: - timestamp = _finite_float(event.get("timestamp")) - sequence = _safe_int(event.get("sequence")) - return ( - 1 if timestamp is None else 0, - 0.0 if timestamp is None else timestamp, - 1 if sequence is None else 0, - 0 if sequence is None else sequence, - ) + return parsed if math.isfinite(parsed) else None def _events_for_market(market_id: str, chain: Iterable[dict[str, Any]]) -> list[dict[str, Any]]: @@ -66,7 +48,7 @@ def _events_for_market(market_id: str, chain: Iterable[dict[str, Any]]) -> list[ continue if _payload(ev).get("market_id") == market_id: out.append(ev) - out.sort(key=_event_order_key) + out.sort(key=lambda e: (float(e.get("timestamp") or 0.0), int(e.get("sequence") or 0))) return out From 90b9f6db1e4ec8e45ff4fb3dd3ba36d34943c19d Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Sat, 15 Aug 2026 23:36:02 +0100 Subject: [PATCH 4/4] Scope snapshot tests to numeric hardening --- .../tests/test_snapshot_invalid_numerics.py | 54 +++++++++---------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/backend/services/infonet/tests/test_snapshot_invalid_numerics.py b/backend/services/infonet/tests/test_snapshot_invalid_numerics.py index d727e16..cf2f7a7 100644 --- a/backend/services/infonet/tests/test_snapshot_invalid_numerics.py +++ b/backend/services/infonet/tests/test_snapshot_invalid_numerics.py @@ -1,14 +1,22 @@ -"""Regression coverage for malformed/non-finite market snapshot inputs.""" +"""Regression coverage for malformed/non-finite market snapshot numerics.""" import math +from typing import Any import pytest -from services.infonet.markets.snapshot import build_snapshot, find_snapshot +from services.infonet.markets.snapshot import build_snapshot -def _prediction(node, side, stake, *, timestamp, sequence): - payload = {"market_id": "m1", "side": side} +def _prediction( + node: str, + side: str, + stake: Any, + *, + timestamp: float, + sequence: int, +) -> dict[str, Any]: + payload: dict[str, Any] = {"market_id": "m1", "side": side} if stake is not None: payload["stake_amount"] = stake return { @@ -20,10 +28,11 @@ def _prediction(node, side, stake, *, timestamp, sequence): } -def test_nonfinite_paid_stake_does_not_poison_snapshot(): +@pytest.mark.parametrize("invalid_stake", [float("nan"), float("inf"), "not-a-number", -1.0, 0.0]) +def test_invalid_paid_stake_does_not_poison_or_count_snapshot(invalid_stake: Any) -> None: chain = [ _prediction("alice", "yes", None, timestamp=100.0, sequence=1), - _prediction("mallory", "no", float("inf"), timestamp=101.0, sequence=2), + _prediction("mallory", "no", invalid_stake, timestamp=101.0, sequence=2), ] snapshot = build_snapshot("m1", chain, frozen_at=200.0) @@ -35,36 +44,21 @@ def test_nonfinite_paid_stake_does_not_poison_snapshot(): assert all(math.isfinite(v) for v in snapshot["frozen_probability_state"].values()) -def test_malformed_ordering_metadata_does_not_break_snapshot_build(): +def test_finite_numeric_string_stake_is_preserved() -> None: chain = [ - _prediction("alice", "yes", 5.0, timestamp="bad", sequence="bad"), - _prediction("bob", "no", 5.0, timestamp=100.0, sequence=1), + _prediction("alice", "yes", "2.5", timestamp=100.0, sequence=1), + _prediction("bob", "no", "7.5", timestamp=101.0, sequence=2), ] - snapshot = build_snapshot("m1", chain, frozen_at=200.0) + snapshot = build_snapshot("m1", chain, frozen_at="200.5") assert snapshot["frozen_participant_count"] == 2 assert snapshot["frozen_total_stake"] == 10.0 - assert snapshot["frozen_probability_state"] == {"yes": 0.5, "no": 0.5} + assert snapshot["frozen_probability_state"] == {"yes": 0.25, "no": 0.75} + assert snapshot["frozen_at"] == 200.5 -def test_invalid_snapshot_ordering_does_not_outrank_valid_snapshot(): - invalid = { - "event_type": "market_snapshot", - "timestamp": "bad", - "sequence": "bad", - "payload": {"market_id": "m1", "marker": "invalid"}, - } - valid = { - "event_type": "market_snapshot", - "timestamp": 100.0, - "sequence": 1, - "payload": {"market_id": "m1", "marker": "valid"}, - } - - assert find_snapshot("m1", [invalid, valid])["marker"] == "valid" - - -def test_nonfinite_frozen_at_is_rejected(): +@pytest.mark.parametrize("invalid_frozen_at", [float("nan"), float("inf"), "not-a-time"]) +def test_nonfinite_or_malformed_frozen_at_is_rejected(invalid_frozen_at: Any) -> None: with pytest.raises(ValueError, match="frozen_at must be finite"): - build_snapshot("m1", [], frozen_at=float("nan")) + build_snapshot("m1", [], frozen_at=invalid_frozen_at)