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/config/protocols.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Protocol suite and host multiplexer configuration blocks."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import dataclasses
|
|
5
|
+
from typing import Any, Mapping, Optional
|
|
6
|
+
|
|
7
|
+
from .base import Block, Problems, is_http_url, is_relay_url, opt, positive_int
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
'HostMultiplexerConfig',
|
|
11
|
+
'NostrProtocolConfig',
|
|
12
|
+
'ATProtoProtocolConfig',
|
|
13
|
+
'ActivityPubProtocolConfig',
|
|
14
|
+
'SSBProtocolConfig',
|
|
15
|
+
'P2PandaProtocolConfig',
|
|
16
|
+
'RhioProtocolConfig',
|
|
17
|
+
'ProtocolSuitesConfig',
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclasses.dataclass(frozen=True)
|
|
22
|
+
class HostMultiplexerConfig(Block):
|
|
23
|
+
listen: str = '127.0.0.1'
|
|
24
|
+
port: int = 0
|
|
25
|
+
public_url: Optional[str] = None
|
|
26
|
+
bind_wait: float = 10.0
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
30
|
+
if not isinstance(raw, Mapping):
|
|
31
|
+
p.add('protocols.host must be an object'); return
|
|
32
|
+
opt(raw, 'listen', lambda v: isinstance(v, str) and len(v) > 0, p, 'protocols.host.listen must be a non-empty string')
|
|
33
|
+
opt(raw, 'port', lambda v: isinstance(v, int) and 0 <= v <= 65535, p, 'protocols.host.port must be 0..65535')
|
|
34
|
+
opt(raw, 'public_url', is_http_url, p, 'protocols.host.public_url must be an http or https URL')
|
|
35
|
+
opt(raw, 'bind_wait', lambda v: isinstance(v, (int, float)) and v >= 0, p, 'protocols.host.bind_wait must be non-negative')
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_dict(cls, raw: Any) -> 'HostMultiplexerConfig':
|
|
39
|
+
if not isinstance(raw, Mapping):
|
|
40
|
+
return cls()
|
|
41
|
+
return cls(
|
|
42
|
+
listen=str(raw.get('listen', '127.0.0.1')),
|
|
43
|
+
port=int(raw.get('port', 0)),
|
|
44
|
+
public_url=str(raw['public_url']) if raw.get('public_url') else None,
|
|
45
|
+
bind_wait=float(raw.get('bind_wait', 10.0)),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclasses.dataclass(frozen=True)
|
|
50
|
+
class NostrProtocolConfig(Block):
|
|
51
|
+
enabled: bool = True
|
|
52
|
+
relays: tuple[str, ...] = ()
|
|
53
|
+
require_auth: bool = False
|
|
54
|
+
future_seconds: int = 900
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
58
|
+
if not isinstance(raw, Mapping):
|
|
59
|
+
p.add('protocols.nostr must be an object'); return
|
|
60
|
+
opt(raw, 'enabled', lambda v: isinstance(v, bool), p, 'protocols.nostr.enabled must be a boolean')
|
|
61
|
+
opt(raw, 'relays', lambda v: isinstance(v, (list, tuple)) and all(is_relay_url(u) for u in v), p,
|
|
62
|
+
'protocols.nostr.relays must be a list of relay URLs (wss:// or ws://)')
|
|
63
|
+
opt(raw, 'require_auth', lambda v: isinstance(v, bool), p, 'protocols.nostr.require_auth must be a boolean')
|
|
64
|
+
opt(raw, 'future_seconds', positive_int, p, 'protocols.nostr.future_seconds must be a positive integer')
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def from_dict(cls, raw: Any) -> 'NostrProtocolConfig':
|
|
68
|
+
if not isinstance(raw, Mapping):
|
|
69
|
+
return cls()
|
|
70
|
+
return cls(
|
|
71
|
+
enabled=bool(raw.get('enabled', True)),
|
|
72
|
+
relays=tuple(str(u) for u in (raw.get('relays') or ())),
|
|
73
|
+
require_auth=bool(raw.get('require_auth', False)),
|
|
74
|
+
future_seconds=int(raw.get('future_seconds', 900)),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclasses.dataclass(frozen=True)
|
|
79
|
+
class ATProtoProtocolConfig(Block):
|
|
80
|
+
enabled: bool = False
|
|
81
|
+
handle: Optional[str] = None
|
|
82
|
+
service_did: Optional[str] = None
|
|
83
|
+
pds_enabled: bool = True
|
|
84
|
+
firehose_subscribe: bool = False
|
|
85
|
+
firehose_publish: bool = True
|
|
86
|
+
relay_upstream: Optional[str] = None
|
|
87
|
+
metered_peering: bool = True
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
91
|
+
if not isinstance(raw, Mapping):
|
|
92
|
+
p.add('protocols.atproto must be an object'); return
|
|
93
|
+
opt(raw, 'enabled', lambda v: isinstance(v, bool), p, 'protocols.atproto.enabled must be a boolean')
|
|
94
|
+
opt(raw, 'handle', lambda v: isinstance(v, str) and len(v) > 0, p, 'protocols.atproto.handle must be a string')
|
|
95
|
+
opt(raw, 'service_did', lambda v: isinstance(v, str) and v.startswith('did:'), p, 'protocols.atproto.service_did must be a did: URI')
|
|
96
|
+
opt(raw, 'pds_enabled', lambda v: isinstance(v, bool), p, 'protocols.atproto.pds_enabled must be a boolean')
|
|
97
|
+
opt(raw, 'firehose_subscribe', lambda v: isinstance(v, bool), p, 'protocols.atproto.firehose_subscribe must be a boolean')
|
|
98
|
+
opt(raw, 'firehose_publish', lambda v: isinstance(v, bool), p, 'protocols.atproto.firehose_publish must be a boolean')
|
|
99
|
+
opt(raw, 'relay_upstream', is_relay_url, p, 'protocols.atproto.relay_upstream must be a websocket URL')
|
|
100
|
+
opt(raw, 'metered_peering', lambda v: isinstance(v, bool), p, 'protocols.atproto.metered_peering must be a boolean')
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def from_dict(cls, raw: Any) -> 'ATProtoProtocolConfig':
|
|
104
|
+
if not isinstance(raw, Mapping):
|
|
105
|
+
return cls()
|
|
106
|
+
return cls(
|
|
107
|
+
enabled=bool(raw.get('enabled', False)),
|
|
108
|
+
handle=str(raw['handle']) if raw.get('handle') else None,
|
|
109
|
+
service_did=str(raw['service_did']) if raw.get('service_did') else None,
|
|
110
|
+
pds_enabled=bool(raw.get('pds_enabled', True)),
|
|
111
|
+
firehose_subscribe=bool(raw.get('firehose_subscribe', False)),
|
|
112
|
+
firehose_publish=bool(raw.get('firehose_publish', True)),
|
|
113
|
+
relay_upstream=str(raw['relay_upstream']) if raw.get('relay_upstream') else None,
|
|
114
|
+
metered_peering=bool(raw.get('metered_peering', True)),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclasses.dataclass(frozen=True)
|
|
119
|
+
class ActivityPubProtocolConfig(Block):
|
|
120
|
+
enabled: bool = False
|
|
121
|
+
domain: Optional[str] = None
|
|
122
|
+
summary: str = 'Fleetmesh Sovereign Edge Node'
|
|
123
|
+
rate_limit_per_inbox: int = 60
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
127
|
+
if not isinstance(raw, Mapping):
|
|
128
|
+
p.add('protocols.activitypub must be an object'); return
|
|
129
|
+
opt(raw, 'enabled', lambda v: isinstance(v, bool), p, 'protocols.activitypub.enabled must be a boolean')
|
|
130
|
+
opt(raw, 'domain', lambda v: isinstance(v, str) and len(v) > 0, p, 'protocols.activitypub.domain must be a string')
|
|
131
|
+
opt(raw, 'summary', lambda v: isinstance(v, str), p, 'protocols.activitypub.summary must be a string')
|
|
132
|
+
opt(raw, 'rate_limit_per_inbox', positive_int, p, 'protocols.activitypub.rate_limit_per_inbox must be a positive integer')
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def from_dict(cls, raw: Any) -> 'ActivityPubProtocolConfig':
|
|
136
|
+
if not isinstance(raw, Mapping):
|
|
137
|
+
return cls()
|
|
138
|
+
return cls(
|
|
139
|
+
enabled=bool(raw.get('enabled', False)),
|
|
140
|
+
domain=str(raw['domain']) if raw.get('domain') else None,
|
|
141
|
+
summary=str(raw.get('summary', 'Fleetmesh Sovereign Edge Node')),
|
|
142
|
+
rate_limit_per_inbox=int(raw.get('rate_limit_per_inbox', 60)),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclasses.dataclass(frozen=True)
|
|
147
|
+
class SSBProtocolConfig(Block):
|
|
148
|
+
enabled: bool = False
|
|
149
|
+
port: int = 8008
|
|
150
|
+
caps: str = 'd4a1da283c1e443e022da12bc6139952b324e4001f40fa38ba5754035a49807f'
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
154
|
+
if not isinstance(raw, Mapping):
|
|
155
|
+
p.add('protocols.ssb must be an object'); return
|
|
156
|
+
opt(raw, 'enabled', lambda v: isinstance(v, bool), p, 'protocols.ssb.enabled must be a boolean')
|
|
157
|
+
opt(raw, 'port', lambda v: isinstance(v, int) and 0 <= v <= 65535, p, 'protocols.ssb.port must be 0..65535')
|
|
158
|
+
opt(raw, 'caps', lambda v: isinstance(v, str) and len(v) == 64, p, 'protocols.ssb.caps must be a 64-char hex string')
|
|
159
|
+
|
|
160
|
+
@classmethod
|
|
161
|
+
def from_dict(cls, raw: Any) -> 'SSBProtocolConfig':
|
|
162
|
+
if not isinstance(raw, Mapping):
|
|
163
|
+
return cls()
|
|
164
|
+
return cls(
|
|
165
|
+
enabled=bool(raw.get('enabled', False)),
|
|
166
|
+
port=int(raw.get('port', 8008)),
|
|
167
|
+
caps=str(raw.get('caps', 'd4a1da283c1e443e022da12bc6139952b324e4001f40fa38ba5754035a49807f')),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclasses.dataclass(frozen=True)
|
|
172
|
+
class P2PandaProtocolConfig(Block):
|
|
173
|
+
enabled: bool = False
|
|
174
|
+
port: int = 9102
|
|
175
|
+
network_id: str = 'fleetmesh-default'
|
|
176
|
+
discovery: bool = True
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
180
|
+
if not isinstance(raw, Mapping):
|
|
181
|
+
p.add('protocols.p2panda must be an object'); return
|
|
182
|
+
opt(raw, 'enabled', lambda v: isinstance(v, bool), p, 'protocols.p2panda.enabled must be a boolean')
|
|
183
|
+
opt(raw, 'port', lambda v: isinstance(v, int) and 0 <= v <= 65535, p, 'protocols.p2panda.port must be 0..65535')
|
|
184
|
+
opt(raw, 'network_id', lambda v: isinstance(v, str) and len(v) > 0, p, 'protocols.p2panda.network_id must be a string')
|
|
185
|
+
opt(raw, 'discovery', lambda v: isinstance(v, bool), p, 'protocols.p2panda.discovery must be a boolean')
|
|
186
|
+
|
|
187
|
+
@classmethod
|
|
188
|
+
def from_dict(cls, raw: Any) -> 'P2PandaProtocolConfig':
|
|
189
|
+
if not isinstance(raw, Mapping):
|
|
190
|
+
return cls()
|
|
191
|
+
return cls(
|
|
192
|
+
enabled=bool(raw.get('enabled', False)),
|
|
193
|
+
port=int(raw.get('port', 9102)),
|
|
194
|
+
network_id=str(raw.get('network_id', 'fleetmesh-default')),
|
|
195
|
+
discovery=bool(raw.get('discovery', True)),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@dataclasses.dataclass(frozen=True)
|
|
200
|
+
class RhioProtocolConfig(Block):
|
|
201
|
+
enabled: bool = False
|
|
202
|
+
nats_endpoint: str = 'localhost:4222'
|
|
203
|
+
s3_endpoint: str = 'http://localhost:9000'
|
|
204
|
+
network_id: str = 'rhio-default-network-1'
|
|
205
|
+
bind_port: int = 9102
|
|
206
|
+
http_bind_port: int = 3000
|
|
207
|
+
|
|
208
|
+
@classmethod
|
|
209
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
210
|
+
if not isinstance(raw, Mapping):
|
|
211
|
+
p.add('protocols.rhio must be an object'); return
|
|
212
|
+
opt(raw, 'enabled', lambda v: isinstance(v, bool), p, 'protocols.rhio.enabled must be a boolean')
|
|
213
|
+
opt(raw, 'nats_endpoint', lambda v: isinstance(v, str) and len(v) > 0, p, 'protocols.rhio.nats_endpoint must be a string')
|
|
214
|
+
opt(raw, 's3_endpoint', lambda v: isinstance(v, str) and len(v) > 0, p, 'protocols.rhio.s3_endpoint must be a string')
|
|
215
|
+
opt(raw, 'network_id', lambda v: isinstance(v, str) and len(v) > 0, p, 'protocols.rhio.network_id must be a string')
|
|
216
|
+
opt(raw, 'bind_port', lambda v: isinstance(v, int) and 0 <= v <= 65535, p, 'protocols.rhio.bind_port must be 0..65535')
|
|
217
|
+
opt(raw, 'http_bind_port', lambda v: isinstance(v, int) and 0 <= v <= 65535, p, 'protocols.rhio.http_bind_port must be 0..65535')
|
|
218
|
+
|
|
219
|
+
@classmethod
|
|
220
|
+
def from_dict(cls, raw: Any) -> 'RhioProtocolConfig':
|
|
221
|
+
if not isinstance(raw, Mapping):
|
|
222
|
+
return cls()
|
|
223
|
+
return cls(
|
|
224
|
+
enabled=bool(raw.get('enabled', False)),
|
|
225
|
+
nats_endpoint=str(raw.get('nats_endpoint', 'localhost:4222')),
|
|
226
|
+
s3_endpoint=str(raw.get('s3_endpoint', 'http://localhost:9000')),
|
|
227
|
+
network_id=str(raw.get('network_id', 'rhio-default-network-1')),
|
|
228
|
+
bind_port=int(raw.get('bind_port', 9102)),
|
|
229
|
+
http_bind_port=int(raw.get('http_bind_port', 3000)),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@dataclasses.dataclass(frozen=True)
|
|
234
|
+
class ProtocolSuitesConfig(Block):
|
|
235
|
+
host: HostMultiplexerConfig = HostMultiplexerConfig()
|
|
236
|
+
nostr: NostrProtocolConfig = NostrProtocolConfig()
|
|
237
|
+
atproto: ATProtoProtocolConfig = ATProtoProtocolConfig()
|
|
238
|
+
activitypub: ActivityPubProtocolConfig = ActivityPubProtocolConfig()
|
|
239
|
+
ssb: SSBProtocolConfig = SSBProtocolConfig()
|
|
240
|
+
p2panda: P2PandaProtocolConfig = P2PandaProtocolConfig()
|
|
241
|
+
rhio: RhioProtocolConfig = RhioProtocolConfig()
|
|
242
|
+
|
|
243
|
+
@classmethod
|
|
244
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
245
|
+
if not isinstance(raw, Mapping):
|
|
246
|
+
p.add('protocols must be an object'); return
|
|
247
|
+
if 'host' in raw:
|
|
248
|
+
HostMultiplexerConfig.check(raw['host'], p)
|
|
249
|
+
if 'nostr' in raw:
|
|
250
|
+
NostrProtocolConfig.check(raw['nostr'], p)
|
|
251
|
+
if 'atproto' in raw:
|
|
252
|
+
ATProtoProtocolConfig.check(raw['atproto'], p)
|
|
253
|
+
if 'activitypub' in raw:
|
|
254
|
+
ActivityPubProtocolConfig.check(raw['activitypub'], p)
|
|
255
|
+
if 'ssb' in raw:
|
|
256
|
+
SSBProtocolConfig.check(raw['ssb'], p)
|
|
257
|
+
if 'p2panda' in raw:
|
|
258
|
+
P2PandaProtocolConfig.check(raw['p2panda'], p)
|
|
259
|
+
if 'rhio' in raw:
|
|
260
|
+
RhioProtocolConfig.check(raw['rhio'], p)
|
|
261
|
+
|
|
262
|
+
@classmethod
|
|
263
|
+
def from_dict(cls, raw: Any) -> 'ProtocolSuitesConfig':
|
|
264
|
+
if not isinstance(raw, Mapping):
|
|
265
|
+
return cls()
|
|
266
|
+
return cls(
|
|
267
|
+
host=HostMultiplexerConfig.from_dict(raw.get('host')),
|
|
268
|
+
nostr=NostrProtocolConfig.from_dict(raw.get('nostr')),
|
|
269
|
+
atproto=ATProtoProtocolConfig.from_dict(raw.get('atproto')),
|
|
270
|
+
activitypub=ActivityPubProtocolConfig.from_dict(raw.get('activitypub')),
|
|
271
|
+
ssb=SSBProtocolConfig.from_dict(raw.get('ssb')),
|
|
272
|
+
p2panda=P2PandaProtocolConfig.from_dict(raw.get('p2panda')),
|
|
273
|
+
rhio=RhioProtocolConfig.from_dict(raw.get('rhio')),
|
|
274
|
+
)
|
node/config/records.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""The records config.json carries in lists and maps: an endpoint, a peer to
|
|
2
|
+
pull from, a coverage claim, a trust decision, a retention rule, and the two
|
|
3
|
+
small objects the node itself reads (face, policy).
|
|
4
|
+
|
|
5
|
+
These are read in many places - `peers` in five compartments, `coverage` in
|
|
6
|
+
four - and before this each reader implied its own shape. One dataclass per
|
|
7
|
+
record is the shape.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import dataclasses
|
|
12
|
+
from typing import Any, Mapping, Optional
|
|
13
|
+
|
|
14
|
+
from .base import Block, Problems, all_str, is_hex64, is_int, non_negative_int, opt, positive_int
|
|
15
|
+
|
|
16
|
+
TIERS: tuple[str, ...] = ('probation', 'member', 'trusted', 'anchor')
|
|
17
|
+
CLASSES: tuple[str, ...] = ('leaf', 'edge', 'anchor')
|
|
18
|
+
COMPLETENESS: tuple[str, ...] = ('asserted', 'best-effort')
|
|
19
|
+
HANDOVER: tuple[str, ...] = ('offer', 'never')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclasses.dataclass(frozen=True)
|
|
23
|
+
class Endpoint(Block):
|
|
24
|
+
"""One way to reach this node: `{transport, address[, priority]}`."""
|
|
25
|
+
transport: str
|
|
26
|
+
address: str
|
|
27
|
+
priority: int = 50
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
31
|
+
# The list check names the whole list once; see NodeConfig.
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def well_formed(e: Any) -> bool:
|
|
36
|
+
return isinstance(e, Mapping) and isinstance(e.get('transport'), str) and isinstance(e.get('address'), str)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_dict(cls, raw: Any) -> 'Endpoint':
|
|
40
|
+
return cls(transport=raw['transport'], address=raw['address'], priority=int(raw.get('priority', 50)))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclasses.dataclass(frozen=True)
|
|
44
|
+
class PeerPull(Block):
|
|
45
|
+
"""A peer this node pulls a shard from: `{pubkey, shard, filter[, pull_every, window]}`."""
|
|
46
|
+
pubkey: str
|
|
47
|
+
shard: str
|
|
48
|
+
filter: Mapping[str, Any]
|
|
49
|
+
pull_every: int = 900
|
|
50
|
+
window: Optional[int] = None # the peering manifest's budget.window, seconds
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def check_one(i: int, e: Any, p: Problems) -> None:
|
|
54
|
+
if not isinstance(e, Mapping) or not is_hex64(e.get('pubkey')) or not isinstance(e.get('shard'), str) \
|
|
55
|
+
or not isinstance(e.get('filter'), dict):
|
|
56
|
+
p.add('peers[%d] must be {pubkey (64 hex), shard, filter (object)[, pull_every]}' % i)
|
|
57
|
+
elif 'pull_every' in e and not positive_int(e['pull_every']):
|
|
58
|
+
p.add('peers[%d].pull_every must be a positive integer' % i)
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_dict(cls, raw: Any) -> 'PeerPull':
|
|
62
|
+
return cls(pubkey=raw['pubkey'], shard=raw['shard'], filter=dict(raw['filter']),
|
|
63
|
+
pull_every=int(raw.get('pull_every', 900)), window=raw.get('window'))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclasses.dataclass(frozen=True)
|
|
67
|
+
class CoverageClaim(Block):
|
|
68
|
+
"""A shard this node tells the mesh it holds, as a kind 30803 claim."""
|
|
69
|
+
shard: str
|
|
70
|
+
filter: Mapping[str, Any]
|
|
71
|
+
completeness: str = 'best-effort'
|
|
72
|
+
since: Optional[int] = None
|
|
73
|
+
until: Optional[int] = None
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def check_one(i: int, e: Any, p: Problems) -> None:
|
|
77
|
+
if not isinstance(e, Mapping) or not isinstance(e.get('shard'), str) or not isinstance(e.get('filter'), dict) \
|
|
78
|
+
or e.get('completeness', 'best-effort') not in COMPLETENESS:
|
|
79
|
+
p.add('coverage[%d] must be {shard, filter (object)[, completeness asserted|best-effort]}' % i)
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def from_dict(cls, raw: Any) -> 'CoverageClaim':
|
|
83
|
+
return cls(shard=raw['shard'], filter=dict(raw['filter']), completeness=raw.get('completeness', 'best-effort'),
|
|
84
|
+
since=raw.get('since'), until=raw.get('until'))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclasses.dataclass(frozen=True)
|
|
88
|
+
class TrustDecision(Block):
|
|
89
|
+
"""An operator's out-of-band decision about a peer: a tier, and when."""
|
|
90
|
+
tier: str
|
|
91
|
+
at: int = 0
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def check_one(pk: Any, e: Any, p: Problems) -> None:
|
|
95
|
+
tier = e.get('tier') if isinstance(e, Mapping) else e
|
|
96
|
+
if not is_hex64(pk) or tier not in TIERS or (isinstance(e, Mapping) and not is_int(e.get('at', 0))):
|
|
97
|
+
p.add('trust[%s] must be a tier name or {tier, at}' % str(pk)[:16])
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def from_dict(cls, raw: Any) -> 'TrustDecision':
|
|
101
|
+
if isinstance(raw, Mapping):
|
|
102
|
+
return cls(tier=raw['tier'], at=int(raw.get('at', 0)))
|
|
103
|
+
return cls(tier=raw)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# How far in the future the relay face accepts an event. The face constant
|
|
107
|
+
# (node/relayserver.py) is defined from this one: the config layer imports
|
|
108
|
+
# nothing that reaches a crypto dependency, so a tool that only reads config
|
|
109
|
+
# needs none.
|
|
110
|
+
FACE_FUTURE_MAX_SECONDS: int = 900
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclasses.dataclass(frozen=True)
|
|
114
|
+
class FaceConfig(Block):
|
|
115
|
+
"""The relay face: where it listens and how far in the future it accepts."""
|
|
116
|
+
key = 'face'
|
|
117
|
+
host: str = '127.0.0.1'
|
|
118
|
+
port: int = 0
|
|
119
|
+
future_seconds: int = FACE_FUTURE_MAX_SECONDS
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
123
|
+
if not isinstance(raw, Mapping) or not isinstance(raw.get('host', '127.0.0.1'), str) \
|
|
124
|
+
or not is_int(raw.get('port', 0)) or not 0 <= raw.get('port', 0) <= 65535:
|
|
125
|
+
p.add('face must be {host, port} with a port from 0 to 65535')
|
|
126
|
+
elif 'future_seconds' in raw and not non_negative_int(raw['future_seconds']):
|
|
127
|
+
p.add('face.future_seconds must be a non-negative integer')
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def from_dict(cls, raw: Any) -> 'FaceConfig':
|
|
131
|
+
return cls(host=raw.get('host', '127.0.0.1'), port=int(raw.get('port', 0)),
|
|
132
|
+
future_seconds=int(raw.get('future_seconds', FACE_FUTURE_MAX_SECONDS)))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
from .parameter import Parameter, ParamBlock
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclasses.dataclass(frozen=True)
|
|
139
|
+
class PolicyConfig(ParamBlock):
|
|
140
|
+
"""Whether strangers may enrol, and which protocols a peer must speak."""
|
|
141
|
+
key = 'policy'
|
|
142
|
+
open_enrolment: bool = True
|
|
143
|
+
protocols: tuple[str, ...] = ()
|
|
144
|
+
DERIVED: bool = False
|
|
145
|
+
PARAMS = (
|
|
146
|
+
Parameter(
|
|
147
|
+
key='open_enrolment', name='Open Enrolment',
|
|
148
|
+
description='Whether strangers may enrol without prior grant',
|
|
149
|
+
type=bool, default=True, tags=('peering', 'policy'),
|
|
150
|
+
),
|
|
151
|
+
Parameter(
|
|
152
|
+
key='protocols', name='Required Protocols',
|
|
153
|
+
description='Protocols a peer must speak to peer with this node',
|
|
154
|
+
type=list, item=str, default=(), tags=('peering', 'policy'),
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def check(cls, raw: Any, p: Problems) -> None:
|
|
160
|
+
if not isinstance(raw, Mapping) or not isinstance(raw.get('open_enrolment', True), bool):
|
|
161
|
+
p.add('policy must be an object with a boolean open_enrolment')
|
|
162
|
+
else:
|
|
163
|
+
opt(raw, 'protocols', all_str, p, 'policy.protocols must be a list of identifiers')
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
def from_dict(cls, raw: Any) -> 'PolicyConfig':
|
|
167
|
+
return cls(open_enrolment=bool(raw.get('open_enrolment', True)), protocols=tuple(raw.get('protocols') or ()))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
__all__ = ['Endpoint', 'PeerPull', 'CoverageClaim', 'TrustDecision', 'FaceConfig', 'PolicyConfig',
|
|
171
|
+
'TIERS', 'CLASSES', 'COMPLETENESS', 'HANDOVER']
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Programs that use a node from outside it; each is run by a test."""
|
node/examples/demo.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The MVP, end to end, in one run.
|
|
3
|
+
|
|
4
|
+
python3 node/demo.py
|
|
5
|
+
|
|
6
|
+
Three nodes and one public relay, all in this process. A client posts to node
|
|
7
|
+
A over ordinary NIP-01. A and B find each other's descriptors on the relay,
|
|
8
|
+
peer over DIDComm, publish their grants, and negentropy moves the events to B.
|
|
9
|
+
Receipts flow, a window closes clean, and the grant is amended. Then the
|
|
10
|
+
reference's bad peer is pointed at A and a kind 30802 comes out.
|
|
11
|
+
|
|
12
|
+
Nothing is mocked and nothing is stubbed: real BIP-340 signatures, real
|
|
13
|
+
NIP-44 envelopes, real gift wraps, real websockets, real SQLite. The clock is
|
|
14
|
+
the only thing driven by hand, because a window is an hour long.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import functools
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
import tempfile
|
|
23
|
+
import time
|
|
24
|
+
from typing import Any, Callable, Mapping, cast
|
|
25
|
+
|
|
26
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')))
|
|
27
|
+
from node import app as app_mod # noqa: E402
|
|
28
|
+
from node.protocols.nostr import relayserver # noqa: E402
|
|
29
|
+
from node.storage import grants # noqa: E402
|
|
30
|
+
from node.protocols.nostr import client as relay # noqa: E402
|
|
31
|
+
from fleetmesh_core.accounting import accounting, window # noqa: E402
|
|
32
|
+
from fleetmesh_core.crypto import crypto # noqa: E402
|
|
33
|
+
from fleetmesh_core.peering import badpeer, reports # noqa: E402
|
|
34
|
+
from fleetmesh_core.storage import store # noqa: E402
|
|
35
|
+
from fleetmesh_core.wire.interfaces import WindowEvaluationDetail # noqa: E402
|
|
36
|
+
|
|
37
|
+
W: int = 3600
|
|
38
|
+
NOW: int = (1787100000 // W) * W + 100
|
|
39
|
+
T: list[int] = [NOW]
|
|
40
|
+
clock: Callable[[], int] = lambda: T[0]
|
|
41
|
+
OPERATOR: str = crypto.pubkey('33' * 32)
|
|
42
|
+
BRISTOL: Mapping[str, Any] = {'kinds': [1], '#t': ['bristol']}
|
|
43
|
+
STEP: list[int] = [0]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def say(line: str = '') -> None:
|
|
47
|
+
print(line)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def step(title: str) -> None:
|
|
51
|
+
STEP[0] += 1
|
|
52
|
+
print('\n\033[1m%d. %s\033[0m' % (STEP[0], title))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def wait_for(pred: Callable[[], bool], seconds: float = 20.0) -> bool:
|
|
56
|
+
"""Poll until `pred` holds or the clock runs out. The demo asserts its
|
|
57
|
+
own steps rather than sleeping past them."""
|
|
58
|
+
deadline = time.time() + seconds
|
|
59
|
+
while time.time() < deadline and not pred():
|
|
60
|
+
time.sleep(0.05)
|
|
61
|
+
return pred()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main() -> None:
|
|
65
|
+
tmp = tempfile.mkdtemp(prefix='fleetmesh-demo-')
|
|
66
|
+
public = relayserver.RelayServer(clock=clock, name='public-relay').start()
|
|
67
|
+
relays = [public.url]
|
|
68
|
+
|
|
69
|
+
def node(name: str, **over: Any) -> app_mod.NodeApp:
|
|
70
|
+
cfg = dict(app_mod.defaults(), **{'relays': relays, 'operator': OPERATOR, 'pow_bits': 8,
|
|
71
|
+
'name': name, 'tick_seconds': 1})
|
|
72
|
+
cfg.update(over)
|
|
73
|
+
d = os.path.join(tmp, name)
|
|
74
|
+
app_mod.save_config(d, {k: v for k, v in cfg.items() if not k.startswith('_')})
|
|
75
|
+
return app_mod.NodeApp(d, cfg, clock=clock).start()
|
|
76
|
+
|
|
77
|
+
step('Two nodes start. Each mines a descriptor and publishes it.')
|
|
78
|
+
A, B = node('alpha'), node('beta')
|
|
79
|
+
time.sleep(0.5)
|
|
80
|
+
for n in (A, B):
|
|
81
|
+
say(' %-6s %s' % (n.cfg['name'], n.ks.did))
|
|
82
|
+
say(' relay face %s descriptor %s' % (n.face.url, n._descriptor()['id'][:16]))
|
|
83
|
+
found = relay.query_all(relays, [{'kinds': [11801]}])
|
|
84
|
+
say(' the public relay holds %d descriptors; neither node was told the other exists' % len(found))
|
|
85
|
+
|
|
86
|
+
step('A client posts to A over ordinary NIP-01. A is just a relay to it.')
|
|
87
|
+
client = relay.Relay(A.face.url, timeout=5)
|
|
88
|
+
posted = []
|
|
89
|
+
for i in range(6):
|
|
90
|
+
ev = store.make_signed_event('77' * 32, NOW - 100 + i, 1, [['t', 'bristol']], 'bristol note %d' % i)
|
|
91
|
+
accepted, note = client.publish(ev)
|
|
92
|
+
posted.append(ev)
|
|
93
|
+
off = store.make_signed_event('77' * 32, NOW - 90, 1, [['t', 'cardiff']], 'a note from elsewhere')
|
|
94
|
+
client.publish(off)
|
|
95
|
+
say(' posted 6 events tagged bristol and 1 tagged cardiff; A accepted all 7')
|
|
96
|
+
|
|
97
|
+
step('B resolves A from the relay, peers with it, and pulls the shard.')
|
|
98
|
+
B.cfg['peers'] = [{'pubkey': A.ks.node_pk, 'shard': 'bristol', 'filter': BRISTOL, 'pull_every': 600}]
|
|
99
|
+
app_mod.save_config(B.dir, {k: v for k, v in B.cfg.items() if not k.startswith('_')})
|
|
100
|
+
did = B.tick()
|
|
101
|
+
for d in did:
|
|
102
|
+
say(' %s' % ' '.join(str(x)[:24] for x in d))
|
|
103
|
+
held = B.grants.get(A.ks.node_pk, grants.RECEIVED)
|
|
104
|
+
assert held is not None, 'B took no grant from A' # the peering above must have landed
|
|
105
|
+
g = held['payload']
|
|
106
|
+
say(' B holds a grant from A: tier %s, events_in %d, window %ds' % (g['tier'], g['limits']['events_in'], g['window']))
|
|
107
|
+
env = relay.query_all(relays, [{'ids': [held['envelope_id']]}])[0]
|
|
108
|
+
say(' its kind 30801 on the relay shows only %s' % ', '.join(sorted({t[0] for t in env['tags']})))
|
|
109
|
+
say(' B now holds %d events; the cardiff one did not cross' % len(B.store._all()))
|
|
110
|
+
say(' both stores fingerprint the shard as %s' % A.store.fingerprint_range(BRISTOL, 0, 2 ** 31, T[0]))
|
|
111
|
+
|
|
112
|
+
step('B accounts for what it used. A acknowledges.')
|
|
113
|
+
T[0] += 700
|
|
114
|
+
for d in B.tick():
|
|
115
|
+
say(' %s' % ' '.join(str(x)[:24] for x in d))
|
|
116
|
+
wait_for(lambda: B.budget.acks.get(A.ks.node_pk) is not None)
|
|
117
|
+
E0 = accounting.epoch(NOW, W)
|
|
118
|
+
r = A.budget.receipts_in[B.ks.node_pk][E0][0]
|
|
119
|
+
say(' receipt seq %d: %s' % (r['seq'], ', '.join('%s=%s' % kv for kv in sorted(r['counters'].items()))))
|
|
120
|
+
say(' A answers: %s' % B.budget.acks[A.ks.node_pk]['status'])
|
|
121
|
+
|
|
122
|
+
step('The window closes. A evaluates it and amends the grant.')
|
|
123
|
+
T[0] = (E0 + 1) * W + 100
|
|
124
|
+
B.tick()
|
|
125
|
+
wait_for(lambda: len(A.budget.receipts_in.get(B.ks.node_pk, {}).get(E0, [])) >= 2)
|
|
126
|
+
T[0] = (E0 + 1) * W + 400
|
|
127
|
+
A.tick()
|
|
128
|
+
rec = [x for x in A.budget.evaluations if x.peer == B.ks.node_pk][0]
|
|
129
|
+
say(' window %d: measured against B\'s last receipt, severity %s' % (rec.epoch, rec.severity))
|
|
130
|
+
say(' remedy: %s, in force from window %s' % (rec.note, rec.effective_from))
|
|
131
|
+
T[0] = (E0 + 2) * W + 5
|
|
132
|
+
A.tick()
|
|
133
|
+
issued = A.grants.get(B.ks.node_pk, grants.ISSUED)
|
|
134
|
+
assert issued is not None, 'A holds no grant for B'
|
|
135
|
+
new_env = issued['envelope_id']
|
|
136
|
+
wait_for(lambda: (r := B.grants.get(A.ks.node_pk, grants.RECEIVED)) is not None and r.get('envelope_id') == new_env)
|
|
137
|
+
say(' at the boundary A published it and B took it up: %s' % new_env[:16])
|
|
138
|
+
say(' A published nothing else about it: a clean window is not an event')
|
|
139
|
+
|
|
140
|
+
step('A bad peer joins. It floods A with events A already holds.')
|
|
141
|
+
Flood = functools.partial(badpeer.BadPeer, offence='duplicate-flood', flood=8)
|
|
142
|
+
D = node('delta', _session_cls=Flood)
|
|
143
|
+
time.sleep(0.5)
|
|
144
|
+
dclient = relay.Relay(D.face.url, timeout=5)
|
|
145
|
+
for i in range(10):
|
|
146
|
+
dclient.publish(store.make_signed_event('88' * 32, NOW - 50 + i, 1, [['t', 'bristol']], 'flood %d' % i))
|
|
147
|
+
A.propose(D.ks.node_pk, 'bristol', BRISTOL, timeout=20)
|
|
148
|
+
wait_for(lambda: D.grants.get(A.ks.node_pk, grants.RECEIVED) is not None)
|
|
149
|
+
epoch_now = accounting.epoch(T[0], W)
|
|
150
|
+
say(' A peered with it and asked for 10 events')
|
|
151
|
+
res = A.pull(D.ks.node_pk, 'bristol', BRISTOL, timeout=40)
|
|
152
|
+
say(' it sent %d, of which %d were duplicates: a ratio of %.3f against a ceiling of %s'
|
|
153
|
+
% (res['counters']['events_in'], res['counters']['dups_sent'], res['dup_ratio'],
|
|
154
|
+
window.C['tier_limits']['dup_ratio']['all_tiers']))
|
|
155
|
+
say(' it never sent a receipt for any of it')
|
|
156
|
+
|
|
157
|
+
step('A prices that window, and keeps the finding to itself.')
|
|
158
|
+
T[0] = (epoch_now + 1) * W + int(W * 0.1) + 1
|
|
159
|
+
A.tick()
|
|
160
|
+
rec = [x for x in A.budget.evaluations if x.peer == D.ks.node_pk][0]
|
|
161
|
+
say(' window %d: %s, %s' % (rec.epoch, rec.state, rec.severity))
|
|
162
|
+
dup = cast(WindowEvaluationDetail, rec.detail['dup_ratio'])
|
|
163
|
+
say(' priced on the ratio alone: overrun %.2f over the ceiling' % dup['overrun'])
|
|
164
|
+
say(' remedy: %s' % rec.note)
|
|
165
|
+
unpub = [u for u in A.reporter.unpublishable if u[0] == D.ks.node_pk]
|
|
166
|
+
say(' A holds an S3 against D and will not publish it:')
|
|
167
|
+
for reason in unpub[0][3]:
|
|
168
|
+
say(' %s' % reason)
|
|
169
|
+
say(' D sent no receipt, so D signed nothing this window that A could quote.')
|
|
170
|
+
say(' Silence is the one case with no evidence to have: neither half of the claim')
|
|
171
|
+
say(' is checkable by a reader, so A says nothing in public and zeroes the grant.')
|
|
172
|
+
|
|
173
|
+
step('An offence that leaves something signed behind.')
|
|
174
|
+
Push = functools.partial(badpeer.BadPeer, offence='out-of-scope-push')
|
|
175
|
+
E = node('epsilon', _session_cls=Push)
|
|
176
|
+
time.sleep(0.5)
|
|
177
|
+
someone_else = store.make_signed_event('aa' * 32, T[0] - 20, 1, [['t', 'cardiff']], 'a note E did not write')
|
|
178
|
+
E.store.insert_event(someone_else, T[0])
|
|
179
|
+
eclient = relay.Relay(E.face.url, timeout=5)
|
|
180
|
+
for i in range(2):
|
|
181
|
+
eclient.publish(store.make_signed_event('99' * 32, T[0] - 30 + i, 1, [['t', 'bristol']], 'shard note %d' % i))
|
|
182
|
+
A.propose(E.ks.node_pk, 'bristol', BRISTOL, timeout=20)
|
|
183
|
+
wait_for(lambda: E.grants.get(A.ks.node_pk, grants.RECEIVED) is not None)
|
|
184
|
+
res2 = A.pull(E.ks.node_pk, 'bristol', BRISTOL, timeout=40)
|
|
185
|
+
say(' E served the shard and slipped in an event tagged cardiff, written by somebody else')
|
|
186
|
+
say(' A refused it: %s' % (res2['findings'][0][1][:76] if res2['findings'] else 'nothing refused'))
|
|
187
|
+
say(' E wrote no part of that event, but it signed the hop that carried it,')
|
|
188
|
+
say(' and the report carries that envelope whole, so a reader checks it against E\'s key')
|
|
189
|
+
published = [p for p in A.reporter.published if any(t[:2] == ['d', E.ks.node_pk] for t in p['tags'])]
|
|
190
|
+
if published:
|
|
191
|
+
report = published[0]
|
|
192
|
+
say(' kind 30802 %s, signed by A, on the public relay:' % report['id'][:16])
|
|
193
|
+
for t in report['tags']:
|
|
194
|
+
say(' %s' % ' '.join(t)[:96])
|
|
195
|
+
inline = reports.inline_evidence(report)
|
|
196
|
+
say(' its content carries the cited artefact, %d bytes, resolving nothing:' % len(report['content']))
|
|
197
|
+
say(' %s' % json.dumps(list(inline.values())[0])[:92])
|
|
198
|
+
|
|
199
|
+
step('What a third party makes of it.')
|
|
200
|
+
C = node('gamma')
|
|
201
|
+
time.sleep(0.5)
|
|
202
|
+
about = C.reporter.reports_about(E.ks.node_pk)
|
|
203
|
+
say(' C finds %d report(s) about E; admissible: %s' % (len(about), [a['admissible'] for a in about]))
|
|
204
|
+
view, weights = C.reporter.view(E.ks.node_pk)
|
|
205
|
+
if weights:
|
|
206
|
+
say(' C holds no grant to A, so A weighs %.1f and C\'s view of E stays %s' % (weights[0]['weight'], view))
|
|
207
|
+
for e in range(168):
|
|
208
|
+
C.reporter.ledger.record(A.ks.node_pk, 496000 + e, 'S0', 'anchor')
|
|
209
|
+
view, weights = C.reporter.view(E.ks.node_pk)
|
|
210
|
+
say(' after a week of clean windows with A, A weighs %.1f and C\'s view of E becomes %s'
|
|
211
|
+
% (weights[0]['weight'], view))
|
|
212
|
+
say(' nobody imported anybody\'s ban: C ran the arithmetic itself')
|
|
213
|
+
|
|
214
|
+
say()
|
|
215
|
+
say('\033[1mThe whole protocol, once, over a wire.\033[0m')
|
|
216
|
+
say(' %d nodes, %d relay, %d events stored at A, %d grants issued, %d report published, %d withheld'
|
|
217
|
+
% (5, 1, len(A.store._all()), len(A.grants.list(grants.ISSUED)), len(A.reporter.published),
|
|
218
|
+
len(A.reporter.unpublishable)))
|
|
219
|
+
say(' state in %s' % tmp)
|
|
220
|
+
for n in (A, B, C, D, E):
|
|
221
|
+
n.stop()
|
|
222
|
+
public.stop()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == '__main__':
|
|
226
|
+
main()
|