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
node/metrics/face.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""The relay face's metrics, as declarations.
|
|
2
|
+
|
|
3
|
+
`relayserver.render_metrics` built these eight by hand, with the names, HELP
|
|
4
|
+
and TYPE typed inline and every reading wrapped in `hasattr` plus a bare
|
|
5
|
+
`except: pass`. They were the only metrics the node exposed.
|
|
6
|
+
|
|
7
|
+
Declaring them changes nothing an operator scrapes, and that is the point.
|
|
8
|
+
Every exposed name and every TYPE is reproduced exactly, because a metric
|
|
9
|
+
name is an interface: a dashboard, an alert rule and a recording rule are all
|
|
10
|
+
written against it, and renaming one to suit a convention breaks them
|
|
11
|
+
silently. Two of these are not what their names suggest - `fleetmesh_blobs_total`
|
|
12
|
+
is a gauge despite the counter suffix, and `fleetmesh_events_stored_total`
|
|
13
|
+
counts what is in the store now and can go down - and both are kept as they
|
|
14
|
+
are rather than corrected. The place to fix an exposition name is a release
|
|
15
|
+
note, not a refactor.
|
|
16
|
+
|
|
17
|
+
What does change: the readings come through `Monitors`, so a sampler that
|
|
18
|
+
raises is counted rather than swallowed by `except: pass`, and the face's
|
|
19
|
+
numbers sit in the same census as every compartment's.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Any, Callable, Mapping
|
|
24
|
+
|
|
25
|
+
from . import COUNTER, GAUGE, Metric
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _try(fn: Callable[[], Any], default: Any = 0) -> Any:
|
|
29
|
+
"""A reading, or the default.
|
|
30
|
+
|
|
31
|
+
The old code caught everything and moved on, which is why a store that
|
|
32
|
+
could not be counted reported zero and nobody knew. `Monitors.read`
|
|
33
|
+
counts a sampler that raises, so a sampler here lets a real error through
|
|
34
|
+
and only guards the shape - an object that does not implement `count` at
|
|
35
|
+
all, which is a configuration and not a fault.
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
return fn()
|
|
39
|
+
except (AttributeError, TypeError):
|
|
40
|
+
return default
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def face_metrics(face: Any) -> tuple[Metric, ...]:
|
|
44
|
+
"""The eight the relay face has always exposed, bound to this server.
|
|
45
|
+
|
|
46
|
+
`face` is the RelayServer. The samplers close over it rather than being
|
|
47
|
+
handed values, so each scrape reads the live number.
|
|
48
|
+
"""
|
|
49
|
+
def stored() -> int:
|
|
50
|
+
return int(_try(lambda: face.store.count()))
|
|
51
|
+
|
|
52
|
+
def conns() -> int:
|
|
53
|
+
with face._lock:
|
|
54
|
+
return len(face._conns)
|
|
55
|
+
|
|
56
|
+
def subs() -> int:
|
|
57
|
+
with face._lock:
|
|
58
|
+
return sum(len(s) for s in face._subs.values())
|
|
59
|
+
|
|
60
|
+
def blob_count() -> int:
|
|
61
|
+
if face.blob_store is None:
|
|
62
|
+
return 0
|
|
63
|
+
return int(_try(lambda: face.blob_store.count()))
|
|
64
|
+
|
|
65
|
+
def blob_bytes() -> int:
|
|
66
|
+
if face.blob_store is None:
|
|
67
|
+
return 0
|
|
68
|
+
return int(_try(lambda: face.blob_store.total_bytes(), 0) or 0)
|
|
69
|
+
|
|
70
|
+
def by_tier() -> Mapping[str, int]:
|
|
71
|
+
"""Peers per trust tier, which is the one labelled metric here."""
|
|
72
|
+
out: dict[str, int] = {}
|
|
73
|
+
if face.grants is None:
|
|
74
|
+
return out
|
|
75
|
+
for g in _try(lambda: list(face.grants.list()), []):
|
|
76
|
+
payload = g.get('payload')
|
|
77
|
+
t = payload.get('tier') if isinstance(payload, dict) else g.get('tier', 'unknown')
|
|
78
|
+
out[str(t)] = out.get(str(t), 0) + 1
|
|
79
|
+
return out
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
# Named `_total` and typed counter since it was written, though it
|
|
83
|
+
# counts what is in the store now and can fall. Kept as it is: the
|
|
84
|
+
# name is what alerts are written against.
|
|
85
|
+
Metric(name='events_stored', description='Total events stored in local store.',
|
|
86
|
+
kind=COUNTER, sample=stored, tags=('face', 'store')),
|
|
87
|
+
Metric(name='active_connections', description='Current active WebSocket connections.',
|
|
88
|
+
kind=GAUGE, sample=conns, tags=('face', 'connections')),
|
|
89
|
+
Metric(name='active_subscriptions', description='Current active event subscriptions.',
|
|
90
|
+
kind=GAUGE, sample=subs, tags=('face', 'subscriptions')),
|
|
91
|
+
Metric(name='events_accepted', description='Total valid events accepted through relay face.',
|
|
92
|
+
kind=COUNTER, sample=lambda: int(getattr(face, 'accepted', 0)),
|
|
93
|
+
tags=('face', 'events')),
|
|
94
|
+
Metric(name='events_refused_future',
|
|
95
|
+
description='Total events refused due to future timestamps.',
|
|
96
|
+
kind=COUNTER, sample=lambda: int(getattr(face, 'refused_future', 0)),
|
|
97
|
+
tags=('face', 'events', 'refusals')),
|
|
98
|
+
# A gauge wearing a counter's suffix, preserved deliberately.
|
|
99
|
+
Metric(name='blobs_total', description='Total content-addressed blobs in Blossom CAS.',
|
|
100
|
+
kind=GAUGE, sample=blob_count, tags=('face', 'blobs')),
|
|
101
|
+
Metric(name='blobs_bytes_total',
|
|
102
|
+
description='Total size in bytes of blobs in Blossom CAS.',
|
|
103
|
+
kind=GAUGE, sample=blob_bytes, tags=('face', 'blobs')),
|
|
104
|
+
Metric(name='peers_by_tier',
|
|
105
|
+
description='Number of peers categorized by trust tier.',
|
|
106
|
+
kind=GAUGE, labels=('tier',), sample=by_tier, tags=('face', 'peering')),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
__all__ = ['face_metrics']
|
node/planes/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Architectural planes of the Fleetmesh node.
|
|
2
|
+
|
|
3
|
+
As defined in fleetmesh.org/docs/implementing.md and enforced mechanically
|
|
4
|
+
by check-implementing.py:
|
|
5
|
+
1. identity (discovery, announce, presence, linked)
|
|
6
|
+
2. store (sync, retention, blob_wire, blob_replication)
|
|
7
|
+
3. peering (trust, strain, peering, budget, capacity, reporter)
|
|
8
|
+
4. transport (link, lan, p2p, mediation, bridge)
|
|
9
|
+
5. services (gossip, compute, compute_wire, settle, paidfetch, sidecars, dvm, nsite)
|
|
10
|
+
6. operations (scheduler, updates)
|
|
11
|
+
"""
|
|
12
|
+
from . import identity, store, peering, transport, services, operations
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
'identity',
|
|
16
|
+
'store',
|
|
17
|
+
'peering',
|
|
18
|
+
'transport',
|
|
19
|
+
'services',
|
|
20
|
+
'operations',
|
|
21
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Identity plane: discovery, announce, presence, linked."""
|
|
2
|
+
from . import announce, discovery, linked, presence
|
|
3
|
+
from .announce.compartment import Announce
|
|
4
|
+
from .discovery.compartment import Discovery, DiscoveryConfig
|
|
5
|
+
from .linked.compartment import LinkedConfig, LinkedFace
|
|
6
|
+
from .presence.compartment import Presence, PresenceConfig
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
'Announce',
|
|
10
|
+
'Discovery',
|
|
11
|
+
'DiscoveryConfig',
|
|
12
|
+
'LinkedConfig',
|
|
13
|
+
'LinkedFace',
|
|
14
|
+
'Presence',
|
|
15
|
+
'PresenceConfig',
|
|
16
|
+
'announce',
|
|
17
|
+
'discovery',
|
|
18
|
+
'linked',
|
|
19
|
+
'presence',
|
|
20
|
+
]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""The gossipsub announce topic, reached through a sidecar, as a compartment.
|
|
2
|
+
|
|
3
|
+
One-module compartment: everything is in compartment.py.
|
|
4
|
+
"""
|
|
5
|
+
from .compartment import Announce
|
|
6
|
+
from .types import AnnounceStateReport, AnnounceTotals
|
|
7
|
+
|
|
8
|
+
__all__ = ['Announce', 'AnnounceStateReport', 'AnnounceTotals']
|
|
9
|
+
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
"""The gossipsub announce topic, reached through a sidecar, as a compartment.
|
|
2
|
+
|
|
3
|
+
Bridge 2 in the specification: a node republishes its descriptor on
|
|
4
|
+
`/fleetmesh/announce/1` so that nodes which have never shared a relay with it
|
|
5
|
+
hear about it anyway. The relay path (bridge 1, `discovery_wire.py`) always
|
|
6
|
+
works; this one reaches further.
|
|
7
|
+
|
|
8
|
+
It is a sidecar and not a library because of what a library would cost. `fleetmesh_core/`
|
|
9
|
+
is standard library only and `node/` depends on `cryptography` and nothing
|
|
10
|
+
else - `ws.py` is a hand-rolled RFC 6455 for exactly that reason. Installing
|
|
11
|
+
the one maintained Python libp2p pulls in sixty-three packages, eight of them
|
|
12
|
+
compiled; it does not install on Python 3.14 at all; and it is trio where this
|
|
13
|
+
node is threads. A second implementer reading the specification should not
|
|
14
|
+
inherit that, so the libp2p stack lives in its own process with its own
|
|
15
|
+
dependencies and this file speaks to it over a unix socket in the node's
|
|
16
|
+
directory, one JSON object per line, the same shape `control.py` already uses.
|
|
17
|
+
|
|
18
|
+
The node never starts the sidecar and never needs it. No socket is not an
|
|
19
|
+
error, it is a node with one discovery bridge instead of two, and it is said
|
|
20
|
+
once rather than once a tick.
|
|
21
|
+
|
|
22
|
+
**Nothing here trusts the sidecar.** Every row it hands over goes through
|
|
23
|
+
`admit` - the same function the sidecar ran before gossipsub propagated
|
|
24
|
+
anything - and then through `peers.learn`, exactly as a descriptor fetched
|
|
25
|
+
from a relay does. The work floor is re-applied here because it is this
|
|
26
|
+
node's policy and not the sidecar's.
|
|
27
|
+
|
|
28
|
+
The one check `admit` needs from the sidecar is which libp2p peer originated
|
|
29
|
+
the message, because a Multikey is compared against it. A sidecar that lies
|
|
30
|
+
about the originator can only make this node reject a descriptor it would
|
|
31
|
+
have accepted; it cannot make it accept one, because the Multikey is inside
|
|
32
|
+
an event the node key signed.
|
|
33
|
+
|
|
34
|
+
The pilot bridge (node/sidecar_bridge): the link to the sidecar, saying it
|
|
35
|
+
once each way, the version gate, the `announce.sidecar` and `announce.probe`
|
|
36
|
+
commands and the restart all come from `Bridge`, as they will for every other
|
|
37
|
+
sidecar. What is left here is what is announce's own - the topic, `admit`,
|
|
38
|
+
what a drain may teach this node.
|
|
39
|
+
"""
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import hashlib
|
|
43
|
+
import json
|
|
44
|
+
import os
|
|
45
|
+
import tempfile
|
|
46
|
+
from typing import Any, Callable, Optional, cast
|
|
47
|
+
|
|
48
|
+
from node import channels as ch_mod
|
|
49
|
+
from node.protocols.nostr import descriptor as descriptor_mod
|
|
50
|
+
from node.compartment import lifecycle as lifecycle_mod
|
|
51
|
+
from node import metrics as mx_mod
|
|
52
|
+
from node.compartment import stores as st
|
|
53
|
+
from node.config import AnnounceConfig
|
|
54
|
+
from fleetmesh_core.crypto import keys
|
|
55
|
+
from fleetmesh_core.identity import identity
|
|
56
|
+
from fleetmesh_core.wire.interfaces import NostrEvent
|
|
57
|
+
from node.compartment import Command, Need
|
|
58
|
+
from node.compartment.sidecar import UP, Bridge, Sidecar, SidecarError, jsonline_call
|
|
59
|
+
|
|
60
|
+
SOCK: str = 'announce.sock'
|
|
61
|
+
PROTOCOL: str = 'fleetmesh-announce/1'
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
from .types import AnnounceStateReport
|
|
65
|
+
TOPIC: str = '/fleetmesh/announce/1'
|
|
66
|
+
MAX_LINE: int = 1 << 20
|
|
67
|
+
# What one drain may bring back. A topic is a flood channel with no grant in
|
|
68
|
+
# front of it, so the bound is here rather than in the sender's good manners.
|
|
69
|
+
DRAIN_MAX: int = 64
|
|
70
|
+
KIND: int = 11801
|
|
71
|
+
# What one descriptor may weigh on the topic. A kind 11801 carrying every tag
|
|
72
|
+
# the specification allows is a couple of kilobytes; ten times that is a
|
|
73
|
+
# payload nobody needs and everybody would carry.
|
|
74
|
+
PAYLOAD_MAX: int = 20000
|
|
75
|
+
# The proof-of-work floor this node requires of a descriptor heard on the
|
|
76
|
+
# topic. Local policy, not a protocol constant: two nodes with different
|
|
77
|
+
# floors interoperate, the stricter one simply propagates less. Zero means
|
|
78
|
+
# the descriptor's own declared target is the only bar, which is what a node
|
|
79
|
+
# on a private mesh wants.
|
|
80
|
+
POW_BITS: int = 0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def socket_path(directory: str, configured: Optional[str] = None) -> str:
|
|
84
|
+
"""Where the sidecar listens. Same derivation as the control socket, for
|
|
85
|
+
the same reason: macOS caps a unix socket path near a hundred bytes and a
|
|
86
|
+
test's temporary directory is longer than that."""
|
|
87
|
+
if configured:
|
|
88
|
+
return os.path.abspath(os.path.expanduser(configured))
|
|
89
|
+
real = os.path.realpath(directory)
|
|
90
|
+
p = os.path.join(real, SOCK)
|
|
91
|
+
if len(p.encode()) < 100:
|
|
92
|
+
return p
|
|
93
|
+
return os.path.join(tempfile.gettempdir(),
|
|
94
|
+
'fleetmesh-announce-%s.sock' % hashlib.sha256(real.encode()).hexdigest()[:16])
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def transport_multikey(desc: NostrEvent) -> Optional[str]:
|
|
98
|
+
"""The `key transport` Multikey a descriptor declares, or None."""
|
|
99
|
+
for t in desc.get('tags', []):
|
|
100
|
+
if t and t[0] == 'key' and len(t) > 2 and t[1] == 'transport':
|
|
101
|
+
return t[2]
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def binds(desc: NostrEvent, ed25519_hex: str) -> bool:
|
|
106
|
+
"""Whether the descriptor names this Ed25519 key as its transport key.
|
|
107
|
+
|
|
108
|
+
The gossipsub message was signed under StrictSign by the *originator*, and
|
|
109
|
+
a forwarder does not re-sign, so this is the originator's key and the
|
|
110
|
+
descriptor must be the originator's own. A descriptor relayed by a third
|
|
111
|
+
node is still valid on a flood topic - the check is against what the
|
|
112
|
+
descriptor claims, never against who handed it over.
|
|
113
|
+
"""
|
|
114
|
+
mk = transport_multikey(desc)
|
|
115
|
+
if not mk or not ed25519_hex:
|
|
116
|
+
return False
|
|
117
|
+
try:
|
|
118
|
+
kind, raw = keys.parse_multikey(mk)
|
|
119
|
+
except ValueError:
|
|
120
|
+
return False
|
|
121
|
+
return kind == 'transport' and raw.hex() == ed25519_hex.lower()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def admit(
|
|
125
|
+
payload: Any,
|
|
126
|
+
origin: Optional[str],
|
|
127
|
+
now: int,
|
|
128
|
+
pow_bits: int = 0,
|
|
129
|
+
payload_max: int = PAYLOAD_MAX,
|
|
130
|
+
) -> tuple[Optional[NostrEvent], Optional[str]]:
|
|
131
|
+
"""Whether the topic should carry this. (descriptor, None) or (None, why).
|
|
132
|
+
|
|
133
|
+
The whole of the defence on a flood channel with no grant in front of it,
|
|
134
|
+
and the one place it is written. The sidecar calls this before gossipsub
|
|
135
|
+
propagates anything, and the node calls it again on what the sidecar hands
|
|
136
|
+
over, because a separate process is not a reason to believe a descriptor.
|
|
137
|
+
|
|
138
|
+
Ordered cheapest first: a size that needs no parse, a parse, a kind, a
|
|
139
|
+
signature, the descriptor rules, the work, and last the binding, which is
|
|
140
|
+
the only check that needs to know who sent it.
|
|
141
|
+
|
|
142
|
+
`origin` is the *originator's* Ed25519 key - gossipsub StrictSign's
|
|
143
|
+
`from`, not the peer who handed the message over. A forwarder does not
|
|
144
|
+
re-sign, and a descriptor relayed by a third node is still valid here.
|
|
145
|
+
"""
|
|
146
|
+
if isinstance(payload, (bytes, bytearray)):
|
|
147
|
+
if len(payload) > payload_max:
|
|
148
|
+
return None, 'payload over %d bytes' % payload_max
|
|
149
|
+
try:
|
|
150
|
+
desc = json.loads(bytes(payload).decode('utf-8'))
|
|
151
|
+
except (ValueError, UnicodeDecodeError):
|
|
152
|
+
return None, 'payload is not JSON'
|
|
153
|
+
else:
|
|
154
|
+
desc = payload
|
|
155
|
+
if not isinstance(desc, dict):
|
|
156
|
+
return None, 'not an event'
|
|
157
|
+
if desc.get('kind') != KIND:
|
|
158
|
+
return None, 'not a kind %d' % KIND
|
|
159
|
+
if not descriptor_mod.genuine(desc):
|
|
160
|
+
return None, 'signature does not verify'
|
|
161
|
+
problems = identity.validate_descriptor(desc, now=now, digest=descriptor_mod.DIGEST)
|
|
162
|
+
if problems:
|
|
163
|
+
return None, 'descriptor: %s' % str(problems[0])[:48]
|
|
164
|
+
if pow_bits and identity.pow_bits(str(desc.get('id') or '')) < pow_bits:
|
|
165
|
+
return None, 'below %d bits of work' % pow_bits
|
|
166
|
+
desc_ev = cast(NostrEvent, desc)
|
|
167
|
+
if not binds(desc_ev, origin or ''):
|
|
168
|
+
return None, 'transport key does not bind'
|
|
169
|
+
return desc_ev, None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def call(path: str, cmd: str, timeout: float = 5.0, **args: Any) -> Any:
|
|
173
|
+
"""One request, one reply, one connection. Raises SidecarError for all of
|
|
174
|
+
no sidecar, a sidecar that will not answer, and a sidecar that says no.
|
|
175
|
+
|
|
176
|
+
For a caller outside a node (a script, the sidecar's own tests). Inside
|
|
177
|
+
one, the sidecar is reached through the bridge - `Announce.call` - which
|
|
178
|
+
also moves the link and checks the protocol the sidecar answered in.
|
|
179
|
+
"""
|
|
180
|
+
return jsonline_call(path, cmd, timeout=timeout, **args)[0]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
from .types import AnnounceTotals
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class Announce(Bridge):
|
|
187
|
+
|
|
188
|
+
name: str = 'announce'
|
|
189
|
+
reads: tuple[str, ...] = ('announce',)
|
|
190
|
+
config_cls = AnnounceConfig
|
|
191
|
+
|
|
192
|
+
SIDECAR = Sidecar(
|
|
193
|
+
name='announce', protocol=PROTOCOL,
|
|
194
|
+
upstream='py-libp2p gossipsub on %s' % TOPIC,
|
|
195
|
+
standalone='fleetmesh-announce NODE_DIR [--bootstrap MULTIADDR]',
|
|
196
|
+
probe='status')
|
|
197
|
+
CONSEQUENCE = 'this node is discoverable over relays only'
|
|
198
|
+
|
|
199
|
+
# Declared by the bridge from SIDECAR, under these names, with their
|
|
200
|
+
# counters (announce_sidecar_reachable / _unreachable / _incompatible).
|
|
201
|
+
SIDECAR_REACHABLE: str = 'announce.sidecar.reachable'
|
|
202
|
+
SIDECAR_UNREACHABLE: str = 'announce.sidecar.unreachable'
|
|
203
|
+
SIDECAR_INCOMPATIBLE: str = 'announce.sidecar.incompatible'
|
|
204
|
+
DESCRIPTOR_PUBLISHED: str = 'announce.descriptor.published'
|
|
205
|
+
PEERS_LEARNED: str = 'announce.peers.learned'
|
|
206
|
+
|
|
207
|
+
CHANNELS = (
|
|
208
|
+
ch_mod.Channel(
|
|
209
|
+
name=DESCRIPTOR_PUBLISHED,
|
|
210
|
+
description='The node descriptor was republished to the gossipsub announce topic.',
|
|
211
|
+
level=ch_mod.INFO,
|
|
212
|
+
wire=lambda id, topic: {'id': id, 'topic': topic},
|
|
213
|
+
say='published {id} on {topic}',
|
|
214
|
+
tags=('announce', 'descriptor')),
|
|
215
|
+
ch_mod.Channel(
|
|
216
|
+
name=PEERS_LEARNED,
|
|
217
|
+
description='New descriptors heard on the announce topic were verified and learned.',
|
|
218
|
+
level=ch_mod.INFO,
|
|
219
|
+
wire=lambda learned, heard, topic: {'learned': learned, 'heard': heard, 'topic': topic},
|
|
220
|
+
say='{learned} new peers learned from {heard} heard on {topic}',
|
|
221
|
+
tags=('announce', 'peers')),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
METRICS = (
|
|
225
|
+
mx_mod.Metric(
|
|
226
|
+
name='announce_published',
|
|
227
|
+
description='Times this node published its descriptor on the announce topic.',
|
|
228
|
+
kind=mx_mod.COUNTER, counts=DESCRIPTOR_PUBLISHED,
|
|
229
|
+
tags=('announce', 'descriptor')),
|
|
230
|
+
mx_mod.Metric(
|
|
231
|
+
name='announce_peers_learned',
|
|
232
|
+
description='Times new peers were learned from the announce topic.',
|
|
233
|
+
kind=mx_mod.COUNTER, counts=PEERS_LEARNED,
|
|
234
|
+
tags=('announce', 'peers')),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
COMMANDS = (
|
|
238
|
+
Command(
|
|
239
|
+
key='show',
|
|
240
|
+
name='Announce Status',
|
|
241
|
+
description='the announce topic: whether the sidecar is there, what it has carried, '
|
|
242
|
+
'and what this node refused off the topic',
|
|
243
|
+
tags=('announce', 'sidecar', 'status'),
|
|
244
|
+
parameters=(),
|
|
245
|
+
permission='announce/show',
|
|
246
|
+
process=('sidecar',),
|
|
247
|
+
run='state',
|
|
248
|
+
reply='{enabled, socket, connected, topic, peer_id, mesh_peers, published, '
|
|
249
|
+
'heard, learned, sidecar}',
|
|
250
|
+
reads=('totals',),
|
|
251
|
+
),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# What the topic has carried since this node first ran - not since its
|
|
255
|
+
# last upgrade: SNAPSHOT, so the lifecycle hands it to the next process.
|
|
256
|
+
# Not `published`: the next process's link starts unknown and the sidecar
|
|
257
|
+
# must hear the descriptor again (it may have restarted too).
|
|
258
|
+
STORES = (
|
|
259
|
+
st.Store(key='totals', description='descriptors carried up the topic, learned, and echoed back',
|
|
260
|
+
kind=st.COUNTERS, shape=AnnounceTotals, persist=st.SNAPSHOT),
|
|
261
|
+
)
|
|
262
|
+
# Restartable like every bridge; snapshot v1 is the shape of `totals`.
|
|
263
|
+
LIFECYCLE = lifecycle_mod.Lifecycle(restart=True, snapshot=1)
|
|
264
|
+
NEEDS = (
|
|
265
|
+
Need('peers', 'peers'),
|
|
266
|
+
Need('directory', 'dir'),
|
|
267
|
+
Need('timeout', 'timeout'),
|
|
268
|
+
Need('descriptor', 'descriptor'),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def __init__(
|
|
272
|
+
self,
|
|
273
|
+
peers: Any,
|
|
274
|
+
directory: str,
|
|
275
|
+
clock: Optional[Callable[[], int]] = None,
|
|
276
|
+
timeout: float = 5.0,
|
|
277
|
+
node: Optional[Any] = None,
|
|
278
|
+
descriptor: Optional[Callable[[], Optional[NostrEvent]]] = None,
|
|
279
|
+
) -> None:
|
|
280
|
+
super().__init__(node, clock)
|
|
281
|
+
# The node's own descriptor, read each publish through a getter
|
|
282
|
+
# because it is built after this compartment is.
|
|
283
|
+
self.descriptor: Callable[[], Optional[NostrEvent]] = descriptor or (lambda: getattr(node, 'descriptor', None))
|
|
284
|
+
self.peers: Any = peers
|
|
285
|
+
self.dir: str = directory
|
|
286
|
+
# Read once: a node that heard itself would otherwise cache itself.
|
|
287
|
+
self.node_pk: str = getattr(getattr(node, 'ks', None), 'node_pk', '') or ''
|
|
288
|
+
self.timeout: float = timeout
|
|
289
|
+
self.published: Optional[str] = None # the descriptor id last handed to the sidecar
|
|
290
|
+
self.totals: st.Counters # built and bound by the base, from STORES
|
|
291
|
+
self.mesh_peers: int = 0
|
|
292
|
+
self.peer_id: Optional[str] = None
|
|
293
|
+
|
|
294
|
+
# The totals by name, as they were read before they were a store.
|
|
295
|
+
@property
|
|
296
|
+
def heard(self) -> int:
|
|
297
|
+
"""Descriptors the sidecar carried up."""
|
|
298
|
+
return self.totals['heard']
|
|
299
|
+
|
|
300
|
+
@heard.setter
|
|
301
|
+
def heard(self, n: int) -> None:
|
|
302
|
+
self.totals['heard'] = n
|
|
303
|
+
|
|
304
|
+
@property
|
|
305
|
+
def learned(self) -> int:
|
|
306
|
+
"""Of those, the ones this node did not already hold."""
|
|
307
|
+
return self.totals['learned']
|
|
308
|
+
|
|
309
|
+
@learned.setter
|
|
310
|
+
def learned(self, n: int) -> None:
|
|
311
|
+
self.totals['learned'] = n
|
|
312
|
+
|
|
313
|
+
@property
|
|
314
|
+
def echoed(self) -> int:
|
|
315
|
+
"""And the ones that were this node's own publish returning."""
|
|
316
|
+
return self.totals['echoed']
|
|
317
|
+
|
|
318
|
+
@echoed.setter
|
|
319
|
+
def echoed(self, n: int) -> None:
|
|
320
|
+
self.totals['echoed'] = n
|
|
321
|
+
|
|
322
|
+
# ---- the bridge -----------------------------------------------------------
|
|
323
|
+
def endpoint(self) -> str:
|
|
324
|
+
return self.path
|
|
325
|
+
|
|
326
|
+
def wanted(self) -> bool:
|
|
327
|
+
return self.enabled
|
|
328
|
+
|
|
329
|
+
def forget(self) -> None:
|
|
330
|
+
# A sidecar that went away, or a bridge restarted, must hear the
|
|
331
|
+
# descriptor again when the link comes back.
|
|
332
|
+
self.published = None
|
|
333
|
+
|
|
334
|
+
# ---- configuration --------------------------------------------------------
|
|
335
|
+
@property
|
|
336
|
+
def enabled(self) -> bool:
|
|
337
|
+
return self.settings().enabled
|
|
338
|
+
|
|
339
|
+
@property
|
|
340
|
+
def path(self) -> str:
|
|
341
|
+
return socket_path(self.dir, self.settings().socket)
|
|
342
|
+
|
|
343
|
+
@property
|
|
344
|
+
def pow_bits(self) -> int:
|
|
345
|
+
return self.settings().pow_bits
|
|
346
|
+
|
|
347
|
+
# ---- the tick -------------------------------------------------------------
|
|
348
|
+
def tick(self, now: int) -> list[tuple[Any, ...]]:
|
|
349
|
+
if not self.enabled:
|
|
350
|
+
return []
|
|
351
|
+
did: list[tuple[Any, ...]] = []
|
|
352
|
+
try:
|
|
353
|
+
did += self._publish(now)
|
|
354
|
+
did += self._drain(now)
|
|
355
|
+
except SidecarError:
|
|
356
|
+
# The bridge has moved the link, said so once and refused it;
|
|
357
|
+
# a missing sidecar costs this tick its gossip and nothing else.
|
|
358
|
+
return did
|
|
359
|
+
return did
|
|
360
|
+
|
|
361
|
+
def _publish(self, now: int) -> list[tuple[Any, ...]]:
|
|
362
|
+
"""The node's own descriptor on the topic, republished only when it changes."""
|
|
363
|
+
desc = self.descriptor()
|
|
364
|
+
if not isinstance(desc, dict) or not desc.get('id'):
|
|
365
|
+
return []
|
|
366
|
+
if self.published == desc['id']:
|
|
367
|
+
return []
|
|
368
|
+
self.call('publish', descriptor=desc)
|
|
369
|
+
self.published = desc['id']
|
|
370
|
+
self.say(self.DESCRIPTOR_PUBLISHED, desc['id'][:12] + '…', TOPIC)
|
|
371
|
+
return [('announce', desc['id'][:12] + '…', 'published on ' + TOPIC)]
|
|
372
|
+
|
|
373
|
+
def _drain(self, now: int) -> list[tuple[Any, ...]]:
|
|
374
|
+
"""Everything the sidecar heard since the last tick, checked here again."""
|
|
375
|
+
res = self.call('drain', max=DRAIN_MAX)
|
|
376
|
+
if not isinstance(res, dict):
|
|
377
|
+
bad = 'drain answered something that is not an object'
|
|
378
|
+
self._down(bad, now)
|
|
379
|
+
raise SidecarError(bad)
|
|
380
|
+
self.peer_id = res.get('peer_id') or self.peer_id
|
|
381
|
+
try:
|
|
382
|
+
self.mesh_peers = int(res.get('mesh_peers') or 0)
|
|
383
|
+
except (TypeError, ValueError):
|
|
384
|
+
self.mesh_peers = 0
|
|
385
|
+
learned = 0
|
|
386
|
+
for row in (res.get('heard') or [])[:DRAIN_MAX]:
|
|
387
|
+
if not isinstance(row, dict):
|
|
388
|
+
continue
|
|
389
|
+
self.totals.incr('heard')
|
|
390
|
+
payload = row.get('descriptor')
|
|
391
|
+
origin = str(row.get('from') or '')
|
|
392
|
+
# The same check the sidecar ran before propagating it. Run again
|
|
393
|
+
# because a separate process is not a reason to believe an event,
|
|
394
|
+
# and because the work floor is this node's policy and not its.
|
|
395
|
+
desc, why = admit(payload, origin, now, pow_bits=self.pow_bits)
|
|
396
|
+
if desc is None:
|
|
397
|
+
subject = (payload.get('pubkey') if isinstance(payload, dict) else None) or None
|
|
398
|
+
self.refusals.refuse('heard', str(why), subject=subject,
|
|
399
|
+
detail={'from': origin[:16]}, at=now)
|
|
400
|
+
continue
|
|
401
|
+
if self.node_pk and desc['pubkey'] == self.node_pk:
|
|
402
|
+
# A node hears its own publish: gossipsub runs the validator on
|
|
403
|
+
# the way out, so the descriptor this node just put on the topic
|
|
404
|
+
# comes straight back. Learning it would put this node in its own
|
|
405
|
+
# peer cache, which is where `known()` is read from. Not a
|
|
406
|
+
# refusal - it is the topic working.
|
|
407
|
+
self.totals.incr('echoed')
|
|
408
|
+
continue
|
|
409
|
+
if self.peers.learn(desc):
|
|
410
|
+
learned += 1
|
|
411
|
+
# A descriptor this node already holds at the same age is ordinary
|
|
412
|
+
# on a flood topic and is not a refusal: the same descriptor
|
|
413
|
+
# arrives many times over, which is what the topic is for.
|
|
414
|
+
self.totals.incr('learned', learned)
|
|
415
|
+
for reason, n in sorted((res.get('dropped') or {}).items()):
|
|
416
|
+
# What the sidecar's own validator refused before propagating it.
|
|
417
|
+
# Counted here so `meshnode refusals` shows one topic, not two.
|
|
418
|
+
self.refusals.refuse('sidecar', str(reason)[:64], detail={'count': n}, at=now)
|
|
419
|
+
if learned:
|
|
420
|
+
self.say(self.PEERS_LEARNED, learned, len(res.get('heard') or []), TOPIC)
|
|
421
|
+
return [('announce', '%d heard' % len(res.get('heard') or []), '%d new' % learned)] if learned else []
|
|
422
|
+
|
|
423
|
+
# ---- being watched --------------------------------------------------------
|
|
424
|
+
def state(self) -> AnnounceStateReport:
|
|
425
|
+
return {
|
|
426
|
+
'enabled': self.enabled,
|
|
427
|
+
'socket': self.path,
|
|
428
|
+
'connected': self.link == UP,
|
|
429
|
+
'topic': TOPIC,
|
|
430
|
+
'peer_id': self.peer_id,
|
|
431
|
+
'mesh_peers': self.mesh_peers,
|
|
432
|
+
'pow_bits': self.pow_bits,
|
|
433
|
+
'published': self.published,
|
|
434
|
+
'heard': self.heard,
|
|
435
|
+
'learned': self.learned,
|
|
436
|
+
'echoed': self.echoed,
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
def trouble(self) -> list[str]:
|
|
440
|
+
# Down or incompatible is the bridge's to say, the same way for every
|
|
441
|
+
# sidecar; an empty topic is announce's own.
|
|
442
|
+
out = super().trouble()
|
|
443
|
+
if out or not self.wanted():
|
|
444
|
+
return out
|
|
445
|
+
if self.link == UP and not self.mesh_peers:
|
|
446
|
+
return ['on %s with no gossipsub peers: nothing hears this node there' % TOPIC]
|
|
447
|
+
return []
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Types and records for the Announce compartment."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Optional, TypedDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AnnounceStateReport(TypedDict, total=False):
|
|
8
|
+
"""The report Announce.state() gives of itself."""
|
|
9
|
+
enabled: bool
|
|
10
|
+
socket: Optional[str]
|
|
11
|
+
connected: bool
|
|
12
|
+
topic: str
|
|
13
|
+
peer_id: Optional[str]
|
|
14
|
+
mesh_peers: int
|
|
15
|
+
pow_bits: int
|
|
16
|
+
published: Optional[str | int]
|
|
17
|
+
heard: int
|
|
18
|
+
learned: int
|
|
19
|
+
echoed: int
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AnnounceTotals(TypedDict):
|
|
23
|
+
"""What the announce topic has carried: one counter each."""
|
|
24
|
+
heard: int # descriptors the sidecar carried up
|
|
25
|
+
learned: int # of those, the ones this node did not already hold
|
|
26
|
+
echoed: int # and the ones that were this node's own publish returning
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""What this node tells the mesh it is and what it holds, as a compartment.
|
|
2
|
+
|
|
3
|
+
One-module compartment: everything is in compartment.py.
|
|
4
|
+
"""
|
|
5
|
+
from .compartment import Discovery
|
|
6
|
+
from .types import DiscoveryStateReport
|
|
7
|
+
|
|
8
|
+
__all__ = ['Discovery', 'DiscoveryStateReport']
|
|
9
|
+
|