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,154 @@
|
|
|
1
|
+
"""gossip/1.0 wire vocabulary: PIURI, message types, and the GossipError.
|
|
2
|
+
|
|
3
|
+
Separate from core.py because the vocabulary is what validate.py, the tests,
|
|
4
|
+
and the subsystem all need, and it has no dependency on the mesh/seen-store
|
|
5
|
+
machinery that acts on it.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import uuid
|
|
10
|
+
from typing import Any, Mapping, Optional, Sequence
|
|
11
|
+
|
|
12
|
+
from fleetmesh_core.wire.wire_types import GossipGraftBody, GossipIdsBody, GossipPruneBody, GossipPublishBody, Message
|
|
13
|
+
|
|
14
|
+
PIURI: str = 'https://fleetmesh.org/gossip/1.0'
|
|
15
|
+
GRAFT_TYPE: str = PIURI + '/graft'
|
|
16
|
+
PRUNE_TYPE: str = PIURI + '/prune'
|
|
17
|
+
IHAVE_TYPE: str = PIURI + '/ihave'
|
|
18
|
+
IWANT_TYPE: str = PIURI + '/iwant'
|
|
19
|
+
PUBLISH_TYPE: str = PIURI + '/publish'
|
|
20
|
+
|
|
21
|
+
MESSAGES: tuple[str, ...] = ('graft', 'prune', 'ihave', 'iwant', 'publish')
|
|
22
|
+
|
|
23
|
+
# Target mesh degree per topic, the gossipsub D parameter. A node grafts up to
|
|
24
|
+
# this many peers per topic and regrafts on tick when it falls below.
|
|
25
|
+
DEFAULT_MESH_DEGREE: int = 6
|
|
26
|
+
|
|
27
|
+
MEMBERSHIP_KIND: int = 30804
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GossipError(Exception):
|
|
31
|
+
"""Raised when a gossip/1.0 wire message or mesh transition is invalid."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def make_graft(
|
|
35
|
+
from_did: str,
|
|
36
|
+
to_did: str,
|
|
37
|
+
topic: str,
|
|
38
|
+
membership_id: str,
|
|
39
|
+
created_time: Optional[int] = None,
|
|
40
|
+
msg_id: Optional[str] = None,
|
|
41
|
+
) -> Message[GossipGraftBody]:
|
|
42
|
+
"""Construct a DIDComm gossip/1.0/graft message.
|
|
43
|
+
|
|
44
|
+
`membership_id` is the id of the kind 30804 membership event the sender
|
|
45
|
+
holds naming its own pubkey for this topic: the receiver checks it names
|
|
46
|
+
the sender before adding them to its mesh, rather than trusting the ask.
|
|
47
|
+
"""
|
|
48
|
+
msg: Message[GossipGraftBody] = {
|
|
49
|
+
'id': msg_id or str(uuid.uuid4()),
|
|
50
|
+
'type': GRAFT_TYPE,
|
|
51
|
+
'from': from_did,
|
|
52
|
+
'to': [to_did],
|
|
53
|
+
'body': GossipGraftBody(topic=topic, membership=membership_id),
|
|
54
|
+
}
|
|
55
|
+
if created_time is not None:
|
|
56
|
+
msg['created_time'] = int(created_time)
|
|
57
|
+
return msg
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def make_prune(
|
|
61
|
+
from_did: str,
|
|
62
|
+
to_did: str,
|
|
63
|
+
topic: str,
|
|
64
|
+
reason: str = 'degree',
|
|
65
|
+
created_time: Optional[int] = None,
|
|
66
|
+
msg_id: Optional[str] = None,
|
|
67
|
+
) -> Message[GossipPruneBody]:
|
|
68
|
+
"""Construct a DIDComm gossip/1.0/prune message."""
|
|
69
|
+
msg: Message[GossipPruneBody] = {
|
|
70
|
+
'id': msg_id or str(uuid.uuid4()),
|
|
71
|
+
'type': PRUNE_TYPE,
|
|
72
|
+
'from': from_did,
|
|
73
|
+
'to': [to_did],
|
|
74
|
+
'body': GossipPruneBody(topic=topic, reason=reason),
|
|
75
|
+
}
|
|
76
|
+
if created_time is not None:
|
|
77
|
+
msg['created_time'] = int(created_time)
|
|
78
|
+
return msg
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def make_ihave(
|
|
82
|
+
from_did: str,
|
|
83
|
+
to_did: str,
|
|
84
|
+
topic: str,
|
|
85
|
+
ids: Sequence[str],
|
|
86
|
+
created_time: Optional[int] = None,
|
|
87
|
+
msg_id: Optional[str] = None,
|
|
88
|
+
) -> Message[GossipIdsBody]:
|
|
89
|
+
"""Construct a DIDComm gossip/1.0/ihave message: ids only, no payload."""
|
|
90
|
+
msg: Message[GossipIdsBody] = {
|
|
91
|
+
'id': msg_id or str(uuid.uuid4()),
|
|
92
|
+
'type': IHAVE_TYPE,
|
|
93
|
+
'from': from_did,
|
|
94
|
+
'to': [to_did],
|
|
95
|
+
'body': GossipIdsBody(topic=topic, ids=list(ids)),
|
|
96
|
+
}
|
|
97
|
+
if created_time is not None:
|
|
98
|
+
msg['created_time'] = int(created_time)
|
|
99
|
+
return msg
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def make_iwant(
|
|
103
|
+
from_did: str,
|
|
104
|
+
to_did: str,
|
|
105
|
+
topic: str,
|
|
106
|
+
ids: Sequence[str],
|
|
107
|
+
created_time: Optional[int] = None,
|
|
108
|
+
msg_id: Optional[str] = None,
|
|
109
|
+
) -> Message[GossipIdsBody]:
|
|
110
|
+
"""Construct a DIDComm gossip/1.0/iwant message: ids being asked for."""
|
|
111
|
+
msg: Message[GossipIdsBody] = {
|
|
112
|
+
'id': msg_id or str(uuid.uuid4()),
|
|
113
|
+
'type': IWANT_TYPE,
|
|
114
|
+
'from': from_did,
|
|
115
|
+
'to': [to_did],
|
|
116
|
+
'body': GossipIdsBody(topic=topic, ids=list(ids)),
|
|
117
|
+
}
|
|
118
|
+
if created_time is not None:
|
|
119
|
+
msg['created_time'] = int(created_time)
|
|
120
|
+
return msg
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def make_publish(
|
|
124
|
+
from_did: str,
|
|
125
|
+
to_did: str,
|
|
126
|
+
topic: str,
|
|
127
|
+
event_id: str,
|
|
128
|
+
envelope: Mapping[str, Any],
|
|
129
|
+
created_time: Optional[int] = None,
|
|
130
|
+
msg_id: Optional[str] = None,
|
|
131
|
+
) -> Message[GossipPublishBody]:
|
|
132
|
+
"""Construct a DIDComm gossip/1.0/publish message.
|
|
133
|
+
|
|
134
|
+
`envelope` is the already-sealed NIP-59 seal+wrap this node floods: gossip
|
|
135
|
+
never sees plaintext, only the per-member sealed bytes keyed by `event_id`
|
|
136
|
+
(the wrap's own id, which is what the seen-id store dedupes on).
|
|
137
|
+
"""
|
|
138
|
+
msg: Message[GossipPublishBody] = {
|
|
139
|
+
'id': msg_id or str(uuid.uuid4()),
|
|
140
|
+
'type': PUBLISH_TYPE,
|
|
141
|
+
'from': from_did,
|
|
142
|
+
'to': [to_did],
|
|
143
|
+
'body': GossipPublishBody(topic=topic, event_id=event_id, envelope=dict(envelope)),
|
|
144
|
+
}
|
|
145
|
+
if created_time is not None:
|
|
146
|
+
msg['created_time'] = int(created_time)
|
|
147
|
+
return msg
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
__all__ = [
|
|
151
|
+
'PIURI', 'GRAFT_TYPE', 'PRUNE_TYPE', 'IHAVE_TYPE', 'IWANT_TYPE', 'PUBLISH_TYPE',
|
|
152
|
+
'MESSAGES', 'DEFAULT_MESH_DEGREE', 'MEMBERSHIP_KIND', 'GossipError',
|
|
153
|
+
'make_graft', 'make_prune', 'make_ihave', 'make_iwant', 'make_publish',
|
|
154
|
+
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""The seen-id store: sqlite-backed, bounded by TTL and size, durable across
|
|
2
|
+
restarts.
|
|
3
|
+
|
|
4
|
+
A node that forgets an id the moment it restarts re-floods everything it has
|
|
5
|
+
already seen back out to its whole mesh, which is indistinguishable from an
|
|
6
|
+
attacker replaying a topic's history at every peer. Durability is what keeps
|
|
7
|
+
a restart from being an amplification event. Unbounded growth is the other
|
|
8
|
+
failure: a long-lived topic would otherwise make this table the largest
|
|
9
|
+
thing on disk for a compartment whose whole job is "have I seen this once
|
|
10
|
+
already" - so entries expire (SEEN_TTL_SECONDS) and the table is swept to
|
|
11
|
+
MAX_SEEN_ROWS on insert pressure, oldest first by `first_seen_at`, which is the
|
|
12
|
+
LRU reading of when a topic first carried an event.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import time
|
|
18
|
+
from typing import Any, Mapping, Optional
|
|
19
|
+
|
|
20
|
+
from node.compartment.schema import Column, Index, Table
|
|
21
|
+
from node.storage import SqliteStore
|
|
22
|
+
|
|
23
|
+
DEFAULT_SEEN_TTL_SECONDS: int = 24 * 3600
|
|
24
|
+
DEFAULT_MAX_SEEN_ROWS: int = 200_000
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SeenStore(SqliteStore):
|
|
28
|
+
"""(topic, event_id) -> first_seen_at, for gossip loop prevention."""
|
|
29
|
+
|
|
30
|
+
SCHEMA = (
|
|
31
|
+
Table('seen', (
|
|
32
|
+
Column('topic', 'TEXT', not_null=True),
|
|
33
|
+
Column('event_id', 'TEXT', not_null=True),
|
|
34
|
+
Column('first_seen_at', 'INTEGER', not_null=True),
|
|
35
|
+
), primary_key=('topic', 'event_id'), indices=(
|
|
36
|
+
Index('seen_at', ('first_seen_at',)),
|
|
37
|
+
)),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def __init__(self, path: str = ':memory:', ttl_seconds: int = DEFAULT_SEEN_TTL_SECONDS,
|
|
41
|
+
max_rows: int = DEFAULT_MAX_SEEN_ROWS, **kw: Any) -> None:
|
|
42
|
+
super().__init__(path, **kw)
|
|
43
|
+
self.ttl_seconds: int = ttl_seconds
|
|
44
|
+
self.max_rows: int = max_rows
|
|
45
|
+
|
|
46
|
+
def seen(self, topic: str, event_id: str) -> bool:
|
|
47
|
+
return self._one('SELECT 1 FROM seen WHERE topic=? AND event_id=?', (topic, event_id)) is not None
|
|
48
|
+
|
|
49
|
+
def mark(self, topic: str, event_id: str, now: Optional[int] = None) -> bool:
|
|
50
|
+
"""Record (topic, event_id) as seen. Returns True if this was new."""
|
|
51
|
+
at = int(now if now is not None else time.time())
|
|
52
|
+
with self._lock:
|
|
53
|
+
try:
|
|
54
|
+
self.execute('INSERT INTO seen VALUES (?,?,?)', (topic, event_id, at))
|
|
55
|
+
self.db.commit()
|
|
56
|
+
except Exception:
|
|
57
|
+
# Already present: the INSERT on the primary key failed. Roll
|
|
58
|
+
# the implicit transaction back, or this connection holds a
|
|
59
|
+
# write lock on the file until the next commit.
|
|
60
|
+
self.db.rollback()
|
|
61
|
+
return False
|
|
62
|
+
n = self._one('SELECT COUNT(*) FROM seen')
|
|
63
|
+
if n and n[0] > self.max_rows:
|
|
64
|
+
self._sweep_oldest(n[0] - self.max_rows)
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
def _sweep_oldest(self, excess: int) -> None:
|
|
68
|
+
with self._lock:
|
|
69
|
+
# Named by the declared key, not by SQLite's implicit rowid: the
|
|
70
|
+
# ordering was always `first_seen_at`, and rowid was only how the
|
|
71
|
+
# selected rows were identified. `(topic, event_id)` is the table's
|
|
72
|
+
# own PRIMARY KEY and says the same thing on any backend.
|
|
73
|
+
self.execute(
|
|
74
|
+
'DELETE FROM seen WHERE (topic, event_id) IN '
|
|
75
|
+
'(SELECT topic, event_id FROM seen ORDER BY first_seen_at ASC LIMIT ?)', (excess,))
|
|
76
|
+
self.db.commit()
|
|
77
|
+
|
|
78
|
+
def expire(self, now: Optional[int] = None) -> int:
|
|
79
|
+
"""Drop rows older than the TTL. Returns the count removed."""
|
|
80
|
+
cut = int(now if now is not None else time.time()) - self.ttl_seconds
|
|
81
|
+
with self._lock:
|
|
82
|
+
cur = self.execute('DELETE FROM seen WHERE first_seen_at < ?', (cut,))
|
|
83
|
+
self.db.commit()
|
|
84
|
+
return cur.rowcount if cur.rowcount is not None else 0
|
|
85
|
+
|
|
86
|
+
def count(self, topic: Optional[str] = None) -> int:
|
|
87
|
+
if topic is None:
|
|
88
|
+
row = self._one('SELECT COUNT(*) FROM seen')
|
|
89
|
+
else:
|
|
90
|
+
row = self._one('SELECT COUNT(*) FROM seen WHERE topic=?', (topic,))
|
|
91
|
+
return int(row[0]) if row else 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class RosterStore(SqliteStore):
|
|
95
|
+
"""topic -> the current admin-signed kind 30804 roster, durable across restarts.
|
|
96
|
+
|
|
97
|
+
A topic the operator created and then lost on the next restart is a topic
|
|
98
|
+
that silently stopped gossiping: no peer would graft to a node with no
|
|
99
|
+
membership event, and nothing would say why. The roster is the whole of
|
|
100
|
+
what makes a node a member, so it lives on disk next to the seen-ids.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
SCHEMA = (
|
|
104
|
+
Table('rosters', (
|
|
105
|
+
Column('topic', 'TEXT', primary_key=True),
|
|
106
|
+
Column('event', 'TEXT', not_null=True),
|
|
107
|
+
Column('created_at', 'INTEGER', not_null=True),
|
|
108
|
+
)),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def put(self, topic: str, event: Mapping[str, Any]) -> None:
|
|
112
|
+
with self._lock:
|
|
113
|
+
self.execute('INSERT OR REPLACE INTO rosters VALUES (?,?,?)',
|
|
114
|
+
(topic, json.dumps(event, separators=(',', ':'), sort_keys=True),
|
|
115
|
+
int(event.get('created_at', 0))))
|
|
116
|
+
self.db.commit()
|
|
117
|
+
|
|
118
|
+
def delete(self, topic: str) -> None:
|
|
119
|
+
with self._lock:
|
|
120
|
+
self.execute('DELETE FROM rosters WHERE topic=?', (topic,))
|
|
121
|
+
self.db.commit()
|
|
122
|
+
|
|
123
|
+
def all(self) -> dict[str, Mapping[str, Any]]:
|
|
124
|
+
return {topic: json.loads(raw) for topic, raw in self._all('SELECT topic, event FROM rosters')}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
__all__ = ['SeenStore', 'RosterStore', 'DEFAULT_SEEN_TTL_SECONDS', 'DEFAULT_MAX_SEEN_ROWS']
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Types and records for the Gossip compartment."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Mapping, Optional, TypedDict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GossipTotals(TypedDict):
|
|
8
|
+
"""Every gossip event this node took part in, counted since it started."""
|
|
9
|
+
published: int
|
|
10
|
+
delivered: int
|
|
11
|
+
offered: int
|
|
12
|
+
wanted: int
|
|
13
|
+
served: int
|
|
14
|
+
auto_admitted: int
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TopicView(TypedDict):
|
|
18
|
+
topic: str
|
|
19
|
+
admin: Optional[str]
|
|
20
|
+
members: list[str]
|
|
21
|
+
mesh: list[str]
|
|
22
|
+
expires: int
|
|
23
|
+
roster_id: Optional[str]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TopicCreationResult(TypedDict):
|
|
27
|
+
topic: str
|
|
28
|
+
event: Mapping[str, Any]
|
|
29
|
+
relays: dict[str, list[Any]]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class OriginateResult(TypedDict):
|
|
33
|
+
topic: str
|
|
34
|
+
event_id: str
|
|
35
|
+
sent_to: list[str]
|
|
36
|
+
mesh: int
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class GossipStateReport(TypedDict):
|
|
40
|
+
topics: list[str]
|
|
41
|
+
mesh: dict[str, int]
|
|
42
|
+
seen: int
|
|
43
|
+
published: int
|
|
44
|
+
auto_admitted: int
|
|
45
|
+
delivered: int
|
|
46
|
+
offered: int
|
|
47
|
+
wanted: int
|
|
48
|
+
served: int
|
|
49
|
+
mcache: int
|
|
50
|
+
refused: int
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""nsite client-side publishing and site walking workflows."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import mimetypes
|
|
5
|
+
import os
|
|
6
|
+
from typing import Callable, Sequence
|
|
7
|
+
|
|
8
|
+
MAX_FILES = 5000
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def walk_site_files(root: str, max_files: int = MAX_FILES) -> list[tuple[str, str]]:
|
|
12
|
+
"""Walk local directory for static site files.
|
|
13
|
+
|
|
14
|
+
Returns a list of (rel_path, full_path) tuples.
|
|
15
|
+
Raises ValueError if invalid directory, empty, or exceeds max_files.
|
|
16
|
+
"""
|
|
17
|
+
root = os.path.abspath(root)
|
|
18
|
+
if not os.path.isdir(root):
|
|
19
|
+
raise ValueError('%s is not a directory' % root)
|
|
20
|
+
out: list[tuple[str, str]] = []
|
|
21
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
22
|
+
dirnames[:] = sorted(d for d in dirnames if not d.startswith('.'))
|
|
23
|
+
for f in sorted(filenames):
|
|
24
|
+
if f.startswith('.'):
|
|
25
|
+
continue
|
|
26
|
+
full = os.path.join(dirpath, f)
|
|
27
|
+
rel = '/' + os.path.relpath(full, root).replace(os.sep, '/')
|
|
28
|
+
out.append((rel, full))
|
|
29
|
+
if not out:
|
|
30
|
+
raise ValueError('%s holds no files' % root)
|
|
31
|
+
if len(out) > max_files:
|
|
32
|
+
raise ValueError('%d files; a site here is at most %d' % (len(out), max_files))
|
|
33
|
+
return out
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def upload_site_blobs(
|
|
37
|
+
files: Sequence[tuple[str, str]],
|
|
38
|
+
face_http_url: str,
|
|
39
|
+
node_sk: str,
|
|
40
|
+
progress: Callable[[str, str, int], None] | None = None,
|
|
41
|
+
) -> tuple[dict[str, str], int]:
|
|
42
|
+
"""Upload site files to local Blossom face.
|
|
43
|
+
|
|
44
|
+
Returns (paths_dict, total_bytes) where paths_dict maps rel_path -> sha256.
|
|
45
|
+
"""
|
|
46
|
+
from node.protocols.nostr import blossom as blossom_mod
|
|
47
|
+
|
|
48
|
+
paths: dict[str, str] = {}
|
|
49
|
+
total = 0
|
|
50
|
+
for rel, full in files:
|
|
51
|
+
with open(full, 'rb') as fh:
|
|
52
|
+
data = fh.read()
|
|
53
|
+
ctype = mimetypes.guess_type(rel)[0] or 'application/octet-stream'
|
|
54
|
+
desc = blossom_mod.BlossomClient.upload_blob(
|
|
55
|
+
face_http_url, data, content_type=ctype, signer_sk=node_sk, allow_private=True
|
|
56
|
+
)
|
|
57
|
+
h = str(desc.get('sha256', '')).lower()
|
|
58
|
+
paths[rel] = h
|
|
59
|
+
total += len(data)
|
|
60
|
+
if progress is not None:
|
|
61
|
+
progress(rel, h, len(data))
|
|
62
|
+
return paths, total
|