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,274 @@
1
+ """Protocol suite and host multiplexer configuration blocks."""
2
+ from __future__ import annotations
3
+
4
+ import dataclasses
5
+ from typing import Any, Mapping, Optional
6
+
7
+ from .base import Block, Problems, is_http_url, is_relay_url, opt, positive_int
8
+
9
+ __all__ = [
10
+ 'HostMultiplexerConfig',
11
+ 'NostrProtocolConfig',
12
+ 'ATProtoProtocolConfig',
13
+ 'ActivityPubProtocolConfig',
14
+ 'SSBProtocolConfig',
15
+ 'P2PandaProtocolConfig',
16
+ 'RhioProtocolConfig',
17
+ 'ProtocolSuitesConfig',
18
+ ]
19
+
20
+
21
+ @dataclasses.dataclass(frozen=True)
22
+ class HostMultiplexerConfig(Block):
23
+ listen: str = '127.0.0.1'
24
+ port: int = 0
25
+ public_url: Optional[str] = None
26
+ bind_wait: float = 10.0
27
+
28
+ @classmethod
29
+ def check(cls, raw: Any, p: Problems) -> None:
30
+ if not isinstance(raw, Mapping):
31
+ p.add('protocols.host must be an object'); return
32
+ opt(raw, 'listen', lambda v: isinstance(v, str) and len(v) > 0, p, 'protocols.host.listen must be a non-empty string')
33
+ opt(raw, 'port', lambda v: isinstance(v, int) and 0 <= v <= 65535, p, 'protocols.host.port must be 0..65535')
34
+ opt(raw, 'public_url', is_http_url, p, 'protocols.host.public_url must be an http or https URL')
35
+ opt(raw, 'bind_wait', lambda v: isinstance(v, (int, float)) and v >= 0, p, 'protocols.host.bind_wait must be non-negative')
36
+
37
+ @classmethod
38
+ def from_dict(cls, raw: Any) -> 'HostMultiplexerConfig':
39
+ if not isinstance(raw, Mapping):
40
+ return cls()
41
+ return cls(
42
+ listen=str(raw.get('listen', '127.0.0.1')),
43
+ port=int(raw.get('port', 0)),
44
+ public_url=str(raw['public_url']) if raw.get('public_url') else None,
45
+ bind_wait=float(raw.get('bind_wait', 10.0)),
46
+ )
47
+
48
+
49
+ @dataclasses.dataclass(frozen=True)
50
+ class NostrProtocolConfig(Block):
51
+ enabled: bool = True
52
+ relays: tuple[str, ...] = ()
53
+ require_auth: bool = False
54
+ future_seconds: int = 900
55
+
56
+ @classmethod
57
+ def check(cls, raw: Any, p: Problems) -> None:
58
+ if not isinstance(raw, Mapping):
59
+ p.add('protocols.nostr must be an object'); return
60
+ opt(raw, 'enabled', lambda v: isinstance(v, bool), p, 'protocols.nostr.enabled must be a boolean')
61
+ opt(raw, 'relays', lambda v: isinstance(v, (list, tuple)) and all(is_relay_url(u) for u in v), p,
62
+ 'protocols.nostr.relays must be a list of relay URLs (wss:// or ws://)')
63
+ opt(raw, 'require_auth', lambda v: isinstance(v, bool), p, 'protocols.nostr.require_auth must be a boolean')
64
+ opt(raw, 'future_seconds', positive_int, p, 'protocols.nostr.future_seconds must be a positive integer')
65
+
66
+ @classmethod
67
+ def from_dict(cls, raw: Any) -> 'NostrProtocolConfig':
68
+ if not isinstance(raw, Mapping):
69
+ return cls()
70
+ return cls(
71
+ enabled=bool(raw.get('enabled', True)),
72
+ relays=tuple(str(u) for u in (raw.get('relays') or ())),
73
+ require_auth=bool(raw.get('require_auth', False)),
74
+ future_seconds=int(raw.get('future_seconds', 900)),
75
+ )
76
+
77
+
78
+ @dataclasses.dataclass(frozen=True)
79
+ class ATProtoProtocolConfig(Block):
80
+ enabled: bool = False
81
+ handle: Optional[str] = None
82
+ service_did: Optional[str] = None
83
+ pds_enabled: bool = True
84
+ firehose_subscribe: bool = False
85
+ firehose_publish: bool = True
86
+ relay_upstream: Optional[str] = None
87
+ metered_peering: bool = True
88
+
89
+ @classmethod
90
+ def check(cls, raw: Any, p: Problems) -> None:
91
+ if not isinstance(raw, Mapping):
92
+ p.add('protocols.atproto must be an object'); return
93
+ opt(raw, 'enabled', lambda v: isinstance(v, bool), p, 'protocols.atproto.enabled must be a boolean')
94
+ opt(raw, 'handle', lambda v: isinstance(v, str) and len(v) > 0, p, 'protocols.atproto.handle must be a string')
95
+ opt(raw, 'service_did', lambda v: isinstance(v, str) and v.startswith('did:'), p, 'protocols.atproto.service_did must be a did: URI')
96
+ opt(raw, 'pds_enabled', lambda v: isinstance(v, bool), p, 'protocols.atproto.pds_enabled must be a boolean')
97
+ opt(raw, 'firehose_subscribe', lambda v: isinstance(v, bool), p, 'protocols.atproto.firehose_subscribe must be a boolean')
98
+ opt(raw, 'firehose_publish', lambda v: isinstance(v, bool), p, 'protocols.atproto.firehose_publish must be a boolean')
99
+ opt(raw, 'relay_upstream', is_relay_url, p, 'protocols.atproto.relay_upstream must be a websocket URL')
100
+ opt(raw, 'metered_peering', lambda v: isinstance(v, bool), p, 'protocols.atproto.metered_peering must be a boolean')
101
+
102
+ @classmethod
103
+ def from_dict(cls, raw: Any) -> 'ATProtoProtocolConfig':
104
+ if not isinstance(raw, Mapping):
105
+ return cls()
106
+ return cls(
107
+ enabled=bool(raw.get('enabled', False)),
108
+ handle=str(raw['handle']) if raw.get('handle') else None,
109
+ service_did=str(raw['service_did']) if raw.get('service_did') else None,
110
+ pds_enabled=bool(raw.get('pds_enabled', True)),
111
+ firehose_subscribe=bool(raw.get('firehose_subscribe', False)),
112
+ firehose_publish=bool(raw.get('firehose_publish', True)),
113
+ relay_upstream=str(raw['relay_upstream']) if raw.get('relay_upstream') else None,
114
+ metered_peering=bool(raw.get('metered_peering', True)),
115
+ )
116
+
117
+
118
+ @dataclasses.dataclass(frozen=True)
119
+ class ActivityPubProtocolConfig(Block):
120
+ enabled: bool = False
121
+ domain: Optional[str] = None
122
+ summary: str = 'Fleetmesh Sovereign Edge Node'
123
+ rate_limit_per_inbox: int = 60
124
+
125
+ @classmethod
126
+ def check(cls, raw: Any, p: Problems) -> None:
127
+ if not isinstance(raw, Mapping):
128
+ p.add('protocols.activitypub must be an object'); return
129
+ opt(raw, 'enabled', lambda v: isinstance(v, bool), p, 'protocols.activitypub.enabled must be a boolean')
130
+ opt(raw, 'domain', lambda v: isinstance(v, str) and len(v) > 0, p, 'protocols.activitypub.domain must be a string')
131
+ opt(raw, 'summary', lambda v: isinstance(v, str), p, 'protocols.activitypub.summary must be a string')
132
+ opt(raw, 'rate_limit_per_inbox', positive_int, p, 'protocols.activitypub.rate_limit_per_inbox must be a positive integer')
133
+
134
+ @classmethod
135
+ def from_dict(cls, raw: Any) -> 'ActivityPubProtocolConfig':
136
+ if not isinstance(raw, Mapping):
137
+ return cls()
138
+ return cls(
139
+ enabled=bool(raw.get('enabled', False)),
140
+ domain=str(raw['domain']) if raw.get('domain') else None,
141
+ summary=str(raw.get('summary', 'Fleetmesh Sovereign Edge Node')),
142
+ rate_limit_per_inbox=int(raw.get('rate_limit_per_inbox', 60)),
143
+ )
144
+
145
+
146
+ @dataclasses.dataclass(frozen=True)
147
+ class SSBProtocolConfig(Block):
148
+ enabled: bool = False
149
+ port: int = 8008
150
+ caps: str = 'd4a1da283c1e443e022da12bc6139952b324e4001f40fa38ba5754035a49807f'
151
+
152
+ @classmethod
153
+ def check(cls, raw: Any, p: Problems) -> None:
154
+ if not isinstance(raw, Mapping):
155
+ p.add('protocols.ssb must be an object'); return
156
+ opt(raw, 'enabled', lambda v: isinstance(v, bool), p, 'protocols.ssb.enabled must be a boolean')
157
+ opt(raw, 'port', lambda v: isinstance(v, int) and 0 <= v <= 65535, p, 'protocols.ssb.port must be 0..65535')
158
+ opt(raw, 'caps', lambda v: isinstance(v, str) and len(v) == 64, p, 'protocols.ssb.caps must be a 64-char hex string')
159
+
160
+ @classmethod
161
+ def from_dict(cls, raw: Any) -> 'SSBProtocolConfig':
162
+ if not isinstance(raw, Mapping):
163
+ return cls()
164
+ return cls(
165
+ enabled=bool(raw.get('enabled', False)),
166
+ port=int(raw.get('port', 8008)),
167
+ caps=str(raw.get('caps', 'd4a1da283c1e443e022da12bc6139952b324e4001f40fa38ba5754035a49807f')),
168
+ )
169
+
170
+
171
+ @dataclasses.dataclass(frozen=True)
172
+ class P2PandaProtocolConfig(Block):
173
+ enabled: bool = False
174
+ port: int = 9102
175
+ network_id: str = 'fleetmesh-default'
176
+ discovery: bool = True
177
+
178
+ @classmethod
179
+ def check(cls, raw: Any, p: Problems) -> None:
180
+ if not isinstance(raw, Mapping):
181
+ p.add('protocols.p2panda must be an object'); return
182
+ opt(raw, 'enabled', lambda v: isinstance(v, bool), p, 'protocols.p2panda.enabled must be a boolean')
183
+ opt(raw, 'port', lambda v: isinstance(v, int) and 0 <= v <= 65535, p, 'protocols.p2panda.port must be 0..65535')
184
+ opt(raw, 'network_id', lambda v: isinstance(v, str) and len(v) > 0, p, 'protocols.p2panda.network_id must be a string')
185
+ opt(raw, 'discovery', lambda v: isinstance(v, bool), p, 'protocols.p2panda.discovery must be a boolean')
186
+
187
+ @classmethod
188
+ def from_dict(cls, raw: Any) -> 'P2PandaProtocolConfig':
189
+ if not isinstance(raw, Mapping):
190
+ return cls()
191
+ return cls(
192
+ enabled=bool(raw.get('enabled', False)),
193
+ port=int(raw.get('port', 9102)),
194
+ network_id=str(raw.get('network_id', 'fleetmesh-default')),
195
+ discovery=bool(raw.get('discovery', True)),
196
+ )
197
+
198
+
199
+ @dataclasses.dataclass(frozen=True)
200
+ class RhioProtocolConfig(Block):
201
+ enabled: bool = False
202
+ nats_endpoint: str = 'localhost:4222'
203
+ s3_endpoint: str = 'http://localhost:9000'
204
+ network_id: str = 'rhio-default-network-1'
205
+ bind_port: int = 9102
206
+ http_bind_port: int = 3000
207
+
208
+ @classmethod
209
+ def check(cls, raw: Any, p: Problems) -> None:
210
+ if not isinstance(raw, Mapping):
211
+ p.add('protocols.rhio must be an object'); return
212
+ opt(raw, 'enabled', lambda v: isinstance(v, bool), p, 'protocols.rhio.enabled must be a boolean')
213
+ opt(raw, 'nats_endpoint', lambda v: isinstance(v, str) and len(v) > 0, p, 'protocols.rhio.nats_endpoint must be a string')
214
+ opt(raw, 's3_endpoint', lambda v: isinstance(v, str) and len(v) > 0, p, 'protocols.rhio.s3_endpoint must be a string')
215
+ opt(raw, 'network_id', lambda v: isinstance(v, str) and len(v) > 0, p, 'protocols.rhio.network_id must be a string')
216
+ opt(raw, 'bind_port', lambda v: isinstance(v, int) and 0 <= v <= 65535, p, 'protocols.rhio.bind_port must be 0..65535')
217
+ opt(raw, 'http_bind_port', lambda v: isinstance(v, int) and 0 <= v <= 65535, p, 'protocols.rhio.http_bind_port must be 0..65535')
218
+
219
+ @classmethod
220
+ def from_dict(cls, raw: Any) -> 'RhioProtocolConfig':
221
+ if not isinstance(raw, Mapping):
222
+ return cls()
223
+ return cls(
224
+ enabled=bool(raw.get('enabled', False)),
225
+ nats_endpoint=str(raw.get('nats_endpoint', 'localhost:4222')),
226
+ s3_endpoint=str(raw.get('s3_endpoint', 'http://localhost:9000')),
227
+ network_id=str(raw.get('network_id', 'rhio-default-network-1')),
228
+ bind_port=int(raw.get('bind_port', 9102)),
229
+ http_bind_port=int(raw.get('http_bind_port', 3000)),
230
+ )
231
+
232
+
233
+ @dataclasses.dataclass(frozen=True)
234
+ class ProtocolSuitesConfig(Block):
235
+ host: HostMultiplexerConfig = HostMultiplexerConfig()
236
+ nostr: NostrProtocolConfig = NostrProtocolConfig()
237
+ atproto: ATProtoProtocolConfig = ATProtoProtocolConfig()
238
+ activitypub: ActivityPubProtocolConfig = ActivityPubProtocolConfig()
239
+ ssb: SSBProtocolConfig = SSBProtocolConfig()
240
+ p2panda: P2PandaProtocolConfig = P2PandaProtocolConfig()
241
+ rhio: RhioProtocolConfig = RhioProtocolConfig()
242
+
243
+ @classmethod
244
+ def check(cls, raw: Any, p: Problems) -> None:
245
+ if not isinstance(raw, Mapping):
246
+ p.add('protocols must be an object'); return
247
+ if 'host' in raw:
248
+ HostMultiplexerConfig.check(raw['host'], p)
249
+ if 'nostr' in raw:
250
+ NostrProtocolConfig.check(raw['nostr'], p)
251
+ if 'atproto' in raw:
252
+ ATProtoProtocolConfig.check(raw['atproto'], p)
253
+ if 'activitypub' in raw:
254
+ ActivityPubProtocolConfig.check(raw['activitypub'], p)
255
+ if 'ssb' in raw:
256
+ SSBProtocolConfig.check(raw['ssb'], p)
257
+ if 'p2panda' in raw:
258
+ P2PandaProtocolConfig.check(raw['p2panda'], p)
259
+ if 'rhio' in raw:
260
+ RhioProtocolConfig.check(raw['rhio'], p)
261
+
262
+ @classmethod
263
+ def from_dict(cls, raw: Any) -> 'ProtocolSuitesConfig':
264
+ if not isinstance(raw, Mapping):
265
+ return cls()
266
+ return cls(
267
+ host=HostMultiplexerConfig.from_dict(raw.get('host')),
268
+ nostr=NostrProtocolConfig.from_dict(raw.get('nostr')),
269
+ atproto=ATProtoProtocolConfig.from_dict(raw.get('atproto')),
270
+ activitypub=ActivityPubProtocolConfig.from_dict(raw.get('activitypub')),
271
+ ssb=SSBProtocolConfig.from_dict(raw.get('ssb')),
272
+ p2panda=P2PandaProtocolConfig.from_dict(raw.get('p2panda')),
273
+ rhio=RhioProtocolConfig.from_dict(raw.get('rhio')),
274
+ )
node/config/records.py ADDED
@@ -0,0 +1,171 @@
1
+ """The records config.json carries in lists and maps: an endpoint, a peer to
2
+ pull from, a coverage claim, a trust decision, a retention rule, and the two
3
+ small objects the node itself reads (face, policy).
4
+
5
+ These are read in many places - `peers` in five compartments, `coverage` in
6
+ four - and before this each reader implied its own shape. One dataclass per
7
+ record is the shape.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import dataclasses
12
+ from typing import Any, Mapping, Optional
13
+
14
+ from .base import Block, Problems, all_str, is_hex64, is_int, non_negative_int, opt, positive_int
15
+
16
+ TIERS: tuple[str, ...] = ('probation', 'member', 'trusted', 'anchor')
17
+ CLASSES: tuple[str, ...] = ('leaf', 'edge', 'anchor')
18
+ COMPLETENESS: tuple[str, ...] = ('asserted', 'best-effort')
19
+ HANDOVER: tuple[str, ...] = ('offer', 'never')
20
+
21
+
22
+ @dataclasses.dataclass(frozen=True)
23
+ class Endpoint(Block):
24
+ """One way to reach this node: `{transport, address[, priority]}`."""
25
+ transport: str
26
+ address: str
27
+ priority: int = 50
28
+
29
+ @classmethod
30
+ def check(cls, raw: Any, p: Problems) -> None:
31
+ # The list check names the whole list once; see NodeConfig.
32
+ pass
33
+
34
+ @staticmethod
35
+ def well_formed(e: Any) -> bool:
36
+ return isinstance(e, Mapping) and isinstance(e.get('transport'), str) and isinstance(e.get('address'), str)
37
+
38
+ @classmethod
39
+ def from_dict(cls, raw: Any) -> 'Endpoint':
40
+ return cls(transport=raw['transport'], address=raw['address'], priority=int(raw.get('priority', 50)))
41
+
42
+
43
+ @dataclasses.dataclass(frozen=True)
44
+ class PeerPull(Block):
45
+ """A peer this node pulls a shard from: `{pubkey, shard, filter[, pull_every, window]}`."""
46
+ pubkey: str
47
+ shard: str
48
+ filter: Mapping[str, Any]
49
+ pull_every: int = 900
50
+ window: Optional[int] = None # the peering manifest's budget.window, seconds
51
+
52
+ @staticmethod
53
+ def check_one(i: int, e: Any, p: Problems) -> None:
54
+ if not isinstance(e, Mapping) or not is_hex64(e.get('pubkey')) or not isinstance(e.get('shard'), str) \
55
+ or not isinstance(e.get('filter'), dict):
56
+ p.add('peers[%d] must be {pubkey (64 hex), shard, filter (object)[, pull_every]}' % i)
57
+ elif 'pull_every' in e and not positive_int(e['pull_every']):
58
+ p.add('peers[%d].pull_every must be a positive integer' % i)
59
+
60
+ @classmethod
61
+ def from_dict(cls, raw: Any) -> 'PeerPull':
62
+ return cls(pubkey=raw['pubkey'], shard=raw['shard'], filter=dict(raw['filter']),
63
+ pull_every=int(raw.get('pull_every', 900)), window=raw.get('window'))
64
+
65
+
66
+ @dataclasses.dataclass(frozen=True)
67
+ class CoverageClaim(Block):
68
+ """A shard this node tells the mesh it holds, as a kind 30803 claim."""
69
+ shard: str
70
+ filter: Mapping[str, Any]
71
+ completeness: str = 'best-effort'
72
+ since: Optional[int] = None
73
+ until: Optional[int] = None
74
+
75
+ @staticmethod
76
+ def check_one(i: int, e: Any, p: Problems) -> None:
77
+ if not isinstance(e, Mapping) or not isinstance(e.get('shard'), str) or not isinstance(e.get('filter'), dict) \
78
+ or e.get('completeness', 'best-effort') not in COMPLETENESS:
79
+ p.add('coverage[%d] must be {shard, filter (object)[, completeness asserted|best-effort]}' % i)
80
+
81
+ @classmethod
82
+ def from_dict(cls, raw: Any) -> 'CoverageClaim':
83
+ return cls(shard=raw['shard'], filter=dict(raw['filter']), completeness=raw.get('completeness', 'best-effort'),
84
+ since=raw.get('since'), until=raw.get('until'))
85
+
86
+
87
+ @dataclasses.dataclass(frozen=True)
88
+ class TrustDecision(Block):
89
+ """An operator's out-of-band decision about a peer: a tier, and when."""
90
+ tier: str
91
+ at: int = 0
92
+
93
+ @staticmethod
94
+ def check_one(pk: Any, e: Any, p: Problems) -> None:
95
+ tier = e.get('tier') if isinstance(e, Mapping) else e
96
+ if not is_hex64(pk) or tier not in TIERS or (isinstance(e, Mapping) and not is_int(e.get('at', 0))):
97
+ p.add('trust[%s] must be a tier name or {tier, at}' % str(pk)[:16])
98
+
99
+ @classmethod
100
+ def from_dict(cls, raw: Any) -> 'TrustDecision':
101
+ if isinstance(raw, Mapping):
102
+ return cls(tier=raw['tier'], at=int(raw.get('at', 0)))
103
+ return cls(tier=raw)
104
+
105
+
106
+ # How far in the future the relay face accepts an event. The face constant
107
+ # (node/relayserver.py) is defined from this one: the config layer imports
108
+ # nothing that reaches a crypto dependency, so a tool that only reads config
109
+ # needs none.
110
+ FACE_FUTURE_MAX_SECONDS: int = 900
111
+
112
+
113
+ @dataclasses.dataclass(frozen=True)
114
+ class FaceConfig(Block):
115
+ """The relay face: where it listens and how far in the future it accepts."""
116
+ key = 'face'
117
+ host: str = '127.0.0.1'
118
+ port: int = 0
119
+ future_seconds: int = FACE_FUTURE_MAX_SECONDS
120
+
121
+ @classmethod
122
+ def check(cls, raw: Any, p: Problems) -> None:
123
+ if not isinstance(raw, Mapping) or not isinstance(raw.get('host', '127.0.0.1'), str) \
124
+ or not is_int(raw.get('port', 0)) or not 0 <= raw.get('port', 0) <= 65535:
125
+ p.add('face must be {host, port} with a port from 0 to 65535')
126
+ elif 'future_seconds' in raw and not non_negative_int(raw['future_seconds']):
127
+ p.add('face.future_seconds must be a non-negative integer')
128
+
129
+ @classmethod
130
+ def from_dict(cls, raw: Any) -> 'FaceConfig':
131
+ return cls(host=raw.get('host', '127.0.0.1'), port=int(raw.get('port', 0)),
132
+ future_seconds=int(raw.get('future_seconds', FACE_FUTURE_MAX_SECONDS)))
133
+
134
+
135
+ from .parameter import Parameter, ParamBlock
136
+
137
+
138
+ @dataclasses.dataclass(frozen=True)
139
+ class PolicyConfig(ParamBlock):
140
+ """Whether strangers may enrol, and which protocols a peer must speak."""
141
+ key = 'policy'
142
+ open_enrolment: bool = True
143
+ protocols: tuple[str, ...] = ()
144
+ DERIVED: bool = False
145
+ PARAMS = (
146
+ Parameter(
147
+ key='open_enrolment', name='Open Enrolment',
148
+ description='Whether strangers may enrol without prior grant',
149
+ type=bool, default=True, tags=('peering', 'policy'),
150
+ ),
151
+ Parameter(
152
+ key='protocols', name='Required Protocols',
153
+ description='Protocols a peer must speak to peer with this node',
154
+ type=list, item=str, default=(), tags=('peering', 'policy'),
155
+ ),
156
+ )
157
+
158
+ @classmethod
159
+ def check(cls, raw: Any, p: Problems) -> None:
160
+ if not isinstance(raw, Mapping) or not isinstance(raw.get('open_enrolment', True), bool):
161
+ p.add('policy must be an object with a boolean open_enrolment')
162
+ else:
163
+ opt(raw, 'protocols', all_str, p, 'policy.protocols must be a list of identifiers')
164
+
165
+ @classmethod
166
+ def from_dict(cls, raw: Any) -> 'PolicyConfig':
167
+ return cls(open_enrolment=bool(raw.get('open_enrolment', True)), protocols=tuple(raw.get('protocols') or ()))
168
+
169
+
170
+ __all__ = ['Endpoint', 'PeerPull', 'CoverageClaim', 'TrustDecision', 'FaceConfig', 'PolicyConfig',
171
+ 'TIERS', 'CLASSES', 'COMPLETENESS', 'HANDOVER']
@@ -0,0 +1 @@
1
+ """Programs that use a node from outside it; each is run by a test."""
node/examples/demo.py ADDED
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env python3
2
+ """The MVP, end to end, in one run.
3
+
4
+ python3 node/demo.py
5
+
6
+ Three nodes and one public relay, all in this process. A client posts to node
7
+ A over ordinary NIP-01. A and B find each other's descriptors on the relay,
8
+ peer over DIDComm, publish their grants, and negentropy moves the events to B.
9
+ Receipts flow, a window closes clean, and the grant is amended. Then the
10
+ reference's bad peer is pointed at A and a kind 30802 comes out.
11
+
12
+ Nothing is mocked and nothing is stubbed: real BIP-340 signatures, real
13
+ NIP-44 envelopes, real gift wraps, real websockets, real SQLite. The clock is
14
+ the only thing driven by hand, because a window is an hour long.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import functools
19
+ import json
20
+ import os
21
+ import sys
22
+ import tempfile
23
+ import time
24
+ from typing import Any, Callable, Mapping, cast
25
+
26
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')))
27
+ from node import app as app_mod # noqa: E402
28
+ from node.protocols.nostr import relayserver # noqa: E402
29
+ from node.storage import grants # noqa: E402
30
+ from node.protocols.nostr import client as relay # noqa: E402
31
+ from fleetmesh_core.accounting import accounting, window # noqa: E402
32
+ from fleetmesh_core.crypto import crypto # noqa: E402
33
+ from fleetmesh_core.peering import badpeer, reports # noqa: E402
34
+ from fleetmesh_core.storage import store # noqa: E402
35
+ from fleetmesh_core.wire.interfaces import WindowEvaluationDetail # noqa: E402
36
+
37
+ W: int = 3600
38
+ NOW: int = (1787100000 // W) * W + 100
39
+ T: list[int] = [NOW]
40
+ clock: Callable[[], int] = lambda: T[0]
41
+ OPERATOR: str = crypto.pubkey('33' * 32)
42
+ BRISTOL: Mapping[str, Any] = {'kinds': [1], '#t': ['bristol']}
43
+ STEP: list[int] = [0]
44
+
45
+
46
+ def say(line: str = '') -> None:
47
+ print(line)
48
+
49
+
50
+ def step(title: str) -> None:
51
+ STEP[0] += 1
52
+ print('\n\033[1m%d. %s\033[0m' % (STEP[0], title))
53
+
54
+
55
+ def wait_for(pred: Callable[[], bool], seconds: float = 20.0) -> bool:
56
+ """Poll until `pred` holds or the clock runs out. The demo asserts its
57
+ own steps rather than sleeping past them."""
58
+ deadline = time.time() + seconds
59
+ while time.time() < deadline and not pred():
60
+ time.sleep(0.05)
61
+ return pred()
62
+
63
+
64
+ def main() -> None:
65
+ tmp = tempfile.mkdtemp(prefix='fleetmesh-demo-')
66
+ public = relayserver.RelayServer(clock=clock, name='public-relay').start()
67
+ relays = [public.url]
68
+
69
+ def node(name: str, **over: Any) -> app_mod.NodeApp:
70
+ cfg = dict(app_mod.defaults(), **{'relays': relays, 'operator': OPERATOR, 'pow_bits': 8,
71
+ 'name': name, 'tick_seconds': 1})
72
+ cfg.update(over)
73
+ d = os.path.join(tmp, name)
74
+ app_mod.save_config(d, {k: v for k, v in cfg.items() if not k.startswith('_')})
75
+ return app_mod.NodeApp(d, cfg, clock=clock).start()
76
+
77
+ step('Two nodes start. Each mines a descriptor and publishes it.')
78
+ A, B = node('alpha'), node('beta')
79
+ time.sleep(0.5)
80
+ for n in (A, B):
81
+ say(' %-6s %s' % (n.cfg['name'], n.ks.did))
82
+ say(' relay face %s descriptor %s' % (n.face.url, n._descriptor()['id'][:16]))
83
+ found = relay.query_all(relays, [{'kinds': [11801]}])
84
+ say(' the public relay holds %d descriptors; neither node was told the other exists' % len(found))
85
+
86
+ step('A client posts to A over ordinary NIP-01. A is just a relay to it.')
87
+ client = relay.Relay(A.face.url, timeout=5)
88
+ posted = []
89
+ for i in range(6):
90
+ ev = store.make_signed_event('77' * 32, NOW - 100 + i, 1, [['t', 'bristol']], 'bristol note %d' % i)
91
+ accepted, note = client.publish(ev)
92
+ posted.append(ev)
93
+ off = store.make_signed_event('77' * 32, NOW - 90, 1, [['t', 'cardiff']], 'a note from elsewhere')
94
+ client.publish(off)
95
+ say(' posted 6 events tagged bristol and 1 tagged cardiff; A accepted all 7')
96
+
97
+ step('B resolves A from the relay, peers with it, and pulls the shard.')
98
+ B.cfg['peers'] = [{'pubkey': A.ks.node_pk, 'shard': 'bristol', 'filter': BRISTOL, 'pull_every': 600}]
99
+ app_mod.save_config(B.dir, {k: v for k, v in B.cfg.items() if not k.startswith('_')})
100
+ did = B.tick()
101
+ for d in did:
102
+ say(' %s' % ' '.join(str(x)[:24] for x in d))
103
+ held = B.grants.get(A.ks.node_pk, grants.RECEIVED)
104
+ assert held is not None, 'B took no grant from A' # the peering above must have landed
105
+ g = held['payload']
106
+ say(' B holds a grant from A: tier %s, events_in %d, window %ds' % (g['tier'], g['limits']['events_in'], g['window']))
107
+ env = relay.query_all(relays, [{'ids': [held['envelope_id']]}])[0]
108
+ say(' its kind 30801 on the relay shows only %s' % ', '.join(sorted({t[0] for t in env['tags']})))
109
+ say(' B now holds %d events; the cardiff one did not cross' % len(B.store._all()))
110
+ say(' both stores fingerprint the shard as %s' % A.store.fingerprint_range(BRISTOL, 0, 2 ** 31, T[0]))
111
+
112
+ step('B accounts for what it used. A acknowledges.')
113
+ T[0] += 700
114
+ for d in B.tick():
115
+ say(' %s' % ' '.join(str(x)[:24] for x in d))
116
+ wait_for(lambda: B.budget.acks.get(A.ks.node_pk) is not None)
117
+ E0 = accounting.epoch(NOW, W)
118
+ r = A.budget.receipts_in[B.ks.node_pk][E0][0]
119
+ say(' receipt seq %d: %s' % (r['seq'], ', '.join('%s=%s' % kv for kv in sorted(r['counters'].items()))))
120
+ say(' A answers: %s' % B.budget.acks[A.ks.node_pk]['status'])
121
+
122
+ step('The window closes. A evaluates it and amends the grant.')
123
+ T[0] = (E0 + 1) * W + 100
124
+ B.tick()
125
+ wait_for(lambda: len(A.budget.receipts_in.get(B.ks.node_pk, {}).get(E0, [])) >= 2)
126
+ T[0] = (E0 + 1) * W + 400
127
+ A.tick()
128
+ rec = [x for x in A.budget.evaluations if x.peer == B.ks.node_pk][0]
129
+ say(' window %d: measured against B\'s last receipt, severity %s' % (rec.epoch, rec.severity))
130
+ say(' remedy: %s, in force from window %s' % (rec.note, rec.effective_from))
131
+ T[0] = (E0 + 2) * W + 5
132
+ A.tick()
133
+ issued = A.grants.get(B.ks.node_pk, grants.ISSUED)
134
+ assert issued is not None, 'A holds no grant for B'
135
+ new_env = issued['envelope_id']
136
+ wait_for(lambda: (r := B.grants.get(A.ks.node_pk, grants.RECEIVED)) is not None and r.get('envelope_id') == new_env)
137
+ say(' at the boundary A published it and B took it up: %s' % new_env[:16])
138
+ say(' A published nothing else about it: a clean window is not an event')
139
+
140
+ step('A bad peer joins. It floods A with events A already holds.')
141
+ Flood = functools.partial(badpeer.BadPeer, offence='duplicate-flood', flood=8)
142
+ D = node('delta', _session_cls=Flood)
143
+ time.sleep(0.5)
144
+ dclient = relay.Relay(D.face.url, timeout=5)
145
+ for i in range(10):
146
+ dclient.publish(store.make_signed_event('88' * 32, NOW - 50 + i, 1, [['t', 'bristol']], 'flood %d' % i))
147
+ A.propose(D.ks.node_pk, 'bristol', BRISTOL, timeout=20)
148
+ wait_for(lambda: D.grants.get(A.ks.node_pk, grants.RECEIVED) is not None)
149
+ epoch_now = accounting.epoch(T[0], W)
150
+ say(' A peered with it and asked for 10 events')
151
+ res = A.pull(D.ks.node_pk, 'bristol', BRISTOL, timeout=40)
152
+ say(' it sent %d, of which %d were duplicates: a ratio of %.3f against a ceiling of %s'
153
+ % (res['counters']['events_in'], res['counters']['dups_sent'], res['dup_ratio'],
154
+ window.C['tier_limits']['dup_ratio']['all_tiers']))
155
+ say(' it never sent a receipt for any of it')
156
+
157
+ step('A prices that window, and keeps the finding to itself.')
158
+ T[0] = (epoch_now + 1) * W + int(W * 0.1) + 1
159
+ A.tick()
160
+ rec = [x for x in A.budget.evaluations if x.peer == D.ks.node_pk][0]
161
+ say(' window %d: %s, %s' % (rec.epoch, rec.state, rec.severity))
162
+ dup = cast(WindowEvaluationDetail, rec.detail['dup_ratio'])
163
+ say(' priced on the ratio alone: overrun %.2f over the ceiling' % dup['overrun'])
164
+ say(' remedy: %s' % rec.note)
165
+ unpub = [u for u in A.reporter.unpublishable if u[0] == D.ks.node_pk]
166
+ say(' A holds an S3 against D and will not publish it:')
167
+ for reason in unpub[0][3]:
168
+ say(' %s' % reason)
169
+ say(' D sent no receipt, so D signed nothing this window that A could quote.')
170
+ say(' Silence is the one case with no evidence to have: neither half of the claim')
171
+ say(' is checkable by a reader, so A says nothing in public and zeroes the grant.')
172
+
173
+ step('An offence that leaves something signed behind.')
174
+ Push = functools.partial(badpeer.BadPeer, offence='out-of-scope-push')
175
+ E = node('epsilon', _session_cls=Push)
176
+ time.sleep(0.5)
177
+ someone_else = store.make_signed_event('aa' * 32, T[0] - 20, 1, [['t', 'cardiff']], 'a note E did not write')
178
+ E.store.insert_event(someone_else, T[0])
179
+ eclient = relay.Relay(E.face.url, timeout=5)
180
+ for i in range(2):
181
+ eclient.publish(store.make_signed_event('99' * 32, T[0] - 30 + i, 1, [['t', 'bristol']], 'shard note %d' % i))
182
+ A.propose(E.ks.node_pk, 'bristol', BRISTOL, timeout=20)
183
+ wait_for(lambda: E.grants.get(A.ks.node_pk, grants.RECEIVED) is not None)
184
+ res2 = A.pull(E.ks.node_pk, 'bristol', BRISTOL, timeout=40)
185
+ say(' E served the shard and slipped in an event tagged cardiff, written by somebody else')
186
+ say(' A refused it: %s' % (res2['findings'][0][1][:76] if res2['findings'] else 'nothing refused'))
187
+ say(' E wrote no part of that event, but it signed the hop that carried it,')
188
+ say(' and the report carries that envelope whole, so a reader checks it against E\'s key')
189
+ published = [p for p in A.reporter.published if any(t[:2] == ['d', E.ks.node_pk] for t in p['tags'])]
190
+ if published:
191
+ report = published[0]
192
+ say(' kind 30802 %s, signed by A, on the public relay:' % report['id'][:16])
193
+ for t in report['tags']:
194
+ say(' %s' % ' '.join(t)[:96])
195
+ inline = reports.inline_evidence(report)
196
+ say(' its content carries the cited artefact, %d bytes, resolving nothing:' % len(report['content']))
197
+ say(' %s' % json.dumps(list(inline.values())[0])[:92])
198
+
199
+ step('What a third party makes of it.')
200
+ C = node('gamma')
201
+ time.sleep(0.5)
202
+ about = C.reporter.reports_about(E.ks.node_pk)
203
+ say(' C finds %d report(s) about E; admissible: %s' % (len(about), [a['admissible'] for a in about]))
204
+ view, weights = C.reporter.view(E.ks.node_pk)
205
+ if weights:
206
+ say(' C holds no grant to A, so A weighs %.1f and C\'s view of E stays %s' % (weights[0]['weight'], view))
207
+ for e in range(168):
208
+ C.reporter.ledger.record(A.ks.node_pk, 496000 + e, 'S0', 'anchor')
209
+ view, weights = C.reporter.view(E.ks.node_pk)
210
+ say(' after a week of clean windows with A, A weighs %.1f and C\'s view of E becomes %s'
211
+ % (weights[0]['weight'], view))
212
+ say(' nobody imported anybody\'s ban: C ran the arithmetic itself')
213
+
214
+ say()
215
+ say('\033[1mThe whole protocol, once, over a wire.\033[0m')
216
+ say(' %d nodes, %d relay, %d events stored at A, %d grants issued, %d report published, %d withheld'
217
+ % (5, 1, len(A.store._all()), len(A.grants.list(grants.ISSUED)), len(A.reporter.published),
218
+ len(A.reporter.unpublishable)))
219
+ say(' state in %s' % tmp)
220
+ for n in (A, B, C, D, E):
221
+ n.stop()
222
+ public.stop()
223
+
224
+
225
+ if __name__ == '__main__':
226
+ main()