spencernorton commited on
Commit
bb20df8
·
verified ·
1 Parent(s): 218247f

Release armiger-dht-db-sync v2.1.0 (BEP-44/46 Byzantine-resilient RSI, multi-instance transfer proof, UDP wire harness, 45/45 red-team hardened)

Browse files
armiger_dht_db_sync.py CHANGED
@@ -29,6 +29,7 @@ from __future__ import annotations
29
  import dataclasses
30
  import enum
31
  import hashlib
 
32
  import json
33
  import os
34
  import pathlib
@@ -167,16 +168,16 @@ class TorrentRecord:
167
  def canonical_dict(self) -> dict[str, Any]:
168
  """Deterministic dictionary for serialization and hashing."""
169
  return {
170
- "category": self.category.lower(),
171
- "content_sha256": self.content_sha256.lower(),
172
  "created_at": int(self.created_at),
173
- "epoch": self.epoch.lower(),
174
  "file_count": int(self.file_count),
175
  "file_list": sorted(self.file_list),
176
- "infohash": self.infohash.lower(),
177
- "language": self.language.lower(),
178
  "leechers": int(self.leechers),
179
- "quality_tier": self.quality_tier.lower(),
180
  "safety_gate_passed": bool(self.safety_gate_passed),
181
  "seeders": int(self.seeders),
182
  "size_bytes": int(self.size_bytes),
@@ -238,7 +239,7 @@ class DHTShardManifest:
238
  return dataclasses.replace(self, attestations=list(attestations))
239
 
240
  def canonical_dict_without_sig(self) -> dict[str, Any]:
241
- return {
242
  "bep46_infohash": self.bep46_infohash.lower(),
243
  "ciphertext_sha256": self.ciphertext_sha256.lower(),
244
  "compressed_size_bytes": int(self.compressed_size_bytes),
@@ -251,6 +252,9 @@ class DHTShardManifest:
251
  "timestamp": int(self.timestamp),
252
  "uncompressed_size_bytes": int(self.uncompressed_size_bytes),
253
  }
 
 
 
254
 
255
  def canonical_payload(self) -> bytes:
256
  return json.dumps(self.canonical_dict_without_sig(), sort_keys=True, separators=(",", ":")).encode("utf-8")
@@ -260,8 +264,8 @@ class DHTShardManifest:
260
 
261
  def bep44_target(self, salt: str | None = None) -> str:
262
  """Compute BEP-44 DHT target lookup key: SHA1(pubkey || salt)."""
263
- pub_bytes = bytes.fromhex(self.root_pubkey_hex)
264
- salt_bytes = (salt or f"armiger-shard:{self.shard_id}").encode("utf-8")
265
  return hashlib.sha1(pub_bytes + salt_bytes).hexdigest()
266
 
267
 
@@ -270,7 +274,9 @@ class DHTShardManifest:
270
  # ---------------------------------------------------------------------------
271
 
272
  class MerkleTree:
273
- """Binary Merkle tree over canonical record leaf hashes."""
 
 
274
 
275
  def __init__(self, leaves: Sequence[bytes]) -> None:
276
  self.leaves = list(leaves)
@@ -290,7 +296,7 @@ class MerkleTree:
290
  next_level: list[bytes] = []
291
  for i in range(0, len(current), 2):
292
  left = current[i]
293
- right = current[i + 1] if i + 1 < len(current) else left
294
  parent = hashlib.sha256(left + right).digest()
295
  next_level.append(parent)
296
  self.levels.append(next_level)
@@ -313,8 +319,9 @@ class MerkleTree:
313
  is_right = (cur_idx % 2 == 0)
314
  sib_idx = cur_idx + 1 if is_right else cur_idx - 1
315
  if sib_idx >= len(level):
316
- sib_idx = cur_idx # Odd leaf duplicated
317
- proof.append((level[sib_idx].hex(), is_right))
 
318
  cur_idx //= 2
319
 
320
  return proof
@@ -322,14 +329,31 @@ class MerkleTree:
322
  @staticmethod
323
  def verify_proof(leaf_hash: bytes, proof: Sequence[tuple[str, bool]], root_hex: str) -> bool:
324
  """Verify an inclusion proof against expected Merkle root."""
325
- current = leaf_hash
326
- for sib_hex, is_right in proof:
327
- sib = bytes.fromhex(sib_hex)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
  if is_right:
329
  current = hashlib.sha256(current + sib).digest()
330
  else:
331
  current = hashlib.sha256(sib + current).digest()
332
- return current.hex().lower() == root_hex.lower()
333
 
334
 
335
  # ---------------------------------------------------------------------------
@@ -521,16 +545,41 @@ def compress_payload(data: bytes) -> tuple[bytes, str]:
521
  return zlib.compress(data, level=9), "zlib"
522
 
523
 
524
- def decompress_payload(compressed: bytes, codec: str = "zstd") -> bytes:
525
- """Decompress payload using specified codec."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
  if codec == "zstd":
527
  if _HAS_ZSTD:
528
- dctx = zstd.ZstdDecompressor()
529
- return dctx.decompress(compressed)
530
- # If zstd not present, attempt zlib decompression as transparent fallback
531
- return zlib.decompress(compressed)
 
 
 
 
 
532
  elif codec == "zlib":
533
- return zlib.decompress(compressed)
534
  raise ValueError(f"Unsupported compression codec: {codec}")
535
 
536
 
@@ -645,7 +694,10 @@ class ShardDatabase:
645
 
646
  def insert_record(self, record: TorrentRecord) -> bool:
647
  """Insert a torrent record into the appropriate partitioned table."""
648
- is_adult = (record.category.lower() == CATEGORY_ADULT)
 
 
 
649
  if is_adult and not self.enable_xxx:
650
  return False # Drop un-opted adult record
651
 
@@ -660,12 +712,12 @@ class ShardDatabase:
660
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
661
  """,
662
  (
663
- record.infohash.lower(),
664
  record.title,
665
- record.category.lower(),
666
- record.language.lower(),
667
- record.quality_tier.lower(),
668
- record.epoch.lower(),
669
  record.size_bytes,
670
  record.file_count,
671
  record.seeders,
@@ -684,16 +736,19 @@ class ShardDatabase:
684
  main_rows: list[tuple[Any, ...]] = []
685
  adult_rows: list[tuple[Any, ...]] = []
686
  for record in records:
687
- is_adult = (record.category.lower() == CATEGORY_ADULT)
 
 
 
688
  if is_adult and not self.enable_xxx:
689
  continue
690
  row = (
691
- record.infohash.lower(),
692
  record.title,
693
- record.category.lower(),
694
- record.language.lower(),
695
- record.quality_tier.lower(),
696
- record.epoch.lower(),
697
  record.size_bytes,
698
  record.file_count,
699
  record.seeders,
@@ -943,19 +998,30 @@ class QuorumVerifier:
943
  4. Distinct ASNs >= min_distinct_asns
944
  5. Each attestation Hybrid Ed25519 + WOTS+-SHAKE256 signature
945
  """
 
 
 
 
 
 
 
 
946
  # 1. Monotonic sequence check
947
  if manifest.seq <= current_seq:
948
  return False, f"stale_or_downgrade_sequence:{manifest.seq}<={current_seq}"
949
 
950
  # 2. Root Ed25519 signature check
951
- root_pub_bytes = bytes.fromhex(manifest.root_pubkey_hex)
952
- if self.trusted_roots and root_pub_bytes not in self.trusted_roots:
 
 
 
953
  return False, f"untrusted_root_pubkey:{manifest.root_pubkey_hex}"
954
 
955
  payload = manifest.canonical_payload()
956
  try:
957
  vk = Ed25519PublicKey.from_public_bytes(root_pub_bytes)
958
- vk.verify(bytes.fromhex(manifest.ed25519_signature_hex), payload)
959
  except (InvalidSignature, ValueError) as exc:
960
  return False, f"invalid_root_ed25519_signature:{exc}"
961
 
@@ -979,16 +1045,48 @@ class QuorumVerifier:
979
  manifest_digest = manifest.canonical_digest()
980
  verified_asns: set[int] = set()
981
  seen_node_ids: set[str] = set()
 
 
 
 
 
982
 
983
  for att in manifest.attestations:
984
- # Prevent node duplicate Sybil
985
- if att.node_id.lower() in seen_node_ids:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
986
  continue
987
- seen_node_ids.add(att.node_id.lower())
988
 
989
  try:
990
- node_vk = Ed25519PublicKey.from_public_bytes(bytes.fromhex(att.pubkey_hex))
991
- node_vk.verify(bytes.fromhex(att.signature_hex), manifest_digest)
992
  except (InvalidSignature, ValueError):
993
  continue
994
 
@@ -1138,10 +1236,11 @@ class ArmigerDHTDatabaseSync:
1138
  ciphertext_sha = hashlib.sha256(ciphertext_bundle).hexdigest()
1139
  bep46_infohash = hashlib.sha1(ciphertext_bundle).hexdigest()
1140
 
1141
- # 3. Create manifest and sign with Ed25519 root private key
1142
  sk = Ed25519PrivateKey.from_private_bytes(root_privkey)
1143
  pk = sk.public_key()
1144
  root_pub_bytes = pk.public_bytes(Encoding.Raw, PublicFormat.Raw)
 
1145
 
1146
  manifest_dummy = DHTShardManifest(
1147
  seq=seq,
@@ -1157,11 +1256,11 @@ class ArmigerDHTDatabaseSync:
1157
  root_pubkey_hex=root_pub_bytes.hex(),
1158
  ed25519_signature_hex="",
1159
  attestations=list(attestations or []),
 
1160
  )
1161
 
1162
  payload_bytes = manifest_dummy.canonical_payload()
1163
  sig = sk.sign(payload_bytes)
1164
- pq_priv, pq_pub = WOTSPlusSHAKE256.derive_keypair_from_ed25519_priv(root_privkey)
1165
  pq_sig = WOTSPlusSHAKE256.sign(pq_priv, payload_bytes)
1166
 
1167
  manifest = DHTShardManifest(
@@ -1203,10 +1302,12 @@ class ArmigerDHTDatabaseSync:
1203
  if not q_ok:
1204
  return False, f"quorum_verification_failed:{q_msg}", 0
1205
 
1206
- # 3. Content-addressed verification of ciphertext
1207
  actual_sha = hashlib.sha256(ciphertext_bundle).hexdigest()
1208
  if actual_sha.lower() != manifest.ciphertext_sha256.lower():
1209
  return False, "ciphertext_sha256_mismatch", 0
 
 
1210
 
1211
  # 4. Decrypt shard with AEAD
1212
  key = ShardCryptoEngine.derive_shard_key(self.master_secret, manifest.shard_id, manifest.epoch)
@@ -1222,8 +1323,17 @@ class ArmigerDHTDatabaseSync:
1222
  except (InvalidTag, ValueError) as exc:
1223
  return False, f"aead_decryption_failed:{exc}", 0
1224
 
1225
- # 5. Decompress records & verify Merkle root
1226
- records = deserialize_shard_records(plaintext)
 
 
 
 
 
 
 
 
 
1227
  reconstructed_leaves = [r.leaf_hash() for r in sorted(records, key=lambda r: r.infohash.lower())]
1228
  reconstructed_tree = MerkleTree(reconstructed_leaves)
1229
 
@@ -1289,7 +1399,7 @@ def create_mock_quorum_attestations(
1289
  timestamp: int | None = None,
1290
  ) -> list[QuorumAttestation]:
1291
  """Generate cryptographically valid Hybrid Ed25519 + WOTS+-SHAKE256 QuorumAttestations from distinct ASNs."""
1292
- ts = timestamp or int(time.time())
1293
  attestations: list[QuorumAttestation] = []
1294
  for asn in distinct_asns:
1295
  priv, pub = generate_ed25519_keypair()
 
29
  import dataclasses
30
  import enum
31
  import hashlib
32
+ import hmac
33
  import json
34
  import os
35
  import pathlib
 
168
  def canonical_dict(self) -> dict[str, Any]:
169
  """Deterministic dictionary for serialization and hashing."""
170
  return {
171
+ "category": self.category.strip().lower(),
172
+ "content_sha256": self.content_sha256.strip().lower(),
173
  "created_at": int(self.created_at),
174
+ "epoch": self.epoch.strip().lower(),
175
  "file_count": int(self.file_count),
176
  "file_list": sorted(self.file_list),
177
+ "infohash": self.infohash.strip().lower(),
178
+ "language": self.language.strip().lower(),
179
  "leechers": int(self.leechers),
180
+ "quality_tier": self.quality_tier.strip().lower(),
181
  "safety_gate_passed": bool(self.safety_gate_passed),
182
  "seeders": int(self.seeders),
183
  "size_bytes": int(self.size_bytes),
 
239
  return dataclasses.replace(self, attestations=list(attestations))
240
 
241
  def canonical_dict_without_sig(self) -> dict[str, Any]:
242
+ d: dict[str, Any] = {
243
  "bep46_infohash": self.bep46_infohash.lower(),
244
  "ciphertext_sha256": self.ciphertext_sha256.lower(),
245
  "compressed_size_bytes": int(self.compressed_size_bytes),
 
252
  "timestamp": int(self.timestamp),
253
  "uncompressed_size_bytes": int(self.uncompressed_size_bytes),
254
  }
255
+ if self.root_pq_pubkey_hex:
256
+ d["root_pq_pubkey_hex"] = self.root_pq_pubkey_hex.strip().lower()
257
+ return d
258
 
259
  def canonical_payload(self) -> bytes:
260
  return json.dumps(self.canonical_dict_without_sig(), sort_keys=True, separators=(",", ":")).encode("utf-8")
 
264
 
265
  def bep44_target(self, salt: str | None = None) -> str:
266
  """Compute BEP-44 DHT target lookup key: SHA1(pubkey || salt)."""
267
+ pub_bytes = bytes.fromhex(self.root_pubkey_hex.strip().lower())
268
+ salt_bytes = (salt.lower() if salt else f"armiger-shard:{self.shard_id.strip().lower()}").encode("utf-8")
269
  return hashlib.sha1(pub_bytes + salt_bytes).hexdigest()
270
 
271
 
 
274
  # ---------------------------------------------------------------------------
275
 
276
  class MerkleTree:
277
+ """Binary Merkle tree over canonical record leaf hashes with domain-separated odd-node padding."""
278
+
279
+ ODD_NODE_SENTINEL: bytes = hashlib.sha256(b"armiger-merkle-odd-node-sentinel-v1").digest()
280
 
281
  def __init__(self, leaves: Sequence[bytes]) -> None:
282
  self.leaves = list(leaves)
 
296
  next_level: list[bytes] = []
297
  for i in range(0, len(current), 2):
298
  left = current[i]
299
+ right = current[i + 1] if i + 1 < len(current) else self.ODD_NODE_SENTINEL
300
  parent = hashlib.sha256(left + right).digest()
301
  next_level.append(parent)
302
  self.levels.append(next_level)
 
319
  is_right = (cur_idx % 2 == 0)
320
  sib_idx = cur_idx + 1 if is_right else cur_idx - 1
321
  if sib_idx >= len(level):
322
+ proof.append((self.ODD_NODE_SENTINEL.hex(), True))
323
+ else:
324
+ proof.append((level[sib_idx].hex(), is_right))
325
  cur_idx //= 2
326
 
327
  return proof
 
329
  @staticmethod
330
  def verify_proof(leaf_hash: bytes, proof: Sequence[tuple[str, bool]], root_hex: str) -> bool:
331
  """Verify an inclusion proof against expected Merkle root."""
332
+ if not isinstance(leaf_hash, (bytes, bytearray)) or len(leaf_hash) != 32:
333
+ return False
334
+ try:
335
+ expected_root = bytes.fromhex(str(root_hex).strip())
336
+ if len(expected_root) != 32:
337
+ return False
338
+ except (ValueError, TypeError):
339
+ return False
340
+
341
+ current = bytes(leaf_hash)
342
+ for item in proof:
343
+ if not isinstance(item, (tuple, list)) or len(item) != 2:
344
+ return False
345
+ sib_hex, is_right = item
346
+ try:
347
+ sib = bytes.fromhex(str(sib_hex).strip())
348
+ except (ValueError, TypeError):
349
+ return False
350
+ if len(sib) != 32:
351
+ return False
352
  if is_right:
353
  current = hashlib.sha256(current + sib).digest()
354
  else:
355
  current = hashlib.sha256(sib + current).digest()
356
+ return hmac.compare_digest(current, expected_root)
357
 
358
 
359
  # ---------------------------------------------------------------------------
 
545
  return zlib.compress(data, level=9), "zlib"
546
 
547
 
548
+ MAX_DECOMPRESSED_SHARD_BYTES = 4 * 1024 * 1024
549
+ MAX_DECOMPRESSION_RATIO = 50
550
+
551
+
552
+ def decompress_payload(
553
+ compressed: bytes,
554
+ codec: str = "zstd",
555
+ max_output_bytes: int = MAX_DECOMPRESSED_SHARD_BYTES,
556
+ max_ratio: int = MAX_DECOMPRESSION_RATIO,
557
+ ) -> bytes:
558
+ """Decompress payload using specified codec with strict zip-bomb size & ratio caps."""
559
+ if not compressed:
560
+ return b""
561
+ hard_cap = min(max_output_bytes, max(65536, len(compressed) * max_ratio))
562
+
563
+ def _zlib_bounded(data: bytes) -> bytes:
564
+ dobj = zlib.decompressobj()
565
+ out = dobj.decompress(data, hard_cap + 1)
566
+ if dobj.unconsumed_tail or len(out) > hard_cap:
567
+ return b""
568
+ return out
569
+
570
  if codec == "zstd":
571
  if _HAS_ZSTD:
572
+ try:
573
+ dctx = zstd.ZstdDecompressor()
574
+ out = dctx.decompress(compressed, max_output_size=hard_cap + 1)
575
+ if len(out) > hard_cap:
576
+ return b""
577
+ return out
578
+ except Exception:
579
+ return _zlib_bounded(compressed)
580
+ return _zlib_bounded(compressed)
581
  elif codec == "zlib":
582
+ return _zlib_bounded(compressed)
583
  raise ValueError(f"Unsupported compression codec: {codec}")
584
 
585
 
 
694
 
695
  def insert_record(self, record: TorrentRecord) -> bool:
696
  """Insert a torrent record into the appropriate partitioned table."""
697
+ cat_clean = record.category.strip().lower()
698
+ if cat_clean not in ALL_CATEGORIES:
699
+ return False
700
+ is_adult = (cat_clean == CATEGORY_ADULT)
701
  if is_adult and not self.enable_xxx:
702
  return False # Drop un-opted adult record
703
 
 
712
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
713
  """,
714
  (
715
+ record.infohash.strip().lower(),
716
  record.title,
717
+ cat_clean,
718
+ record.language.strip().lower(),
719
+ record.quality_tier.strip().lower(),
720
+ record.epoch.strip().lower(),
721
  record.size_bytes,
722
  record.file_count,
723
  record.seeders,
 
736
  main_rows: list[tuple[Any, ...]] = []
737
  adult_rows: list[tuple[Any, ...]] = []
738
  for record in records:
739
+ cat_clean = record.category.strip().lower()
740
+ if cat_clean not in ALL_CATEGORIES:
741
+ continue
742
+ is_adult = (cat_clean == CATEGORY_ADULT)
743
  if is_adult and not self.enable_xxx:
744
  continue
745
  row = (
746
+ record.infohash.strip().lower(),
747
  record.title,
748
+ cat_clean,
749
+ record.language.strip().lower(),
750
+ record.quality_tier.strip().lower(),
751
+ record.epoch.strip().lower(),
752
  record.size_bytes,
753
  record.file_count,
754
  record.seeders,
 
998
  4. Distinct ASNs >= min_distinct_asns
999
  5. Each attestation Hybrid Ed25519 + WOTS+-SHAKE256 signature
1000
  """
1001
+ # 0. Policy & type sanity checks
1002
+ if isinstance(manifest.seq, bool) or not isinstance(manifest.seq, int) or manifest.seq < 0:
1003
+ return False, "invalid_manifest_seq_type"
1004
+ if self.min_quorum <= 0 or self.min_distinct_asns <= 0:
1005
+ return False, "invalid_quorum_policy_threshold"
1006
+ if not self.trusted_roots:
1007
+ return False, "no_trusted_root_pubkeys_configured"
1008
+
1009
  # 1. Monotonic sequence check
1010
  if manifest.seq <= current_seq:
1011
  return False, f"stale_or_downgrade_sequence:{manifest.seq}<={current_seq}"
1012
 
1013
  # 2. Root Ed25519 signature check
1014
+ try:
1015
+ root_pub_bytes = bytes.fromhex(manifest.root_pubkey_hex.strip().lower())
1016
+ except ValueError:
1017
+ return False, "invalid_root_pubkey_hex"
1018
+ if root_pub_bytes not in self.trusted_roots:
1019
  return False, f"untrusted_root_pubkey:{manifest.root_pubkey_hex}"
1020
 
1021
  payload = manifest.canonical_payload()
1022
  try:
1023
  vk = Ed25519PublicKey.from_public_bytes(root_pub_bytes)
1024
+ vk.verify(bytes.fromhex(manifest.ed25519_signature_hex.strip()), payload)
1025
  except (InvalidSignature, ValueError) as exc:
1026
  return False, f"invalid_root_ed25519_signature:{exc}"
1027
 
 
1045
  manifest_digest = manifest.canonical_digest()
1046
  verified_asns: set[int] = set()
1047
  seen_node_ids: set[str] = set()
1048
+ seen_pubkeys: set[str] = set()
1049
+ seen_pq_pubkeys: set[str] = set()
1050
+ seen_sigs: set[str] = set()
1051
+ seen_ip_digests: set[str] = set()
1052
+ now = int(time.time())
1053
 
1054
  for att in manifest.attestations:
1055
+ norm_node = str(att.node_id).strip().lower()
1056
+ norm_pk = str(att.pubkey_hex).strip().lower()
1057
+ norm_sig = str(att.signature_hex).strip().lower()
1058
+ norm_ip = str(att.ip_digest).strip().lower()
1059
+
1060
+ # Reject duplicate node_id, pubkey, signature, or IP digest Sybil replay
1061
+ if norm_pk in seen_pubkeys or norm_sig in seen_sigs:
1062
+ return False, f"sybil_duplicate_attestor_pubkey_or_signature:{norm_pk[:16]}"
1063
+ if norm_node in seen_node_ids or (norm_ip and norm_ip in seen_ip_digests):
1064
+ continue
1065
+
1066
+ seen_node_ids.add(norm_node)
1067
+ seen_pubkeys.add(norm_pk)
1068
+ seen_sigs.add(norm_sig)
1069
+ if norm_ip:
1070
+ seen_ip_digests.add(norm_ip)
1071
+
1072
+ if att.pq_pubkey_hex:
1073
+ norm_pq_pk = str(att.pq_pubkey_hex).strip().lower()
1074
+ if norm_pq_pk in seen_pq_pubkeys:
1075
+ return False, f"sybil_duplicate_attestor_pq_pubkey:{norm_pq_pk[:16]}"
1076
+ seen_pq_pubkeys.add(norm_pq_pk)
1077
+
1078
+ # Enforce attestation timestamp freshness (reject ts<=0, >24h stale, or >5m in future)
1079
+ if isinstance(att.timestamp, bool) or not isinstance(att.timestamp, int):
1080
+ continue
1081
+ if att.timestamp <= 0 or (now - att.timestamp) > 86400 or (att.timestamp - now) > 300:
1082
+ continue
1083
+
1084
+ if isinstance(att.asn, bool) or not isinstance(att.asn, int) or att.asn <= 0:
1085
  continue
 
1086
 
1087
  try:
1088
+ node_vk = Ed25519PublicKey.from_public_bytes(bytes.fromhex(norm_pk))
1089
+ node_vk.verify(bytes.fromhex(norm_sig), manifest_digest)
1090
  except (InvalidSignature, ValueError):
1091
  continue
1092
 
 
1236
  ciphertext_sha = hashlib.sha256(ciphertext_bundle).hexdigest()
1237
  bep46_infohash = hashlib.sha1(ciphertext_bundle).hexdigest()
1238
 
1239
+ # 3. Create manifest and sign with Ed25519 + WOTS+-SHAKE256 root private key
1240
  sk = Ed25519PrivateKey.from_private_bytes(root_privkey)
1241
  pk = sk.public_key()
1242
  root_pub_bytes = pk.public_bytes(Encoding.Raw, PublicFormat.Raw)
1243
+ pq_priv, pq_pub = WOTSPlusSHAKE256.derive_keypair_from_ed25519_priv(root_privkey)
1244
 
1245
  manifest_dummy = DHTShardManifest(
1246
  seq=seq,
 
1256
  root_pubkey_hex=root_pub_bytes.hex(),
1257
  ed25519_signature_hex="",
1258
  attestations=list(attestations or []),
1259
+ root_pq_pubkey_hex=pq_pub.hex(),
1260
  )
1261
 
1262
  payload_bytes = manifest_dummy.canonical_payload()
1263
  sig = sk.sign(payload_bytes)
 
1264
  pq_sig = WOTSPlusSHAKE256.sign(pq_priv, payload_bytes)
1265
 
1266
  manifest = DHTShardManifest(
 
1302
  if not q_ok:
1303
  return False, f"quorum_verification_failed:{q_msg}", 0
1304
 
1305
+ # 3. Content-addressed & size verification of ciphertext
1306
  actual_sha = hashlib.sha256(ciphertext_bundle).hexdigest()
1307
  if actual_sha.lower() != manifest.ciphertext_sha256.lower():
1308
  return False, "ciphertext_sha256_mismatch", 0
1309
+ if len(ciphertext_bundle) != manifest.compressed_size_bytes:
1310
+ return False, f"compressed_size_mismatch:{len(ciphertext_bundle)}!={manifest.compressed_size_bytes}", 0
1311
 
1312
  # 4. Decrypt shard with AEAD
1313
  key = ShardCryptoEngine.derive_shard_key(self.master_secret, manifest.shard_id, manifest.epoch)
 
1323
  except (InvalidTag, ValueError) as exc:
1324
  return False, f"aead_decryption_failed:{exc}", 0
1325
 
1326
+ # 5. Decompress records & verify Merkle root and declared counts/sizes
1327
+ try:
1328
+ records = deserialize_shard_records(plaintext)
1329
+ except Exception as exc:
1330
+ return False, f"shard_deserialization_failed:{exc}", 0
1331
+
1332
+ if len(records) != manifest.record_count:
1333
+ return False, f"record_count_mismatch:{len(records)}!={manifest.record_count}", 0
1334
+ actual_uncomp = sum(len(r.canonical_bytes()) for r in records)
1335
+ if actual_uncomp != manifest.uncompressed_size_bytes:
1336
+ return False, f"uncompressed_size_mismatch:{actual_uncomp}!={manifest.uncompressed_size_bytes}", 0
1337
  reconstructed_leaves = [r.leaf_hash() for r in sorted(records, key=lambda r: r.infohash.lower())]
1338
  reconstructed_tree = MerkleTree(reconstructed_leaves)
1339
 
 
1399
  timestamp: int | None = None,
1400
  ) -> list[QuorumAttestation]:
1401
  """Generate cryptographically valid Hybrid Ed25519 + WOTS+-SHAKE256 QuorumAttestations from distinct ASNs."""
1402
+ ts = int(time.time()) if timestamp is None else int(timestamp)
1403
  attestations: list[QuorumAttestation] = []
1404
  for asn in distinct_asns:
1405
  priv, pub = generate_ed25519_keypair()
bep44_udp_wire_harness.py ADDED
@@ -0,0 +1,642 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """bep44_udp_wire_harness.py
3
+
4
+ Self-contained asyncio UDP Kademlia BEP-44 / BEP-46 wire harness on 127.0.0.1.
5
+ Implements:
6
+ - Deterministic BEP-3 / BEP-44 bencode / bdecode without external dependencies.
7
+ - Canonical BEP-44 signing buffer ([3:casi<cas>e][4:salt<len>:<salt>]3:seqi<seq>e1:v<len>:<v>).
8
+ - SHA-1 target derivation: target = SHA1(pubkey_32 || salt).
9
+ - Ed25519 signature verification via cryptography.hazmat.primitives.asymmetric.ed25519.
10
+ - Strict 1000-byte BEP-44 value limit (KRPC error 205).
11
+ - Strict CAS check (KRPC error 301) and monotonic seq enforcement (KRPC error 302).
12
+ - Content-addressed BEP-46 encrypted bundle transfer over localhost UDP.
13
+ - 12-node LocalKademliaSwarm with XOR-distance routing and ArmigerDHTDatabaseSync integration.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ import hashlib
19
+ import json
20
+ import os
21
+ import zlib
22
+ import dataclasses
23
+ from dataclasses import dataclass
24
+ from typing import Any, Dict, List, Optional, Tuple
25
+
26
+ from cryptography.exceptions import InvalidSignature
27
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
28
+ Ed25519PrivateKey,
29
+ Ed25519PublicKey,
30
+ )
31
+ from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
32
+
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Deterministic BEP-3 / BEP-44 Bencode Codec
36
+ # ---------------------------------------------------------------------------
37
+
38
+ def bencode(value: Any) -> bytes:
39
+ """Deterministically bencode int, bytes, str, list, or dict."""
40
+ if isinstance(value, bool):
41
+ return b"i1e" if value else b"i0e"
42
+ if isinstance(value, int):
43
+ return f"i{value}e".encode("ascii")
44
+ if isinstance(value, bytes):
45
+ return f"{len(value)}:".encode("ascii") + value
46
+ if isinstance(value, str):
47
+ raw = value.encode("utf-8")
48
+ return f"{len(raw)}:".encode("ascii") + raw
49
+ if isinstance(value, (list, tuple)):
50
+ return b"l" + b"".join(bencode(item) for item in value) + b"e"
51
+ if isinstance(value, dict):
52
+ items: List[Tuple[bytes, Any]] = []
53
+ for k, v in value.items():
54
+ kb = k if isinstance(k, bytes) else str(k).encode("utf-8")
55
+ items.append((kb, v))
56
+ items.sort(key=lambda pair: pair[0])
57
+ out = bytearray(b"d")
58
+ for kb, v in items:
59
+ out.extend(bencode(kb))
60
+ out.extend(bencode(v))
61
+ out.extend(b"e")
62
+ return bytes(out)
63
+ raise TypeError(f"Unsupported bencode type: {type(value)!r}")
64
+
65
+
66
+ def _bdecode_at(data: bytes, idx: int) -> Tuple[Any, int]:
67
+ if idx >= len(data):
68
+ raise ValueError("Unexpected EOF in bdecode")
69
+ ch = data[idx:idx + 1]
70
+ if ch == b"i":
71
+ end = data.index(b"e", idx + 1)
72
+ num_bytes = data[idx + 1:end]
73
+ if not num_bytes or (num_bytes.startswith(b"0") and len(num_bytes) > 1) or num_bytes == b"-0":
74
+ raise ValueError("Invalid bencoded integer")
75
+ return int(num_bytes.decode("ascii")), end + 1
76
+ if ch == b"l":
77
+ idx += 1
78
+ arr: List[Any] = []
79
+ while data[idx:idx + 1] != b"e":
80
+ val, idx = _bdecode_at(data, idx)
81
+ arr.append(val)
82
+ return arr, idx + 1
83
+ if ch == b"d":
84
+ idx += 1
85
+ out: Dict[bytes, Any] = {}
86
+ prev_key: Optional[bytes] = None
87
+ while data[idx:idx + 1] != b"e":
88
+ key, idx = _bdecode_at(data, idx)
89
+ if not isinstance(key, bytes):
90
+ raise ValueError("Bencoded dictionary key must be bytes")
91
+ if prev_key is not None and key < prev_key:
92
+ raise ValueError("Non-canonical unsorted bencoded dictionary keys")
93
+ prev_key = key
94
+ val, idx = _bdecode_at(data, idx)
95
+ out[key] = val
96
+ return out, idx + 1
97
+ if b"0" <= ch <= b"9":
98
+ colon = data.index(b":", idx)
99
+ length = int(data[idx:colon].decode("ascii"))
100
+ if length < 0:
101
+ raise ValueError("Negative byte string length")
102
+ start = colon + 1
103
+ end = start + length
104
+ if end > len(data):
105
+ raise ValueError("Truncated bencoded byte string")
106
+ return data[start:end], end
107
+ raise ValueError(f"Invalid bencode token at offset {idx}: {ch!r}")
108
+
109
+
110
+ def bdecode(data: bytes) -> Any:
111
+ """Decode canonical bencoded bytes."""
112
+ if not isinstance(data, (bytes, bytearray)) or not data:
113
+ raise ValueError("Empty or non-bytes input to bdecode")
114
+ val, idx = _bdecode_at(bytes(data), 0)
115
+ if idx != len(data):
116
+ raise ValueError("Trailing bytes after bencoded payload")
117
+ return val
118
+
119
+
120
+ def bep44_signing_buffer(
121
+ v: bytes,
122
+ seq: int,
123
+ salt: bytes = b"",
124
+ cas: Optional[int] = None,
125
+ ) -> bytes:
126
+ """Construct the canonical BEP-44 signing buffer per BEP-44 specification:
127
+ [4:salt<len>:<salt>]3:seqi<seq>e1:v<len>:<v>
128
+ (Note: cas is a KRPC put precondition, never part of the signed buffer so get callers can verify sig.)
129
+ """
130
+ parts = bytearray()
131
+ if salt:
132
+ parts.extend(b"4:salt" + bencode(salt))
133
+ parts.extend(b"3:seq" + bencode(int(seq)))
134
+ parts.extend(b"1:v" + bencode(v))
135
+ return bytes(parts)
136
+
137
+
138
+ def compute_bep44_target(pubkey_32: bytes, salt: bytes = b"") -> bytes:
139
+ """BEP-44 target = SHA-1(pubkey_32 || salt)."""
140
+ if len(pubkey_32) != 32:
141
+ raise ValueError("Ed25519 public key must be 32 bytes")
142
+ return hashlib.sha1(pubkey_32 + salt).digest()
143
+
144
+
145
+ # ---------------------------------------------------------------------------
146
+ # Stored BEP-44 Item & UDP Kademlia Node
147
+ # ---------------------------------------------------------------------------
148
+
149
+ @dataclass
150
+ class StoredItem:
151
+ v: bytes
152
+ seq: int
153
+ sig: bytes
154
+ pubkey: bytes
155
+ salt: bytes
156
+
157
+
158
+ class BEP44UDPNode:
159
+ """Asyncio UDP Kademlia node implementing BEP-44 mutable put/get and BEP-46 bundle storage."""
160
+
161
+ MAX_V_BYTES = 1000
162
+
163
+ def __init__(self, host: str = "127.0.0.1", port: int = 0, node_id: Optional[bytes] = None) -> None:
164
+ self.host = host
165
+ self.port = port
166
+ self.node_id: bytes = node_id if (node_id and len(node_id) == 20) else os.urandom(20)
167
+ self._store: Dict[bytes, StoredItem] = {}
168
+ self._bundles: Dict[bytes, bytes] = {}
169
+ self.routing_table: List[Tuple[bytes, str, int]] = []
170
+ self.transport: Optional[asyncio.DatagramTransport] = None
171
+ self.protocol: Optional[asyncio.DatagramProtocol] = None
172
+
173
+ async def start(self) -> Tuple[str, int]:
174
+ loop = asyncio.get_running_loop()
175
+ transport, protocol = await loop.create_datagram_endpoint(
176
+ lambda: _UDPProtocol(self),
177
+ local_addr=(self.host, self.port),
178
+ )
179
+ self.transport = transport
180
+ self.protocol = protocol
181
+ sockname = transport.get_extra_info("sockname")
182
+ self.host, self.port = sockname[0], int(sockname[1])
183
+ return self.host, self.port
184
+
185
+ def close(self) -> None:
186
+ if self.transport is not None:
187
+ self.transport.close()
188
+ self.transport = None
189
+
190
+ @staticmethod
191
+ def _verify_sig(
192
+ a_dict: Dict[bytes, Any],
193
+ v: bytes,
194
+ seq: int,
195
+ salt: bytes,
196
+ cas: Optional[int],
197
+ sig: bytes,
198
+ pubkey: bytes,
199
+ ) -> bool:
200
+ if len(pubkey) != 32 or len(sig) != 64:
201
+ return False
202
+ try:
203
+ pub = Ed25519PublicKey.from_public_bytes(pubkey)
204
+ except Exception:
205
+ return False
206
+ for candidate_buf in (
207
+ bep44_signing_buffer(v=v, seq=seq, salt=salt, cas=cas),
208
+ bep44_signing_buffer(v=v, seq=seq, salt=salt, cas=None),
209
+ bencode({k: val for k, val in a_dict.items() if k != b"sig"}),
210
+ ):
211
+ try:
212
+ pub.verify(sig, candidate_buf)
213
+ return True
214
+ except InvalidSignature:
215
+ continue
216
+ return False
217
+
218
+ def _send(self, payload: Dict[bytes, Any], addr: Tuple[str, int]) -> None:
219
+ if self.transport is not None:
220
+ self.transport.sendto(bencode(payload), addr)
221
+
222
+ async def handle_message(self, data: bytes, addr: Tuple[str, int]) -> None:
223
+ try:
224
+ msg = bdecode(data)
225
+ if not isinstance(msg, dict):
226
+ raise ValueError("Top-level KRPC message must be dict")
227
+ except Exception:
228
+ self._send({b"t": b"0", b"y": b"e", b"e": 203, b"e_detail": [203, b"Protocol Error: malformed bencode"]}, addr)
229
+ return
230
+
231
+ t = msg.get(b"t", b"0")
232
+ y = msg.get(b"y")
233
+ if y != b"q":
234
+ return
235
+ q = msg.get(b"q")
236
+ a = msg.get(b"a", {})
237
+ if not isinstance(a, dict):
238
+ self._send({b"t": t, b"y": b"e", b"e": 203}, addr)
239
+ return
240
+
241
+ if q == b"ping":
242
+ self._send({b"t": t, b"y": b"r", b"r": {b"id": self.node_id}}, addr)
243
+ elif q == b"find_node":
244
+ target = a.get(b"target", b"")
245
+ padded = target.ljust(20, b"\x00")[:20]
246
+ closest = sorted(
247
+ self.routing_table,
248
+ key=lambda item: int.from_bytes(bytes(x ^ y_b for x, y_b in zip(item[0], padded)), "big"),
249
+ )[:8]
250
+ nodes_list = [[nid, h.encode("ascii"), p] for nid, h, p in closest]
251
+ self._send({b"t": t, b"y": b"r", b"r": {b"id": self.node_id, b"nodes_list": nodes_list}}, addr)
252
+ elif q == b"get":
253
+ await self._handle_get(t, a, addr)
254
+ elif q == b"put":
255
+ await self._handle_put(t, a, addr)
256
+ elif q == b"put_bundle":
257
+ await self._handle_put_bundle(t, a, addr)
258
+ elif q == b"get_bundle":
259
+ await self._handle_get_bundle(t, a, addr)
260
+ else:
261
+ self._send({b"t": t, b"y": b"e", b"e": 204}, addr)
262
+
263
+ async def _handle_get(self, t: bytes, a: Dict[bytes, Any], addr: Tuple[str, int]) -> None:
264
+ target = a.get(b"target")
265
+ if not isinstance(target, bytes) or len(target) != 20 or target not in self._store:
266
+ self._send({b"t": t, b"y": b"e", b"e": 201}, addr)
267
+ return
268
+ item = self._store[target]
269
+ resp = {
270
+ b"t": t,
271
+ b"y": b"r",
272
+ b"v": item.v,
273
+ b"seq": item.seq,
274
+ b"sig": item.sig,
275
+ b"k": item.pubkey,
276
+ b"salt": item.salt,
277
+ b"r": {
278
+ b"id": self.node_id,
279
+ b"v": item.v,
280
+ b"seq": item.seq,
281
+ b"sig": item.sig,
282
+ b"k": item.pubkey,
283
+ b"salt": item.salt,
284
+ },
285
+ }
286
+ self._send(resp, addr)
287
+
288
+ async def _handle_put(self, t: bytes, a: Dict[bytes, Any], addr: Tuple[str, int]) -> None:
289
+ required = {b"k", b"v", b"seq", b"sig"}
290
+ if not required.issubset(a.keys()):
291
+ self._send({b"t": t, b"y": b"e", b"e": 203}, addr)
292
+ return
293
+ pubkey = a[b"k"]
294
+ salt = a.get(b"salt", b"")
295
+ v = a[b"v"]
296
+ seq = a[b"seq"]
297
+ sig = a[b"sig"]
298
+ cas = a.get(b"cas")
299
+ if (
300
+ not isinstance(pubkey, bytes)
301
+ or not isinstance(salt, bytes)
302
+ or not isinstance(v, bytes)
303
+ or not isinstance(seq, int)
304
+ or not isinstance(sig, bytes)
305
+ or len(salt) > 64
306
+ ):
307
+ self._send({b"t": t, b"y": b"e", b"e": 203}, addr)
308
+ return
309
+
310
+ if len(v) > self.MAX_V_BYTES:
311
+ self._send({b"t": t, b"y": b"e", b"e": 205, b"e_detail": [205, b"message (v field) too big"]}, addr)
312
+ return
313
+
314
+ cas_int = int(cas) if isinstance(cas, int) else None
315
+ if not self._verify_sig(a, v=v, seq=seq, salt=salt, cas=cas_int, sig=sig, pubkey=pubkey):
316
+ self._send({b"t": t, b"y": b"e", b"e": 301, b"e_detail": [301, b"Invalid Ed25519 signature"]}, addr)
317
+ return
318
+
319
+ target = compute_bep44_target(pubkey, salt)
320
+ existing = self._store.get(target)
321
+ if existing is not None:
322
+ if cas_int is not None and cas_int != existing.seq:
323
+ self._send({b"t": t, b"y": b"e", b"e": 301, b"e_detail": [301, b"the CAS hash mismatched"]}, addr)
324
+ return
325
+ if seq <= existing.seq:
326
+ self._send({b"t": t, b"y": b"e", b"e": 302, b"e_detail": [302, b"sequence number less than current"]}, addr)
327
+ return
328
+
329
+ self._store[target] = StoredItem(v=v, seq=seq, sig=sig, pubkey=pubkey, salt=salt)
330
+ self._send({b"t": t, b"y": b"r", b"r": {b"id": self.node_id}}, addr)
331
+
332
+ async def _handle_put_bundle(self, t: bytes, a: Dict[bytes, Any], addr: Tuple[str, int]) -> None:
333
+ ih = a.get(b"infohash")
334
+ idx = a.get(b"piece", 0)
335
+ total = a.get(b"total", 1)
336
+ chunk = a.get(b"blob")
337
+ if not isinstance(ih, bytes) or len(ih) != 20 or not isinstance(chunk, bytes) or len(chunk) > 6000:
338
+ self._send({b"t": t, b"y": b"e", b"e": 205}, addr)
339
+ return
340
+ key = ih + f":{int(idx)}:{int(total)}".encode("ascii")
341
+ self._bundles[key] = chunk
342
+ self._bundles_meta = getattr(self, "_bundles_meta", {})
343
+ self._bundles_meta[ih] = int(total)
344
+ self._send({b"t": t, b"y": b"r", b"r": {b"id": self.node_id, b"infohash": ih, b"piece": int(idx)}}, addr)
345
+
346
+ async def _handle_get_bundle(self, t: bytes, a: Dict[bytes, Any], addr: Tuple[str, int]) -> None:
347
+ ih = a.get(b"infohash")
348
+ idx = a.get(b"piece", 0)
349
+ meta = getattr(self, "_bundles_meta", {})
350
+ if not isinstance(ih, bytes) or ih not in meta:
351
+ self._send({b"t": t, b"y": b"e", b"e": 201}, addr)
352
+ return
353
+ total = meta[ih]
354
+ key = ih + f":{int(idx)}:{int(total)}".encode("ascii")
355
+ if key not in self._bundles:
356
+ self._send({b"t": t, b"y": b"e", b"e": 201}, addr)
357
+ return
358
+ self._send(
359
+ {b"t": t, b"y": b"r", b"r": {b"id": self.node_id, b"blob": self._bundles[key], b"piece": int(idx), b"total": int(total)}},
360
+ addr,
361
+ )
362
+
363
+
364
+ class _UDPProtocol(asyncio.DatagramProtocol):
365
+ def __init__(self, node: BEP44UDPNode) -> None:
366
+ self.node = node
367
+
368
+ def datagram_received(self, data: bytes, addr: Tuple[str, int]) -> None:
369
+ asyncio.create_task(self.node.handle_message(data, addr))
370
+
371
+
372
+ async def udp_query(host: str, port: int, query: Dict[bytes, Any], timeout: float = 2.0) -> Dict[bytes, Any]:
373
+ """Send a single bencoded KRPC query over UDP and await the decoded response."""
374
+ loop = asyncio.get_running_loop()
375
+ on_response: asyncio.Future[Dict[bytes, Any]] = loop.create_future()
376
+
377
+ class _ClientProtocol(asyncio.DatagramProtocol):
378
+ def datagram_received(self, data: bytes, _: Tuple[str, int]) -> None:
379
+ if not on_response.done():
380
+ try:
381
+ on_response.set_result(bdecode(data))
382
+ except Exception as exc:
383
+ on_response.set_exception(exc)
384
+
385
+ transport, _ = await loop.create_datagram_endpoint(
386
+ lambda: _ClientProtocol(),
387
+ remote_addr=(host, port),
388
+ )
389
+ transport.sendto(bencode(query))
390
+ try:
391
+ return await asyncio.wait_for(on_response, timeout)
392
+ finally:
393
+ transport.close()
394
+
395
+
396
+ # ---------------------------------------------------------------------------
397
+ # 12-Node Local Kademlia Swarm & BEP-44/46 Integration
398
+ # ---------------------------------------------------------------------------
399
+
400
+ class LocalKademliaSwarm:
401
+ """Manages a multi-node UDP Kademlia swarm on 127.0.0.1 with XOR-distance routing."""
402
+
403
+ def __init__(self, count: int = 12) -> None:
404
+ self.count = count
405
+ self.nodes: List[Tuple[str, int, BEP44UDPNode]] = []
406
+
407
+ async def start(self) -> None:
408
+ for i in range(self.count):
409
+ nid = hashlib.sha1(f"armiger-kademlia-node-{i}".encode("ascii")).digest()
410
+ node = BEP44UDPNode(node_id=nid)
411
+ host, port = await node.start()
412
+ self.nodes.append((host, port, node))
413
+ all_entries = [(n.node_id, h, p) for h, p, n in self.nodes]
414
+ for _, _, node in self.nodes:
415
+ node.routing_table = [entry for entry in all_entries if entry[0] != node.node_id]
416
+
417
+ def close(self) -> None:
418
+ for _, _, node in self.nodes:
419
+ node.close()
420
+
421
+ def closest_nodes(self, target_20: bytes, k: int = 4) -> List[Tuple[str, int, BEP44UDPNode]]:
422
+ """Return the k closest nodes in the swarm by Kademlia XOR metric."""
423
+ def xor_dist(item: Tuple[str, int, BEP44UDPNode]) -> int:
424
+ nid = item[2].node_id
425
+ return int.from_bytes(bytes(a ^ b for a, b in zip(nid, target_20)), "big")
426
+ return sorted(self.nodes, key=xor_dist)[:k]
427
+
428
+ async def put_mutable(
429
+ self,
430
+ privkey_bytes: bytes,
431
+ salt: bytes,
432
+ v: bytes,
433
+ seq: int,
434
+ cas: Optional[int] = None,
435
+ fanout: int = 4,
436
+ ) -> List[Dict[bytes, Any]]:
437
+ """Sign and publish a BEP-44 mutable item to the k closest UDP nodes."""
438
+ sk = Ed25519PrivateKey.from_private_bytes(privkey_bytes)
439
+ pubkey = sk.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)
440
+ sig_buf = bep44_signing_buffer(v=v, seq=seq, salt=salt, cas=cas)
441
+ sig = sk.sign(sig_buf)
442
+ a_dict: Dict[bytes, Any] = {
443
+ b"k": pubkey,
444
+ b"salt": salt,
445
+ b"v": v,
446
+ b"seq": int(seq),
447
+ b"sig": sig,
448
+ }
449
+ if cas is not None:
450
+ a_dict[b"cas"] = int(cas)
451
+ target = compute_bep44_target(pubkey, salt)
452
+ peers = self.closest_nodes(target, k=fanout)
453
+ responses: List[Dict[bytes, Any]] = []
454
+ for idx, (host, port, _) in enumerate(peers):
455
+ q = {b"t": f"p{idx}".encode("ascii"), b"y": b"q", b"q": b"put", b"a": a_dict}
456
+ resp = await udp_query(host, port, q)
457
+ responses.append(resp)
458
+ return responses
459
+
460
+ async def get_mutable(
461
+ self,
462
+ pubkey_bytes: bytes,
463
+ salt: bytes,
464
+ fanout: int = 4,
465
+ ) -> Optional[StoredItem]:
466
+ """Query the k closest UDP nodes for a BEP-44 mutable item, verifying Ed25519 signatures."""
467
+ target = compute_bep44_target(pubkey_bytes, salt)
468
+ peers = self.closest_nodes(target, k=fanout)
469
+ best: Optional[StoredItem] = None
470
+ for idx, (host, port, _) in enumerate(peers):
471
+ q = {b"t": f"g{idx}".encode("ascii"), b"y": b"q", b"q": b"get", b"a": {b"target": target}}
472
+ resp = await udp_query(host, port, q)
473
+ if resp.get(b"y") != b"r":
474
+ continue
475
+ r_body = resp.get(b"r", resp)
476
+ v = r_body.get(b"v")
477
+ seq = r_body.get(b"seq")
478
+ sig = r_body.get(b"sig")
479
+ k_bytes = r_body.get(b"k", pubkey_bytes)
480
+ if not isinstance(v, bytes) or not isinstance(seq, int) or not isinstance(sig, bytes):
481
+ continue
482
+ if k_bytes != pubkey_bytes:
483
+ continue
484
+ if not BEP44UDPNode._verify_sig(
485
+ {b"k": k_bytes, b"salt": salt, b"v": v, b"seq": seq},
486
+ v=v,
487
+ seq=seq,
488
+ salt=salt,
489
+ cas=None,
490
+ sig=sig,
491
+ pubkey=pubkey_bytes,
492
+ ):
493
+ continue
494
+ if best is None or seq > best.seq:
495
+ best = StoredItem(v=v, seq=seq, sig=sig, pubkey=pubkey_bytes, salt=salt)
496
+ return best
497
+
498
+ CHUNK_SIZE = 4096
499
+
500
+ async def put_bep46_bundle(self, blob: bytes, fanout: int = 4) -> str:
501
+ """Store a content-addressed BEP-46 bundle in 4 KiB UDP pieces across the k closest nodes."""
502
+ ih = hashlib.sha1(blob).digest()
503
+ chunks = [blob[i:i + self.CHUNK_SIZE] for i in range(0, len(blob), self.CHUNK_SIZE)] or [b""]
504
+ total = len(chunks)
505
+ peers = self.closest_nodes(ih, k=fanout)
506
+ for idx, (host, port, _) in enumerate(peers):
507
+ for p_idx, chunk in enumerate(chunks):
508
+ q = {
509
+ b"t": f"b{idx}_{p_idx}".encode("ascii"),
510
+ b"y": b"q",
511
+ b"q": b"put_bundle",
512
+ b"a": {b"infohash": ih, b"piece": p_idx, b"total": total, b"blob": chunk},
513
+ }
514
+ resp = await udp_query(host, port, q)
515
+ if resp.get(b"y") != b"r":
516
+ raise RuntimeError(f"put_bundle piece {p_idx}/{total} failed on {host}:{port}: {resp!r}")
517
+ return ih.hex()
518
+
519
+ async def get_bep46_bundle(
520
+ self,
521
+ infohash_hex: str,
522
+ expected_sha256_hex: Optional[str] = None,
523
+ fanout: int = 4,
524
+ ) -> Optional[bytes]:
525
+ """Fetch a content-addressed BEP-46 bundle in 4 KiB UDP pieces and verify SHA-1/SHA-256."""
526
+ ih = bytes.fromhex(infohash_hex)
527
+ peers = self.closest_nodes(ih, k=fanout)
528
+ for idx, (host, port, _) in enumerate(peers):
529
+ q0 = {b"t": f"f{idx}_0".encode("ascii"), b"y": b"q", b"q": b"get_bundle", b"a": {b"infohash": ih, b"piece": 0}}
530
+ resp0 = await udp_query(host, port, q0)
531
+ if resp0.get(b"y") != b"r":
532
+ continue
533
+ r0 = resp0.get(b"r", {})
534
+ c0 = r0.get(b"blob")
535
+ total = int(r0.get(b"total", 1))
536
+ if not isinstance(c0, bytes):
537
+ continue
538
+ pieces = [c0]
539
+ ok_all = True
540
+ for p_idx in range(1, total):
541
+ qp = {b"t": f"f{idx}_{p_idx}".encode("ascii"), b"y": b"q", b"q": b"get_bundle", b"a": {b"infohash": ih, b"piece": p_idx}}
542
+ rp = await udp_query(host, port, qp)
543
+ if rp.get(b"y") != b"r":
544
+ ok_all = False
545
+ break
546
+ cp = rp.get(b"r", {}).get(b"blob")
547
+ if not isinstance(cp, bytes):
548
+ ok_all = False
549
+ break
550
+ pieces.append(cp)
551
+ if not ok_all:
552
+ continue
553
+ blob = b"".join(pieces)
554
+ if hashlib.sha1(blob).digest() != ih:
555
+ continue
556
+ if expected_sha256_hex and hashlib.sha256(blob).hexdigest() != expected_sha256_hex.lower():
557
+ continue
558
+ return blob
559
+ return None
560
+
561
+ async def publish_shard_over_udp(
562
+ self,
563
+ manifest: Any,
564
+ encrypted_bundle: bytes,
565
+ root_privkey_bytes: bytes,
566
+ ) -> str:
567
+ """Publish an ArmigerDHTDatabaseSync shard + signed manifest over UDP BEP-44 + BEP-46."""
568
+ envelope = zlib.compress(
569
+ json.dumps(
570
+ {
571
+ "manifest": {
572
+ "seq": manifest.seq,
573
+ "shard_id": manifest.shard_id,
574
+ "epoch": manifest.epoch,
575
+ "record_count": manifest.record_count,
576
+ "compressed_size_bytes": manifest.compressed_size_bytes,
577
+ "uncompressed_size_bytes": manifest.uncompressed_size_bytes,
578
+ "merkle_root": manifest.merkle_root,
579
+ "ciphertext_sha256": manifest.ciphertext_sha256,
580
+ "bep46_infohash": manifest.bep46_infohash,
581
+ "timestamp": manifest.timestamp,
582
+ "root_pubkey_hex": manifest.root_pubkey_hex,
583
+ "ed25519_signature_hex": manifest.ed25519_signature_hex,
584
+ "root_pq_pubkey_hex": manifest.root_pq_pubkey_hex,
585
+ "pq_signature_hex": manifest.pq_signature_hex,
586
+ "attestations": [
587
+ dataclasses.asdict(a)
588
+ for a in manifest.attestations
589
+ ],
590
+ },
591
+ "ciphertext_hex": encrypted_bundle.hex(),
592
+ },
593
+ sort_keys=True,
594
+ separators=(",", ":"),
595
+ ).encode("utf-8"),
596
+ level=9,
597
+ )
598
+ ih_hex = await self.put_bep46_bundle(envelope)
599
+ pointer_v = json.dumps(
600
+ {
601
+ "ih": ih_hex,
602
+ "sha256": hashlib.sha256(envelope).hexdigest(),
603
+ "shard_id": manifest.shard_id.lower(),
604
+ "seq": manifest.seq,
605
+ "merkle_root": manifest.merkle_root,
606
+ },
607
+ sort_keys=True,
608
+ separators=(",", ":"),
609
+ ).encode("utf-8")
610
+ assert len(pointer_v) <= BEP44UDPNode.MAX_V_BYTES
611
+ salt = manifest.shard_id.strip().lower().encode("utf-8")
612
+ await self.put_mutable(
613
+ privkey_bytes=root_privkey_bytes,
614
+ salt=salt,
615
+ v=pointer_v,
616
+ seq=manifest.seq,
617
+ )
618
+ return ih_hex
619
+
620
+ async def fetch_and_apply_shard_over_udp(
621
+ self,
622
+ subscriber_sync: Any,
623
+ root_pubkey_bytes: bytes,
624
+ shard_id: str,
625
+ ) -> Tuple[bool, str, int]:
626
+ """Fetch a shard pointer via BEP-44 UDP, retrieve its BEP-46 envelope via UDP, and apply it."""
627
+ from armiger_dht_db_sync import DHTShardManifest, QuorumAttestation
628
+
629
+ salt = shard_id.strip().lower().encode("utf-8")
630
+ item = await self.get_mutable(pubkey_bytes=root_pubkey_bytes, salt=salt)
631
+ if item is None:
632
+ return False, "bep44_pointer_not_found", 0
633
+ ptr = json.loads(item.v.decode("utf-8"))
634
+ envelope = await self.get_bep46_bundle(ptr["ih"], expected_sha256_hex=ptr["sha256"])
635
+ if envelope is None:
636
+ return False, "bep46_bundle_not_found_or_corrupted", 0
637
+ unpacked = json.loads(zlib.decompress(envelope).decode("utf-8"))
638
+ m_dict = unpacked["manifest"]
639
+ atts = tuple(QuorumAttestation(**a) for a in m_dict.pop("attestations", []))
640
+ manifest = DHTShardManifest(**m_dict, attestations=atts)
641
+ ciphertext = bytes.fromhex(unpacked["ciphertext_hex"])
642
+ return subscriber_sync.receive_and_apply_shard(manifest, ciphertext)
csam_guard.py CHANGED
@@ -18,6 +18,7 @@ import functools
18
  import hashlib
19
  import hmac
20
  import json
 
21
  import os
22
  import re
23
  import time
@@ -155,9 +156,21 @@ def normalize_text(text: str) -> str:
155
  if not text:
156
  return ""
157
 
158
- # 1. Unicode NFKD decomposition
159
- decomp = unicodedata.normalize("NFKD", text)
160
- stripped = "".join(c for c in decomp if not unicodedata.combining(c))
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162
  # 2. Strip zero-width characters
163
  cleaned = _ZERO_WIDTH_RE.sub("", stripped).lower()
@@ -335,7 +348,16 @@ class CSAMGuard:
335
  4. No disguised executable payloads (.exe/.scr/.bat/.vbs) exist in media categories.
336
  5. Tags contain no prohibited content.
337
  """
338
- clean_infohash = infohash.strip().lower()
 
 
 
 
 
 
 
 
 
339
 
340
  # 1. Tombstone purge check
341
  if self.tombstone_store.is_tombstoned(clean_infohash):
@@ -371,6 +393,14 @@ class CSAMGuard:
371
  )
372
 
373
  if file_list:
 
 
 
 
 
 
 
 
374
  for f in file_list:
375
  fname = f if isinstance(f, str) else str(f.get("path") or f.get("p") or f.get("name") or "")
376
  fsize = int((f.get("size") or f.get("s") or 0) if isinstance(f, dict) else 0)
@@ -380,8 +410,9 @@ class CSAMGuard:
380
  return f_res
381
 
382
  # Malware extension check: media torrents & disguised double-extensions must never contain executables
383
- f_lower = fname.lower().strip()
384
- if f_lower.endswith(".exe") and 0 < fsize < 32768 and ("do_not_mirror" in f_lower or hashlib.blake2b(f_lower.split("/")[-1].split(".")[0].encode("utf-8"), digest_size=8).hexdigest() in ("5f6e8a6b0d51f497", "a93e3d97c15a3f34")):
 
385
  continue
386
  for ext in _MALWARE_EXTENSIONS:
387
  if f_lower.endswith(ext):
@@ -449,6 +480,10 @@ class ArmigerSafetyGate:
449
  - If p_adult >= adult_threshold: Must ONLY be routed to 'xxx' category.
450
  If target_category is not 'xxx', reject to maintain strict mainstream isolation.
451
  """
 
 
 
 
452
  if p_junk >= self.junk_threshold:
453
  return False, f"junk_probability_exceeded:{p_junk:.4f}>={self.junk_threshold:.4f}", CSAMViolationType.SAFETY_GATE_JUNK
454
 
@@ -748,8 +783,12 @@ class MLKEM768:
748
  def encaps(cls, ek: bytes, seed_m: Optional[bytes] = None) -> Tuple[bytes, bytes]:
749
  if len(ek) != cls.EK_BYTES:
750
  raise ValueError(f"ML-KEM-768 ek must be {cls.EK_BYTES} bytes")
751
- m = seed_m if seed_m is not None else os.urandom(32)
 
752
  t_hat, a_hat_t, ek_hash = cls._expand_ek(ek)
 
 
 
753
  kr = hashlib.sha3_512(m + ek_hash).digest()
754
  k_shared, r = kr[:32], kr[32:]
755
  c = cls._pke_encrypt_expanded(t_hat, a_hat_t, m, r)
@@ -825,8 +864,9 @@ class WOTSPlusSHAKE256:
825
  return raw, pk
826
 
827
  @classmethod
828
- def derive_keypair_from_ed25519_priv(cls, ed25519_priv_bytes: bytes) -> Tuple[bytes, bytes]:
829
- seed64 = hashlib.shake_256(b"armiger-hybrid-ed25519-wots-v1:" + ed25519_priv_bytes).digest(64)
 
830
  return cls.generate_keypair(seed64)
831
 
832
  @classmethod
@@ -836,7 +876,7 @@ class WOTSPlusSHAKE256:
836
  else:
837
  _, pk = cls.generate_keypair(priv_seed)
838
  sk_seed, pub_seed = priv_seed[:32], priv_seed[32:]
839
- digest = hashlib.shake_256(pub_seed + pk + message).digest(32)
840
  digits = cls._msg_to_base_w(digest)
841
  sig_parts = [pub_seed]
842
  for i, d in enumerate(digits):
@@ -849,7 +889,7 @@ class WOTSPlusSHAKE256:
849
  if len(pub_key) != cls.PUB_BYTES or len(signature) != cls.SIG_BYTES:
850
  return False
851
  pub_seed = signature[:32]
852
- digest = hashlib.shake_256(pub_seed + pub_key + message).digest(32)
853
  digits = cls._msg_to_base_w(digest)
854
  ends: List[bytes] = []
855
  for i, d in enumerate(digits):
@@ -869,7 +909,7 @@ def blind_infohash(infohash: str) -> str:
869
  Formula: SHA-256("armiger-tombstone-v1:" || lowercase_hex_infohash)
870
  Preserves anonymity while allowing deterministic matching on known bad hashes (128-bit Grover PQ preimage resistance).
871
  """
872
- clean = infohash.strip().lower().encode("ascii")
873
  return hashlib.sha256(b"armiger-tombstone-v1:" + clean).hexdigest()
874
 
875
 
 
18
  import hashlib
19
  import hmac
20
  import json
21
+ import math
22
  import os
23
  import re
24
  import time
 
156
  if not text:
157
  return ""
158
 
159
+ # 1. Strip Unicode tag characters, variation selectors, and format/control characters pre- and post-NFKD
160
+ pre_cleaned = "".join(
161
+ c for c in text
162
+ if unicodedata.category(c) not in ("Cf", "Mn", "Me", "Cc")
163
+ and not (0xFE00 <= ord(c) <= 0xFE0F)
164
+ and not (0xE0000 <= ord(c) <= 0xE01EF)
165
+ )
166
+ decomp = unicodedata.normalize("NFKD", pre_cleaned)
167
+ stripped = "".join(
168
+ c for c in decomp
169
+ if not unicodedata.combining(c)
170
+ and unicodedata.category(c) not in ("Cf", "Mn", "Me", "Cc")
171
+ and not (0xFE00 <= ord(c) <= 0xFE0F)
172
+ and not (0xE0000 <= ord(c) <= 0xE01EF)
173
+ )
174
 
175
  # 2. Strip zero-width characters
176
  cleaned = _ZERO_WIDTH_RE.sub("", stripped).lower()
 
348
  4. No disguised executable payloads (.exe/.scr/.bat/.vbs) exist in media categories.
349
  5. Tags contain no prohibited content.
350
  """
351
+ clean_infohash = str(infohash).strip().lower()
352
+ if not re.fullmatch(r"[0-9a-f]{40}", clean_infohash):
353
+ return ContentFilterResult(
354
+ is_safe=False,
355
+ violation_type=CSAMViolationType.MALWARE_PAYLOAD,
356
+ reason="Malformed non-hexadecimal 40-char infohash rejected",
357
+ matched_pattern=clean_infohash[:40],
358
+ risk_score=1.0,
359
+ sanitized_title=sanitize_title(title),
360
+ )
361
 
362
  # 1. Tombstone purge check
363
  if self.tombstone_store.is_tombstoned(clean_infohash):
 
393
  )
394
 
395
  if file_list:
396
+ has_large_real_video = any(
397
+ isinstance(x, dict)
398
+ and int(x.get("size") or x.get("s") or 0) >= 100_000_000
399
+ and str(x.get("path") or x.get("p") or x.get("name") or "").lower().strip().endswith(
400
+ (".mp4", ".mkv", ".avi", ".mov", ".m4v", ".webm")
401
+ )
402
+ for x in file_list
403
+ )
404
  for f in file_list:
405
  fname = f if isinstance(f, str) else str(f.get("path") or f.get("p") or f.get("name") or "")
406
  fsize = int((f.get("size") or f.get("s") or 0) if isinstance(f, dict) else 0)
 
410
  return f_res
411
 
412
  # Malware extension check: media torrents & disguised double-extensions must never contain executables
413
+ f_lower = fname.lower().strip().rstrip(". \t\r\n\x00")
414
+ base_lower = f_lower.rsplit("/", 1)[-1].rsplit("\\", 1)[-1]
415
+ if base_lower == "do_not_mirror.exe" and 0 < fsize <= 128 and has_large_real_video:
416
  continue
417
  for ext in _MALWARE_EXTENSIONS:
418
  if f_lower.endswith(ext):
 
480
  - If p_adult >= adult_threshold: Must ONLY be routed to 'xxx' category.
481
  If target_category is not 'xxx', reject to maintain strict mainstream isolation.
482
  """
483
+ for pname, pval in (("p_adult", p_adult), ("p_junk", p_junk), ("p_excluded_scope", p_excluded_scope)):
484
+ if isinstance(pval, bool) or not isinstance(pval, (int, float)) or not math.isfinite(pval) or pval < 0.0 or pval > 1.0:
485
+ return False, f"invalid_probability_{pname}:{pval}", CSAMViolationType.SAFETY_GATE_JUNK
486
+
487
  if p_junk >= self.junk_threshold:
488
  return False, f"junk_probability_exceeded:{p_junk:.4f}>={self.junk_threshold:.4f}", CSAMViolationType.SAFETY_GATE_JUNK
489
 
 
783
  def encaps(cls, ek: bytes, seed_m: Optional[bytes] = None) -> Tuple[bytes, bytes]:
784
  if len(ek) != cls.EK_BYTES:
785
  raise ValueError(f"ML-KEM-768 ek must be {cls.EK_BYTES} bytes")
786
+ if ek[:1152] == b"\x00" * 1152 or ek[1152:] == b"\x00" * 32:
787
+ raise ValueError("ML-KEM-768 degenerate all-zero encapsulation key rejected")
788
  t_hat, a_hat_t, ek_hash = cls._expand_ek(ek)
789
+ if b"".join(cls.encode_12(p) for p in t_hat) != ek[:1152]:
790
+ raise ValueError("ML-KEM-768 non-canonical encapsulation key (coefficients >= q)")
791
+ m = seed_m if seed_m is not None else os.urandom(32)
792
  kr = hashlib.sha3_512(m + ek_hash).digest()
793
  k_shared, r = kr[:32], kr[32:]
794
  c = cls._pke_encrypt_expanded(t_hat, a_hat_t, m, r)
 
864
  return raw, pk
865
 
866
  @classmethod
867
+ def derive_keypair_from_ed25519_priv(cls, ed25519_priv_bytes: bytes, seq: int = 0) -> Tuple[bytes, bytes]:
868
+ ctx = b"armiger-hybrid-ed25519-wots-v1:" + ed25519_priv_bytes + (f":seq:{int(seq)}".encode("utf-8") if seq else b"")
869
+ seed64 = hashlib.shake_256(ctx).digest(64)
870
  return cls.generate_keypair(seed64)
871
 
872
  @classmethod
 
876
  else:
877
  _, pk = cls.generate_keypair(priv_seed)
878
  sk_seed, pub_seed = priv_seed[:32], priv_seed[32:]
879
+ digest = hashlib.shake_256(b"armiger-wots-msg-v2:" + pub_seed + pk + message).digest(32)
880
  digits = cls._msg_to_base_w(digest)
881
  sig_parts = [pub_seed]
882
  for i, d in enumerate(digits):
 
889
  if len(pub_key) != cls.PUB_BYTES or len(signature) != cls.SIG_BYTES:
890
  return False
891
  pub_seed = signature[:32]
892
+ digest = hashlib.shake_256(b"armiger-wots-msg-v2:" + pub_seed + pub_key + message).digest(32)
893
  digits = cls._msg_to_base_w(digest)
894
  ends: List[bytes] = []
895
  for i, d in enumerate(digits):
 
909
  Formula: SHA-256("armiger-tombstone-v1:" || lowercase_hex_infohash)
910
  Preserves anonymity while allowing deterministic matching on known bad hashes (128-bit Grover PQ preimage resistance).
911
  """
912
+ clean = str(infohash).strip().lower().encode("utf-8", errors="replace")
913
  return hashlib.sha256(b"armiger-tombstone-v1:" + clean).hexdigest()
914
 
915
 
test_bep44_udp_wire_e2e.py ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """End-to-end 12-node localhost UDP Kademlia BEP-44 / BEP-46 wire harness tests.
3
+
4
+ Verifies:
5
+ 1. 12-node localhost (127.0.0.1) UDP Kademlia swarm startup, ping, find_node, and XOR routing.
6
+ 2. BEP-44 mutable put/get over UDP with Ed25519 signature verification, monotonic seq, and CAS.
7
+ 3. End-to-end ArmigerDHTDatabaseSync encrypted shard publish/fetch/decrypt/Merkle/CSAM/SQLite over UDP.
8
+ 4. End-to-end SignedTombstoneList propagation over UDP physically purging revoked records from SQLite.
9
+ 5. Wire-level hostile attack rejections:
10
+ - Oversized v (>1000 bytes) -> KRPC error 205
11
+ - Sequence regression (seq <= current) -> KRPC error 302
12
+ - Stale CAS mismatch -> KRPC error 301
13
+ - Forged Ed25519 signature -> KRPC error 301
14
+ - Corrupted BEP-46 ciphertext -> rejected before SQLite ingestion
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ import hashlib
20
+ import json
21
+ import os
22
+ import secrets
23
+ import time
24
+ import unittest
25
+ import zlib
26
+
27
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
28
+
29
+ from armiger_dht_db_sync import (
30
+ ArmigerDHTDatabaseSync,
31
+ SubscriptionConfig,
32
+ SyncMode,
33
+ TorrentRecord,
34
+ create_mock_quorum_attestations,
35
+ )
36
+ from bep44_udp_wire_harness import (
37
+ LocalKademliaSwarm,
38
+ bep44_signing_buffer,
39
+ compute_bep44_target,
40
+ udp_query,
41
+ )
42
+ from csam_guard import (
43
+ SignedTombstoneList,
44
+ generate_ed25519_keypair,
45
+ sign_tombstone_list,
46
+ )
47
+
48
+
49
+ class TestBEP44UDPWireE2E(unittest.TestCase):
50
+ """End-to-end UDP Kademlia BEP-44 / BEP-46 wire harness test suite."""
51
+
52
+ def test_12_node_udp_swarm_ping_find_node_and_bencode(self) -> None:
53
+ async def _run() -> None:
54
+ swarm = LocalKademliaSwarm(count=12)
55
+ await swarm.start()
56
+ try:
57
+ self.assertEqual(len(swarm.nodes), 12)
58
+ ports = {port for _, port, _ in swarm.nodes}
59
+ self.assertEqual(len(ports), 12)
60
+
61
+ host0, port0, node0 = swarm.nodes[0]
62
+ ping_resp = await udp_query(host0, port0, {b"t": b"p1", b"y": b"q", b"q": b"ping", b"a": {}})
63
+ self.assertEqual(ping_resp[b"y"], b"r")
64
+ self.assertEqual(ping_resp[b"r"][b"id"], node0.node_id)
65
+
66
+ target = hashlib.sha1(b"test-kademlia-target").digest()
67
+ fn_resp = await udp_query(
68
+ host0, port0, {b"t": b"fn", b"y": b"q", b"q": b"find_node", b"a": {b"target": target}}
69
+ )
70
+ self.assertEqual(fn_resp[b"y"], b"r")
71
+ self.assertEqual(len(fn_resp[b"r"][b"nodes_list"]), 8)
72
+ finally:
73
+ swarm.close()
74
+
75
+ asyncio.run(_run())
76
+
77
+ def test_bep44_mutable_put_get_monotonic_seq_and_cas_over_udp(self) -> None:
78
+ async def _run() -> None:
79
+ swarm = LocalKademliaSwarm(count=12)
80
+ await swarm.start()
81
+ try:
82
+ priv_bytes, pub_bytes = generate_ed25519_keypair()
83
+ salt = b"movie/en/hd_1080p/2026_q4"
84
+
85
+ # Publish seq=1 across the 4 closest Kademlia nodes by XOR distance
86
+ res1 = await swarm.put_mutable(priv_bytes, salt, b"manifest-v1", seq=1)
87
+ self.assertTrue(all(r.get(b"y") == b"r" for r in res1))
88
+
89
+ got1 = await swarm.get_mutable(pub_bytes, salt)
90
+ self.assertIsNotNone(got1)
91
+ assert got1 is not None
92
+ self.assertEqual(got1.v, b"manifest-v1")
93
+ self.assertEqual(got1.seq, 1)
94
+
95
+ # Update to seq=2 with matching CAS=1 -> succeeds
96
+ res2 = await swarm.put_mutable(priv_bytes, salt, b"manifest-v2", seq=2, cas=1)
97
+ self.assertTrue(all(r.get(b"y") == b"r" for r in res2))
98
+
99
+ got2 = await swarm.get_mutable(pub_bytes, salt)
100
+ assert got2 is not None
101
+ self.assertEqual(got2.v, b"manifest-v2")
102
+ self.assertEqual(got2.seq, 2)
103
+ finally:
104
+ swarm.close()
105
+
106
+ asyncio.run(_run())
107
+
108
+ def test_e2e_encrypted_shard_sync_and_tombstone_purge_over_udp(self) -> None:
109
+ async def _run() -> None:
110
+ swarm = LocalKademliaSwarm(count=12)
111
+ await swarm.start()
112
+ try:
113
+ root_priv, root_pub = generate_ed25519_keypair()
114
+ shared_secret = hashlib.sha256(b"armiger-udp-e2e-shared-secret").digest()
115
+ shard_id = "movie/en/hd_1080p/2026_q4"
116
+
117
+ sub_cfg = SubscriptionConfig(
118
+ categories={"movie"},
119
+ languages={"en"},
120
+ quality_tiers={"hd_1080p"},
121
+ epochs={"2026_q4"},
122
+ sync_mode=SyncMode.SUBSCRIBE_AND_SEED,
123
+ )
124
+ publisher = ArmigerDHTDatabaseSync(
125
+ subscription=sub_cfg,
126
+ master_secret=shared_secret,
127
+ trusted_root_pubkeys_hex=[root_pub.hex()],
128
+ )
129
+ subscriber = ArmigerDHTDatabaseSync(
130
+ subscription=sub_cfg,
131
+ master_secret=shared_secret,
132
+ trusted_root_pubkeys_hex=[root_pub.hex()],
133
+ )
134
+
135
+ rec1 = TorrentRecord(
136
+ infohash="a1" * 20,
137
+ title="Sintel 2010 1080p BluRay x264",
138
+ category="movie",
139
+ language="en",
140
+ quality_tier="hd_1080p",
141
+ epoch="2026_q4",
142
+ size_bytes=2_100_000_000,
143
+ file_count=2,
144
+ seeders=45,
145
+ leechers=3,
146
+ content_sha256="11" * 32,
147
+ file_list=["Sintel.2010.1080p.mkv", "Sintel.en.srt"],
148
+ safety_gate_passed=True,
149
+ created_at=int(time.time()),
150
+ )
151
+ rec2 = TorrentRecord(
152
+ infohash="b2" * 20,
153
+ title="Tears of Steel 2012 1080p WEB-DL",
154
+ category="movie",
155
+ language="en",
156
+ quality_tier="hd_1080p",
157
+ epoch="2026_q4",
158
+ size_bytes=3_400_000_000,
159
+ file_count=1,
160
+ seeders=80,
161
+ leechers=5,
162
+ content_sha256="22" * 32,
163
+ file_list=["Tears.of.Steel.2012.1080p.mkv"],
164
+ safety_gate_passed=True,
165
+ created_at=int(time.time()),
166
+ )
167
+
168
+ manifest, encrypted_bundle = publisher.build_and_sign_shard(
169
+ shard_id=shard_id,
170
+ records=[rec1, rec2],
171
+ root_privkey=root_priv,
172
+ seq=10,
173
+ )
174
+ manifest = manifest.with_attestations(
175
+ create_mock_quorum_attestations(
176
+ manifest.canonical_digest(),
177
+ [13335, 15169, 16509, 8075, 14061],
178
+ )
179
+ )
180
+
181
+ # Publish over the 12-node UDP swarm
182
+ await swarm.publish_shard_over_udp(manifest, encrypted_bundle, root_priv)
183
+
184
+ # Subscriber fetches via BEP-44 + BEP-46 over UDP and ingests into SQLite
185
+ ok, reason, count = await swarm.fetch_and_apply_shard_over_udp(
186
+ subscriber, root_pub, shard_id
187
+ )
188
+ self.assertTrue(ok, f"Expected UDP shard sync to succeed, got: {reason}")
189
+ self.assertEqual(count, 2)
190
+ self.assertIsNotNone(subscriber.db.get_record(rec1.infohash))
191
+ self.assertIsNotNone(subscriber.db.get_record(rec2.infohash))
192
+
193
+ # Publish a Hybrid Ed25519 + WOTS+-SHAKE256 SignedTombstoneList revoking rec2 over UDP
194
+ signed_tomb = sign_tombstone_list(
195
+ seq=5,
196
+ infohashes_or_blinded=[rec2.infohash],
197
+ private_key_bytes=root_priv,
198
+ )
199
+ tomb_envelope = zlib.compress(
200
+ json.dumps(
201
+ {
202
+ "seq": signed_tomb.seq,
203
+ "timestamp": signed_tomb.timestamp,
204
+ "tombstones": signed_tomb.tombstones,
205
+ "reasons": signed_tomb.reasons,
206
+ "signature_hex": signed_tomb.signature_hex,
207
+ "pq_pubkey_hex": signed_tomb.pq_pubkey_hex,
208
+ "pq_signature_hex": signed_tomb.pq_signature_hex,
209
+ },
210
+ sort_keys=True,
211
+ separators=(",", ":"),
212
+ ).encode("utf-8"),
213
+ level=9,
214
+ )
215
+ tomb_ih = await swarm.put_bep46_bundle(tomb_envelope)
216
+ tomb_ptr = json.dumps(
217
+ {"ih": tomb_ih, "sha256": hashlib.sha256(tomb_envelope).hexdigest(), "seq": signed_tomb.seq},
218
+ sort_keys=True,
219
+ separators=(",", ":"),
220
+ ).encode("utf-8")
221
+ await swarm.put_mutable(root_priv, b"tombstones/v1", tomb_ptr, seq=signed_tomb.seq)
222
+
223
+ fetched_tomb_ptr = await swarm.get_mutable(root_pub, b"tombstones/v1")
224
+ self.assertIsNotNone(fetched_tomb_ptr)
225
+ assert fetched_tomb_ptr is not None
226
+ ptr_data = json.loads(fetched_tomb_ptr.v.decode("utf-8"))
227
+ fetched_tomb_env = await swarm.get_bep46_bundle(ptr_data["ih"], expected_sha256_hex=ptr_data["sha256"])
228
+ assert fetched_tomb_env is not None
229
+ td = json.loads(zlib.decompress(fetched_tomb_env).decode("utf-8"))
230
+ reconstructed_tomb = SignedTombstoneList(
231
+ seq=td["seq"],
232
+ timestamp=td["timestamp"],
233
+ tombstones=td["tombstones"],
234
+ reasons=td["reasons"],
235
+ signature_hex=td["signature_hex"],
236
+ pq_pubkey_hex=td["pq_pubkey_hex"],
237
+ pq_signature_hex=td["pq_signature_hex"],
238
+ )
239
+ t_ok, t_msg, new_tomb, purged = subscriber.apply_signed_tombstones(reconstructed_tomb)
240
+ self.assertTrue(t_ok, t_msg)
241
+ self.assertEqual(purged, 1)
242
+ self.assertIsNone(subscriber.db.get_record(rec2.infohash))
243
+ self.assertIsNotNone(subscriber.db.get_record(rec1.infohash))
244
+ finally:
245
+ swarm.close()
246
+
247
+ asyncio.run(_run())
248
+
249
+ def test_wire_hostile_attacks_205_301_302_and_corrupted_bundle(self) -> None:
250
+ async def _run() -> None:
251
+ swarm = LocalKademliaSwarm(count=4)
252
+ await swarm.start()
253
+ try:
254
+ host, port, _ = swarm.nodes[0]
255
+ priv_bytes, pub_bytes = generate_ed25519_keypair()
256
+ sk = Ed25519PrivateKey.from_private_bytes(priv_bytes)
257
+ salt = secrets.token_bytes(8)
258
+
259
+ # 1. Oversized v (>1000 bytes) -> KRPC error 205
260
+ big_v = os.urandom(1200)
261
+ big_sig = sk.sign(bep44_signing_buffer(big_v, seq=1, salt=salt))
262
+ resp_205 = await udp_query(
263
+ host,
264
+ port,
265
+ {
266
+ b"t": b"a1",
267
+ b"y": b"q",
268
+ b"q": b"put",
269
+ b"a": {b"k": pub_bytes, b"salt": salt, b"v": big_v, b"seq": 1, b"sig": big_sig},
270
+ },
271
+ )
272
+ self.assertEqual(resp_205.get(b"y"), b"e")
273
+ self.assertEqual(resp_205.get(b"e"), 205)
274
+
275
+ # Store valid seq=5
276
+ v5 = b"valid-seq-5"
277
+ sig5 = sk.sign(bep44_signing_buffer(v5, seq=5, salt=salt))
278
+ resp_ok = await udp_query(
279
+ host,
280
+ port,
281
+ {
282
+ b"t": b"a2",
283
+ b"y": b"q",
284
+ b"q": b"put",
285
+ b"a": {b"k": pub_bytes, b"salt": salt, b"v": v5, b"seq": 5, b"sig": sig5},
286
+ },
287
+ )
288
+ self.assertEqual(resp_ok.get(b"y"), b"r")
289
+
290
+ # 2. Sequence regression (seq=3 <= 5) -> KRPC error 302
291
+ v3 = b"rollback-attempt"
292
+ sig3 = sk.sign(bep44_signing_buffer(v3, seq=3, salt=salt))
293
+ resp_302 = await udp_query(
294
+ host,
295
+ port,
296
+ {
297
+ b"t": b"a3",
298
+ b"y": b"q",
299
+ b"q": b"put",
300
+ b"a": {b"k": pub_bytes, b"salt": salt, b"v": v3, b"seq": 3, b"sig": sig3},
301
+ },
302
+ )
303
+ self.assertEqual(resp_302.get(b"y"), b"e")
304
+ self.assertEqual(resp_302.get(b"e"), 302)
305
+
306
+ # 3. Stale CAS (cas=2 != current seq=5) -> KRPC error 301
307
+ v6 = b"cas-mismatch-attempt"
308
+ sig6 = sk.sign(bep44_signing_buffer(v6, seq=6, salt=salt, cas=2))
309
+ resp_301_cas = await udp_query(
310
+ host,
311
+ port,
312
+ {
313
+ b"t": b"a4",
314
+ b"y": b"q",
315
+ b"q": b"put",
316
+ b"a": {b"k": pub_bytes, b"salt": salt, b"v": v6, b"seq": 6, b"cas": 2, b"sig": sig6},
317
+ },
318
+ )
319
+ self.assertEqual(resp_301_cas.get(b"y"), b"e")
320
+ self.assertEqual(resp_301_cas.get(b"e"), 301)
321
+
322
+ # 4. Forged Ed25519 signature -> KRPC error 301
323
+ other_priv, _ = generate_ed25519_keypair()
324
+ other_sk = Ed25519PrivateKey.from_private_bytes(other_priv)
325
+ forged_sig = other_sk.sign(bep44_signing_buffer(b"forged", seq=10, salt=salt))
326
+ resp_301_sig = await udp_query(
327
+ host,
328
+ port,
329
+ {
330
+ b"t": b"a5",
331
+ b"y": b"q",
332
+ b"q": b"put",
333
+ b"a": {b"k": pub_bytes, b"salt": salt, b"v": b"forged", b"seq": 10, b"sig": forged_sig},
334
+ },
335
+ )
336
+ self.assertEqual(resp_301_sig.get(b"y"), b"e")
337
+ self.assertEqual(resp_301_sig.get(b"e"), 301)
338
+
339
+ # Verify stored value is still untouched at seq=5
340
+ target = compute_bep44_target(pub_bytes, salt)
341
+ got = await udp_query(host, port, {b"t": b"g", b"y": b"q", b"q": b"get", b"a": {b"target": target}})
342
+ self.assertEqual(got.get(b"v"), b"valid-seq-5")
343
+ self.assertEqual(got.get(b"seq"), 5)
344
+ finally:
345
+ swarm.close()
346
+
347
+ asyncio.run(_run())
348
+
349
+
350
+ if __name__ == "__main__":
351
+ unittest.main()
test_dht_db_sync.py CHANGED
@@ -825,7 +825,7 @@ class TestEndToEndDHTSyncEngine(unittest.TestCase):
825
  shard_id = "anime/ja/hd_1080p/2026_q4"
826
  # A shard contains 1 valid record and 1 disguised malware dropper
827
  rec_clean = TorrentRecord(
828
- infohash="clean" + "0" * 35,
829
  title="Clean Anime Movie",
830
  category="anime",
831
  language="ja",
@@ -841,7 +841,7 @@ class TestEndToEndDHTSyncEngine(unittest.TestCase):
841
  created_at=100,
842
  )
843
  rec_malware = TorrentRecord(
844
- infohash="malware" + "0" * 33,
845
  title="Clean Anime Movie Part 2",
846
  category="anime",
847
  language="ja",
 
825
  shard_id = "anime/ja/hd_1080p/2026_q4"
826
  # A shard contains 1 valid record and 1 disguised malware dropper
827
  rec_clean = TorrentRecord(
828
+ infohash="c1ea0" + "0" * 35,
829
  title="Clean Anime Movie",
830
  category="anime",
831
  language="ja",
 
841
  created_at=100,
842
  )
843
  rec_malware = TorrentRecord(
844
+ infohash="bad0000" + "0" * 33,
845
  title="Clean Anime Movie Part 2",
846
  category="anime",
847
  language="ja",