mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-05-13 12:04:56 +02:00
154 lines
4.9 KiB
Python
154 lines
4.9 KiB
Python
"""Gate locking — "constitutionalize-a-gate".
|
|
|
|
Source of truth: ``infonet-economy/RULES_SKELETON.md`` §5.3 step 4 +
|
|
``CONFIG['gate_lock_cost_per_member']`` / ``CONFIG['gate_lock_min_members']``.
|
|
|
|
Locking semantics:
|
|
|
|
- Each ``gate_lock`` event records one member contributing
|
|
``CONFIG['gate_lock_cost_per_member']`` (default 10) common rep.
|
|
- A gate is "locked" once ≥ ``CONFIG['gate_lock_min_members']``
|
|
(default 5) distinct current members have each emitted a valid
|
|
``gate_lock`` event.
|
|
- Once locked, the gate's rules become immutable — no governance
|
|
petition can modify them. Only an upgrade-hash governance event
|
|
(out of scope for Sprint 6) can amend a locked gate's rules.
|
|
|
|
Validation rules for an incoming ``gate_lock`` event (callers in
|
|
production should run these *before* emitting):
|
|
|
|
- The gate exists.
|
|
- The locker is a current member.
|
|
- The locker hasn't already locked this gate (one lock per node).
|
|
- The locker has paid (the burn happens at emit time; this module
|
|
asserts the schematic ``lock_cost`` matches CONFIG).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any, Iterable
|
|
|
|
from services.infonet.config import CONFIG
|
|
from services.infonet.gates.sacrifice import compute_member_set
|
|
from services.infonet.gates.state import events_for_gate
|
|
|
|
|
|
def _payload(event: dict[str, Any]) -> dict[str, Any]:
|
|
p = event.get("payload")
|
|
return p if isinstance(p, dict) else {}
|
|
|
|
|
|
def _lock_cost_per_member() -> int:
|
|
return int(CONFIG["gate_lock_cost_per_member"])
|
|
|
|
|
|
def _lock_min_members() -> int:
|
|
return int(CONFIG["gate_lock_min_members"])
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class LockedGateState:
|
|
locked: bool
|
|
locked_at: float | None
|
|
locked_by: tuple[str, ...]
|
|
|
|
|
|
def _collect_lock_contributions(
|
|
gate_id: str,
|
|
chain: Iterable[dict[str, Any]],
|
|
) -> list[tuple[str, float]]:
|
|
"""Return ``[(node_id, timestamp)]`` for each accepted ``gate_lock``
|
|
event in chain order. Subsequent locks from the same node are
|
|
ignored (one lock per node)."""
|
|
chain_list = list(chain)
|
|
members = compute_member_set(gate_id, chain_list)
|
|
seen: set[str] = set()
|
|
out: list[tuple[str, float]] = []
|
|
for ev in events_for_gate(gate_id, chain_list):
|
|
if ev.get("event_type") != "gate_lock":
|
|
continue
|
|
node = ev.get("node_id")
|
|
if not isinstance(node, str) or not node:
|
|
continue
|
|
if node in seen:
|
|
continue
|
|
if node not in members:
|
|
# Non-member lock attempt — ignored. The producer-side
|
|
# check should also refuse to emit, but resolver-side
|
|
# enforcement is defense-in-depth.
|
|
continue
|
|
p = _payload(ev)
|
|
try:
|
|
paid = float(p.get("lock_cost") or 0.0)
|
|
except (TypeError, ValueError):
|
|
paid = 0.0
|
|
if paid < float(_lock_cost_per_member()):
|
|
continue
|
|
seen.add(node)
|
|
out.append((node, float(ev.get("timestamp") or 0.0)))
|
|
return out
|
|
|
|
|
|
def _state(gate_id: str, chain: Iterable[dict[str, Any]]) -> LockedGateState:
|
|
contributions = _collect_lock_contributions(gate_id, chain)
|
|
if len(contributions) < _lock_min_members():
|
|
return LockedGateState(locked=False, locked_at=None, locked_by=())
|
|
contributions.sort(key=lambda c: c[1])
|
|
threshold_ts = contributions[_lock_min_members() - 1][1]
|
|
nodes = tuple(c[0] for c in contributions)
|
|
return LockedGateState(locked=True, locked_at=threshold_ts, locked_by=nodes)
|
|
|
|
|
|
def is_locked(gate_id: str, chain: Iterable[dict[str, Any]]) -> bool:
|
|
return _state(gate_id, chain).locked
|
|
|
|
|
|
def locked_at(gate_id: str, chain: Iterable[dict[str, Any]]) -> float | None:
|
|
return _state(gate_id, chain).locked_at
|
|
|
|
|
|
def locked_by(gate_id: str, chain: Iterable[dict[str, Any]]) -> tuple[str, ...]:
|
|
return _state(gate_id, chain).locked_by
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class LockValidation:
|
|
accepted: bool
|
|
reason: str
|
|
cost: int
|
|
|
|
|
|
def validate_lock_request(
|
|
node_id: str,
|
|
gate_id: str,
|
|
chain: Iterable[dict[str, Any]],
|
|
*,
|
|
lock_cost: int | None = None,
|
|
) -> LockValidation:
|
|
"""Pre-emit check for a ``gate_lock`` event from ``node_id``.
|
|
|
|
Returns ``accepted=False`` with a structured ``reason`` when
|
|
rejected — the UI surfaces these directly so the user knows what
|
|
needs to change.
|
|
"""
|
|
chain_list = list(chain)
|
|
cost = int(_lock_cost_per_member() if lock_cost is None else lock_cost)
|
|
if cost < _lock_cost_per_member():
|
|
return LockValidation(False, "lock_cost_below_min", cost)
|
|
if node_id not in compute_member_set(gate_id, chain_list):
|
|
return LockValidation(False, "not_a_member", cost)
|
|
if node_id in {n for n, _ in _collect_lock_contributions(gate_id, chain_list)}:
|
|
return LockValidation(False, "already_locked_by_node", cost)
|
|
return LockValidation(True, "ok", cost)
|
|
|
|
|
|
__all__ = [
|
|
"LockedGateState",
|
|
"LockValidation",
|
|
"is_locked",
|
|
"locked_at",
|
|
"locked_by",
|
|
"validate_lock_request",
|
|
]
|