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,311 @@
1
+ """The SyncPeer compartment: construction, the metered send/receive every
2
+ role shares, the session table, and the compartment contract. The roles are
3
+ in initiator.py / responder.py; the handover in handover.py."""
4
+ from __future__ import annotations
5
+
6
+ import threading
7
+ from typing import Any, Callable, Mapping, Optional
8
+
9
+ from fleetmesh_core.storage import sync
10
+ from node.channels import bus as eventbus_mod
11
+ from fleetmesh_core.identity.did import did_nostr
12
+ from node.compartment import stores as st
13
+ from node.channels import Channel, INFO
14
+ from node.metrics import COUNTER, Metric
15
+ from node.compartment import Command, Handler, Need, Outbound, WireProtocol
16
+ from .envelope import KEEP, RECENT, SESSIONS_CEILING, SESSION_IDLE, SyncError
17
+ from .handover import HandoverMixin
18
+ from .initiator import InitiatorMixin
19
+ from .responder import ResponderMixin
20
+ from .shape import SyncSessionResult
21
+ from .types import SyncStateReport
22
+
23
+
24
+ from .types import SyncTotals
25
+
26
+
27
+ def _log(key: str, what: str, bound: int) -> st.Store:
28
+ return st.Store(key=key, description='%s, newest last' % what, kind=st.RING, shape=tuple, bound=bound)
29
+
30
+
31
+ import dataclasses
32
+ from node.config.parameter import Parameter, ParamBlock
33
+
34
+
35
+ @dataclasses.dataclass(frozen=True)
36
+ class SyncConfig(ParamBlock):
37
+ key = 'sync'
38
+ name = 'sync'
39
+ DERIVED: bool = False
40
+ PARAMS = (
41
+ Parameter(
42
+ key='sessions_ceiling', name='Sessions Ceiling',
43
+ description='Maximum concurrent open sync sessions answered',
44
+ type=int, default=SESSIONS_CEILING, minimum=1, tags=('sync', 'concurrency'),
45
+ ),
46
+ Parameter(
47
+ key='recent_kept', name='Recent Kept',
48
+ description='Number of recent completed or refused sessions kept',
49
+ type=int, default=RECENT, minimum=10, tags=('sync', 'history'),
50
+ ),
51
+ )
52
+ sessions_ceiling: int = SESSIONS_CEILING
53
+ recent_kept: int = RECENT
54
+
55
+
56
+ class SyncPeer(InitiatorMixin, ResponderMixin, HandoverMixin, WireProtocol):
57
+ """Both roles of sync/1.0 for one node, as a compartment.
58
+
59
+ Its refusals are the sessions it would not open - a grant that does not
60
+ cover one, a peer with too many already, a node over its own ceiling - and
61
+ the deliveries it would not store. Those lived in three separate lists that
62
+ only the CLI knew how to read.
63
+ """
64
+
65
+ name: str = 'sync'
66
+ requires: tuple[str, ...] = ('budget', 'strain')
67
+ piuris: tuple[str, ...] = (sync.PIURI,)
68
+ reads: tuple[str, ...] = ('handover', 'coverage')
69
+ config_cls = SyncConfig
70
+ live: tuple[str, ...] = ('handover',)
71
+ CHANNELS = (
72
+ Channel('handed', 'Handover signed and offered to peer', level=INFO,
73
+ say='handed {shard} to {peer} ({events} events)'),
74
+ Channel('held', 'Handover accepted from peer', level=INFO,
75
+ say='held {shard} handed over by {peer} ({events} events, {chain_len} in the chain)'),
76
+ )
77
+ METRICS = (
78
+ Metric(name='sync_handovers_offered', description='Times shard coverage was handed over.',
79
+ kind=COUNTER, counts='handed', tags=('sync', 'handover')),
80
+ Metric(name='sync_handovers_accepted', description='Times shard coverage was accepted.',
81
+ kind=COUNTER, counts='held', tags=('sync', 'handover')),
82
+ )
83
+ HANDLERS = (
84
+ Handler('open', run='handle_open', doc='answer inbound sync/1.0 open session request'),
85
+ Handler('handover', run='handle_handover', doc='accept inbound sync/1.0 handover envelope'),
86
+ Handler('range', run='handle_range', doc='answer inbound sync/1.0 range request'),
87
+ Handler('need', run='handle_need', doc='answer inbound sync/1.0 need request'),
88
+ Handler('done', run='handle_done', doc='answer inbound sync/1.0 done session finish'),
89
+ )
90
+ OUTBOUND = (
91
+ Outbound('open', reply=('range', 'deliver', 'done'), doc='open sync session against peer'),
92
+ Outbound('range', reply=('range', 'have', 'need', 'deliver', 'done'), doc='query or answer negentropy range'),
93
+ Outbound('have', reply=('need', 'done'), doc='answer negentropy have with event ids'),
94
+ Outbound('need', reply=('deliver', 'done'), doc='request event delivery for missing ids'),
95
+ Outbound('deliver', reply=('range', 'need', 'done'), doc='deliver events in signed envelope attachment'),
96
+ Outbound('done', doc='conclude sync session with final status'),
97
+ Outbound('handover', doc='hand over verified shard coverage chain'),
98
+ )
99
+ COMMANDS = (
100
+ Command(
101
+ key='show',
102
+ name='Sync',
103
+ description='sessions open, refused and aborted, and what could not be sent',
104
+ tags=('sync', 'status'),
105
+ permission='sync/show',
106
+ run='state',
107
+ reply='{open, refused, aborted, completed, unsent, malformed, handed, held}',
108
+ ),
109
+ )
110
+
111
+ # What it keeps. aborted, refused and completed were plain lists appended
112
+ # once per session and never trimmed - memory that grew with every peer's
113
+ # sessions for the life of the process - while nothing read more than
114
+ # their length and the newest few. Now a ring of the recent ones, with the
115
+ # length kept in `totals` so `state` reports the same numbers. The four
116
+ # that were already cut to 200 by hand (`del x[:-200]`) keep that bound
117
+ # as a declaration instead, and `state` still reports their length.
118
+ STORES = (
119
+ st.Store(key='totals', description='sessions since the process started, by how they ended',
120
+ kind=st.COUNTERS, shape=SyncTotals),
121
+ _log('aborted', 'sessions a peer stopped answering, as (peer, thid)', RECENT),
122
+ _log('refused', 'sessions this node would not open, as (peer, thid, why)', RECENT),
123
+ _log('completed', 'sessions that ended, as (peer, role, result)', RECENT),
124
+ _log('rejected', 'messages without the shape of their type, as (peer, kind, why)', KEEP),
125
+ _log('unsent', 'messages no relay would take, as (peer, type, why, at)', KEEP),
126
+ _log('handed', 'handovers this node signed and sent, as (peer, shard, fingerprint, at)', KEEP),
127
+ _log('held', 'handovers this node accepted, as (peer, shard, at)', KEEP),
128
+ # REFUSE: an open session dropped for room is a peer left waiting on a
129
+ # done that never comes. A full table refuses the open instead, with
130
+ # a done and a reason, like the per-peer cap in front of it.
131
+ st.Store(key='sessions', description='sessions this node is answering, with when each last moved',
132
+ kind=st.TABLE, shape=tuple, bound=SESSIONS_CEILING, evict=st.REFUSE, keyed_by='thid'),
133
+ # OLDEST, which the hand trim this replaces meant to be and was not: it
134
+ # cut a set to an arbitrary 2048 of its members, not the newest. A thid
135
+ # forgotten here only charges a late message on a long-finished
136
+ # session to the wrong side.
137
+ st.Store(key='_initiated', description='sessions this node opened, by thid', kind=st.SET,
138
+ shape=str, bound=4096, keyed_by='thid'),
139
+ # OLDEST is the harmless direction: a memo forgotten is a handover
140
+ # offered twice, and the receiver fingerprints the second copy as the
141
+ # first (see HandoverMixin._unoffer).
142
+ st.Store(key='_offered', description='the fingerprint last handed over, per peer and shard',
143
+ kind=st.TABLE, shape=str, bound=4096, keyed_by='peer and shard'),
144
+ )
145
+
146
+ NEEDS = (
147
+ Need('keystore', 'ks'),
148
+ Need('my_store', 'stores.events'),
149
+ Need('transport'),
150
+ Need('grant_store', 'stores.grants'),
151
+ Need('budget'),
152
+ Need('session_cls', 'cfg', field='_session_cls'),
153
+ Need('strain'),
154
+ Need('accounts', 'stores.accounts'),
155
+ Need('events'),
156
+ Need('discovery', late=True),
157
+ Need('presence', late=True),
158
+ Need('reporter', late=True),
159
+ )
160
+
161
+ def bind(self, discovery: Any = None, presence: Any = None) -> None:
162
+ """The two soft dependencies that tick after sync: discovery holds
163
+ the coverage claims a handover rests on, presence says whether a
164
+ peer that went quiet had announced it was leaving. Both are built
165
+ after sync, so they are bound once all three exist; either may stay
166
+ None (a bare SyncPeer in a suite), and the handover and partition
167
+ paths check for that."""
168
+ self.discovery = discovery
169
+ self.presence = presence
170
+
171
+ def start(self) -> None:
172
+ if self.reporter is not None and self.reporter.on_session not in self.on_session:
173
+ self.on_session.append(self.reporter.on_session)
174
+ if self.events is not None:
175
+ events = self.events
176
+ self.on_session.append(lambda *a: events.emit(eventbus_mod.TOPIC_SESSION_COMPLETED, *a))
177
+
178
+ def __init__(
179
+ self,
180
+ keystore: Any,
181
+ my_store: Any,
182
+ transport: Any,
183
+ grant_store: Any,
184
+ clock: Optional[Callable[[], int]] = None,
185
+ budget: Optional[Any] = None,
186
+ session_cls: Optional[Any] = None,
187
+ strain: Optional[Any] = None,
188
+ node: Optional[Any] = None,
189
+ accounts: Optional[Any] = None,
190
+ events: Optional[Any] = None,
191
+ discovery: Optional[Any] = None,
192
+ presence: Optional[Any] = None,
193
+ reporter: Optional[Any] = None,
194
+ ) -> None:
195
+ super().__init__(node, clock)
196
+ self.strain: Optional[Any] = strain # what this node has survived, or None
197
+ self.accounts: Optional[Any] = accounts # where handover chains are kept, or None
198
+ self.discovery = discovery # bound after assembly (ticks after sync)
199
+ self.presence = presence # bound after assembly (ticks after sync)
200
+ self.reporter = reporter
201
+ self.events = events
202
+ self.session_cls: Any = session_cls or sync.Session # the bad peer plugs in here
203
+ self.ks: Any = keystore
204
+ self.store: Any = my_store
205
+ self.transport: Any = transport
206
+ self.grants: Any = grant_store
207
+ self.budget: Optional[Any] = budget # meters every message both ways, if present
208
+ self.on_session: list[Callable[[str, str, SyncSessionResult], Any]] = [] # callbacks (peer, role, result) when a session ends
209
+ # The stores were built and bound by the base (see STORES).
210
+ self.totals: st.Counters
211
+ self.aborted: st.Ring[tuple[str, str]] # (peer, thid) sessions a peer stopped answering
212
+ self.refused: st.Ring[tuple[str, Optional[str], str]] # (peer, thid, why) sessions the grant did not cover
213
+ self.sessions: st.Table[str, tuple[str, Any, int]] # thid -> (peer_pk, Session, last activity)
214
+ # The sessions this node opened, by thid. A message is charged to the
215
+ # session's initiator on both sides, and this is how the wire knows
216
+ # which side that was. Kept after the session ends: a late message on
217
+ # a finished thid is still that session's, and still the initiator's.
218
+ self._initiated: st.Members[str]
219
+ self.rejected: st.Ring[tuple[str, str, str]] # (peer, kind, why) messages that did not have the shape of their type
220
+ # (peer, type, why, at). The time is what stops one message lost in a
221
+ # restart warning for the rest of the process: this peering completed
222
+ # thirty-four sessions while still reporting it.
223
+ self.unsent: st.Ring[tuple[str, str, str, int]] # (peer, type, why, at)
224
+ self.completed: st.Ring[tuple[str, str, SyncSessionResult]] # (peer_pk, role, counters, findings, status)
225
+ self.handed: st.Ring[tuple[str, str, str, int]] # (peer_pk, shard, fingerprint, at) handovers this node signed and sent
226
+ self.held: st.Ring[tuple[str, str, int]] # (peer_pk, shard, at) handovers this node accepted
227
+ self._offered: st.Table[tuple[str, str], str] # (peer_pk, shard) -> fingerprint last handed, so equality twice is one offer
228
+ self._lock: threading.Lock = threading.Lock()
229
+
230
+ def _record(self, ring: 'st.Ring[Any]', entry: tuple[Any, ...]) -> None:
231
+ """One session's end: kept in its ring, and counted in `totals`."""
232
+ ring.append(entry)
233
+ self.totals.incr(ring.store.key)
234
+
235
+ def _grant(self, peer_pk: str, direction: str) -> Any:
236
+ row = self.grants.get(peer_pk, direction)
237
+ if row is None or row['status'] != 'live':
238
+ raise SyncError('no live %s grant with %s' % (direction, peer_pk[:16]))
239
+ return sync.Grant.from_payload(row['payload'], row['envelope_id'])
240
+
241
+ def _send(self, m: Mapping[str, Any], peer_pk: str) -> dict[str, tuple[bool, str]]:
242
+ msg_dict = dict(m)
243
+ msg_dict['from'] = self.ks.did
244
+ msg_dict['to'] = [did_nostr(peer_pk)]
245
+ try:
246
+ res: dict[str, tuple[bool, str]] = self.wire_client.send(peer_pk, msg_dict)
247
+ except Exception as e:
248
+ # Nothing left the node: not metered, and said where an operator
249
+ # can see it rather than left as the peer's timeout.
250
+ self.unsent.append((peer_pk, msg_dict.get('type', '').rsplit('/', 1)[-1],
251
+ '%s: %s' % (type(e).__name__, e), self.clock()))
252
+ return {}
253
+ # Meter what a relay took, not what was attempted. A deliver every
254
+ # relay refused never reached the peer; counting it claims traffic the
255
+ # peer will measure as absent, and the window prices that gap as a
256
+ # misreport against the sender. Three machines showed it: 111 events
257
+ # claimed, 37 arrived, S2 against an honest node.
258
+ if res and not any(ok for ok, _ in res.values()):
259
+ why = '; '.join(n for _, n in res.values() if n)[:160] or 'refused'
260
+ self.unsent.append((peer_pk, msg_dict.get('type', '').rsplit('/', 1)[-1], why, self.clock()))
261
+ return res
262
+ if self.budget is not None:
263
+ self.budget.observe_out(peer_pk, msg_dict, i_initiated=msg_dict.get('thid') in self._initiated)
264
+ return res
265
+
266
+ def _seen(self, peer_pk: str, m: Mapping[str, Any]) -> None:
267
+ if self.budget is not None:
268
+ self.budget.observe_in(peer_pk, m, i_initiated=m.get('thid') in self._initiated)
269
+
270
+
271
+ # ---- the compartment contract -------------------------------------------------
272
+ def state(self) -> SyncStateReport:
273
+ return {
274
+ 'open': len(self.sessions),
275
+ 'refused': self.totals['refused'],
276
+ 'aborted': self.totals['aborted'],
277
+ 'completed': self.totals['completed'],
278
+ 'unsent': len(self.unsent),
279
+ 'malformed': len(self.rejected),
280
+ 'handed': [{'peer': p, 'shard': sh, 'at': at} for p, sh, _, at in self.handed.last(20)],
281
+ 'held': [{'peer': p, 'shard': sh, 'at': at} for p, sh, at in self.held.last(20)],
282
+ }
283
+
284
+ def trouble(self) -> list[str]:
285
+ out: list[str] = []
286
+ recent = self.refusals.within(self.TROUBLE_WINDOW, self.clock())
287
+ if recent:
288
+ out.append('%d sync session(s) refused: %s'
289
+ % (len(recent), '; '.join(sorted({r.why for r in recent}))[:120]))
290
+ still = [u for u in self.unsent if self.clock() - u[-1] <= self.TROUBLE_WINDOW]
291
+ if still:
292
+ # A relay that refused a message says nothing to the peer, which
293
+ # waits on an envelope that never finishes. Only the sender knows.
294
+ out.append('%d message(s) no relay would take - the peer is waiting on an '
295
+ 'envelope that will never finish' % len(still))
296
+ return out
297
+
298
+
299
+ def _expire(self, now: int) -> list[str]:
300
+ """Forget sessions whose peer went quiet. No finding: quiet is not lying."""
301
+ with self._lock:
302
+ stale = [t for t, v in self.sessions.items() if now - v[2] > SESSION_IDLE]
303
+ for t in stale:
304
+ self.sessions.pop(t, None)
305
+ return stale
306
+
307
+ def _finish(self, thid: str, peer_pk: str, sess: Any, status: Any) -> None:
308
+ with self._lock:
309
+ self.sessions.pop(thid, None)
310
+ self._record(self.completed, (peer_pk, 'responder', {'status': status, 'counters': dict(sess.counters),
311
+ 'findings': list(sess.findings), 'thid': thid}))
@@ -0,0 +1,106 @@
1
+ """The wire vocabulary of sync/1.0 as this node carries it: the PIURI, the
2
+ session bounds, the delivery-envelope attachment it sends and the
3
+ verification it demands of one it receives."""
4
+ from __future__ import annotations
5
+
6
+ import os
7
+ from typing import Any, Mapping, Sequence
8
+
9
+ from fleetmesh_core.storage import replication, sync
10
+ from node.interfaces import (
11
+ DIDCommAttachment,
12
+ DIDCommMessage,
13
+ DeliveryEnvelope,
14
+ NostrEvent,
15
+ )
16
+
17
+
18
+ PIURI: str = sync.PIURI
19
+ ATTACHMENT: str = sync.ENVELOPE_ATTACHMENT
20
+ # A peer could open sessions and never finish them, one per thread id, and
21
+ # the responder kept every one. Four at once is the old conn_max advisory
22
+ # ceiling for probation and member; an idle session is forgotten after the
23
+ # receipt cadence has passed twice.
24
+ MAX_SESSIONS_PER_PEER: int = 4
25
+ SESSION_IDLE: int = 1200
26
+ # The cap above is per peer; this one is the node's. Many peers each within
27
+ # their four can still add up, and past this the open is refused with a done
28
+ # rather than one of the sessions already answering dropped.
29
+ SESSIONS_CEILING: int = 1024
30
+ # The history SyncPeer keeps: RECENT sessions of each ending (the counts are
31
+ # in `totals`), and KEEP of each message log, the bound they were trimmed to
32
+ # by hand before they were declared.
33
+ RECENT: int = 256
34
+ KEEP: int = 200
35
+
36
+
37
+ class SyncError(Exception):
38
+ pass
39
+
40
+
41
+ def _thid(shard: str) -> str:
42
+ return 'sync-%s-%s' % (shard, os.urandom(4).hex())
43
+
44
+
45
+ def envelope_attachment(
46
+ events: Sequence[NostrEvent],
47
+ node_did: str,
48
+ sk: str,
49
+ now: int,
50
+ ) -> DIDCommAttachment:
51
+ """One delivery envelope per event, each with this node's hop appended."""
52
+ return {
53
+ 'id': ATTACHMENT,
54
+ 'mime_type': 'application/json',
55
+ 'data': {
56
+ 'json': {
57
+ 'envelopes': [
58
+ replication.append_hop({'event': ev, 'path': []}, node_did, sk, now)
59
+ for ev in events
60
+ ]
61
+ }
62
+ },
63
+ }
64
+
65
+
66
+ def verified_events(
67
+ message: DIDCommMessage | Mapping[str, Any],
68
+ findings: list[Any],
69
+ ) -> tuple[list[NostrEvent], dict[str, DeliveryEnvelope]]:
70
+ """(events, {event id: envelope}) for the deliveries whose envelopes verify.
71
+
72
+ A deliver carries its events in the delivery-envelope attachment and
73
+ nowhere else. The envelope is what makes a delivery citable: the peer
74
+ signed the hop even for an event somebody else authored, so it is the one
75
+ artefact that proves this peer sent this, and the only evidence available
76
+ for an offence the sender did not author.
77
+
78
+ Which is why a deliver with no attachment is refused rather than read out
79
+ of the body. Events accepted without provenance are events whose delivery
80
+ can never be reported: the reporter can cite an envelope, or an event the
81
+ peer itself authored, and a third party's event with no envelope is
82
+ neither. Omitting the attachment was a free way to commit the interest
83
+ offence unpunishably. A hop that does not verify is refused the same way.
84
+ """
85
+ body = message.get('body', {})
86
+ envs = None
87
+ for att in message.get('attachments', []) or []:
88
+ if att.get('id') == ATTACHMENT:
89
+ envs = (att.get('data', {}).get('json') or {}).get('envelopes')
90
+ if envs is None:
91
+ findings.append(('S3', 'deliver carries no %s attachment: %s event(s) with no provenance'
92
+ % (ATTACHMENT, body.get('count', len(body.get('events') or [])))))
93
+ return [], {}
94
+ kept: list[NostrEvent] = []
95
+ by_id: dict[str, DeliveryEnvelope] = {}
96
+ for env in envs:
97
+ problems = replication.verify_envelope(env)
98
+ if problems:
99
+ findings.append(('S3', 'delivery envelope refused: %s' % '; '.join(problems)))
100
+ continue
101
+ kept.append(env['event'])
102
+ by_id[env['event']['id']] = env
103
+ if body.get('count') is not None and body['count'] != len(envs):
104
+ findings.append(('S2', 'count says %d, %d envelope(s) attached' % (body['count'], len(envs))))
105
+ return kept, by_id
106
+
@@ -0,0 +1,206 @@
1
+ """The handover that may follow a done: offering a shard this session
2
+ earned, handing over everything on retirement, and accepting one from a
3
+ peer only if it fingerprints to what this node now holds."""
4
+ from __future__ import annotations
5
+
6
+ from typing import Any, Optional, Sequence
7
+
8
+ from fleetmesh_core.peering import coverage
9
+ from fleetmesh_core.wire.interfaces import NostrEvent
10
+ from fleetmesh_core.wire.wire_types import HandoverLink, SyncHandoverBody
11
+ from fleetmesh_core.identity.did import did_nostr
12
+ from . import rules as handover
13
+ from .envelope import _thid
14
+ from .shape import SyncShape
15
+
16
+
17
+ class HandoverMixin:
18
+ # ---- the handover ----------------------------------------------------------
19
+ def _offer_handover(self: SyncShape, thid: str, peer_pk: str, sess: Any, status: Any) -> Optional[str]:
20
+ """Hand the shard over if this session earned one. Returns what was done, or None.
21
+
22
+ Whether it earned one is `handover.offered_set`: this does the signing
23
+ and the sending, which are the parts that need a key and a transport.
24
+ The same set to the same peer is offered once.
25
+ """
26
+ discovery = self.discovery
27
+ claim = discovery.published_claim(sess.shard) if discovery is not None else None
28
+ now = self.clock()
29
+ fingerprint, _why = handover.offered_set(claim, sess, now, self.config('handover', 'offer'), status)
30
+ # offered_set returns None for a missing claim, so a fingerprint here
31
+ # means the claim exists; the check is stated for the type checker and
32
+ # for the next reader rather than left to be inferred.
33
+ if fingerprint is None or claim is None or self._offered.get((peer_pk, sess.shard)) == fingerprint:
34
+ return None
35
+ chain = self._chain_for(sess.shard, claim, peer_pk, now)
36
+ if chain is None:
37
+ return None
38
+ # Recorded before it is sent, like the done above it and the relay
39
+ # face's counters, and taken back if nothing left the node. Both
40
+ # orders are wrong in one direction. Written after the send, a peer
41
+ # holding the artefact can ask this node whether it handed one over
42
+ # and be told no. Written before and kept, a send no relay took is a
43
+ # handover this node believes it made and will never make again, so a
44
+ # replica that should be able to assert the shard silently cannot.
45
+ # The record goes first and the failure undoes it, which leaves
46
+ # handing the same set over twice as the only residual - and that is
47
+ # noise rather than damage: the artefact is signed and idempotent, and
48
+ # `_accept_handover` fingerprints what it is given against what the
49
+ # receiver holds, so the second copy is the first one again.
50
+ entry = (peer_pk, sess.shard, fingerprint, now)
51
+ previous = self._offered.get((peer_pk, sess.shard))
52
+ self._offered[(peer_pk, sess.shard)] = fingerprint
53
+ with self._lock:
54
+ self.handed.append(entry)
55
+ # Inserts, not the length: at its bound the log drops one for each it
56
+ # takes, so a failure there left the length unchanged and the memo of
57
+ # a handover that never left kept.
58
+ unsent = self.unsent.inserts
59
+ self._send(coverage.handover_message(chain, thid), peer_pk)
60
+ if self.unsent.inserts != unsent:
61
+ # `_send` is the one place that decides nothing left the node -
62
+ # the transport raised, or every relay refused - and it has
63
+ # already said so where the operator reads it. A second thread
64
+ # recording its own failure here would cost this offer a needless
65
+ # retry, which is the harmless direction.
66
+ self._unoffer((peer_pk, sess.shard), previous, entry)
67
+ return None
68
+ self.say('handed', shard=sess.shard, peer=peer_pk[:16], events=chain[-1]['count'])
69
+ return 'handed over ' + sess.shard
70
+
71
+ def _unoffer(self: SyncShape, key: tuple[str, str], previous: Optional[str], entry: tuple[Any, ...]) -> None:
72
+ """Undo the memo of a handover that never left the node."""
73
+ if previous is None:
74
+ self._offered.pop(key, None)
75
+ else:
76
+ self._offered[key] = previous
77
+ with self._lock:
78
+ self.handed.discard(entry)
79
+
80
+ def hand_over_all(self: SyncShape, peers: Sequence[str], now: Optional[int] = None) -> list[tuple[Any, ...]]:
81
+ """Hand every closed asserted shard to every peer named, on the way out.
82
+
83
+ The opportunistic offer waits for a peer to pull, which is the right
84
+ shape while a node is running and useless while it is retiring: a
85
+ drain would have to outlast the slowest peer's pull interval, and the
86
+ peer that is offline right now would never be reached at all. So a
87
+ retiring node sends the statement rather than waiting to be asked.
88
+
89
+ It is sent and not reconciled. Opening a session would contradict the
90
+ rung a drain is entering, put ingest load on neighbours whose own
91
+ capacity this node cannot see, and buy only the equality evidence the
92
+ receiver already checks for itself: `_accept_handover` refuses a
93
+ handover that does not fingerprint to what that node holds. So a
94
+ statement to a peer that does not hold the set is harmless, and one to
95
+ a peer that does is exactly right. The signature is the same one this
96
+ node would have given had the peer pulled.
97
+
98
+ Stored, because the peer this matters most to is the one that is
99
+ offline while its only first-hand holder retires.
100
+
101
+ Nothing here waits on an answer and nothing here may raise.
102
+ """
103
+ now_val = now if now is not None else self.clock()
104
+ did: list[tuple[Any, ...]] = []
105
+ discovery = self.discovery
106
+ if discovery is None or self.config('handover', 'offer') != 'offer':
107
+ return did
108
+ for claim in discovery.asserted_claims():
109
+ shard = [t[1] for t in claim['tags'] if t[0] == 'd'][0]
110
+ for peer_pk in peers:
111
+ entry: Optional[tuple[Any, ...]] = None
112
+ previous: Optional[str] = None
113
+ try:
114
+ chain = self._chain_for(shard, claim, peer_pk, now_val)
115
+ if chain is None:
116
+ continue
117
+ art = chain[-1]
118
+ msg = coverage.handover_message(chain, _thid(shard))
119
+ msg = dict(msg, **{'from': self.ks.did, 'to': [did_nostr(peer_pk)]})
120
+ # Same order and same reason as the opportunistic offer,
121
+ # and it matters more here: this is the last thing the
122
+ # node does for this shard, so a memo kept over a send
123
+ # that failed is a handover nobody will ever make.
124
+ entry = (peer_pk, shard, art['fingerprint'], now_val)
125
+ previous = self._offered.get((peer_pk, shard))
126
+ self._offered[(peer_pk, shard)] = art['fingerprint']
127
+ with self._lock:
128
+ self.handed.append(entry)
129
+ res = self.wire_client.send(peer_pk, msg, stored=True)
130
+ if res and not any(ok for ok, _ in res.values()):
131
+ self._unoffer((peer_pk, shard), previous, entry)
132
+ why = '; '.join(n for _, n in res.values() if n)[:160] or 'refused'
133
+ did.append(('handover unsent', shard, peer_pk, why))
134
+ continue
135
+ did.append(('handed over', shard, peer_pk, '%d events' % art['count']))
136
+ except Exception as e:
137
+ # One peer that cannot be told must not cost the others
138
+ # theirs. The memo written just above is taken back so a
139
+ # peer the transport could not address now is still owed
140
+ # the statement - and `entry` is None when the throw came
141
+ # before there was one to take back.
142
+ if entry is not None:
143
+ self._unoffer((peer_pk, shard), previous, entry)
144
+ did.append(('handover unsent', shard, peer_pk, '%s: %s' % (type(e).__name__, e)))
145
+ return did
146
+
147
+ def _chain_for(
148
+ self: SyncShape,
149
+ shard: str,
150
+ claim: NostrEvent,
151
+ peer_pk: str,
152
+ now: int,
153
+ ) -> Optional[list[HandoverLink]]:
154
+ """The chain to send: what this node was handed, plus its own link.
155
+
156
+ The signing is here because the key is; `handover.extend` decides
157
+ whether the result is a chain this node may stand behind.
158
+ """
159
+ try:
160
+ link = coverage.handover(self.ks.node_sk, peer_pk, claim, now)
161
+ except ValueError as e:
162
+ self.refusals.refuse('handover', str(e), subject=peer_pk, detail={'shard': shard}, at=now)
163
+ return None
164
+ accounts = self.accounts
165
+ held = (accounts.handover(shard) if accounts is not None else None) or []
166
+ chain, why = handover.extend(held, link)
167
+ if chain is None:
168
+ self.refusals.refuse('handover', why or 'the chain could not be extended', subject=peer_pk,
169
+ detail={'shard': shard}, at=now)
170
+ return chain
171
+
172
+ def _accept_handover(self: SyncShape, peer_pk: str, body: SyncHandoverBody) -> tuple[Any, ...]:
173
+ """One inbound sync/1.0/handover. Kept only if it is ours to carry.
174
+
175
+ `handover.acceptable` says whether; this does the looking up and the
176
+ keeping. The whole chain is kept, because the whole chain is what this
177
+ node will carry in its own claim and pass on if it hands the shard
178
+ along.
179
+ """
180
+ now = self.clock()
181
+ accounts = self.accounts
182
+ chain = [h for h in (body.get('chain') or []) if isinstance(h, dict)]
183
+ art = chain[-1] if chain else {}
184
+ shard_name = art.get('shard')
185
+ shard_cfg = (handover.shard_config(self.config('coverage'), shard_name)
186
+ if isinstance(shard_name, str) else None)
187
+ held_ids = None
188
+ why: Optional[str]
189
+ if accounts is None:
190
+ why = 'this node keeps no account store'
191
+ else:
192
+ if shard_cfg is not None and art:
193
+ held_ids = self.store.enumerate_range(shard_cfg['filter'], art.get('since') or 0,
194
+ art.get('until') or 0, now)
195
+ why = handover.acceptable(chain, self.ks.node_pk, peer_pk, shard_cfg, held_ids)
196
+ if why is None:
197
+ assert accounts is not None # why is only None on the branch that had one
198
+ accounts.put_handover(art['shard'], chain)
199
+ with self._lock:
200
+ self.held.append((peer_pk, art['shard'], now))
201
+ self.say('held', shard=art['shard'], peer=peer_pk[:16],
202
+ events=art['count'], chain_len=len(chain))
203
+ return ('handover', peer_pk, art['shard'])
204
+ self.refusals.refuse('handover', why, subject=peer_pk, detail={'shard': art.get('shard')}, at=now)
205
+ return ('handover refused', peer_pk, why)
206
+