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,110 @@
1
+ """The scheduler's tick: what is due for every peer, and the retention
2
+ horizon. Separate because it is the one place the receipts and the windows
3
+ are driven from, and the one place that forgets.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from typing import Any, Optional
8
+
9
+ from fleetmesh_core.accounting import accounting
10
+ from node.storage import grants as grants_mod
11
+ from .meter import C, KEEP_FINDINGS
12
+ from .shape import BudgetShape
13
+
14
+
15
+ class TickMixin:
16
+ # ---- the scheduler's tick ---------------------------------------------------
17
+ def tick(self: BudgetShape, now: Optional[int] = None) -> list[tuple[Any, ...]]:
18
+ """Send due receipts; evaluate closed windows. Returns what it did."""
19
+ now_val = now if now is not None else self.clock()
20
+ did: list[tuple[Any, ...]] = []
21
+ self._save_meters()
22
+ self._forget_old(now_val)
23
+ for lst in (self.findings, self.clock_drift, self.receipt_gaps, self.unpublished, self.evaluations):
24
+ if isinstance(lst, list) and len(lst) > KEEP_FINDINGS:
25
+ del lst[:len(lst) - KEEP_FINDINGS]
26
+ for peer in self._peers():
27
+ try:
28
+ did += self._tick_peer(peer, now_val)
29
+ except Exception as e:
30
+ # One peer must not end the tick for the others. A peer whose
31
+ # descriptor no longer resolves - it expired, or it declares a
32
+ # protocol digest this node refuses - raised TransportError out
33
+ # of `send_receipt`, and every peer after it in `grants.peers()`
34
+ # order silently missed its receipts for that window. On the
35
+ # 2026-09-26 roll that was three of four nodes, once a window,
36
+ # for as long as one peer stayed behind the digest.
37
+ #
38
+ # `scheduler.py` already learned this and says so at its own two
39
+ # call sites. This is the same fault in the compartment that
40
+ # owes the receipts.
41
+ self.refusals.refuse('accounting', 'the peer raised', subject=peer,
42
+ detail={'error': '%s: %s' % (type(e).__name__, e)}, at=now_val)
43
+ did.append(('accounting failed', peer, '%s: %s' % (type(e).__name__, e)))
44
+ return did
45
+
46
+ def _tick_peer(self: BudgetShape, peer: str, now_val: int) -> list[tuple[Any, ...]]:
47
+ """One peer's share of the tick: its grant, its receipts, its windows."""
48
+ did: list[tuple[Any, ...]] = []
49
+ out = self.publish_pending(peer, now_val)
50
+ if out is not None:
51
+ did.append(('grant published', peer, out['payload']['tier'], out['envelope']['id']))
52
+ prev = self.final_due(peer, now_val)
53
+ if prev is not None:
54
+ body, head = self.send_receipt(peer, now_val, final=True, epoch=prev)
55
+ self.chain_out[peer][prev].final = True
56
+ did.append(('final receipt', peer, prev, body['seq']))
57
+ if self.receipt_due(peer, now_val):
58
+ body, head = self.send_receipt(peer, now_val)
59
+ did.append(('receipt', peer, body['seq']))
60
+ for e in self.reopen_due(peer):
61
+ rp = self.reprice(peer, e, now_val)
62
+ if rp:
63
+ did.append(('repriced', peer, e, rp.was, rp.severity))
64
+ for e in self.closeable(peer, now_val):
65
+ ev = self.evaluate(peer, e, now_val)
66
+ if ev:
67
+ did.append(('evaluated', peer, e, ev.severity))
68
+ return did
69
+
70
+ def _forget_old(self: BudgetShape, now: int) -> None:
71
+ """Drop accounting for windows nothing can still be said about.
72
+
73
+ Both halves of it. This forgot the disk and not the memory the disk is
74
+ written back from: `_save_meters` walks usage_by and usage_of_me and
75
+ writes every epoch in them, so meters deleted on one tick were re-
76
+ inserted on the next, while the receipts and the evaluated marker
77
+ beside them stayed gone.
78
+
79
+ A window in that state - metered, no receipt, no record of having been
80
+ priced - is exactly the shape of active silence, and it was priced as
81
+ such. On the live mesh nyc3 held meters from epoch 496806 against
82
+ receipts that began at 496818, and re-priced the twelve windows between
83
+ as S2 on every restart, a batch at a time, against a peer that had done
84
+ nothing. It was pricing its own retention policy as the peer's fault.
85
+ """
86
+ if self.accounts is None:
87
+ return
88
+ oldest = min((self._window_of(p, grants_mod.ISSUED) for p in self._peers()),
89
+ default=C['window_seconds_default'])
90
+ keep = self.KEEP_WINDOWS
91
+ if self.strain is not None:
92
+ # A reserve, not a constant. A node that has watched peers vanish
93
+ # and come back keeps their accounting longer, so a chain still
94
+ # links when one does. Counted by direction, so one peer
95
+ # disappearing over and over buys a single direction's patience.
96
+ keep = int(self.strain.reserve(self.KEEP_WINDOWS, 'partition'))
97
+ horizon = accounting.epoch(now, oldest) - keep
98
+ if horizon > getattr(self, '_forgotten_to', -1):
99
+ self.accounts.forget_before(horizon)
100
+ self._forget_in_memory(horizon)
101
+ self._forgotten_to = horizon
102
+
103
+ def _forget_in_memory(self: BudgetShape, horizon: int) -> None:
104
+ """The same cutoff, applied to what the tick writes back."""
105
+ with self._lock:
106
+ for table in (self.usage_by, self.usage_of_me, self.receipts_in):
107
+ for by_epoch in table.values():
108
+ for e in [e for e in by_epoch if e < horizon]:
109
+ by_epoch.pop(e, None)
110
+ self.reopen = {(pk, e) for pk, e in self.reopen if e >= horizon} # not a store: pruned horizon
@@ -0,0 +1,67 @@
1
+ """Types and records for the Budget compartment."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Mapping, Optional, TypedDict
5
+
6
+ from fleetmesh_core.wire.interfaces import ReceiptBody, SilenceEvaluation, WindowEvaluationDetail
7
+ from node.interfaces import ReceiptRefusedRow
8
+
9
+
10
+ class OutChainDict(TypedDict, total=False):
11
+ """The JSON serialization shape of an OutChain."""
12
+ grant: str
13
+ seq: int
14
+ prev: str
15
+ sent_at: Optional[int]
16
+ last: Optional[dict[str, int]]
17
+ heads: list[str]
18
+ final: bool
19
+
20
+
21
+ class EvaluationDict(TypedDict, total=False):
22
+ """The JSON serialization shape of an Evaluation."""
23
+ peer: str
24
+ epoch: int
25
+ state: str
26
+ measured: dict[str, int]
27
+ claimed: Optional[dict[str, int]]
28
+ tier_before: str
29
+ grant: str
30
+ limits: Mapping[str, int | float]
31
+ severity: Optional[str]
32
+ note: str
33
+ detail: dict[str, WindowEvaluationDetail | SilenceEvaluation]
34
+ tier_after: Optional[str]
35
+ limits_after: Optional[Mapping[str, int | float]]
36
+ effective_from: Optional[int]
37
+ receipt_heads: list[str]
38
+ receipts: list[ReceiptBody]
39
+
40
+
41
+ class RepricingDict(TypedDict):
42
+ """The JSON serialization shape of a Repricing."""
43
+ peer: str
44
+ epoch: int
45
+ was: str
46
+ severity: str
47
+ state: str
48
+ measured: dict[str, int]
49
+ claimed: Optional[dict[str, int]]
50
+ receipts: int
51
+ remedy_cancelled: bool
52
+ at: int
53
+
54
+
55
+ class BudgetStateReport(TypedDict):
56
+ peers: int
57
+ windows_evaluated: int
58
+ findings: int
59
+ grants_waiting_for_a_boundary: int
60
+ clock_drift: int
61
+ receipt_gaps: int
62
+ receipts_behind: dict[str, int]
63
+ receipts_unpublished: int
64
+ receipt_chains_open: int
65
+ idle_streaks: dict[str, int]
66
+ rejected: dict[str, int]
67
+ receipts_refused: list[ReceiptRefusedRow]
@@ -0,0 +1,346 @@
1
+ """The window, priced: evaluate a closed one, publish the grant it decided
2
+ on, enforce the grant in force, and reprice on a late receipt. Separate
3
+ because this is where fleetmesh_core/window.py's ladder is applied and a grant is
4
+ changed; nothing here reads the wire.
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.storage import sync
13
+ from node.storage import grants as grants_mod
14
+ from .evaluation import Evaluation, Repricing
15
+ from .meter import C, KEEP_FINDINGS, Meter
16
+ from .shape import BudgetShape
17
+
18
+
19
+ class WindowsMixin:
20
+ # ---- issuer: closing windows ---------------------------------------------
21
+ def evaluate(self: BudgetShape, peer: str, epoch: int, now: Optional[int] = None) -> Optional[Evaluation]:
22
+ """One closed window against one subject. Returns the evaluation record."""
23
+ now_val = now if now is not None else self.clock()
24
+ row = self.grants.get(peer, grants_mod.ISSUED)
25
+ if row is None or row['status'] != 'live':
26
+ return None
27
+ # Priced under the grant that governed the window, not the one that is
28
+ # current when the grace period ends: a window that ran under one set
29
+ # of limits and was judged under another was a finding about nothing.
30
+ governed = self.governing(peer, grants_mod.ISSUED, epoch) or row
31
+ limits, tier = governed['payload']['limits'], governed['payload']['tier']
32
+ # The remedy applies to the grant that will be in force, which is the
33
+ # one already waiting for the boundary if a window was closed before
34
+ # this one in the same grace period, and otherwise the current one.
35
+ pending = self.grants.pending(peer)
36
+ base = pending['payload'] if pending else row['payload']
37
+ measured = self.usage_by.get(peer, {}).get(epoch, Meter()).snapshot()
38
+ receipts = self.receipts_in.get(peer, {}).get(epoch, [])
39
+ state, claimed = accounting.window_claim(receipts, measured)
40
+ rec = Evaluation(peer=peer, epoch=epoch, state=state, measured=measured, claimed=claimed,
41
+ tier_before=tier, grant=governed['envelope_id'], limits=limits)
42
+ if state == 'not-evaluated':
43
+ rec.note = 'nothing moved and nothing was claimed'
44
+ self.idle[peer] = self.idle.get(peer, 0) + 1
45
+ # Capacity earned in one busy week used to sit there a year later as
46
+ # surface nobody was watching: a limit moved up on a clean window
47
+ # and down on a fault and never decayed. A grant that has measured
48
+ # nothing for long enough gives some of itself back, down to where a
49
+ # peering opens and never below. Nothing happened, which is the whole
50
+ # of the reason, so it is amended with cause disuse and is not a
51
+ # finding about anyone.
52
+ if self.idle[peer] >= C['decay_idle_windows'] and base.get('limits'):
53
+ decayed = window.decay(base['limits'], base['tier'])
54
+ decayed = {k: (int(v) if float(v).is_integer() else v) for k, v in decayed.items()}
55
+ if decayed != base['limits']:
56
+ # Disuse is not a finding: the pending row carries no
57
+ # severity and `_apply_pending` reads that absence as the
58
+ # cause. GrantStore.put_pending annotates severity as str
59
+ # although it stores NULL here (node/grants.py, not owned
60
+ # by this change).
61
+ self.grants.put_pending(
62
+ peer, dict(base, limits=decayed, blinding=secrets.token_bytes(32).hex()),
63
+ 'decayed after %d windows that measured nothing' % self.idle[peer],
64
+ None,
65
+ accounting.epoch(now_val, self._window_of(peer, grants_mod.ISSUED)))
66
+ rec.note += '; grant decayed for disuse'
67
+ self.idle[peer] = 0
68
+ self.evaluations.append(rec)
69
+ self.evaluated.add((peer, epoch))
70
+ if self.accounts is not None:
71
+ self.accounts.put_evaluated(peer, epoch, None)
72
+ return rec
73
+ # A window with no receipt is still a window that was measured. Silence
74
+ # is a floor of S2 and not a ceiling: skipping the arithmetic here - which
75
+ # is what this did - priced a peer that flooded and said nothing BELOW the
76
+ # same peer reporting it honestly, because honest excess reaches S3 on
77
+ # overrun alone. fleetmesh_core/badpeer.py plugged into a node is what found it, and
78
+ # it is the same inversion the severity ladder itself had. An absent claim
79
+ # is the empty claim, which is how vectors/window.json has always priced
80
+ # "no receipt at all".
81
+ self.idle[peer] = 0
82
+ ev = window.evaluate_window(limits, measured, claimed or {})
83
+ severity, detail = ev['severity'], ev['per_limit']
84
+ if state == 'active-silence':
85
+ severity = window.worse(severity, 'S2')
86
+ detail = dict(detail, silence=accounting.silence(measured))
87
+ self.findings.append((peer, epoch, severity, 'traffic measured and no receipt: active silence'))
88
+ for name, d in ev['per_limit'].items():
89
+ if d['severity'] != 'S0':
90
+ self.findings.append((peer, epoch, d['severity'], '%s: %s' % (name, d)))
91
+ # The clean branch grows each limit by what this window actually carried.
92
+ new_limits, new_tier, note = window.adjust(base['limits'], base['tier'], severity, measured)
93
+ if severity == 'S3':
94
+ self.zero_until[peer] = now_val + C['s3_zero_hours'] * 3600
95
+ rounded = {k: (int(v) if float(v).is_integer() else v) for k, v in new_limits.items()}
96
+ new_payload = dict(base, limits=rounded, tier=new_tier, blinding=secrets.token_bytes(32).hex())
97
+ decided = accounting.epoch(now_val, self._window_of(peer, grants_mod.ISSUED))
98
+ self.grants.put_pending(peer, new_payload, '%s after %s' % (note, severity), severity, decided)
99
+ self.ledger.record(peer, epoch, severity, new_tier)
100
+ rec.severity, rec.detail, rec.note = severity, detail, note
101
+ rec.tier_after, rec.limits_after, rec.effective_from = new_tier, rounded, decided + 1
102
+ self.evaluations.append(rec)
103
+ self.evaluated.add((peer, epoch))
104
+ if self.accounts is not None:
105
+ self.accounts.put_evaluated(peer, epoch, severity, new_tier)
106
+ rec.receipt_heads = [accounting.link(r) for r in receipts]
107
+ rec.receipts = list(receipts) # the bodies, which are the evidence
108
+ for cb in self.on_evaluated:
109
+ cb(rec)
110
+ return rec
111
+
112
+ def publish_pending(self: BudgetShape, peer: str, now: Optional[int] = None) -> Optional[Mapping[str, Any]]:
113
+ """At a window boundary, the grant the last evaluation decided on goes
114
+ out: written, published as a new kind 30801 at the same coordinate, and
115
+ the subject told with peering/1.0/amend. Returns the envelope or None."""
116
+ now_val = now if now is not None else self.clock()
117
+ pend = self.grants.pending(peer)
118
+ if pend is None:
119
+ return None
120
+ w = self._window_of(peer, grants_mod.ISSUED)
121
+ current = accounting.epoch(now_val, w)
122
+ if current <= pend['decided_epoch']:
123
+ return None
124
+ row = self.grants.get(peer, grants_mod.ISSUED)
125
+ if row is None or row['status'] != 'live':
126
+ self.grants.clear_pending(peer)
127
+ return None
128
+ payload = pend['payload']
129
+ # The tier ceiling is enforced where a grant is written and not only
130
+ # where one is computed. window.adjust holds its own result inside the
131
+ # tier it returns, but the thing the specification constrains is the
132
+ # grant that reaches the wire, and a pending payload can have been
133
+ # decided by an older build and slept through a restart. If this ever
134
+ # fires the defect is upstream, so it is recorded against this node.
135
+ # A pending grant with no severity was not decided by the ladder. The only
136
+ # path that writes one is decay, so this is where disuse reaches the wire.
137
+ cause = 'remedy' if pend.get('severity') else 'disuse'
138
+ # The clamp, the envelope, the store, the amend: peering owns all of it
139
+ # now. This used to be its own copy of that procedure, and it was the
140
+ # only copy that clamped the payload to its tier ceiling.
141
+ out = self.peering.issue(peer, payload, cause, pend['note'], effective_epoch=current, now=now_val,
142
+ on_clamp=lambda pk, tier, over: self.findings.append(
143
+ ('self', None, None,
144
+ 'pending grant for %s carried limits above the %s ceiling: %s'
145
+ % (pk[:8], tier, over))))
146
+ # What standing tells a third party about this peer is the tier of the
147
+ # grant in force, so it moves when the grant does. The operator path
148
+ # used to set this itself the moment a person decided; now that every
149
+ # decision lands at the boundary, the boundary is where it belongs, and
150
+ # both paths get it from the same line.
151
+ if self.ledger is not None:
152
+ self.ledger.tier[peer] = out['payload'].get('tier')
153
+ return {'envelope': out['envelope'], 'payload': out['payload'],
154
+ 'published': out['published'], 'severity': pend['severity']}
155
+
156
+ # ---- the grant, enforced ----------------------------------------------------
157
+ def _over(self: BudgetShape, limits: Mapping[str, Any], counters: Mapping[str, Any], opening: bool = False) -> Optional[str]:
158
+ """The first limit a session would go past, or None. `opening` adds
159
+ the connection and request the open itself costs. The reference's own
160
+ `Grant.exceeded` decides, so what is refused here is exactly what the
161
+ ladder would price, under the same grant.counter_of mapping."""
162
+ c = dict(counters)
163
+ if opening:
164
+ c['conn_open'] = c.get('conn_open', 0) + 1
165
+ c['req'] = c.get('req', 0) + 1
166
+ over = sync.Grant('', '', limits).exceeded(c)
167
+ if not over:
168
+ return None
169
+ name = sorted(over)[0]
170
+ return '%s %s over the grant\'s %s' % (name, c.get(window.counter_of(name), 0), limits[name])
171
+
172
+ def throttled(self: BudgetShape, peer: str, now: Optional[int] = None) -> Optional[str]:
173
+ """Why this node must not open a session with the peer right now, or
174
+ None. Read from the grant it holds: revoked, zero, or a limit the
175
+ window's usage so far has reached."""
176
+ now_val = now if now is not None else self.clock()
177
+ row = self.grants.get(peer, grants_mod.RECEIVED)
178
+ if row is None or row['status'] != 'live':
179
+ return 'no live grant from the peer'
180
+ limits = row['payload'].get('limits', {})
181
+ if row['payload'].get('tier') == 'revoked' or not any(limits.values()):
182
+ return 'the grant is zero'
183
+ epoch = self.epoch(peer, grants_mod.RECEIVED, now_val)
184
+ used = self._meter(self.usage_of_me, peer, epoch).snapshot()
185
+ return self._over(limits, used, opening=True)
186
+
187
+ def granted_rate(self: BudgetShape, peer: str) -> tuple[int, int]:
188
+ """(sessions the grant allows per window, the window in seconds).
189
+
190
+ What a cadence is derived from when none is configured: the rate the
191
+ peer actually granted, so a tier promotion changes how often this node
192
+ pulls without anybody editing a number beside it.
193
+ """
194
+ row = self.grants.get(peer, grants_mod.RECEIVED)
195
+ w = self._window_of(peer, grants_mod.RECEIVED)
196
+ if row is None or row['status'] != 'live':
197
+ return (0, w)
198
+ limits = row['payload'].get('limits', {})
199
+ return (int(limits.get('conn_rate', 0) or 0), w)
200
+
201
+ def sessions_left(self: BudgetShape, peer: str, now: Optional[int] = None) -> tuple[int, int]:
202
+ """How many more sessions this node may open with the peer in the
203
+ window that is running, and when that window closes.
204
+
205
+ The pair the scheduler needs to pace itself from the contract rather
206
+ than from a configured number. `conn_rate` is what the peer granted
207
+ per window; `conn_open` is what this node has already spent of it, and
208
+ it is read from the meter, which is the record the peer also prices -
209
+ not a private clock the scheduler keeps on the side.
210
+
211
+ Returns (0, close) when the budget is spent, which is a wait rather
212
+ than a refusal: the allowance returns when the window turns.
213
+ """
214
+ now_val = now if now is not None else self.clock()
215
+ row = self.grants.get(peer, grants_mod.RECEIVED)
216
+ if row is None or row['status'] != 'live':
217
+ return (0, now_val)
218
+ limits = row['payload'].get('limits', {})
219
+ allowed = int(limits.get('conn_rate', 0) or 0)
220
+ w = self._window_of(peer, grants_mod.RECEIVED)
221
+ epoch = self.epoch(peer, grants_mod.RECEIVED, now_val)
222
+ closes = accounting.window_bounds(epoch, w)[1]
223
+ if row['payload'].get('tier') == 'revoked' or allowed <= 0:
224
+ return (0, closes)
225
+ used = self._meter(self.usage_of_me, peer, epoch).snapshot()
226
+ spent = int(used.get(window.counter_of('conn_rate'), 0) or 0)
227
+ return (max(0, allowed - spent), closes)
228
+
229
+ def refuses(self: BudgetShape, peer: str, now: Optional[int] = None) -> Optional[str]:
230
+ """Why this node must not serve the peer's session, or None. Read from
231
+ the grant it issued and governing now, plus an S3 zero still running."""
232
+ now_val = now if now is not None else self.clock()
233
+ if self.zero_until.get(peer, 0) > now_val:
234
+ return 'zeroed after S3 until %d' % self.zero_until[peer]
235
+ row = self.grants.get(peer, grants_mod.ISSUED)
236
+ if row is None or row['status'] != 'live':
237
+ return 'no live grant to the peer'
238
+ epoch = self.epoch(peer, grants_mod.ISSUED, now_val)
239
+ limits = (self.governing(peer, grants_mod.ISSUED, epoch) or row)['payload'].get('limits', {})
240
+ if row['payload'].get('tier') == 'revoked' or not any(limits.values()):
241
+ return 'the grant is zero'
242
+ used = self._meter(self.usage_by, peer, epoch).snapshot()
243
+ return self._over(limits, used)
244
+
245
+ def closeable(self: BudgetShape, peer: str, now: int) -> list[int]:
246
+ """Epochs of my grant to this peer that have closed and passed grace."""
247
+ w = self._window_of(peer, grants_mod.ISSUED)
248
+ current = accounting.epoch(now, w)
249
+ # The window a receipt may not name is a window that may not be priced.
250
+ # on_receipt refuses one outside this bound as "answerable by nothing",
251
+ # and a verdict reached where no answer is admissible is not a verdict:
252
+ # the subject cannot refute it, and the evidence to support it is what
253
+ # this node deliberately threw away. Same bound, both directions.
254
+ oldest = current - self.KEEP_WINDOWS
255
+ with self._lock: # the sync thread grows these; see _save_meters
256
+ seen = set(self.usage_by.get(peer, {})) | set(self.receipts_in.get(peer, {}))
257
+ out: list[int] = []
258
+ for e in sorted(seen):
259
+ if e < oldest:
260
+ continue
261
+ if e < current and (peer, e) not in self.evaluated and now >= accounting.window_bounds(e, w)[1] + accounting.window_grace(w):
262
+ out.append(e)
263
+ return out
264
+
265
+ def reopen_due(self: BudgetShape, peer: str) -> list[int]:
266
+ """Windows already priced that a receipt has since landed on. Draining
267
+ the queue is what makes a reprice happen once and not on every tick."""
268
+ with self._lock:
269
+ mine = sorted(e for pk, e in self.reopen if pk == peer)
270
+ self.reopen -= {(peer, e) for e in mine}
271
+ return mine
272
+
273
+ def reprice(self: BudgetShape, peer: str, epoch: int, now: Optional[int] = None) -> Optional[Repricing]:
274
+ """Price one window again, because a receipt covering it arrived late.
275
+
276
+ A window closes on the clock and is judged once its grace has run, and
277
+ the subject's last receipt for it can arrive after that: the grace is
278
+ window_grace_fraction of the window - 360 seconds at the default - and
279
+ a control-plane gap of 882 seconds has been measured on a running mesh.
280
+ What the issuer holds then is a claim missing its tail, which is
281
+ arithmetically indistinguishable from a claim that was understated. So
282
+ an honest peer earns the S2 misreport floor for a delivery gap, and
283
+ cannot refute it: the published classifier run over the receipt it did
284
+ sign returns S2, correctly. That is the fourth layer at which something
285
+ other than dishonesty has been priced as dishonesty, after the session
286
+ abort, the standing streak, and the node that vanishes mid-window which
287
+ `farewell` answers. The first three were fixed by saying something
288
+ rather than nothing; this one cannot be, because the subject did say it
289
+ and the wire lost it. So the issuer reads again instead.
290
+
291
+ **A reprice may only lower a severity, and that is not a courtesy.**
292
+ The receipt chain appends and does nothing else - seq, prev and every
293
+ counter are checked before a receipt is stored - so a late receipt is
294
+ the tail of a claim the subject had already committed to, and can only
295
+ move that claim toward what was measured. A second reading that came
296
+ out worse would therefore not be a later truth but a different one, and
297
+ a node that could be made to price harder by a late receipt would hand
298
+ every peer a way to worsen a verdict it had already earned. If the new
299
+ reading is not strictly better, the first one stands and nothing here
300
+ happens.
301
+ """
302
+ now_val = now if now is not None else self.clock()
303
+ was = self.ledger.severity_of(peer, epoch)
304
+ if was is None:
305
+ return None
306
+ row = self.grants.get(peer, grants_mod.ISSUED)
307
+ if row is None or row['status'] != 'live':
308
+ return None
309
+ governed = self.governing(peer, grants_mod.ISSUED, epoch) or row
310
+ limits = governed['payload']['limits']
311
+ measured = self.usage_by.get(peer, {}).get(epoch, Meter()).snapshot()
312
+ receipts = self.receipts_in.get(peer, {}).get(epoch, [])
313
+ state, claimed = accounting.window_claim(receipts, measured)
314
+ if state == 'not-evaluated':
315
+ return None
316
+ severity = window.evaluate_window(limits, measured, claimed or {})['severity']
317
+ if state == 'active-silence':
318
+ severity = window.worse(severity, 'S2')
319
+ if severity == was or window.worse(severity, was) != was:
320
+ return None
321
+ self.ledger.amend(peer, epoch, severity)
322
+ if self.accounts is not None:
323
+ self.accounts.put_evaluated(peer, epoch, severity, self.ledger.tier.get(peer))
324
+ # The remedy is cancelled only where cancelling it is the whole answer:
325
+ # the window was clean and the grant that would have punished it has
326
+ # not been issued yet. A reading that is milder but still adverse
327
+ # leaves the stricter remedy standing rather than deriving a new one
328
+ # from a base that is already gone, and a remedy that has reached the
329
+ # wire is a signed artefact this must not quietly unwind. The record is
330
+ # corrected in every case; only the unissued grant is withdrawn, and
331
+ # the direction of the error is the strict one either way. A tier the
332
+ # ladder dropped stays dropped, because raising one is a person's
333
+ # decision and nothing here is a person.
334
+ pend = self.grants.pending(peer)
335
+ cancelled = False
336
+ if severity == 'S0' and pend is not None and pend.get('severity') == was:
337
+ self.grants.clear_pending(peer)
338
+ cancelled = True
339
+ rec = Repricing(peer=peer, epoch=epoch, was=was, severity=severity, state=state,
340
+ measured=measured, claimed=claimed, receipts=len(receipts),
341
+ remedy_cancelled=cancelled, at=now_val)
342
+ self.repriced.append(rec)
343
+ del self.repriced[:-KEEP_FINDINGS]
344
+ for cb in self.on_repriced:
345
+ cb(peer, epoch, was, severity)
346
+ return rec
@@ -0,0 +1,7 @@
1
+ """The node's own ceiling, held across ticks, and the shed ladder it drives.
2
+
3
+ One-module compartment: everything is in compartment.py.
4
+ """
5
+ from .compartment import Capacity
6
+
7
+ __all__ = ['Capacity']