mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-08-18 08:27:18 +02:00
166 lines
5.9 KiB
Python
166 lines
5.9 KiB
Python
"""Market lifecycle state machine.
|
|
|
|
Source of truth: ``infonet-economy/RULES_SKELETON.md`` §5.2 + §3.10.
|
|
|
|
Five logical statuses:
|
|
|
|
PREDICTING — open for predictions; no snapshot yet.
|
|
EVIDENCE — snapshot frozen; evidence window open
|
|
(CONFIG['evidence_window_hours']).
|
|
RESOLVING — evidence window closed; resolution staking window open
|
|
(CONFIG['resolution_window_hours']).
|
|
FINAL — resolution_finalize event landed with a real outcome.
|
|
INVALID — resolution_finalize event landed with outcome="invalid".
|
|
|
|
Transitions are decided by ``chain_majority_time`` (per RULES §3.14
|
|
Rule 3) — no single node's local clock can unilaterally advance a
|
|
market. That rule keeps producers honest even when network partitions
|
|
shift local time.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from enum import Enum
|
|
from typing import Any, Iterable
|
|
|
|
from services.infonet.config import CONFIG
|
|
from services.infonet.markets.event_selection import (
|
|
events_for_market,
|
|
finite_float,
|
|
first_authoritative_event,
|
|
has_valid_ordering,
|
|
payload,
|
|
)
|
|
|
|
|
|
class MarketStatus(str, Enum):
|
|
PREDICTING = "predicting"
|
|
EVIDENCE = "evidence"
|
|
RESOLVING = "resolving"
|
|
FINAL = "final"
|
|
INVALID = "invalid"
|
|
|
|
|
|
_SECONDS_PER_HOUR = 3600.0
|
|
|
|
|
|
def _snapshot_time(snapshot: dict[str, Any]) -> float | None:
|
|
"""Return a valid snapshot time without masking malformed metadata."""
|
|
if not has_valid_ordering(snapshot):
|
|
return None
|
|
raw_timestamp = snapshot.get("timestamp")
|
|
# Preserve the pre-existing fallback only for an actually absent/zero
|
|
# timestamp. A present-but-malformed timestamp must fail closed instead
|
|
# of being disguised by a valid payload frozen_at.
|
|
if raw_timestamp in (None, "", 0, 0.0):
|
|
return finite_float(payload(snapshot).get("frozen_at"))
|
|
return finite_float(raw_timestamp)
|
|
|
|
|
|
def compute_market_status(
|
|
market_id: str,
|
|
chain: Iterable[dict[str, Any]],
|
|
*,
|
|
now: float,
|
|
) -> MarketStatus:
|
|
"""Return the current status of ``market_id`` at chain time ``now``.
|
|
|
|
Status is derived from the chain — it's never stored. The producer
|
|
that emits ``market_snapshot`` and ``resolution_finalize`` events
|
|
is responsible for using the same ``now`` value (typically
|
|
``chain_majority_time(chain)``) so every node converges on the same
|
|
status.
|
|
"""
|
|
events = events_for_market(market_id, chain)
|
|
if not events:
|
|
return MarketStatus.PREDICTING # treated as not-yet-existing
|
|
|
|
create_event = first_authoritative_event(events, "prediction_create")
|
|
if create_event is None or not has_valid_ordering(create_event):
|
|
return MarketStatus.PREDICTING
|
|
|
|
# The first finalize remains authoritative by chain order. A malformed
|
|
# first finalize cannot exert terminal authority, and a later finalize
|
|
# must not replace it; projection therefore falls through to the prior
|
|
# snapshot-backed phase.
|
|
finalize = first_authoritative_event(events, "resolution_finalize")
|
|
if finalize is not None and has_valid_ordering(finalize):
|
|
outcome = payload(finalize).get("outcome")
|
|
return MarketStatus.INVALID if outcome == "invalid" else MarketStatus.FINAL
|
|
|
|
# Likewise, never let a later snapshot replace the first commitment.
|
|
snapshot = first_authoritative_event(events, "market_snapshot")
|
|
if snapshot is None:
|
|
return MarketStatus.PREDICTING
|
|
|
|
snapshot_ts = _snapshot_time(snapshot)
|
|
if snapshot_ts is None:
|
|
return MarketStatus.PREDICTING
|
|
evidence_close = snapshot_ts + float(CONFIG["evidence_window_hours"]) * _SECONDS_PER_HOUR
|
|
if now < evidence_close:
|
|
return MarketStatus.EVIDENCE
|
|
return MarketStatus.RESOLVING
|
|
|
|
|
|
def should_advance_phase(
|
|
market_id: str,
|
|
chain: Iterable[dict[str, Any]],
|
|
*,
|
|
now: float,
|
|
) -> tuple[MarketStatus, MarketStatus] | None:
|
|
"""If a phase advance is due, return ``(current, next)``. Else ``None``.
|
|
|
|
The producer should call this on a heartbeat and emit the
|
|
appropriate event when a transition is ready:
|
|
|
|
- PREDICTING → EVIDENCE: emit ``market_snapshot``.
|
|
- EVIDENCE → RESOLVING: just a status change (no chain event).
|
|
- RESOLVING → FINAL/INVALID: emit ``resolution_finalize``.
|
|
"""
|
|
events = events_for_market(market_id, chain)
|
|
if not events:
|
|
return None
|
|
|
|
create_event = first_authoritative_event(events, "prediction_create")
|
|
if create_event is None or not has_valid_ordering(create_event):
|
|
return None
|
|
|
|
finalize = first_authoritative_event(events, "resolution_finalize")
|
|
if finalize is not None:
|
|
# Whether valid (terminal) or malformed (fail closed), do not let a
|
|
# later finalize replace the first authoritative chain event.
|
|
return None
|
|
|
|
create_payload = payload(create_event)
|
|
trigger_date = finite_float(create_payload.get("trigger_date"))
|
|
snapshot = first_authoritative_event(events, "market_snapshot")
|
|
|
|
if snapshot is None:
|
|
# PREDICTING — advance to EVIDENCE iff trigger_date has passed in
|
|
# majority chain time. Invalid trigger metadata cannot authorize
|
|
# a phase transition.
|
|
if trigger_date is not None and now >= trigger_date:
|
|
return (MarketStatus.PREDICTING, MarketStatus.EVIDENCE)
|
|
return None
|
|
|
|
snapshot_ts = _snapshot_time(snapshot)
|
|
if snapshot_ts is None:
|
|
# An invalid first snapshot cannot authorize a transition, and a
|
|
# later snapshot must not replace the commitment boundary.
|
|
return None
|
|
evidence_close = snapshot_ts + float(CONFIG["evidence_window_hours"]) * _SECONDS_PER_HOUR
|
|
resolution_close = evidence_close + float(CONFIG["resolution_window_hours"]) * _SECONDS_PER_HOUR
|
|
|
|
if now < evidence_close:
|
|
return None # still EVIDENCE
|
|
if now < resolution_close:
|
|
return (MarketStatus.EVIDENCE, MarketStatus.RESOLVING)
|
|
return (MarketStatus.RESOLVING, MarketStatus.FINAL)
|
|
|
|
|
|
__all__ = [
|
|
"MarketStatus",
|
|
"compute_market_status",
|
|
"should_advance_phase",
|
|
]
|