File size: 8,480 Bytes
da3d6db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
"""test_anonymous_opt_in_dht_sharing.py

Comprehensive Pytest Suite for Armiger Distributed Anonymous & Opt-In-By-Default
Weight and Catalog Data Sharing (`opt_in_sharing=False` by default + `AnonymityGuard`).
"""
from __future__ import annotations

import asyncio
import json
import math
import os
from pathlib import Path
import sys

import pytest

ROOT_DIR = Path(__file__).resolve().parent
if str(ROOT_DIR) not in sys.path:
    sys.path.insert(0, str(ROOT_DIR))
from armiger import ArmigerPipeline
from armiger_dht_db_sync import ArmigerDHTDBSync, ArmigerDHTDatabaseSync
from armiger_dht_rsi import (
    AnonymityGuard,
    AnonymityViolationError,
    ArmigerDHTRSIEngine,
    AudioWebhookAdapter,
    MovieWebhookAdapter,
    PublicationWebhookAdapter,
    QBittorrentAdapter,
    SeriesWebhookAdapter,
    TransmissionAdapter,
    resolve_opt_in_sharing,
)
from armiger_ultra import ArmigerUltraPipeline
from bep44_udp_wire_harness import LocalKademliaSwarm
from benchmarks.proof_anonymous_opt_in_sharing import (
    generate_500_adversarial_canaries,
    run_proof_1_default_zero_egress,
    run_proof_2_canary_pii_firewall,
    run_proof_3_unlinkability_and_dp_convergence,
)


def test_opt_in_sharing_env_and_default_resolution(monkeypatch: pytest.MonkeyPatch) -> None:
    """Verify resolve_opt_in_sharing defaults to False and only enables on explicit True or env 1/true/yes."""
    monkeypatch.delenv("ARMIGER_DHT_SHARE_OPT_IN", raising=False)
    assert resolve_opt_in_sharing() is False
    assert resolve_opt_in_sharing(False) is False
    assert resolve_opt_in_sharing(True) is True

    for falsy in ("0", "false", "no", "off", "", "random"):
        monkeypatch.setenv("ARMIGER_DHT_SHARE_OPT_IN", falsy)
        assert resolve_opt_in_sharing() is False

    for truthy in ("1", "true", "TRUE", "yes", "YES"):
        monkeypatch.setenv("ARMIGER_DHT_SHARE_OPT_IN", truthy)
        assert resolve_opt_in_sharing() is True
        # Explicit False argument still overrides env var
        assert resolve_opt_in_sharing(False) is False


def test_all_adapters_and_engines_default_opt_in_false(monkeypatch: pytest.MonkeyPatch) -> None:
    """Verify every adapter, engine, DB sync, pipeline, and bridge defaults to opt_in_sharing=False."""
    monkeypatch.delenv("ARMIGER_DHT_SHARE_OPT_IN", raising=False)
    rsi = ArmigerDHTRSIEngine()
    db = ArmigerDHTDBSync()
    s_ad = SeriesWebhookAdapter()
    m_ad = MovieWebhookAdapter()
    a_ad = AudioWebhookAdapter()
    p_ad = PublicationWebhookAdapter()
    q_ad = QBittorrentAdapter()
    t_ad = TransmissionAdapter()

    for obj in (rsi, db, s_ad, m_ad, a_ad, p_ad, q_ad, t_ad):
        assert obj.opt_in_sharing is False
        st = obj.sharing_status()
        assert st["opt_in_sharing_enabled"] is False
        assert st["outbound_bundles_shared"] == 0
        assert st["outbound_bytes_shared"] == 0

    assert rsi.export_signed_bundle(delta_w=[0.1, 0.2], delta_b=[0.0], n_samples=50) is None
    assert rsi.export_signed_delta_bundle(delta_w=[0.1, 0.2], delta_b=[0.0], n_samples=50) is None
    assert rsi.broadcast_weights([0.1, 0.2], [0.0], n_samples=50) is None
    assert rsi.get_pending_telemetry_batch() == []
    assert rsi.sharing_status()["outbound_packets_blocked_opt_out"] >= 4

    assert db.export_signed_bundle() is None
    assert db.export_signed_delta_bundle() is None
    assert db.sharing_status()["outbound_packets_blocked_opt_out"] >= 2
    db.close()


def test_proof_1_default_zero_egress_100_events() -> None:
    """Execute Proof 1: 100 synthetic webhook/catalog events across default instances -> 0 outbound packets."""
    res = asyncio.run(run_proof_1_default_zero_egress())
    assert res["passed"] is True
    assert res["synthetic_events_processed"] == 100
    assert res["all_components_default_opt_in_false"] is True
    assert res["outbound_bundles_shared"] == 0
    assert res["outbound_bytes_shared"] == 0
    assert res["udp_put_packets_emitted"] == 0
    assert res["outbound_attempts_blocked_by_opt_out"] >= 100
    assert res["inbound_readonly_verification_passed"] is True


def test_anonymity_guard_ephemeral_key_rotation_and_coarse_timestamps() -> None:
    """Verify Ephemeral Ed25519 key rotation and coarse UTC hour timestamp bucketing."""
    guard = AnonymityGuard(ephemeral_identity=True, min_k_samples=25, clip_norm=1.0, dp_sigma=0.05)
    seen_keys = {guard.current_pubkey_hex()}
    for ep in range(20):
        new_pk = guard.rotate_ephemeral_identity(epoch=ep)
        assert len(new_pk) == 64
        assert new_pk not in seen_keys
        seen_keys.add(new_pk)

    # Coarse UTC hour bucketing: floor(ts / 3600) * 3600
    for offset in (0, 1, 42, 1799, 3599):
        b_ts = guard.bucket_timestamp(1792350000 + offset)
        assert b_ts == 1792350000
        assert b_ts % 3600 == 0


def test_anonymity_guard_l2_clipping_dp_and_k_anonymity() -> None:
    """Verify L2 norm clipping, Gaussian DP noise, int8 quantization, and k-anonymity enforcement."""
    guard = AnonymityGuard(min_k_samples=25, clip_norm=1.0, dp_sigma=0.05)

    # Rejects n_samples < 25
    for bad_n in (0, 1, 5, 12, 24):
        with pytest.raises(AnonymityViolationError, match="k_anonymity"):
            guard.clip_and_privatize_weights([0.5, -0.5], [0.1], n_samples=bad_n)

    # Clips large vectors with L2 norm >> 1.0 down to <= 1.0
    huge_w = [10.0, -10.0, 10.0, -10.0]  # L2 norm = 20.0
    huge_b = [5.0, -5.0]
    res = guard.clip_and_privatize_weights(huge_w, huge_b, n_samples=55)
    assert res["pre_dp_l2_norm"] > 20.0
    assert res["clipped_l2_norm"] <= 1.0 + 1e-9
    post_dp_l2 = math.sqrt(sum(x * x for x in res["delta_w_dp"]) + sum(x * x for x in res["delta_b_dp"]))
    assert post_dp_l2 <= 1.05
    assert res["n_bucket"] == 50
    assert all(-127 <= q <= 127 for q in res["delta_w_q8"])

    # Reliability bins k-anonymity check
    with pytest.raises(AnonymityViolationError, match="k_anonymity"):
        guard.sanitize_reliability_bins([{"bin": 0, "conf": 0.5, "acc": 0.5, "n": 10}])

    clean_bins = guard.sanitize_reliability_bins([{"bin": 0, "conf": 0.8, "acc": 0.78, "n": 42}])
    assert len(clean_bins) == 1
    assert clean_bins[0][3] == 40


def test_anonymity_guard_domain_separated_blake2b_catalog_hashing() -> None:
    """Verify 16-hex-char BLAKE2b-64(domain='armiger-catalog-v2' || normalized_key) and PII rejection."""
    h1 = AnonymityGuard.hash_catalog_key("sintel|2010")
    h2 = AnonymityGuard.hash_catalog_key("  SINTEL | 2010  ")
    assert h1 == h2
    assert len(h1) == 16
    assert all(c in "0123456789abcdef" for c in h1)

    # Rejects raw filenames, release titles, IPs, paths, URLs, infohashes, external IDs
    bad_keys = [
        "/" + "Users" + "/alice/Downloads/Sintel.2010.mkv",
        "Sintel.2010.2160p.UHD.BluRay.REMUX.HEVC.TrueHD.7.1",
        "magnet:?xt=urn:btih:1111222233334444555566667777888899990000",
        "198.51.100.42",
        "tt1234567",
        "tmdb:693134",
        "https://tracker.example.org/announce?passkey=secret123",
    ]
    for bk in bad_keys:
        with pytest.raises(AnonymityViolationError):
            AnonymityGuard.hash_catalog_key(bk)


def test_proof_2_500_adversarial_canaries_blocked_zero_wire_leak() -> None:
    """Execute Proof 2: 500 adversarial canaries across 12-node UDP swarm -> 500/500 blocked, 0/500 on wire."""
    canaries = generate_500_adversarial_canaries()
    assert len(canaries) == 500
    res = asyncio.run(run_proof_2_canary_pii_firewall())
    assert res["passed"] is True
    assert res["total_adversarial_canaries_injected"] == 500
    assert res["canaries_blocked_or_stripped_before_wire"] == 500
    assert res["block_rate_pct"] == 100.0
    assert res["canary_substrings_on_udp_wire"] == 0
    assert res["wire_leak_rate_pct"] == 0.0


def test_proof_3_12_node_udp_swarm_unlinkability_and_dp_convergence() -> None:
    """Execute Proof 3: 12-node UDP swarm over 5 epochs -> 60/60 unique keys, ECE & F1 convergence."""
    res = asyncio.run(run_proof_3_unlinkability_and_dp_convergence())
    assert res["passed"] is True
    assert res["swarm_nodes"] == 12
    assert res["epochs_run"] == 5
    assert res["unique_ephemeral_ed25519_pubkeys"] == 60
    assert res["cross_epoch_key_reuse_count"] == 0
    assert res["coarse_hour_timestamp_bucketing_rate_pct"] == 100.0
    assert res["final_calibration_ece"] < res["initial_calibration_ece"] * 0.35
    assert res["final_armiger_ultra_f1"] > res["initial_armiger_ultra_f1"] + 0.025