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/standing.py
ADDED
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
"""PeerStandingStore: SQLite-backed peer standing, window AIMD budget evaluation,
|
|
2
|
+
reporter weighting, and standing-gated NIP-77 Negentropy shard reconciliation.
|
|
3
|
+
|
|
4
|
+
Standardizes the integration of:
|
|
5
|
+
- `fleetmesh_core.accounting.standing.Ledger` (window-ordered streak tracking,
|
|
6
|
+
`tell` / `answer`, `report_record`, `on_revocation`)
|
|
7
|
+
- `fleetmesh_core.accounting.window` (`opening_limits`, `evaluate_window`,
|
|
8
|
+
`view`, `adjust`, `reporter_weight`, fixed-point millionths consensus)
|
|
9
|
+
- `fleetmesh_core.storage.sync` (`Grant`, `Session`, `run`)
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import contextlib
|
|
14
|
+
import json
|
|
15
|
+
import sqlite3
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
from typing import Any, Callable, Iterator, Mapping, Optional, Sequence, TypedDict
|
|
19
|
+
|
|
20
|
+
from fleetmesh_core.accounting import standing as standing_mod
|
|
21
|
+
from fleetmesh_core.accounting import window
|
|
22
|
+
from fleetmesh_core.storage import store, sync
|
|
23
|
+
from node.compartment.schema import Column, Table
|
|
24
|
+
|
|
25
|
+
from .projected import ProjectedNodeStore
|
|
26
|
+
from .sharding import HandoverAcceptReply, ShardCoverageManager
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
'PEER_STANDING_TABLE',
|
|
30
|
+
'PeerStandingRecord',
|
|
31
|
+
'PeerStandingStore',
|
|
32
|
+
'ReconcileShardsReply',
|
|
33
|
+
'ShardReconcileEntry',
|
|
34
|
+
'WeightedReportEntry',
|
|
35
|
+
'WindowEvalRecord',
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
VALID_TIERS: tuple[str, ...] = ('anchor', 'trusted', 'member', 'probation')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PeerStandingRecord(TypedDict):
|
|
42
|
+
peer_pk: str
|
|
43
|
+
peer_did: str
|
|
44
|
+
tier: str
|
|
45
|
+
limits: dict[str, int]
|
|
46
|
+
clean_windows: int
|
|
47
|
+
evaluated_windows: int
|
|
48
|
+
contradicted_reports: int
|
|
49
|
+
last_severity: str
|
|
50
|
+
last_note: str
|
|
51
|
+
zero_until: int
|
|
52
|
+
reporter_weight: float
|
|
53
|
+
can_sync: bool
|
|
54
|
+
updated_at: int
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class WeightedReportEntry(TypedDict):
|
|
58
|
+
severity: str
|
|
59
|
+
weight: float
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Optional keys sit in total=False bases: typing.NotRequired is Python 3.11+
|
|
63
|
+
# and this package supports 3.10.
|
|
64
|
+
class _WindowEvalOptional(TypedDict, total=False):
|
|
65
|
+
weighted_reports: list[WeightedReportEntry]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class WindowEvalRecord(PeerStandingRecord, _WindowEvalOptional):
|
|
69
|
+
window_severity: str
|
|
70
|
+
final_severity: str
|
|
71
|
+
per_limit: Mapping[str, Any]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ShardReconcileEntry(TypedDict):
|
|
75
|
+
pull_status: Optional[str]
|
|
76
|
+
push_status: Optional[str]
|
|
77
|
+
fingerprint_match: bool
|
|
78
|
+
findings: list[tuple[str, str]]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class _ReconcileShardsOptional(TypedDict, total=False):
|
|
82
|
+
standing: PeerStandingRecord
|
|
83
|
+
handovers: dict[str, HandoverAcceptReply]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ReconcileShardsReply(_ReconcileShardsOptional):
|
|
87
|
+
ok: bool
|
|
88
|
+
status: str
|
|
89
|
+
standing_before: PeerStandingRecord
|
|
90
|
+
standing_after: PeerStandingRecord | WindowEvalRecord
|
|
91
|
+
shards: dict[str, ShardReconcileEntry]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
PEER_STANDING_TABLE = Table(
|
|
95
|
+
'peer_standing',
|
|
96
|
+
(
|
|
97
|
+
Column('peer_pk', 'TEXT', primary_key=True),
|
|
98
|
+
Column('peer_did', 'TEXT', not_null=True),
|
|
99
|
+
Column('tier', 'TEXT', not_null=True),
|
|
100
|
+
Column('limits_json', 'TEXT', not_null=True),
|
|
101
|
+
Column('clean_windows', 'INTEGER', not_null=True),
|
|
102
|
+
Column('evaluated_windows', 'INTEGER', not_null=True),
|
|
103
|
+
Column('contradicted_reports', 'INTEGER', not_null=True),
|
|
104
|
+
Column('last_severity', 'TEXT', not_null=True),
|
|
105
|
+
Column('last_note', 'TEXT', not_null=True),
|
|
106
|
+
Column('zero_until', 'INTEGER', not_null=True),
|
|
107
|
+
Column('updated_at', 'INTEGER', not_null=True),
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class PeerStandingStore:
|
|
113
|
+
"""Persistent peer standing book backed by SQLite and hydrated into a
|
|
114
|
+
`fleetmesh_core.accounting.standing.Ledger`."""
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
conn: Optional[sqlite3.Connection] = None,
|
|
119
|
+
*,
|
|
120
|
+
lock: Optional[threading.RLock] = None,
|
|
121
|
+
clock: Optional[Callable[[], int]] = None,
|
|
122
|
+
tier_fallback_fn: Optional[Callable[[str], Optional[str]]] = None,
|
|
123
|
+
on_tier_change: Optional[Callable[[str, str], None]] = None,
|
|
124
|
+
) -> None:
|
|
125
|
+
self._conn = conn if conn is not None else sqlite3.connect(':memory:', check_same_thread=False)
|
|
126
|
+
self._lock = lock if lock is not None else threading.RLock()
|
|
127
|
+
self.clock: Callable[[], int] = clock or (lambda: int(time.time()))
|
|
128
|
+
self._tier_fallback_fn = tier_fallback_fn
|
|
129
|
+
self._on_tier_change = on_tier_change
|
|
130
|
+
self.ledger = standing_mod.Ledger()
|
|
131
|
+
self._epoch_seq: int = 0
|
|
132
|
+
self._tables_ready = False
|
|
133
|
+
self.ensure_tables()
|
|
134
|
+
|
|
135
|
+
def close(self) -> None:
|
|
136
|
+
"""Close the underlying SQLite connection."""
|
|
137
|
+
with self._lock:
|
|
138
|
+
self._conn.close()
|
|
139
|
+
|
|
140
|
+
@contextlib.contextmanager
|
|
141
|
+
def _tx(self) -> Iterator[sqlite3.Connection]:
|
|
142
|
+
with self._lock:
|
|
143
|
+
with self._conn:
|
|
144
|
+
yield self._conn
|
|
145
|
+
|
|
146
|
+
def ensure_tables(self) -> None:
|
|
147
|
+
if self._tables_ready:
|
|
148
|
+
return
|
|
149
|
+
with self._tx() as conn:
|
|
150
|
+
with contextlib.closing(conn.execute(PEER_STANDING_TABLE.to_sql())):
|
|
151
|
+
pass
|
|
152
|
+
for sec in PEER_STANDING_TABLE.secondary_sql():
|
|
153
|
+
with contextlib.closing(conn.execute(sec)):
|
|
154
|
+
pass
|
|
155
|
+
self._tables_ready = True
|
|
156
|
+
self._hydrate_ledger(conn)
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _build_window_history(clean_w: int, eval_w: int, last_sev: str = 'S0') -> list[tuple[int, str]]:
|
|
160
|
+
c_n = max(0, int(clean_w))
|
|
161
|
+
e_n = max(c_n, int(eval_w))
|
|
162
|
+
win_list: list[tuple[int, str]] = []
|
|
163
|
+
non_clean = e_n - c_n
|
|
164
|
+
non_clean_sev = str(last_sev) if str(last_sev) != 'S0' else 'S1'
|
|
165
|
+
for i in range(non_clean):
|
|
166
|
+
win_list.append((i + 1, non_clean_sev))
|
|
167
|
+
for i in range(c_n):
|
|
168
|
+
win_list.append((non_clean + i + 1, 'S0'))
|
|
169
|
+
return win_list
|
|
170
|
+
|
|
171
|
+
def _hydrate_ledger(self, conn: sqlite3.Connection) -> None:
|
|
172
|
+
with contextlib.closing(
|
|
173
|
+
conn.execute(
|
|
174
|
+
'SELECT peer_pk, tier, clean_windows, evaluated_windows, last_severity FROM peer_standing'
|
|
175
|
+
)
|
|
176
|
+
) as cur:
|
|
177
|
+
rows = cur.fetchall()
|
|
178
|
+
for pk, tier, clean_w, eval_w, last_sev in rows:
|
|
179
|
+
clean_pk = str(pk).lower()
|
|
180
|
+
self.ledger.tier[clean_pk] = str(tier)
|
|
181
|
+
c_n = int(clean_w or 0)
|
|
182
|
+
e_n = max(c_n, int(eval_w or 0))
|
|
183
|
+
self.ledger.windows[clean_pk] = self._build_window_history(c_n, e_n, str(last_sev or 'S0'))
|
|
184
|
+
self._epoch_seq = max(self._epoch_seq, e_n)
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def _row_to_record(row: Sequence[Any], now_ts: int) -> PeerStandingRecord:
|
|
188
|
+
clean_pk = str(row[0]).lower()
|
|
189
|
+
tier = str(row[2])
|
|
190
|
+
limits = json.loads(str(row[3]))
|
|
191
|
+
clean_win = int(row[4])
|
|
192
|
+
eval_win = int(row[5])
|
|
193
|
+
contra = int(row[6])
|
|
194
|
+
last_sev = str(row[7])
|
|
195
|
+
last_note = str(row[8])
|
|
196
|
+
zero_until = int(row[9])
|
|
197
|
+
updated_at = int(row[10])
|
|
198
|
+
|
|
199
|
+
rep_weight = (
|
|
200
|
+
window.reporter_weight(tier, clean_win, contra, eval_win)
|
|
201
|
+
if tier in VALID_TIERS
|
|
202
|
+
else 0.0
|
|
203
|
+
)
|
|
204
|
+
can_sync = (
|
|
205
|
+
tier != 'revoked'
|
|
206
|
+
and int(limits.get('events_in', 0)) > 0
|
|
207
|
+
and now_ts >= zero_until
|
|
208
|
+
)
|
|
209
|
+
return {
|
|
210
|
+
'peer_pk': clean_pk,
|
|
211
|
+
'peer_did': str(row[1]),
|
|
212
|
+
'tier': tier,
|
|
213
|
+
'limits': limits,
|
|
214
|
+
'clean_windows': clean_win,
|
|
215
|
+
'evaluated_windows': eval_win,
|
|
216
|
+
'contradicted_reports': contra,
|
|
217
|
+
'last_severity': last_sev,
|
|
218
|
+
'last_note': last_note,
|
|
219
|
+
'zero_until': zero_until,
|
|
220
|
+
'reporter_weight': round(rep_weight, 6),
|
|
221
|
+
'can_sync': can_sync,
|
|
222
|
+
'updated_at': updated_at,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
def get(
|
|
226
|
+
self,
|
|
227
|
+
peer_pk: str,
|
|
228
|
+
*,
|
|
229
|
+
default_tier: Optional[str] = None,
|
|
230
|
+
create_if_missing: bool = False,
|
|
231
|
+
) -> PeerStandingRecord:
|
|
232
|
+
"""Return (and optionally initialize when `create_if_missing=True`) the
|
|
233
|
+
standing record for `peer_pk`."""
|
|
234
|
+
self.ensure_tables()
|
|
235
|
+
clean_pk = (peer_pk or '').strip().lower()
|
|
236
|
+
if not store.is_hex64(clean_pk):
|
|
237
|
+
raise ValueError('peer_pk must be a 64-character lowercase hex pubkey')
|
|
238
|
+
now_ts = self.clock()
|
|
239
|
+
with self._lock:
|
|
240
|
+
with contextlib.closing(
|
|
241
|
+
self._conn.execute(
|
|
242
|
+
'SELECT peer_pk, peer_did, tier, limits_json, clean_windows, evaluated_windows, '
|
|
243
|
+
'contradicted_reports, last_severity, last_note, zero_until, updated_at '
|
|
244
|
+
'FROM peer_standing WHERE peer_pk = ?',
|
|
245
|
+
(clean_pk,),
|
|
246
|
+
)
|
|
247
|
+
) as cur:
|
|
248
|
+
row = cur.fetchone()
|
|
249
|
+
if row is None:
|
|
250
|
+
fallback = self._tier_fallback_fn(clean_pk) if self._tier_fallback_fn else None
|
|
251
|
+
tier = (
|
|
252
|
+
str(default_tier)
|
|
253
|
+
if default_tier in VALID_TIERS
|
|
254
|
+
else str(fallback)
|
|
255
|
+
if fallback in VALID_TIERS
|
|
256
|
+
else 'probation'
|
|
257
|
+
)
|
|
258
|
+
limits = window.opening_limits(tier)
|
|
259
|
+
rep_weight = (
|
|
260
|
+
window.reporter_weight(tier, 0, 0, 0)
|
|
261
|
+
if tier in VALID_TIERS
|
|
262
|
+
else 0.0
|
|
263
|
+
)
|
|
264
|
+
default_rec: PeerStandingRecord = {
|
|
265
|
+
'peer_pk': clean_pk,
|
|
266
|
+
'peer_did': f'did:nostr:{clean_pk}',
|
|
267
|
+
'tier': tier,
|
|
268
|
+
'limits': limits,
|
|
269
|
+
'clean_windows': 0,
|
|
270
|
+
'evaluated_windows': 0,
|
|
271
|
+
'contradicted_reports': 0,
|
|
272
|
+
'last_severity': 'S0',
|
|
273
|
+
'last_note': 'opening',
|
|
274
|
+
'zero_until': 0,
|
|
275
|
+
'reporter_weight': round(rep_weight, 6),
|
|
276
|
+
'can_sync': tier != 'revoked' and int(limits.get('events_in', 0)) > 0,
|
|
277
|
+
'updated_at': now_ts,
|
|
278
|
+
}
|
|
279
|
+
if not create_if_missing:
|
|
280
|
+
return default_rec
|
|
281
|
+
with self._conn:
|
|
282
|
+
with contextlib.closing(
|
|
283
|
+
self._conn.execute(
|
|
284
|
+
"""INSERT INTO peer_standing
|
|
285
|
+
(peer_pk, peer_did, tier, limits_json, clean_windows, evaluated_windows,
|
|
286
|
+
contradicted_reports, last_severity, last_note, zero_until, updated_at)
|
|
287
|
+
VALUES (?, ?, ?, ?, 0, 0, 0, 'S0', 'opening', 0, ?)""",
|
|
288
|
+
(clean_pk, f'did:nostr:{clean_pk}', tier, json.dumps(limits, sort_keys=True), now_ts),
|
|
289
|
+
)
|
|
290
|
+
):
|
|
291
|
+
pass
|
|
292
|
+
self.ledger.tier[clean_pk] = tier
|
|
293
|
+
self.ledger.windows.setdefault(clean_pk, [])
|
|
294
|
+
return default_rec
|
|
295
|
+
|
|
296
|
+
return self._row_to_record(row, now_ts)
|
|
297
|
+
|
|
298
|
+
def set_tier(
|
|
299
|
+
self,
|
|
300
|
+
peer_pk: str,
|
|
301
|
+
tier: str,
|
|
302
|
+
*,
|
|
303
|
+
clean_windows: Optional[int] = None,
|
|
304
|
+
evaluated_windows: Optional[int] = None,
|
|
305
|
+
now: Optional[int] = None,
|
|
306
|
+
) -> PeerStandingRecord:
|
|
307
|
+
"""Seed or update an operator-granted tier (`anchor`, `trusted`,
|
|
308
|
+
`member`, `probation`) for `peer_pk` and reset its opening limits."""
|
|
309
|
+
self.ensure_tables()
|
|
310
|
+
if tier not in VALID_TIERS:
|
|
311
|
+
raise ValueError(f'Invalid Fleetmesh tier: {tier!r}')
|
|
312
|
+
st_doc = self.get(peer_pk, default_tier=tier, create_if_missing=True)
|
|
313
|
+
clean_pk = st_doc['peer_pk']
|
|
314
|
+
limits = window.opening_limits(tier)
|
|
315
|
+
now_ts = int(now if now is not None else self.clock())
|
|
316
|
+
c_win = int(clean_windows if clean_windows is not None else st_doc['clean_windows'])
|
|
317
|
+
e_win = int(evaluated_windows if evaluated_windows is not None else max(c_win, st_doc['evaluated_windows']))
|
|
318
|
+
with self._tx() as conn:
|
|
319
|
+
with contextlib.closing(
|
|
320
|
+
conn.execute(
|
|
321
|
+
"""UPDATE peer_standing
|
|
322
|
+
SET tier = ?, limits_json = ?, clean_windows = ?, evaluated_windows = ?,
|
|
323
|
+
last_severity = 'S0', last_note = 'operator_seed', zero_until = 0, updated_at = ?
|
|
324
|
+
WHERE peer_pk = ?""",
|
|
325
|
+
(tier, json.dumps(limits, sort_keys=True), c_win, e_win, now_ts, clean_pk),
|
|
326
|
+
)
|
|
327
|
+
):
|
|
328
|
+
pass
|
|
329
|
+
self.ledger.tier[clean_pk] = tier
|
|
330
|
+
self.ledger.windows[clean_pk] = self._build_window_history(c_win, e_win, 'S0')
|
|
331
|
+
self._epoch_seq = max(self._epoch_seq, e_win)
|
|
332
|
+
if self._on_tier_change is not None:
|
|
333
|
+
self._on_tier_change(clean_pk, tier)
|
|
334
|
+
return self.get(clean_pk)
|
|
335
|
+
|
|
336
|
+
def evaluate_window(
|
|
337
|
+
self,
|
|
338
|
+
peer_pk: str,
|
|
339
|
+
*,
|
|
340
|
+
measured: Mapping[str, int | float],
|
|
341
|
+
claimed: Optional[Mapping[str, int | float]] = None,
|
|
342
|
+
reports: Optional[Sequence[Mapping[str, Any]]] = None,
|
|
343
|
+
own_severity: str = 'S0',
|
|
344
|
+
now: Optional[int] = None,
|
|
345
|
+
) -> WindowEvalRecord:
|
|
346
|
+
"""Evaluate a closed window for `peer_pk` via `window.evaluate_window`,
|
|
347
|
+
`window.view`, and `window.adjust`, updating both SQLite and
|
|
348
|
+
`self.ledger`."""
|
|
349
|
+
self.ensure_tables()
|
|
350
|
+
now_ts = int(now if now is not None else self.clock())
|
|
351
|
+
st_doc = self.get(peer_pk, create_if_missing=True)
|
|
352
|
+
clean_pk = st_doc['peer_pk']
|
|
353
|
+
cur_tier = st_doc['tier']
|
|
354
|
+
cur_limits = dict(st_doc['limits'])
|
|
355
|
+
|
|
356
|
+
if cur_tier == 'revoked':
|
|
357
|
+
return {
|
|
358
|
+
**st_doc,
|
|
359
|
+
'window_severity': 'S4',
|
|
360
|
+
'final_severity': 'S4',
|
|
361
|
+
'per_limit': {},
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
actual_claimed = claimed if claimed is not None else measured
|
|
365
|
+
win_eval = window.evaluate_window(cur_limits, measured, actual_claimed)
|
|
366
|
+
combined_own = window.worse(str(win_eval['severity']), own_severity)
|
|
367
|
+
|
|
368
|
+
weighted_reports: list[WeightedReportEntry] = []
|
|
369
|
+
for r in (reports or []):
|
|
370
|
+
sev = str(r.get('severity') or 'S0')
|
|
371
|
+
if 'weight' in r and isinstance(r['weight'], (int, float)):
|
|
372
|
+
w = float(r['weight'])
|
|
373
|
+
elif r.get('reporter_pk'):
|
|
374
|
+
rep_st = self.get(str(r['reporter_pk']))
|
|
375
|
+
w = float(rep_st['reporter_weight'])
|
|
376
|
+
else:
|
|
377
|
+
w = 0.0
|
|
378
|
+
weighted_reports.append({'severity': sev, 'weight': w})
|
|
379
|
+
|
|
380
|
+
final_sev = window.view(combined_own, weighted_reports)
|
|
381
|
+
new_limits, new_tier, note = window.adjust(
|
|
382
|
+
cur_limits,
|
|
383
|
+
cur_tier,
|
|
384
|
+
final_sev,
|
|
385
|
+
used=measured,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
with self._tx() as conn:
|
|
389
|
+
self._epoch_seq += 1
|
|
390
|
+
if new_tier == 'revoked':
|
|
391
|
+
self.ledger.on_revocation(clean_pk)
|
|
392
|
+
else:
|
|
393
|
+
self.ledger.record(clean_pk, self._epoch_seq, final_sev, new_tier)
|
|
394
|
+
if final_sev in standing_mod.ADVERSE:
|
|
395
|
+
self.ledger.reported_against(clean_pk)
|
|
396
|
+
|
|
397
|
+
clean_win = st_doc['clean_windows']
|
|
398
|
+
eval_win = st_doc['evaluated_windows'] + 1
|
|
399
|
+
zero_until = st_doc['zero_until']
|
|
400
|
+
if final_sev == 'S0':
|
|
401
|
+
clean_win += 1
|
|
402
|
+
elif final_sev in ('S2', 'S3', 'S4'):
|
|
403
|
+
clean_win = 0
|
|
404
|
+
if final_sev == 'S3':
|
|
405
|
+
s3_hours = int(window.C['grant']['s3_zero_hours'])
|
|
406
|
+
zero_until = now_ts + s3_hours * 3600
|
|
407
|
+
|
|
408
|
+
with contextlib.closing(
|
|
409
|
+
conn.execute(
|
|
410
|
+
"""UPDATE peer_standing
|
|
411
|
+
SET tier = ?, limits_json = ?, clean_windows = ?, evaluated_windows = ?,
|
|
412
|
+
last_severity = ?, last_note = ?, zero_until = ?, updated_at = ?
|
|
413
|
+
WHERE peer_pk = ?""",
|
|
414
|
+
(
|
|
415
|
+
new_tier,
|
|
416
|
+
json.dumps(new_limits, sort_keys=True),
|
|
417
|
+
clean_win,
|
|
418
|
+
eval_win,
|
|
419
|
+
final_sev,
|
|
420
|
+
note,
|
|
421
|
+
zero_until,
|
|
422
|
+
now_ts,
|
|
423
|
+
clean_pk,
|
|
424
|
+
),
|
|
425
|
+
)
|
|
426
|
+
):
|
|
427
|
+
pass
|
|
428
|
+
if self._on_tier_change is not None and new_tier in (*VALID_TIERS, 'revoked'):
|
|
429
|
+
self._on_tier_change(clean_pk, new_tier)
|
|
430
|
+
|
|
431
|
+
updated = self.get(clean_pk)
|
|
432
|
+
return {
|
|
433
|
+
**updated,
|
|
434
|
+
'window_severity': str(win_eval['severity']),
|
|
435
|
+
'final_severity': final_sev,
|
|
436
|
+
'per_limit': dict(win_eval['per_limit']),
|
|
437
|
+
'weighted_reports': weighted_reports,
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
def list_all(self) -> list[PeerStandingRecord]:
|
|
441
|
+
"""Return all tracked peer standing records ordered by `updated_at DESC`."""
|
|
442
|
+
self.ensure_tables()
|
|
443
|
+
now_ts = self.clock()
|
|
444
|
+
with self._lock:
|
|
445
|
+
with contextlib.closing(
|
|
446
|
+
self._conn.execute(
|
|
447
|
+
'SELECT peer_pk, peer_did, tier, limits_json, clean_windows, evaluated_windows, '
|
|
448
|
+
'contradicted_reports, last_severity, last_note, zero_until, updated_at '
|
|
449
|
+
'FROM peer_standing ORDER BY updated_at DESC'
|
|
450
|
+
)
|
|
451
|
+
) as cur:
|
|
452
|
+
rows = cur.fetchall()
|
|
453
|
+
return [self._row_to_record(r, now_ts) for r in rows]
|
|
454
|
+
|
|
455
|
+
def reconcile_shards(
|
|
456
|
+
self,
|
|
457
|
+
local_store: ProjectedNodeStore,
|
|
458
|
+
peer_store: ProjectedNodeStore,
|
|
459
|
+
peer_pk: str,
|
|
460
|
+
*,
|
|
461
|
+
shard_filters: Mapping[str, Mapping[str, Any]],
|
|
462
|
+
local_sharding: Optional[ShardCoverageManager] = None,
|
|
463
|
+
peer_sharding: Optional[ShardCoverageManager] = None,
|
|
464
|
+
issue_handovers: bool = True,
|
|
465
|
+
now: Optional[int] = None,
|
|
466
|
+
) -> ReconcileShardsReply:
|
|
467
|
+
"""Run standing-gated bidirectional NIP-77 Negentropy (`sync/1.0`)
|
|
468
|
+
reconciliation over `shard_filters`, metering session counters through
|
|
469
|
+
`evaluate_window` and optionally issuing/accepting `Kind 30803`
|
|
470
|
+
handover chains."""
|
|
471
|
+
clean_pk = (peer_pk or '').strip().lower()
|
|
472
|
+
st_before = self.get(clean_pk)
|
|
473
|
+
if not st_before['can_sync']:
|
|
474
|
+
return {
|
|
475
|
+
'ok': False,
|
|
476
|
+
'status': 'refused_by_standing',
|
|
477
|
+
'standing': st_before,
|
|
478
|
+
'standing_before': st_before,
|
|
479
|
+
'standing_after': st_before,
|
|
480
|
+
'shards': {},
|
|
481
|
+
'handovers': {},
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
now_ts = int(now if now is not None else self.clock()) + 120
|
|
485
|
+
agg_counters: dict[str, int] = dict.fromkeys(sync.COUNTERS, 0)
|
|
486
|
+
worst_finding = 'S0'
|
|
487
|
+
shard_reports: dict[str, ShardReconcileEntry] = {}
|
|
488
|
+
handovers_completed: dict[str, HandoverAcceptReply] = {}
|
|
489
|
+
|
|
490
|
+
for shard_id, raw_filt in shard_filters.items():
|
|
491
|
+
filt = dict(raw_filt)
|
|
492
|
+
grant = sync.Grant(
|
|
493
|
+
f'grant-{shard_id}-{clean_pk[:8]}',
|
|
494
|
+
st_before['tier'],
|
|
495
|
+
st_before['limits'],
|
|
496
|
+
scope={'pull': filt, 'push': filt},
|
|
497
|
+
)
|
|
498
|
+
sess_init = sync.Session(
|
|
499
|
+
local_store, grant, shard_id, filt, since=0, until=now_ts, now=now_ts, is_initiator=True
|
|
500
|
+
)
|
|
501
|
+
sess_resp = sync.Session(
|
|
502
|
+
peer_store, grant, shard_id, filt, since=0, until=now_ts, now=now_ts, is_initiator=False
|
|
503
|
+
)
|
|
504
|
+
rep_pull = sync.run(sess_init, sess_resp)
|
|
505
|
+
|
|
506
|
+
sess_push_init = sync.Session(
|
|
507
|
+
peer_store, grant, shard_id, filt, since=0, until=now_ts, now=now_ts, is_initiator=True
|
|
508
|
+
)
|
|
509
|
+
sess_push_resp = sync.Session(
|
|
510
|
+
local_store, grant, shard_id, filt, since=0, until=now_ts, now=now_ts, is_initiator=False
|
|
511
|
+
)
|
|
512
|
+
rep_push = sync.run(sess_push_init, sess_push_resp)
|
|
513
|
+
|
|
514
|
+
for k in sync.COUNTERS:
|
|
515
|
+
agg_counters[k] = max(agg_counters[k], int(sess_init.counters.get(k, 0)))
|
|
516
|
+
for sev, _why in rep_pull['findings'] + rep_push['findings']:
|
|
517
|
+
if sev in window.SEV_ORDER:
|
|
518
|
+
worst_finding = window.worse(worst_finding, sev)
|
|
519
|
+
|
|
520
|
+
fp_match = (
|
|
521
|
+
local_store.fingerprint_range(filt, 0, now_ts, now_ts)
|
|
522
|
+
== peer_store.fingerprint_range(filt, 0, now_ts, now_ts)
|
|
523
|
+
)
|
|
524
|
+
shard_reports[shard_id] = {
|
|
525
|
+
'pull_status': rep_pull['status'],
|
|
526
|
+
'push_status': rep_push['status'],
|
|
527
|
+
'fingerprint_match': fp_match,
|
|
528
|
+
'findings': rep_pull['findings'] + rep_push['findings'],
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (
|
|
532
|
+
issue_handovers
|
|
533
|
+
and local_sharding is not None
|
|
534
|
+
and peer_sharding is not None
|
|
535
|
+
and fp_match
|
|
536
|
+
and rep_pull['status'] in ('complete', 'in_sync')
|
|
537
|
+
and rep_push['status'] in ('complete', 'in_sync')
|
|
538
|
+
):
|
|
539
|
+
ho_issued = peer_sharding.issue_handover(
|
|
540
|
+
shard_id, local_sharding.node_pk, since=0, until=now_ts, now=now_ts
|
|
541
|
+
)
|
|
542
|
+
ho_accepted = local_sharding.accept_handover(
|
|
543
|
+
shard_id, ho_issued['chain'], peer_sharding.node_pk, now=now_ts
|
|
544
|
+
)
|
|
545
|
+
handovers_completed[shard_id] = ho_accepted
|
|
546
|
+
|
|
547
|
+
st_after = self.evaluate_window(
|
|
548
|
+
clean_pk,
|
|
549
|
+
measured=agg_counters,
|
|
550
|
+
claimed=agg_counters,
|
|
551
|
+
own_severity=worst_finding,
|
|
552
|
+
now=now_ts,
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
all_ok = all(
|
|
556
|
+
r['pull_status'] in ('complete', 'in_sync')
|
|
557
|
+
and r['push_status'] in ('complete', 'in_sync')
|
|
558
|
+
and r['fingerprint_match']
|
|
559
|
+
for r in shard_reports.values()
|
|
560
|
+
)
|
|
561
|
+
return {
|
|
562
|
+
'ok': all_ok and st_after['final_severity'] == 'S0',
|
|
563
|
+
'status': 'complete' if all_ok else 'incomplete',
|
|
564
|
+
'standing_before': st_before,
|
|
565
|
+
'standing_after': st_after,
|
|
566
|
+
'shards': shard_reports,
|
|
567
|
+
'handovers': handovers_completed,
|
|
568
|
+
}
|
node/testnet/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Local testnet tooling: a multi-node cluster on one machine and the in-process
|
|
2
|
+
FIPS daemon that stands in for fipsd. Development and operator rehearsal only -
|
|
3
|
+
nothing in the node runtime imports this package (test_imports holds that line);
|
|
4
|
+
the `meshnode cluster` verb is its one caller outside the tests.
|
|
5
|
+
"""
|
|
6
|
+
from .cluster import ChaosSimulator, LocalCluster, TOPOLOGIES, generate_topology_edges
|
|
7
|
+
from .fips_daemon import ClusterFipsDaemon
|
|
8
|
+
|
|
9
|
+
__all__ = ['ChaosSimulator', 'LocalCluster', 'TOPOLOGIES', 'generate_topology_edges', 'ClusterFipsDaemon']
|