mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-05-11 11:27:13 +02:00
148 lines
5.5 KiB
Python
148 lines
5.5 KiB
Python
"""Sprint 6 — gate locking.
|
||
|
||
Maps to IMPLEMENTATION_PLAN §7.1 Sprint 6 row:
|
||
"Gate locking requires 5 members × 10 rep. Locked gate rules immutable."
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from services.infonet.config import CONFIG
|
||
from services.infonet.gates import (
|
||
is_locked,
|
||
locked_at,
|
||
locked_by,
|
||
validate_lock_request,
|
||
)
|
||
from services.infonet.tests._gate_factory import (
|
||
make_gate_create,
|
||
make_gate_enter,
|
||
make_gate_lock,
|
||
)
|
||
|
||
|
||
def _setup_gate_with_n_members(n: int) -> tuple[list, list[str]]:
|
||
"""Returns (chain, member_ids). Members are named m0, m1, ..."""
|
||
base = 1_000_000.0
|
||
chain = [make_gate_create("g1", "creator", ts=base, seq=1)]
|
||
members = [f"m{i}" for i in range(n)]
|
||
for i, m in enumerate(members):
|
||
chain.append(make_gate_enter("g1", m, ts=base + 100 + i, seq=2 + i))
|
||
return chain, members
|
||
|
||
|
||
def test_unlocked_when_zero_locks():
|
||
chain, _ = _setup_gate_with_n_members(5)
|
||
assert not is_locked("g1", chain)
|
||
|
||
|
||
def test_locks_below_threshold_do_not_lock():
|
||
chain, members = _setup_gate_with_n_members(5)
|
||
base = 1_001_000.0
|
||
threshold = int(CONFIG["gate_lock_min_members"])
|
||
# threshold - 1 locks.
|
||
for i, m in enumerate(members[:threshold - 1]):
|
||
chain.append(make_gate_lock("g1", m, ts=base + i, seq=200 + i))
|
||
assert not is_locked("g1", chain)
|
||
|
||
|
||
def test_locks_at_exact_threshold_lock_the_gate():
|
||
chain, members = _setup_gate_with_n_members(5)
|
||
base = 1_001_000.0
|
||
threshold = int(CONFIG["gate_lock_min_members"])
|
||
for i, m in enumerate(members[:threshold]):
|
||
chain.append(make_gate_lock("g1", m, ts=base + i, seq=200 + i))
|
||
assert is_locked("g1", chain)
|
||
# locked_at is the timestamp of the LAST contributing lock.
|
||
assert locked_at("g1", chain) == base + threshold - 1
|
||
assert set(locked_by("g1", chain)) == set(members[:threshold])
|
||
|
||
|
||
def test_below_min_lock_cost_rejected():
|
||
"""A gate_lock event with lock_cost below CONFIG is ignored —
|
||
cannot count toward the threshold."""
|
||
chain, members = _setup_gate_with_n_members(5)
|
||
base = 1_001_000.0
|
||
cost_per = int(CONFIG["gate_lock_cost_per_member"])
|
||
for i, m in enumerate(members):
|
||
chain.append(make_gate_lock("g1", m, ts=base + i, seq=200 + i,
|
||
lock_cost=cost_per - 1))
|
||
assert not is_locked("g1", chain)
|
||
|
||
|
||
def test_lock_from_non_member_ignored():
|
||
chain, members = _setup_gate_with_n_members(4) # only 4 members
|
||
base = 1_001_000.0
|
||
# Add 5 locks but include a non-member (no entry event for "ghost").
|
||
for i, m in enumerate(members + ["ghost"]):
|
||
chain.append(make_gate_lock("g1", m, ts=base + i, seq=200 + i))
|
||
# Only 4 valid locks — below threshold of 5.
|
||
assert not is_locked("g1", chain)
|
||
|
||
|
||
def test_duplicate_locks_from_same_node_count_once():
|
||
chain, members = _setup_gate_with_n_members(5)
|
||
base = 1_001_000.0
|
||
# 4 distinct members + 1 duplicate from m0 = 5 events but 4 distinct nodes.
|
||
for i, m in enumerate(members[:4] + [members[0]]):
|
||
chain.append(make_gate_lock("g1", m, ts=base + i, seq=200 + i))
|
||
assert not is_locked("g1", chain)
|
||
|
||
|
||
def test_validate_lock_request_accepts_member():
|
||
chain, members = _setup_gate_with_n_members(3)
|
||
decision = validate_lock_request(members[0], "g1", chain)
|
||
assert decision.accepted
|
||
assert decision.cost == int(CONFIG["gate_lock_cost_per_member"])
|
||
|
||
|
||
def test_validate_lock_request_rejects_non_member():
|
||
chain, _ = _setup_gate_with_n_members(3)
|
||
decision = validate_lock_request("ghost", "g1", chain)
|
||
assert not decision.accepted
|
||
assert decision.reason == "not_a_member"
|
||
|
||
|
||
def test_validate_lock_request_rejects_below_min_cost():
|
||
chain, members = _setup_gate_with_n_members(3)
|
||
decision = validate_lock_request(
|
||
members[0], "g1", chain,
|
||
lock_cost=int(CONFIG["gate_lock_cost_per_member"]) - 1,
|
||
)
|
||
assert not decision.accepted
|
||
assert decision.reason == "lock_cost_below_min"
|
||
|
||
|
||
def test_validate_lock_request_rejects_double_lock():
|
||
chain, members = _setup_gate_with_n_members(5)
|
||
base = 1_001_000.0
|
||
chain.append(make_gate_lock("g1", members[0], ts=base, seq=200))
|
||
decision = validate_lock_request(members[0], "g1", chain)
|
||
assert not decision.accepted
|
||
assert decision.reason == "already_locked_by_node"
|
||
|
||
|
||
def test_locked_gate_rules_unchanged_in_chain():
|
||
"""Once locked, the gate's static metadata (entry_sacrifice etc.)
|
||
in get_gate_meta is unchanged. There is no on-chain event type
|
||
that could mutate gate_create's rules — the immutability is
|
||
structural."""
|
||
chain, members = _setup_gate_with_n_members(5)
|
||
base = 1_001_000.0
|
||
threshold = int(CONFIG["gate_lock_min_members"])
|
||
for i, m in enumerate(members[:threshold]):
|
||
chain.append(make_gate_lock("g1", m, ts=base + i, seq=200 + i))
|
||
assert is_locked("g1", chain)
|
||
|
||
# The gate's metadata is read from its FIRST gate_create event.
|
||
# find_snapshot-style first-write-wins: any forged subsequent
|
||
# gate_create with a different gate_id is ignored. We verify by
|
||
# appending a forged "amend" gate_create with conflicting rules.
|
||
from services.infonet.gates import get_gate_meta
|
||
from services.infonet.tests._gate_factory import make_gate_create
|
||
chain.append(make_gate_create("g1", "attacker", ts=base + 99999, seq=99999,
|
||
entry_sacrifice=0, min_overall_rep=0))
|
||
meta = get_gate_meta("g1", chain)
|
||
assert meta is not None
|
||
assert meta.entry_sacrifice == 5 # original value, unchanged
|
||
assert meta.creator_node_id == "creator"
|