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,338 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The libp2p half of the announce bridge, in its own process.
|
|
3
|
+
|
|
4
|
+
fleetmesh-announce DIR [--bootstrap MULTIADDR]... [--port N] [--pow-bits N]
|
|
5
|
+
|
|
6
|
+
Joins the gossipsub topic `/fleetmesh/announce/1`, republishes the descriptor
|
|
7
|
+
the node hands it, and hands back the descriptors it hears. One JSON object
|
|
8
|
+
per line over a unix socket in the node's directory, the protocol
|
|
9
|
+
`fleetmesh-announce/1`, the same shape `node/control.py` uses.
|
|
10
|
+
|
|
11
|
+
Why a separate process. `fleetmesh_core/` is standard library only and `node/` depends on
|
|
12
|
+
`cryptography` and nothing else. Installing the one maintained Python libp2p
|
|
13
|
+
pulls in sixty-three packages, eight of them compiled; it will not install at
|
|
14
|
+
all on Python 3.14 today; and on macOS it installs and then fails to import
|
|
15
|
+
without a system GMP for `fastecdsa`. A second implementer reading the
|
|
16
|
+
specification should inherit none of that to build a node, so it is here, and
|
|
17
|
+
a node without it is a node with one discovery bridge instead of two.
|
|
18
|
+
|
|
19
|
+
**This process holds the transport key and nothing else.** It reads
|
|
20
|
+
`transport_seed` out of the node's `keys.json` and never touches `node_sk`.
|
|
21
|
+
The specification says the transport key MAY rotate freely and that a
|
|
22
|
+
verifier binds it to a DID only through a live descriptor, so a compromise
|
|
23
|
+
here costs one descriptor republish. The node key, which never rotates,
|
|
24
|
+
stays in the node.
|
|
25
|
+
|
|
26
|
+
**What this validates, and why here.** A gossipsub topic is a flood channel
|
|
27
|
+
with no grant in front of it, so the only thing standing between the mesh and
|
|
28
|
+
descriptor spam is the validator that runs before propagation.
|
|
29
|
+
|
|
30
|
+
`node.announce.compartment.admit` is that validator, and it lives there rather than here
|
|
31
|
+
because it needs nothing libp2p has: a size, a parse, a kind, a signature,
|
|
32
|
+
`fleetmesh_core.identity.validate_descriptor`, a NIP-13 floor, and last the tie between
|
|
33
|
+
the descriptor's `key transport` Multikey and the originator's PeerId. The
|
|
34
|
+
node runs the same function again on what this process hands over, so a check
|
|
35
|
+
that exists in two places cannot come to disagree with itself.
|
|
36
|
+
|
|
37
|
+
The originator is `msg.from_id` and never the peer who forwarded the message.
|
|
38
|
+
A forwarder does not re-sign under StrictSign, and a descriptor relayed by a
|
|
39
|
+
third node is still valid on a flood topic.
|
|
40
|
+
"""
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import argparse
|
|
44
|
+
import collections
|
|
45
|
+
import json
|
|
46
|
+
import os
|
|
47
|
+
import stat
|
|
48
|
+
import sys
|
|
49
|
+
import time
|
|
50
|
+
from typing import Any, Deque, Mapping, Optional, TypedDict
|
|
51
|
+
|
|
52
|
+
import multiaddr
|
|
53
|
+
import trio
|
|
54
|
+
from libp2p import new_host
|
|
55
|
+
from libp2p.crypto.ed25519 import create_new_key_pair
|
|
56
|
+
from libp2p.custom_types import TProtocol
|
|
57
|
+
from libp2p.peer.id import ID
|
|
58
|
+
from libp2p.peer.peerinfo import info_from_p2p_addr
|
|
59
|
+
from libp2p.pubsub.gossipsub import GossipSub
|
|
60
|
+
from libp2p.pubsub.pubsub import Pubsub
|
|
61
|
+
from libp2p.tools.anyio_service import background_trio_service
|
|
62
|
+
|
|
63
|
+
_ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
|
|
64
|
+
sys.path.insert(0, _ROOT)
|
|
65
|
+
from node.planes.identity.announce.compartment import (KIND, MAX_LINE, PAYLOAD_MAX, PROTOCOL, # noqa: E402
|
|
66
|
+
TOPIC, admit, socket_path)
|
|
67
|
+
|
|
68
|
+
GOSSIPSUB: TProtocol = TProtocol('/meshsub/1.0.0')
|
|
69
|
+
# Descriptors held for the node to drain. The node ticks every thirty seconds
|
|
70
|
+
# and a flood must not outgrow the interval, so the oldest goes and the loss
|
|
71
|
+
# is counted rather than hidden.
|
|
72
|
+
HEARD_MAX: int = 256
|
|
73
|
+
# The control socket: a client that connects and does not finish a line
|
|
74
|
+
# within this many seconds is dropped, so it cannot hold a task open.
|
|
75
|
+
CONTROL_TIMEOUT: float = 10.0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def transport_seed(directory: str) -> bytes:
|
|
79
|
+
"""The Ed25519 seed out of the node's key file, and nothing else from it."""
|
|
80
|
+
path = os.path.join(directory, 'keys.json')
|
|
81
|
+
mode = stat.S_IMODE(os.stat(path).st_mode)
|
|
82
|
+
if mode & 0o077:
|
|
83
|
+
raise PermissionError('%s is readable by others (mode %o); refusing to load' % (path, mode))
|
|
84
|
+
with open(path, encoding='utf-8') as f:
|
|
85
|
+
d = json.load(f)
|
|
86
|
+
if d.get('format') != 'meshnode-keys/1':
|
|
87
|
+
raise ValueError('not a meshnode key file')
|
|
88
|
+
seed = bytes.fromhex(d['transport_seed'])
|
|
89
|
+
if len(seed) != 32:
|
|
90
|
+
raise ValueError('the transport seed is 32 bytes')
|
|
91
|
+
return seed
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
AnnounceHeardRecord = TypedDict('AnnounceHeardRecord', {
|
|
95
|
+
'descriptor': Any,
|
|
96
|
+
'from': str,
|
|
97
|
+
'at': int,
|
|
98
|
+
}, total=False)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class AnnounceStatusReport(TypedDict, total=False):
|
|
102
|
+
"""The report Announce.status() gives of itself."""
|
|
103
|
+
topic: str
|
|
104
|
+
peer_id: Optional[str]
|
|
105
|
+
mesh_peers: int
|
|
106
|
+
pow_bits: int
|
|
107
|
+
accepted: int
|
|
108
|
+
published: int
|
|
109
|
+
waiting: int
|
|
110
|
+
overflowed: int
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class Announce:
|
|
114
|
+
"""The topic, what it carried, and what it refused."""
|
|
115
|
+
|
|
116
|
+
def __init__(self, pow_bits: int = 0, now: Any = None) -> None:
|
|
117
|
+
self.pow_bits: int = max(0, int(pow_bits))
|
|
118
|
+
self.now = now or (lambda: int(time.time()))
|
|
119
|
+
self.heard: Deque[AnnounceHeardRecord] = collections.deque(maxlen=HEARD_MAX)
|
|
120
|
+
self.dropped: collections.Counter[str] = collections.Counter()
|
|
121
|
+
self.accepted: int = 0
|
|
122
|
+
self.published: int = 0
|
|
123
|
+
self.overflowed: int = 0
|
|
124
|
+
self.pubsub: Optional[Pubsub] = None
|
|
125
|
+
self.host: Any = None
|
|
126
|
+
|
|
127
|
+
# ---- the validator, run before anything is propagated -------------------
|
|
128
|
+
def validate(self, msg_forwarder: ID, msg: Any) -> bool:
|
|
129
|
+
"""gossipsub's pre-propagation hook. False stops the message dead.
|
|
130
|
+
|
|
131
|
+
`msg_forwarder` is the peer who handed it over and is deliberately
|
|
132
|
+
unused: under StrictSign the signature is the *originator's*, carried
|
|
133
|
+
in `msg.from_id`, and a forwarder does not re-sign. A descriptor
|
|
134
|
+
relayed by a third node is still valid on a flood topic, so the tie
|
|
135
|
+
to check is between the descriptor and whoever signed it.
|
|
136
|
+
"""
|
|
137
|
+
origin = self._ed25519_of(ID(msg.from_id))
|
|
138
|
+
if origin is None:
|
|
139
|
+
self.dropped['originator is not an Ed25519 peer'] += 1
|
|
140
|
+
return False
|
|
141
|
+
desc, why = admit(msg.data, origin, self.now(), pow_bits=self.pow_bits,
|
|
142
|
+
payload_max=PAYLOAD_MAX)
|
|
143
|
+
if desc is None:
|
|
144
|
+
self.dropped[str(why)] += 1
|
|
145
|
+
return False
|
|
146
|
+
if len(self.heard) == self.heard.maxlen:
|
|
147
|
+
self.overflowed += 1
|
|
148
|
+
self.heard.append({'descriptor': desc, 'from': origin, 'at': self.now()})
|
|
149
|
+
self.accepted += 1
|
|
150
|
+
return True
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _ed25519_of(peer: ID) -> Optional[str]:
|
|
154
|
+
"""The raw Ed25519 public key a PeerId carries, as hex.
|
|
155
|
+
|
|
156
|
+
An Ed25519 key is small enough that libp2p puts it in the PeerId
|
|
157
|
+
inline, under an identity multihash, so this needs no lookup. Anything
|
|
158
|
+
else - an RSA peer, whose id is a SHA-256 - returns None and is
|
|
159
|
+
refused, because fleetmesh nodes are Ed25519 by the key table.
|
|
160
|
+
"""
|
|
161
|
+
try:
|
|
162
|
+
pub = peer.extract_public_key()
|
|
163
|
+
except Exception:
|
|
164
|
+
return None
|
|
165
|
+
if pub is None:
|
|
166
|
+
return None
|
|
167
|
+
raw = pub.to_bytes()
|
|
168
|
+
return raw.hex() if len(raw) == 32 else None
|
|
169
|
+
|
|
170
|
+
# ---- the socket ---------------------------------------------------------
|
|
171
|
+
async def serve(self, path: str) -> None:
|
|
172
|
+
# Refuse to take a socket another instance is answering on; a stale
|
|
173
|
+
# file left by a crash is unlinked.
|
|
174
|
+
if os.path.exists(path):
|
|
175
|
+
probe = trio.socket.socket(trio.socket.AF_UNIX, trio.socket.SOCK_STREAM)
|
|
176
|
+
try:
|
|
177
|
+
await probe.connect(path)
|
|
178
|
+
except OSError:
|
|
179
|
+
os.unlink(path)
|
|
180
|
+
else:
|
|
181
|
+
raise RuntimeError('another fleetmesh-announce is already serving %s' % path)
|
|
182
|
+
finally:
|
|
183
|
+
probe.close()
|
|
184
|
+
s = trio.socket.socket(trio.socket.AF_UNIX, trio.socket.SOCK_STREAM)
|
|
185
|
+
old = os.umask(0o177)
|
|
186
|
+
try:
|
|
187
|
+
await s.bind(path)
|
|
188
|
+
os.chmod(path, 0o600)
|
|
189
|
+
finally:
|
|
190
|
+
os.umask(old)
|
|
191
|
+
s.listen(8)
|
|
192
|
+
await trio.serve_listeners(self._one, [trio.SocketListener(s)])
|
|
193
|
+
|
|
194
|
+
async def _one(self, stream: trio.SocketStream) -> None:
|
|
195
|
+
try:
|
|
196
|
+
buf = b''
|
|
197
|
+
with trio.move_on_after(CONTROL_TIMEOUT):
|
|
198
|
+
while not buf.endswith(b'\n') and len(buf) <= MAX_LINE:
|
|
199
|
+
chunk = await stream.receive_some(65536)
|
|
200
|
+
if not chunk:
|
|
201
|
+
break
|
|
202
|
+
buf += chunk
|
|
203
|
+
try:
|
|
204
|
+
if not buf.endswith(b'\n') or len(buf) > MAX_LINE:
|
|
205
|
+
raise ValueError('a request is one JSON line under %d bytes' % MAX_LINE)
|
|
206
|
+
req = json.loads(buf.decode('utf-8'))
|
|
207
|
+
if not isinstance(req, dict) or not isinstance(req.get('cmd'), str):
|
|
208
|
+
raise ValueError('a request is {"cmd": ..., "args": {...}}')
|
|
209
|
+
args = req.get('args') or {}
|
|
210
|
+
if not isinstance(args, dict):
|
|
211
|
+
raise ValueError('args is an object')
|
|
212
|
+
result = await self.dispatch(req['cmd'], args)
|
|
213
|
+
reply = {'ok': True, 'protocol': PROTOCOL, 'result': result}
|
|
214
|
+
except Exception as e: # one bad request answers one request
|
|
215
|
+
reply = {'ok': False, 'protocol': PROTOCOL, 'error': '%s: %s' % (type(e).__name__, e)}
|
|
216
|
+
# Everything status()/drain() return is str/int/dict already;
|
|
217
|
+
# no `default=str`, which would let a stray object type leak
|
|
218
|
+
# through as its repr and be parsed by the node as a value.
|
|
219
|
+
await stream.send_all((json.dumps(reply) + '\n').encode('utf-8'))
|
|
220
|
+
except (trio.BrokenResourceError, trio.ClosedResourceError, OSError):
|
|
221
|
+
pass
|
|
222
|
+
finally:
|
|
223
|
+
await trio.aclose_forcefully(stream)
|
|
224
|
+
|
|
225
|
+
async def dispatch(self, cmd: str, args: Mapping[str, Any]) -> Any:
|
|
226
|
+
if cmd == 'publish':
|
|
227
|
+
desc = args.get('descriptor')
|
|
228
|
+
if not isinstance(desc, dict) or desc.get('kind') != KIND:
|
|
229
|
+
raise ValueError('publish takes one kind %d event' % KIND)
|
|
230
|
+
if self.pubsub is None:
|
|
231
|
+
raise RuntimeError('the topic is not joined yet')
|
|
232
|
+
body = json.dumps(desc, separators=(',', ':'), sort_keys=True).encode('utf-8')
|
|
233
|
+
if len(body) > PAYLOAD_MAX:
|
|
234
|
+
raise ValueError('a descriptor of %d bytes is over the %d the topic carries'
|
|
235
|
+
% (len(body), PAYLOAD_MAX))
|
|
236
|
+
await self.pubsub.publish(TOPIC, body)
|
|
237
|
+
self.published += 1
|
|
238
|
+
return {'published': self.published, 'bytes': len(body)}
|
|
239
|
+
if cmd == 'drain':
|
|
240
|
+
try:
|
|
241
|
+
want = min(int(args.get('max') or HEARD_MAX), HEARD_MAX)
|
|
242
|
+
except (TypeError, ValueError):
|
|
243
|
+
want = HEARD_MAX
|
|
244
|
+
out = [self.heard.popleft() for _ in range(min(want, len(self.heard)))]
|
|
245
|
+
dropped = dict(self.dropped)
|
|
246
|
+
self.dropped.clear()
|
|
247
|
+
return dict(self.status(), heard=out, dropped=dropped)
|
|
248
|
+
if cmd == 'status':
|
|
249
|
+
return self.status()
|
|
250
|
+
raise KeyError(cmd)
|
|
251
|
+
|
|
252
|
+
def status(self) -> AnnounceStatusReport:
|
|
253
|
+
peers: list[str] = []
|
|
254
|
+
if self.pubsub is not None:
|
|
255
|
+
peers = [str(p) for p in self.pubsub.peer_topics.get(TOPIC, [])]
|
|
256
|
+
return {
|
|
257
|
+
'topic': TOPIC,
|
|
258
|
+
'peer_id': str(self.host.get_id()) if self.host is not None else None,
|
|
259
|
+
'mesh_peers': len(peers),
|
|
260
|
+
'pow_bits': self.pow_bits,
|
|
261
|
+
'accepted': self.accepted,
|
|
262
|
+
'published': self.published,
|
|
263
|
+
'waiting': len(self.heard),
|
|
264
|
+
'overflowed': self.overflowed,
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
async def run(directory: str, bootstrap: list[str], port: int, pow_bits: int,
|
|
269
|
+
path: Optional[str] = None) -> None:
|
|
270
|
+
key_pair = create_new_key_pair(transport_seed(directory))
|
|
271
|
+
host = new_host(key_pair=key_pair)
|
|
272
|
+
gossipsub = GossipSub(protocols=[GOSSIPSUB], degree=6, degree_low=4, degree_high=12)
|
|
273
|
+
pubsub = Pubsub(host, gossipsub) # strict_signing defaults to True
|
|
274
|
+
state = Announce(pow_bits=pow_bits)
|
|
275
|
+
state.host, state.pubsub = host, pubsub
|
|
276
|
+
sock = path or socket_path(directory)
|
|
277
|
+
listen = [multiaddr.Multiaddr('/ip4/0.0.0.0/tcp/%d' % port)]
|
|
278
|
+
|
|
279
|
+
async with host.run(listen_addrs=listen), trio.open_nursery() as nursery:
|
|
280
|
+
async with background_trio_service(pubsub), background_trio_service(gossipsub):
|
|
281
|
+
await pubsub.wait_until_ready()
|
|
282
|
+
pubsub.set_topic_validator(TOPIC, state.validate, False)
|
|
283
|
+
await pubsub.subscribe(TOPIC)
|
|
284
|
+
print('%s peer %s' % (TOPIC, host.get_id()), flush=True)
|
|
285
|
+
for a in host.get_addrs():
|
|
286
|
+
print(' listening %s' % a, flush=True)
|
|
287
|
+
for addr in bootstrap:
|
|
288
|
+
try:
|
|
289
|
+
await host.connect(info_from_p2p_addr(multiaddr.Multiaddr(addr)))
|
|
290
|
+
print(' bootstrap %s' % addr, flush=True)
|
|
291
|
+
except Exception as e:
|
|
292
|
+
# A bootstrap address that does not answer is ordinary. The
|
|
293
|
+
# topic works with one peer and the relay bridge works with
|
|
294
|
+
# none, so this is never fatal.
|
|
295
|
+
print(' bootstrap %s did not answer (%s)' % (addr, e), flush=True)
|
|
296
|
+
nursery.start_soon(state.serve, sock)
|
|
297
|
+
print(' socket %s' % sock, flush=True)
|
|
298
|
+
await trio.sleep_forever()
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _leaves(e: BaseException) -> list[BaseException]:
|
|
302
|
+
"""The real exceptions under however many ExceptionGroups trio nested."""
|
|
303
|
+
subs = getattr(e, 'exceptions', None)
|
|
304
|
+
if not subs:
|
|
305
|
+
return [e]
|
|
306
|
+
out: list[BaseException] = []
|
|
307
|
+
for s in subs:
|
|
308
|
+
out.extend(_leaves(s))
|
|
309
|
+
return out
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def main(argv: Optional[list[str]] = None) -> int:
|
|
313
|
+
ap = argparse.ArgumentParser(prog='fleetmesh-announce', description=__doc__.splitlines()[0])
|
|
314
|
+
ap.add_argument('directory', help="the node directory, for keys.json and the socket")
|
|
315
|
+
ap.add_argument('--bootstrap', action='append', default=[],
|
|
316
|
+
metavar='MULTIADDR', help='a libp2p peer to dial on start; repeatable')
|
|
317
|
+
ap.add_argument('--port', type=int, default=0, help='tcp port to listen on (0 asks the kernel)')
|
|
318
|
+
ap.add_argument('--pow-bits', type=int, default=0,
|
|
319
|
+
help='refuse a descriptor below this many bits of NIP-13 work. Local policy: '
|
|
320
|
+
'two nodes with different floors interoperate, the stricter propagates less')
|
|
321
|
+
ap.add_argument('--socket', default=None, help='where to listen (default: announce.sock in the directory)')
|
|
322
|
+
a = ap.parse_args(argv)
|
|
323
|
+
try:
|
|
324
|
+
trio.run(run, a.directory, a.bootstrap, a.port, a.pow_bits, a.socket)
|
|
325
|
+
except KeyboardInterrupt:
|
|
326
|
+
return 0
|
|
327
|
+
except Exception as e:
|
|
328
|
+
# A refusal at startup - a live socket, a loose keys.json, a missing
|
|
329
|
+
# key file - is one line, not a nursery traceback. trio wraps what a
|
|
330
|
+
# nursery task raised in an ExceptionGroup; unwrap for the message.
|
|
331
|
+
for inner in _leaves(e):
|
|
332
|
+
print('fleetmesh-announce: %s: %s' % (type(inner).__name__, inner), file=sys.stderr, flush=True)
|
|
333
|
+
return 1
|
|
334
|
+
return 0
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
if __name__ == '__main__':
|
|
338
|
+
raise SystemExit(main())
|