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/storage/grants.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""Grants on disk: the ones this node issued and the ones it holds.
|
|
2
|
+
|
|
3
|
+
One SQLite table, one row per (peer, direction). The payload is the grant as
|
|
4
|
+
decrypted or as written, the envelope id is the kind 30801 that committed to
|
|
5
|
+
it, and status is live or revoked. A node that restarts must know exactly
|
|
6
|
+
what it granted and what it was granted, or its receipts are fiction.
|
|
7
|
+
|
|
8
|
+
Two more tables came from the two-machine run. `history` keeps every grant a
|
|
9
|
+
pair has been under and the epoch from which it governed, because a window is
|
|
10
|
+
priced under the grant that governed it and not under whatever grant happens
|
|
11
|
+
to be current when the grace period ends. `pending` holds the grant a window
|
|
12
|
+
evaluation decided on until the next window boundary, because a grant that
|
|
13
|
+
changes mid-window changes what a receipt in flight means: the issuer was
|
|
14
|
+
rejecting every receipt naming the grant it had just replaced, and the
|
|
15
|
+
subject was restarting its chain at seq 0 on the grant it had just taken up,
|
|
16
|
+
so every amend filed an S2 misreport against an honest node. An amend now
|
|
17
|
+
takes effect at a boundary, as the specification's grant loop says: the
|
|
18
|
+
verdict on one window shapes "the next window's grant".
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import builtins
|
|
23
|
+
import json
|
|
24
|
+
import time
|
|
25
|
+
from typing import Any, Literal, Mapping, Optional
|
|
26
|
+
|
|
27
|
+
from ..compartment.schema import Column, Migration, Table
|
|
28
|
+
from . import SqliteStore
|
|
29
|
+
from fleetmesh_core.wire.interfaces import EventLike, GrantPayload
|
|
30
|
+
from fleetmesh_core.wire.wire_types import GoverningRow, GrantListRow, GrantRow, HistoryRow, PendingRow
|
|
31
|
+
|
|
32
|
+
GrantDirection = Literal['issued', 'received']
|
|
33
|
+
ISSUED: GrantDirection = 'issued'
|
|
34
|
+
RECEIVED: GrantDirection = 'received'
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class GrantStore(SqliteStore):
|
|
38
|
+
SCHEMA = (
|
|
39
|
+
Table('grants', (
|
|
40
|
+
Column('peer', 'TEXT', not_null=True),
|
|
41
|
+
Column('direction', 'TEXT', not_null=True),
|
|
42
|
+
Column('envelope_id', 'TEXT', not_null=True),
|
|
43
|
+
Column('payload', 'TEXT', not_null=True),
|
|
44
|
+
Column('tier', 'TEXT', not_null=True),
|
|
45
|
+
Column('status', 'TEXT', not_null=True, default="'live'"),
|
|
46
|
+
Column('updated_at', 'INTEGER', not_null=True),
|
|
47
|
+
), primary_key=('peer', 'direction')),
|
|
48
|
+
Table('history', (
|
|
49
|
+
Column('peer', 'TEXT', not_null=True),
|
|
50
|
+
Column('direction', 'TEXT', not_null=True),
|
|
51
|
+
Column('envelope_id', 'TEXT', not_null=True),
|
|
52
|
+
Column('payload', 'TEXT', not_null=True),
|
|
53
|
+
Column('effective_epoch', 'INTEGER', not_null=True),
|
|
54
|
+
Column('put_at', 'INTEGER', not_null=True),
|
|
55
|
+
), primary_key=('peer', 'direction', 'envelope_id')),
|
|
56
|
+
Table('pending', (
|
|
57
|
+
Column('peer', 'TEXT', primary_key=True),
|
|
58
|
+
Column('payload', 'TEXT', not_null=True),
|
|
59
|
+
Column('note', 'TEXT'),
|
|
60
|
+
Column('severity', 'TEXT'),
|
|
61
|
+
Column('decided_epoch', 'INTEGER', not_null=True),
|
|
62
|
+
)),
|
|
63
|
+
Table('seeds', (
|
|
64
|
+
Column('peer', 'TEXT', primary_key=True),
|
|
65
|
+
Column('tier', 'TEXT', not_null=True),
|
|
66
|
+
Column('at', 'INTEGER', not_null=True),
|
|
67
|
+
)),
|
|
68
|
+
)
|
|
69
|
+
MIGRATIONS = (
|
|
70
|
+
# The envelope's own created_at, which is what NIP-01 supersedes on. A
|
|
71
|
+
# grant is only ever superseded forwards, so this is how a node refuses
|
|
72
|
+
# an amend that arrived out of order.
|
|
73
|
+
Migration(1, up='ALTER TABLE grants ADD COLUMN env_created_at INTEGER', name='grants_env_created_at'),
|
|
74
|
+
# The signed kind 30801 itself, not only its id. A grant is addressable
|
|
75
|
+
# under a blinded tag, so each new one replaces the last at that
|
|
76
|
+
# coordinate and no relay serves the superseded one - and a report
|
|
77
|
+
# about a past window cites exactly that. A report carries its own
|
|
78
|
+
# copy now, which it can only do if this node kept it.
|
|
79
|
+
Migration(2, up='ALTER TABLE history ADD COLUMN event TEXT', name='history_event'),
|
|
80
|
+
# Insertion order, carried explicitly. `history` ordered by
|
|
81
|
+
# (effective_epoch, put_at, rowid) and rowid is SQLite's implicit
|
|
82
|
+
# insertion counter, which PostgreSQL has no equivalent for - ctid is a
|
|
83
|
+
# physical location, not insertion order, and moves when a row is
|
|
84
|
+
# updated. Two grants for one peer and direction written in the same
|
|
85
|
+
# second at the same epoch tie on everything else, and which one won
|
|
86
|
+
# decided which grant governed the window - the record a peer's
|
|
87
|
+
# standing is read from. So the tiebreaker becomes a column.
|
|
88
|
+
Migration(3, up='ALTER TABLE history ADD COLUMN seq INTEGER NOT NULL DEFAULT 0', name='history_seq'),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def _next_seq(self, peer: str, direction: str) -> int:
|
|
92
|
+
"""The next insertion number for this peer and direction.
|
|
93
|
+
|
|
94
|
+
Explicit rather than an auto-increment, because the two backends spell
|
|
95
|
+
that differently and SQLite cannot add one with ALTER TABLE. Every
|
|
96
|
+
caller holds the store lock, so the read-then-write cannot race.
|
|
97
|
+
"""
|
|
98
|
+
row = self._one('SELECT COALESCE(MAX(seq), 0) + 1 FROM history '
|
|
99
|
+
'WHERE peer=? AND direction=?', (peer, direction))
|
|
100
|
+
return int(row[0]) if row and row[0] is not None else 1
|
|
101
|
+
|
|
102
|
+
def _post_schema(self) -> None:
|
|
103
|
+
# Rows written before `seq` existed get their insertion order from the
|
|
104
|
+
# backend's own, where it has one. SQLite's is `rowid`; PostgreSQL has
|
|
105
|
+
# none - `ctid` is a physical location, not insertion order, and moves
|
|
106
|
+
# when a row is updated - but a PostgreSQL store is new, so its history
|
|
107
|
+
# is empty and there is nothing to carry over. `WHERE seq = 0` makes
|
|
108
|
+
# this run once: `_next_seq` starts at 1, so a row it numbered is never
|
|
109
|
+
# mistaken for one that needs numbering.
|
|
110
|
+
expr = getattr(self.dialect, 'rowid_expr', None)
|
|
111
|
+
if expr and self.has_column('history', 'seq'):
|
|
112
|
+
self.execute('UPDATE history SET seq = %s WHERE seq = 0' % expr)
|
|
113
|
+
|
|
114
|
+
# A store from before history existed: whatever it holds now has
|
|
115
|
+
# governed since the beginning of time, as far as it can tell.
|
|
116
|
+
for peer, direction, env, payload, updated_at in self._all(
|
|
117
|
+
'SELECT peer, direction, envelope_id, payload, updated_at FROM grants'):
|
|
118
|
+
if self._one('SELECT 1 FROM history WHERE peer=? AND direction=?',
|
|
119
|
+
(peer, direction)) is None:
|
|
120
|
+
self.execute(
|
|
121
|
+
'INSERT OR IGNORE INTO history '
|
|
122
|
+
'(peer, direction, envelope_id, payload, effective_epoch, put_at, event, seq) '
|
|
123
|
+
'VALUES (?,?,?,?,?,?,NULL,?)',
|
|
124
|
+
(peer, direction, env, payload, 0, updated_at,
|
|
125
|
+
self._next_seq(peer, direction)))
|
|
126
|
+
|
|
127
|
+
def put(
|
|
128
|
+
self,
|
|
129
|
+
peer: str,
|
|
130
|
+
direction: GrantDirection,
|
|
131
|
+
payload: GrantPayload | Mapping[str, Any],
|
|
132
|
+
envelope_id: str,
|
|
133
|
+
now: Optional[int] = None,
|
|
134
|
+
env_created_at: Optional[int] = None,
|
|
135
|
+
effective_epoch: Optional[int] = None,
|
|
136
|
+
event: Optional[EventLike] = None,
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Record a grant. effective_epoch is the first window it governs; a
|
|
139
|
+
caller that does not say gets the window `now` falls in."""
|
|
140
|
+
if direction not in (ISSUED, RECEIVED):
|
|
141
|
+
raise ValueError('direction is issued or received')
|
|
142
|
+
now = int(now or time.time())
|
|
143
|
+
if effective_epoch is None:
|
|
144
|
+
effective_epoch = now // (payload.get('window') or 3600)
|
|
145
|
+
with self._lock:
|
|
146
|
+
if env_created_at is not None:
|
|
147
|
+
held = self._one('SELECT env_created_at, envelope_id FROM grants WHERE peer=? AND direction=?',
|
|
148
|
+
(peer, direction))
|
|
149
|
+
if held and held[0] is not None and (env_created_at, envelope_id) < (held[0], held[1]):
|
|
150
|
+
# A grant is only superseded forwards, in the order a relay
|
|
151
|
+
# resolves the coordinate: latest created_at, lower id on a
|
|
152
|
+
# tie. Two written for one subject by two threads were put
|
|
153
|
+
# in the other order, and the issuer then held the one the
|
|
154
|
+
# relay had let go. The history still records it.
|
|
155
|
+
self.execute(
|
|
156
|
+
'INSERT OR REPLACE INTO history '
|
|
157
|
+
'(peer, direction, envelope_id, payload, effective_epoch, put_at, event, seq) '
|
|
158
|
+
'VALUES (?,?,?,?,?,?,?,?)',
|
|
159
|
+
(peer, direction, envelope_id, json.dumps(payload, sort_keys=True),
|
|
160
|
+
int(effective_epoch), now,
|
|
161
|
+
json.dumps(event, sort_keys=True) if event is not None else None,
|
|
162
|
+
self._next_seq(peer, direction)))
|
|
163
|
+
self.db.commit()
|
|
164
|
+
return
|
|
165
|
+
self.execute('INSERT OR REPLACE INTO grants VALUES (?,?,?,?,?,?,?,?)',
|
|
166
|
+
(peer, direction, envelope_id, json.dumps(payload, sort_keys=True),
|
|
167
|
+
payload.get('tier', ''), 'revoked' if payload.get('tier') == 'revoked' else 'live',
|
|
168
|
+
now, env_created_at))
|
|
169
|
+
self.execute(
|
|
170
|
+
'INSERT OR REPLACE INTO history '
|
|
171
|
+
'(peer, direction, envelope_id, payload, effective_epoch, put_at, event, seq) '
|
|
172
|
+
'VALUES (?,?,?,?,?,?,?,?)',
|
|
173
|
+
(peer, direction, envelope_id, json.dumps(payload, sort_keys=True),
|
|
174
|
+
int(effective_epoch), now,
|
|
175
|
+
json.dumps(event, sort_keys=True) if event is not None else
|
|
176
|
+
self.envelope_event(peer, direction, envelope_id, as_json=True),
|
|
177
|
+
self._next_seq(peer, direction)))
|
|
178
|
+
self.db.commit()
|
|
179
|
+
|
|
180
|
+
def governing(self, peer: str, direction: GrantDirection, epoch: int) -> Optional[GoverningRow]:
|
|
181
|
+
"""The grant that governed one window: the latest one effective at or
|
|
182
|
+
before it. Two written in the same second tie on put_at, so insertion
|
|
183
|
+
order breaks the tie: a grant is only superseded forwards. Falls back
|
|
184
|
+
to the current row, so a store that predates the history still answers."""
|
|
185
|
+
with self._lock:
|
|
186
|
+
row = self._one(
|
|
187
|
+
'SELECT payload, envelope_id, effective_epoch FROM history WHERE peer=? AND direction=? '
|
|
188
|
+
'AND effective_epoch<=? ORDER BY effective_epoch DESC, put_at DESC, seq DESC LIMIT 1',
|
|
189
|
+
(peer, direction, int(epoch)))
|
|
190
|
+
if row is None: # a window before the first grant: the first grant is the nearest answer
|
|
191
|
+
row = self._one(
|
|
192
|
+
'SELECT payload, envelope_id, effective_epoch FROM history WHERE peer=? AND direction=? '
|
|
193
|
+
'ORDER BY effective_epoch, put_at, seq LIMIT 1', (peer, direction))
|
|
194
|
+
if row:
|
|
195
|
+
return {'payload': json.loads(row[0]), 'envelope_id': row[1], 'effective_epoch': row[2]}
|
|
196
|
+
cur = self.get(peer, direction)
|
|
197
|
+
if cur is None:
|
|
198
|
+
return None
|
|
199
|
+
return {'payload': cur['payload'], 'envelope_id': cur['envelope_id'], 'effective_epoch': None}
|
|
200
|
+
|
|
201
|
+
def envelope_event(
|
|
202
|
+
self,
|
|
203
|
+
peer: str,
|
|
204
|
+
direction: GrantDirection,
|
|
205
|
+
envelope_id: str,
|
|
206
|
+
as_json: bool = False,
|
|
207
|
+
) -> Any:
|
|
208
|
+
"""The signed kind 30801 this node held under that id, if it kept one."""
|
|
209
|
+
with self._lock:
|
|
210
|
+
row = self._one('SELECT event FROM history WHERE peer=? AND direction=? AND envelope_id=?',
|
|
211
|
+
(peer, direction, envelope_id))
|
|
212
|
+
if not row or not row[0]:
|
|
213
|
+
return None
|
|
214
|
+
return row[0] if as_json else json.loads(row[0])
|
|
215
|
+
|
|
216
|
+
def known_envelopes(self, peer: str, direction: GrantDirection) -> list[str]:
|
|
217
|
+
with self._lock:
|
|
218
|
+
return [r[0] for r in self._all(
|
|
219
|
+
'SELECT envelope_id FROM history WHERE peer=? AND direction=? ORDER BY effective_epoch, put_at',
|
|
220
|
+
(peer, direction))]
|
|
221
|
+
|
|
222
|
+
# ---- the grant a window decided on, waiting for the boundary -------------
|
|
223
|
+
def put_pending(self, peer: str, payload: GrantPayload | Mapping[str, Any], note: str, severity: Optional[str],
|
|
224
|
+
decided_epoch: int) -> None:
|
|
225
|
+
with self._lock:
|
|
226
|
+
self.execute('INSERT OR REPLACE INTO pending VALUES (?,?,?,?,?)',
|
|
227
|
+
(peer, json.dumps(payload, sort_keys=True), note, severity, int(decided_epoch)))
|
|
228
|
+
self.db.commit()
|
|
229
|
+
|
|
230
|
+
def pending(self, peer: str) -> Optional[PendingRow]:
|
|
231
|
+
with self._lock:
|
|
232
|
+
row = self._one('SELECT payload, note, severity, decided_epoch FROM pending WHERE peer=?',
|
|
233
|
+
(peer,))
|
|
234
|
+
if not row:
|
|
235
|
+
return None
|
|
236
|
+
return {'payload': json.loads(row[0]), 'note': row[1], 'severity': row[2], 'decided_epoch': row[3]}
|
|
237
|
+
|
|
238
|
+
def clear_pending(self, peer: str) -> None:
|
|
239
|
+
with self._lock:
|
|
240
|
+
self.execute('DELETE FROM pending WHERE peer=?', (peer,))
|
|
241
|
+
self.db.commit()
|
|
242
|
+
|
|
243
|
+
# ---- the operator's decisions, as applied ----------------------------------
|
|
244
|
+
def seed_applied(self, peer: str) -> Optional[tuple[str, int]]:
|
|
245
|
+
with self._lock:
|
|
246
|
+
row = self._one('SELECT tier, at FROM seeds WHERE peer=?', (peer,))
|
|
247
|
+
return (row[0], row[1]) if row else None
|
|
248
|
+
|
|
249
|
+
def put_seed(self, peer: str, tier: str, at: int) -> None:
|
|
250
|
+
with self._lock:
|
|
251
|
+
self.execute('INSERT OR REPLACE INTO seeds VALUES (?,?,?)', (peer, tier, int(at)))
|
|
252
|
+
self.db.commit()
|
|
253
|
+
|
|
254
|
+
def get(self, peer: str, direction: GrantDirection) -> Optional[GrantRow]:
|
|
255
|
+
# Every write here took the lock and every read did not, on a
|
|
256
|
+
# connection opened with check_same_thread=False and shared by the
|
|
257
|
+
# tick, sync, peering and transport threads. sqlite answers that with
|
|
258
|
+
# an intermittent "bad parameter or other API misuse", which is what a
|
|
259
|
+
# sweep caught in grants.get after the capacity ladder added a reader
|
|
260
|
+
# on the peering path. A read is as much a use of the connection as a
|
|
261
|
+
# write.
|
|
262
|
+
with self._lock:
|
|
263
|
+
row = self._one('SELECT payload, envelope_id, status, env_created_at, updated_at FROM grants '
|
|
264
|
+
'WHERE peer=? AND direction=?', (peer, direction))
|
|
265
|
+
if not row:
|
|
266
|
+
return None
|
|
267
|
+
return {'payload': json.loads(row[0]), 'envelope_id': row[1], 'status': row[2],
|
|
268
|
+
'env_created_at': row[3], 'updated_at': row[4]}
|
|
269
|
+
|
|
270
|
+
def history(self, peer: str, direction: GrantDirection) -> list[HistoryRow]:
|
|
271
|
+
"""Every grant that has governed a window with this peer, oldest first.
|
|
272
|
+
|
|
273
|
+
Here because `grant_view` in app.py used to read it straight off the
|
|
274
|
+
connection, without the lock, from the control socket's thread while
|
|
275
|
+
the tick and the sync threads were writing. `windows_view` beside it
|
|
276
|
+
took the account store's lock, so the rule was known and this one call
|
|
277
|
+
was missed. A store that hands out its connection cannot enforce
|
|
278
|
+
anything about how it is used.
|
|
279
|
+
"""
|
|
280
|
+
return [{'envelope_id': r[0], 'effective_epoch': r[1], 'tier': json.loads(r[2]).get('tier')}
|
|
281
|
+
for r in self._all(
|
|
282
|
+
'SELECT envelope_id, effective_epoch, payload FROM history '
|
|
283
|
+
'WHERE peer=? AND direction=? ORDER BY effective_epoch, put_at, seq',
|
|
284
|
+
(peer, direction))]
|
|
285
|
+
|
|
286
|
+
# `builtins.list` because the method itself is called `list`, and inside the
|
|
287
|
+
# class body the bare name would resolve to the method, not the type.
|
|
288
|
+
def list(self, direction: Optional[GrantDirection] = None) -> builtins.list[GrantListRow]:
|
|
289
|
+
with self._lock:
|
|
290
|
+
q = 'SELECT peer, direction, payload, envelope_id, status FROM grants'
|
|
291
|
+
args: tuple[Any, ...] = ()
|
|
292
|
+
if direction:
|
|
293
|
+
q += ' WHERE direction=?'
|
|
294
|
+
args = (direction,)
|
|
295
|
+
return [{'peer': r[0], 'direction': r[1], 'payload': json.loads(r[2]),
|
|
296
|
+
'envelope_id': r[3], 'status': r[4]} for r in self._all(q, args)]
|
|
297
|
+
|
|
298
|
+
def zero(self, peer: str, now: Optional[int] = None) -> None:
|
|
299
|
+
"""Both directions to zero and revoked: a terminate or a revocation."""
|
|
300
|
+
with self._lock:
|
|
301
|
+
for direction in (ISSUED, RECEIVED):
|
|
302
|
+
row = self.get(peer, direction)
|
|
303
|
+
if row:
|
|
304
|
+
p = row['payload']
|
|
305
|
+
p['limits'] = {k: 0 for k in p.get('limits', {})}
|
|
306
|
+
p['tier'] = 'revoked'
|
|
307
|
+
self.execute('UPDATE grants SET payload=?, tier=?, status=?, updated_at=? '
|
|
308
|
+
'WHERE peer=? AND direction=?',
|
|
309
|
+
(json.dumps(p, sort_keys=True), 'revoked', 'revoked',
|
|
310
|
+
int(now or time.time()), peer, direction))
|
|
311
|
+
self.execute('DELETE FROM pending WHERE peer=?', (peer,))
|
|
312
|
+
self.db.commit()
|
|
313
|
+
|
|
314
|
+
def peers(self) -> builtins.list[str]:
|
|
315
|
+
with self._lock:
|
|
316
|
+
return sorted({r[0] for r in self._all('SELECT peer FROM grants WHERE status=\'live\'')})
|
|
317
|
+
|
node/storage/locator.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Where each of the node's stores lives, decided once from config.
|
|
2
|
+
|
|
3
|
+
Every store site in the tree reads the same two things: a directory and a
|
|
4
|
+
logical name, joined into `<directory>/<name>.sqlite`. That join is the only
|
|
5
|
+
reason the tree is tied to SQLite now that the dialect layer exists, so it is
|
|
6
|
+
the thing to replace - not with a flag threaded through fourteen files, but
|
|
7
|
+
with one object that answers "where does the store called `grants` live" and
|
|
8
|
+
is built once from the node's configuration.
|
|
9
|
+
|
|
10
|
+
loc = Locator.from_config(directory) # the default
|
|
11
|
+
loc = Locator.from_config(directory, url='postgresql://...')
|
|
12
|
+
store = loc.open(grants.GrantStore, 'grants')
|
|
13
|
+
|
|
14
|
+
On SQLite that is the same path the site computed before, so the default node
|
|
15
|
+
is byte-for-byte unchanged. On PostgreSQL every store gets the same URL: one
|
|
16
|
+
node per database is settled (docs/storage-backends.md section 10), the forty-
|
|
17
|
+
four table names do not collide, and the schema version is already keyed by
|
|
18
|
+
store name rather than by database - the defect that taught us so is recorded
|
|
19
|
+
at postgres.py:218.
|
|
20
|
+
|
|
21
|
+
One dialect instance is shared by every store it opens. `PostgresDialect`
|
|
22
|
+
learns each table's columns and primary key from the DDL passing through
|
|
23
|
+
`sql()`, so sharing means the reflection is learned once rather than fourteen
|
|
24
|
+
times, and the methods that use a connection take it as an argument.
|
|
25
|
+
|
|
26
|
+
A backend is named by URL scheme, and an unknown one is refused by name
|
|
27
|
+
rather than quietly becoming a filename.
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import os
|
|
32
|
+
from typing import Any, Callable, Optional, TypedDict
|
|
33
|
+
|
|
34
|
+
from .dialect import Dialect, SqliteDialect
|
|
35
|
+
|
|
36
|
+
SQLITE_SUFFIX = '.sqlite'
|
|
37
|
+
POSTGRES_SCHEMES = ('postgresql://', 'postgres://')
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _Backend(TypedDict):
|
|
41
|
+
backend: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class StorageBackend(_Backend, total=False):
|
|
45
|
+
"""The backend as `meshnode health` reports it.
|
|
46
|
+
|
|
47
|
+
A named record rather than a loose mapping, for the same reason
|
|
48
|
+
`StoreLease` is one: check-dicts.py holds this tree to typing what it
|
|
49
|
+
returns, and a row an operator reads by field name is the case that rule
|
|
50
|
+
exists for. Exactly one of `directory` and `server` is present - which one
|
|
51
|
+
is the answer being reported.
|
|
52
|
+
"""
|
|
53
|
+
directory: str
|
|
54
|
+
server: str
|
|
55
|
+
owner: str
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Locator:
|
|
59
|
+
"""The node's storage target. Build it with `from_config`."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, directory: str, dialect: Dialect,
|
|
62
|
+
url: Optional[str] = None, owner: Optional[str] = None) -> None:
|
|
63
|
+
self.directory: str = directory
|
|
64
|
+
self.dialect: Dialect = dialect
|
|
65
|
+
self.url: Optional[str] = url
|
|
66
|
+
self.owner: Optional[str] = owner
|
|
67
|
+
|
|
68
|
+
# ---- construction -----------------------------------------------------
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def from_config(cls, directory: str, url: Optional[str] = None,
|
|
72
|
+
owner: Optional[str] = None) -> 'Locator':
|
|
73
|
+
"""The locator a node's config asks for.
|
|
74
|
+
|
|
75
|
+
No URL is the default and needs nothing of the operator. A URL names
|
|
76
|
+
its backend by scheme; anything else is a configuration error and is
|
|
77
|
+
reported as one, because treating it as a path would produce a node
|
|
78
|
+
that silently ran on SQLite while an operator believed otherwise.
|
|
79
|
+
|
|
80
|
+
`owner` is the node's identity for the ownership lease. It is what
|
|
81
|
+
makes one node per database enforced rather than assumed
|
|
82
|
+
(docs/storage-backends.md section 5a): `BaseStore` claims a lease only
|
|
83
|
+
when it is given an owner, so a locator without one opens stores that
|
|
84
|
+
claim nothing. On a server every store shares the database and
|
|
85
|
+
therefore shares the one `fleetmesh_owner` row, so the first store to
|
|
86
|
+
open claims the database for this node and a second node is refused.
|
|
87
|
+
"""
|
|
88
|
+
if not url:
|
|
89
|
+
return cls(directory, SqliteDialect(), owner=owner)
|
|
90
|
+
if url.startswith(POSTGRES_SCHEMES):
|
|
91
|
+
from .postgres import PostgresDialect # noqa: PLC0415 (optional)
|
|
92
|
+
return cls(directory, PostgresDialect(), url=url, owner=owner)
|
|
93
|
+
raise ValueError(
|
|
94
|
+
'unknown storage URL scheme: %r - expected one of %s, or no URL for the '
|
|
95
|
+
'SQLite default' % (url.split('://')[0], ', '.join(POSTGRES_SCHEMES)))
|
|
96
|
+
|
|
97
|
+
# ---- use --------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
def target(self, name: str) -> str:
|
|
100
|
+
"""The connection target for the store called `name`.
|
|
101
|
+
|
|
102
|
+
A file per store on SQLite. One database for all of them on a server,
|
|
103
|
+
which is what "one node per database" means.
|
|
104
|
+
"""
|
|
105
|
+
if self.url is not None:
|
|
106
|
+
return self.url
|
|
107
|
+
return os.path.join(self.directory, name + SQLITE_SUFFIX)
|
|
108
|
+
|
|
109
|
+
def open(self, store_cls: Callable[..., Any], name: str, **kw: Any) -> Any:
|
|
110
|
+
"""Construct a store of `store_cls` where `name` belongs.
|
|
111
|
+
|
|
112
|
+
`kw` is passed through untouched, so a store with its own arguments -
|
|
113
|
+
`ttl_seconds`, `readonly` - keeps them, and a caller naming `owner`
|
|
114
|
+
itself wins over the locator's.
|
|
115
|
+
"""
|
|
116
|
+
if self.owner is not None:
|
|
117
|
+
kw.setdefault('owner', self.owner)
|
|
118
|
+
return store_cls(self.target(name), dialect=self.dialect, **kw)
|
|
119
|
+
|
|
120
|
+
# ---- what an operator is shown ----------------------------------------
|
|
121
|
+
|
|
122
|
+
def describe(self) -> StorageBackend:
|
|
123
|
+
"""The backend, for `meshnode health`.
|
|
124
|
+
|
|
125
|
+
Which backend is live is otherwise invisible until something breaks,
|
|
126
|
+
and a node believed to be on PostgreSQL while running on SQLite is
|
|
127
|
+
exactly the failure this reports. The URL is reduced to host and
|
|
128
|
+
database: a connection string carries a password.
|
|
129
|
+
"""
|
|
130
|
+
out: StorageBackend = {'backend': self.dialect.name}
|
|
131
|
+
if self.url is None:
|
|
132
|
+
out['directory'] = self.directory
|
|
133
|
+
else:
|
|
134
|
+
out['server'] = redact(self.url)
|
|
135
|
+
if self.owner is not None:
|
|
136
|
+
out['owner'] = self.owner
|
|
137
|
+
return out
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def redact(url: str) -> str:
|
|
141
|
+
"""`postgresql://user:pw@host:5432/db` -> `host:5432/db`.
|
|
142
|
+
|
|
143
|
+
A password in a connection string must not reach a log, a health block or
|
|
144
|
+
a receipt, and the part an operator needs to recognise the server is the
|
|
145
|
+
part after the credentials.
|
|
146
|
+
"""
|
|
147
|
+
rest = url.split('://', 1)[1] if '://' in url else url
|
|
148
|
+
if '@' in rest:
|
|
149
|
+
rest = rest.rsplit('@', 1)[1]
|
|
150
|
+
return rest.split('?', 1)[0]
|