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,100 @@
1
+ """The constants of budget/1.0 and the metering primitives: what one message
2
+ costs and which side owes it. Separate because it holds no node state - every
3
+ other module in the package charges through these and the tests read them
4
+ directly.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, Mapping
9
+
10
+ from fleetmesh_core.accounting import accounting, window
11
+ from fleetmesh_core.storage import negentropy, sync
12
+
13
+ # Lists that grew for as long as the node ran. Findings feed the reporter and
14
+ # the tests read them, so they stay lists, trimmed on the tick.
15
+ KEEP_FINDINGS: int = 1000
16
+
17
+ PIURI: str = 'https://fleetmesh.org/budget/1.0'
18
+ AMEND: str = 'https://fleetmesh.org/peering/1.0/amend'
19
+ SYNC: str = sync.PIURI
20
+ ZERO: str = accounting.ZERO
21
+ C: Mapping[str, Any] = window.C['grant']
22
+ TOLERANCE: float = C['divergence_tolerance']
23
+
24
+
25
+ class Meter:
26
+ def __init__(self) -> None:
27
+ self.c: dict[str, int] = dict.fromkeys(sync.COUNTERS, 0)
28
+
29
+ def add(self, name: str, n: int = 1) -> None:
30
+ self.c[name] = self.c.get(name, 0) + n
31
+
32
+ def snapshot(self) -> dict[str, int]:
33
+ return {k: v for k, v in self.c.items() if v}
34
+
35
+
36
+ def _ranges(frame_hex: str) -> int:
37
+ try:
38
+ return len(negentropy.decode_message(bytes.fromhex(frame_hex)))
39
+ except Exception:
40
+ return 0
41
+
42
+
43
+ METERED: tuple[str, ...] = ('id', 'type', 'thid', 'body', 'attachments')
44
+
45
+
46
+ def metered_bytes(message: Mapping[str, Any]) -> int:
47
+ """What both sides charge for one message: the parts the protocol wrote.
48
+ Addressing and created_time are added by the transport on the way out and
49
+ would make the sender's count differ from the receiver's."""
50
+ return sync.wire_bytes({k: v for k, v in message.items() if k in METERED})
51
+
52
+
53
+ def _observe(session_meter: Meter, deliverer_meter: Meter, message: Mapping[str, Any],
54
+ from_initiator: bool) -> None:
55
+ """Charge one sync/1.0 message: the session to its initiator, the events
56
+ to whoever delivered them.
57
+
58
+ Two different things cross in a sync session and they are owed to
59
+ different sides. Opening it, asking, reconciling, and the bytes both ways
60
+ are the SESSION, and a session is paid for by whoever opened it, under
61
+ the grant the other side issued - sync_wire's docstring says so and the
62
+ reference Session binds each side to exactly that grant. Events are
63
+ different: an event is pushed into the receiver by whoever sent it,
64
+ whichever side opened the session, and `events_in` is the receiver's
65
+ limit on exactly that. The receiver prices duplicates against the
66
+ deliverer for the same reason, and `observe_session` already does.
67
+
68
+ This used to charge whoever SENT each message and then mirror the bytes
69
+ onto the other side's meter. On a symmetric peering the phantom that
70
+ produced appeared identically in the issuer's measurement and the
71
+ subject's claim - both sides run this code - so the two errors cancelled
72
+ and nobody saw it for a week. The first asymmetric peering exposed it:
73
+ New York pulled from a newcomer that never pulled back, booked the bytes
74
+ it RECEIVED as the newcomer's consumption of New York's grant, and when
75
+ the newcomer sent no receipt - it had delivered nothing into a converged
76
+ shard and had nothing to claim - priced that as active silence. Fifty
77
+ windows of S2 against a stranger's node on its first contact with the
78
+ mesh, for answering.
79
+ """
80
+ wb = metered_bytes(message)
81
+ kind = message.get('type', '').rsplit('/', 1)[1]
82
+ body = message.get('body', {})
83
+ # The session, to its initiator. Direction is relative to the responder,
84
+ # because that is what the grant's limits name.
85
+ session_meter.add('bytes_in' if from_initiator else 'bytes_out', wb)
86
+ if from_initiator:
87
+ if kind == 'open':
88
+ session_meter.add('conn_open')
89
+ session_meter.add('req')
90
+ session_meter.add('sync_ranges', _ranges(body.get('initial_frame', '')))
91
+ elif kind == 'need':
92
+ session_meter.add('req')
93
+ elif kind == 'range':
94
+ session_meter.add('sync_ranges', _ranges(body.get('frame', '')))
95
+ elif kind == 'range':
96
+ # A reconciliation's frames go both ways and are one cost.
97
+ session_meter.add('sync_ranges', _ranges(body.get('frame', '')))
98
+ # The events, to whoever pushed them.
99
+ if kind == 'deliver':
100
+ deliverer_meter.add('events_in', sync.delivered(dict(message)))
@@ -0,0 +1,439 @@
1
+ """The receipt chain, both ends: this node as subject writing receipts and
2
+ the farewell, and as issuer reading receipts, closes and acks off the wire.
3
+ Separate because it is the whole of what budget/1.0 puts on the wire; the
4
+ pricing of a window is windows.py.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import secrets
9
+ from typing import Any, Mapping, Optional
10
+
11
+ from fleetmesh_core.accounting import accounting, window
12
+ from fleetmesh_core.wire.interfaces import ReceiptBody
13
+ from fleetmesh_core.wire.wire_types import BudgetBody, BudgetCloseBody, BudgetReceiptAckBody, Message, ReceiptAckStatus
14
+ from fleetmesh_core.wire import validate
15
+ from fleetmesh_core.identity.did import did_nostr
16
+ from node.storage import accounts as accounts_mod, grants as grants_mod
17
+ from .chain import OutChain
18
+ from .meter import C, PIURI, ZERO
19
+ from .shape import BudgetShape
20
+
21
+
22
+ class ReceiptsMixin:
23
+ # ---- subject: receipts out -------------------------------------------------
24
+ def _msg(self: BudgetShape, kind: str, body: BudgetBody, peer: str, piuri: str = PIURI) -> Message[BudgetBody]:
25
+ msg: Message[BudgetBody] = {
26
+ 'id': 'budget-%s-%s' % (kind, secrets.token_hex(4)),
27
+ 'type': '%s/%s' % (piuri, kind),
28
+ 'from': self.ks.did,
29
+ 'to': [did_nostr(peer)],
30
+ 'body': body,
31
+ }
32
+ return msg
33
+
34
+ def receipt_due(self: BudgetShape, peer: str, now: int) -> bool:
35
+ row = self.grants.get(peer, grants_mod.RECEIVED)
36
+ if row is None or row['status'] != 'live':
37
+ return False
38
+ cadence = (row['payload'].get('receipt') or [600])[0]
39
+ epoch = self.epoch(peer, grants_mod.RECEIVED, now)
40
+ state = self.chain_out.get(peer, {}).get(epoch)
41
+ claim = self._meter(self.usage_of_me, peer, epoch).snapshot()
42
+ if not claim:
43
+ return False
44
+ if state is None:
45
+ return True
46
+ return state.sent_at is not None and (now - state.sent_at >= cadence) and (claim != state.last)
47
+
48
+ def send_receipt(
49
+ self: BudgetShape,
50
+ peer: str,
51
+ now: Optional[int] = None,
52
+ final: bool = False,
53
+ epoch: Optional[int] = None,
54
+ ) -> tuple[ReceiptBody, str]:
55
+ now_val = now if now is not None else self.clock()
56
+ row = self.grants.get(peer, grants_mod.RECEIVED)
57
+ assert row is not None
58
+ epoch_val = self.epoch(peer, grants_mod.RECEIVED, now_val) if epoch is None else epoch
59
+ # The grant a receipt names is the one that governed its window. The
60
+ # chain is per window and is never restarted because the grant changed:
61
+ # that restart, met by an issuer expecting seq N, was an S2 misreport
62
+ # filed against an honest node every time a window was amended.
63
+ governing = self.governing(peer, grants_mod.RECEIVED, epoch_val) or row
64
+ with self._lock:
65
+ state = self.chain_out.setdefault(peer, {}).setdefault(epoch_val, OutChain(grant=governing['envelope_id']))
66
+ state.grant = governing['envelope_id']
67
+ counters = self._meter(self.usage_of_me, peer, epoch_val).snapshot()
68
+ body = accounting.sign_receipt(
69
+ ReceiptBody(grant=governing['envelope_id'], window=epoch_val, seq=state.seq,
70
+ prev=state.prev, counters=counters, at=now_val), self.ks.node_sk)
71
+ head = accounting.link(body)
72
+ # The heads this node produced, in sequence order. An ack carries
73
+ # the head the receiver holds, and without our own list that number
74
+ # is unreadable - it has been arriving on every ack since the
75
+ # protocol was written and being thrown away. With the list it says
76
+ # exactly how many receipts behind the receiver is.
77
+ state.seq, state.prev, state.sent_at, state.last = state.seq + 1, head, now_val, counters
78
+ state.heads = state.heads + [head]
79
+ if self.accounts is not None:
80
+ # Before the message leaves, not after: a receipt this node has
81
+ # sent and does not remember sending is the whole bug.
82
+ self.accounts.put_chain(peer, epoch_val, state.to_dict())
83
+ self.accounts.put_meter(peer, epoch_val, accounts_mod.OF_ME, counters, now_val)
84
+ # A receipt is a live wrap, and the attempt to make it store-and-forward
85
+ # is written down here because the reason it failed is not in the code.
86
+ #
87
+ # The case for storing it stands: a receipt lost while a subscription is
88
+ # down is priced as active silence, S2, unpublishable, and on
89
+ # 2026-09-05 that took a peering apart in both directions for eleven
90
+ # windows. So it was moved to kind 1059 on 2026-09-06, like the close
91
+ # that follows it. Seventeen hours later every peering in the mesh was
92
+ # pricing every other S2 every window: `relay.pocketstr.com` ACCEPTS a
93
+ # kind 1059 - the publish returns OK and no drop is recorded - and does
94
+ # not deliver it to a live subscription. A probe put both kinds through
95
+ # the same relay to the same subscription; the 21059 arrived and the
96
+ # 1059 did not.
97
+ #
98
+ # So the sender cannot tell accepted from delivered, and the whole
99
+ # store-and-forward binding is only as real as the relays under it.
100
+ # Anything this protocol sends stored - a grant amendment, a complaint
101
+ # notice, a window close, a withdrawal - has the same hole on this
102
+ # deployment, and had it before this change: moving receipts onto that
103
+ # path is what made it visible. Fixing it properly is choosing relays
104
+ # that serve 1059, or re-sending on an ack that never comes, and it is
105
+ # not something to decide while the mesh cannot account.
106
+ if final:
107
+ close = BudgetCloseBody(grant=governing['envelope_id'], window=epoch_val, final_receipt_head=head, counters=counters)
108
+ sent = self.wire_client.send(peer, self._msg('receipt', body, peer))
109
+ self.wire_client.send(peer, self._msg('close', close, peer), stored=True)
110
+ else:
111
+ sent = self.wire_client.send(peer, self._msg('receipt', body, peer))
112
+ # The publish result was thrown away. It does not prove delivery - a
113
+ # relay that accepts a message and never serves it returns exactly this
114
+ # - but a receipt NO relay would take has certainly not arrived, and
115
+ # that was invisible too. Cheap, and it is the half of the question the
116
+ # sender can answer on its own.
117
+ if sent and not any(ok for ok, _note in sent.values()):
118
+ self.unpublished.append((peer, epoch_val, body['seq'], now_val))
119
+ self.refusals.refuse('receipt-out', 'no relay accepted the receipt', subject=peer, at=now_val,
120
+ detail={'window': epoch_val, 'seq': body['seq'],
121
+ 'relays': {u: n for u, (_ok, n) in sent.items()}})
122
+ return body, head
123
+
124
+ def _ack_lag(self: BudgetShape, peer: str, body: Mapping[str, Any]) -> int:
125
+ """How many receipts behind the receiver is, from the ack it just sent.
126
+
127
+ Every ack carries the head of the chain the receiver holds. This node
128
+ holds the head of every receipt it produced, in order, so the two
129
+ together say exactly where the receiver has got to - and that number
130
+ has been on the wire since the protocol was written, unread. A relay
131
+ that accepts a receipt and never serves it looks identical to a
132
+ delivered one from the publish result; it does not look identical
133
+ here.
134
+
135
+ Zero when the receiver is level, or when the head is one this node
136
+ cannot place - a fresh window, a peer that restarted, an ack for a
137
+ window already forgotten. Not knowing is not the same as being behind,
138
+ and reporting a lag this node cannot substantiate is the mistake every
139
+ other diagnosis here has had to be corrected for. Caller holds the lock.
140
+ """
141
+ head = body.get('head')
142
+ if not isinstance(head, str):
143
+ return 0
144
+ for epoch in sorted(self.chain_out.get(peer, {}), reverse=True):
145
+ heads = self.chain_out[peer][epoch].heads
146
+ if not heads:
147
+ continue
148
+ if head not in heads:
149
+ continue
150
+ idx = heads.index(head)
151
+ furthest = self.ack_head.get(peer)
152
+ if furthest is not None and furthest[0] == epoch and furthest[1] > idx:
153
+ # An ack for an older head arriving after one for a newer:
154
+ # two receipts sent in the same second come back over
155
+ # different relays in either order. The peer has not gone
156
+ # backwards; this is the later-sent ack arriving late.
157
+ return 0
158
+ self.ack_head[peer] = (epoch, idx)
159
+ behind = len(heads) - 1 - idx
160
+ if behind:
161
+ self.ack_lag[peer] = (epoch, behind, self.clock())
162
+ else:
163
+ self.ack_lag.pop(peer, None)
164
+ return behind
165
+ return 0
166
+
167
+ def farewell(self: BudgetShape, now: Optional[int] = None) -> list[tuple[Any, ...]]:
168
+ """Say what this node used, to every peer, before it stops.
169
+
170
+ A node that vanished mid-window left its issuer with traffic measured
171
+ and no receipt, which is active silence: an S2 floor, and S2 drops the
172
+ tier to probation. A tier is only ever raised by a person, so the
173
+ upgrade this specification REQUIRES for a digest change - roll every
174
+ node in one pass or they decline to peer - cost every peering its seeded
175
+ tier, every time. Measured on the test mesh: one fleet restart, an S2 at
176
+ every peering, every streak back to zero, and an operator-seeded trusted
177
+ grant pending demotion.
178
+
179
+ It is the third layer at which an operator restart was being read as
180
+ dishonesty, after the session abort and the standing streak. The answer
181
+ is the same as it was there: say something rather than vanish. A receipt
182
+ for the open window is a claim, and an honest node's window then prices
183
+ on its own numbers.
184
+
185
+ Nothing here waits on an answer and nothing here may raise: a node that
186
+ could not say goodbye must still stop.
187
+ """
188
+ now_val = now if now is not None else self.clock()
189
+ did: list[tuple[Any, ...]] = []
190
+ for peer in self._peers():
191
+ try:
192
+ row = self.grants.get(peer, grants_mod.RECEIVED)
193
+ if row is None or row['status'] != 'live':
194
+ continue
195
+ epoch = self.epoch(peer, grants_mod.RECEIVED, now_val)
196
+ if not self._meter(self.usage_of_me, peer, epoch).snapshot():
197
+ continue # nothing used this window; silence is honest
198
+ body, _head = self.send_receipt(peer, now_val)
199
+ did.append(('farewell receipt', peer, epoch, body['seq']))
200
+ except Exception as e:
201
+ did.append(('farewell failed', peer, '%s: %s' % (type(e).__name__, e)))
202
+ return did
203
+
204
+ def final_due(self: BudgetShape, peer: str, now: int) -> Optional[int]:
205
+ """The previous window closed and my last word on it is stale: say the
206
+ final word while the grace period is open. Traffic after the last
207
+ receipt is priced by divergence, so a node that syncs near the end of
208
+ a window and then only ticks in the next one would look like a liar."""
209
+ row = self.grants.get(peer, grants_mod.RECEIVED)
210
+ if row is None or row['status'] != 'live':
211
+ return None
212
+ w = row['payload'].get('window') or C['window_seconds_default']
213
+ prev = accounting.epoch(now, w) - 1
214
+ if now > accounting.window_bounds(prev, w)[1] + accounting.window_grace(w):
215
+ return None
216
+ meter = self.usage_of_me.get(peer, {}).get(prev)
217
+ if meter is None or not meter.snapshot():
218
+ return None
219
+ state = self.chain_out.get(peer, {}).get(prev)
220
+ if state is not None and (state.final or state.last == meter.snapshot()):
221
+ return None
222
+ return prev
223
+
224
+ # ---- issuer: receipts in ---------------------------------------------------
225
+ def on_receipt(self: BudgetShape, peer: str, body: Any, now: Optional[int] = None) -> Message[BudgetReceiptAckBody]:
226
+ now_val = now if now is not None else self.clock()
227
+ why = validate.check_body('budget', 'receipt', body)
228
+ if why is not None:
229
+ # Not a claim, not an offence: a receipt this node cannot read is
230
+ # answered so the subject learns at once, and counted, and that is all.
231
+ #
232
+ # The seq is echoed like every other refusal branch echoes it. This
233
+ # one answered 0 whatever it was sent, so a subject at seq 5 heard
234
+ # that seq 0 was divergent and could not tell which receipt the
235
+ # answer was about - an answer that identifies nothing, in the one
236
+ # branch whose whole purpose is to tell the subject at once. The
237
+ # ack body is {seq, head, status} and says nothing about why, so
238
+ # the reason lives in the refusal channel, where `meshnode
239
+ # refusals` reads it. A body that is not a mapping has no seq to
240
+ # echo and that is the one case that still answers 0.
241
+ self.refusals.refuse('receipt', why, subject=peer, at=now_val)
242
+ seq = body.get('seq') if isinstance(body, dict) else None
243
+ return self._msg('receipt-ack', BudgetReceiptAckBody(
244
+ seq=seq if isinstance(seq, int) and not isinstance(seq, bool) else 0,
245
+ head=ZERO, status='divergent'), peer)
246
+ row = self.grants.get(peer, grants_mod.ISSUED)
247
+ epoch = body['window']
248
+ # A receipt is a claim about a window this grant could be about. One
249
+ # naming a window that will never close is never evaluated and never
250
+ # forgotten - forget_before only drops what is older than the horizon -
251
+ # so a peer naming a fresh future window per message takes as much of
252
+ # this node's memory and disk as it cares to. One naming a window long
253
+ # past is answerable by nothing either. Neither is an offence: it is a
254
+ # claim nobody can be held to, so it is refused the way an unreadable
255
+ # receipt is, counted and not stored.
256
+ #
257
+ # The upper bound is the CURRENT window, not the next one. A receipt
258
+ # about a window that has not opened yet claims usage that has not
259
+ # happened, and the one extra epoch that used to be accepted was a
260
+ # whole window of storage a peer could fill ahead of time; clock skew
261
+ # is already absorbed by the fact that a receipt about the window that
262
+ # just closed remains acceptable for KEEP_WINDOWS more.
263
+ current = self.epoch(peer, grants_mod.ISSUED, now_val)
264
+ oldest = current - self.KEEP_WINDOWS
265
+ if not isinstance(epoch, int) or isinstance(epoch, bool) or not (oldest <= epoch <= current):
266
+ self.refusals.refuse('receipt', 'names a window this grant cannot be about',
267
+ subject=peer, at=now_val,
268
+ detail={'window': epoch, 'acceptable': [oldest, current]})
269
+ return self._msg('receipt-ack', BudgetReceiptAckBody(seq=body.get('seq', 0), head=ZERO, status='divergent'), peer)
270
+ # The one divergent branch that recorded no reason, and the one that
271
+ # fired. Every receipt on a four-node mesh was refused here for an hour
272
+ # while budget_rejected stayed empty, so an operator saw an S2 a window
273
+ # with no claim earns and nothing at all about why the claim was
274
+ # refused. A refusal that is not counted is indistinguishable from
275
+ # silence, which is the thing this protocol is least able to afford.
276
+ if row is None:
277
+ self.refusals.refuse('receipt', 'no grant issued to this peer', subject=peer, at=now_val)
278
+ return self._msg('receipt-ack', BudgetReceiptAckBody(seq=body['seq'], head=ZERO, status='divergent'), peer)
279
+ acceptable = self._acceptable_grants(peer, epoch)
280
+ if body['grant'] not in acceptable:
281
+ self.refusals.refuse('receipt', 'names a grant that did not govern that window',
282
+ subject=peer, at=now_val,
283
+ detail={'window': epoch, 'named': body['grant'][:16],
284
+ 'acceptable': [g[:16] for g in sorted(acceptable)]})
285
+ return self._msg('receipt-ack', BudgetReceiptAckBody(seq=body['seq'], head=ZERO, status='divergent'), peer)
286
+ limits = (self.governing(peer, grants_mod.ISSUED, epoch) or row)['payload']['limits']
287
+ with self._lock:
288
+ chain = self.receipts_in.setdefault(peer, {}).setdefault(epoch, [])
289
+ # The chain expects the next sequence number, not the length it
290
+ # happens to have: once a gap is tolerated those stop being the
291
+ # same number, and reading the expectation off the length would
292
+ # refuse every receipt after the first gap for the rest of the
293
+ # window - the failure this is here to end.
294
+ expected_seq = chain[-1]['seq'] + 1 if chain else 0
295
+ expected_prev = accounting.link(chain[-1]) if chain else ZERO
296
+ problems: list[str] = []
297
+ # A receipt's `at` is the subject's clock. constants.json states a
298
+ # tolerance of max_clock_skew_seconds and nothing in this project
299
+ # read it, while the specification says drift beyond it produces
300
+ # *unearned* divergence - a wrong answer from honest nodes, whose
301
+ # remedy is NTP rather than the ladder. So this is a diagnosis and
302
+ # not a finding: pricing a bad clock as dishonesty is the mistake
303
+ # two other things here have already had to be corrected for. It is
304
+ # recorded so an operator staring at an unexplained divergence has
305
+ # somewhere to look.
306
+ at = int(body.get('at', now_val))
307
+ drift = abs(at - now_val)
308
+ # A receipt dated before this node started is one a relay held
309
+ # while it was down - the peers' farewells, replayed on the
310
+ # resubscription - and is late, not drifted; nothing here can tell
311
+ # the two apart, so it is not written down as either. Dated after
312
+ # this clock, or late by more than this node has been up, it is
313
+ # a clock and nothing else.
314
+ if drift > C['max_clock_skew_seconds'] and (at > now_val or at >= self.started):
315
+ self.clock_drift.append((peer, epoch, drift, now_val))
316
+ if self.strain is not None:
317
+ self.strain.record('drift', peer=peer)
318
+ if not accounting.verify_receipt(body, peer):
319
+ # An unsigned receipt is not a claim anyone can be held to, so it
320
+ # is not a claim. Saying so at once is the only way the subject
321
+ # learns before the window closes on it.
322
+ problems.append('no valid subject signature over the chain link')
323
+ # A gap is the wire, and a rewrite is the subject.
324
+ #
325
+ # This required seq == len(chain) and refused anything else, so one
326
+ # receipt lost in transit cost the whole window: the subject went
327
+ # on incrementing, every later receipt was refused as a chain
328
+ # break, and the window closed on a claim missing everything after
329
+ # the hole. It only ever cleared at the boundary, because the chain
330
+ # is keyed per epoch. Seventeen hours of a four-node mesh were
331
+ # priced that way, against peers that had sent every receipt.
332
+ #
333
+ # What the strict form bought was the ability to say a subject had
334
+ # OMITTED a receipt on purpose, as distinct from the wire dropping
335
+ # one. That distinction was never really available - the transport
336
+ # demonstrably drops them - and it was being resolved against the
337
+ # subject every time. What it must not give up is the guarantee
338
+ # that a subject cannot understate, and that survives whole:
339
+ # counters are cumulative and monotonic, a counter that goes
340
+ # backwards is still refused, and the most recent receipt alone
341
+ # bounds the claim. A skipped middle receipt therefore buys the
342
+ # subject nothing, and a missing tail is already priced as the
343
+ # under-claim it is.
344
+ #
345
+ # Behind the expectation is a different thing and stays a fault: a
346
+ # receipt at a sequence already held is a rewrite of what the
347
+ # subject signed, which is exactly what the chain exists to refuse.
348
+ seq = body.get('seq')
349
+ gap = 0
350
+ if not isinstance(seq, int) or isinstance(seq, bool) or seq < expected_seq:
351
+ problems.append('seq %s, expected %d or later' % (seq, expected_seq))
352
+ elif seq > expected_seq:
353
+ gap = seq - expected_seq
354
+ if not gap and body.get('prev') != expected_prev:
355
+ # Only checkable with no gap: the links the wire lost are the
356
+ # ones that would have joined this receipt to the last one held.
357
+ problems.append('prev does not chain')
358
+ if chain:
359
+ for k, v in body.get('counters', {}).items():
360
+ if v < chain[-1]['counters'].get(k, 0):
361
+ problems.append('counter %s went backwards' % k)
362
+ if problems:
363
+ self.findings.append((peer, epoch, 'S2', 'receipt chain: %s' % '; '.join(problems)))
364
+ head = expected_prev
365
+ status: ReceiptAckStatus = 'divergent'
366
+ else:
367
+ if gap:
368
+ # A diagnosis and not a finding, recorded the way clock
369
+ # drift is: an operator reading an unexplained under-claim
370
+ # has somewhere to look, and nothing crosses the wire.
371
+ self.receipt_gaps.append((peer, epoch, expected_seq, seq, now_val))
372
+ chain.append(body)
373
+ head = accounting.link(body)
374
+ self.heads_in.setdefault(peer, {})[epoch] = head
375
+ if self.accounts is not None:
376
+ self.accounts.put_receipt(peer, epoch, body)
377
+ # A receipt for a window this node has already priced. The
378
+ # window is not reopened here - that writes grants and fires
379
+ # the reporter, and this runs inside an inbound handler under
380
+ # the lock - it is queued for the tick, where every other
381
+ # evaluation happens.
382
+ if (peer, epoch) in self.evaluated:
383
+ self.reopen.add((peer, epoch))
384
+ measured = self._meter(self.usage_by, peer, epoch).snapshot()
385
+ ev = window.evaluate_window(limits, measured, body['counters'])
386
+ # evaluate_window already classifies each limit; this ack is
387
+ # about MISREPORT only, so a limit counts as divergent when
388
+ # its class is worse than the overrun alone would have earned.
389
+ # The hand-rolled `divergence > TOLERANCE` that stood here was
390
+ # a second copy of classify()'s threshold, and ignored the
391
+ # sample floor evaluate_window applies, so a receipt could be
392
+ # acked divergent for a window the tick then scored S0.
393
+ divergent = any(d['severity'] != window.classify(d['overrun'], 0.0)
394
+ for d in ev['per_limit'].values())
395
+ status = 'divergent' if divergent else 'accepted'
396
+ return self._msg('receipt-ack', BudgetReceiptAckBody(seq=body.get('seq', 0), head=head, status=status), peer)
397
+
398
+ def on_close(self: BudgetShape, peer: str, body: Any, now: Optional[int] = None) -> Message[BudgetReceiptAckBody]:
399
+ now_val = now if now is not None else self.clock()
400
+ why = validate.check_body('budget', 'close', body)
401
+ if why is not None:
402
+ self.refusals.refuse('close', why, subject=peer, at=now_val)
403
+ return self._msg('receipt-ack', BudgetReceiptAckBody(seq=0, head=ZERO, status='divergent'), peer)
404
+ epoch = body['window']
405
+ w = self._window_of(peer, grants_mod.ISSUED)
406
+ head = self.heads_in.get(peer, {}).get(epoch, ZERO)
407
+ final = body.get('final_receipt_head') == head and accounting.close_is_final(
408
+ now_val, accounting.window_bounds(epoch, w)[1], w)
409
+ return self._msg('receipt-ack', BudgetReceiptAckBody(
410
+ seq=len(self.receipts_in.get(peer, {}).get(epoch, [])),
411
+ head=head, status='final' if final else 'accepted'), peer)
412
+
413
+ def handle_receipt(self: BudgetShape, inbound: Any, sender: str) -> tuple[Any, ...]:
414
+ msg = getattr(inbound, 'message', inbound)
415
+ body = msg.get('body')
416
+ reply = self.on_receipt(sender, body)
417
+ self.wire_client.reply(inbound, reply, sender=sender)
418
+ return ('acked-out', sender, reply['body']['status'])
419
+
420
+ def handle_close(self: BudgetShape, inbound: Any, sender: str) -> tuple[Any, ...]:
421
+ msg = getattr(inbound, 'message', inbound)
422
+ body = msg.get('body')
423
+ reply = self.on_close(sender, body)
424
+ self.wire_client.reply(inbound, reply, sender=sender)
425
+ return ('acked-out', sender, reply['body']['status'])
426
+
427
+ def handle_receipt_ack(self: BudgetShape, inbound: Any, sender: str) -> tuple[Any, ...]:
428
+ msg = getattr(inbound, 'message', inbound)
429
+ body = msg.get('body')
430
+ why = validate.check_body('budget', 'receipt-ack', body)
431
+ if why is not None:
432
+ self.refusals.refuse('receipt-ack', why, subject=sender)
433
+ return ('ignored', sender, 'malformed receipt-ack: ' + why)
434
+ with self._lock:
435
+ self.acks[sender] = body
436
+ behind = self._ack_lag(sender, body)
437
+ if behind:
438
+ return ('acked', sender, '%s, %d receipt(s) behind' % (body['status'], behind))
439
+ return ('acked', sender, body['status'])
@@ -0,0 +1,101 @@
1
+ """The shape of a Budget as its mixins see it.
2
+
3
+ The one class is split across receipts.py, windows.py, tick.py and core.py by
4
+ concern, but every method reads instance state set in Budget.__init__ and
5
+ calls methods that live on another of those modules. Each mixin method
6
+ annotates `self` as this Protocol so the checker knows what is on it without
7
+ the mixins inheriting from one another, stubbing each other's methods, or
8
+ falling back to Any. It is a declaration only; Budget satisfies it
9
+ structurally and nothing here runs.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import threading
14
+ from typing import Any, Callable, Mapping, Optional, Protocol
15
+
16
+ from fleetmesh_core.accounting import standing
17
+ from fleetmesh_core.wire.interfaces import ReceiptBody
18
+ from fleetmesh_core.wire.wire_types import BudgetBody, BudgetReceiptAckBody, GoverningRow, Message
19
+ from node.compartment import Refusals, stores
20
+ from node.storage import grants as grants_mod
21
+ from .chain import OutChain
22
+ from .evaluation import Evaluation, Repricing
23
+ from .meter import Meter
24
+
25
+
26
+ class BudgetShape(Protocol):
27
+ # class attributes on Budget and Compartment
28
+ KEEP_WINDOWS: int
29
+ TROUBLE_WINDOW: int
30
+ # set in Compartment.__init__
31
+ refusals: Refusals
32
+ # set in WireProtocol.__init__
33
+ wire_client: Any
34
+ # set in Budget.__init__, in that order
35
+ strain: Optional[Any]
36
+ accounts: Optional[Any]
37
+ ks: Any
38
+ transport: Any
39
+ grants: grants_mod.GrantStore
40
+ peering: Any
41
+ clock: Callable[[], int]
42
+ ledger: standing.Ledger
43
+ _lock: threading.RLock
44
+ usage_by: dict[str, dict[int, Meter]]
45
+ usage_of_me: dict[str, dict[int, Meter]]
46
+ receipts_in: dict[str, dict[int, list[ReceiptBody]]]
47
+ heads_in: dict[str, dict[int, str]]
48
+ chain_out: dict[str, dict[int, OutChain]]
49
+ acks: dict[str, Mapping[str, Any]]
50
+ evaluated: set[tuple[str, int]]
51
+ evaluations: stores.Ring[Evaluation]
52
+ findings: stores.Ring[tuple[Any, ...]]
53
+ zero_until: dict[str, int]
54
+ idle: dict[str, int]
55
+ clock_drift: stores.Ring[tuple[Any, ...]]
56
+ started: int
57
+ receipt_gaps: stores.Ring[tuple[Any, ...]]
58
+ ack_lag: dict[str, tuple[int, int, int]]
59
+ ack_head: dict[str, tuple[int, int]]
60
+ unpublished: stores.Ring[tuple[Any, ...]]
61
+ on_evaluated: list[Callable[[Evaluation], Any]]
62
+ reopen: set[tuple[str, int]]
63
+ repriced: list[Repricing]
64
+ on_repriced: list[Callable[[str, int, str, str], Any]]
65
+ # set by the tick, the first time it forgets
66
+ _forgotten_to: int
67
+
68
+ # core.py
69
+ def _save_meters(self, peer: Optional[str] = None, epoch: Optional[int] = None) -> None: ...
70
+ def _window_of(self, peer: str, direction: grants_mod.GrantDirection) -> int: ...
71
+ def _peers(self) -> list[str]: ...
72
+ def governing(self, peer: str, direction: grants_mod.GrantDirection, epoch: int) -> Optional[GoverningRow]: ...
73
+ def _acceptable_grants(self, peer: str, epoch: int) -> set[str]: ...
74
+ def epoch(self, peer: str, direction: grants_mod.GrantDirection, at: Optional[int] = None) -> int: ...
75
+ def _meter(self, table: dict[str, dict[int, Meter]], peer: str, epoch: int) -> Meter: ...
76
+
77
+ # receipts.py
78
+ def _msg(self, kind: str, body: BudgetBody, peer: str, piuri: str = ...) -> Message[BudgetBody]: ...
79
+ def receipt_due(self, peer: str, now: int) -> bool: ...
80
+ def send_receipt(self, peer: str, now: Optional[int] = None, final: bool = False,
81
+ epoch: Optional[int] = None) -> tuple[ReceiptBody, str]: ...
82
+ def _ack_lag(self, peer: str, body: Mapping[str, Any]) -> int: ...
83
+ def final_due(self, peer: str, now: int) -> Optional[int]: ...
84
+ def on_receipt(self, peer: str, body: Any, now: Optional[int] = None) -> Message[BudgetReceiptAckBody]: ...
85
+ def on_close(self, peer: str, body: Any, now: Optional[int] = None) -> Message[BudgetReceiptAckBody]: ...
86
+ def handle_receipt(self, inbound: Any, sender: str) -> tuple[Any, ...]: ...
87
+ def handle_close(self, inbound: Any, sender: str) -> tuple[Any, ...]: ...
88
+ def handle_receipt_ack(self, inbound: Any, sender: str) -> tuple[Any, ...]: ...
89
+
90
+ # windows.py
91
+ def evaluate(self, peer: str, epoch: int, now: Optional[int] = None) -> Optional[Evaluation]: ...
92
+ def publish_pending(self, peer: str, now: Optional[int] = None) -> Optional[Mapping[str, Any]]: ...
93
+ def _over(self, limits: Mapping[str, Any], counters: Mapping[str, Any], opening: bool = False) -> Optional[str]: ...
94
+ def closeable(self, peer: str, now: int) -> list[int]: ...
95
+ def reopen_due(self, peer: str) -> list[int]: ...
96
+ def reprice(self, peer: str, epoch: int, now: Optional[int] = None) -> Optional[Repricing]: ...
97
+
98
+ # tick.py
99
+ def _tick_peer(self, peer: str, now_val: int) -> list[tuple[Any, ...]]: ...
100
+ def _forget_old(self, now: int) -> None: ...
101
+ def _forget_in_memory(self, horizon: int) -> None: ...