mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-06-08 07:13:53 +02:00
172 lines
5.5 KiB
Python
172 lines
5.5 KiB
Python
"""Market snapshot — frozen at PREDICTING → EVIDENCE transition.
|
|
|
|
Source of truth: ``infonet-economy/RULES_SKELETON.md`` §2.2 (snapshot
|
|
fields), §3.10 (snapshot_event_hash usage), §5.2 (when emitted).
|
|
|
|
The snapshot is the **commitment boundary** for all downstream
|
|
evaluation. Once frozen:
|
|
|
|
- Liquidity gates (``min_market_participants``,
|
|
``min_market_total_stake``) are evaluated against frozen values, not
|
|
live state.
|
|
- Predictor exclusion is computed from ``frozen_predictor_ids``
|
|
(UNION ``rotation_descendants`` at resolution time).
|
|
- Bootstrap PoW uses ``snapshot_event_hash`` as its salt so attackers
|
|
can't pre-mine before the boundary.
|
|
|
|
The snapshot itself is **immutable** by spec — the producer emits it
|
|
once and never updates it. Sprint 4 enforces immutability by ignoring
|
|
any subsequent ``market_snapshot`` events with the same market_id
|
|
(``find_snapshot`` returns the FIRST one). Tests assert this invariant.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from typing import Any, Iterable
|
|
|
|
|
|
def _payload(event: dict[str, Any]) -> dict[str, Any]:
|
|
p = event.get("payload")
|
|
return p if isinstance(p, dict) else {}
|
|
|
|
|
|
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:
|
|
if not isinstance(ev, dict):
|
|
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)))
|
|
return out
|
|
|
|
|
|
def build_snapshot(
|
|
market_id: str,
|
|
chain: Iterable[dict[str, Any]],
|
|
*,
|
|
frozen_at: float,
|
|
) -> dict[str, Any]:
|
|
"""Compute the snapshot payload deterministically from chain history.
|
|
|
|
Walks ``prediction_place`` events for ``market_id``, in chain order,
|
|
and produces the frozen counts / stake totals / predictor list /
|
|
yes-no probability state. The resulting dict is ready to be written
|
|
as the payload of a ``market_snapshot`` event.
|
|
|
|
``frozen_at`` is the canonical commitment timestamp — typically
|
|
``chain_majority_time(chain)`` at the moment the producer decides
|
|
to advance to EVIDENCE. Pass it explicitly so the function stays
|
|
pure and deterministic.
|
|
"""
|
|
events = _events_for_market(market_id, chain)
|
|
|
|
predictor_ids: list[str] = []
|
|
seen_predictors: set[str] = set()
|
|
yes_weight = 0.0
|
|
no_weight = 0.0
|
|
total_stake = 0.0
|
|
|
|
for ev in events:
|
|
if ev.get("event_type") != "prediction_place":
|
|
continue
|
|
node = ev.get("node_id")
|
|
if not isinstance(node, str) or not node:
|
|
continue
|
|
p = _payload(ev)
|
|
side = p.get("side")
|
|
if side not in ("yes", "no"):
|
|
continue
|
|
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
|
|
else:
|
|
# Free pick = 1.0 virtual stake (RULES §5.2).
|
|
if side == "yes":
|
|
yes_weight += 1.0
|
|
else:
|
|
no_weight += 1.0
|
|
|
|
pool = yes_weight + no_weight
|
|
if pool > 0:
|
|
yes_p = yes_weight / pool
|
|
else:
|
|
yes_p = 0.5
|
|
no_p = 1.0 - yes_p
|
|
|
|
return {
|
|
"market_id": market_id,
|
|
"frozen_participant_count": len(predictor_ids),
|
|
"frozen_total_stake": total_stake,
|
|
"frozen_predictor_ids": predictor_ids,
|
|
"frozen_probability_state": {"yes": yes_p, "no": no_p},
|
|
"frozen_at": float(frozen_at),
|
|
}
|
|
|
|
|
|
def compute_snapshot_event_hash(
|
|
snapshot_payload: dict[str, Any],
|
|
*,
|
|
market_id: str,
|
|
creator_node_id: str,
|
|
sequence: int,
|
|
) -> str:
|
|
"""Canonical SHA-256 of the snapshot event.
|
|
|
|
This hash is what bootstrap PoW uses as its salt (RULES §3.10 step
|
|
0.5) — committing this value on-chain prevents pre-mining of
|
|
bootstrap votes. The serialization is canonical (sorted keys,
|
|
compact separators, UTF-8) so every node arrives at the same hex.
|
|
|
|
The producer should append this value to the snapshot payload as
|
|
``snapshot_event_hash`` before emitting the event.
|
|
"""
|
|
canonical = {
|
|
"event_type": "market_snapshot",
|
|
"market_id": market_id,
|
|
"node_id": creator_node_id,
|
|
"sequence": int(sequence),
|
|
"payload": snapshot_payload,
|
|
}
|
|
encoded = json.dumps(canonical, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
|
return hashlib.sha256(encoded.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def find_snapshot(
|
|
market_id: str,
|
|
chain: Iterable[dict[str, Any]],
|
|
) -> dict[str, Any] | None:
|
|
"""Return the FIRST ``market_snapshot`` payload for ``market_id``.
|
|
|
|
Subsequent ``market_snapshot`` events with the same market_id are
|
|
ignored — snapshots are immutable per RULES §2.2. This is a
|
|
structural enforcement, not just a convention; an attacker who
|
|
forges a second snapshot cannot influence resolution.
|
|
"""
|
|
events = _events_for_market(market_id, chain)
|
|
for ev in events:
|
|
if ev.get("event_type") == "market_snapshot":
|
|
return _payload(ev)
|
|
return None
|
|
|
|
|
|
__all__ = [
|
|
"build_snapshot",
|
|
"compute_snapshot_event_hash",
|
|
"find_snapshot",
|
|
]
|