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,285 @@
|
|
|
1
|
+
"""Publishing a finding as a kind 30802 report, the relay-score projection
|
|
2
|
+
that rides beside it, withdrawal, and the tick that drives all of it.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any, Mapping, Optional
|
|
7
|
+
from fleetmesh_core.accounting import window # noqa: E402
|
|
8
|
+
from fleetmesh_core.crypto import giftwrap # noqa: E402
|
|
9
|
+
from fleetmesh_core.peering import complaint, projection, reports # noqa: E402
|
|
10
|
+
from fleetmesh_core.storage import store # noqa: E402
|
|
11
|
+
from fleetmesh_core.wire.interfaces import NostrEvent # noqa: E402
|
|
12
|
+
from node.storage import grants as grants_mod # noqa: E402
|
|
13
|
+
from node.protocols.nostr import client as relay # noqa: E402
|
|
14
|
+
from .constants import FINDING_WINDOWS, PUBLISH_TRIES, REPORT_TTL
|
|
15
|
+
from .finding import Finding
|
|
16
|
+
from .shape import ReporterShape
|
|
17
|
+
|
|
18
|
+
from fleetmesh_core.identity.did import did_nostr as _did
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PublishMixin:
|
|
22
|
+
def publish(self: ReporterShape, p: Finding, now: Optional[int] = None) -> Optional[NostrEvent]:
|
|
23
|
+
now_val = now if now is not None else self.clock()
|
|
24
|
+
worst = p.worst
|
|
25
|
+
observed = (worst[0], worst[1]['measured'], worst[1]['limit']) if worst else None
|
|
26
|
+
claimed = (worst[0], worst[1]['claimed']) if worst and worst[1].get('claimed') is not None else None
|
|
27
|
+
notice = (p.thid, p.notice_at) if p.thid else None
|
|
28
|
+
if not p.evidence:
|
|
29
|
+
# Nothing the subject signed exists for this window. Active silence is
|
|
30
|
+
# the case that can never produce one: the subject said nothing, so
|
|
31
|
+
# there is nothing of its own to quote. Do not invent a citation.
|
|
32
|
+
reason = ['the subject signed nothing this window that could be cited']
|
|
33
|
+
with self._lock:
|
|
34
|
+
p.unpublishable = reason
|
|
35
|
+
p.tries = PUBLISH_TRIES # nothing about a closed window will change this
|
|
36
|
+
self._note_unpublishable(p, reason)
|
|
37
|
+
self._persist(p)
|
|
38
|
+
return None
|
|
39
|
+
# The grant this report cites travels with it. It is addressable under
|
|
40
|
+
# a blinded tag, so the one that governed a past window has already
|
|
41
|
+
# been replaced at that coordinate and no relay serves it; a report
|
|
42
|
+
# that did not carry its own became inadmissible to every reader
|
|
43
|
+
# within about an hour of being published.
|
|
44
|
+
envelope = self.grants.envelope_event(p.peer, grants_mod.ISSUED, p.grant)
|
|
45
|
+
ev = reports.build(self.ks.node_pk, p.peer, p.severity, p.grant, (p.epoch, p.epoch),
|
|
46
|
+
p.evidence, now_val, REPORT_TTL, observed=observed, claimed=claimed, notice=notice,
|
|
47
|
+
content='; '.join(str(w) for w in p.why)[:400] or p.source,
|
|
48
|
+
grant_envelope=envelope)
|
|
49
|
+
at = self.forward_clock('report:' + p.peer, now_val) if self.forward_clock else now_val
|
|
50
|
+
signed = store.make_signed_event(self.ks.node_sk, at, 30802, ev['tags'], ev['content'])
|
|
51
|
+
# Do not publish what a reader will discard. A report whose evidence
|
|
52
|
+
# does not resolve to something the subject signed is inadmissible, and
|
|
53
|
+
# publishing it is pure cost: readers count it as unsupported, which
|
|
54
|
+
# raises this node's own false_report_rate and lowers what its word is
|
|
55
|
+
# worth. The finding stays local and the grant stays zeroed.
|
|
56
|
+
okay, reasons = reports.admissible(signed, self.ks.node_pk, self._resolver(), giftwrap.verify_event)
|
|
57
|
+
if not okay:
|
|
58
|
+
with self._lock:
|
|
59
|
+
p.unpublishable = reasons
|
|
60
|
+
p.tries = p.tries + 1
|
|
61
|
+
self._note_unpublishable(p, reasons)
|
|
62
|
+
self._persist(p)
|
|
63
|
+
return None
|
|
64
|
+
relay.publish_all(self.relays, signed, self.timeout)
|
|
65
|
+
self.ledger.reported_against(p.peer)
|
|
66
|
+
with self._lock:
|
|
67
|
+
p.report = signed
|
|
68
|
+
self.published.append(signed)
|
|
69
|
+
self._persist(p)
|
|
70
|
+
self._project(signed, now_val)
|
|
71
|
+
return signed
|
|
72
|
+
|
|
73
|
+
def _project(self: ReporterShape, report: NostrEvent, now: int) -> list[NostrEvent]:
|
|
74
|
+
"""The relay score a client can read, derived from the report just published.
|
|
75
|
+
|
|
76
|
+
A NIP-85 kind 30385 per ws or wss endpoint the subject's descriptor
|
|
77
|
+
declares; see fleetmesh_core/projection.py for the shape and the reason it is
|
|
78
|
+
adverse only. A subject that declares no relay endpoint gets none:
|
|
79
|
+
there is nothing a client could connect to. Addressable under d = the
|
|
80
|
+
relay URL, so it is stamped forward like the report it follows.
|
|
81
|
+
"""
|
|
82
|
+
if not self.project:
|
|
83
|
+
return []
|
|
84
|
+
subject = [t[1] for t in report['tags'] if t[0] == 'd'][0]
|
|
85
|
+
desc = self.transport.peers.descriptor(subject)
|
|
86
|
+
endpoints = projection.relay_endpoints(desc) if desc is not None else []
|
|
87
|
+
out: list[NostrEvent] = []
|
|
88
|
+
for ev in projection.project(report, endpoints, now):
|
|
89
|
+
d = [t[1] for t in ev['tags'] if t[0] == 'd'][0]
|
|
90
|
+
at = self.forward_clock('projection:' + d, now) if self.forward_clock else now
|
|
91
|
+
signed = store.make_signed_event(self.ks.node_sk, at, projection.KIND, ev['tags'], ev['content'])
|
|
92
|
+
relay.publish_all(self.relays, signed, self.timeout)
|
|
93
|
+
out.append(signed)
|
|
94
|
+
with self._lock:
|
|
95
|
+
for signed_ev in out:
|
|
96
|
+
self.projected.append(signed_ev)
|
|
97
|
+
try:
|
|
98
|
+
lbl_ev = projection.label(report, now)
|
|
99
|
+
at_lbl = self.forward_clock('label:' + subject, now) if self.forward_clock else now
|
|
100
|
+
signed_lbl = store.make_signed_event(self.ks.node_sk, at_lbl, projection.LABEL_KIND, lbl_ev['tags'], lbl_ev['content'])
|
|
101
|
+
relay.publish_all(self.relays, signed_lbl, self.timeout)
|
|
102
|
+
with self._lock:
|
|
103
|
+
self.labeled.append(signed_lbl)
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
return out
|
|
107
|
+
|
|
108
|
+
def _note_unpublishable(self: ReporterShape, p: Finding, reasons: list[str]) -> None:
|
|
109
|
+
with self._lock:
|
|
110
|
+
self.unpublishable.append((p.peer, p.epoch, p.severity, reasons))
|
|
111
|
+
|
|
112
|
+
def _withdraw(self: ReporterShape, p: Finding, reason: str) -> None:
|
|
113
|
+
body = complaint.withdraw(self.clock(), reason)
|
|
114
|
+
self.wire_client.send(p.peer, complaint.message('withdraw', body, self.ks.did, _did(p.peer), thid=p.thid), stored=True)
|
|
115
|
+
p.withdrawn = True
|
|
116
|
+
self._persist(p)
|
|
117
|
+
|
|
118
|
+
def retract(self: ReporterShape, p: Finding, reason: str) -> Optional[str]:
|
|
119
|
+
"""Withdraw a finding AND take back what it did. Returns the tier restored.
|
|
120
|
+
|
|
121
|
+
A withdrawal used to say the accusation was wrong and leave its whole
|
|
122
|
+
effect standing: the S2 stayed in the ledger, the demotion it earned
|
|
123
|
+
stayed in force, and the streak stayed broken. Clearing 297 of them off
|
|
124
|
+
a live mesh still left four peerings at probation that a person had to
|
|
125
|
+
seed back by hand, which is the whole of the argument that the command
|
|
126
|
+
was only half a command.
|
|
127
|
+
|
|
128
|
+
Not every withdrawal may do this. The ladder is one-way by design, so
|
|
129
|
+
the line is WHY the finding is going, not that it is:
|
|
130
|
+
|
|
131
|
+
- the verdict was wrong - a person says so, or a receipt arrived late
|
|
132
|
+
and the window was repriced - and the tier it produced goes with it.
|
|
133
|
+
Nothing is earned; the node returns to where it was before it made
|
|
134
|
+
a mistake it now admits.
|
|
135
|
+
- the peer misbehaved and then fixed it. That is the ladder working,
|
|
136
|
+
and giving the tier back for it would be a two-way ladder bought
|
|
137
|
+
with a fault, which antifragility.html rules out by name.
|
|
138
|
+
|
|
139
|
+
It is refused outright when this node still stands behind something
|
|
140
|
+
adverse and newer, because then the tier in force is not this finding's
|
|
141
|
+
doing and is not this finding's to lift.
|
|
142
|
+
"""
|
|
143
|
+
self._withdraw(p, reason)
|
|
144
|
+
return self._restore(p)
|
|
145
|
+
|
|
146
|
+
def _restore(self: ReporterShape, p: Finding) -> Optional[str]:
|
|
147
|
+
"""Take the window back out of the history, and the tier back off it."""
|
|
148
|
+
peer, epoch = p.peer, int(p.epoch)
|
|
149
|
+
was = self.ledger.forget_window(peer, epoch)
|
|
150
|
+
if self.accounts is not None:
|
|
151
|
+
self.accounts.put_evaluated(peer, epoch, None)
|
|
152
|
+
if was is None or was == 'S0':
|
|
153
|
+
return None
|
|
154
|
+
if any(e > epoch and sev != 'S0' for e, sev in self.ledger.windows.get(peer) or []):
|
|
155
|
+
return None
|
|
156
|
+
governing = self.grants.governing(peer, grants_mod.ISSUED, epoch)
|
|
157
|
+
before = governing['payload'].get('tier') if governing else None
|
|
158
|
+
held = self.ledger.tier.get(peer)
|
|
159
|
+
if not before or not window.outranks(before, held or 'probation'):
|
|
160
|
+
return None # never a demotion, and never past where it stood
|
|
161
|
+
self.ledger.tier[peer] = before
|
|
162
|
+
if self.regrant is not None:
|
|
163
|
+
self.regrant(peer, before, self.clock())
|
|
164
|
+
self.log.append(('restored', peer, epoch, held, before))
|
|
165
|
+
return before
|
|
166
|
+
|
|
167
|
+
def withdraw(self: ReporterShape, peer: str, epoch: int, why: Optional[str] = None) -> Mapping[str, Any] | list[Mapping[str, Any]]:
|
|
168
|
+
"""Withdraw one finding because a person said to.
|
|
169
|
+
|
|
170
|
+
Every other path to a withdrawal is the node deciding: a remedy landed
|
|
171
|
+
and the next window was clean, or the window was repriced. This one is
|
|
172
|
+
the operator, and it exists because the node cannot always find out on
|
|
173
|
+
its own that a finding was wrong. The two S2s that made it necessary
|
|
174
|
+
were about receipts that arrived after their window closed, and by the
|
|
175
|
+
time that was understood the windows had been priced under a build that
|
|
176
|
+
could not read them again.
|
|
177
|
+
|
|
178
|
+
A published report is not deleted. It carries its own expiration and
|
|
179
|
+
stands until it lapses; what a withdrawal does is tell the subject, on
|
|
180
|
+
the thread the notice opened, so the peer holding the accusation hears
|
|
181
|
+
that it is retracted from the node that made it.
|
|
182
|
+
"""
|
|
183
|
+
with self._lock:
|
|
184
|
+
hit = [q for q in self.pending
|
|
185
|
+
if q.peer == peer and q.epoch == int(epoch) and not q.withdrawn]
|
|
186
|
+
if not hit:
|
|
187
|
+
raise KeyError('no finding held about %s for window %s that is not already withdrawn'
|
|
188
|
+
% (peer[:16], epoch))
|
|
189
|
+
out: list[Mapping[str, Any]] = []
|
|
190
|
+
for q in hit:
|
|
191
|
+
published = q.report
|
|
192
|
+
reason = why or 'withdrawn by the operator'
|
|
193
|
+
restored = self.retract(q, reason)
|
|
194
|
+
out.append({'peer': q.peer, 'window': q.epoch, 'was_published': published is not None,
|
|
195
|
+
'report': published['id'] if published is not None else None,
|
|
196
|
+
'restored': restored, 'why': reason})
|
|
197
|
+
return out[0] if len(out) == 1 else out
|
|
198
|
+
|
|
199
|
+
def tick(self: ReporterShape, now: Optional[int] = None) -> list[tuple[Any, ...]]:
|
|
200
|
+
"""Publish what may be published; withdraw what a fix and a clean window earned.
|
|
201
|
+
Pending entries that are done - published or withdrawn - are dropped once
|
|
202
|
+
the report they made has itself expired; the list is not a history."""
|
|
203
|
+
with self._lock:
|
|
204
|
+
at = now if now is not None else self.clock()
|
|
205
|
+
horizon = at - REPORT_TTL
|
|
206
|
+
# A finding that became a report, or was withdrawn, waits for the
|
|
207
|
+
# report it made to expire. One that became NEITHER used to wait
|
|
208
|
+
# forever: the condition required a report or a withdrawal, and an
|
|
209
|
+
# unpublishable finding - active silence, where the subject signed
|
|
210
|
+
# nothing that could be cited - has neither and never will. So the
|
|
211
|
+
# list was not a history, as the line above says, except for
|
|
212
|
+
# exactly the entries that can never leave it. Four nodes carried
|
|
213
|
+
# 253 between them, all of them from one outage, none of them ever
|
|
214
|
+
# to be published, and every future silent window would have added
|
|
215
|
+
# another.
|
|
216
|
+
#
|
|
217
|
+
# Dropping one loses nothing that was doing any work. The remedy it
|
|
218
|
+
# earned was applied to the grant when the window closed, and
|
|
219
|
+
# standing/1.0 carries the severity in the ledger; the finding is
|
|
220
|
+
# only the paperwork for a report that cannot be written. It goes
|
|
221
|
+
# when the accounting under it goes, which is the point past which
|
|
222
|
+
# this node could not substantiate it to anyone, itself included.
|
|
223
|
+
done = []
|
|
224
|
+
for p in self.pending:
|
|
225
|
+
if p.done:
|
|
226
|
+
if p.opened_at < horizon:
|
|
227
|
+
done.append(p)
|
|
228
|
+
elif p.opened_at < at - FINDING_WINDOWS * (p.window_seconds or 3600):
|
|
229
|
+
done.append(p)
|
|
230
|
+
self.pending = [p for p in self.pending if p not in done] # not a store: the queue accounts keeps on disk, for persist=DURABLE
|
|
231
|
+
for p in done:
|
|
232
|
+
if self.accounts is not None:
|
|
233
|
+
self.accounts.drop_finding(p.fid)
|
|
234
|
+
now_val = now if now is not None else self.clock()
|
|
235
|
+
did: list[tuple[Any, ...]] = []
|
|
236
|
+
with self._lock:
|
|
237
|
+
# A finding that could not be published is retried, because a
|
|
238
|
+
# grant that did not resolve may be a relay that was down. One
|
|
239
|
+
# that has failed PUBLISH_TRIES times is not: it stays on the
|
|
240
|
+
# record, unpublished, and stops being attempted. Findings
|
|
241
|
+
# survive restarts now, so a permanent refusal that kept being
|
|
242
|
+
# retried logged the same line every half minute for as long as
|
|
243
|
+
# the node ran, and the list of them grew without bound.
|
|
244
|
+
todo = [p for p in self.pending if not p.done
|
|
245
|
+
and p.tries < PUBLISH_TRIES]
|
|
246
|
+
for p in todo:
|
|
247
|
+
# `_notify` puts a complaint on the wire, so a subject whose
|
|
248
|
+
# descriptor no longer resolves raises here - and used to take
|
|
249
|
+
# every finding behind it in `todo` with it. Same fault as the
|
|
250
|
+
# budget's peer loop and the scheduler's before that: one
|
|
251
|
+
# unreachable peer is not a reason to stop working for the rest.
|
|
252
|
+
try:
|
|
253
|
+
did += self._tick_finding(p, now_val)
|
|
254
|
+
except Exception as e:
|
|
255
|
+
self.refusals.refuse('finding', 'the subject raised', subject=p.peer,
|
|
256
|
+
detail={'epoch': p.epoch,
|
|
257
|
+
'error': '%s: %s' % (type(e).__name__, e)}, at=now_val)
|
|
258
|
+
did.append(('finding failed', p.peer, p.epoch, '%s: %s' % (type(e).__name__, e)))
|
|
259
|
+
return did
|
|
260
|
+
|
|
261
|
+
def _tick_finding(self: ReporterShape, p: Finding, now_val: int) -> list[tuple[Any, ...]]:
|
|
262
|
+
"""One finding's share of the tick: notice it, withdraw it, or publish it."""
|
|
263
|
+
if p.notice_at is None and p.severity in complaint.C['notice_required_for']:
|
|
264
|
+
# Opened and never noticed: the node stopped between the two.
|
|
265
|
+
# The notice is what the rule requires, so send it now and
|
|
266
|
+
# let the window run from here.
|
|
267
|
+
self._notify(p)
|
|
268
|
+
return [('noticed', p.peer, p.epoch)]
|
|
269
|
+
if not complaint.may_publish(p.severity, now_val, p.notice_at, p.window_seconds, reachable=True):
|
|
270
|
+
return []
|
|
271
|
+
remedy = p.remedy or {}
|
|
272
|
+
# fleetmesh_core.peering.complaint.resolve is the reference's reading of a remedy: every
|
|
273
|
+
# remedy_action but `contested` makes the finding withdraw-eligible.
|
|
274
|
+
# A local list of fix names restated the same rule and would drift
|
|
275
|
+
# from constants.remedy_actions on the next addition.
|
|
276
|
+
if remedy and complaint.resolve(remedy) == 'withdraw-eligible' and self.ledger.clean_windows(p.peer) >= 1:
|
|
277
|
+
self._withdraw(p, 'remedied: %s, and the next window was clean' % remedy['action'])
|
|
278
|
+
return [('withdrawn', p.peer, p.epoch)]
|
|
279
|
+
ev = self.publish(p, now_val)
|
|
280
|
+
if ev is not None:
|
|
281
|
+
return [('published', p.peer, p.epoch, ev['id'])]
|
|
282
|
+
if p.tries >= PUBLISH_TRIES:
|
|
283
|
+
return [('unpublishable', p.peer, p.epoch, p.severity,
|
|
284
|
+
'; '.join(p.unpublishable or [])[:120])]
|
|
285
|
+
return []
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Reading reports about a node, pricing a reporter's word, standing/1.0
|
|
2
|
+
asks, and inbound dispatch for complaint/1.0 and standing/1.0 messages.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import secrets
|
|
7
|
+
from typing import Any, Callable, Mapping, Optional
|
|
8
|
+
from fleetmesh_core.accounting import standing, window # noqa: E402
|
|
9
|
+
from fleetmesh_core.crypto import giftwrap # noqa: E402
|
|
10
|
+
from fleetmesh_core.peering import complaint, reports # noqa: E402
|
|
11
|
+
from fleetmesh_core.wire.interfaces import NostrEvent # noqa: E402
|
|
12
|
+
from fleetmesh_core.wire import validate # noqa: E402
|
|
13
|
+
from node.protocols.nostr import client as relay # noqa: E402
|
|
14
|
+
from .shape import ReporterShape
|
|
15
|
+
|
|
16
|
+
from fleetmesh_core.identity.did import did_nostr as _did
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ReadingMixin:
|
|
20
|
+
# ---- the complaint, subject side ------------------------------------------
|
|
21
|
+
def answer_notice(self: ReporterShape, peer: str, msg: Mapping[str, Any], budget: Optional[Any] = None) -> Mapping[str, Any]:
|
|
22
|
+
"""What this node says when accused. If our own meter agrees with their
|
|
23
|
+
observation we were over and say we will throttle; if it disagrees we
|
|
24
|
+
contest with our own count. Both are honest; neither erases the window."""
|
|
25
|
+
body = msg['body']
|
|
26
|
+
thid = msg.get('thid') or msg['id']
|
|
27
|
+
self.notices_in[thid] = body # the oldest thread forgotten at the store's bound
|
|
28
|
+
ours = None
|
|
29
|
+
if budget is not None:
|
|
30
|
+
m = budget.usage_of_me.get(peer, {}).get(body.get('window'))
|
|
31
|
+
ours = m.snapshot() if m else {}
|
|
32
|
+
metric = body.get('metric')
|
|
33
|
+
if ours is not None and body.get('claimed') is not None and ours.get(metric, 0) != body.get('observed'):
|
|
34
|
+
remedy = complaint.remedy('contested', 'our meter says %s' % ours.get(metric, 0),
|
|
35
|
+
counter_evidence={'counters': ours, 'window': body.get('window')})
|
|
36
|
+
elif (body.get('observed') or 0) > (body.get('limit') or 0):
|
|
37
|
+
remedy = complaint.remedy('budget_throttled', 'over the limit on %s; throttling' % metric)
|
|
38
|
+
else:
|
|
39
|
+
remedy = complaint.remedy('clock_adjusted', 'no overrun on our side; checking the clock')
|
|
40
|
+
self.wire_client.send(peer, complaint.message('remedy', dict(remedy), self.ks.did, _did(peer), thid=thid), stored=True)
|
|
41
|
+
return dict(remedy)
|
|
42
|
+
|
|
43
|
+
# ---- reading ----------------------------------------------------------------
|
|
44
|
+
def _resolver(self: ReporterShape) -> Callable[[str], Optional[NostrEvent]]:
|
|
45
|
+
cache: dict[str, Optional[NostrEvent]] = {}
|
|
46
|
+
|
|
47
|
+
def resolve(eid: str) -> Optional[NostrEvent]:
|
|
48
|
+
if eid not in cache:
|
|
49
|
+
found = relay.query_all(self.relays, [{'ids': [eid]}], self.timeout)
|
|
50
|
+
cache[eid] = found[0] if found else None
|
|
51
|
+
return cache[eid]
|
|
52
|
+
return resolve
|
|
53
|
+
|
|
54
|
+
def reports_about(self: ReporterShape, target_pk: str) -> list[Mapping[str, Any]]:
|
|
55
|
+
"""Every kind 30802 about a node, with admissibility decided here.
|
|
56
|
+
|
|
57
|
+
Reading a report is also the moment this node can price its reporter.
|
|
58
|
+
A report that is inadmissible is unsupported; one that is admissible
|
|
59
|
+
but whose own cited numbers do not produce the outcome it claims is
|
|
60
|
+
refuted. Both are contradicted, both are decidable here alone, and
|
|
61
|
+
both are what the third term of w(r) counts.
|
|
62
|
+
"""
|
|
63
|
+
found = relay.query_all(self.relays, [{'kinds': [30802], '#d': [target_pk]}], self.timeout)
|
|
64
|
+
resolve = self._resolver()
|
|
65
|
+
out: list[Mapping[str, Any]] = []
|
|
66
|
+
for ev in found:
|
|
67
|
+
if not giftwrap.verify_event(ev):
|
|
68
|
+
continue
|
|
69
|
+
okay, reasons = reports.admissible(ev, ev['pubkey'], resolve, giftwrap.verify_event)
|
|
70
|
+
contradicted, why = (True, 'unsupported: %s' % '; '.join(reasons[:2])) if not okay else self._refuted(ev)
|
|
71
|
+
self.ledger.record_report(ev['pubkey'], ev['id'], contradicted)
|
|
72
|
+
if self.accounts is not None:
|
|
73
|
+
self.accounts.put_report_seen(ev['pubkey'], ev['id'], contradicted)
|
|
74
|
+
for cb in self.on_report_read:
|
|
75
|
+
cb(ev['pubkey'], target_pk, ev['id'], okay, contradicted)
|
|
76
|
+
out.append({
|
|
77
|
+
'event': ev,
|
|
78
|
+
'reporter': ev['pubkey'],
|
|
79
|
+
'admissible': okay,
|
|
80
|
+
'reasons': reasons,
|
|
81
|
+
'contradicted': contradicted,
|
|
82
|
+
'why': why,
|
|
83
|
+
'severity': next((t[1] for t in ev['tags'] if t[0] == 'outcome'), None),
|
|
84
|
+
})
|
|
85
|
+
return out
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def _refuted(ev: NostrEvent | Mapping[str, Any]) -> tuple[bool, str]:
|
|
89
|
+
"""(refuted, why) for an admissible report, from its own tags.
|
|
90
|
+
|
|
91
|
+
A report states what it measured, what the subject claimed and what
|
|
92
|
+
the limit was, and the classifier is deterministic, so a reader needs
|
|
93
|
+
neither the grant nor anyone's agreement to check that the outcome
|
|
94
|
+
follows. A report that states no numbers is not refutable: it is a
|
|
95
|
+
finding this reader cannot check, not one it may hold against the
|
|
96
|
+
reporter.
|
|
97
|
+
"""
|
|
98
|
+
tags = {t[0]: t for t in ev.get('tags', []) if t}
|
|
99
|
+
observed, claimed = tags.get('observed'), tags.get('claimed')
|
|
100
|
+
if not observed or len(observed) < 4:
|
|
101
|
+
return False, 'no observation to check'
|
|
102
|
+
try:
|
|
103
|
+
metric, measured, limit = observed[1], int(observed[2]), int(observed[3])
|
|
104
|
+
said = int(claimed[2]) if claimed and len(claimed) > 2 else None
|
|
105
|
+
except (TypeError, ValueError):
|
|
106
|
+
return False, 'observation is not numeric'
|
|
107
|
+
got, want = reports.refuted(ev, {metric: limit}, {metric: measured},
|
|
108
|
+
{metric: said} if said is not None else {},
|
|
109
|
+
window.evaluate_window)
|
|
110
|
+
return got, ('refuted: its own numbers give %s' % want) if got else 'the finding follows from its evidence'
|
|
111
|
+
|
|
112
|
+
def weight_of(self: ReporterShape, reporter_pk: str) -> float:
|
|
113
|
+
"""What this node's own ledger says a reporter's word is worth.
|
|
114
|
+
|
|
115
|
+
The whole of w(r): the tier this node granted the reporter, how long
|
|
116
|
+
its clean streak is, and how often its reports have turned out to be
|
|
117
|
+
unsupported or refuted. That third term had never been passed, so a
|
|
118
|
+
reporter that filed nothing but refutable findings weighed exactly as
|
|
119
|
+
much as an honest one, and filing a false report cost nothing - which
|
|
120
|
+
is the price the attack surface says a reporter pays.
|
|
121
|
+
"""
|
|
122
|
+
if not self.ledger.knows(reporter_pk):
|
|
123
|
+
return 0.0
|
|
124
|
+
contradicted, evaluated = self.ledger.report_record(reporter_pk)
|
|
125
|
+
return window.reporter_weight(self.ledger.tier[reporter_pk], self.ledger.clean_windows(reporter_pk),
|
|
126
|
+
contradicted, evaluated)
|
|
127
|
+
|
|
128
|
+
def view(self: ReporterShape, target_pk: str, own_severity: str = 'S0') -> tuple[str, list[Mapping[str, Any]]]:
|
|
129
|
+
rs: list[Mapping[str, Any]] = [{'severity': r['severity'], 'weight': self.weight_of(r['reporter'])}
|
|
130
|
+
for r in self.reports_about(target_pk) if r['admissible'] and r['severity']]
|
|
131
|
+
return window.view(own_severity, rs), rs
|
|
132
|
+
|
|
133
|
+
# ---- standing/1.0 -------------------------------------------------------------
|
|
134
|
+
def ask_standing(self: ReporterShape, peer_pk: str, target_pk: str, timeout: float = 15.0) -> tuple[Optional[str], Optional[Mapping[str, Any]]]:
|
|
135
|
+
msg = standing.message('ask', dict(standing.ask(_did(target_pk))), self.ks.did, _did(peer_pk),
|
|
136
|
+
mid='standing-ask-%s' % secrets.token_hex(4))
|
|
137
|
+
reply = self.wire_client.request(peer_pk, msg, timeout=timeout)
|
|
138
|
+
if reply is None:
|
|
139
|
+
return None, None
|
|
140
|
+
return reply.message['type'].rsplit('/', 1)[1], reply.message['body']
|
|
141
|
+
|
|
142
|
+
# ---- dispatch -----------------------------------------------------------------
|
|
143
|
+
def handle_notice(self: ReporterShape, inbound: Any, sender: str, budget: Optional[Any] = None) -> Optional[tuple[Any, ...]]:
|
|
144
|
+
msg = getattr(inbound, 'message', inbound)
|
|
145
|
+
why = validate.check_body('complaint', 'notice', msg.get('body'))
|
|
146
|
+
if why is not None:
|
|
147
|
+
self.refusals.refuse('notice', why, subject=sender)
|
|
148
|
+
return ('ignored', sender, 'malformed notice: ' + why)
|
|
149
|
+
if budget is None:
|
|
150
|
+
budget = getattr(self, 'budget', None)
|
|
151
|
+
return ('remedied', sender, self.answer_notice(sender, msg, budget)['action'])
|
|
152
|
+
|
|
153
|
+
def handle_remedy(self: ReporterShape, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]:
|
|
154
|
+
msg = getattr(inbound, 'message', inbound)
|
|
155
|
+
why = validate.check_body('complaint', 'remedy', msg.get('body'))
|
|
156
|
+
if why is not None:
|
|
157
|
+
self.refusals.refuse('remedy', why, subject=sender)
|
|
158
|
+
return ('ignored', sender, 'malformed remedy: ' + why)
|
|
159
|
+
thid = msg.get('thid') or msg.get('id')
|
|
160
|
+
self.remedies_in[thid] = msg['body']
|
|
161
|
+
with self._lock:
|
|
162
|
+
hit = [p for p in self.pending if p.thid == thid]
|
|
163
|
+
for p in hit:
|
|
164
|
+
p.remedy = msg['body']
|
|
165
|
+
for p in hit:
|
|
166
|
+
self._persist(p)
|
|
167
|
+
return ('remedy', sender, complaint.resolve(msg['body']))
|
|
168
|
+
|
|
169
|
+
def handle_withdraw(self: ReporterShape, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]:
|
|
170
|
+
msg = getattr(inbound, 'message', inbound)
|
|
171
|
+
why = validate.check_body('complaint', 'withdraw', msg.get('body'))
|
|
172
|
+
if why is not None:
|
|
173
|
+
self.refusals.refuse('withdraw', why, subject=sender)
|
|
174
|
+
return ('ignored', sender, 'malformed withdraw: ' + why)
|
|
175
|
+
thid = msg.get('thid') or msg.get('id')
|
|
176
|
+
self.log.append(('withdrawn by', sender, msg['body']))
|
|
177
|
+
return ('withdrawn', sender, thid)
|
|
178
|
+
|
|
179
|
+
def handle_ask(self: ReporterShape, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]:
|
|
180
|
+
msg = getattr(inbound, 'message', inbound)
|
|
181
|
+
why = validate.check_body('standing', 'ask', msg.get('body'))
|
|
182
|
+
if why is not None:
|
|
183
|
+
self.refusals.refuse('ask', why, subject=sender)
|
|
184
|
+
return ('ignored', sender, 'malformed ask: ' + why)
|
|
185
|
+
answer, body = self.ledger.answer(msg['body'], willing=True)
|
|
186
|
+
self.wire_client.send(sender, standing.message(answer, body, self.ks.did, _did(sender), thid=msg['id']))
|
|
187
|
+
return ('answered', sender, answer)
|
|
188
|
+
|
|
189
|
+
def handle_unknown(self: ReporterShape, inbound: Any, sender: str) -> tuple[Any, ...]:
|
|
190
|
+
msg = getattr(inbound, 'message', inbound)
|
|
191
|
+
t = str(msg.get('type', '')) if isinstance(msg, dict) else ''
|
|
192
|
+
kind = t.rsplit('/', 1)[1] if '/' in t else t
|
|
193
|
+
return ('ignored', sender, kind)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""The shape of a Reporter as its mixins see it.
|
|
2
|
+
|
|
3
|
+
The one class is split across findings.py, publish.py, reading.py and core.py
|
|
4
|
+
by concern, the same way node/budget/ is split: each mixin method annotates
|
|
5
|
+
`self` as this Protocol so the checker knows what is on it without the mixins
|
|
6
|
+
inheriting from one another or falling back to Any. It is a declaration only;
|
|
7
|
+
Reporter satisfies it structurally and nothing here runs.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import threading
|
|
12
|
+
from typing import Any, Callable, Mapping, Optional, Protocol
|
|
13
|
+
|
|
14
|
+
from fleetmesh_core.accounting import standing
|
|
15
|
+
from fleetmesh_core.wire.interfaces import ComplaintNoticeBody, ComplaintRemedyBody, NostrEvent
|
|
16
|
+
from node.compartment import stores as st
|
|
17
|
+
from node.storage import grants as grants_mod
|
|
18
|
+
from node.compartment import Refusals
|
|
19
|
+
from .finding import Finding, LimitDetail
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ReporterShape(Protocol):
|
|
23
|
+
# class attributes on Reporter and Compartment
|
|
24
|
+
TROUBLE_WINDOW: int
|
|
25
|
+
# set in Compartment.__init__
|
|
26
|
+
refusals: Refusals
|
|
27
|
+
wire_client: Any
|
|
28
|
+
node: Any
|
|
29
|
+
# set in Reporter.__init__, in that order
|
|
30
|
+
regrant: Optional[Callable[[str, str, int], Any]]
|
|
31
|
+
ks: Any
|
|
32
|
+
accounts: Optional[Any]
|
|
33
|
+
project: bool
|
|
34
|
+
projected: st.Ring[NostrEvent]
|
|
35
|
+
labeled: st.Ring[NostrEvent]
|
|
36
|
+
transport: Any
|
|
37
|
+
grants: grants_mod.GrantStore
|
|
38
|
+
relays: list[str]
|
|
39
|
+
ledger: standing.Ledger
|
|
40
|
+
clock: Callable[[], int]
|
|
41
|
+
timeout: float
|
|
42
|
+
_lock: threading.RLock
|
|
43
|
+
pending: list[Finding]
|
|
44
|
+
published: st.Ring[NostrEvent]
|
|
45
|
+
unpublishable: st.Ring[tuple[str, int, str, list[str]]]
|
|
46
|
+
notices_in: st.Table[str, ComplaintNoticeBody]
|
|
47
|
+
remedies_in: st.Table[str, ComplaintRemedyBody]
|
|
48
|
+
log: st.Ring[tuple[Any, ...]]
|
|
49
|
+
forward_clock: Optional[Callable[[str, int], int]]
|
|
50
|
+
budget: Optional[Any]
|
|
51
|
+
on_report_read: list[Callable[[str, str, str, bool, bool], Any]]
|
|
52
|
+
|
|
53
|
+
# core.py
|
|
54
|
+
def _persist(self, p: Finding) -> None: ...
|
|
55
|
+
@property
|
|
56
|
+
def rejected(self) -> list[tuple[Optional[str], str, str]]: ...
|
|
57
|
+
|
|
58
|
+
# findings.py
|
|
59
|
+
def _open(self, peer: str, epoch: int, severity: str, evidence: list[Any],
|
|
60
|
+
worst: Optional[tuple[str, LimitDetail]], source: str,
|
|
61
|
+
raw: Any) -> Optional[Finding]: ...
|
|
62
|
+
def _notify(self, p: Finding) -> None: ...
|
|
63
|
+
|
|
64
|
+
# publish.py
|
|
65
|
+
def publish(self, p: Finding, now: Optional[int] = None) -> Optional[NostrEvent]: ...
|
|
66
|
+
def _project(self, report: NostrEvent, now: int) -> list[NostrEvent]: ...
|
|
67
|
+
def _note_unpublishable(self, p: Finding, reasons: list[str]) -> None: ...
|
|
68
|
+
def _withdraw(self, p: Finding, reason: str) -> None: ...
|
|
69
|
+
def withdraw(self, peer: str, epoch: int, why: Optional[str] = None) -> Mapping[str, Any] | list[Mapping[str, Any]]: ...
|
|
70
|
+
def retract(self, p: Finding, reason: str) -> Optional[str]: ...
|
|
71
|
+
def _restore(self, p: Finding) -> Optional[str]: ...
|
|
72
|
+
def _tick_finding(self, p: Finding, now_val: int) -> list[tuple[Any, ...]]: ...
|
|
73
|
+
|
|
74
|
+
# reading.py
|
|
75
|
+
def _resolver(self) -> Callable[[str], Optional[NostrEvent]]: ...
|
|
76
|
+
def reports_about(self, target_pk: str) -> list[Mapping[str, Any]]: ...
|
|
77
|
+
def _refuted(self, ev: Any) -> tuple[bool, str]: ...
|
|
78
|
+
def weight_of(self, reporter_pk: str) -> float: ...
|
|
79
|
+
def view(self, target_pk: str, own_severity: str = 'S0') -> tuple[str, list[Mapping[str, Any]]]: ...
|
|
80
|
+
def ask_standing(self, peer_pk: str, target_pk: str, timeout: float = 15.0) -> tuple[Optional[str], Optional[Mapping[str, Any]]]: ...
|
|
81
|
+
def answer_notice(self, peer: str, msg: Any, budget: Optional[Any] = None) -> Mapping[str, Any]: ...
|
|
82
|
+
def handle_notice(self, inbound: Any, sender: str, budget: Optional[Any] = None) -> Optional[tuple[Any, ...]]: ...
|
|
83
|
+
def handle_remedy(self, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]: ...
|
|
84
|
+
def handle_withdraw(self, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]: ...
|
|
85
|
+
def handle_ask(self, inbound: Any, sender: str) -> Optional[tuple[Any, ...]]: ...
|
|
86
|
+
def handle_unknown(self, inbound: Any, sender: str) -> tuple[Any, ...]: ...
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Types and records for the Reporter compartment."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Optional, TypedDict
|
|
5
|
+
|
|
6
|
+
from fleetmesh_core.wire.interfaces import ComplaintRemedyBody, NostrEvent
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LimitDetail(TypedDict, total=False):
|
|
10
|
+
"""One per-limit entry of fleetmesh_core.accounting.window.evaluate's `detail`: what was
|
|
11
|
+
measured against what limit, and what the subject's receipt claimed."""
|
|
12
|
+
measured: float
|
|
13
|
+
claimed: Optional[float]
|
|
14
|
+
limit: float
|
|
15
|
+
overrun: float
|
|
16
|
+
divergence: float
|
|
17
|
+
severity: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FindingDict(TypedDict, total=False):
|
|
21
|
+
"""The JSON serialization shape of a Finding."""
|
|
22
|
+
subject: str
|
|
23
|
+
severity: str
|
|
24
|
+
evidence: list[tuple[Any, ...]]
|
|
25
|
+
worst: Optional[tuple[str, LimitDetail]]
|
|
26
|
+
source: str
|
|
27
|
+
grant: str
|
|
28
|
+
window_seconds: int
|
|
29
|
+
opened_at: int
|
|
30
|
+
notice_at: Optional[int]
|
|
31
|
+
thid: Optional[str]
|
|
32
|
+
remedy: Optional[ComplaintRemedyBody]
|
|
33
|
+
report: Optional[NostrEvent]
|
|
34
|
+
withdrawn: bool
|
|
35
|
+
why: list[Any]
|
|
36
|
+
measured: dict[str, float]
|
|
37
|
+
state: Optional[str]
|
|
38
|
+
limits: dict[str, float]
|
|
39
|
+
evidence_partial: bool
|
|
40
|
+
tries: int
|
|
41
|
+
unpublishable: Optional[list[str]]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ReporterStateReport(TypedDict):
|
|
45
|
+
pending: int
|
|
46
|
+
published: int
|
|
47
|
+
withdrawn: int
|
|
48
|
+
noticed: int
|
|
49
|
+
unpublishable: int
|
|
50
|
+
malformed: int
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""What this node has survived, and the reserves it sets.
|
|
2
|
+
|
|
3
|
+
One-module compartment: everything is in compartment.py.
|
|
4
|
+
"""
|
|
5
|
+
from .compartment import StrainLedger
|
|
6
|
+
from .types import FlagTreeReport, StrainStateReport
|
|
7
|
+
|
|
8
|
+
__all__ = ['StrainLedger', 'FlagTreeReport', 'StrainStateReport']
|
|
9
|
+
|