fix(infonet): harden market snapshot projection

This commit is contained in:
Sylvester Kaczmarek
2026-08-14 10:26:42 +01:00
parent b6516a21bf
commit 8cbffc8fce
+52 -19
View File
@@ -24,6 +24,7 @@ from __future__ import annotations
import hashlib import hashlib
import json import json
import math
from typing import Any, Iterable 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 {} 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]]: def _events_for_market(market_id: str, chain: Iterable[dict[str, Any]]) -> list[dict[str, Any]]:
out: list[dict[str, Any]] = [] out: list[dict[str, Any]] = []
for ev in chain: for ev in chain:
@@ -39,7 +66,7 @@ def _events_for_market(market_id: str, chain: Iterable[dict[str, Any]]) -> list[
continue continue
if _payload(ev).get("market_id") == market_id: if _payload(ev).get("market_id") == market_id:
out.append(ev) 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 return out
@@ -61,6 +88,10 @@ def build_snapshot(
to advance to EVIDENCE. Pass it explicitly so the function stays to advance to EVIDENCE. Pass it explicitly so the function stays
pure and deterministic. 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) events = _events_for_market(market_id, chain)
predictor_ids: list[str] = [] predictor_ids: list[str] = []
@@ -79,27 +110,29 @@ def build_snapshot(
side = p.get("side") side = p.get("side")
if side not in ("yes", "no"): if side not in ("yes", "no"):
continue 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: if node not in seen_predictors:
seen_predictors.add(node) seen_predictors.add(node)
predictor_ids.append(node) predictor_ids.append(node)
stake = p.get("stake_amount")
if stake is not None: total_stake += staked_amount
try: if side == "yes":
a = float(stake) yes_weight += weight
except (TypeError, ValueError):
a = 0.0
if a > 0:
total_stake += a
if side == "yes":
yes_weight += a
else:
no_weight += a
else: else:
# Free pick = 1.0 virtual stake (RULES §5.2). no_weight += weight
if side == "yes":
yes_weight += 1.0
else:
no_weight += 1.0
pool = yes_weight + no_weight pool = yes_weight + no_weight
if pool > 0: if pool > 0:
@@ -114,7 +147,7 @@ def build_snapshot(
"frozen_total_stake": total_stake, "frozen_total_stake": total_stake,
"frozen_predictor_ids": predictor_ids, "frozen_predictor_ids": predictor_ids,
"frozen_probability_state": {"yes": yes_p, "no": no_p}, "frozen_probability_state": {"yes": yes_p, "no": no_p},
"frozen_at": float(frozen_at), "frozen_at": frozen_at_value,
} }