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,433 @@
1
+ """Compute market events: offers (11803), asks (30810), order-book parsing and matching.
2
+
3
+ Separate because the market layer is pure event construction and matching and shares nothing with execution.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import time
8
+ from typing import Any, Mapping, Optional, Sequence
9
+
10
+ from fleetmesh_core.accounting import market
11
+ from fleetmesh_core.storage import store
12
+ from fleetmesh_core.wire.interfaces import NostrEvent
13
+ from node import keystore
14
+ from .common import ComputeError, OFFER_KIND, ASK_KIND, DEFAULT_EXPIRATION
15
+
16
+ # -------------------------------------------------------------------------
17
+ # Kind 11803: Compute Offer
18
+ # -------------------------------------------------------------------------
19
+ def build_offer(
20
+ ks: keystore.Keystore,
21
+ executors: Sequence[Sequence[str]],
22
+ limits: Optional[Sequence[Sequence[str]]] = None,
23
+ runtimes: Optional[Sequence[Sequence[str]]] = None,
24
+ pay: Optional[Sequence[Sequence[str]]] = None,
25
+ expiration: Optional[int] = None,
26
+ now: Optional[int] = None,
27
+ ) -> NostrEvent:
28
+ """Build a signed kind 11803 compute offer event.
29
+
30
+ Content MUST be empty. At least one executor tag is required.
31
+ """
32
+ now_val = int(now if now is not None else time.time())
33
+ exp_val = int(expiration if expiration is not None else now_val + DEFAULT_EXPIRATION)
34
+
35
+ if not executors:
36
+ raise ComputeError('at least one executor tag is required')
37
+
38
+ tags: list[list[str]] = []
39
+ for ex in executors:
40
+ tags.append(['executor'] + list(ex))
41
+
42
+ for lim in limits or []:
43
+ tags.append(['limit'] + list(lim))
44
+
45
+ for rt in runtimes or []:
46
+ tags.append(['runtime'] + list(rt))
47
+
48
+ for p in pay or []:
49
+ tags.append(['pay'] + list(p))
50
+
51
+ tags.append(['expiration', str(exp_val)])
52
+
53
+ ev = store.make_signed_event(ks.node_sk, now_val, OFFER_KIND, tags, '')
54
+ return ev
55
+
56
+
57
+ def validate_offer(offer: NostrEvent | Mapping[str, Any]) -> list[str]:
58
+ """Validate that an offer complies with kind 11803 constraints."""
59
+ problems: list[str] = []
60
+ if offer.get('kind') != OFFER_KIND:
61
+ problems.append('not a kind 11803')
62
+ if offer.get('content') != '':
63
+ problems.append('compute offer content must be empty')
64
+
65
+ tags = offer.get('tags', [])
66
+ has_exec = any(t and t[0] == 'executor' and len(t) >= 2 for t in tags)
67
+ if not has_exec:
68
+ problems.append('missing required executor tag')
69
+
70
+ has_exp = any(t and t[0] == 'expiration' and len(t) == 2 for t in tags)
71
+ if not has_exp:
72
+ problems.append('missing required expiration tag')
73
+
74
+ return problems
75
+
76
+
77
+ # -------------------------------------------------------------------------
78
+ # Kind 30810: Compute Ask
79
+ # -------------------------------------------------------------------------
80
+ def build_ask(
81
+ buyer_sk: str,
82
+ ask_id: str,
83
+ job_payload: Mapping[str, Any],
84
+ executor: str,
85
+ bid_currency: str,
86
+ bid_price: int,
87
+ deadline: int,
88
+ fuel_max: int,
89
+ verify_mode: str,
90
+ expiration: Optional[int] = None,
91
+ verify_param: Optional[str] = None,
92
+ extra_tags: Optional[Sequence[Sequence[str]]] = None,
93
+ now: Optional[int] = None,
94
+ ) -> NostrEvent:
95
+ """Build a signed kind 30810 compute ask event.
96
+
97
+ Content MUST be empty. Module and input MUST NOT be named in tags to preserve
98
+ job confidentiality on public relays.
99
+ """
100
+ now_val = int(now if now is not None else time.time())
101
+ exp_val = int(expiration if expiration is not None else now_val + DEFAULT_EXPIRATION)
102
+
103
+ if verify_mode not in market.MODES:
104
+ raise ComputeError('unknown verify mode: %r' % (verify_mode,))
105
+
106
+ commit = market.ask_commitment(job_payload)
107
+
108
+ tags: list[list[str]] = [
109
+ ['d', ask_id],
110
+ ['commit', commit],
111
+ ['executor', executor],
112
+ ['bid', bid_currency, str(int(bid_price))],
113
+ ['deadline', str(int(deadline))],
114
+ ['fuel_max', str(int(fuel_max))],
115
+ ]
116
+
117
+ if verify_param:
118
+ tags.append(['verify', verify_mode, verify_param])
119
+ else:
120
+ tags.append(['verify', verify_mode])
121
+
122
+ tags.append(['expiration', str(exp_val)])
123
+
124
+ for extra in extra_tags or []:
125
+ if extra and extra[0] in ('module', 'input'):
126
+ raise ComputeError('ask must not leak module or input in tags')
127
+ tags.append(list(extra))
128
+
129
+ ev = store.make_signed_event(buyer_sk, now_val, ASK_KIND, tags, '')
130
+ return ev
131
+
132
+
133
+ def verify_ask(ask: NostrEvent | Mapping[str, Any], job_payload: Mapping[str, Any]) -> bool:
134
+ """Check that a received job payload matches the commitment in the ask."""
135
+ if ask.get('kind') != ASK_KIND:
136
+ return False
137
+ # market.* take a concrete dict; copying a Mapping into one is cheap and
138
+ # keeps the read-only contract this function advertises to callers.
139
+ ask_dict = dict(ask)
140
+ if market.leaks_job(ask_dict):
141
+ return False
142
+ return market.commitment_holds(ask_dict, job_payload)
143
+
144
+
145
+ # -------------------------------------------------------------------------
146
+ # Order-Book Parsing & Matching Engine
147
+ # -------------------------------------------------------------------------
148
+ def parse_offer(offer: NostrEvent | Mapping[str, Any]) -> Mapping[str, Any]:
149
+ """Extract structured offer parameters from a kind 11803 event."""
150
+ tags = offer.get('tags', [])
151
+ executors = []
152
+ limits: dict[str, int] = {}
153
+ runtimes: list[dict[str, str]] = []
154
+ pay: list[str] = []
155
+ expiration: int = 0
156
+
157
+ for t in tags:
158
+ if not t or not isinstance(t, (list, tuple)):
159
+ continue
160
+ # Each arm binds the tag's positional fields by shape; a tag that is
161
+ # too short for its name simply falls through and is ignored, as the
162
+ # spec allows unknown or malformed tags on an offer.
163
+ match list(t):
164
+ case ['executor', engine, *rest]:
165
+ unit = str(rest[0]) if rest else 'msat_per_mfuel'
166
+ try:
167
+ rate = int(rest[1]) if len(rest) > 1 else 0
168
+ except ValueError:
169
+ rate = 0
170
+ executors.append({'engine': str(engine), 'unit': unit, 'rate': rate})
171
+ case ['limit', key, val, *_]:
172
+ try:
173
+ limits[str(key)] = int(val)
174
+ except ValueError:
175
+ pass
176
+ case ['runtime', engine, version, digest, *_]:
177
+ runtimes.append({
178
+ 'engine': str(engine),
179
+ 'version': str(version),
180
+ 'wasi_profile_digest': str(digest),
181
+ })
182
+ case ['pay', method, *_]:
183
+ pay.append(str(method))
184
+ case ['expiration', raw, *_]:
185
+ try:
186
+ expiration = int(raw)
187
+ except ValueError:
188
+ expiration = 0
189
+ case _:
190
+ pass
191
+
192
+ return {
193
+ 'id': offer.get('id', ''),
194
+ 'pubkey': offer.get('pubkey', ''),
195
+ 'created_at': offer.get('created_at', 0),
196
+ 'executors': executors,
197
+ 'limits': limits,
198
+ 'runtimes': runtimes,
199
+ 'pay': pay,
200
+ 'expiration': expiration,
201
+ }
202
+
203
+
204
+ def parse_ask(ask: NostrEvent | Mapping[str, Any]) -> Mapping[str, Any]:
205
+ """Extract structured ask parameters from a kind 30810 event."""
206
+ tags = ask.get('tags', [])
207
+ ask_id: str = ''
208
+ commit: str = ''
209
+ executor: str = ''
210
+ bid_currency: str = 'msat_per_mfuel'
211
+ bid_price: int = 0
212
+ deadline: int = 0
213
+ fuel_max: int = 0
214
+ verify_mode: str = 'none'
215
+ verify_param: Optional[str] = None
216
+ expiration: int = 0
217
+ runtime_digest: Optional[str] = None
218
+
219
+ for t in tags:
220
+ if not t or not isinstance(t, (list, tuple)):
221
+ continue
222
+ # Shape-matched like parse_offer: a too-short tag falls through.
223
+ match list(t):
224
+ case ['d', raw, *_]:
225
+ ask_id = str(raw)
226
+ case ['commit', raw, *_]:
227
+ commit = str(raw)
228
+ case ['executor', raw, *_]:
229
+ executor = str(raw)
230
+ case ['bid', currency, price, *_]:
231
+ bid_currency = str(currency)
232
+ try:
233
+ bid_price = int(price)
234
+ except ValueError:
235
+ bid_price = 0
236
+ case ['deadline', raw, *_]:
237
+ try:
238
+ deadline = int(raw)
239
+ except ValueError:
240
+ deadline = 0
241
+ case ['fuel_max', raw, *_]:
242
+ try:
243
+ fuel_max = int(raw)
244
+ except ValueError:
245
+ fuel_max = 0
246
+ case ['verify', mode, *rest]:
247
+ verify_mode = str(mode)
248
+ if rest:
249
+ verify_param = str(rest[0])
250
+ case ['runtime', _, _, digest, *_]:
251
+ runtime_digest = str(digest)
252
+ case ['expiration', raw, *_]:
253
+ try:
254
+ expiration = int(raw)
255
+ except ValueError:
256
+ expiration = 0
257
+ case _:
258
+ pass
259
+
260
+ return {
261
+ 'id': ask.get('id', ''),
262
+ 'pubkey': ask.get('pubkey', ''),
263
+ 'created_at': ask.get('created_at', 0),
264
+ 'ask_id': ask_id,
265
+ 'commit': commit,
266
+ 'executor': executor,
267
+ 'bid_currency': bid_currency,
268
+ 'bid_price': bid_price,
269
+ 'deadline': deadline,
270
+ 'fuel_max': fuel_max,
271
+ 'verify_mode': verify_mode,
272
+ 'verify_param': verify_param,
273
+ 'runtime_digest': runtime_digest,
274
+ 'expiration': expiration,
275
+ }
276
+
277
+
278
+ def match_ask_to_offers(
279
+ ask: NostrEvent | Mapping[str, Any],
280
+ offers: Sequence[NostrEvent | Mapping[str, Any]],
281
+ trust_weights: Optional[Mapping[str, float]] = None,
282
+ now: Optional[int] = None,
283
+ ) -> list[Mapping[str, Any]]:
284
+ """Match a compute ask against candidate compute offers in the order book.
285
+
286
+ Returns candidate matches ranked by:
287
+ 1. Zero-cost / free offers first (volunteer / spare cycles),
288
+ 2. Lowest rate amount (msat_per_mfuel),
289
+ 3. Lowest reported CPU load percentage (cpu_load_pct),
290
+ 4. Highest trust weight / reputation standing,
291
+ 5. Furthest expiration deadline.
292
+ """
293
+ now_val = int(now if now is not None else time.time())
294
+ ask_p = parse_ask(ask)
295
+ matches: list[Mapping[str, Any]] = []
296
+
297
+ for off in offers:
298
+ off_p = parse_offer(off)
299
+ # 1. Expiration check
300
+ if off_p['expiration'] and off_p['expiration'] < now_val:
301
+ continue
302
+ if ask_p['deadline'] and off_p['expiration'] and off_p['expiration'] < ask_p['deadline']:
303
+ continue
304
+
305
+ # 2. Fuel limit and CPU/slot headroom check
306
+ if 'fuel_max' in off_p['limits'] and off_p['limits']['fuel_max'] < ask_p['fuel_max']:
307
+ continue
308
+ if 'free_slots' in off_p['limits'] and int(off_p['limits']['free_slots']) <= 0:
309
+ continue
310
+ if 'cpu_load_pct' in off_p['limits'] and int(off_p['limits']['cpu_load_pct']) >= 85:
311
+ continue
312
+
313
+ # 3. Match executor engine
314
+ matched_exec = None
315
+ for ex in off_p['executors']:
316
+ if ex['engine'] == ask_p['executor'] or (
317
+ ask_p['executor'] == 'wasm' and ex['engine'] in ('wasm', 'wasmtime', 'wasmer')
318
+ ) or (
319
+ ask_p['executor'] in ('ffmpeg', 'media-transcode', 'video-transcode', 'transcode')
320
+ and ex['engine'] in ('ffmpeg', 'media-transcode', 'video-transcode', 'transcode')
321
+ ):
322
+ matched_exec = ex
323
+ break
324
+ if not matched_exec:
325
+ continue
326
+
327
+ # 4. Price check: offer rate must not exceed ask bid (or offer is free: rate == 0)
328
+ offer_rate = matched_exec['rate']
329
+ if offer_rate > ask_p['bid_price'] and offer_rate != 0:
330
+ continue
331
+
332
+ # 5. Runtime digest check if specified in both
333
+ if ask_p['runtime_digest']:
334
+ has_matching_rt = any(
335
+ rt.get('wasi_profile_digest') == ask_p['runtime_digest']
336
+ for rt in off_p['runtimes']
337
+ )
338
+ if off_p['runtimes'] and not has_matching_rt:
339
+ continue
340
+
341
+ # Price estimation
342
+ fuel = ask_p['fuel_max'] or 1000000
343
+ est_cost_msat = (fuel * offer_rate) // 1000000 if offer_rate > 0 else 0
344
+
345
+ seller_pk = off_p['pubkey']
346
+ seller_trust = float(trust_weights.get(seller_pk, 0.0)) if trust_weights else 0.0
347
+ cpu_load_pct = int(off_p['limits'].get('cpu_load_pct', 0))
348
+ free_slots = int(off_p['limits'].get('free_slots', 1))
349
+
350
+ matches.append({
351
+ 'offer_id': off_p['id'],
352
+ 'seller_pubkey': seller_pk,
353
+ 'executor': matched_exec['engine'],
354
+ 'rate_unit': matched_exec['unit'],
355
+ 'rate': offer_rate,
356
+ 'estimated_cost_msat': est_cost_msat,
357
+ 'is_free': (offer_rate == 0),
358
+ 'cpu_load_pct': cpu_load_pct,
359
+ 'free_slots': free_slots,
360
+ 'seller_trust': seller_trust,
361
+ 'expiration': off_p['expiration'],
362
+ 'offer_event': off,
363
+ })
364
+
365
+ # Sort matches: free offers first (0 vs 1), lowest rate, lowest CPU load, highest trust, furthest expiration
366
+ matches.sort(
367
+ key=lambda m: (
368
+ 0 if m['is_free'] else 1,
369
+ m['rate'],
370
+ m['cpu_load_pct'],
371
+ -m['seller_trust'],
372
+ -m['expiration'],
373
+ )
374
+ )
375
+ return matches
376
+
377
+
378
+ def match_offer_to_asks(
379
+ offer: NostrEvent | Mapping[str, Any],
380
+ asks: Sequence[NostrEvent | Mapping[str, Any]],
381
+ now: Optional[int] = None,
382
+ ) -> list[Mapping[str, Any]]:
383
+ """Match a compute offer against pending compute asks in the order book."""
384
+ now_val = int(now if now is not None else time.time())
385
+ off_p = parse_offer(offer)
386
+ if off_p['expiration'] and off_p['expiration'] < now_val:
387
+ return []
388
+
389
+ matches: list[Mapping[str, Any]] = []
390
+ for ask in asks:
391
+ ask_p = parse_ask(ask)
392
+ if ask_p['expiration'] and ask_p['expiration'] < now_val:
393
+ continue
394
+ if ask_p['deadline'] and off_p['expiration'] and off_p['expiration'] < ask_p['deadline']:
395
+ continue
396
+ if 'fuel_max' in off_p['limits'] and off_p['limits']['fuel_max'] < ask_p['fuel_max']:
397
+ continue
398
+
399
+ matched_exec = None
400
+ for ex in off_p['executors']:
401
+ if ex['engine'] == ask_p['executor'] or (
402
+ ask_p['executor'] == 'wasm' and ex['engine'] in ('wasm', 'wasmtime', 'wasmer')
403
+ ) or (
404
+ ask_p['executor'] in ('ffmpeg', 'media-transcode', 'video-transcode', 'transcode')
405
+ and ex['engine'] in ('ffmpeg', 'media-transcode', 'video-transcode', 'transcode')
406
+ ):
407
+ matched_exec = ex
408
+ break
409
+ if not matched_exec:
410
+ continue
411
+
412
+ offer_rate = matched_exec['rate']
413
+ if offer_rate > ask_p['bid_price'] and offer_rate != 0:
414
+ continue
415
+
416
+ fuel = ask_p['fuel_max'] or 1000000
417
+ est_cost_msat = (fuel * offer_rate) // 1000000 if offer_rate > 0 else 0
418
+
419
+ matches.append({
420
+ 'ask_id': ask_p['ask_id'],
421
+ 'buyer_pubkey': ask_p['pubkey'],
422
+ 'executor': matched_exec['engine'],
423
+ 'bid_price': ask_p['bid_price'],
424
+ 'rate': offer_rate,
425
+ 'estimated_revenue_msat': est_cost_msat,
426
+ 'is_free': (offer_rate == 0),
427
+ 'deadline': ask_p['deadline'],
428
+ 'ask_event': ask,
429
+ })
430
+
431
+ # Sort asks: highest bid first, nearest deadline
432
+ matches.sort(key=lambda m: (-m['bid_price'], m['deadline']))
433
+ return matches