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,409 @@
|
|
|
1
|
+
"""What this node says it is alive, and what it makes of a peer saying so.
|
|
2
|
+
|
|
3
|
+
Three things that are one thing: the descriptor this node publishes, the
|
|
4
|
+
heartbeat that says the descriptor is still true, and the subscription that
|
|
5
|
+
reads a peer's. All three ran loose in the node's tick, and the pair of them
|
|
6
|
+
that can find a fault - a heartbeat whose class contradicts the peer's live
|
|
7
|
+
descriptor, one whose endpoint digest says the descriptor has gone stale - fed
|
|
8
|
+
a finding into the budget and recorded nothing an operator could read back.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from typing import Any, Callable, Optional, Sequence
|
|
15
|
+
|
|
16
|
+
from fleetmesh_core.crypto import giftwrap
|
|
17
|
+
from fleetmesh_core.identity import relaylist
|
|
18
|
+
from fleetmesh_core.services import lifecycle, telemetry
|
|
19
|
+
from fleetmesh_core.storage import store as ref_store
|
|
20
|
+
from fleetmesh_core.wire.interfaces import NostrEvent
|
|
21
|
+
from node.channels import journal
|
|
22
|
+
from node.compartment import stores as st
|
|
23
|
+
from node.protocols.nostr import client as relay
|
|
24
|
+
import dataclasses
|
|
25
|
+
from node.config.parameter import Parameter, ParamBlock
|
|
26
|
+
from node.channels import Channel, NOTE, INFO
|
|
27
|
+
from node.metrics import COUNTER, Metric
|
|
28
|
+
from node.compartment import Command, Compartment, Every, Need, Worker
|
|
29
|
+
from .types import PresenceStateReport
|
|
30
|
+
|
|
31
|
+
# One kind 21802 per window. The counters are the transport's and the
|
|
32
|
+
# strain ledger's totals, binned by fleetmesh_core/telemetry so nothing finer than
|
|
33
|
+
# an order of magnitude leaves the node and no peer is named.
|
|
34
|
+
TELEMETRY_WINDOW: int = 3600
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclasses.dataclass(frozen=True)
|
|
38
|
+
class PresenceConfig(ParamBlock):
|
|
39
|
+
key = 'presence'
|
|
40
|
+
name = 'presence'
|
|
41
|
+
DERIVED: bool = False
|
|
42
|
+
PARAMS = (
|
|
43
|
+
Parameter(
|
|
44
|
+
key='heartbeat', name='Heartbeat Enabled',
|
|
45
|
+
description='Whether to publish periodic heartbeat events',
|
|
46
|
+
type=bool, default=True, tags=('presence', 'heartbeat'),
|
|
47
|
+
),
|
|
48
|
+
Parameter(
|
|
49
|
+
key='telemetry_window', name='Telemetry Window',
|
|
50
|
+
description='Window size in seconds for telemetry aggregation',
|
|
51
|
+
type=int, default=3600, minimum=60, tags=('presence', 'telemetry'),
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
heartbeat: bool = True
|
|
55
|
+
telemetry_window: int = 3600
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Presence(Compartment):
|
|
59
|
+
requires: tuple[str, ...] = ('budget', 'strain') # strain totals are one of the five telemetry metrics
|
|
60
|
+
|
|
61
|
+
name: str = 'presence'
|
|
62
|
+
# `heartbeat` and `class` are read every tick but neither is live: a node's
|
|
63
|
+
# class is what the node IS, and a heartbeat switched off under a running
|
|
64
|
+
# node would take its liveness off the mesh without anything on the mesh
|
|
65
|
+
# being told. Both change on a restart.
|
|
66
|
+
reads: tuple[str, ...] = ('heartbeat', 'class', 'telemetry')
|
|
67
|
+
config_cls = PresenceConfig
|
|
68
|
+
NEEDS = (
|
|
69
|
+
Need('keystore', 'ks'),
|
|
70
|
+
Need('peers'),
|
|
71
|
+
Need('grant_store', 'stores.grants'),
|
|
72
|
+
Need('relays'),
|
|
73
|
+
Need('budget'),
|
|
74
|
+
Need('stop'),
|
|
75
|
+
Need('timeout'),
|
|
76
|
+
Need('strain'),
|
|
77
|
+
Need('descriptor'),
|
|
78
|
+
Need('rebuild_descriptor', 'build_descriptor'),
|
|
79
|
+
Need('transport'),
|
|
80
|
+
)
|
|
81
|
+
TELEMETRY_WINDOW: int = TELEMETRY_WINDOW
|
|
82
|
+
CHANNELS = (
|
|
83
|
+
Channel('peer_retiring', 'Peer indicated graceful retirement', level=NOTE,
|
|
84
|
+
say='peer {peer} says it is retiring; the partition that follows is not stress from that direction'),
|
|
85
|
+
Channel('inbound_event', 'Inbound heartbeat event processed', level=INFO,
|
|
86
|
+
say='< {event}'),
|
|
87
|
+
)
|
|
88
|
+
METRICS = (
|
|
89
|
+
Metric(name='presence_peers_retiring', description='Times peers reported retirement.',
|
|
90
|
+
kind=COUNTER, counts='peer_retiring', tags=('presence', 'peers')),
|
|
91
|
+
Metric(name='presence_events_inbound', description='Times heartbeat events were processed.',
|
|
92
|
+
kind=COUNTER, counts='inbound_event', tags=('presence', 'heartbeat')),
|
|
93
|
+
)
|
|
94
|
+
EVERY = (Every(seconds=TELEMETRY_WINDOW, run='_run_telemetry'),)
|
|
95
|
+
WORKERS = (Worker(run='_listen', name='presence-listen'),)
|
|
96
|
+
STORES = (
|
|
97
|
+
st.Store(
|
|
98
|
+
key='_seen',
|
|
99
|
+
description='peer -> the id of the last heartbeat read from it',
|
|
100
|
+
bound=1024,
|
|
101
|
+
evict=st.LRU,
|
|
102
|
+
kind=st.TABLE,
|
|
103
|
+
keyed_by='peer',
|
|
104
|
+
),
|
|
105
|
+
st.Store(
|
|
106
|
+
key='_retiring',
|
|
107
|
+
description='peers that said they were going, and when',
|
|
108
|
+
bound=1024,
|
|
109
|
+
evict=st.LRU,
|
|
110
|
+
kind=st.TABLE,
|
|
111
|
+
keyed_by='peer',
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
COMMANDS = (
|
|
115
|
+
Command(
|
|
116
|
+
key='show',
|
|
117
|
+
name='Presence',
|
|
118
|
+
description='the descriptor this node publishes, when it expires, and the peers it '
|
|
119
|
+
'has heard from',
|
|
120
|
+
tags=('presence', 'status'),
|
|
121
|
+
permission='presence/show',
|
|
122
|
+
run='state',
|
|
123
|
+
reply='{descriptor, expires, renews_at, last_heartbeat, inbox_relays, heard_from, '
|
|
124
|
+
'retiring}',
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def __init__(
|
|
129
|
+
self,
|
|
130
|
+
keystore: Any,
|
|
131
|
+
peers: Any,
|
|
132
|
+
grant_store: Any,
|
|
133
|
+
relays: Sequence[str],
|
|
134
|
+
budget: Optional[Any] = None,
|
|
135
|
+
stop: Optional[threading.Event] = None,
|
|
136
|
+
clock: Optional[Callable[[], int]] = None,
|
|
137
|
+
timeout: float = 15.0,
|
|
138
|
+
node: Optional[Any] = None,
|
|
139
|
+
strain: Optional[Any] = None,
|
|
140
|
+
descriptor: Optional[Callable[[], Optional[NostrEvent]]] = None,
|
|
141
|
+
rebuild_descriptor: Optional[Callable[[], NostrEvent]] = None,
|
|
142
|
+
transport: Optional[Any] = None,
|
|
143
|
+
publisher: Optional[Callable[[Sequence[str], Any], Any]] = None,
|
|
144
|
+
) -> None:
|
|
145
|
+
super().__init__(node, clock)
|
|
146
|
+
# How a heartbeat, telemetry or renewed descriptor reaches the relays;
|
|
147
|
+
# given so a suite can record them without a socket.
|
|
148
|
+
self.publisher: Callable[[Sequence[str], Any], Any] = publisher or relay.publish_all
|
|
149
|
+
# The transport whose nostr-rung counters the telemetry event reports.
|
|
150
|
+
self.transport: Optional[Any] = transport if transport is not None else getattr(node, 'transport', None)
|
|
151
|
+
# The node's current descriptor, and the one call that rebuilds it
|
|
152
|
+
# from what the compartments say now. Suites that pass a fake node
|
|
153
|
+
# get both off it.
|
|
154
|
+
self.descriptor: Callable[[], Optional[NostrEvent]] = descriptor or (lambda: getattr(node, 'descriptor', None))
|
|
155
|
+
self.rebuild_descriptor: Callable[[], NostrEvent] = rebuild_descriptor or (lambda: getattr(node, 'build_descriptor')())
|
|
156
|
+
self.ks: Any = keystore
|
|
157
|
+
self.peers: Any = peers
|
|
158
|
+
self.grants: Any = grant_store
|
|
159
|
+
self.relays: list[str] = list(relays) # not a store: the configured relays
|
|
160
|
+
self.budget: Optional[Any] = budget
|
|
161
|
+
self.strain: Optional[Any] = strain
|
|
162
|
+
self.timeout: float = timeout
|
|
163
|
+
self._stop: threading.Event = stop or getattr(self, '_stop_event', None) or threading.Event()
|
|
164
|
+
self._last_heartbeat: Optional[int] = None
|
|
165
|
+
self._last_telemetry: Optional[int] = None
|
|
166
|
+
# The relay list this node last published (kind 10050): the relays it
|
|
167
|
+
# reads, as a tuple, so a changed set is published once and an
|
|
168
|
+
# unchanged one never again.
|
|
169
|
+
self._inbox_said: Optional[tuple[str, ...]] = None
|
|
170
|
+
# The event itself, for whoever projects the floor (the linked face).
|
|
171
|
+
self.last_inbox: Optional[NostrEvent] = None
|
|
172
|
+
self._seen: st.Table[str, str] # peer -> the id of the last heartbeat read from it
|
|
173
|
+
# Peers that said they were going, and when. A partition from one of
|
|
174
|
+
# these is not stress: this node was told. Cleared when the peer is
|
|
175
|
+
# heard from again without the flag, because that is a node that came
|
|
176
|
+
# back and is expecting to be counted like any other.
|
|
177
|
+
self._retiring: st.Table[str, int]
|
|
178
|
+
|
|
179
|
+
# ---- what this node says --------------------------------------------------
|
|
180
|
+
def _renews_at(self) -> tuple[int, int]:
|
|
181
|
+
"""A descriptor is renewed inside the last tenth of its life."""
|
|
182
|
+
desc = self.descriptor()
|
|
183
|
+
created_at = desc.get('created_at', self.clock()) if desc else self.clock()
|
|
184
|
+
exp = None
|
|
185
|
+
if desc:
|
|
186
|
+
for t in desc.get('tags', []):
|
|
187
|
+
if isinstance(t, (list, tuple)) and len(t) > 1 and t[0] == 'expiration':
|
|
188
|
+
try:
|
|
189
|
+
exp = int(t[1])
|
|
190
|
+
break
|
|
191
|
+
except (ValueError, TypeError):
|
|
192
|
+
pass
|
|
193
|
+
if exp is None:
|
|
194
|
+
exp = created_at + 86400
|
|
195
|
+
return exp, exp - (exp - created_at) // 10
|
|
196
|
+
|
|
197
|
+
def publish_inbox(self, now: int) -> Optional[NostrEvent]:
|
|
198
|
+
"""The kind 10050 that states this node's relay floor: the relays it
|
|
199
|
+
reads are where a stored wrap for it goes (NIP-17). Every node
|
|
200
|
+
publishes one - the relay rung is the one way a node can always be
|
|
201
|
+
reached, and this is where that is said, in the place any Nostr
|
|
202
|
+
client already looks. Published on the first tick and again only
|
|
203
|
+
when the set changes; None when nothing was due."""
|
|
204
|
+
if not self.relays:
|
|
205
|
+
return None
|
|
206
|
+
now_set = tuple(self.relays)
|
|
207
|
+
if self._inbox_said == now_set:
|
|
208
|
+
return None
|
|
209
|
+
tags = relaylist.build(self.relays)
|
|
210
|
+
ev: NostrEvent = ref_store.make_signed_event(self.ks.node_sk, now, relaylist.KIND, tags, '')
|
|
211
|
+
self.publisher(self.relays, ev)
|
|
212
|
+
self._inbox_said = now_set
|
|
213
|
+
self.last_inbox = ev
|
|
214
|
+
return ev
|
|
215
|
+
|
|
216
|
+
def tick(self, now: int) -> list[tuple[Any, ...]]:
|
|
217
|
+
did: list[tuple[Any, ...]] = []
|
|
218
|
+
inbox = self.publish_inbox(now)
|
|
219
|
+
if inbox is not None:
|
|
220
|
+
did.append(('inbox relays published', inbox['id']))
|
|
221
|
+
exp, renew = self._renews_at()
|
|
222
|
+
if now >= renew:
|
|
223
|
+
desc = self.rebuild_descriptor()
|
|
224
|
+
if self.relays:
|
|
225
|
+
self.publisher(self.relays, desc)
|
|
226
|
+
did.append(('descriptor renewed', desc['id']))
|
|
227
|
+
# anchors must, edges should, leaves never
|
|
228
|
+
if self.config('heartbeat', True) and lifecycle.heartbeat_due(
|
|
229
|
+
self.config('class'), self._last_heartbeat, now):
|
|
230
|
+
cur = self.descriptor()
|
|
231
|
+
eps = [t for t in cur['tags'] if t[0] == 'endpoint'] if cur is not None else []
|
|
232
|
+
hb = lifecycle.heartbeat(self.ks.node_pk, self.config('class'), eps, now)
|
|
233
|
+
signed_hb = ref_store.make_signed_event(self.ks.node_sk, now, hb['kind'], hb['tags'], hb['content'])
|
|
234
|
+
if self.relays:
|
|
235
|
+
self.publisher(self.relays, signed_hb)
|
|
236
|
+
self._last_heartbeat = now
|
|
237
|
+
did.append(('heartbeat', signed_hb['id']))
|
|
238
|
+
did += self.run_every(now)
|
|
239
|
+
return did
|
|
240
|
+
|
|
241
|
+
_tick_runs_every = True
|
|
242
|
+
|
|
243
|
+
def _run_telemetry(self, now: Optional[int] = None) -> list[tuple[Any, ...]]:
|
|
244
|
+
now = now if now is not None else self.clock()
|
|
245
|
+
if not (self.config('telemetry', False) and self.relays):
|
|
246
|
+
return []
|
|
247
|
+
tm = self._telemetry(now)
|
|
248
|
+
signed_tm = ref_store.make_signed_event(self.ks.node_sk, now, tm['kind'], tm['tags'], tm['content'])
|
|
249
|
+
self.publisher(self.relays, signed_tm)
|
|
250
|
+
self._last_telemetry = now
|
|
251
|
+
return [('telemetry', signed_tm['id'])]
|
|
252
|
+
|
|
253
|
+
# One kind 21802 per window. The counters are the transport's and the
|
|
254
|
+
# strain ledger's totals, binned by fleetmesh_core/telemetry so nothing finer than
|
|
255
|
+
# an order of magnitude leaves the node and no peer is named.
|
|
256
|
+
|
|
257
|
+
def _telemetry(self, now: int) -> Any:
|
|
258
|
+
tr = getattr(self.transport, 'nostr', None) if self.transport is not None else None
|
|
259
|
+
counters: dict[str, int] = {'peers': len(self.grants.peers())}
|
|
260
|
+
if tr is not None:
|
|
261
|
+
counters['wraps_in'] = sum(getattr(tr, 'wraps_in', {}).values())
|
|
262
|
+
counters['wraps_seen_again'] = sum(getattr(tr, 'wraps_seen_again', {}).values())
|
|
263
|
+
counters['wraps_dropped'] = len(getattr(tr, 'dropped', ()))
|
|
264
|
+
if self.strain is not None:
|
|
265
|
+
counters['strain'] = sum(self.strain.by_direction().values())
|
|
266
|
+
return telemetry.telemetry(self.ks.node_pk, now, self.TELEMETRY_WINDOW, counters,
|
|
267
|
+
expiration=now + 2 * self.TELEMETRY_WINDOW)
|
|
268
|
+
|
|
269
|
+
def announce_retiring(self, now: Optional[int] = None) -> list[tuple[Any, ...]]:
|
|
270
|
+
"""Say this node is going, before it starts giving things away.
|
|
271
|
+
|
|
272
|
+
Published directly rather than through the tick's due check: a
|
|
273
|
+
retirement does not wait for the hour to come round, and the whole
|
|
274
|
+
value of the flag is that it arrives before the silence it explains.
|
|
275
|
+
A leaf publishes no heartbeat and so cannot announce one, which is
|
|
276
|
+
correct - a leaf claims nothing and hands nothing over.
|
|
277
|
+
|
|
278
|
+
Ephemeral, like every heartbeat: only a peer listening now hears it.
|
|
279
|
+
That is the right answer and it is why it is advisory. A peer that was
|
|
280
|
+
itself down records the partition, and the partition is true.
|
|
281
|
+
"""
|
|
282
|
+
now_val = now if now is not None else self.clock()
|
|
283
|
+
if self.config('class') == 'leaf' or not self.relays or not self.config('heartbeat', True):
|
|
284
|
+
return []
|
|
285
|
+
desc = self.descriptor()
|
|
286
|
+
eps = [t for t in desc['tags'] if t[0] == 'endpoint'] if desc is not None else []
|
|
287
|
+
hb = lifecycle.heartbeat(self.ks.node_pk, self.config('class'), eps, now_val, load=1.0, retiring=True)
|
|
288
|
+
signed = ref_store.make_signed_event(self.ks.node_sk, now_val, hb['kind'], hb['tags'], hb['content'])
|
|
289
|
+
self.publisher(self.relays, signed)
|
|
290
|
+
self._last_heartbeat = now_val
|
|
291
|
+
return [('retiring announced', signed['id'])]
|
|
292
|
+
|
|
293
|
+
def is_retiring(self, peer_pk: str) -> bool:
|
|
294
|
+
"""Did this peer tell us it was going? Read by anything that would
|
|
295
|
+
otherwise price the silence that follows."""
|
|
296
|
+
return peer_pk in self._retiring
|
|
297
|
+
|
|
298
|
+
# ---- what a peer says -----------------------------------------------------
|
|
299
|
+
def on_heartbeat(self, hb: Any) -> list[tuple[Any, ...]]:
|
|
300
|
+
"""One kind 21801 from a peer, checked against what that peer published.
|
|
301
|
+
|
|
302
|
+
A node published one of these every hour and nothing ever read one, so
|
|
303
|
+
two rules the reference already implements were dead. A heartbeat whose
|
|
304
|
+
class contradicts the peer's live descriptor is a misreport about
|
|
305
|
+
itself and an S2. One whose endpoint digest differs from the
|
|
306
|
+
descriptor's says the descriptor has gone stale, which is the whole
|
|
307
|
+
reason that digest is in there: a peer can tell without dialling every
|
|
308
|
+
address it was given.
|
|
309
|
+
|
|
310
|
+
A heartbeat is *ephemeral*, so no relay stores one and none can be
|
|
311
|
+
asked for after the fact. Liveness is a statement about now, and the
|
|
312
|
+
only way to hear it is to be listening when it is made - which is why
|
|
313
|
+
this is fed by a subscription and not by a query. A node that was down
|
|
314
|
+
misses them, and that is the right answer.
|
|
315
|
+
"""
|
|
316
|
+
did: list[tuple[Any, ...]] = []
|
|
317
|
+
# A subscription by author returns whatever a relay chooses to serve
|
|
318
|
+
# under that name. Believing it unsigned would let a relay file an S2
|
|
319
|
+
# against any peer this node holds a grant with.
|
|
320
|
+
if not isinstance(hb, dict) or not giftwrap.verify_event(hb) or hb.get('kind') != lifecycle.HEARTBEAT:
|
|
321
|
+
self.refusals.refuse('heartbeat', 'not a signed kind 21801',
|
|
322
|
+
subject=(hb or {}).get('pubkey') if isinstance(hb, dict) else None)
|
|
323
|
+
return did
|
|
324
|
+
pk = hb.get('pubkey')
|
|
325
|
+
if not isinstance(pk, str) or pk not in set(self.grants.peers()):
|
|
326
|
+
self.refusals.refuse('heartbeat', 'from a node this one holds no grant with', subject=pk)
|
|
327
|
+
return did
|
|
328
|
+
hb_id = hb.get('id')
|
|
329
|
+
if not isinstance(hb_id, str) or self._seen.get(pk) == hb_id:
|
|
330
|
+
return did
|
|
331
|
+
self._seen[pk] = hb_id
|
|
332
|
+
desc = self.peers.descriptor(pk)
|
|
333
|
+
if desc is None:
|
|
334
|
+
self.refusals.refuse('heartbeat', 'the peer publishes no descriptor to check it against', subject=pk)
|
|
335
|
+
return did
|
|
336
|
+
problems, findings = lifecycle.check_heartbeat(hb, desc)
|
|
337
|
+
for severity, why in findings:
|
|
338
|
+
if self.budget is not None:
|
|
339
|
+
self.budget.findings.append((pk, None, severity, 'heartbeat: %s' % why))
|
|
340
|
+
self.refusals.refuse('heartbeat', 'contradicts the descriptor the peer publishes',
|
|
341
|
+
subject=pk, detail={'severity': severity, 'what': why})
|
|
342
|
+
did.append(('heartbeat contradicts its descriptor', pk, severity))
|
|
343
|
+
if lifecycle.is_retiring(hb):
|
|
344
|
+
# Not a finding and not a fault. The peer is telling us the silence
|
|
345
|
+
# that follows is a departure, so nothing downstream prices it.
|
|
346
|
+
if pk not in self._retiring:
|
|
347
|
+
self._retiring[pk] = self.clock()
|
|
348
|
+
self.say('peer_retiring', peer=pk[:16])
|
|
349
|
+
did.append(('peer is retiring', pk))
|
|
350
|
+
elif self._retiring.pop(pk, None) is not None:
|
|
351
|
+
did.append(('peer came back', pk))
|
|
352
|
+
if problems:
|
|
353
|
+
self.refusals.refuse('heartbeat', 'malformed', subject=pk,
|
|
354
|
+
detail={'problems': problems[:2]})
|
|
355
|
+
did.append(('heartbeat is malformed', pk, '; '.join(problems[:2])))
|
|
356
|
+
elif lifecycle.descriptor_is_stale(hb, desc):
|
|
357
|
+
self.peers.descriptor(pk, refresh=True)
|
|
358
|
+
did.append(('descriptor was stale, refreshed', pk))
|
|
359
|
+
return did
|
|
360
|
+
|
|
361
|
+
def _listen(self) -> None:
|
|
362
|
+
"""One subscription per relay for the liveness of peers we hold grants with."""
|
|
363
|
+
while not self._stop.is_set():
|
|
364
|
+
peers_held = self.grants.peers()
|
|
365
|
+
if not peers_held or not self.relays:
|
|
366
|
+
self._stop.wait(30)
|
|
367
|
+
continue
|
|
368
|
+
r = relay.Relay(self.relays[0], timeout=5.0)
|
|
369
|
+
try:
|
|
370
|
+
sid = r.connect().subscribe([{'kinds': [lifecycle.HEARTBEAT], 'authors': peers_held}])
|
|
371
|
+
started, watching = time.time(), set(peers_held)
|
|
372
|
+
for hb in r.events(sid):
|
|
373
|
+
if self._stop.is_set():
|
|
374
|
+
break
|
|
375
|
+
for d in self.on_heartbeat(hb):
|
|
376
|
+
self.say('inbound_event', event=' '.join(journal.short(x) for x in d))
|
|
377
|
+
# re-subscribe when the peer set has moved on
|
|
378
|
+
if time.time() - started > 300 and set(self.grants.peers()) != watching:
|
|
379
|
+
break
|
|
380
|
+
except Exception as e:
|
|
381
|
+
self.refusals.refuse('subscription', 'could not be held open',
|
|
382
|
+
subject=self.relays[0], detail={'error': str(e)})
|
|
383
|
+
finally:
|
|
384
|
+
r.close()
|
|
385
|
+
self._stop.wait(5)
|
|
386
|
+
|
|
387
|
+
# ---- being watched --------------------------------------------------------
|
|
388
|
+
def state(self) -> PresenceStateReport:
|
|
389
|
+
desc = self.descriptor()
|
|
390
|
+
exp, renew = self._renews_at() if desc else (None, None)
|
|
391
|
+
return {
|
|
392
|
+
'descriptor': desc['id'] if desc else None,
|
|
393
|
+
'expires': exp,
|
|
394
|
+
'renews_at': renew,
|
|
395
|
+
'last_heartbeat': self._last_heartbeat,
|
|
396
|
+
'inbox_relays': list(self._inbox_said or ()),
|
|
397
|
+
'heard_from': sorted(pk[:16] for pk in self._seen),
|
|
398
|
+
'retiring': sorted(pk[:16] for pk in self._retiring),
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
def trouble(self) -> list[str]:
|
|
402
|
+
out: list[str] = []
|
|
403
|
+
wrong = self.refusals.within(self.TROUBLE_WINDOW, self.clock(), kind='heartbeat',
|
|
404
|
+
why='contradicts the descriptor the peer publishes')
|
|
405
|
+
if wrong:
|
|
406
|
+
out.append('%d heartbeat(s) contradict the descriptor their peer publishes: %s. '
|
|
407
|
+
'Each is a finding against that peer'
|
|
408
|
+
% (len(wrong), ', '.join(sorted({(r.subject or '')[:16] for r in wrong}))))
|
|
409
|
+
return out
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Types and records for the Presence compartment."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Optional, TypedDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PresenceStateReport(TypedDict):
|
|
8
|
+
descriptor: Optional[str]
|
|
9
|
+
expires: Optional[int]
|
|
10
|
+
renews_at: Optional[int]
|
|
11
|
+
last_heartbeat: Optional[int]
|
|
12
|
+
inbox_relays: list[str]
|
|
13
|
+
heard_from: list[str]
|
|
14
|
+
retiring: list[str]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Operations plane: node lifecycle, task scheduling, health, and software updates.
|
|
2
|
+
|
|
3
|
+
The 2 compartments of the operations plane:
|
|
4
|
+
- scheduler: cadence pacing, peer sync throttling, and task scheduling
|
|
5
|
+
- updates: check-and-report software updates and release notifications
|
|
6
|
+
"""
|
|
7
|
+
from .scheduler import Scheduler
|
|
8
|
+
from .updates import Updates
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
'Scheduler',
|
|
12
|
+
'Updates',
|
|
13
|
+
]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""The pulls this node is configured to make, and every reason one did not happen.
|
|
2
|
+
|
|
3
|
+
One-module compartment: everything is in compartment.py.
|
|
4
|
+
"""
|
|
5
|
+
from .compartment import Scheduler
|
|
6
|
+
from .types import SchedulerPullStatus, SchedulerStateReport
|
|
7
|
+
|
|
8
|
+
__all__ = ['Scheduler', 'SchedulerPullStatus', 'SchedulerStateReport']
|
|
9
|
+
|