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,338 @@
1
+ #!/usr/bin/env python3
2
+ """The libp2p half of the announce bridge, in its own process.
3
+
4
+ fleetmesh-announce DIR [--bootstrap MULTIADDR]... [--port N] [--pow-bits N]
5
+
6
+ Joins the gossipsub topic `/fleetmesh/announce/1`, republishes the descriptor
7
+ the node hands it, and hands back the descriptors it hears. One JSON object
8
+ per line over a unix socket in the node's directory, the protocol
9
+ `fleetmesh-announce/1`, the same shape `node/control.py` uses.
10
+
11
+ Why a separate process. `fleetmesh_core/` is standard library only and `node/` depends on
12
+ `cryptography` and nothing else. Installing the one maintained Python libp2p
13
+ pulls in sixty-three packages, eight of them compiled; it will not install at
14
+ all on Python 3.14 today; and on macOS it installs and then fails to import
15
+ without a system GMP for `fastecdsa`. A second implementer reading the
16
+ specification should inherit none of that to build a node, so it is here, and
17
+ a node without it is a node with one discovery bridge instead of two.
18
+
19
+ **This process holds the transport key and nothing else.** It reads
20
+ `transport_seed` out of the node's `keys.json` and never touches `node_sk`.
21
+ The specification says the transport key MAY rotate freely and that a
22
+ verifier binds it to a DID only through a live descriptor, so a compromise
23
+ here costs one descriptor republish. The node key, which never rotates,
24
+ stays in the node.
25
+
26
+ **What this validates, and why here.** A gossipsub topic is a flood channel
27
+ with no grant in front of it, so the only thing standing between the mesh and
28
+ descriptor spam is the validator that runs before propagation.
29
+
30
+ `node.announce.compartment.admit` is that validator, and it lives there rather than here
31
+ because it needs nothing libp2p has: a size, a parse, a kind, a signature,
32
+ `fleetmesh_core.identity.validate_descriptor`, a NIP-13 floor, and last the tie between
33
+ the descriptor's `key transport` Multikey and the originator's PeerId. The
34
+ node runs the same function again on what this process hands over, so a check
35
+ that exists in two places cannot come to disagree with itself.
36
+
37
+ The originator is `msg.from_id` and never the peer who forwarded the message.
38
+ A forwarder does not re-sign under StrictSign, and a descriptor relayed by a
39
+ third node is still valid on a flood topic.
40
+ """
41
+ from __future__ import annotations
42
+
43
+ import argparse
44
+ import collections
45
+ import json
46
+ import os
47
+ import stat
48
+ import sys
49
+ import time
50
+ from typing import Any, Deque, Mapping, Optional, TypedDict
51
+
52
+ import multiaddr
53
+ import trio
54
+ from libp2p import new_host
55
+ from libp2p.crypto.ed25519 import create_new_key_pair
56
+ from libp2p.custom_types import TProtocol
57
+ from libp2p.peer.id import ID
58
+ from libp2p.peer.peerinfo import info_from_p2p_addr
59
+ from libp2p.pubsub.gossipsub import GossipSub
60
+ from libp2p.pubsub.pubsub import Pubsub
61
+ from libp2p.tools.anyio_service import background_trio_service
62
+
63
+ _ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
64
+ sys.path.insert(0, _ROOT)
65
+ from node.planes.identity.announce.compartment import (KIND, MAX_LINE, PAYLOAD_MAX, PROTOCOL, # noqa: E402
66
+ TOPIC, admit, socket_path)
67
+
68
+ GOSSIPSUB: TProtocol = TProtocol('/meshsub/1.0.0')
69
+ # Descriptors held for the node to drain. The node ticks every thirty seconds
70
+ # and a flood must not outgrow the interval, so the oldest goes and the loss
71
+ # is counted rather than hidden.
72
+ HEARD_MAX: int = 256
73
+ # The control socket: a client that connects and does not finish a line
74
+ # within this many seconds is dropped, so it cannot hold a task open.
75
+ CONTROL_TIMEOUT: float = 10.0
76
+
77
+
78
+ def transport_seed(directory: str) -> bytes:
79
+ """The Ed25519 seed out of the node's key file, and nothing else from it."""
80
+ path = os.path.join(directory, 'keys.json')
81
+ mode = stat.S_IMODE(os.stat(path).st_mode)
82
+ if mode & 0o077:
83
+ raise PermissionError('%s is readable by others (mode %o); refusing to load' % (path, mode))
84
+ with open(path, encoding='utf-8') as f:
85
+ d = json.load(f)
86
+ if d.get('format') != 'meshnode-keys/1':
87
+ raise ValueError('not a meshnode key file')
88
+ seed = bytes.fromhex(d['transport_seed'])
89
+ if len(seed) != 32:
90
+ raise ValueError('the transport seed is 32 bytes')
91
+ return seed
92
+
93
+
94
+ AnnounceHeardRecord = TypedDict('AnnounceHeardRecord', {
95
+ 'descriptor': Any,
96
+ 'from': str,
97
+ 'at': int,
98
+ }, total=False)
99
+
100
+
101
+ class AnnounceStatusReport(TypedDict, total=False):
102
+ """The report Announce.status() gives of itself."""
103
+ topic: str
104
+ peer_id: Optional[str]
105
+ mesh_peers: int
106
+ pow_bits: int
107
+ accepted: int
108
+ published: int
109
+ waiting: int
110
+ overflowed: int
111
+
112
+
113
+ class Announce:
114
+ """The topic, what it carried, and what it refused."""
115
+
116
+ def __init__(self, pow_bits: int = 0, now: Any = None) -> None:
117
+ self.pow_bits: int = max(0, int(pow_bits))
118
+ self.now = now or (lambda: int(time.time()))
119
+ self.heard: Deque[AnnounceHeardRecord] = collections.deque(maxlen=HEARD_MAX)
120
+ self.dropped: collections.Counter[str] = collections.Counter()
121
+ self.accepted: int = 0
122
+ self.published: int = 0
123
+ self.overflowed: int = 0
124
+ self.pubsub: Optional[Pubsub] = None
125
+ self.host: Any = None
126
+
127
+ # ---- the validator, run before anything is propagated -------------------
128
+ def validate(self, msg_forwarder: ID, msg: Any) -> bool:
129
+ """gossipsub's pre-propagation hook. False stops the message dead.
130
+
131
+ `msg_forwarder` is the peer who handed it over and is deliberately
132
+ unused: under StrictSign the signature is the *originator's*, carried
133
+ in `msg.from_id`, and a forwarder does not re-sign. A descriptor
134
+ relayed by a third node is still valid on a flood topic, so the tie
135
+ to check is between the descriptor and whoever signed it.
136
+ """
137
+ origin = self._ed25519_of(ID(msg.from_id))
138
+ if origin is None:
139
+ self.dropped['originator is not an Ed25519 peer'] += 1
140
+ return False
141
+ desc, why = admit(msg.data, origin, self.now(), pow_bits=self.pow_bits,
142
+ payload_max=PAYLOAD_MAX)
143
+ if desc is None:
144
+ self.dropped[str(why)] += 1
145
+ return False
146
+ if len(self.heard) == self.heard.maxlen:
147
+ self.overflowed += 1
148
+ self.heard.append({'descriptor': desc, 'from': origin, 'at': self.now()})
149
+ self.accepted += 1
150
+ return True
151
+
152
+ @staticmethod
153
+ def _ed25519_of(peer: ID) -> Optional[str]:
154
+ """The raw Ed25519 public key a PeerId carries, as hex.
155
+
156
+ An Ed25519 key is small enough that libp2p puts it in the PeerId
157
+ inline, under an identity multihash, so this needs no lookup. Anything
158
+ else - an RSA peer, whose id is a SHA-256 - returns None and is
159
+ refused, because fleetmesh nodes are Ed25519 by the key table.
160
+ """
161
+ try:
162
+ pub = peer.extract_public_key()
163
+ except Exception:
164
+ return None
165
+ if pub is None:
166
+ return None
167
+ raw = pub.to_bytes()
168
+ return raw.hex() if len(raw) == 32 else None
169
+
170
+ # ---- the socket ---------------------------------------------------------
171
+ async def serve(self, path: str) -> None:
172
+ # Refuse to take a socket another instance is answering on; a stale
173
+ # file left by a crash is unlinked.
174
+ if os.path.exists(path):
175
+ probe = trio.socket.socket(trio.socket.AF_UNIX, trio.socket.SOCK_STREAM)
176
+ try:
177
+ await probe.connect(path)
178
+ except OSError:
179
+ os.unlink(path)
180
+ else:
181
+ raise RuntimeError('another fleetmesh-announce is already serving %s' % path)
182
+ finally:
183
+ probe.close()
184
+ s = trio.socket.socket(trio.socket.AF_UNIX, trio.socket.SOCK_STREAM)
185
+ old = os.umask(0o177)
186
+ try:
187
+ await s.bind(path)
188
+ os.chmod(path, 0o600)
189
+ finally:
190
+ os.umask(old)
191
+ s.listen(8)
192
+ await trio.serve_listeners(self._one, [trio.SocketListener(s)])
193
+
194
+ async def _one(self, stream: trio.SocketStream) -> None:
195
+ try:
196
+ buf = b''
197
+ with trio.move_on_after(CONTROL_TIMEOUT):
198
+ while not buf.endswith(b'\n') and len(buf) <= MAX_LINE:
199
+ chunk = await stream.receive_some(65536)
200
+ if not chunk:
201
+ break
202
+ buf += chunk
203
+ try:
204
+ if not buf.endswith(b'\n') or len(buf) > MAX_LINE:
205
+ raise ValueError('a request is one JSON line under %d bytes' % MAX_LINE)
206
+ req = json.loads(buf.decode('utf-8'))
207
+ if not isinstance(req, dict) or not isinstance(req.get('cmd'), str):
208
+ raise ValueError('a request is {"cmd": ..., "args": {...}}')
209
+ args = req.get('args') or {}
210
+ if not isinstance(args, dict):
211
+ raise ValueError('args is an object')
212
+ result = await self.dispatch(req['cmd'], args)
213
+ reply = {'ok': True, 'protocol': PROTOCOL, 'result': result}
214
+ except Exception as e: # one bad request answers one request
215
+ reply = {'ok': False, 'protocol': PROTOCOL, 'error': '%s: %s' % (type(e).__name__, e)}
216
+ # Everything status()/drain() return is str/int/dict already;
217
+ # no `default=str`, which would let a stray object type leak
218
+ # through as its repr and be parsed by the node as a value.
219
+ await stream.send_all((json.dumps(reply) + '\n').encode('utf-8'))
220
+ except (trio.BrokenResourceError, trio.ClosedResourceError, OSError):
221
+ pass
222
+ finally:
223
+ await trio.aclose_forcefully(stream)
224
+
225
+ async def dispatch(self, cmd: str, args: Mapping[str, Any]) -> Any:
226
+ if cmd == 'publish':
227
+ desc = args.get('descriptor')
228
+ if not isinstance(desc, dict) or desc.get('kind') != KIND:
229
+ raise ValueError('publish takes one kind %d event' % KIND)
230
+ if self.pubsub is None:
231
+ raise RuntimeError('the topic is not joined yet')
232
+ body = json.dumps(desc, separators=(',', ':'), sort_keys=True).encode('utf-8')
233
+ if len(body) > PAYLOAD_MAX:
234
+ raise ValueError('a descriptor of %d bytes is over the %d the topic carries'
235
+ % (len(body), PAYLOAD_MAX))
236
+ await self.pubsub.publish(TOPIC, body)
237
+ self.published += 1
238
+ return {'published': self.published, 'bytes': len(body)}
239
+ if cmd == 'drain':
240
+ try:
241
+ want = min(int(args.get('max') or HEARD_MAX), HEARD_MAX)
242
+ except (TypeError, ValueError):
243
+ want = HEARD_MAX
244
+ out = [self.heard.popleft() for _ in range(min(want, len(self.heard)))]
245
+ dropped = dict(self.dropped)
246
+ self.dropped.clear()
247
+ return dict(self.status(), heard=out, dropped=dropped)
248
+ if cmd == 'status':
249
+ return self.status()
250
+ raise KeyError(cmd)
251
+
252
+ def status(self) -> AnnounceStatusReport:
253
+ peers: list[str] = []
254
+ if self.pubsub is not None:
255
+ peers = [str(p) for p in self.pubsub.peer_topics.get(TOPIC, [])]
256
+ return {
257
+ 'topic': TOPIC,
258
+ 'peer_id': str(self.host.get_id()) if self.host is not None else None,
259
+ 'mesh_peers': len(peers),
260
+ 'pow_bits': self.pow_bits,
261
+ 'accepted': self.accepted,
262
+ 'published': self.published,
263
+ 'waiting': len(self.heard),
264
+ 'overflowed': self.overflowed,
265
+ }
266
+
267
+
268
+ async def run(directory: str, bootstrap: list[str], port: int, pow_bits: int,
269
+ path: Optional[str] = None) -> None:
270
+ key_pair = create_new_key_pair(transport_seed(directory))
271
+ host = new_host(key_pair=key_pair)
272
+ gossipsub = GossipSub(protocols=[GOSSIPSUB], degree=6, degree_low=4, degree_high=12)
273
+ pubsub = Pubsub(host, gossipsub) # strict_signing defaults to True
274
+ state = Announce(pow_bits=pow_bits)
275
+ state.host, state.pubsub = host, pubsub
276
+ sock = path or socket_path(directory)
277
+ listen = [multiaddr.Multiaddr('/ip4/0.0.0.0/tcp/%d' % port)]
278
+
279
+ async with host.run(listen_addrs=listen), trio.open_nursery() as nursery:
280
+ async with background_trio_service(pubsub), background_trio_service(gossipsub):
281
+ await pubsub.wait_until_ready()
282
+ pubsub.set_topic_validator(TOPIC, state.validate, False)
283
+ await pubsub.subscribe(TOPIC)
284
+ print('%s peer %s' % (TOPIC, host.get_id()), flush=True)
285
+ for a in host.get_addrs():
286
+ print(' listening %s' % a, flush=True)
287
+ for addr in bootstrap:
288
+ try:
289
+ await host.connect(info_from_p2p_addr(multiaddr.Multiaddr(addr)))
290
+ print(' bootstrap %s' % addr, flush=True)
291
+ except Exception as e:
292
+ # A bootstrap address that does not answer is ordinary. The
293
+ # topic works with one peer and the relay bridge works with
294
+ # none, so this is never fatal.
295
+ print(' bootstrap %s did not answer (%s)' % (addr, e), flush=True)
296
+ nursery.start_soon(state.serve, sock)
297
+ print(' socket %s' % sock, flush=True)
298
+ await trio.sleep_forever()
299
+
300
+
301
+ def _leaves(e: BaseException) -> list[BaseException]:
302
+ """The real exceptions under however many ExceptionGroups trio nested."""
303
+ subs = getattr(e, 'exceptions', None)
304
+ if not subs:
305
+ return [e]
306
+ out: list[BaseException] = []
307
+ for s in subs:
308
+ out.extend(_leaves(s))
309
+ return out
310
+
311
+
312
+ def main(argv: Optional[list[str]] = None) -> int:
313
+ ap = argparse.ArgumentParser(prog='fleetmesh-announce', description=__doc__.splitlines()[0])
314
+ ap.add_argument('directory', help="the node directory, for keys.json and the socket")
315
+ ap.add_argument('--bootstrap', action='append', default=[],
316
+ metavar='MULTIADDR', help='a libp2p peer to dial on start; repeatable')
317
+ ap.add_argument('--port', type=int, default=0, help='tcp port to listen on (0 asks the kernel)')
318
+ ap.add_argument('--pow-bits', type=int, default=0,
319
+ help='refuse a descriptor below this many bits of NIP-13 work. Local policy: '
320
+ 'two nodes with different floors interoperate, the stricter propagates less')
321
+ ap.add_argument('--socket', default=None, help='where to listen (default: announce.sock in the directory)')
322
+ a = ap.parse_args(argv)
323
+ try:
324
+ trio.run(run, a.directory, a.bootstrap, a.port, a.pow_bits, a.socket)
325
+ except KeyboardInterrupt:
326
+ return 0
327
+ except Exception as e:
328
+ # A refusal at startup - a live socket, a loose keys.json, a missing
329
+ # key file - is one line, not a nursery traceback. trio wraps what a
330
+ # nursery task raised in an ExceptionGroup; unwrap for the message.
331
+ for inner in _leaves(e):
332
+ print('fleetmesh-announce: %s: %s' % (type(inner).__name__, inner), file=sys.stderr, flush=True)
333
+ return 1
334
+ return 0
335
+
336
+
337
+ if __name__ == '__main__':
338
+ raise SystemExit(main())