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,304 @@
|
|
|
1
|
+
"""The node's event store: the reference SqliteStore, safe to share across threads.
|
|
2
|
+
|
|
3
|
+
The relay face serves every connection on its own thread and the sync
|
|
4
|
+
sessions run on others, all against one store. sqlite3 connections refuse
|
|
5
|
+
use from a thread other than the one that opened them unless told otherwise,
|
|
6
|
+
and even then two threads must not interleave statements. So: one
|
|
7
|
+
connection opened for any thread, and one lock around every primitive the
|
|
8
|
+
reference defines. The reference's own semantics - what supersedes what,
|
|
9
|
+
tombstones, ranges, fingerprints - are inherited untouched.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any, Callable, Iterable, Optional, TypedDict
|
|
14
|
+
|
|
15
|
+
from node.compartment.schema import Column, Index, Table
|
|
16
|
+
from node.interfaces import NostrEvent
|
|
17
|
+
from node.storage import SqliteStore
|
|
18
|
+
from fleetmesh_core.storage import store
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BridgeCursorRecord(TypedDict):
|
|
22
|
+
relay: str
|
|
23
|
+
last_reconciled_at: int
|
|
24
|
+
last_event_at: int
|
|
25
|
+
events_reconciled: int
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NodeStore(SqliteStore, store.SqliteStore):
|
|
29
|
+
LOCAL: str = 'local'
|
|
30
|
+
MESH: str = 'mesh'
|
|
31
|
+
|
|
32
|
+
# The reference's own tables first, verbatim, then this node's additions.
|
|
33
|
+
# The connection, PRAGMAs and lock come from BaseSqliteStore; the
|
|
34
|
+
# reference's query semantics - what supersedes what, tombstones, ranges,
|
|
35
|
+
# fingerprints - come from store.SqliteStore, whose __init__ is bypassed
|
|
36
|
+
# because the base already opened the connection and built the schema.
|
|
37
|
+
SCHEMA = (
|
|
38
|
+
Table('events', (
|
|
39
|
+
Column('id', 'TEXT', primary_key=True),
|
|
40
|
+
Column('pubkey', 'TEXT'),
|
|
41
|
+
Column('created_at', 'INTEGER'),
|
|
42
|
+
Column('kind', 'INTEGER'),
|
|
43
|
+
Column('json', 'TEXT'),
|
|
44
|
+
), indices=(
|
|
45
|
+
Index('events_created_at', ('created_at', 'id')),
|
|
46
|
+
# Supersession and deletion ask for one author's events, and
|
|
47
|
+
# asked it of the whole table until this existed.
|
|
48
|
+
Index('events_author_kind', ('pubkey', 'kind')),
|
|
49
|
+
Index('events_kind_created_at', ('kind', 'created_at', 'id')),
|
|
50
|
+
)),
|
|
51
|
+
Table('tombstones', (
|
|
52
|
+
Column('key', 'TEXT', not_null=True),
|
|
53
|
+
Column('deleter', 'TEXT', not_null=True),
|
|
54
|
+
), primary_key=('key', 'deleter')),
|
|
55
|
+
# Where each event came from. Not a protocol field and not in the
|
|
56
|
+
# reference: the specification only says what a node may *claim*,
|
|
57
|
+
# and forbids asserting completeness over a shard it holds only
|
|
58
|
+
# through mesh replication, which is how third-party replication
|
|
59
|
+
# would launder unverified peer data into an authoritative record.
|
|
60
|
+
# Knowing which is which is this implementation's own problem.
|
|
61
|
+
Table('origin', (
|
|
62
|
+
Column('id', 'TEXT', primary_key=True),
|
|
63
|
+
Column('origin', 'TEXT', not_null=True),
|
|
64
|
+
)),
|
|
65
|
+
# Single-letter tags, which is the set NIP-01 indexes. A client
|
|
66
|
+
# asking for its own gift wraps decoded every wrap in the store
|
|
67
|
+
# without this.
|
|
68
|
+
Table('tags', (
|
|
69
|
+
Column('id', 'TEXT', not_null=True),
|
|
70
|
+
Column('name', 'TEXT', not_null=True),
|
|
71
|
+
Column('value', 'TEXT', not_null=True),
|
|
72
|
+
), primary_key=('id', 'name', 'value'), indices=(
|
|
73
|
+
Index('tags_name_value', ('name', 'value')),
|
|
74
|
+
), without_rowid=True),
|
|
75
|
+
# Persistent checkpoints for external relay bridge reconciliation.
|
|
76
|
+
Table('bridge_cursor', (
|
|
77
|
+
Column('relay', 'TEXT', primary_key=True),
|
|
78
|
+
Column('last_reconciled_at', 'INTEGER'),
|
|
79
|
+
Column('last_event_at', 'INTEGER'),
|
|
80
|
+
Column('events_reconciled', 'INTEGER', default=0),
|
|
81
|
+
)),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def __init__(self, path: str = ':memory:', **kw: Any) -> None:
|
|
85
|
+
SqliteStore.__init__(self, path, **kw)
|
|
86
|
+
# After the lock exists and not before it. _index_tags goes through
|
|
87
|
+
# _locked like every other writer, so backfilling above this line
|
|
88
|
+
# raised AttributeError on the way up and took every node down at once
|
|
89
|
+
# - which is the same mistake, in the same shape, as the Reporter that
|
|
90
|
+
# read its account store four lines above the ledger it filled.
|
|
91
|
+
self._backfill_tags()
|
|
92
|
+
self.origin_swept: int = self._sweep_origin()
|
|
93
|
+
|
|
94
|
+
def _sweep_origin(self) -> int:
|
|
95
|
+
"""Drop provenance rows for events this store no longer holds.
|
|
96
|
+
|
|
97
|
+
`_drop` used to leave them: every superseded replaceable event and
|
|
98
|
+
every NIP-09 deletion left its `origin` row behind - measured at two
|
|
99
|
+
events held against seven rows - and nothing ever read or removed
|
|
100
|
+
them (audit 2026-10-15, B3). `_drop` clears its own row now, which
|
|
101
|
+
stops that source. One remains by design: sync records a delivered
|
|
102
|
+
event's origin before deciding whether to keep it (`sync/initiator`),
|
|
103
|
+
so a delivery that is refused leaves a row too. Hence on every open,
|
|
104
|
+
not once: after the first it removes only those, in one indexed
|
|
105
|
+
anti-join, before anything else can write.
|
|
106
|
+
"""
|
|
107
|
+
with self._lock:
|
|
108
|
+
cur = self.execute('DELETE FROM origin WHERE id NOT IN (SELECT id FROM events)')
|
|
109
|
+
self.db.commit()
|
|
110
|
+
return max(0, int(cur.rowcount or 0))
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def _locked(name: str) -> Callable[..., Any]:
|
|
114
|
+
def method(self: Any, *a: Any, **k: Any) -> Any:
|
|
115
|
+
with self._lock:
|
|
116
|
+
return getattr(store.SqliteStore, name)(self, *a, **k)
|
|
117
|
+
method.__name__ = name
|
|
118
|
+
return method
|
|
119
|
+
|
|
120
|
+
_put = _locked('_put')
|
|
121
|
+
|
|
122
|
+
def _drop(self, event_id: str) -> None:
|
|
123
|
+
"""The reference's drop, plus this node's own provenance row.
|
|
124
|
+
|
|
125
|
+
Called by supersession and by a NIP-09 deletion, inside the one
|
|
126
|
+
transaction `insert_event` holds (`_atomic` joins it), so the event,
|
|
127
|
+
its tags and its origin go together or not at all. `forget` already
|
|
128
|
+
removed all three; this is the other way an event leaves (B3).
|
|
129
|
+
"""
|
|
130
|
+
with self._lock, self._atomic():
|
|
131
|
+
store.SqliteStore._drop(self, event_id)
|
|
132
|
+
self.execute('DELETE FROM origin WHERE id = ?', (event_id,))
|
|
133
|
+
|
|
134
|
+
def _all(self, q: Optional[str] = None, args: Any = ()) -> list[Any]:
|
|
135
|
+
if q is not None:
|
|
136
|
+
return SqliteStore._all(self, q, args)
|
|
137
|
+
with self._lock:
|
|
138
|
+
return store.SqliteStore._all(self)
|
|
139
|
+
_get = _locked('_get')
|
|
140
|
+
# The narrowing reads go through the lock like every other primitive: the
|
|
141
|
+
# relay face serves each connection on its own thread and the sync sessions
|
|
142
|
+
# run on others, and two threads must not interleave statements on one
|
|
143
|
+
# connection whatever the statement is.
|
|
144
|
+
_narrow = _locked('_narrow')
|
|
145
|
+
# Held across the read rather than around a fetchall: the point of it is
|
|
146
|
+
# that it stops early, and a fetchall would put the whole match set back
|
|
147
|
+
# in memory as the rows it was written to avoid parsing.
|
|
148
|
+
newest_matching = _locked('newest_matching')
|
|
149
|
+
_index_tags = _locked('_index_tags')
|
|
150
|
+
_by_author_kind = _locked('_by_author_kind')
|
|
151
|
+
_by_author = _locked('_by_author')
|
|
152
|
+
_tombstone = _locked('_tombstone')
|
|
153
|
+
_tombstoned_by = _locked('_tombstoned_by')
|
|
154
|
+
# insert_event reads then writes; hold the lock across the whole decision.
|
|
155
|
+
insert_event = _locked('insert_event')
|
|
156
|
+
|
|
157
|
+
def by_kind_before(self, kind: int, cutoff: int, cap: Optional[int] = None,
|
|
158
|
+
after: Optional[tuple[int, str]] = None) -> list[NostrEvent]:
|
|
159
|
+
"""Events of one kind dated before a cutoff, oldest first, one page.
|
|
160
|
+
|
|
161
|
+
For retention, which is the only caller. The index is on
|
|
162
|
+
(created_at, id), so this reads a range rather than the table.
|
|
163
|
+
|
|
164
|
+
`cap` and `after` are what keep the sweep from being the thing that
|
|
165
|
+
kills the node: a policy set for the first time on a face holding days
|
|
166
|
+
of gift wraps has every one of them past the cutoff at once, and this
|
|
167
|
+
used to answer with all of them parsed. `after` is the (created_at, id)
|
|
168
|
+
of the last row of the previous page, and the cursor rather than an
|
|
169
|
+
offset because the sweep is deleting as it walks - and because an event
|
|
170
|
+
held back inside a claimed shard stays, so a page that re-read what it
|
|
171
|
+
had refused would never end.
|
|
172
|
+
"""
|
|
173
|
+
import json as _json
|
|
174
|
+
where: list[str] = ['kind = ?', 'created_at < ?']
|
|
175
|
+
args: list[int | str] = [int(kind), int(cutoff)]
|
|
176
|
+
if after is not None:
|
|
177
|
+
where.append('(created_at, id) > (?, ?)')
|
|
178
|
+
args.extend([int(after[0]), str(after[1])])
|
|
179
|
+
sql = ('SELECT json FROM events WHERE ' + ' AND '.join(where)
|
|
180
|
+
+ ' ORDER BY created_at, id')
|
|
181
|
+
if cap is not None:
|
|
182
|
+
sql += ' LIMIT ?'
|
|
183
|
+
args.append(int(cap))
|
|
184
|
+
with self._lock:
|
|
185
|
+
rows = self.execute(sql, args).fetchall()
|
|
186
|
+
return [_json.loads(r[0]) for r in rows]
|
|
187
|
+
|
|
188
|
+
def count(self) -> int:
|
|
189
|
+
"""How many events this node holds, as one number from the index.
|
|
190
|
+
|
|
191
|
+
Written into status.json on every tick, and until this existed it was
|
|
192
|
+
`len(self._all())`: the whole store parsed into dicts to be thrown
|
|
193
|
+
away, so the node's peak heap was the size of its store and the
|
|
194
|
+
allocator kept it. On the public face that was 600 MB of a 1 GB box
|
|
195
|
+
for a number that a COUNT answers without reading a row.
|
|
196
|
+
"""
|
|
197
|
+
with self._lock:
|
|
198
|
+
return int(self.execute('SELECT COUNT(*) FROM events').fetchone()[0])
|
|
199
|
+
|
|
200
|
+
def forget(self, event_id: str) -> bool:
|
|
201
|
+
"""Drop one event because this node stopped keeping it. NOT a deletion.
|
|
202
|
+
|
|
203
|
+
A NIP-09 kind 5 tombstones an id so a later re-offer cannot resurrect
|
|
204
|
+
it, because there the author said the thing should not exist. Retention
|
|
205
|
+
says only that THIS node stopped holding it, which is a fact about a
|
|
206
|
+
disk and not about the event: a peer may hand it back tomorrow and be
|
|
207
|
+
right to. So the row goes and no tombstone is written.
|
|
208
|
+
"""
|
|
209
|
+
with self._lock:
|
|
210
|
+
cur = self.execute('DELETE FROM events WHERE id = ?', (event_id,))
|
|
211
|
+
self.execute('DELETE FROM origin WHERE id = ?', (event_id,))
|
|
212
|
+
self.execute('DELETE FROM tags WHERE id = ?', (event_id,))
|
|
213
|
+
self.db.commit()
|
|
214
|
+
return cur.rowcount > 0
|
|
215
|
+
|
|
216
|
+
def note_origin(self, event_id: str, origin: str) -> None:
|
|
217
|
+
"""Record how one event reached this node. First writer wins: an event
|
|
218
|
+
this node already held locally is not reclassified because a peer later
|
|
219
|
+
sent it too."""
|
|
220
|
+
if origin not in (self.LOCAL, self.MESH):
|
|
221
|
+
raise ValueError('origin is local or mesh')
|
|
222
|
+
with self._lock:
|
|
223
|
+
self.execute('INSERT OR IGNORE INTO origin VALUES (?,?)', (event_id, origin))
|
|
224
|
+
self.db.commit()
|
|
225
|
+
|
|
226
|
+
def origin_of(self, event_id: str) -> Optional[str]:
|
|
227
|
+
with self._lock:
|
|
228
|
+
row = self.execute('SELECT origin FROM origin WHERE id = ?', (event_id,)).fetchone()
|
|
229
|
+
return row[0] if row else None
|
|
230
|
+
|
|
231
|
+
def any_replicated(self, event_ids: Iterable[str]) -> bool:
|
|
232
|
+
"""True if any of these arrived through the mesh, or is of unknown origin.
|
|
233
|
+
|
|
234
|
+
Unknown counts as replicated. A claim of completeness is a falsifiable
|
|
235
|
+
assertion about data this node stands behind, and an event it cannot
|
|
236
|
+
account for the arrival of is not data it can stand behind.
|
|
237
|
+
"""
|
|
238
|
+
ids = list(event_ids)
|
|
239
|
+
if not ids:
|
|
240
|
+
return False
|
|
241
|
+
with self._lock:
|
|
242
|
+
known: dict[str, str] = {}
|
|
243
|
+
# Chunked: a have/need may carry validate.MAX_IDS ids, past what
|
|
244
|
+
# SQLite binds in one statement.
|
|
245
|
+
for chunk in store.chunked(ids):
|
|
246
|
+
sql, vals = store.in_clause('id', chunk)
|
|
247
|
+
known.update((r[0], r[1]) for r in self.execute(
|
|
248
|
+
'SELECT id, origin FROM origin WHERE ' + sql, vals))
|
|
249
|
+
return any(known.get(i) != self.LOCAL for i in ids)
|
|
250
|
+
|
|
251
|
+
def get_bridge_cursor(self, relay_url: str) -> Optional[BridgeCursorRecord]:
|
|
252
|
+
with self._lock:
|
|
253
|
+
cur = self.db.cursor()
|
|
254
|
+
cur.execute('SELECT last_reconciled_at, last_event_at, events_reconciled FROM bridge_cursor WHERE relay = ?', (relay_url,))
|
|
255
|
+
row = cur.fetchone()
|
|
256
|
+
if row is None:
|
|
257
|
+
return None
|
|
258
|
+
return {
|
|
259
|
+
'relay': relay_url,
|
|
260
|
+
'last_reconciled_at': row[0],
|
|
261
|
+
'last_event_at': row[1],
|
|
262
|
+
'events_reconciled': row[2],
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
def set_bridge_cursor(
|
|
266
|
+
self,
|
|
267
|
+
relay_url: str,
|
|
268
|
+
last_reconciled_at: Optional[int] = None,
|
|
269
|
+
last_event_at: Optional[int] = None,
|
|
270
|
+
count_increment: int = 0,
|
|
271
|
+
) -> None:
|
|
272
|
+
with self._lock:
|
|
273
|
+
cur = self.db.cursor()
|
|
274
|
+
cur.execute('SELECT last_reconciled_at, last_event_at, events_reconciled FROM bridge_cursor WHERE relay = ?', (relay_url,))
|
|
275
|
+
row = cur.fetchone()
|
|
276
|
+
if row is None:
|
|
277
|
+
cur.execute(
|
|
278
|
+
'INSERT INTO bridge_cursor (relay, last_reconciled_at, last_event_at, events_reconciled) VALUES (?, ?, ?, ?)',
|
|
279
|
+
(relay_url, last_reconciled_at or 0, last_event_at or 0, count_increment)
|
|
280
|
+
)
|
|
281
|
+
else:
|
|
282
|
+
new_reconciled = last_reconciled_at if last_reconciled_at is not None else row[0]
|
|
283
|
+
new_event_at = max(row[1] or 0, last_event_at or 0)
|
|
284
|
+
new_count = (row[2] or 0) + count_increment
|
|
285
|
+
cur.execute(
|
|
286
|
+
'UPDATE bridge_cursor SET last_reconciled_at = ?, last_event_at = ?, events_reconciled = ? WHERE relay = ?',
|
|
287
|
+
(new_reconciled, new_event_at, new_count, relay_url)
|
|
288
|
+
)
|
|
289
|
+
self.db.commit()
|
|
290
|
+
|
|
291
|
+
def all_bridge_cursors(self) -> list[BridgeCursorRecord]:
|
|
292
|
+
with self._lock:
|
|
293
|
+
cur = self.db.cursor()
|
|
294
|
+
cur.execute('SELECT relay, last_reconciled_at, last_event_at, events_reconciled FROM bridge_cursor ORDER BY relay')
|
|
295
|
+
rows = cur.fetchall()
|
|
296
|
+
return [
|
|
297
|
+
{
|
|
298
|
+
'relay': r[0],
|
|
299
|
+
'last_reconciled_at': r[1],
|
|
300
|
+
'last_event_at': r[2],
|
|
301
|
+
'events_reconciled': r[3],
|
|
302
|
+
}
|
|
303
|
+
for r in rows
|
|
304
|
+
]
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""fleetmesh-protocol-nostr: Decoupled Nostr & Blossom protocol package.
|
|
2
|
+
|
|
3
|
+
Conforms to ProtocolPackage (fleetmesh_core.wire.interfaces.ProtocolPackage).
|
|
4
|
+
Wraps RelayServer and BlossomServer, allowing them to be mounted onto
|
|
5
|
+
ProtocolHostRouter or operated with zero listening overhead when disabled.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from fleetmesh_core.wire import spelling # noqa: E402
|
|
10
|
+
|
|
11
|
+
from typing import Any, Callable, Mapping, Optional
|
|
12
|
+
|
|
13
|
+
from fleetmesh_core.wire.interfaces import (
|
|
14
|
+
ProtocolHealth,
|
|
15
|
+
ProtocolHostRouter,
|
|
16
|
+
ProtocolIdentityAdapter,
|
|
17
|
+
)
|
|
18
|
+
from . import relayserver
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NostrIdentityAdapter:
|
|
22
|
+
"""Identity and assertion adapter for Nostr (secp256k1 / BIP-340)."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, keystore: Any) -> None:
|
|
25
|
+
self.ks = keystore
|
|
26
|
+
|
|
27
|
+
def resolve_id(self, uri: str) -> Optional[str]:
|
|
28
|
+
# Handle npub1... or hex
|
|
29
|
+
if uri.startswith('npub1'):
|
|
30
|
+
try:
|
|
31
|
+
from fleetmesh_core.crypto import bech32
|
|
32
|
+
raw = bech32.bech32_decode('npub', uri)
|
|
33
|
+
if raw is not None:
|
|
34
|
+
return raw.hex()
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
elif spelling.is_hex(uri, 64, upper=True):
|
|
38
|
+
return uri.lower()
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
def sign_assertion(self, payload: bytes) -> bytes:
|
|
42
|
+
import hashlib
|
|
43
|
+
from fleetmesh_core.crypto import crypto
|
|
44
|
+
msg32 = hashlib.sha256(payload).digest() if len(payload) != 32 else payload
|
|
45
|
+
sk = getattr(self.ks, 'node_sk', None)
|
|
46
|
+
sk_hex = sk.hex() if isinstance(sk, (bytes, bytearray)) else str(sk or '')
|
|
47
|
+
sig = crypto.schnorr_sign(msg32, sk_hex)
|
|
48
|
+
return bytes.fromhex(sig)
|
|
49
|
+
|
|
50
|
+
def verify_assertion(self, payload: bytes, signature: bytes, pubkey: str) -> bool:
|
|
51
|
+
import hashlib
|
|
52
|
+
from fleetmesh_core.crypto import crypto
|
|
53
|
+
msg32 = hashlib.sha256(payload).digest() if len(payload) != 32 else payload
|
|
54
|
+
try:
|
|
55
|
+
return crypto.schnorr_verify(msg32, pubkey, signature.hex())
|
|
56
|
+
except Exception:
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class NostrProtocolPackage:
|
|
61
|
+
"""The Nostr & Blossom protocol suite for Fleetmesh."""
|
|
62
|
+
|
|
63
|
+
name: str = 'nostr'
|
|
64
|
+
version: str = '1.0.0'
|
|
65
|
+
standards: tuple[str, ...] = (
|
|
66
|
+
'NIP-01', 'NIP-11', 'NIP-40', 'NIP-42', 'NIP-44', 'NIP-59', 'NIP-77',
|
|
67
|
+
'BUD-01', 'BUD-02', 'BUD-04',
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
backend: Optional[Any] = None,
|
|
73
|
+
blob_store: Optional[Any] = None,
|
|
74
|
+
grants: Optional[Any] = None,
|
|
75
|
+
node: Optional[Any] = None,
|
|
76
|
+
clock: Optional[Callable[[], int]] = None,
|
|
77
|
+
name: str = 'meshnode',
|
|
78
|
+
pubkey: Optional[str] = None,
|
|
79
|
+
) -> None:
|
|
80
|
+
self.backend = backend
|
|
81
|
+
self.blob_store = blob_store
|
|
82
|
+
self.grants = grants
|
|
83
|
+
self.node = node
|
|
84
|
+
self.clock = clock
|
|
85
|
+
self.node_name = name
|
|
86
|
+
self.pubkey = pubkey
|
|
87
|
+
self.config: Mapping[str, Any] = {}
|
|
88
|
+
self.relay_server: Optional[relayserver.RelayServer] = None
|
|
89
|
+
self._host: Optional[ProtocolHostRouter] = None
|
|
90
|
+
self._enabled: bool = True
|
|
91
|
+
|
|
92
|
+
def configure(self, config: Mapping[str, Any]) -> None:
|
|
93
|
+
self.config = dict(config)
|
|
94
|
+
self._enabled = self.config.get('enabled', True)
|
|
95
|
+
|
|
96
|
+
def identity_adapter(self, keystore: Any) -> Optional[ProtocolIdentityAdapter]:
|
|
97
|
+
if keystore is None:
|
|
98
|
+
return None
|
|
99
|
+
return NostrIdentityAdapter(keystore)
|
|
100
|
+
|
|
101
|
+
def mount_ingress(self, host: ProtocolHostRouter) -> None:
|
|
102
|
+
if not self._enabled:
|
|
103
|
+
return
|
|
104
|
+
self._host = host
|
|
105
|
+
if self.relay_server is None:
|
|
106
|
+
self.relay_server = relayserver.RelayServer(
|
|
107
|
+
backend=self.backend,
|
|
108
|
+
blob_store=self.blob_store,
|
|
109
|
+
grants=self.grants,
|
|
110
|
+
node=self.node,
|
|
111
|
+
clock=self.clock,
|
|
112
|
+
name=self.node_name,
|
|
113
|
+
pubkey=self.pubkey,
|
|
114
|
+
future_seconds=self.config.get('future_seconds', 900),
|
|
115
|
+
require_auth=self.config.get('require_auth', False),
|
|
116
|
+
host_router=host,
|
|
117
|
+
)
|
|
118
|
+
self.relay_server.mount_ingress(host, owner='nostr')
|
|
119
|
+
|
|
120
|
+
def transport_rung(self, context: Any) -> Optional[Any]:
|
|
121
|
+
# Outbound RelayRung for TransportRouter
|
|
122
|
+
if not self._enabled:
|
|
123
|
+
return None
|
|
124
|
+
from node.transport.relay_rung import RelayRung
|
|
125
|
+
from node.transport.rung import RelayRungAdapter
|
|
126
|
+
if context is not None and hasattr(context, 'nostr'):
|
|
127
|
+
return RelayRungAdapter(context)
|
|
128
|
+
if context is not None and hasattr(context, 'ks') and hasattr(context, 'peers'):
|
|
129
|
+
return RelayRung(
|
|
130
|
+
keystore=context.ks,
|
|
131
|
+
peers=context.peers,
|
|
132
|
+
relay_urls=self.config.get('relays', ()),
|
|
133
|
+
clock=context.clock,
|
|
134
|
+
)
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
def compartments(self) -> tuple[type[Any], ...]:
|
|
138
|
+
# Background compartments associated with Nostr
|
|
139
|
+
return ()
|
|
140
|
+
|
|
141
|
+
def start(self) -> None:
|
|
142
|
+
if not self._enabled:
|
|
143
|
+
return
|
|
144
|
+
if self.relay_server is not None:
|
|
145
|
+
self.relay_server.start()
|
|
146
|
+
|
|
147
|
+
def stop(self) -> None:
|
|
148
|
+
if self.relay_server is not None:
|
|
149
|
+
self.relay_server.stop()
|
|
150
|
+
|
|
151
|
+
def health(self) -> ProtocolHealth:
|
|
152
|
+
if not self._enabled:
|
|
153
|
+
return ProtocolHealth(
|
|
154
|
+
healthy=True,
|
|
155
|
+
status="disabled",
|
|
156
|
+
connections_active=0,
|
|
157
|
+
messages_in=0,
|
|
158
|
+
messages_out=0,
|
|
159
|
+
errors=(),
|
|
160
|
+
)
|
|
161
|
+
if self.relay_server is not None:
|
|
162
|
+
return ProtocolHealth(
|
|
163
|
+
healthy=True,
|
|
164
|
+
status="serving",
|
|
165
|
+
connections_active=len(getattr(self.relay_server, '_conns', {})),
|
|
166
|
+
messages_in=self.relay_server.accepted,
|
|
167
|
+
messages_out=0,
|
|
168
|
+
errors=(),
|
|
169
|
+
)
|
|
170
|
+
return ProtocolHealth(
|
|
171
|
+
healthy=True,
|
|
172
|
+
status="configured",
|
|
173
|
+
connections_active=0,
|
|
174
|
+
messages_in=0,
|
|
175
|
+
messages_out=0,
|
|
176
|
+
errors=(),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def descriptor_capabilities(self) -> tuple[tuple[str, str], ...]:
|
|
180
|
+
if not self._enabled:
|
|
181
|
+
return ()
|
|
182
|
+
return (('c', 'relay'), ('c', 'negentropy'))
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""What this node knows about other nodes: their live descriptors.
|
|
2
|
+
|
|
3
|
+
A peer is addressed by its node pubkey. From its descriptor come the
|
|
4
|
+
agreement key an envelope is encrypted to, the class, the protocols it
|
|
5
|
+
implements and the endpoints. Descriptors are cached and refreshed from
|
|
6
|
+
relays when missing or expired, and every one goes through
|
|
7
|
+
fleetmesh_core.identity.validate_descriptor before it is believed.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from typing import Callable, Optional, Sequence
|
|
14
|
+
|
|
15
|
+
from cryptography.hazmat.primitives.asymmetric import x25519
|
|
16
|
+
from fleetmesh_core.crypto import keys # noqa: E402
|
|
17
|
+
from fleetmesh_core.identity import identity # noqa: E402
|
|
18
|
+
from node.interfaces import NostrEvent # noqa: E402
|
|
19
|
+
from . import descriptor # noqa: E402
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def agreement_key(desc: NostrEvent) -> Optional[x25519.X25519PublicKey]:
|
|
23
|
+
"""The X25519 public key a descriptor's agreement Multikey names, or None."""
|
|
24
|
+
for t in desc.get('tags', []):
|
|
25
|
+
if t and t[0] == 'key' and len(t) > 2 and t[1] == 'agreement':
|
|
26
|
+
kind, raw = keys.parse_multikey(t[2])
|
|
27
|
+
if kind == 'agreement':
|
|
28
|
+
return keys.agreement_public(raw)
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
from fleetmesh_core.identity.did import node_of_did
|
|
33
|
+
|
|
34
|
+
__all__ = ['Peers', 'agreement_key']
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Peers:
|
|
38
|
+
# A descriptor that is not on the relays is asked for again on every
|
|
39
|
+
# message its key sends, and the asking happens on the transport's
|
|
40
|
+
# listener thread with the relay timeout in front of it. A stranger with
|
|
41
|
+
# no descriptor could hold that thread for fifteen seconds per wrap. A
|
|
42
|
+
# miss is remembered for this long before the relays are asked again.
|
|
43
|
+
MISS_TTL: int = 60
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
relay_urls: Sequence[str] = (),
|
|
48
|
+
clock: Optional[Callable[[], int]] = None,
|
|
49
|
+
timeout: float = 15.0,
|
|
50
|
+
resolver: Optional[Callable[[str, Sequence[str], Optional[int], float], Optional[NostrEvent]]] = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
self.relays: list[str] = list(relay_urls)
|
|
53
|
+
self.clock: Callable[[], int] = clock or (lambda: int(time.time()))
|
|
54
|
+
self.timeout: float = timeout
|
|
55
|
+
# How a descriptor not held is looked up; given so a test can count
|
|
56
|
+
# the lookups without touching the descriptor module.
|
|
57
|
+
self.resolver = resolver or (lambda pk, urls, now, timeout: descriptor.resolve(pk, urls, now=now, timeout=timeout))
|
|
58
|
+
self._cache: dict[str, NostrEvent] = {}
|
|
59
|
+
self._missing: dict[str, int] = {} # pubkey -> when a failed resolve may be retried
|
|
60
|
+
self._lock: threading.Lock = threading.Lock()
|
|
61
|
+
|
|
62
|
+
def learn(self, desc: NostrEvent) -> bool:
|
|
63
|
+
"""Accept a descriptor if it is genuine, validates and is newer than what we hold."""
|
|
64
|
+
if not descriptor.genuine(desc) or identity.validate_descriptor(desc, now=self.clock(), digest=descriptor.DIGEST):
|
|
65
|
+
return False
|
|
66
|
+
with self._lock:
|
|
67
|
+
have = self._cache.get(desc['pubkey'])
|
|
68
|
+
if have is None or desc['created_at'] > have['created_at']:
|
|
69
|
+
self._cache[desc['pubkey']] = desc
|
|
70
|
+
self._missing.pop(desc['pubkey'], None)
|
|
71
|
+
return True
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
def descriptor(self, pubkey: str, refresh: bool = False) -> Optional[NostrEvent]:
|
|
75
|
+
now = self.clock()
|
|
76
|
+
with self._lock:
|
|
77
|
+
d = self._cache.get(pubkey)
|
|
78
|
+
if d is not None and not refresh and not identity.validate_descriptor(d, now=now, digest=descriptor.DIGEST):
|
|
79
|
+
return d
|
|
80
|
+
with self._lock:
|
|
81
|
+
until = self._missing.get(pubkey)
|
|
82
|
+
if until is not None and now < until and not refresh:
|
|
83
|
+
return None
|
|
84
|
+
if self.relays:
|
|
85
|
+
fresh = self.resolver(pubkey, self.relays, now, self.timeout)
|
|
86
|
+
if fresh is not None:
|
|
87
|
+
self.learn(fresh)
|
|
88
|
+
return fresh
|
|
89
|
+
with self._lock:
|
|
90
|
+
self._missing[pubkey] = now + self.MISS_TTL
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
def agreement(self, pubkey: str) -> Optional[x25519.X25519PublicKey]:
|
|
94
|
+
d = self.descriptor(pubkey)
|
|
95
|
+
return agreement_key(d) if d is not None else None
|
|
96
|
+
|
|
97
|
+
def resolve_sender(self, skid: str) -> Optional[x25519.X25519PublicKey]:
|
|
98
|
+
"""For didcomm.decrypt: the static agreement key a skid names, if the
|
|
99
|
+
descriptor behind it is live. The kid must be the #agree fragment."""
|
|
100
|
+
if not skid.endswith('#agree'):
|
|
101
|
+
return None
|
|
102
|
+
try:
|
|
103
|
+
return self.agreement(node_of_did(skid))
|
|
104
|
+
except ValueError:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
def known(self) -> list[str]:
|
|
108
|
+
with self._lock:
|
|
109
|
+
return sorted(self._cache)
|