mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-06-03 21:08:13 +02:00
1d7fa5185a
Private gate messages and offline DMs now ride the Infonet hashchain as ciphertext-only events, replicated across nodes via private transports (Tor onion / RNS / loopback) and decrypted only by parties holding the gate or recipient keys. Hashchain core (mesh_hashchain.py) ---------------------------------- * New ``append_private_gate_message`` and ``append_private_dm_message`` append paths with full signature verification, public-key binding, revocation check, and replay protection in a dedicated sequence domain (so a gate post does not consume the author's public broadcast sequence, and a DM cannot replay-block a public message at sequence=1). * Fork validation and full-chain validation now accept the gate signature compatibility variants — older signatures that canonicalize with/without epoch or reply_to still verify, so a re-sync from an older peer doesn't reject still-valid history. * DM hashchain spool: capped at 2 active sealed offline DMs per recipient mailbox, plus a per-(sender, recipient) cap so one prolific sender can't consume both slots. 1-hour TTL on the cap counter. Spool intentionally small — it's an offline bootstrap channel, not a persistent mailbox. * Rebuild-state preserves the gate sequence domain across reloads so a chain reload doesn't accidentally let an old gate sequence replay-collide on next append. Schema enforcement (mesh_schema.py) ----------------------------------- * Private gate + DM payloads have closed allowlists of fields. Plaintext keys (``message``, ``plaintext``, ``_local_plaintext``, ``_local_reply_to``) are explicit rejection-bait — they raise before the event ever touches the chain. * DM ciphertext + nonce must look like base64-ish sealed bytes; obvious base64-encoded plaintext shapes are rejected. * ``transport_lock`` required: DM hashchain spool requires ``private_strong``; gate accepts ``private``/``private_strong``/ ``rns``/``onion``. Defense-in-depth at the network layer (main.py + mesh_public.py) ---------------------------------------------------------------- * ``_infonet_sync_response_events`` now silently redacts private events (gate_message + dm_message) unless the request looks like a loopback / onion / RNS / private transport caller. If an operator accidentally exposes :8000 to the public internet, an external puller gets public events only — never ciphertext. * ``_sync_from_peer`` raises ``PeerSyncRateLimited`` for 429 (handled as 4-tuple return with retry_after_s) and ``PeerSyncHTTPError`` for other non-200 statuses (handled by ``_run_public_sync_cycle`` to honor server cooldown hints even outside the 429 path). DM relay hydration (main.py) ----------------------------- * New ``_hydrate_dm_relay_from_chain``: when accepted dm_message chain events arrive on a node, they get deposited into the local DM relay store with a deterministic sender_token_hash so re-sync of the same event is idempotent. Recipients see the ciphertext as a normal DM on their next poll and decrypt with their existing recipient key. Other surfaces -------------- * meshnode.bat / meshnode.sh now set ``MESH_INFONET_ALLOW_CLEARNET_SYNC= false`` and the participant runtime flags by default so a freshly spun-up node defaults to private-only sync. * InfonetTerminal/InfonetShell.tsx adds a gate directory renderer for the new private-gate workflow. * docker-compose.relay.yml binds the relay backend to 127.0.0.1:8000 only; Tor's hidden service forwards onion traffic into 127.0.0.1. Public clearnet :8000 stays off the network edge. Tests ----- * 7 new tests in test_private_gate_hashchain.py + test_private_dm_ hashchain.py covering: gate fork accepts ciphertext propagation, gate fork rejects plaintext, append rejects plaintext before normalize, append requires private_strong, append rejects non-sealed ciphertext shape, DM spool 2-per-recipient + 1-per-pair cap, DM hydration delivers to poll/claim. * Updated test_mesh_node_bootstrap_runtime.py covers 429 backoff via PeerSyncRateLimited 4-tuple AND PeerSyncHTTPError exception. * Updated test_s14b_public_sync_gate_filter.py + test_s9b_gate_store_ hydration.py + test_gate_write_cutover.py cover the new private redaction on public sync responses. * test_private_gate_hashchain.py + test_private_dm_hashchain.py: 10 passed locally. * Combined mesh-relevant suite (the 5 modified existing tests + 2 new): 17 passed. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
128 lines
3.8 KiB
Python
128 lines
3.8 KiB
Python
from services.mesh.mesh_infonet_sync_support import (
|
|
SyncWorkerState,
|
|
begin_sync,
|
|
eligible_sync_peers,
|
|
finish_sync,
|
|
finish_solo_sync,
|
|
should_run_sync,
|
|
)
|
|
from services.mesh.mesh_peer_store import make_bootstrap_peer_record, make_sync_peer_record
|
|
|
|
|
|
def test_eligible_sync_peers_filters_bucket_and_cooldown():
|
|
records = [
|
|
make_bootstrap_peer_record(
|
|
peer_url="https://seed.example",
|
|
transport="clearnet",
|
|
role="seed",
|
|
signer_id="bootstrap-a",
|
|
now=100,
|
|
),
|
|
make_sync_peer_record(
|
|
peer_url="https://active.example",
|
|
transport="clearnet",
|
|
now=100,
|
|
),
|
|
make_sync_peer_record(
|
|
peer_url="https://cooldown.example",
|
|
transport="clearnet",
|
|
now=100,
|
|
),
|
|
]
|
|
cooled = records[2]
|
|
records[2] = type(cooled)(**{**cooled.to_dict(), "cooldown_until": 500})
|
|
|
|
candidates = eligible_sync_peers(records, now=200)
|
|
|
|
assert [record.peer_url for record in candidates] == ["https://active.example"]
|
|
|
|
|
|
def test_eligible_sync_peers_prioritizes_explicit_bootstrap_seed():
|
|
old_runtime = make_sync_peer_record(
|
|
peer_url="https://old-runtime.example",
|
|
transport="clearnet",
|
|
role="participant",
|
|
source="runtime",
|
|
now=100,
|
|
)
|
|
seed = make_sync_peer_record(
|
|
peer_url="https://node.shadowbroker.info",
|
|
transport="clearnet",
|
|
role="seed",
|
|
source="bundle",
|
|
now=200,
|
|
)
|
|
|
|
candidates = eligible_sync_peers([old_runtime, seed], now=300)
|
|
|
|
assert [record.peer_url for record in candidates] == [
|
|
"https://node.shadowbroker.info",
|
|
"https://old-runtime.example",
|
|
]
|
|
|
|
|
|
def test_finish_sync_success_updates_schedule():
|
|
state = begin_sync(SyncWorkerState(), peer_url="https://seed.example", now=100)
|
|
finished = finish_sync(
|
|
state,
|
|
ok=True,
|
|
peer_url="https://seed.example",
|
|
current_head="abc123",
|
|
now=110,
|
|
interval_s=300,
|
|
)
|
|
|
|
assert finished.last_outcome == "ok"
|
|
assert finished.last_sync_ok_at == 110
|
|
assert finished.next_sync_due_at == 410
|
|
assert finished.current_head == "abc123"
|
|
assert not finished.fork_detected
|
|
|
|
|
|
def test_finish_sync_failure_surfaces_fork_without_auto_merging():
|
|
state = begin_sync(SyncWorkerState(), peer_url="https://seed.example", now=100)
|
|
finished = finish_sync(
|
|
state,
|
|
ok=False,
|
|
peer_url="https://seed.example",
|
|
error="fork detected",
|
|
fork_detected=True,
|
|
now=120,
|
|
failure_backoff_s=45,
|
|
)
|
|
|
|
assert finished.last_outcome == "fork"
|
|
assert finished.fork_detected is True
|
|
assert finished.last_error == "fork detected"
|
|
assert finished.consecutive_failures == 1
|
|
assert finished.next_sync_due_at == 165
|
|
assert should_run_sync(finished, now=150) is False
|
|
assert should_run_sync(finished, now=165) is True
|
|
|
|
|
|
def test_finish_solo_sync_marks_first_node_ready_without_peer_failure():
|
|
state = SyncWorkerState(current_head="genesis")
|
|
finished = finish_solo_sync(
|
|
state,
|
|
current_head="abc123",
|
|
now=200,
|
|
interval_s=300,
|
|
)
|
|
|
|
assert finished.last_outcome == "solo"
|
|
assert finished.last_error == ""
|
|
assert finished.last_peer_url == ""
|
|
assert finished.current_head == "abc123"
|
|
assert finished.consecutive_failures == 0
|
|
assert finished.next_sync_due_at == 500
|
|
assert should_run_sync(finished, now=499) is False
|
|
assert should_run_sync(finished, now=500) is True
|
|
|
|
|
|
def test_should_run_sync_recovers_stale_running_state():
|
|
fresh = SyncWorkerState(last_sync_started_at=100, last_outcome="running")
|
|
stale = SyncWorkerState(last_sync_started_at=100, last_outcome="running")
|
|
|
|
assert should_run_sync(fresh, now=399) is False
|
|
assert should_run_sync(stale, now=400) is True
|