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,568 @@
1
+ """PeerStandingStore: SQLite-backed peer standing, window AIMD budget evaluation,
2
+ reporter weighting, and standing-gated NIP-77 Negentropy shard reconciliation.
3
+
4
+ Standardizes the integration of:
5
+ - `fleetmesh_core.accounting.standing.Ledger` (window-ordered streak tracking,
6
+ `tell` / `answer`, `report_record`, `on_revocation`)
7
+ - `fleetmesh_core.accounting.window` (`opening_limits`, `evaluate_window`,
8
+ `view`, `adjust`, `reporter_weight`, fixed-point millionths consensus)
9
+ - `fleetmesh_core.storage.sync` (`Grant`, `Session`, `run`)
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import contextlib
14
+ import json
15
+ import sqlite3
16
+ import threading
17
+ import time
18
+ from typing import Any, Callable, Iterator, Mapping, Optional, Sequence, TypedDict
19
+
20
+ from fleetmesh_core.accounting import standing as standing_mod
21
+ from fleetmesh_core.accounting import window
22
+ from fleetmesh_core.storage import store, sync
23
+ from node.compartment.schema import Column, Table
24
+
25
+ from .projected import ProjectedNodeStore
26
+ from .sharding import HandoverAcceptReply, ShardCoverageManager
27
+
28
+ __all__ = [
29
+ 'PEER_STANDING_TABLE',
30
+ 'PeerStandingRecord',
31
+ 'PeerStandingStore',
32
+ 'ReconcileShardsReply',
33
+ 'ShardReconcileEntry',
34
+ 'WeightedReportEntry',
35
+ 'WindowEvalRecord',
36
+ ]
37
+
38
+ VALID_TIERS: tuple[str, ...] = ('anchor', 'trusted', 'member', 'probation')
39
+
40
+
41
+ class PeerStandingRecord(TypedDict):
42
+ peer_pk: str
43
+ peer_did: str
44
+ tier: str
45
+ limits: dict[str, int]
46
+ clean_windows: int
47
+ evaluated_windows: int
48
+ contradicted_reports: int
49
+ last_severity: str
50
+ last_note: str
51
+ zero_until: int
52
+ reporter_weight: float
53
+ can_sync: bool
54
+ updated_at: int
55
+
56
+
57
+ class WeightedReportEntry(TypedDict):
58
+ severity: str
59
+ weight: float
60
+
61
+
62
+ # Optional keys sit in total=False bases: typing.NotRequired is Python 3.11+
63
+ # and this package supports 3.10.
64
+ class _WindowEvalOptional(TypedDict, total=False):
65
+ weighted_reports: list[WeightedReportEntry]
66
+
67
+
68
+ class WindowEvalRecord(PeerStandingRecord, _WindowEvalOptional):
69
+ window_severity: str
70
+ final_severity: str
71
+ per_limit: Mapping[str, Any]
72
+
73
+
74
+ class ShardReconcileEntry(TypedDict):
75
+ pull_status: Optional[str]
76
+ push_status: Optional[str]
77
+ fingerprint_match: bool
78
+ findings: list[tuple[str, str]]
79
+
80
+
81
+ class _ReconcileShardsOptional(TypedDict, total=False):
82
+ standing: PeerStandingRecord
83
+ handovers: dict[str, HandoverAcceptReply]
84
+
85
+
86
+ class ReconcileShardsReply(_ReconcileShardsOptional):
87
+ ok: bool
88
+ status: str
89
+ standing_before: PeerStandingRecord
90
+ standing_after: PeerStandingRecord | WindowEvalRecord
91
+ shards: dict[str, ShardReconcileEntry]
92
+
93
+
94
+ PEER_STANDING_TABLE = Table(
95
+ 'peer_standing',
96
+ (
97
+ Column('peer_pk', 'TEXT', primary_key=True),
98
+ Column('peer_did', 'TEXT', not_null=True),
99
+ Column('tier', 'TEXT', not_null=True),
100
+ Column('limits_json', 'TEXT', not_null=True),
101
+ Column('clean_windows', 'INTEGER', not_null=True),
102
+ Column('evaluated_windows', 'INTEGER', not_null=True),
103
+ Column('contradicted_reports', 'INTEGER', not_null=True),
104
+ Column('last_severity', 'TEXT', not_null=True),
105
+ Column('last_note', 'TEXT', not_null=True),
106
+ Column('zero_until', 'INTEGER', not_null=True),
107
+ Column('updated_at', 'INTEGER', not_null=True),
108
+ ),
109
+ )
110
+
111
+
112
+ class PeerStandingStore:
113
+ """Persistent peer standing book backed by SQLite and hydrated into a
114
+ `fleetmesh_core.accounting.standing.Ledger`."""
115
+
116
+ def __init__(
117
+ self,
118
+ conn: Optional[sqlite3.Connection] = None,
119
+ *,
120
+ lock: Optional[threading.RLock] = None,
121
+ clock: Optional[Callable[[], int]] = None,
122
+ tier_fallback_fn: Optional[Callable[[str], Optional[str]]] = None,
123
+ on_tier_change: Optional[Callable[[str, str], None]] = None,
124
+ ) -> None:
125
+ self._conn = conn if conn is not None else sqlite3.connect(':memory:', check_same_thread=False)
126
+ self._lock = lock if lock is not None else threading.RLock()
127
+ self.clock: Callable[[], int] = clock or (lambda: int(time.time()))
128
+ self._tier_fallback_fn = tier_fallback_fn
129
+ self._on_tier_change = on_tier_change
130
+ self.ledger = standing_mod.Ledger()
131
+ self._epoch_seq: int = 0
132
+ self._tables_ready = False
133
+ self.ensure_tables()
134
+
135
+ def close(self) -> None:
136
+ """Close the underlying SQLite connection."""
137
+ with self._lock:
138
+ self._conn.close()
139
+
140
+ @contextlib.contextmanager
141
+ def _tx(self) -> Iterator[sqlite3.Connection]:
142
+ with self._lock:
143
+ with self._conn:
144
+ yield self._conn
145
+
146
+ def ensure_tables(self) -> None:
147
+ if self._tables_ready:
148
+ return
149
+ with self._tx() as conn:
150
+ with contextlib.closing(conn.execute(PEER_STANDING_TABLE.to_sql())):
151
+ pass
152
+ for sec in PEER_STANDING_TABLE.secondary_sql():
153
+ with contextlib.closing(conn.execute(sec)):
154
+ pass
155
+ self._tables_ready = True
156
+ self._hydrate_ledger(conn)
157
+
158
+ @staticmethod
159
+ def _build_window_history(clean_w: int, eval_w: int, last_sev: str = 'S0') -> list[tuple[int, str]]:
160
+ c_n = max(0, int(clean_w))
161
+ e_n = max(c_n, int(eval_w))
162
+ win_list: list[tuple[int, str]] = []
163
+ non_clean = e_n - c_n
164
+ non_clean_sev = str(last_sev) if str(last_sev) != 'S0' else 'S1'
165
+ for i in range(non_clean):
166
+ win_list.append((i + 1, non_clean_sev))
167
+ for i in range(c_n):
168
+ win_list.append((non_clean + i + 1, 'S0'))
169
+ return win_list
170
+
171
+ def _hydrate_ledger(self, conn: sqlite3.Connection) -> None:
172
+ with contextlib.closing(
173
+ conn.execute(
174
+ 'SELECT peer_pk, tier, clean_windows, evaluated_windows, last_severity FROM peer_standing'
175
+ )
176
+ ) as cur:
177
+ rows = cur.fetchall()
178
+ for pk, tier, clean_w, eval_w, last_sev in rows:
179
+ clean_pk = str(pk).lower()
180
+ self.ledger.tier[clean_pk] = str(tier)
181
+ c_n = int(clean_w or 0)
182
+ e_n = max(c_n, int(eval_w or 0))
183
+ self.ledger.windows[clean_pk] = self._build_window_history(c_n, e_n, str(last_sev or 'S0'))
184
+ self._epoch_seq = max(self._epoch_seq, e_n)
185
+
186
+ @staticmethod
187
+ def _row_to_record(row: Sequence[Any], now_ts: int) -> PeerStandingRecord:
188
+ clean_pk = str(row[0]).lower()
189
+ tier = str(row[2])
190
+ limits = json.loads(str(row[3]))
191
+ clean_win = int(row[4])
192
+ eval_win = int(row[5])
193
+ contra = int(row[6])
194
+ last_sev = str(row[7])
195
+ last_note = str(row[8])
196
+ zero_until = int(row[9])
197
+ updated_at = int(row[10])
198
+
199
+ rep_weight = (
200
+ window.reporter_weight(tier, clean_win, contra, eval_win)
201
+ if tier in VALID_TIERS
202
+ else 0.0
203
+ )
204
+ can_sync = (
205
+ tier != 'revoked'
206
+ and int(limits.get('events_in', 0)) > 0
207
+ and now_ts >= zero_until
208
+ )
209
+ return {
210
+ 'peer_pk': clean_pk,
211
+ 'peer_did': str(row[1]),
212
+ 'tier': tier,
213
+ 'limits': limits,
214
+ 'clean_windows': clean_win,
215
+ 'evaluated_windows': eval_win,
216
+ 'contradicted_reports': contra,
217
+ 'last_severity': last_sev,
218
+ 'last_note': last_note,
219
+ 'zero_until': zero_until,
220
+ 'reporter_weight': round(rep_weight, 6),
221
+ 'can_sync': can_sync,
222
+ 'updated_at': updated_at,
223
+ }
224
+
225
+ def get(
226
+ self,
227
+ peer_pk: str,
228
+ *,
229
+ default_tier: Optional[str] = None,
230
+ create_if_missing: bool = False,
231
+ ) -> PeerStandingRecord:
232
+ """Return (and optionally initialize when `create_if_missing=True`) the
233
+ standing record for `peer_pk`."""
234
+ self.ensure_tables()
235
+ clean_pk = (peer_pk or '').strip().lower()
236
+ if not store.is_hex64(clean_pk):
237
+ raise ValueError('peer_pk must be a 64-character lowercase hex pubkey')
238
+ now_ts = self.clock()
239
+ with self._lock:
240
+ with contextlib.closing(
241
+ self._conn.execute(
242
+ 'SELECT peer_pk, peer_did, tier, limits_json, clean_windows, evaluated_windows, '
243
+ 'contradicted_reports, last_severity, last_note, zero_until, updated_at '
244
+ 'FROM peer_standing WHERE peer_pk = ?',
245
+ (clean_pk,),
246
+ )
247
+ ) as cur:
248
+ row = cur.fetchone()
249
+ if row is None:
250
+ fallback = self._tier_fallback_fn(clean_pk) if self._tier_fallback_fn else None
251
+ tier = (
252
+ str(default_tier)
253
+ if default_tier in VALID_TIERS
254
+ else str(fallback)
255
+ if fallback in VALID_TIERS
256
+ else 'probation'
257
+ )
258
+ limits = window.opening_limits(tier)
259
+ rep_weight = (
260
+ window.reporter_weight(tier, 0, 0, 0)
261
+ if tier in VALID_TIERS
262
+ else 0.0
263
+ )
264
+ default_rec: PeerStandingRecord = {
265
+ 'peer_pk': clean_pk,
266
+ 'peer_did': f'did:nostr:{clean_pk}',
267
+ 'tier': tier,
268
+ 'limits': limits,
269
+ 'clean_windows': 0,
270
+ 'evaluated_windows': 0,
271
+ 'contradicted_reports': 0,
272
+ 'last_severity': 'S0',
273
+ 'last_note': 'opening',
274
+ 'zero_until': 0,
275
+ 'reporter_weight': round(rep_weight, 6),
276
+ 'can_sync': tier != 'revoked' and int(limits.get('events_in', 0)) > 0,
277
+ 'updated_at': now_ts,
278
+ }
279
+ if not create_if_missing:
280
+ return default_rec
281
+ with self._conn:
282
+ with contextlib.closing(
283
+ self._conn.execute(
284
+ """INSERT INTO peer_standing
285
+ (peer_pk, peer_did, tier, limits_json, clean_windows, evaluated_windows,
286
+ contradicted_reports, last_severity, last_note, zero_until, updated_at)
287
+ VALUES (?, ?, ?, ?, 0, 0, 0, 'S0', 'opening', 0, ?)""",
288
+ (clean_pk, f'did:nostr:{clean_pk}', tier, json.dumps(limits, sort_keys=True), now_ts),
289
+ )
290
+ ):
291
+ pass
292
+ self.ledger.tier[clean_pk] = tier
293
+ self.ledger.windows.setdefault(clean_pk, [])
294
+ return default_rec
295
+
296
+ return self._row_to_record(row, now_ts)
297
+
298
+ def set_tier(
299
+ self,
300
+ peer_pk: str,
301
+ tier: str,
302
+ *,
303
+ clean_windows: Optional[int] = None,
304
+ evaluated_windows: Optional[int] = None,
305
+ now: Optional[int] = None,
306
+ ) -> PeerStandingRecord:
307
+ """Seed or update an operator-granted tier (`anchor`, `trusted`,
308
+ `member`, `probation`) for `peer_pk` and reset its opening limits."""
309
+ self.ensure_tables()
310
+ if tier not in VALID_TIERS:
311
+ raise ValueError(f'Invalid Fleetmesh tier: {tier!r}')
312
+ st_doc = self.get(peer_pk, default_tier=tier, create_if_missing=True)
313
+ clean_pk = st_doc['peer_pk']
314
+ limits = window.opening_limits(tier)
315
+ now_ts = int(now if now is not None else self.clock())
316
+ c_win = int(clean_windows if clean_windows is not None else st_doc['clean_windows'])
317
+ e_win = int(evaluated_windows if evaluated_windows is not None else max(c_win, st_doc['evaluated_windows']))
318
+ with self._tx() as conn:
319
+ with contextlib.closing(
320
+ conn.execute(
321
+ """UPDATE peer_standing
322
+ SET tier = ?, limits_json = ?, clean_windows = ?, evaluated_windows = ?,
323
+ last_severity = 'S0', last_note = 'operator_seed', zero_until = 0, updated_at = ?
324
+ WHERE peer_pk = ?""",
325
+ (tier, json.dumps(limits, sort_keys=True), c_win, e_win, now_ts, clean_pk),
326
+ )
327
+ ):
328
+ pass
329
+ self.ledger.tier[clean_pk] = tier
330
+ self.ledger.windows[clean_pk] = self._build_window_history(c_win, e_win, 'S0')
331
+ self._epoch_seq = max(self._epoch_seq, e_win)
332
+ if self._on_tier_change is not None:
333
+ self._on_tier_change(clean_pk, tier)
334
+ return self.get(clean_pk)
335
+
336
+ def evaluate_window(
337
+ self,
338
+ peer_pk: str,
339
+ *,
340
+ measured: Mapping[str, int | float],
341
+ claimed: Optional[Mapping[str, int | float]] = None,
342
+ reports: Optional[Sequence[Mapping[str, Any]]] = None,
343
+ own_severity: str = 'S0',
344
+ now: Optional[int] = None,
345
+ ) -> WindowEvalRecord:
346
+ """Evaluate a closed window for `peer_pk` via `window.evaluate_window`,
347
+ `window.view`, and `window.adjust`, updating both SQLite and
348
+ `self.ledger`."""
349
+ self.ensure_tables()
350
+ now_ts = int(now if now is not None else self.clock())
351
+ st_doc = self.get(peer_pk, create_if_missing=True)
352
+ clean_pk = st_doc['peer_pk']
353
+ cur_tier = st_doc['tier']
354
+ cur_limits = dict(st_doc['limits'])
355
+
356
+ if cur_tier == 'revoked':
357
+ return {
358
+ **st_doc,
359
+ 'window_severity': 'S4',
360
+ 'final_severity': 'S4',
361
+ 'per_limit': {},
362
+ }
363
+
364
+ actual_claimed = claimed if claimed is not None else measured
365
+ win_eval = window.evaluate_window(cur_limits, measured, actual_claimed)
366
+ combined_own = window.worse(str(win_eval['severity']), own_severity)
367
+
368
+ weighted_reports: list[WeightedReportEntry] = []
369
+ for r in (reports or []):
370
+ sev = str(r.get('severity') or 'S0')
371
+ if 'weight' in r and isinstance(r['weight'], (int, float)):
372
+ w = float(r['weight'])
373
+ elif r.get('reporter_pk'):
374
+ rep_st = self.get(str(r['reporter_pk']))
375
+ w = float(rep_st['reporter_weight'])
376
+ else:
377
+ w = 0.0
378
+ weighted_reports.append({'severity': sev, 'weight': w})
379
+
380
+ final_sev = window.view(combined_own, weighted_reports)
381
+ new_limits, new_tier, note = window.adjust(
382
+ cur_limits,
383
+ cur_tier,
384
+ final_sev,
385
+ used=measured,
386
+ )
387
+
388
+ with self._tx() as conn:
389
+ self._epoch_seq += 1
390
+ if new_tier == 'revoked':
391
+ self.ledger.on_revocation(clean_pk)
392
+ else:
393
+ self.ledger.record(clean_pk, self._epoch_seq, final_sev, new_tier)
394
+ if final_sev in standing_mod.ADVERSE:
395
+ self.ledger.reported_against(clean_pk)
396
+
397
+ clean_win = st_doc['clean_windows']
398
+ eval_win = st_doc['evaluated_windows'] + 1
399
+ zero_until = st_doc['zero_until']
400
+ if final_sev == 'S0':
401
+ clean_win += 1
402
+ elif final_sev in ('S2', 'S3', 'S4'):
403
+ clean_win = 0
404
+ if final_sev == 'S3':
405
+ s3_hours = int(window.C['grant']['s3_zero_hours'])
406
+ zero_until = now_ts + s3_hours * 3600
407
+
408
+ with contextlib.closing(
409
+ conn.execute(
410
+ """UPDATE peer_standing
411
+ SET tier = ?, limits_json = ?, clean_windows = ?, evaluated_windows = ?,
412
+ last_severity = ?, last_note = ?, zero_until = ?, updated_at = ?
413
+ WHERE peer_pk = ?""",
414
+ (
415
+ new_tier,
416
+ json.dumps(new_limits, sort_keys=True),
417
+ clean_win,
418
+ eval_win,
419
+ final_sev,
420
+ note,
421
+ zero_until,
422
+ now_ts,
423
+ clean_pk,
424
+ ),
425
+ )
426
+ ):
427
+ pass
428
+ if self._on_tier_change is not None and new_tier in (*VALID_TIERS, 'revoked'):
429
+ self._on_tier_change(clean_pk, new_tier)
430
+
431
+ updated = self.get(clean_pk)
432
+ return {
433
+ **updated,
434
+ 'window_severity': str(win_eval['severity']),
435
+ 'final_severity': final_sev,
436
+ 'per_limit': dict(win_eval['per_limit']),
437
+ 'weighted_reports': weighted_reports,
438
+ }
439
+
440
+ def list_all(self) -> list[PeerStandingRecord]:
441
+ """Return all tracked peer standing records ordered by `updated_at DESC`."""
442
+ self.ensure_tables()
443
+ now_ts = self.clock()
444
+ with self._lock:
445
+ with contextlib.closing(
446
+ self._conn.execute(
447
+ 'SELECT peer_pk, peer_did, tier, limits_json, clean_windows, evaluated_windows, '
448
+ 'contradicted_reports, last_severity, last_note, zero_until, updated_at '
449
+ 'FROM peer_standing ORDER BY updated_at DESC'
450
+ )
451
+ ) as cur:
452
+ rows = cur.fetchall()
453
+ return [self._row_to_record(r, now_ts) for r in rows]
454
+
455
+ def reconcile_shards(
456
+ self,
457
+ local_store: ProjectedNodeStore,
458
+ peer_store: ProjectedNodeStore,
459
+ peer_pk: str,
460
+ *,
461
+ shard_filters: Mapping[str, Mapping[str, Any]],
462
+ local_sharding: Optional[ShardCoverageManager] = None,
463
+ peer_sharding: Optional[ShardCoverageManager] = None,
464
+ issue_handovers: bool = True,
465
+ now: Optional[int] = None,
466
+ ) -> ReconcileShardsReply:
467
+ """Run standing-gated bidirectional NIP-77 Negentropy (`sync/1.0`)
468
+ reconciliation over `shard_filters`, metering session counters through
469
+ `evaluate_window` and optionally issuing/accepting `Kind 30803`
470
+ handover chains."""
471
+ clean_pk = (peer_pk or '').strip().lower()
472
+ st_before = self.get(clean_pk)
473
+ if not st_before['can_sync']:
474
+ return {
475
+ 'ok': False,
476
+ 'status': 'refused_by_standing',
477
+ 'standing': st_before,
478
+ 'standing_before': st_before,
479
+ 'standing_after': st_before,
480
+ 'shards': {},
481
+ 'handovers': {},
482
+ }
483
+
484
+ now_ts = int(now if now is not None else self.clock()) + 120
485
+ agg_counters: dict[str, int] = dict.fromkeys(sync.COUNTERS, 0)
486
+ worst_finding = 'S0'
487
+ shard_reports: dict[str, ShardReconcileEntry] = {}
488
+ handovers_completed: dict[str, HandoverAcceptReply] = {}
489
+
490
+ for shard_id, raw_filt in shard_filters.items():
491
+ filt = dict(raw_filt)
492
+ grant = sync.Grant(
493
+ f'grant-{shard_id}-{clean_pk[:8]}',
494
+ st_before['tier'],
495
+ st_before['limits'],
496
+ scope={'pull': filt, 'push': filt},
497
+ )
498
+ sess_init = sync.Session(
499
+ local_store, grant, shard_id, filt, since=0, until=now_ts, now=now_ts, is_initiator=True
500
+ )
501
+ sess_resp = sync.Session(
502
+ peer_store, grant, shard_id, filt, since=0, until=now_ts, now=now_ts, is_initiator=False
503
+ )
504
+ rep_pull = sync.run(sess_init, sess_resp)
505
+
506
+ sess_push_init = sync.Session(
507
+ peer_store, grant, shard_id, filt, since=0, until=now_ts, now=now_ts, is_initiator=True
508
+ )
509
+ sess_push_resp = sync.Session(
510
+ local_store, grant, shard_id, filt, since=0, until=now_ts, now=now_ts, is_initiator=False
511
+ )
512
+ rep_push = sync.run(sess_push_init, sess_push_resp)
513
+
514
+ for k in sync.COUNTERS:
515
+ agg_counters[k] = max(agg_counters[k], int(sess_init.counters.get(k, 0)))
516
+ for sev, _why in rep_pull['findings'] + rep_push['findings']:
517
+ if sev in window.SEV_ORDER:
518
+ worst_finding = window.worse(worst_finding, sev)
519
+
520
+ fp_match = (
521
+ local_store.fingerprint_range(filt, 0, now_ts, now_ts)
522
+ == peer_store.fingerprint_range(filt, 0, now_ts, now_ts)
523
+ )
524
+ shard_reports[shard_id] = {
525
+ 'pull_status': rep_pull['status'],
526
+ 'push_status': rep_push['status'],
527
+ 'fingerprint_match': fp_match,
528
+ 'findings': rep_pull['findings'] + rep_push['findings'],
529
+ }
530
+
531
+ if (
532
+ issue_handovers
533
+ and local_sharding is not None
534
+ and peer_sharding is not None
535
+ and fp_match
536
+ and rep_pull['status'] in ('complete', 'in_sync')
537
+ and rep_push['status'] in ('complete', 'in_sync')
538
+ ):
539
+ ho_issued = peer_sharding.issue_handover(
540
+ shard_id, local_sharding.node_pk, since=0, until=now_ts, now=now_ts
541
+ )
542
+ ho_accepted = local_sharding.accept_handover(
543
+ shard_id, ho_issued['chain'], peer_sharding.node_pk, now=now_ts
544
+ )
545
+ handovers_completed[shard_id] = ho_accepted
546
+
547
+ st_after = self.evaluate_window(
548
+ clean_pk,
549
+ measured=agg_counters,
550
+ claimed=agg_counters,
551
+ own_severity=worst_finding,
552
+ now=now_ts,
553
+ )
554
+
555
+ all_ok = all(
556
+ r['pull_status'] in ('complete', 'in_sync')
557
+ and r['push_status'] in ('complete', 'in_sync')
558
+ and r['fingerprint_match']
559
+ for r in shard_reports.values()
560
+ )
561
+ return {
562
+ 'ok': all_ok and st_after['final_severity'] == 'S0',
563
+ 'status': 'complete' if all_ok else 'incomplete',
564
+ 'standing_before': st_before,
565
+ 'standing_after': st_after,
566
+ 'shards': shard_reports,
567
+ 'handovers': handovers_completed,
568
+ }
@@ -0,0 +1,9 @@
1
+ """Local testnet tooling: a multi-node cluster on one machine and the in-process
2
+ FIPS daemon that stands in for fipsd. Development and operator rehearsal only -
3
+ nothing in the node runtime imports this package (test_imports holds that line);
4
+ the `meshnode cluster` verb is its one caller outside the tests.
5
+ """
6
+ from .cluster import ChaosSimulator, LocalCluster, TOPOLOGIES, generate_topology_edges
7
+ from .fips_daemon import ClusterFipsDaemon
8
+
9
+ __all__ = ['ChaosSimulator', 'LocalCluster', 'TOPOLOGIES', 'generate_topology_edges', 'ClusterFipsDaemon']