Files
Shadowbroker/backend/services/infonet/bootstrap/eligibility.py
T
2026-05-01 22:56:50 -06:00

130 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Bootstrap eligibility — identity age + predictor exclusion.
Source of truth: ``infonet-economy/RULES_SKELETON.md`` §3.10 step 0.5
(``is_bootstrap_eligible``).
Two gates:
1. **Identity age vs ``frozen_at`` (NOT ``now``).** Spec is explicit:
node.created_at + (bootstrap_min_identity_age_days * 86400)
<= market.snapshot.frozen_at
Measuring against the frozen snapshot timestamp keeps eligibility
deterministic — every node computes the same set from the same
chain state. Measuring against ``now`` would make eligibility
depend on local clock, which is a clock-manipulation attack
surface.
2. **Predictor exclusion.** Same as normal resolution:
``frozen_predictor_ids rotation_descendants(frozen_predictor_ids)``.
Reuses ``services.infonet.markets.resolution.excluded_predictor_ids``
(Sprint 4) — single source of truth.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Iterable
from services.infonet.config import CONFIG
from services.infonet.markets.resolution import excluded_predictor_ids
from services.infonet.markets.snapshot import find_snapshot
_SECONDS_PER_DAY = 86400.0
def _payload(event: dict[str, Any]) -> dict[str, Any]:
p = event.get("payload")
return p if isinstance(p, dict) else {}
def _node_created_at(node_id: str, chain: Iterable[dict[str, Any]]) -> float | None:
"""First chain appearance of ``node_id`` — used as a proxy for
``node.created_at``. Per RULES §2.1: "Timestamp of first appearance
on chain". A ``node_register`` event is preferred when present;
otherwise the earliest event signed by ``node_id``.
"""
earliest_register: float | None = None
earliest_any: float | None = None
for ev in chain:
if not isinstance(ev, dict):
continue
author = ev.get("node_id")
if author != node_id:
continue
try:
ts = float(ev.get("timestamp") or 0.0)
except (TypeError, ValueError):
continue
if ev.get("event_type") == "node_register":
if earliest_register is None or ts < earliest_register:
earliest_register = ts
if earliest_any is None or ts < earliest_any:
earliest_any = ts
return earliest_register if earliest_register is not None else earliest_any
def is_identity_age_eligible(
node_id: str,
market_id: str,
chain: Iterable[dict[str, Any]],
*,
min_age_days: float | None = None,
) -> bool:
"""``True`` iff
``node.created_at + min_age_days * 86400 <= market.snapshot.frozen_at``.
Returns ``False`` if the snapshot doesn't exist yet, the node has
no chain history, or the timing condition fails.
"""
chain_list = list(chain)
snapshot = find_snapshot(market_id, chain_list)
if snapshot is None:
return False
try:
frozen_at = float(snapshot.get("frozen_at") or 0.0)
except (TypeError, ValueError):
return False
created_at = _node_created_at(node_id, chain_list)
if created_at is None:
return False
age_days = float(min_age_days if min_age_days is not None
else CONFIG["bootstrap_min_identity_age_days"])
threshold_ts = created_at + age_days * _SECONDS_PER_DAY
return threshold_ts <= frozen_at
@dataclass(frozen=True)
class EligibilityDecision:
eligible: bool
reason: str
def validate_bootstrap_eligibility(
node_id: str,
market_id: str,
chain: Iterable[dict[str, Any]],
) -> EligibilityDecision:
"""Combine identity-age + predictor-exclusion checks.
Used by the Sprint 8 anti-DoS funnel and by the bootstrap
resolution path itself.
"""
chain_list = list(chain)
if find_snapshot(market_id, chain_list) is None:
return EligibilityDecision(False, "snapshot_missing")
if not is_identity_age_eligible(node_id, market_id, chain_list):
return EligibilityDecision(False, "identity_age_too_young")
if node_id in excluded_predictor_ids(market_id, chain_list):
return EligibilityDecision(False, "predictor_excluded")
return EligibilityDecision(True, "ok")
__all__ = [
"EligibilityDecision",
"is_identity_age_eligible",
"validate_bootstrap_eligibility",
]