mirror of
https://github.com/saymrwulf/crisis.git
synced 2026-09-06 19:30:35 +00:00
104 lines
3.6 KiB
Python
104 lines
3.6 KiB
Python
|
|
"""Tests for byzantine equivocation detection."""
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from crisis_agents.agent import MockAgent, MockByzantineAgent
|
||
|
|
from crisis_agents.alarm import AlarmEvent, scan_for_mutations
|
||
|
|
from crisis_agents.claim import Claim
|
||
|
|
from crisis_agents.mothership import Mothership
|
||
|
|
|
||
|
|
|
||
|
|
def _claim(sid: str, verdict: str = "true", evidence: str = "ok") -> Claim:
|
||
|
|
return Claim(statement_id=sid, verdict=verdict, confidence=0.9, # type: ignore[arg-type]
|
||
|
|
evidence=evidence, timestamp_logical=0)
|
||
|
|
|
||
|
|
|
||
|
|
def _equivocating_team() -> Mothership:
|
||
|
|
"""A 3-honest-1-byzantine team where the byzantine equivocates on s03."""
|
||
|
|
m = Mothership()
|
||
|
|
m.add_agent(MockAgent("a", [[]]))
|
||
|
|
m.add_agent(MockAgent("b", [[]]))
|
||
|
|
m.add_agent(MockAgent("c", [[]]))
|
||
|
|
m.open_boundary(MockByzantineAgent(
|
||
|
|
"d",
|
||
|
|
scripted_pairs=[(
|
||
|
|
_claim("s03", verdict="true", evidence="to_a"),
|
||
|
|
_claim("s03", verdict="false", evidence="to_b"),
|
||
|
|
)],
|
||
|
|
split_a={"a", "c"},
|
||
|
|
split_b={"b"},
|
||
|
|
))
|
||
|
|
return m
|
||
|
|
|
||
|
|
|
||
|
|
class TestAlarmDetection:
|
||
|
|
|
||
|
|
def test_no_alarms_in_honest_run(self):
|
||
|
|
m = Mothership()
|
||
|
|
m.add_agent(MockAgent("a", [[]]))
|
||
|
|
m.add_agent(MockAgent("b", [[]]))
|
||
|
|
m.open_boundary(MockAgent("d", [[_claim("s01")]]))
|
||
|
|
m.run_crisis_phase(num_turns=1)
|
||
|
|
alarms = scan_for_mutations(m)
|
||
|
|
assert alarms == []
|
||
|
|
|
||
|
|
def test_equivocation_raises_one_alarm(self):
|
||
|
|
m = _equivocating_team()
|
||
|
|
m.run_crisis_phase(num_turns=1)
|
||
|
|
alarms = scan_for_mutations(m)
|
||
|
|
assert len(alarms) == 1
|
||
|
|
|
||
|
|
a = alarms[0]
|
||
|
|
assert isinstance(a, AlarmEvent)
|
||
|
|
assert a.accused_agent == "d"
|
||
|
|
assert a.statement_id == "s03"
|
||
|
|
assert a.turn == 0
|
||
|
|
assert len(a.witnesses) == 2
|
||
|
|
|
||
|
|
def test_witness_digests_are_distinct(self):
|
||
|
|
m = _equivocating_team()
|
||
|
|
m.run_crisis_phase(num_turns=1)
|
||
|
|
a = scan_for_mutations(m)[0]
|
||
|
|
d1 = a.witnesses[0].message_digest_hex
|
||
|
|
d2 = a.witnesses[1].message_digest_hex
|
||
|
|
assert d1 != d2
|
||
|
|
|
||
|
|
def test_delivery_sets_are_disjoint(self):
|
||
|
|
m = _equivocating_team()
|
||
|
|
m.run_crisis_phase(num_turns=1)
|
||
|
|
a = scan_for_mutations(m)[0]
|
||
|
|
s1 = set(a.witnesses[0].delivered_to)
|
||
|
|
s2 = set(a.witnesses[1].delivered_to)
|
||
|
|
assert s1 & s2 == set()
|
||
|
|
|
||
|
|
def test_spacelike_verified_is_true(self):
|
||
|
|
"""The Crisis layer should confirm the witness vertices are causally
|
||
|
|
incomparable in at least one honest graph."""
|
||
|
|
m = _equivocating_team()
|
||
|
|
m.run_crisis_phase(num_turns=1)
|
||
|
|
a = scan_for_mutations(m)[0]
|
||
|
|
assert a.spacelike_verified is True
|
||
|
|
|
||
|
|
def test_duplicate_broadcast_is_not_equivocation(self):
|
||
|
|
"""If a byzantine emits the SAME payload to two disjoint subsets,
|
||
|
|
the message digests are identical and it's not equivocation."""
|
||
|
|
same = _claim("s03", verdict="true", evidence="same evidence")
|
||
|
|
m = Mothership()
|
||
|
|
m.add_agent(MockAgent("a", [[]]))
|
||
|
|
m.add_agent(MockAgent("b", [[]]))
|
||
|
|
m.open_boundary(MockByzantineAgent(
|
||
|
|
"d",
|
||
|
|
scripted_pairs=[(same, same)],
|
||
|
|
split_a={"a"},
|
||
|
|
split_b={"b"},
|
||
|
|
))
|
||
|
|
m.run_crisis_phase(num_turns=1)
|
||
|
|
alarms = scan_for_mutations(m)
|
||
|
|
# Same payload → same nonce-mined message after PoW → same digest →
|
||
|
|
# no equivocation. (The byzantine has to actually say different
|
||
|
|
# things to be caught.)
|
||
|
|
assert alarms == [] or all(
|
||
|
|
len({w.message_digest_hex for w in alarm.witnesses}) > 1
|
||
|
|
for alarm in alarms
|
||
|
|
)
|