mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-08-09 12:16:01 +02:00
280 lines
12 KiB
Python
280 lines
12 KiB
Python
"""Sprint 7 — upgrade-hash governance.
|
|
|
|
Maps to RULES §3.15 + §5.6:
|
|
- 80% supermajority (vs 67% for param petitions)
|
|
- 40% quorum (vs 30%)
|
|
- 67% of Heavy Nodes signal ready before activation
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from services.infonet.config import CONFIG
|
|
from services.infonet.governance import compute_upgrade_state
|
|
from services.infonet.tests._chain_factory import make_event, make_market_chain
|
|
|
|
|
|
_DAY_S = 86400.0
|
|
_HOUR_S = 3600.0
|
|
|
|
|
|
def _propose(filer: str, proposal_id: str, *,
|
|
ts: float, seq: int,
|
|
release_hash: str = "abc123",
|
|
target: str = "0.2.0") -> dict:
|
|
return make_event(
|
|
"upgrade_propose", filer,
|
|
{"proposal_id": proposal_id, "release_hash": release_hash,
|
|
"release_description": "feature x",
|
|
"target_protocol_version": target},
|
|
timestamp=ts, sequence=seq,
|
|
)
|
|
|
|
|
|
def _sign(signer: str, proposal_id: str, *, ts: float, seq: int) -> dict:
|
|
return make_event(
|
|
"upgrade_sign", signer,
|
|
{"proposal_id": proposal_id},
|
|
timestamp=ts, sequence=seq,
|
|
)
|
|
|
|
|
|
def _vote(voter: str, proposal_id: str, vote: str, *, ts: float, seq: int) -> dict:
|
|
return make_event(
|
|
"upgrade_vote", voter,
|
|
{"proposal_id": proposal_id, "vote": vote},
|
|
timestamp=ts, sequence=seq,
|
|
)
|
|
|
|
|
|
def _signal_ready(node: str, proposal_id: str, release_hash: str, *,
|
|
ts: float, seq: int) -> dict:
|
|
return make_event(
|
|
"upgrade_signal_ready", node,
|
|
{"proposal_id": proposal_id, "release_hash": release_hash},
|
|
timestamp=ts, sequence=seq,
|
|
)
|
|
|
|
|
|
def _seed_oracle(node_id: str, base_ts: float, market_id: str,
|
|
stake: float = 100.0) -> list[dict]:
|
|
return make_market_chain(
|
|
market_id, "creator", outcome="yes",
|
|
predictions=[
|
|
{"node_id": node_id, "side": "yes", "stake_amount": stake},
|
|
{"node_id": f"{node_id}-loser", "side": "no", "stake_amount": stake},
|
|
],
|
|
base_ts=base_ts, participants=5, total_stake=stake * 2,
|
|
)
|
|
|
|
|
|
def test_unknown_proposal_is_not_found():
|
|
state = compute_upgrade_state("nope", [], now=1.0)
|
|
assert state.status == "not_found"
|
|
|
|
|
|
def test_filed_proposal_in_signatures_phase():
|
|
base = 1_000_000.0
|
|
chain = [_propose("alice", "u1", ts=base, seq=1)]
|
|
state = compute_upgrade_state("u1", chain, now=base + 1)
|
|
assert state.status == "signatures"
|
|
|
|
|
|
def test_failed_signatures_after_window():
|
|
base = 1_000_000.0
|
|
chain = [_propose("alice", "u1", ts=base, seq=1)]
|
|
sig_window = float(CONFIG["upgrade_signature_window_days"]) * _DAY_S
|
|
state = compute_upgrade_state("u1", chain, now=base + sig_window + 1)
|
|
assert state.status == "failed_signatures"
|
|
|
|
|
|
def test_supermajority_higher_than_param_petitions():
|
|
"""RULES §5.6: upgrade requires 80% (param petitions: 67%).
|
|
|
|
A proposal with 70% support PASSES a param petition but FAILS an
|
|
upgrade. We verify the threshold separation by reading CONFIG."""
|
|
assert float(CONFIG["upgrade_supermajority"]) > float(CONFIG["petition_supermajority"])
|
|
assert float(CONFIG["upgrade_supermajority"]) >= 0.80
|
|
|
|
|
|
def test_quorum_higher_than_param_petitions():
|
|
assert float(CONFIG["upgrade_quorum"]) > float(CONFIG["petition_quorum"])
|
|
assert float(CONFIG["upgrade_quorum"]) >= 0.40
|
|
|
|
|
|
def test_proposal_passes_to_challenge_with_supermajority():
|
|
base = 1_000_000.0
|
|
chain = []
|
|
chain += _seed_oracle("alice", base, "m1", stake=1000.0)
|
|
chain.append(_propose("alice", "u1", ts=base + 100_000, seq=200))
|
|
chain.append(_sign("alice", "u1", ts=base + 100_100, seq=201))
|
|
chain.append(_vote("alice", "u1", "for", ts=base + 100_500, seq=202))
|
|
sig_ts = base + 100_100
|
|
vote_window = float(CONFIG["upgrade_vote_window_days"]) * _DAY_S
|
|
state = compute_upgrade_state("u1", chain, now=sig_ts + vote_window + 1)
|
|
# alice has effectively 100% of the network's oracle rep → 100%
|
|
# vote share, well above 80% threshold + 40% quorum.
|
|
assert state.status == "challenge"
|
|
|
|
|
|
def test_proposal_advances_to_activation_after_challenge_window():
|
|
base = 1_000_000.0
|
|
chain = []
|
|
chain += _seed_oracle("alice", base, "m1", stake=1000.0)
|
|
chain.append(_propose("alice", "u1", ts=base + 100_000, seq=200))
|
|
chain.append(_sign("alice", "u1", ts=base + 100_100, seq=201))
|
|
chain.append(_vote("alice", "u1", "for", ts=base + 100_500, seq=202))
|
|
|
|
sig_ts = base + 100_100
|
|
vote_window = float(CONFIG["upgrade_vote_window_days"]) * _DAY_S
|
|
challenge_window = float(CONFIG["upgrade_challenge_window_hours"]) * _HOUR_S
|
|
after = sig_ts + vote_window + challenge_window + 1
|
|
state = compute_upgrade_state("u1", chain, now=after)
|
|
assert state.status == "activation"
|
|
|
|
|
|
def test_activation_threshold_67pct_of_heavy_nodes():
|
|
"""At ≥67% Heavy Node readiness, status reports threshold_met=True.
|
|
|
|
7 of 10 = 0.70 clearly crosses the 0.67 threshold (6 of 9 = 0.666
|
|
falls short — that boundary is tested separately below).
|
|
"""
|
|
base = 1_000_000.0
|
|
chain = []
|
|
chain += _seed_oracle("alice", base, "m1", stake=1000.0)
|
|
chain.append(_propose("alice", "u1", ts=base + 100_000, seq=200,
|
|
release_hash="rel-x"))
|
|
chain.append(_sign("alice", "u1", ts=base + 100_100, seq=201))
|
|
chain.append(_vote("alice", "u1", "for", ts=base + 100_500, seq=202))
|
|
|
|
sig_ts = base + 100_100
|
|
vote_window = float(CONFIG["upgrade_vote_window_days"]) * _DAY_S
|
|
challenge_window = float(CONFIG["upgrade_challenge_window_hours"]) * _HOUR_S
|
|
activation_start = sig_ts + vote_window + challenge_window + 1
|
|
# 10 Heavy Nodes total, 7 signal ready → 70%.
|
|
heavy_set = {f"h{i}" for i in range(10)}
|
|
for i, h in enumerate(sorted(heavy_set)[:7]):
|
|
chain.append(_signal_ready(h, "u1", "rel-x",
|
|
ts=activation_start + i, seq=300 + i))
|
|
|
|
state = compute_upgrade_state("u1", chain, now=activation_start + 100,
|
|
heavy_node_ids=heavy_set)
|
|
assert state.readiness.total_heavy_nodes == 10
|
|
assert state.readiness.ready_count == 7
|
|
assert state.readiness.threshold_met is True
|
|
|
|
|
|
def test_activation_at_2_3_falls_short_of_67pct():
|
|
"""6/9 = 0.6666 < 0.67 — boundary check confirms the threshold is
|
|
a strict ≥ (not floating-point loose)."""
|
|
base = 1_000_000.0
|
|
chain = []
|
|
chain += _seed_oracle("alice", base, "m1", stake=1000.0)
|
|
chain.append(_propose("alice", "u1", ts=base + 100_000, seq=200,
|
|
release_hash="rel-x"))
|
|
chain.append(_sign("alice", "u1", ts=base + 100_100, seq=201))
|
|
chain.append(_vote("alice", "u1", "for", ts=base + 100_500, seq=202))
|
|
|
|
sig_ts = base + 100_100
|
|
vote_window = float(CONFIG["upgrade_vote_window_days"]) * _DAY_S
|
|
challenge_window = float(CONFIG["upgrade_challenge_window_hours"]) * _HOUR_S
|
|
activation_start = sig_ts + vote_window + challenge_window + 1
|
|
heavy_set = {f"h{i}" for i in range(9)}
|
|
for i, h in enumerate(sorted(heavy_set)[:6]):
|
|
chain.append(_signal_ready(h, "u1", "rel-x",
|
|
ts=activation_start + i, seq=300 + i))
|
|
|
|
state = compute_upgrade_state("u1", chain, now=activation_start + 100,
|
|
heavy_node_ids=heavy_set)
|
|
# 6/9 = 0.6666... is strictly less than 0.67.
|
|
assert not state.readiness.threshold_met
|
|
|
|
|
|
def test_activation_below_67pct_does_not_meet_threshold():
|
|
base = 1_000_000.0
|
|
chain = []
|
|
chain += _seed_oracle("alice", base, "m1", stake=1000.0)
|
|
chain.append(_propose("alice", "u1", ts=base + 100_000, seq=200,
|
|
release_hash="rel-x"))
|
|
chain.append(_sign("alice", "u1", ts=base + 100_100, seq=201))
|
|
chain.append(_vote("alice", "u1", "for", ts=base + 100_500, seq=202))
|
|
|
|
sig_ts = base + 100_100
|
|
vote_window = float(CONFIG["upgrade_vote_window_days"]) * _DAY_S
|
|
challenge_window = float(CONFIG["upgrade_challenge_window_hours"]) * _HOUR_S
|
|
activation_start = sig_ts + vote_window + challenge_window + 1
|
|
heavy_set = {f"h{i}" for i in range(10)}
|
|
# Only 6 of 10 = 60% (below 67%).
|
|
for i, h in enumerate(sorted(heavy_set)[:6]):
|
|
chain.append(_signal_ready(h, "u1", "rel-x",
|
|
ts=activation_start + i, seq=300 + i))
|
|
state = compute_upgrade_state("u1", chain, now=activation_start + 100,
|
|
heavy_node_ids=heavy_set)
|
|
assert state.readiness.threshold_met is False
|
|
|
|
|
|
def test_signal_ready_with_wrong_release_hash_ignored():
|
|
"""An attacker can't speed up activation by signaling readiness for
|
|
a different release."""
|
|
base = 1_000_000.0
|
|
chain = []
|
|
chain += _seed_oracle("alice", base, "m1", stake=1000.0)
|
|
chain.append(_propose("alice", "u1", ts=base + 100_000, seq=200,
|
|
release_hash="rel-x"))
|
|
chain.append(_sign("alice", "u1", ts=base + 100_100, seq=201))
|
|
chain.append(_vote("alice", "u1", "for", ts=base + 100_500, seq=202))
|
|
|
|
sig_ts = base + 100_100
|
|
vote_window = float(CONFIG["upgrade_vote_window_days"]) * _DAY_S
|
|
challenge_window = float(CONFIG["upgrade_challenge_window_hours"]) * _HOUR_S
|
|
activation_start = sig_ts + vote_window + challenge_window + 1
|
|
heavy_set = {f"h{i}" for i in range(3)}
|
|
# All 3 heavies signal ready, but for a DIFFERENT release_hash.
|
|
for i, h in enumerate(sorted(heavy_set)):
|
|
chain.append(_signal_ready(h, "u1", "rel-FORGED",
|
|
ts=activation_start + i, seq=300 + i))
|
|
state = compute_upgrade_state("u1", chain, now=activation_start + 100,
|
|
heavy_node_ids=heavy_set)
|
|
assert state.readiness.ready_count == 0
|
|
assert not state.readiness.threshold_met
|
|
|
|
|
|
def test_signal_ready_from_non_heavy_node_ignored():
|
|
base = 1_000_000.0
|
|
chain = []
|
|
chain += _seed_oracle("alice", base, "m1", stake=1000.0)
|
|
chain.append(_propose("alice", "u1", ts=base + 100_000, seq=200,
|
|
release_hash="rel-x"))
|
|
chain.append(_sign("alice", "u1", ts=base + 100_100, seq=201))
|
|
chain.append(_vote("alice", "u1", "for", ts=base + 100_500, seq=202))
|
|
|
|
sig_ts = base + 100_100
|
|
vote_window = float(CONFIG["upgrade_vote_window_days"]) * _DAY_S
|
|
challenge_window = float(CONFIG["upgrade_challenge_window_hours"]) * _HOUR_S
|
|
activation_start = sig_ts + vote_window + challenge_window + 1
|
|
heavy_set = {"h1", "h2"}
|
|
# "imposter" is NOT in heavy_set — readiness signal should be ignored.
|
|
chain.append(_signal_ready("imposter", "u1", "rel-x",
|
|
ts=activation_start + 1, seq=300))
|
|
state = compute_upgrade_state("u1", chain, now=activation_start + 100,
|
|
heavy_node_ids=heavy_set)
|
|
assert state.readiness.ready_count == 0
|
|
|
|
|
|
def test_failed_activation_after_window_expires():
|
|
base = 1_000_000.0
|
|
chain = []
|
|
chain += _seed_oracle("alice", base, "m1", stake=1000.0)
|
|
chain.append(_propose("alice", "u1", ts=base + 100_000, seq=200,
|
|
release_hash="rel-x"))
|
|
chain.append(_sign("alice", "u1", ts=base + 100_100, seq=201))
|
|
chain.append(_vote("alice", "u1", "for", ts=base + 100_500, seq=202))
|
|
|
|
sig_ts = base + 100_100
|
|
vote_window = float(CONFIG["upgrade_vote_window_days"]) * _DAY_S
|
|
challenge_window = float(CONFIG["upgrade_challenge_window_hours"]) * _HOUR_S
|
|
activation_window = float(CONFIG["upgrade_activation_window_days"]) * _DAY_S
|
|
after = sig_ts + vote_window + challenge_window + activation_window + 1
|
|
state = compute_upgrade_state("u1", chain, now=after,
|
|
heavy_node_ids={"h1", "h2"})
|
|
assert state.status == "failed_activation"
|