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.
- fleetmesh_node-0.2.0.dist-info/METADATA +57 -0
- fleetmesh_node-0.2.0.dist-info/RECORD +338 -0
- fleetmesh_node-0.2.0.dist-info/WHEEL +5 -0
- fleetmesh_node-0.2.0.dist-info/entry_points.txt +2 -0
- fleetmesh_node-0.2.0.dist-info/licenses/LICENSE +121 -0
- fleetmesh_node-0.2.0.dist-info/top_level.txt +2 -0
- node/RELEASE +1 -0
- node/__init__.py +35 -0
- node/app/__init__.py +49 -0
- node/app/commands.py +265 -0
- node/app/config.py +131 -0
- node/app/control.py +670 -0
- node/app/core.py +867 -0
- node/app/plugins.py +76 -0
- node/app/router.py +83 -0
- node/app/socket.py +431 -0
- node/app/systemd.py +36 -0
- node/app/views.py +204 -0
- node/channels/__init__.py +378 -0
- node/channels/bus.py +331 -0
- node/channels/journal.py +46 -0
- node/commands/__init__.py +140 -0
- node/commands/adapter.py +210 -0
- node/commands/base.py +278 -0
- node/commands/cluster_cmds.py +183 -0
- node/commands/compute_cmds.py +401 -0
- node/commands/core.py +474 -0
- node/commands/gossip_cmds.py +106 -0
- node/commands/nsite_cmds.py +141 -0
- node/commands/peering.py +402 -0
- node/commands/plugin_cmds.py +135 -0
- node/commands/sidecar_cmds.py +120 -0
- node/commands/sync.py +388 -0
- node/commands/transport_cmds.py +451 -0
- node/commands/video_cmds.py +99 -0
- node/compartment/__init__.py +124 -0
- node/compartment/base.py +868 -0
- node/compartment/command.py +458 -0
- node/compartment/context.py +163 -0
- node/compartment/every.py +108 -0
- node/compartment/handler.py +131 -0
- node/compartment/lifecycle.py +460 -0
- node/compartment/lint.py +388 -0
- node/compartment/need.py +158 -0
- node/compartment/problem.py +521 -0
- node/compartment/refusals.py +120 -0
- node/compartment/registry.py +790 -0
- node/compartment/schema.py +290 -0
- node/compartment/sidecar.py +499 -0
- node/compartment/stores.py +662 -0
- node/compartment/wire.py +225 -0
- node/compartment/worker.py +61 -0
- node/config/__init__.py +43 -0
- node/config/base.py +116 -0
- node/config/change.py +441 -0
- node/config/changestore.py +201 -0
- node/config/compartments.py +1144 -0
- node/config/node.py +318 -0
- node/config/parameter.py +563 -0
- node/config/protocols.py +274 -0
- node/config/records.py +171 -0
- node/examples/__init__.py +1 -0
- node/examples/demo.py +226 -0
- node/examples/follower.py +46 -0
- node/interfaces.py +235 -0
- node/keystore/__init__.py +27 -0
- node/keystore/core.py +103 -0
- node/keystore/enclave.py +688 -0
- node/meshnode.py +111 -0
- node/metrics/__init__.py +396 -0
- node/metrics/adapters.py +102 -0
- node/metrics/face.py +110 -0
- node/planes/__init__.py +21 -0
- node/planes/identity/__init__.py +20 -0
- node/planes/identity/announce/__init__.py +9 -0
- node/planes/identity/announce/compartment.py +447 -0
- node/planes/identity/announce/types.py +26 -0
- node/planes/identity/discovery/__init__.py +9 -0
- node/planes/identity/discovery/compartment.py +522 -0
- node/planes/identity/discovery/types.py +13 -0
- node/planes/identity/linked/__init__.py +1 -0
- node/planes/identity/linked/compartment.py +270 -0
- node/planes/identity/linked/types.py +25 -0
- node/planes/identity/presence/__init__.py +7 -0
- node/planes/identity/presence/compartment.py +409 -0
- node/planes/identity/presence/types.py +14 -0
- node/planes/operations/__init__.py +13 -0
- node/planes/operations/scheduler/__init__.py +9 -0
- node/planes/operations/scheduler/compartment.py +353 -0
- node/planes/operations/scheduler/types.py +21 -0
- node/planes/operations/updates/__init__.py +17 -0
- node/planes/operations/updates/core.py +310 -0
- node/planes/operations/updates/store.py +79 -0
- node/planes/operations/updates/types.py +20 -0
- node/planes/peering/__init__.py +28 -0
- node/planes/peering/budget/__init__.py +43 -0
- node/planes/peering/budget/chain.py +43 -0
- node/planes/peering/budget/core.py +537 -0
- node/planes/peering/budget/evaluation.py +80 -0
- node/planes/peering/budget/meter.py +100 -0
- node/planes/peering/budget/receipts.py +439 -0
- node/planes/peering/budget/shape.py +101 -0
- node/planes/peering/budget/tick.py +110 -0
- node/planes/peering/budget/types.py +67 -0
- node/planes/peering/budget/windows.py +346 -0
- node/planes/peering/capacity/__init__.py +7 -0
- node/planes/peering/capacity/compartment.py +342 -0
- node/planes/peering/capacity/types.py +24 -0
- node/planes/peering/peering/__init__.py +25 -0
- node/planes/peering/peering/core.py +464 -0
- node/planes/peering/peering/envelope.py +142 -0
- node/planes/peering/peering/handshake.py +198 -0
- node/planes/peering/peering/shape.py +80 -0
- node/planes/peering/peering/types.py +39 -0
- node/planes/peering/reporter/__init__.py +42 -0
- node/planes/peering/reporter/constants.py +29 -0
- node/planes/peering/reporter/core.py +299 -0
- node/planes/peering/reporter/finding.py +74 -0
- node/planes/peering/reporter/findings.py +171 -0
- node/planes/peering/reporter/publish.py +285 -0
- node/planes/peering/reporter/reading.py +193 -0
- node/planes/peering/reporter/shape.py +86 -0
- node/planes/peering/reporter/types.py +50 -0
- node/planes/peering/strain/__init__.py +9 -0
- node/planes/peering/strain/compartment.py +204 -0
- node/planes/peering/strain/types.py +21 -0
- node/planes/peering/trust/__init__.py +9 -0
- node/planes/peering/trust/compartment.py +239 -0
- node/planes/peering/trust/types.py +30 -0
- node/planes/services/__init__.py +33 -0
- node/planes/services/compute/__init__.py +160 -0
- node/planes/services/compute/common.py +134 -0
- node/planes/services/compute/compartment.py +681 -0
- node/planes/services/compute/executors/__init__.py +121 -0
- node/planes/services/compute/executors/agent.py +840 -0
- node/planes/services/compute/executors/bacalhau.py +605 -0
- node/planes/services/compute/executors/base.py +127 -0
- node/planes/services/compute/executors/harness.py +439 -0
- node/planes/services/compute/executors/mcp.py +454 -0
- node/planes/services/compute/executors/media.py +898 -0
- node/planes/services/compute/executors/nondet.py +283 -0
- node/planes/services/compute/executors/providers.py +499 -0
- node/planes/services/compute/executors/registry.py +140 -0
- node/planes/services/compute/executors/wasm.py +244 -0
- node/planes/services/compute/executors/zk.py +604 -0
- node/planes/services/compute/jobs.py +227 -0
- node/planes/services/compute/load.py +180 -0
- node/planes/services/compute/market.py +433 -0
- node/planes/services/compute/sandbox.py +282 -0
- node/planes/services/compute/types.py +84 -0
- node/planes/services/compute_wire/__init__.py +58 -0
- node/planes/services/compute_wire/compartment.py +256 -0
- node/planes/services/compute_wire/messages.py +320 -0
- node/planes/services/compute_wire/sealing.py +147 -0
- node/planes/services/compute_wire/session.py +572 -0
- node/planes/services/compute_wire/types.py +49 -0
- node/planes/services/compute_wire/verify.py +331 -0
- node/planes/services/dvm/__init__.py +24 -0
- node/planes/services/dvm/gateway.py +526 -0
- node/planes/services/dvm/protocol.py +35 -0
- node/planes/services/dvm/translate.py +15 -0
- node/planes/services/dvm/types.py +25 -0
- node/planes/services/gateway/__init__.py +40 -0
- node/planes/services/gateway/settle.py +279 -0
- node/planes/services/gateway/translate.py +644 -0
- node/planes/services/gossip/__init__.py +22 -0
- node/planes/services/gossip/core.py +929 -0
- node/planes/services/gossip/envelope.py +154 -0
- node/planes/services/gossip/mesh.py +127 -0
- node/planes/services/gossip/types.py +50 -0
- node/planes/services/nsite/__init__.py +7 -0
- node/planes/services/nsite/client.py +62 -0
- node/planes/services/nsite/compartment.py +752 -0
- node/planes/services/nsite/types.py +44 -0
- node/planes/services/paidfetch/__init__.py +7 -0
- node/planes/services/paidfetch/compartment.py +862 -0
- node/planes/services/paidfetch/types.py +99 -0
- node/planes/services/settle/__init__.py +27 -0
- node/planes/services/settle/compartment.py +384 -0
- node/planes/services/settle/engine.py +456 -0
- node/planes/services/settle/gateways.py +541 -0
- node/planes/services/settle/payment.py +512 -0
- node/planes/services/settle/store.py +74 -0
- node/planes/services/settle/types.py +58 -0
- node/planes/services/sidecars/__init__.py +8 -0
- node/planes/services/sidecars/compartment.py +461 -0
- node/planes/services/sidecars/manifest.py +304 -0
- node/planes/services/sidecars/manifests/10-grasp.json +37 -0
- node/planes/services/sidecars/manifests/20-ncontainer.json +31 -0
- node/planes/services/sidecars/manifests/30-magpie.json +28 -0
- node/planes/services/sidecars/types.py +43 -0
- node/planes/services/video/__init__.py +106 -0
- node/planes/services/video/barter.py +407 -0
- node/planes/services/video/cli.py +158 -0
- node/planes/services/video/compartment.py +932 -0
- node/planes/services/video/ingest.py +524 -0
- node/planes/services/video/ledger.py +705 -0
- node/planes/services/video/packager.py +847 -0
- node/planes/services/video/player.py +456 -0
- node/planes/services/video/timed_segments.py +124 -0
- node/planes/services/video/transit.py +478 -0
- node/planes/store/__init__.py +24 -0
- node/planes/store/blob_replication/__init__.py +7 -0
- node/planes/store/blob_replication/compartment.py +496 -0
- node/planes/store/blob_replication/types.py +35 -0
- node/planes/store/blob_wire/__init__.py +48 -0
- node/planes/store/blob_wire/client.py +57 -0
- node/planes/store/blob_wire/core.py +359 -0
- node/planes/store/blob_wire/intake.py +74 -0
- node/planes/store/blob_wire/messages.py +85 -0
- node/planes/store/projected/__init__.py +7 -0
- node/planes/store/projected/compartment.py +258 -0
- node/planes/store/projected/types.py +29 -0
- node/planes/store/retention/__init__.py +7 -0
- node/planes/store/retention/compartment.py +214 -0
- node/planes/store/retention/types.py +12 -0
- node/planes/store/sync/__init__.py +35 -0
- node/planes/store/sync/core.py +311 -0
- node/planes/store/sync/envelope.py +106 -0
- node/planes/store/sync/handover.py +206 -0
- node/planes/store/sync/initiator.py +128 -0
- node/planes/store/sync/responder.py +114 -0
- node/planes/store/sync/rules.py +179 -0
- node/planes/store/sync/shape.py +80 -0
- node/planes/store/sync/types.py +51 -0
- node/planes/transport/__init__.py +23 -0
- node/planes/transport/bridge/__init__.py +13 -0
- node/planes/transport/bridge/compartment.py +785 -0
- node/planes/transport/bridge/ratelimit.py +44 -0
- node/planes/transport/bridge/types.py +43 -0
- node/planes/transport/lan/__init__.py +9 -0
- node/planes/transport/lan/compartment.py +446 -0
- node/planes/transport/lan/types.py +25 -0
- node/planes/transport/link/__init__.py +7 -0
- node/planes/transport/link/compartment.py +257 -0
- node/planes/transport/link/types.py +27 -0
- node/planes/transport/mediation/__init__.py +45 -0
- node/planes/transport/mediation/compartment.py +238 -0
- node/planes/transport/mediation/leaf.py +107 -0
- node/planes/transport/mediation/messages.py +302 -0
- node/planes/transport/mediation/service.py +233 -0
- node/planes/transport/mediation/store.py +37 -0
- node/planes/transport/mediation/types.py +20 -0
- node/planes/transport/p2p/__init__.py +7 -0
- node/planes/transport/p2p/compartment.py +349 -0
- node/planes/transport/p2p/types.py +22 -0
- node/protocols/__init__.py +277 -0
- node/protocols/activitypub/__init__.py +56 -0
- node/protocols/activitypub/actor.py +230 -0
- node/protocols/activitypub/federation.py +490 -0
- node/protocols/activitypub/media.py +145 -0
- node/protocols/activitypub/models.py +286 -0
- node/protocols/activitypub/package.py +271 -0
- node/protocols/activitypub/store.py +564 -0
- node/protocols/arcade/__init__.py +26 -0
- node/protocols/atproto/__init__.py +13 -0
- node/protocols/atproto/firehose.py +126 -0
- node/protocols/atproto/metering.py +165 -0
- node/protocols/atproto/package.py +155 -0
- node/protocols/atproto/repo.py +250 -0
- node/protocols/atproto/xrpc.py +285 -0
- node/protocols/base.py +109 -0
- node/protocols/circles/__init__.py +29 -0
- node/protocols/dm/__init__.py +50 -0
- node/protocols/grasp/__init__.py +22 -0
- node/protocols/isotime.py +15 -0
- node/protocols/nostr/__init__.py +51 -0
- node/protocols/nostr/blobstore.py +383 -0
- node/protocols/nostr/blossom.py +627 -0
- node/protocols/nostr/client.py +377 -0
- node/protocols/nostr/descriptor.py +211 -0
- node/protocols/nostr/eventstore.py +304 -0
- node/protocols/nostr/package.py +182 -0
- node/protocols/nostr/peers.py +109 -0
- node/protocols/nostr/relayserver.py +656 -0
- node/protocols/p2panda/__init__.py +22 -0
- node/protocols/p2panda/identity.py +19 -0
- node/protocols/p2panda/package.py +236 -0
- node/protocols/p2panda/rung.py +152 -0
- node/protocols/p2panda/store.py +273 -0
- node/protocols/rhio/__init__.py +33 -0
- node/protocols/rhio/package.py +275 -0
- node/protocols/rhio/rung.py +136 -0
- node/protocols/rhio/store.py +1423 -0
- node/protocols/ssb/muxrpc.py +256 -0
- node/protocols/ssb/package.py +267 -0
- node/protocols/textile/__init__.py +111 -0
- node/protocols/textile/transducers.py +259 -0
- node/release.py +119 -0
- node/storage/__init__.py +88 -0
- node/storage/accounts.py +305 -0
- node/storage/adapter.py +161 -0
- node/storage/base.py +385 -0
- node/storage/dialect.py +209 -0
- node/storage/grants.py +317 -0
- node/storage/locator.py +150 -0
- node/storage/postgres.py +294 -0
- node/storage/projected.py +953 -0
- node/storage/sharding.py +448 -0
- node/storage/standing.py +568 -0
- node/testnet/__init__.py +9 -0
- node/testnet/cluster.py +731 -0
- node/testnet/fips_daemon.py +230 -0
- node/transport/__init__.py +94 -0
- node/transport/const.py +110 -0
- node/transport/didcomm_inbound.py +251 -0
- node/transport/dnstxt.py +169 -0
- node/transport/fips_rung/__init__.py +67 -0
- node/transport/fips_rung/core.py +666 -0
- node/transport/fips_rung/listener.py +96 -0
- node/transport/fips_rung/types.py +12 -0
- node/transport/host.py +334 -0
- node/transport/iroh_rung/__init__.py +29 -0
- node/transport/iroh_rung/core.py +477 -0
- node/transport/iroh_rung/quic.py +942 -0
- node/transport/iroh_rung/types.py +22 -0
- node/transport/ladder.py +145 -0
- node/transport/lineproto.py +49 -0
- node/transport/mdns.py +303 -0
- node/transport/netfetch.py +283 -0
- node/transport/relay_rung/__init__.py +23 -0
- node/transport/relay_rung/cooldown.py +87 -0
- node/transport/relay_rung/core.py +138 -0
- node/transport/relay_rung/inbound.py +161 -0
- node/transport/relay_rung/ledger.py +104 -0
- node/transport/relay_rung/listen.py +121 -0
- node/transport/relay_rung/outbound.py +181 -0
- node/transport/relay_rung/shape.py +71 -0
- node/transport/relay_rung/types.py +58 -0
- node/transport/relay_rung/wire.py +63 -0
- node/transport/router.py +761 -0
- node/transport/rung.py +276 -0
- node/transport/ssb_rung.py +166 -0
- node/transport/ws.py +502 -0
- sidecar/fleetmesh_announce.py +338 -0
- sidecar/fleetmesh_p2p.py +373 -0
- sidecar/fleetmesh_rhio.py +352 -0
- 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
|
+
|