mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-08-18 16:37:24 +02:00
Merge pull request #513 from sylvesterkaczmarek/fix/market-snapshot-invalid-numerics
fix(infonet): harden market snapshot projection
This commit is contained in:
@@ -24,6 +24,7 @@ from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from typing import Any, Iterable
|
||||
|
||||
|
||||
@@ -32,6 +33,14 @@ 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, OverflowError):
|
||||
return None
|
||||
return parsed if math.isfinite(parsed) else None
|
||||
|
||||
|
||||
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:
|
||||
@@ -61,6 +70,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 +92,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 +129,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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"""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
|
||||
|
||||
|
||||
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 {
|
||||
"event_type": "prediction_place",
|
||||
"node_id": node,
|
||||
"timestamp": timestamp,
|
||||
"sequence": sequence,
|
||||
"payload": payload,
|
||||
}
|
||||
|
||||
|
||||
@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", invalid_stake, 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_finite_numeric_string_stake_is_preserved() -> None:
|
||||
chain = [
|
||||
_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.5")
|
||||
|
||||
assert snapshot["frozen_participant_count"] == 2
|
||||
assert snapshot["frozen_total_stake"] == 10.0
|
||||
assert snapshot["frozen_probability_state"] == {"yes": 0.25, "no": 0.75}
|
||||
assert snapshot["frozen_at"] == 200.5
|
||||
|
||||
|
||||
@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=invalid_frozen_at)
|
||||
Reference in New Issue
Block a user