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,448 @@
1
+ """ShardCoverageManager: reusable Kind 30803 shard coverage claims, HandoverLink
2
+ provenance chains, hole auditing, and claimed-shard retention sweeps over
3
+ `ProjectedNodeStore`.
4
+
5
+ Wraps `fleetmesh_core.peering.coverage` and `node.planes.store.sync.rules` into
6
+ a single stateful manager backed by SQLite so any service or compartment using
7
+ `ProjectedNodeStore` gets:
8
+ - `coverage_claim(shard_id, ...)`: signed Kind 30803 claim with monotonic
9
+ `forward_clock` and automatic inline handover chain attachment when
10
+ `handover_rules.still_fits` holds.
11
+ - `issue_handover(shard_id, to_pubkey, ...)`: signed `HandoverLink` +
12
+ `handover_rules.extend` provenance chain + `sync/1.0/handover` envelope.
13
+ - `accept_handover(shard_id, chain, sender_pubkey, ...)`: validates inbound
14
+ chain via `handover_rules.acceptable`, persists it in
15
+ `projected_shard_handovers`, and publishes the replica's asserted claim.
16
+ - `audit_claim(claim, ...)`: checks `coverage.barred_reporter`,
17
+ `coverage.hole`, and `coverage.hole_in_handover`.
18
+ - `sweep_retention(ages_by_kind, ...)`: paginated age sweep that never evicts
19
+ events inside any claimed shard filter (`protected_filters()`).
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ from typing import Any, Callable, Mapping, Optional, Sequence, TypedDict, cast
25
+
26
+ from fleetmesh_core.peering import coverage
27
+ from fleetmesh_core.storage import replication, store
28
+ from fleetmesh_core.wire.wire_types import HandoverLink
29
+ from node.compartment.schema import Column, Table
30
+ from node.interfaces import NostrEvent, NostrFilter
31
+ from node.planes.store.sync import rules as handover_rules
32
+
33
+ from .projected import ProjectedNodeStore
34
+
35
+ __all__ = [
36
+ 'ClaimAuditReply',
37
+ 'HandoverAcceptReply',
38
+ 'HandoverIssueReply',
39
+ 'SHARD_HANDOVERS_TABLE',
40
+ 'ShardCoverageManager',
41
+ 'ShardSummaryRecord',
42
+ ]
43
+
44
+
45
+ class HandoverIssueReply(TypedDict):
46
+ shard_id: str
47
+ origin_claim: NostrEvent
48
+ link: HandoverLink
49
+ chain: list[HandoverLink]
50
+ handover_message: Mapping[str, Any]
51
+
52
+
53
+ class HandoverAcceptReply(TypedDict):
54
+ ok: bool
55
+ shard_id: str
56
+ from_pubkey: str
57
+ chain_length: int
58
+ fingerprint: str
59
+ count: int
60
+ replica_claim: NostrEvent
61
+ check_coverage_problems: list[str]
62
+
63
+
64
+ class ClaimAuditReply(TypedDict):
65
+ ok: bool
66
+ barred: bool
67
+ barred_reason: Optional[str]
68
+ holes: list[list[str]]
69
+ handover_holes: list[list[str]]
70
+
71
+
72
+ class ShardSummaryRecord(TypedDict):
73
+ shard_id: str
74
+ filter: NostrFilter
75
+ event_count: int
76
+ fingerprint: str
77
+ coverage_claim: NostrEvent
78
+ handover_chain: list[HandoverLink]
79
+
80
+
81
+ SHARD_HANDOVERS_TABLE = Table(
82
+ 'projected_shard_handovers',
83
+ (
84
+ Column('shard_id', 'TEXT', primary_key=True),
85
+ Column('from_pk', 'TEXT', not_null=True),
86
+ Column('since_ts', 'INTEGER', not_null=True),
87
+ Column('until_ts', 'INTEGER', not_null=True),
88
+ Column('fingerprint', 'TEXT', not_null=True),
89
+ Column('event_count', 'INTEGER', not_null=True),
90
+ Column('chain_json', 'TEXT', not_null=True),
91
+ Column('claim_json', 'TEXT', not_null=True),
92
+ Column('updated_at', 'INTEGER', not_null=True),
93
+ ),
94
+ )
95
+
96
+
97
+ class ShardCoverageManager:
98
+ """Manages Kind 30803 coverage claims, handover chains, hole audits, and
99
+ retention sweeps over a `ProjectedNodeStore`."""
100
+
101
+ def __init__(
102
+ self,
103
+ projected_store: ProjectedNodeStore,
104
+ *,
105
+ node_sk: str,
106
+ node_pk: Optional[str] = None,
107
+ extra_shards: Optional[Mapping[str, Mapping[str, Any]]] = None,
108
+ clock: Optional[Callable[[], int]] = None,
109
+ ) -> None:
110
+ self.store = projected_store
111
+ self.node_sk = node_sk.strip().lower()
112
+ from fleetmesh_core.crypto import crypto
113
+ self.node_pk = (node_pk or crypto.pubkey(self.node_sk)).strip().lower()
114
+ self._extra_shards: dict[str, NostrFilter] = {
115
+ k: cast(NostrFilter, dict(v)) for k, v in (extra_shards or {}).items()
116
+ }
117
+ self.clock: Callable[[], int] = clock or self.store.clock
118
+ self._handovers: dict[str, list[HandoverLink]] = {}
119
+ self._claims: dict[str, NostrEvent] = {}
120
+ self._ensure_schema()
121
+ self._load_persisted()
122
+
123
+ def _ensure_schema(self) -> None:
124
+ with self.store._lock:
125
+ if hasattr(self.store.dialect, 'learn_table'):
126
+ self.store.dialect.learn_table(SHARD_HANDOVERS_TABLE)
127
+ self.store.execute(SHARD_HANDOVERS_TABLE.to_sql(dialect=self.store.dialect.name))
128
+ for sec in SHARD_HANDOVERS_TABLE.secondary_sql(dialect=self.store.dialect.name):
129
+ self.store.execute(sec)
130
+ self.store.commit()
131
+
132
+ def _load_persisted(self) -> None:
133
+ with self.store._lock:
134
+ rows = self.store._all(
135
+ 'SELECT shard_id, chain_json, claim_json FROM projected_shard_handovers'
136
+ )
137
+ for sid, chain_raw, claim_raw in rows:
138
+ try:
139
+ self._handovers[str(sid)] = json.loads(str(chain_raw))
140
+ self._claims[str(sid)] = json.loads(str(claim_raw))
141
+ except ValueError:
142
+ continue
143
+
144
+ def shard_filters(self) -> dict[str, NostrFilter]:
145
+ """Return all registered shard filters from `ProjectedNodeStore` merged
146
+ with any `extra_shards`."""
147
+ merged = self.store.shard_filters()
148
+ for sid, filt in self._extra_shards.items():
149
+ merged[sid] = cast(NostrFilter, dict(filt))
150
+ return merged
151
+
152
+ def shard_filter(self, shard_id: str) -> NostrFilter:
153
+ filters = self.shard_filters()
154
+ if shard_id not in filters:
155
+ raise ValueError(f'Unknown shard_id: {shard_id!r}')
156
+ return filters[shard_id]
157
+
158
+ def handover_chain(self, shard_id: str) -> list[HandoverLink]:
159
+ with self.store._lock:
160
+ return [cast(HandoverLink, dict(x)) for x in self._handovers.get(shard_id, [])]
161
+
162
+ def coverage_claim(
163
+ self,
164
+ shard_id: str,
165
+ *,
166
+ since: int = 0,
167
+ until: Optional[int] = None,
168
+ completeness: str = 'asserted',
169
+ now: Optional[int] = None,
170
+ ) -> NostrEvent:
171
+ """Build and sign a Kind 30803 `coverage_claim` for `shard_id`."""
172
+ filt = self.shard_filter(shard_id)
173
+ now_ts = int(now if now is not None else self.clock())
174
+
175
+ with self.store._lock:
176
+ chain = self._handovers.get(shard_id)
177
+ if chain and until is None:
178
+ art = chain[-1]
179
+ h_since = int(art.get('since') or 0)
180
+ h_until = int(art['until'])
181
+ held_ids = self.store.enumerate_range(filt, h_since, h_until, max(now_ts, h_until))
182
+ if handover_rules.still_fits(chain, filt, held_ids) is None:
183
+ claim_ts = self.store.forward_clock(
184
+ f'{coverage.COVERAGE}:{self.node_pk}:{shard_id}',
185
+ max(now_ts, h_until),
186
+ )
187
+ unsigned = coverage.coverage_claim(
188
+ self.node_pk,
189
+ shard_id,
190
+ filt,
191
+ 'asserted',
192
+ held_ids,
193
+ claim_ts,
194
+ since=h_since if art.get('since') is not None else None,
195
+ until=h_until,
196
+ handover=chain,
197
+ )
198
+ signed = cast(
199
+ NostrEvent,
200
+ dict(
201
+ store.make_signed_event(
202
+ self.node_sk,
203
+ unsigned['created_at'],
204
+ unsigned['kind'],
205
+ unsigned['tags'],
206
+ unsigned['content'],
207
+ )
208
+ ),
209
+ )
210
+ self._claims[shard_id] = signed
211
+ return cast(NostrEvent, dict(signed))
212
+
213
+ until_ts = int(until) if until is not None else (now_ts + 1)
214
+ claim_ts = self.store.forward_clock(
215
+ f'{coverage.COVERAGE}:{self.node_pk}:{shard_id}',
216
+ max(now_ts, until_ts),
217
+ )
218
+ ids = self.store.enumerate_range(filt, since, until_ts, claim_ts)
219
+ unsigned = coverage.coverage_claim(
220
+ self.node_pk,
221
+ shard_id,
222
+ filt,
223
+ completeness,
224
+ ids,
225
+ claim_ts,
226
+ since=since,
227
+ until=until_ts,
228
+ )
229
+ signed = cast(
230
+ NostrEvent,
231
+ dict(
232
+ store.make_signed_event(
233
+ self.node_sk,
234
+ unsigned['created_at'],
235
+ unsigned['kind'],
236
+ unsigned['tags'],
237
+ unsigned['content'],
238
+ )
239
+ ),
240
+ )
241
+ self._claims[shard_id] = signed
242
+ return cast(NostrEvent, dict(signed))
243
+
244
+ def issue_handover(
245
+ self,
246
+ shard_id: str,
247
+ to_pubkey: str,
248
+ *,
249
+ since: int = 0,
250
+ until: Optional[int] = None,
251
+ now: Optional[int] = None,
252
+ ) -> HandoverIssueReply:
253
+ """Sign a `HandoverLink` (`coverage.handover`) over `[since, until)` for
254
+ `shard_id` to `to_pubkey`, extending any held upstream handover chain via
255
+ `handover_rules.extend`."""
256
+ filt = self.shard_filter(shard_id)
257
+ clean_to = (to_pubkey or '').strip().lower()
258
+ if not store.is_hex64(clean_to):
259
+ raise ValueError('to_pubkey must be a 64-character lowercase hex Nostr pubkey')
260
+
261
+ now_ts = int(now if now is not None else self.clock()) + 2
262
+ until_ts = int(until) if until is not None else now_ts
263
+ if until_ts > now_ts:
264
+ now_ts = until_ts
265
+
266
+ with self.store._lock:
267
+ held_chain = self._handovers.get(shard_id)
268
+ if held_chain and until is None:
269
+ art = held_chain[-1]
270
+ h_since = int(art.get('since') or 0)
271
+ h_until = int(art['until'])
272
+ held_ids = self.store.enumerate_range(filt, h_since, h_until, max(now_ts, h_until))
273
+ if handover_rules.still_fits(held_chain, filt, held_ids) is None:
274
+ origin_claim = self.coverage_claim(shard_id, now=max(now_ts, h_until))
275
+ link = coverage.handover(self.node_sk, clean_to, origin_claim, max(now_ts, h_until))
276
+ chain, why_not = handover_rules.extend(held_chain, link)
277
+ if why_not is not None or chain is None:
278
+ raise ValueError(f'handover chain extension refused: {why_not}')
279
+ msg = coverage.handover_message(chain, thid=f'sync-{shard_id}')
280
+ return {
281
+ 'shard_id': shard_id,
282
+ 'origin_claim': origin_claim,
283
+ 'link': cast(HandoverLink, dict(link)),
284
+ 'chain': [cast(HandoverLink, dict(c)) for c in chain],
285
+ 'handover_message': dict(msg),
286
+ }
287
+
288
+ origin_claim = self.coverage_claim(
289
+ shard_id,
290
+ since=since,
291
+ until=until_ts,
292
+ completeness='asserted',
293
+ now=now_ts,
294
+ )
295
+ link = coverage.handover(self.node_sk, clean_to, origin_claim, now_ts)
296
+ chain_out = [cast(HandoverLink, dict(link))]
297
+ msg = coverage.handover_message(chain_out, thid=f'sync-{shard_id}')
298
+ return {
299
+ 'shard_id': shard_id,
300
+ 'origin_claim': origin_claim,
301
+ 'link': cast(HandoverLink, dict(link)),
302
+ 'chain': chain_out,
303
+ 'handover_message': dict(msg),
304
+ }
305
+
306
+ def accept_handover(
307
+ self,
308
+ shard_id: str,
309
+ chain: Sequence[Mapping[str, Any]],
310
+ sender_pubkey: str,
311
+ *,
312
+ now: Optional[int] = None,
313
+ ) -> HandoverAcceptReply:
314
+ """Validate an inbound `HandoverLink` chain using `handover_rules.acceptable`,
315
+ persist it, and publish this replica's `asserted` Kind 30803 claim."""
316
+ filt = self.shard_filter(shard_id)
317
+ clean_sender = (sender_pubkey or '').strip().lower()
318
+ if not chain:
319
+ raise ValueError('handover chain is empty')
320
+
321
+ art = chain[-1]
322
+ h_since = int(art.get('since') or 0)
323
+ h_until = int(art.get('until') or 0)
324
+ now_ts = max(int(now if now is not None else self.clock()), h_until)
325
+
326
+ with self.store._lock:
327
+ held_ids = self.store.enumerate_range(filt, h_since, h_until, now_ts)
328
+ shard_cfg = {'shard': shard_id, 'filter': filt}
329
+ why_not = handover_rules.acceptable(
330
+ chain,
331
+ self.node_pk,
332
+ clean_sender,
333
+ shard_cfg,
334
+ held_ids,
335
+ )
336
+ if why_not is not None:
337
+ raise ValueError(f'handover_refused: {why_not}')
338
+
339
+ clean_chain = [cast(HandoverLink, dict(link)) for link in chain]
340
+ self._handovers[shard_id] = clean_chain
341
+ replica_claim = self.coverage_claim(shard_id, now=now_ts)
342
+ problems = coverage.check_coverage(replica_claim)
343
+ if problems:
344
+ del self._handovers[shard_id]
345
+ raise ValueError(f"replica coverage claim invalid: {'; '.join(problems)}")
346
+
347
+ self.store.execute(
348
+ """INSERT OR REPLACE INTO projected_shard_handovers
349
+ (shard_id, from_pk, since_ts, until_ts, fingerprint, event_count, chain_json, claim_json, updated_at)
350
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
351
+ (
352
+ shard_id,
353
+ clean_sender,
354
+ h_since,
355
+ h_until,
356
+ str(art['fingerprint']),
357
+ int(art['count']),
358
+ json.dumps(clean_chain, sort_keys=True),
359
+ json.dumps(replica_claim, sort_keys=True),
360
+ now_ts,
361
+ ),
362
+ )
363
+ self.store.commit()
364
+ return {
365
+ 'ok': True,
366
+ 'shard_id': shard_id,
367
+ 'from_pubkey': clean_sender,
368
+ 'chain_length': len(clean_chain),
369
+ 'fingerprint': str(art['fingerprint']),
370
+ 'count': int(art['count']),
371
+ 'replica_claim': replica_claim,
372
+ 'check_coverage_problems': problems,
373
+ }
374
+
375
+ def audit_claim(
376
+ self,
377
+ claim: Mapping[str, Any],
378
+ *,
379
+ claimant_store: Optional[ProjectedNodeStore] = None,
380
+ reference_events: Optional[Sequence[Mapping[str, Any]]] = None,
381
+ reporter_pubkey: Optional[str] = None,
382
+ ) -> ClaimAuditReply:
383
+ """Audit an `asserted` Kind 30803 `claim` for missing events (`hole` and
384
+ `hole_in_handover`) and check `coverage.barred_reporter`."""
385
+ rep_pk = (reporter_pubkey or self.node_pk).strip().lower()
386
+ bar_reason = coverage.barred_reporter(claim, rep_pk)
387
+ if bar_reason is not None:
388
+ return {
389
+ 'ok': False,
390
+ 'barred': True,
391
+ 'barred_reason': bar_reason,
392
+ 'holes': [],
393
+ 'handover_holes': [],
394
+ }
395
+
396
+ target_store = claimant_store if claimant_store is not None else self.store
397
+ held_fn = lambda eid: target_store.get_event(eid) is not None
398
+
399
+ if reference_events is None:
400
+ f_tag = store.first_tag(claim, 'filter')
401
+ filt = json.loads(f_tag[1]) if (f_tag and len(f_tag) > 1) else {}
402
+ s_tag = store.first_tag(claim, 'since')
403
+ u_tag = store.first_tag(claim, 'until')
404
+ s_ts = int(s_tag[1]) if (s_tag and len(s_tag) > 1) else 0
405
+ u_ts = int(u_tag[1]) if (u_tag and len(u_tag) > 1) else (self.clock() + 120)
406
+ eids = self.store.enumerate_range(filt, s_ts, u_ts, u_ts)
407
+ reference_events = [
408
+ ev for eid in eids
409
+ if (ev := self.store.get_event(eid)) is not None
410
+ ]
411
+
412
+ holes: list[list[str]] = []
413
+ handover_holes: list[list[str]] = []
414
+ inline_ho = coverage.inline_handover(claim)
415
+ for ev in reference_events:
416
+ h_ev = coverage.hole(claim, ev, held_fn)
417
+ if h_ev is not None:
418
+ holes.append(h_ev)
419
+ if inline_ho is not None:
420
+ ho_ev = coverage.hole_in_handover(inline_ho, ev, held_fn)
421
+ if ho_ev is not None:
422
+ handover_holes.append(ho_ev)
423
+
424
+ return {
425
+ 'ok': len(holes) == 0,
426
+ 'barred': False,
427
+ 'barred_reason': None,
428
+ 'holes': holes,
429
+ 'handover_holes': handover_holes,
430
+ }
431
+
432
+ def summary(self, *, now: Optional[int] = None) -> dict[str, ShardSummaryRecord]:
433
+ """Return a per-shard summary with event count, NIP-77 fingerprint,
434
+ coverage claim, and active handover chain."""
435
+ now_ts = int(now if now is not None else self.clock()) + 60
436
+ out: dict[str, ShardSummaryRecord] = {}
437
+ for shard_id, filt in self.shard_filters().items():
438
+ claim = self.coverage_claim(shard_id, now=now_ts)
439
+ ids = self.store.enumerate_range(filt, 0, now_ts, now_ts)
440
+ out[shard_id] = {
441
+ 'shard_id': shard_id,
442
+ 'filter': filt,
443
+ 'event_count': len(ids),
444
+ 'fingerprint': replication.fingerprint(ids),
445
+ 'coverage_claim': claim,
446
+ 'handover_chain': self.handover_chain(shard_id),
447
+ }
448
+ return out