fleetmesh-node 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (338) hide show
  1. fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
  2. fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
  3. fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
  4. fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
  5. fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
  6. fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
  7. node/RELEASE +1 -0
  8. node/__init__.py +35 -0
  9. node/app/__init__.py +49 -0
  10. node/app/commands.py +265 -0
  11. node/app/config.py +131 -0
  12. node/app/control.py +670 -0
  13. node/app/core.py +867 -0
  14. node/app/plugins.py +76 -0
  15. node/app/router.py +83 -0
  16. node/app/socket.py +431 -0
  17. node/app/systemd.py +36 -0
  18. node/app/views.py +204 -0
  19. node/channels/__init__.py +378 -0
  20. node/channels/bus.py +331 -0
  21. node/channels/journal.py +46 -0
  22. node/commands/__init__.py +140 -0
  23. node/commands/adapter.py +210 -0
  24. node/commands/base.py +278 -0
  25. node/commands/cluster_cmds.py +183 -0
  26. node/commands/compute_cmds.py +401 -0
  27. node/commands/core.py +474 -0
  28. node/commands/gossip_cmds.py +106 -0
  29. node/commands/nsite_cmds.py +141 -0
  30. node/commands/peering.py +402 -0
  31. node/commands/plugin_cmds.py +135 -0
  32. node/commands/sidecar_cmds.py +120 -0
  33. node/commands/sync.py +388 -0
  34. node/commands/transport_cmds.py +451 -0
  35. node/commands/video_cmds.py +99 -0
  36. node/compartment/__init__.py +124 -0
  37. node/compartment/base.py +868 -0
  38. node/compartment/command.py +458 -0
  39. node/compartment/context.py +163 -0
  40. node/compartment/every.py +108 -0
  41. node/compartment/handler.py +131 -0
  42. node/compartment/lifecycle.py +460 -0
  43. node/compartment/lint.py +388 -0
  44. node/compartment/need.py +158 -0
  45. node/compartment/problem.py +521 -0
  46. node/compartment/refusals.py +120 -0
  47. node/compartment/registry.py +790 -0
  48. node/compartment/schema.py +290 -0
  49. node/compartment/sidecar.py +499 -0
  50. node/compartment/stores.py +662 -0
  51. node/compartment/wire.py +225 -0
  52. node/compartment/worker.py +61 -0
  53. node/config/__init__.py +43 -0
  54. node/config/base.py +116 -0
  55. node/config/change.py +441 -0
  56. node/config/changestore.py +201 -0
  57. node/config/compartments.py +1144 -0
  58. node/config/node.py +318 -0
  59. node/config/parameter.py +563 -0
  60. node/config/protocols.py +274 -0
  61. node/config/records.py +171 -0
  62. node/examples/__init__.py +1 -0
  63. node/examples/demo.py +226 -0
  64. node/examples/follower.py +46 -0
  65. node/interfaces.py +235 -0
  66. node/keystore/__init__.py +27 -0
  67. node/keystore/core.py +103 -0
  68. node/keystore/enclave.py +688 -0
  69. node/meshnode.py +111 -0
  70. node/metrics/__init__.py +396 -0
  71. node/metrics/adapters.py +102 -0
  72. node/metrics/face.py +110 -0
  73. node/planes/__init__.py +21 -0
  74. node/planes/identity/__init__.py +20 -0
  75. node/planes/identity/announce/__init__.py +9 -0
  76. node/planes/identity/announce/compartment.py +447 -0
  77. node/planes/identity/announce/types.py +26 -0
  78. node/planes/identity/discovery/__init__.py +9 -0
  79. node/planes/identity/discovery/compartment.py +522 -0
  80. node/planes/identity/discovery/types.py +13 -0
  81. node/planes/identity/linked/__init__.py +1 -0
  82. node/planes/identity/linked/compartment.py +270 -0
  83. node/planes/identity/linked/types.py +25 -0
  84. node/planes/identity/presence/__init__.py +7 -0
  85. node/planes/identity/presence/compartment.py +409 -0
  86. node/planes/identity/presence/types.py +14 -0
  87. node/planes/operations/__init__.py +13 -0
  88. node/planes/operations/scheduler/__init__.py +9 -0
  89. node/planes/operations/scheduler/compartment.py +353 -0
  90. node/planes/operations/scheduler/types.py +21 -0
  91. node/planes/operations/updates/__init__.py +17 -0
  92. node/planes/operations/updates/core.py +310 -0
  93. node/planes/operations/updates/store.py +79 -0
  94. node/planes/operations/updates/types.py +20 -0
  95. node/planes/peering/__init__.py +28 -0
  96. node/planes/peering/budget/__init__.py +43 -0
  97. node/planes/peering/budget/chain.py +43 -0
  98. node/planes/peering/budget/core.py +537 -0
  99. node/planes/peering/budget/evaluation.py +80 -0
  100. node/planes/peering/budget/meter.py +100 -0
  101. node/planes/peering/budget/receipts.py +439 -0
  102. node/planes/peering/budget/shape.py +101 -0
  103. node/planes/peering/budget/tick.py +110 -0
  104. node/planes/peering/budget/types.py +67 -0
  105. node/planes/peering/budget/windows.py +346 -0
  106. node/planes/peering/capacity/__init__.py +7 -0
  107. node/planes/peering/capacity/compartment.py +342 -0
  108. node/planes/peering/capacity/types.py +24 -0
  109. node/planes/peering/peering/__init__.py +25 -0
  110. node/planes/peering/peering/core.py +464 -0
  111. node/planes/peering/peering/envelope.py +142 -0
  112. node/planes/peering/peering/handshake.py +198 -0
  113. node/planes/peering/peering/shape.py +80 -0
  114. node/planes/peering/peering/types.py +39 -0
  115. node/planes/peering/reporter/__init__.py +42 -0
  116. node/planes/peering/reporter/constants.py +29 -0
  117. node/planes/peering/reporter/core.py +299 -0
  118. node/planes/peering/reporter/finding.py +74 -0
  119. node/planes/peering/reporter/findings.py +171 -0
  120. node/planes/peering/reporter/publish.py +285 -0
  121. node/planes/peering/reporter/reading.py +193 -0
  122. node/planes/peering/reporter/shape.py +86 -0
  123. node/planes/peering/reporter/types.py +50 -0
  124. node/planes/peering/strain/__init__.py +9 -0
  125. node/planes/peering/strain/compartment.py +204 -0
  126. node/planes/peering/strain/types.py +21 -0
  127. node/planes/peering/trust/__init__.py +9 -0
  128. node/planes/peering/trust/compartment.py +239 -0
  129. node/planes/peering/trust/types.py +30 -0
  130. node/planes/services/__init__.py +33 -0
  131. node/planes/services/compute/__init__.py +160 -0
  132. node/planes/services/compute/common.py +134 -0
  133. node/planes/services/compute/compartment.py +681 -0
  134. node/planes/services/compute/executors/__init__.py +121 -0
  135. node/planes/services/compute/executors/agent.py +840 -0
  136. node/planes/services/compute/executors/bacalhau.py +605 -0
  137. node/planes/services/compute/executors/base.py +127 -0
  138. node/planes/services/compute/executors/harness.py +439 -0
  139. node/planes/services/compute/executors/mcp.py +454 -0
  140. node/planes/services/compute/executors/media.py +898 -0
  141. node/planes/services/compute/executors/nondet.py +283 -0
  142. node/planes/services/compute/executors/providers.py +499 -0
  143. node/planes/services/compute/executors/registry.py +140 -0
  144. node/planes/services/compute/executors/wasm.py +244 -0
  145. node/planes/services/compute/executors/zk.py +604 -0
  146. node/planes/services/compute/jobs.py +227 -0
  147. node/planes/services/compute/load.py +180 -0
  148. node/planes/services/compute/market.py +433 -0
  149. node/planes/services/compute/sandbox.py +282 -0
  150. node/planes/services/compute/types.py +84 -0
  151. node/planes/services/compute_wire/__init__.py +58 -0
  152. node/planes/services/compute_wire/compartment.py +256 -0
  153. node/planes/services/compute_wire/messages.py +320 -0
  154. node/planes/services/compute_wire/sealing.py +147 -0
  155. node/planes/services/compute_wire/session.py +572 -0
  156. node/planes/services/compute_wire/types.py +49 -0
  157. node/planes/services/compute_wire/verify.py +331 -0
  158. node/planes/services/dvm/__init__.py +24 -0
  159. node/planes/services/dvm/gateway.py +526 -0
  160. node/planes/services/dvm/protocol.py +35 -0
  161. node/planes/services/dvm/translate.py +15 -0
  162. node/planes/services/dvm/types.py +25 -0
  163. node/planes/services/gateway/__init__.py +40 -0
  164. node/planes/services/gateway/settle.py +279 -0
  165. node/planes/services/gateway/translate.py +644 -0
  166. node/planes/services/gossip/__init__.py +22 -0
  167. node/planes/services/gossip/core.py +929 -0
  168. node/planes/services/gossip/envelope.py +154 -0
  169. node/planes/services/gossip/mesh.py +127 -0
  170. node/planes/services/gossip/types.py +50 -0
  171. node/planes/services/nsite/__init__.py +7 -0
  172. node/planes/services/nsite/client.py +62 -0
  173. node/planes/services/nsite/compartment.py +752 -0
  174. node/planes/services/nsite/types.py +44 -0
  175. node/planes/services/paidfetch/__init__.py +7 -0
  176. node/planes/services/paidfetch/compartment.py +862 -0
  177. node/planes/services/paidfetch/types.py +99 -0
  178. node/planes/services/settle/__init__.py +27 -0
  179. node/planes/services/settle/compartment.py +384 -0
  180. node/planes/services/settle/engine.py +456 -0
  181. node/planes/services/settle/gateways.py +541 -0
  182. node/planes/services/settle/payment.py +512 -0
  183. node/planes/services/settle/store.py +74 -0
  184. node/planes/services/settle/types.py +58 -0
  185. node/planes/services/sidecars/__init__.py +8 -0
  186. node/planes/services/sidecars/compartment.py +461 -0
  187. node/planes/services/sidecars/manifest.py +304 -0
  188. node/planes/services/sidecars/manifests/10-grasp.json +37 -0
  189. node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
  190. node/planes/services/sidecars/manifests/30-magpie.json +28 -0
  191. node/planes/services/sidecars/types.py +43 -0
  192. node/planes/services/video/__init__.py +106 -0
  193. node/planes/services/video/barter.py +407 -0
  194. node/planes/services/video/cli.py +158 -0
  195. node/planes/services/video/compartment.py +932 -0
  196. node/planes/services/video/ingest.py +524 -0
  197. node/planes/services/video/ledger.py +705 -0
  198. node/planes/services/video/packager.py +847 -0
  199. node/planes/services/video/player.py +456 -0
  200. node/planes/services/video/timed_segments.py +124 -0
  201. node/planes/services/video/transit.py +478 -0
  202. node/planes/store/__init__.py +24 -0
  203. node/planes/store/blob_replication/__init__.py +7 -0
  204. node/planes/store/blob_replication/compartment.py +496 -0
  205. node/planes/store/blob_replication/types.py +35 -0
  206. node/planes/store/blob_wire/__init__.py +48 -0
  207. node/planes/store/blob_wire/client.py +57 -0
  208. node/planes/store/blob_wire/core.py +359 -0
  209. node/planes/store/blob_wire/intake.py +74 -0
  210. node/planes/store/blob_wire/messages.py +85 -0
  211. node/planes/store/projected/__init__.py +7 -0
  212. node/planes/store/projected/compartment.py +258 -0
  213. node/planes/store/projected/types.py +29 -0
  214. node/planes/store/retention/__init__.py +7 -0
  215. node/planes/store/retention/compartment.py +214 -0
  216. node/planes/store/retention/types.py +12 -0
  217. node/planes/store/sync/__init__.py +35 -0
  218. node/planes/store/sync/core.py +311 -0
  219. node/planes/store/sync/envelope.py +106 -0
  220. node/planes/store/sync/handover.py +206 -0
  221. node/planes/store/sync/initiator.py +128 -0
  222. node/planes/store/sync/responder.py +114 -0
  223. node/planes/store/sync/rules.py +179 -0
  224. node/planes/store/sync/shape.py +80 -0
  225. node/planes/store/sync/types.py +51 -0
  226. node/planes/transport/__init__.py +23 -0
  227. node/planes/transport/bridge/__init__.py +13 -0
  228. node/planes/transport/bridge/compartment.py +785 -0
  229. node/planes/transport/bridge/ratelimit.py +44 -0
  230. node/planes/transport/bridge/types.py +43 -0
  231. node/planes/transport/lan/__init__.py +9 -0
  232. node/planes/transport/lan/compartment.py +446 -0
  233. node/planes/transport/lan/types.py +25 -0
  234. node/planes/transport/link/__init__.py +7 -0
  235. node/planes/transport/link/compartment.py +257 -0
  236. node/planes/transport/link/types.py +27 -0
  237. node/planes/transport/mediation/__init__.py +45 -0
  238. node/planes/transport/mediation/compartment.py +238 -0
  239. node/planes/transport/mediation/leaf.py +107 -0
  240. node/planes/transport/mediation/messages.py +302 -0
  241. node/planes/transport/mediation/service.py +233 -0
  242. node/planes/transport/mediation/store.py +37 -0
  243. node/planes/transport/mediation/types.py +20 -0
  244. node/planes/transport/p2p/__init__.py +7 -0
  245. node/planes/transport/p2p/compartment.py +349 -0
  246. node/planes/transport/p2p/types.py +22 -0
  247. node/protocols/__init__.py +277 -0
  248. node/protocols/activitypub/__init__.py +56 -0
  249. node/protocols/activitypub/actor.py +230 -0
  250. node/protocols/activitypub/federation.py +490 -0
  251. node/protocols/activitypub/media.py +145 -0
  252. node/protocols/activitypub/models.py +286 -0
  253. node/protocols/activitypub/package.py +271 -0
  254. node/protocols/activitypub/store.py +564 -0
  255. node/protocols/arcade/__init__.py +26 -0
  256. node/protocols/atproto/__init__.py +13 -0
  257. node/protocols/atproto/firehose.py +126 -0
  258. node/protocols/atproto/metering.py +165 -0
  259. node/protocols/atproto/package.py +155 -0
  260. node/protocols/atproto/repo.py +250 -0
  261. node/protocols/atproto/xrpc.py +285 -0
  262. node/protocols/base.py +109 -0
  263. node/protocols/circles/__init__.py +29 -0
  264. node/protocols/dm/__init__.py +50 -0
  265. node/protocols/grasp/__init__.py +22 -0
  266. node/protocols/isotime.py +15 -0
  267. node/protocols/nostr/__init__.py +51 -0
  268. node/protocols/nostr/blobstore.py +383 -0
  269. node/protocols/nostr/blossom.py +627 -0
  270. node/protocols/nostr/client.py +377 -0
  271. node/protocols/nostr/descriptor.py +211 -0
  272. node/protocols/nostr/eventstore.py +304 -0
  273. node/protocols/nostr/package.py +182 -0
  274. node/protocols/nostr/peers.py +109 -0
  275. node/protocols/nostr/relayserver.py +656 -0
  276. node/protocols/p2panda/__init__.py +22 -0
  277. node/protocols/p2panda/identity.py +19 -0
  278. node/protocols/p2panda/package.py +236 -0
  279. node/protocols/p2panda/rung.py +152 -0
  280. node/protocols/p2panda/store.py +273 -0
  281. node/protocols/rhio/__init__.py +33 -0
  282. node/protocols/rhio/package.py +275 -0
  283. node/protocols/rhio/rung.py +136 -0
  284. node/protocols/rhio/store.py +1423 -0
  285. node/protocols/ssb/muxrpc.py +256 -0
  286. node/protocols/ssb/package.py +267 -0
  287. node/protocols/textile/__init__.py +111 -0
  288. node/protocols/textile/transducers.py +259 -0
  289. node/release.py +119 -0
  290. node/storage/__init__.py +88 -0
  291. node/storage/accounts.py +305 -0
  292. node/storage/adapter.py +161 -0
  293. node/storage/base.py +385 -0
  294. node/storage/dialect.py +209 -0
  295. node/storage/grants.py +317 -0
  296. node/storage/locator.py +150 -0
  297. node/storage/postgres.py +294 -0
  298. node/storage/projected.py +953 -0
  299. node/storage/sharding.py +448 -0
  300. node/storage/standing.py +568 -0
  301. node/testnet/__init__.py +9 -0
  302. node/testnet/cluster.py +731 -0
  303. node/testnet/fips_daemon.py +230 -0
  304. node/transport/__init__.py +94 -0
  305. node/transport/const.py +110 -0
  306. node/transport/didcomm_inbound.py +251 -0
  307. node/transport/dnstxt.py +169 -0
  308. node/transport/fips_rung/__init__.py +67 -0
  309. node/transport/fips_rung/core.py +666 -0
  310. node/transport/fips_rung/listener.py +96 -0
  311. node/transport/fips_rung/types.py +12 -0
  312. node/transport/host.py +334 -0
  313. node/transport/iroh_rung/__init__.py +29 -0
  314. node/transport/iroh_rung/core.py +477 -0
  315. node/transport/iroh_rung/quic.py +942 -0
  316. node/transport/iroh_rung/types.py +22 -0
  317. node/transport/ladder.py +145 -0
  318. node/transport/lineproto.py +49 -0
  319. node/transport/mdns.py +303 -0
  320. node/transport/netfetch.py +283 -0
  321. node/transport/relay_rung/__init__.py +23 -0
  322. node/transport/relay_rung/cooldown.py +87 -0
  323. node/transport/relay_rung/core.py +138 -0
  324. node/transport/relay_rung/inbound.py +161 -0
  325. node/transport/relay_rung/ledger.py +104 -0
  326. node/transport/relay_rung/listen.py +121 -0
  327. node/transport/relay_rung/outbound.py +181 -0
  328. node/transport/relay_rung/shape.py +71 -0
  329. node/transport/relay_rung/types.py +58 -0
  330. node/transport/relay_rung/wire.py +63 -0
  331. node/transport/router.py +761 -0
  332. node/transport/rung.py +276 -0
  333. node/transport/ssb_rung.py +166 -0
  334. node/transport/ws.py +502 -0
  335. sidecar/fleetmesh_announce.py +338 -0
  336. sidecar/fleetmesh_p2p.py +373 -0
  337. sidecar/fleetmesh_rhio.py +352 -0
  338. sidecar/fleetmesh_ssb.py +603 -0
@@ -0,0 +1,279 @@
1
+ """Unified peering capacity, rate-limiting, and Cashu ecash settlement.
2
+
3
+ Enforces bilateral resource quotas and Cashu NUT-18 / NUT-24 ecash settlement
4
+ across all four protocol boundaries:
5
+ - Nostr relays & paid filters
6
+ - ATProto XRPC & Firehose sync
7
+ - ActivityPub S2S federation & media delivery
8
+ - Secure Scuttlebutt MuxRPC & Blob sync
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import base64
13
+ from dataclasses import dataclass, field
14
+ import json
15
+ import threading
16
+ import time
17
+ from typing import Any, Mapping, Optional, Tuple
18
+
19
+ from fleetmesh_core.accounting import cashu
20
+ from fleetmesh_core.conformance.errors import FleetmeshError
21
+ from node.protocols.atproto.metering import TokenBucket
22
+
23
+
24
+ class GatewaySettlementError(FleetmeshError):
25
+ """Raised on settlement or quota verification failures."""
26
+
27
+
28
+ @dataclass
29
+ class CrossProtocolPeer:
30
+ """A tracked peer communicating across any supported wire protocol."""
31
+ protocol: str
32
+ peer_id: str
33
+ bucket: TokenBucket
34
+ balance_sat: int = 0
35
+ total_consumed_tokens: int = 0
36
+ total_settled_sat: int = 0
37
+ penalty_score: float = 0.0
38
+ created_at: int = field(default_factory=lambda: int(time.time()))
39
+
40
+
41
+ @dataclass
42
+ class PaymentChallenge:
43
+ """Standardized payment challenge issued when a peer exceeds quota."""
44
+ protocol: str
45
+ peer_id: str
46
+ required_sat: int
47
+ mint_url: str
48
+ memo: str
49
+
50
+ def to_http_402(self) -> tuple[int, Mapping[str, str], bytes]:
51
+ """Format as HTTP 402 Payment Required with Cashu challenge header & body."""
52
+ headers = {
53
+ "Content-Type": "application/json",
54
+ "WWW-Authenticate": f'Cashu mint="{self.mint_url}", amount={self.required_sat}, unit="sat"',
55
+ }
56
+ body = json.dumps({
57
+ "error": "PaymentRequired",
58
+ "message": f"Peer quota exceeded on {self.protocol}. Deposit Cashu ecash to continue.",
59
+ "mint": self.mint_url,
60
+ "amount_sat": self.required_sat,
61
+ "unit": "sat",
62
+ "memo": self.memo,
63
+ }, indent=2).encode('utf-8')
64
+ return 402, headers, body
65
+
66
+ def to_nostr_notice(self) -> str:
67
+ """Format as Nostr NOTICE message content."""
68
+ return (
69
+ f"restricted: quota exceeded. Pay {self.required_sat} sats via Cashu to {self.mint_url} "
70
+ f"with memo '{self.memo}'"
71
+ )
72
+
73
+ def to_muxrpc_error(self) -> str:
74
+ """Format as SSB MuxRPC error message."""
75
+ return (
76
+ f"PaymentRequired: quota exceeded for @{self.peer_id}. "
77
+ f"Deposit {self.required_sat} sats at {self.mint_url}"
78
+ )
79
+
80
+
81
+ @dataclass
82
+ class SettlementReceipt:
83
+ """Proof of completed ecash settlement credit."""
84
+ protocol: str
85
+ peer_id: str
86
+ credited_sat: int
87
+ new_balance_sat: int
88
+ proof_count: int
89
+ timestamp: int = field(default_factory=lambda: int(time.time()))
90
+
91
+
92
+ class CrossProtocolSettlementGateway:
93
+ """Unified resource accounting, rate limiting, and ecash settlement gateway."""
94
+
95
+ def __init__(
96
+ self,
97
+ default_capacity: float = 30.0,
98
+ default_refill_per_sec: float = 0.5,
99
+ price_per_topup_sat: int = 21,
100
+ tokens_per_topup: int = 100,
101
+ mint_url: str = "https://mint.example.org",
102
+ max_penalty_score: float = 10.0,
103
+ ):
104
+ self.default_capacity = default_capacity
105
+ self.default_refill_per_sec = default_refill_per_sec
106
+ self.price_per_topup_sat = price_per_topup_sat
107
+ self.tokens_per_topup = tokens_per_topup
108
+ self.mint_url = mint_url.rstrip('/')
109
+ self.max_penalty_score = max_penalty_score
110
+
111
+ self._peers: dict[tuple[str, str], CrossProtocolPeer] = {}
112
+ self._spent_secrets: set[str] = set()
113
+ self._total_challenges_issued = 0
114
+ self._total_settled_sat = 0
115
+ self._lock = threading.RLock()
116
+
117
+ def _peer_key(self, protocol: str, peer_id: str) -> tuple[str, str]:
118
+ return (protocol.lower().strip(), peer_id.strip())
119
+
120
+ def get_or_create_peer(
121
+ self,
122
+ protocol: str,
123
+ peer_id: str,
124
+ capacity: Optional[float] = None,
125
+ refill_rate: Optional[float] = None,
126
+ ) -> CrossProtocolPeer:
127
+ """Fetch or initialize a tracked peer record."""
128
+ key = self._peer_key(protocol, peer_id)
129
+ with self._lock:
130
+ if key not in self._peers:
131
+ cap = capacity if capacity is not None else self.default_capacity
132
+ refill = refill_rate if refill_rate is not None else self.default_refill_per_sec
133
+ bucket = TokenBucket(capacity=cap, refill_rate=refill)
134
+ self._peers[key] = CrossProtocolPeer(
135
+ protocol=key[0],
136
+ peer_id=key[1],
137
+ bucket=bucket,
138
+ )
139
+ return self._peers[key]
140
+
141
+ def record_penalty(self, protocol: str, peer_id: str, penalty: float = 1.0) -> float:
142
+ """Increase penalty score for bad behavior (e.g. invalid signatures, flooding)."""
143
+ with self._lock:
144
+ peer = self.get_or_create_peer(protocol, peer_id)
145
+ peer.penalty_score += penalty
146
+ # Also drain tokens proportional to penalty
147
+ peer.bucket.tokens = max(0.0, peer.bucket.tokens - penalty * 2.0)
148
+ return peer.penalty_score
149
+
150
+ def check_access(
151
+ self,
152
+ protocol: str,
153
+ peer_id: str,
154
+ cost: int = 1,
155
+ ) -> Tuple[bool, Optional[PaymentChallenge]]:
156
+ """Authorize a request or issue a payment challenge.
157
+
158
+ Returns:
159
+ (True, None) if permitted under current token bucket or satoshi balance.
160
+ (False, PaymentChallenge) if dry and requires payment.
161
+ """
162
+ with self._lock:
163
+ peer = self.get_or_create_peer(protocol, peer_id)
164
+
165
+ # 1. Circuit breaker: excessive penalties reject without immediate topup
166
+ if peer.penalty_score >= self.max_penalty_score:
167
+ challenge = PaymentChallenge(
168
+ protocol=protocol,
169
+ peer_id=peer_id,
170
+ required_sat=self.price_per_topup_sat * 5,
171
+ mint_url=self.mint_url,
172
+ memo=f"penalty-recovery-{peer_id[:16]}",
173
+ )
174
+ self._total_challenges_issued += 1
175
+ return False, challenge
176
+
177
+ # 2. Try consuming from rate-limiter token bucket
178
+ if peer.bucket.consume(float(cost)):
179
+ peer.total_consumed_tokens += cost
180
+ return True, None
181
+
182
+ # 3. If bucket dry, attempt to convert prepaid satoshi balance to tokens
183
+ if peer.balance_sat >= self.price_per_topup_sat:
184
+ peer.balance_sat -= self.price_per_topup_sat
185
+ peer.bucket.tokens += self.tokens_per_topup
186
+ peer.bucket.consume(float(cost))
187
+ peer.total_consumed_tokens += cost
188
+ return True, None
189
+
190
+ # 4. Quota exceeded: Issue payment challenge
191
+ self._total_challenges_issued += 1
192
+ challenge = PaymentChallenge(
193
+ protocol=protocol,
194
+ peer_id=peer_id,
195
+ required_sat=self.price_per_topup_sat,
196
+ mint_url=self.mint_url,
197
+ memo=f"quota-{protocol}-{peer_id[:16]}",
198
+ )
199
+ return False, challenge
200
+
201
+ def settle_cashu(self, protocol: str, peer_id: str, token_str: str) -> SettlementReceipt:
202
+ """Parse, validate, and credit Cashu ecash payment (V3 cashuA or V4 cashuB)."""
203
+ raw_token = token_str.strip()
204
+ proofs: list[Mapping[str, Any]] = []
205
+
206
+ if raw_token.startswith(cashu.TOKEN_V4_PREFIX):
207
+ try:
208
+ tok_v4 = cashu.decode_token_v4(raw_token)
209
+ proofs = list(tok_v4.get('proofs', []))
210
+ except Exception as e:
211
+ raise GatewaySettlementError(f"Invalid Cashu V4 token: {e}") from e
212
+ elif raw_token.startswith('cashuA'):
213
+ try:
214
+ payload = raw_token[6:]
215
+ # Pad base64 if needed
216
+ payload += '=' * (-len(payload) % 4)
217
+ data = json.loads(base64.urlsafe_b64decode(payload).decode('utf-8'))
218
+ token_entries = data.get('token', [])
219
+ for entry in token_entries:
220
+ proofs.extend(entry.get('proofs', []))
221
+ except Exception as e:
222
+ raise GatewaySettlementError(f"Invalid Cashu V3 token: {e}") from e
223
+ else:
224
+ raise GatewaySettlementError("Unrecognized Cashu token prefix (expected cashuA or cashuB)")
225
+
226
+ proof_count = len(proofs)
227
+ sat_amount = sum(int(p.get('amount', 0)) for p in proofs)
228
+
229
+ if sat_amount <= 0 or proof_count == 0:
230
+ raise GatewaySettlementError("Token contains 0 sats or empty proofs")
231
+
232
+ with self._lock:
233
+ # Double-spend prevention across tokens
234
+ for p in proofs:
235
+ sec = p.get('secret')
236
+ if sec and sec in self._spent_secrets:
237
+ raise GatewaySettlementError(f"Double-spend attempt: proof secret already redeemed ({sec})")
238
+
239
+ for p in proofs:
240
+ sec = p.get('secret')
241
+ if sec:
242
+ self._spent_secrets.add(sec)
243
+
244
+ peer = self.get_or_create_peer(protocol, peer_id)
245
+
246
+ # Credit balance
247
+ peer.balance_sat += sat_amount
248
+ peer.total_settled_sat += sat_amount
249
+ self._total_settled_sat += sat_amount
250
+
251
+ # Refill tokens proportional to satoshis
252
+ granted_tokens = (sat_amount // self.price_per_topup_sat) * self.tokens_per_topup
253
+ if granted_tokens > 0:
254
+ peer.bucket.tokens = min(peer.bucket.capacity * 2, peer.bucket.tokens + granted_tokens)
255
+
256
+ # Decay penalty score on successful payment
257
+ peer.penalty_score = max(0.0, peer.penalty_score - 2.0)
258
+
259
+ return SettlementReceipt(
260
+ protocol=protocol,
261
+ peer_id=peer_id,
262
+ credited_sat=sat_amount,
263
+ new_balance_sat=peer.balance_sat,
264
+ proof_count=proof_count,
265
+ )
266
+
267
+ def stats(self) -> Mapping[str, Any]:
268
+ """Aggregate cross-protocol accounting statistics."""
269
+ with self._lock:
270
+ by_proto: dict[str, int] = {}
271
+ for (proto, _), _ in self._peers.items():
272
+ by_proto[proto] = by_proto.get(proto, 0) + 1
273
+
274
+ return {
275
+ "total_peers": len(self._peers),
276
+ "peers_by_protocol": by_proto,
277
+ "total_challenges_issued": self._total_challenges_issued,
278
+ "total_settled_sat": self._total_settled_sat,
279
+ }