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,299 @@
|
|
|
1
|
+
"""The Reporter subsystem itself: construction and restore, what survives a
|
|
2
|
+
restart, and the Compartment contract. Separate from the mixins because this is
|
|
3
|
+
where the state they all share is created; the mixins are the concerns that
|
|
4
|
+
act on it.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import dataclasses
|
|
9
|
+
import threading
|
|
10
|
+
from typing import Any, Callable, Mapping, Optional, Sequence
|
|
11
|
+
|
|
12
|
+
from node.config.parameter import Parameter, ParamBlock
|
|
13
|
+
from node.channels import Channel, NOTE, TROUBLE, bus as eventbus_mod
|
|
14
|
+
from node.metrics import COUNTER, Metric
|
|
15
|
+
from fleetmesh_core.accounting import standing # noqa: E402
|
|
16
|
+
from fleetmesh_core.peering import complaint # noqa: E402
|
|
17
|
+
from fleetmesh_core.wire.interfaces import ComplaintNoticeBody, ComplaintRemedyBody, NostrEvent # noqa: E402
|
|
18
|
+
from node.storage import grants as grants_mod # noqa: E402
|
|
19
|
+
from node.compartment import stores as st # noqa: E402
|
|
20
|
+
from node.compartment import Command, Handler, Need, Outbound, WireProtocol # noqa: E402
|
|
21
|
+
from .constants import FINDING_WINDOWS, KEEP_LOG, KEEP_SIGNED, KEEP_THREADS, KEEP_UNPUBLISHABLE, PUBLISH_TRIES, REPORT_TTL
|
|
22
|
+
from .finding import Finding
|
|
23
|
+
from .findings import FindingsMixin
|
|
24
|
+
from .publish import PublishMixin
|
|
25
|
+
from .reading import ReadingMixin
|
|
26
|
+
from .types import ReporterStateReport
|
|
27
|
+
|
|
28
|
+
@dataclasses.dataclass(frozen=True)
|
|
29
|
+
class ReporterConfig(ParamBlock):
|
|
30
|
+
key = 'reporter'
|
|
31
|
+
name = 'reporter'
|
|
32
|
+
DERIVED: bool = False
|
|
33
|
+
PARAMS = (
|
|
34
|
+
Parameter(
|
|
35
|
+
key='report_ttl', name='Report TTL',
|
|
36
|
+
description='Time-to-live in seconds for signed reports',
|
|
37
|
+
type=int, default=REPORT_TTL, minimum=86400,
|
|
38
|
+
tags=('reporter', 'retention'),
|
|
39
|
+
),
|
|
40
|
+
Parameter(
|
|
41
|
+
key='finding_windows', name='Finding Windows Horizon',
|
|
42
|
+
description='Window horizon for substantiated findings',
|
|
43
|
+
type=int, default=FINDING_WINDOWS, minimum=1,
|
|
44
|
+
tags=('reporter', 'history'),
|
|
45
|
+
),
|
|
46
|
+
Parameter(
|
|
47
|
+
key='publish_tries', name='Publish Tries',
|
|
48
|
+
description='Maximum publish attempts before finding marked unpublishable',
|
|
49
|
+
type=int, default=PUBLISH_TRIES, minimum=1,
|
|
50
|
+
tags=('reporter', 'publish'),
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
report_ttl: int = REPORT_TTL
|
|
54
|
+
finding_windows: int = FINDING_WINDOWS
|
|
55
|
+
publish_tries: int = PUBLISH_TRIES
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Reporter(FindingsMixin, PublishMixin, ReadingMixin, WireProtocol):
|
|
59
|
+
"""Findings on their way to a report, as a compartment.
|
|
60
|
+
|
|
61
|
+
Its refusals are complaint and standing messages without the shape of their
|
|
62
|
+
type, which were kept in a list nothing rendered. Its trouble is the one an
|
|
63
|
+
operator most needs and would otherwise never see: a finding this node holds
|
|
64
|
+
and cannot publish, because the subject signed nothing that window and
|
|
65
|
+
active silence is unreportable by design.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
name: str = 'reporter'
|
|
69
|
+
piuris: tuple[str, ...] = (complaint.PIURI, standing.PIURI)
|
|
70
|
+
reads: tuple[str, ...] = ('projection',)
|
|
71
|
+
config_cls = ReporterConfig
|
|
72
|
+
CHANNELS = (
|
|
73
|
+
Channel('report_published', 'A signed report was published to relays',
|
|
74
|
+
level=NOTE, say='published report {report_id} on peer {peer}',
|
|
75
|
+
tags=('reporter', 'publish')),
|
|
76
|
+
Channel('report_withdrawn', 'A finding or report was withdrawn by operator',
|
|
77
|
+
level=NOTE, say='withdrew report on peer {peer} window {window}',
|
|
78
|
+
tags=('reporter', 'withdraw')),
|
|
79
|
+
Channel('finding_unpublishable', 'An adverse finding could not be published to relays',
|
|
80
|
+
level=TROUBLE, say='finding on peer {peer} window {window} unpublishable: {reason}',
|
|
81
|
+
tags=('reporter', 'trouble')),
|
|
82
|
+
)
|
|
83
|
+
METRICS = (
|
|
84
|
+
Metric(name='reporter_reports_published', description='Reports published.',
|
|
85
|
+
kind=COUNTER, counts='report_published', tags=('reporter', 'publish')),
|
|
86
|
+
Metric(name='reporter_reports_withdrawn', description='Reports withdrawn.',
|
|
87
|
+
kind=COUNTER, counts='report_withdrawn', tags=('reporter', 'withdraw')),
|
|
88
|
+
Metric(name='reporter_findings_unpublishable', description='Findings unpublishable.',
|
|
89
|
+
kind=COUNTER, counts='finding_unpublishable', tags=('reporter', 'trouble')),
|
|
90
|
+
)
|
|
91
|
+
OUTBOUND = (
|
|
92
|
+
Outbound('notice', reply='remedy', doc='notify peer of adverse finding'),
|
|
93
|
+
Outbound('remedy', doc='submit remedy in response to complaint notice'),
|
|
94
|
+
Outbound('withdraw', doc='withdraw complaint finding'),
|
|
95
|
+
Outbound('ask', reply=('tell', 'decline'), doc='query peer standing'),
|
|
96
|
+
Outbound('tell', doc='answer standing query with peer streak'),
|
|
97
|
+
Outbound('decline', doc='decline standing query'),
|
|
98
|
+
)
|
|
99
|
+
HANDLERS = (
|
|
100
|
+
Handler('notice', run='handle_notice', doc='answer complaint notice'),
|
|
101
|
+
Handler('remedy', run='handle_remedy', doc='receive complaint remedy'),
|
|
102
|
+
Handler('withdraw', run='handle_withdraw', doc='receive complaint withdrawal'),
|
|
103
|
+
Handler('ask', run='handle_ask', doc='answer standing ask from ledger'),
|
|
104
|
+
Handler('*', run='handle_unknown', doc='ignore unknown complaint or standing kind by name'),
|
|
105
|
+
)
|
|
106
|
+
COMMANDS = (
|
|
107
|
+
Command(
|
|
108
|
+
key='show',
|
|
109
|
+
name='Reporter Status',
|
|
110
|
+
description='findings held, reports published, and what cannot be published at all',
|
|
111
|
+
tags=('reporter', 'status'),
|
|
112
|
+
permission='reporter/show',
|
|
113
|
+
run='state',
|
|
114
|
+
reply='{pending, published, withdrawn, unpublishable, noticed, malformed}',
|
|
115
|
+
),
|
|
116
|
+
Command(
|
|
117
|
+
key='withdraw',
|
|
118
|
+
name='Withdraw Finding',
|
|
119
|
+
description="withdraw a finding this node made about one window, published or not; the operator's call",
|
|
120
|
+
tags=('reporter', 'withdraw'),
|
|
121
|
+
parameters=(
|
|
122
|
+
Parameter(key='peer', name='peer', description='peer pubkey or did', type=str, default='', required=True),
|
|
123
|
+
Parameter(key='window', name='window', description='window number', type=int, default=0, required=True),
|
|
124
|
+
Parameter(key='why', name='why', description='reason for withdrawal', type=str, default=None, required=False),
|
|
125
|
+
),
|
|
126
|
+
permission='reporter/withdraw',
|
|
127
|
+
run='withdraw_cmd',
|
|
128
|
+
reply='{peer, window, was_published, report, why}',
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# What it keeps. The three lists of signed events and the log grew by one
|
|
133
|
+
# per report, score, label or complaint message for the life of the
|
|
134
|
+
# process, and `remedies_in` - written by any peer that sends a remedy -
|
|
135
|
+
# was trimmed only when a finding expired. `published` is still counted
|
|
136
|
+
# whole in `state`, from the ring's inserts. `pending` is not here: it is
|
|
137
|
+
# the queue `accounts` keeps on disk, for a durable store to declare.
|
|
138
|
+
STORES = (
|
|
139
|
+
st.Store(key='published', description='kind 30802 reports this node signed, newest last',
|
|
140
|
+
kind=st.RING, shape=dict, bound=KEEP_SIGNED),
|
|
141
|
+
st.Store(key='projected', description='kind 30385 relay scores this node signed, newest last',
|
|
142
|
+
kind=st.RING, shape=dict, bound=KEEP_SIGNED),
|
|
143
|
+
st.Store(key='labeled', description='kind 1985 labels this node signed, newest last',
|
|
144
|
+
kind=st.RING, shape=dict, bound=KEEP_SIGNED),
|
|
145
|
+
st.Store(key='unpublishable', description='findings it may not publish, as (peer, window, severity, reasons)',
|
|
146
|
+
kind=st.RING, shape=tuple, bound=KEEP_UNPUBLISHABLE),
|
|
147
|
+
st.Store(key='notices_in', description='complaint notices received, where this node is the subject',
|
|
148
|
+
kind=st.TABLE, shape=dict, bound=KEEP_THREADS, keyed_by='thid'),
|
|
149
|
+
st.Store(key='remedies_in', description='remedies received, where this node is the issuer',
|
|
150
|
+
kind=st.TABLE, shape=dict, bound=KEEP_THREADS, keyed_by='thid'),
|
|
151
|
+
st.Store(key='log', description='withdrawals against this node and tiers it restored, newest last',
|
|
152
|
+
kind=st.RING, shape=tuple, bound=KEEP_LOG),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
published: st.Ring[NostrEvent]
|
|
156
|
+
projected: st.Ring[NostrEvent]
|
|
157
|
+
labeled: st.Ring[NostrEvent]
|
|
158
|
+
unpublishable: st.Ring[tuple[str, int, str, list[str]]]
|
|
159
|
+
notices_in: st.Table[str, ComplaintNoticeBody]
|
|
160
|
+
remedies_in: st.Table[str, ComplaintRemedyBody]
|
|
161
|
+
log: st.Ring[tuple[Any, ...]]
|
|
162
|
+
|
|
163
|
+
NEEDS = (
|
|
164
|
+
Need('keystore', 'ks'),
|
|
165
|
+
Need('transport'),
|
|
166
|
+
Need('grant_store', 'stores.grants'),
|
|
167
|
+
Need('relay_urls', 'relays'),
|
|
168
|
+
Need('ledger'),
|
|
169
|
+
Need('project', 'cfg', field='projection'),
|
|
170
|
+
Need('timeout'),
|
|
171
|
+
Need('accounts', 'stores.accounts'),
|
|
172
|
+
Need('regrant'),
|
|
173
|
+
Need('events'),
|
|
174
|
+
Need('discovery', late=True),
|
|
175
|
+
Need('budget', late=True),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def start(self) -> None:
|
|
179
|
+
if self.events is not None:
|
|
180
|
+
events = self.events
|
|
181
|
+
self.on_report_read.append(lambda *a: events.emit(eventbus_mod.TOPIC_REPORT_ADMITTED, *a))
|
|
182
|
+
|
|
183
|
+
def __init__(
|
|
184
|
+
self,
|
|
185
|
+
keystore: Any,
|
|
186
|
+
transport: Any,
|
|
187
|
+
grant_store: grants_mod.GrantStore,
|
|
188
|
+
relay_urls: Sequence[str],
|
|
189
|
+
ledger: Optional[standing.Ledger] = None,
|
|
190
|
+
clock: Optional[Callable[[], int]] = None,
|
|
191
|
+
timeout: float = 15.0,
|
|
192
|
+
node: Optional[Any] = None,
|
|
193
|
+
project: bool = True,
|
|
194
|
+
accounts: Optional[Any] = None,
|
|
195
|
+
regrant: Optional[Callable[[str, str, int], Any]] = None,
|
|
196
|
+
events: Optional[Any] = None,
|
|
197
|
+
discovery: Optional[Any] = None,
|
|
198
|
+
budget: Optional[Any] = None,
|
|
199
|
+
) -> None:
|
|
200
|
+
super().__init__(node, clock)
|
|
201
|
+
# The node call that re-issues a grant when a tier is restored;
|
|
202
|
+
# suites that pass a fake node and nothing else get it off that.
|
|
203
|
+
self.regrant: Optional[Callable[[str, str, int], Any]] = regrant if regrant is not None else getattr(node, 'regrant', None)
|
|
204
|
+
self.ks: Any = keystore
|
|
205
|
+
self.accounts: Optional[Any] = accounts # where pending findings survive a restart; None keeps them in memory
|
|
206
|
+
self.project: bool = True if project is None else bool(project) # publish the kind 30385 relay score beside each report
|
|
207
|
+
self.projected: st.Ring[NostrEvent] # the kind 30385 events this node put on the relays
|
|
208
|
+
self.labeled: st.Ring[NostrEvent] # the kind 1985 NIP-32 labels this node put on the relays
|
|
209
|
+
self.transport: Any = transport
|
|
210
|
+
self.grants: grants_mod.GrantStore = grant_store
|
|
211
|
+
self.relays: list[str] = list(relay_urls) # not a store: the configured relays
|
|
212
|
+
self.ledger: standing.Ledger = ledger if ledger is not None else standing.Ledger()
|
|
213
|
+
self.events: Optional[Any] = events
|
|
214
|
+
self.discovery: Optional[Any] = discovery
|
|
215
|
+
self.budget: Optional[Any] = budget
|
|
216
|
+
self._forward_clock: Optional[Callable[[str, int], int]] = None
|
|
217
|
+
# After the ledger exists and not before it. This read the store into
|
|
218
|
+
# `self.ledger` four lines above the line that creates it, so a node
|
|
219
|
+
# started fresh was fine and a node restarted after it had read one
|
|
220
|
+
# report of anyone's died on the way up with AttributeError - a crash
|
|
221
|
+
# loop that only ever reached the nodes doing the most work. Two of a
|
|
222
|
+
# four-node mesh went down this way at once.
|
|
223
|
+
if accounts is not None:
|
|
224
|
+
for reporter, rid, bad in accounts.reports_seen():
|
|
225
|
+
self.ledger.record_report(reporter, rid, bad)
|
|
226
|
+
self.timeout: float = timeout
|
|
227
|
+
self._lock: threading.RLock = threading.RLock()
|
|
228
|
+
self.pending: list[Finding] = [] # not a store: findings on their way to a report - the queue accounts keeps on disk, for persist=DURABLE
|
|
229
|
+
self.published: st.Ring[NostrEvent] # kind 30802 events this node put on the relays
|
|
230
|
+
self.unpublishable: st.Ring[tuple[str, int, str, list[str]]] # findings it may not publish: no evidence a reader can check
|
|
231
|
+
self.notices_in: st.Table[str, ComplaintNoticeBody] # thid -> notice body received (we are the subject)
|
|
232
|
+
# Refusals go through the compartment channel. `rejected` stays as a
|
|
233
|
+
# view in the shape two suites and the health block already read.
|
|
234
|
+
self.remedies_in: st.Table[str, ComplaintRemedyBody] # thid -> remedy body received (we are the issuer)
|
|
235
|
+
self.log: st.Ring[tuple[Any, ...]]
|
|
236
|
+
self.on_report_read: list[Callable[[str, str, str, bool, bool], Any]] = [] # (reporter, target, id, admissible, contradicted)
|
|
237
|
+
if accounts is not None:
|
|
238
|
+
self.pending = [Finding.from_dict(b) for b in accounts.findings()] # not a store: loaded from accounts, for persist=DURABLE
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def forward_clock(self) -> Optional[Callable[[str, int], int]]:
|
|
242
|
+
if self._forward_clock is not None:
|
|
243
|
+
return self._forward_clock
|
|
244
|
+
if getattr(self, 'discovery', None) is not None:
|
|
245
|
+
return getattr(self.discovery, 'forward_clock', None)
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
@forward_clock.setter
|
|
249
|
+
def forward_clock(self, fn: Optional[Callable[[str, int], int]]) -> None:
|
|
250
|
+
self._forward_clock = fn
|
|
251
|
+
|
|
252
|
+
# ---- what survives a restart -------------------------------------------------
|
|
253
|
+
def _persist(self, p: Finding) -> None:
|
|
254
|
+
"""Write a pending finding down. Called at every change of its state:
|
|
255
|
+
opened, noticed, remedied, published, unpublishable, withdrawn.
|
|
256
|
+
|
|
257
|
+
Four nodes on three machines showed why. Two London nodes priced two
|
|
258
|
+
peers S2 at a window close, sent the notices, and were restarted eight
|
|
259
|
+
minutes later for an unrelated fix. The findings, their evidence and
|
|
260
|
+
the notice threads were memory only. The window passed and the reports
|
|
261
|
+
the notices had promised never came, and nothing said so.
|
|
262
|
+
"""
|
|
263
|
+
if self.accounts is None:
|
|
264
|
+
return
|
|
265
|
+
self.accounts.put_finding(p.fid, p.peer, p.epoch, p.to_dict())
|
|
266
|
+
|
|
267
|
+
# ---- the compartment contract -------------------------------------------------
|
|
268
|
+
@property
|
|
269
|
+
def rejected(self) -> list[tuple[Optional[str], str, str]]:
|
|
270
|
+
return [(r.subject, r.kind, r.why) for r in self.refusals.last(200)]
|
|
271
|
+
|
|
272
|
+
def state(self) -> ReporterStateReport:
|
|
273
|
+
held = [p for p in self.pending if not p.done]
|
|
274
|
+
return {
|
|
275
|
+
'pending': len(held),
|
|
276
|
+
'published': self.published.inserts,
|
|
277
|
+
'withdrawn': sum(1 for p in self.pending if p.withdrawn),
|
|
278
|
+
'noticed': sum(1 for p in held if p.notice_at is not None),
|
|
279
|
+
'unpublishable': len(self.unpublishable),
|
|
280
|
+
'malformed': len(self.rejected),
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
def trouble(self) -> list[str]:
|
|
284
|
+
stuck = self.unpublishable
|
|
285
|
+
if not stuck:
|
|
286
|
+
return []
|
|
287
|
+
# The operator's own view of a rule the specification states and nothing
|
|
288
|
+
# else surfaces: a window of active silence cannot produce a kind 30802,
|
|
289
|
+
# because the subject signed nothing that window and no reader could
|
|
290
|
+
# check either half of the claim. The remedy is local and complete.
|
|
291
|
+
return [
|
|
292
|
+
'%d finding(s) can never be published - the subject signed nothing those windows. '
|
|
293
|
+
'The grant is cut locally and standing/1.0 still carries the streak' % len(stuck)
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
def withdraw_cmd(self, args: Mapping[str, Any]) -> Any:
|
|
297
|
+
return self.withdraw(str(args['peer']), int(args['window']), args.get('why'))
|
|
298
|
+
|
|
299
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""A finding on its way to a report, as one record.
|
|
2
|
+
|
|
3
|
+
The reporter used to carry each pending finding as a dict of twenty-odd
|
|
4
|
+
string keys, mutated by four mixins and thawed from JSON on restart. A key
|
|
5
|
+
misspelt in one mixin read as `None` in the next and nothing said so. This is
|
|
6
|
+
the same record with its fields written down once; the JSON edge is
|
|
7
|
+
`to_dict` / `from_dict`, and everything between the store and the wire reads
|
|
8
|
+
attributes.
|
|
9
|
+
|
|
10
|
+
The shape is the one `_open` built and `_thaw` read back: JSON made the
|
|
11
|
+
tuples lists, and the code reads evidence entries, the worst limit and
|
|
12
|
+
session findings positionally, so `from_dict` gives them their shape back.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import asdict, dataclass, field, fields
|
|
17
|
+
from typing import Any, Mapping, Optional, cast
|
|
18
|
+
from fleetmesh_core.wire.interfaces import ComplaintRemedyBody, NostrEvent # noqa: E402
|
|
19
|
+
|
|
20
|
+
__all__ = ['Finding', 'LimitDetail']
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
from .types import LimitDetail, FindingDict
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(slots=True)
|
|
27
|
+
class Finding:
|
|
28
|
+
fid: str
|
|
29
|
+
peer: str
|
|
30
|
+
epoch: int
|
|
31
|
+
severity: str
|
|
32
|
+
# ('receipt', id, receipt) / ('envelope', id, env) / ('event', id) - headed by kind
|
|
33
|
+
evidence: list[tuple[Any, ...]]
|
|
34
|
+
# (metric, {'measured', 'limit', 'claimed', ...}) for a per-limit finding; None for silence
|
|
35
|
+
worst: Optional[tuple[str, LimitDetail]]
|
|
36
|
+
source: str # 'window' | 'session'
|
|
37
|
+
grant: str # envelope id of the grant that governed it
|
|
38
|
+
window_seconds: int
|
|
39
|
+
opened_at: int
|
|
40
|
+
notice_at: Optional[int] = None
|
|
41
|
+
thid: Optional[str] = None # the complaint thread the notice opened
|
|
42
|
+
remedy: Optional[ComplaintRemedyBody] = None # the subject's remedy body, if one came
|
|
43
|
+
report: Optional[NostrEvent] = None # the signed kind 30802, once published
|
|
44
|
+
withdrawn: bool = False
|
|
45
|
+
why: list[Any] = field(default_factory=list) # session findings, read positionally
|
|
46
|
+
measured: dict[str, float] = field(default_factory=dict) # metric -> what this node counted
|
|
47
|
+
state: Optional[str] = None
|
|
48
|
+
limits: dict[str, float] = field(default_factory=dict) # metric -> the grant's ceiling
|
|
49
|
+
evidence_partial: bool = False
|
|
50
|
+
tries: int = 0 # publish attempts that came back inadmissible
|
|
51
|
+
unpublishable: Optional[list[str]] = None # why the last attempt was refused
|
|
52
|
+
|
|
53
|
+
# ---- the JSON edge ---------------------------------------------------------
|
|
54
|
+
def to_dict(self) -> FindingDict:
|
|
55
|
+
return cast(FindingDict, asdict(self))
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_dict(cls, body: Mapping[str, Any]) -> 'Finding':
|
|
59
|
+
"""A finding read back from the store, or built by a test from a
|
|
60
|
+
plain mapping. Unknown keys are dropped; missing optional ones take
|
|
61
|
+
their defaults, so a record written by an older build still thaws."""
|
|
62
|
+
known = {f.name for f in fields(cls)}
|
|
63
|
+
p = {k: v for k, v in body.items() if k in known}
|
|
64
|
+
p['evidence'] = [tuple(e) for e in (p.get('evidence') or [])]
|
|
65
|
+
w = p.get('worst')
|
|
66
|
+
p['worst'] = (w[0], w[1]) if w else None
|
|
67
|
+
p['why'] = [tuple(x) if isinstance(x, list) else x for x in (p.get('why') or [])]
|
|
68
|
+
return cls(**p)
|
|
69
|
+
|
|
70
|
+
# ---- what the mixins ask ---------------------------------------------------
|
|
71
|
+
@property
|
|
72
|
+
def done(self) -> bool:
|
|
73
|
+
"""Published or withdrawn: nothing further will happen to it."""
|
|
74
|
+
return self.report is not None or self.withdrawn
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Findings in: a window the budget evaluated, or a session the sync layer
|
|
2
|
+
ran. Each is a verdict of fleetmesh_core/'s arithmetic against one peer under one grant
|
|
3
|
+
this node issued, and what happens next is the specification's complaint
|
|
4
|
+
rule, executed.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import secrets
|
|
9
|
+
from typing import Any, Mapping, Optional, cast
|
|
10
|
+
from fleetmesh_core.accounting import accounting, window # noqa: E402
|
|
11
|
+
from fleetmesh_core.crypto import giftwrap # noqa: E402
|
|
12
|
+
from fleetmesh_core.peering import complaint, reports # noqa: E402
|
|
13
|
+
from fleetmesh_core.storage import replication # noqa: E402
|
|
14
|
+
from node.storage import grants as grants_mod # noqa: E402
|
|
15
|
+
from node.protocols.nostr import client as relay # noqa: E402
|
|
16
|
+
from node.planes.peering.budget.evaluation import Evaluation, evaluation_like # noqa: E402
|
|
17
|
+
from .constants import LIMIT_METRICS
|
|
18
|
+
from .finding import Finding, LimitDetail
|
|
19
|
+
from .shape import ReporterShape
|
|
20
|
+
|
|
21
|
+
from fleetmesh_core.identity.did import did_nostr as _did
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FindingsMixin:
|
|
25
|
+
# ---- findings in --------------------------------------------------------
|
|
26
|
+
def on_evaluated(self: ReporterShape, rec: Evaluation | Mapping[str, Any]) -> Optional[Finding]:
|
|
27
|
+
"""A window the budget closed. Adverse outcomes become pending reports."""
|
|
28
|
+
rec = evaluation_like(rec)
|
|
29
|
+
sev = rec.severity
|
|
30
|
+
if sev not in reports.ADVERSE:
|
|
31
|
+
return None
|
|
32
|
+
detail = rec.detail
|
|
33
|
+
worst: Optional[tuple[str, LimitDetail]] = None
|
|
34
|
+
for name, d in detail.items():
|
|
35
|
+
# per-limit entries carry a measurement; the silence entry carries none
|
|
36
|
+
if isinstance(d, dict) and d.get('severity') == sev and 'measured' in d:
|
|
37
|
+
worst = (str(name), cast(LimitDetail, d))
|
|
38
|
+
break
|
|
39
|
+
# The subject's own signed claim, carried whole. A head alone resolves to
|
|
40
|
+
# nothing: no relay serves a receipt.
|
|
41
|
+
#
|
|
42
|
+
# Only the gap-free, chain-verified prefix is carried. The budget
|
|
43
|
+
# stores a receipt that arrived after a lost link (docs/mechanisms.md
|
|
44
|
+
# 9g), but a verifier holds cited chains to fleetmesh_core.accounting.verify_chain,
|
|
45
|
+
# which is strict, so a report that attached the stored list whole
|
|
46
|
+
# was inadmissible whenever the wire had dropped one receipt. What is
|
|
47
|
+
# cut is noted on the pending record as evidence_partial.
|
|
48
|
+
signed = [r for r in rec.receipts if accounting.verify_receipt(r, rec.peer)]
|
|
49
|
+
admissible, partial = accounting.admissible_prefix(signed)
|
|
50
|
+
evidence = [('receipt', reports.evidence_id(r), r) for r in admissible]
|
|
51
|
+
raw = dict(rec.to_dict(), evidence_partial=partial)
|
|
52
|
+
return self._open(rec.peer, rec.epoch, sev, evidence, worst, 'window', raw)
|
|
53
|
+
|
|
54
|
+
def on_repriced(self: ReporterShape, peer: str, epoch: int, was: str, severity: str) -> Optional[list[tuple[str, int]]]:
|
|
55
|
+
"""A window this node priced was read again and came out lower, because
|
|
56
|
+
the subject's receipt for it arrived after the grace had run.
|
|
57
|
+
|
|
58
|
+
The finding that window raised is no longer this node's finding. An
|
|
59
|
+
unpublished one is dropped before it can become a public record; a
|
|
60
|
+
published one is withdrawn, which is what complaint/1.0 has a withdraw
|
|
61
|
+
for and what this node already does when a remedy lands and the next
|
|
62
|
+
window is clean. Left alone it would stand for the whole of REPORT_TTL
|
|
63
|
+
as a permanent record of a delivery gap.
|
|
64
|
+
|
|
65
|
+
Only a correction to S0 withdraws. A reading that is milder but still
|
|
66
|
+
adverse leaves the report standing rather than replacing it with a
|
|
67
|
+
second one about the same window, which is the same strict direction
|
|
68
|
+
the budget takes when it declines to cancel a remedy it cannot rebuild.
|
|
69
|
+
"""
|
|
70
|
+
if severity in reports.ADVERSE:
|
|
71
|
+
return None
|
|
72
|
+
with self._lock:
|
|
73
|
+
hit = [q for q in self.pending
|
|
74
|
+
if q.peer == peer and q.epoch == epoch and not q.withdrawn]
|
|
75
|
+
why = 'repriced %s after a receipt for that window arrived late' % was
|
|
76
|
+
out: list[tuple[str, int]] = []
|
|
77
|
+
for q in hit:
|
|
78
|
+
if q.report is None and not q.thid:
|
|
79
|
+
# Nothing was ever said about it: no notice, no report. There
|
|
80
|
+
# is no one to tell, so telling no one is the withdrawal - but
|
|
81
|
+
# the standing it cost is still this node's to give back.
|
|
82
|
+
with self._lock:
|
|
83
|
+
q.withdrawn = True
|
|
84
|
+
self._persist(q)
|
|
85
|
+
self._restore(q)
|
|
86
|
+
else:
|
|
87
|
+
# A receipt arrived late and the window was repriced: the
|
|
88
|
+
# verdict was wrong about a fact, so it takes its tier with it.
|
|
89
|
+
self.retract(q, why)
|
|
90
|
+
out.append((q.peer, q.epoch))
|
|
91
|
+
return out
|
|
92
|
+
|
|
93
|
+
def on_session(self: ReporterShape, peer: str, role: str, result: Mapping[str, Any]) -> Optional[Finding]:
|
|
94
|
+
"""A sync session ended. Its S3 findings carry the events themselves."""
|
|
95
|
+
if role != 'initiator':
|
|
96
|
+
return None
|
|
97
|
+
worst = max((f[0] for f in result.get('findings', [])), default='S0', key=window.SEV_ORDER.index)
|
|
98
|
+
if worst not in reports.ADVERSE:
|
|
99
|
+
return None
|
|
100
|
+
# An envelope citation carries the signed hop; a public event needs
|
|
101
|
+
# only its id. Both are tuples headed by the kind.
|
|
102
|
+
evidence: list[tuple[Any, ...]] = []
|
|
103
|
+
for item in result.get('evidence', []):
|
|
104
|
+
ev, env = (item.get('event'), item.get('envelope')) if isinstance(item, dict) else (item, None)
|
|
105
|
+
# The envelope is the better citation: the peer signed the hop even
|
|
106
|
+
# when somebody else wrote the event, so it proves the peer sent this.
|
|
107
|
+
if env is not None and replication.signed_by(env, peer):
|
|
108
|
+
evidence.append(('envelope', reports.evidence_id(env), env))
|
|
109
|
+
elif ev is not None and giftwrap.verify_event(ev) and ev.get('pubkey') == peer:
|
|
110
|
+
evidence.append(('event', ev['id']))
|
|
111
|
+
relay.publish_all(self.relays, ev, self.timeout) # a public event needs no copy
|
|
112
|
+
row = self.grants.get(peer, grants_mod.ISSUED)
|
|
113
|
+
epoch = self.clock() // ((row['payload'].get('window') if row else None) or 3600)
|
|
114
|
+
return self._open(peer, epoch, worst, evidence, None, 'session', result)
|
|
115
|
+
|
|
116
|
+
def _open(
|
|
117
|
+
self: ReporterShape,
|
|
118
|
+
peer: str,
|
|
119
|
+
epoch: int,
|
|
120
|
+
severity: str,
|
|
121
|
+
evidence: list[Any],
|
|
122
|
+
worst: Optional[tuple[str, LimitDetail]],
|
|
123
|
+
source: str,
|
|
124
|
+
raw: Mapping[str, Any],
|
|
125
|
+
) -> Optional[Finding]:
|
|
126
|
+
row = self.grants.get(peer, grants_mod.ISSUED)
|
|
127
|
+
if row is None:
|
|
128
|
+
return None
|
|
129
|
+
now = self.clock()
|
|
130
|
+
# A window is reported under the grant that governed it, which the
|
|
131
|
+
# budget names; the published report had named the current grant while
|
|
132
|
+
# its evidence was a receipt under the one before.
|
|
133
|
+
p = Finding(
|
|
134
|
+
fid=secrets.token_hex(8),
|
|
135
|
+
peer=peer, epoch=epoch, severity=severity, evidence=evidence, worst=worst,
|
|
136
|
+
source=source, grant=(raw.get('grant') if source == 'window' else None) or row['envelope_id'],
|
|
137
|
+
window_seconds=row['payload'].get('window', 3600),
|
|
138
|
+
opened_at=now,
|
|
139
|
+
why=[f for f in raw.get('findings', [])] if source == 'session' else [],
|
|
140
|
+
measured=raw.get('measured') or {}, state=raw.get('state'),
|
|
141
|
+
limits=dict((raw.get('limits') if source == 'window' else None) or row['payload'].get('limits', {})),
|
|
142
|
+
evidence_partial=bool(raw.get('evidence_partial', False)),
|
|
143
|
+
)
|
|
144
|
+
with self._lock:
|
|
145
|
+
self.pending.append(p)
|
|
146
|
+
self._persist(p)
|
|
147
|
+
if severity in complaint.C['notice_required_for']:
|
|
148
|
+
self._notify(p)
|
|
149
|
+
elif severity in complaint.C['immediate']:
|
|
150
|
+
self.publish(p, now) # may return None: see publish()
|
|
151
|
+
return p
|
|
152
|
+
|
|
153
|
+
# ---- the complaint, issuer side -------------------------------------------
|
|
154
|
+
def _notify(self: ReporterShape, p: Finding) -> None:
|
|
155
|
+
worst = p.worst
|
|
156
|
+
if worst is None:
|
|
157
|
+
# No per-limit finding: active silence. Name the metric that moved most.
|
|
158
|
+
moved = {k: v for k, v in p.measured.items() if k in LIMIT_METRICS}
|
|
159
|
+
metric = max(moved, key=lambda k: moved[k]) if moved else 'bytes_in'
|
|
160
|
+
detail: LimitDetail = {'measured': moved.get(metric, 0), 'limit': p.limits.get(metric, 0), 'claimed': None}
|
|
161
|
+
why = 'traffic measured and no receipt: active silence'
|
|
162
|
+
else:
|
|
163
|
+
metric, detail = worst
|
|
164
|
+
why = '; '.join(str(w) for w in p.why)[:200]
|
|
165
|
+
body = complaint.notice(p.severity, p.grant, p.epoch, metric, detail, self.clock(),
|
|
166
|
+
p.window_seconds, description=why)
|
|
167
|
+
# ref builds a typed body; the DIDComm envelope takes the plain mapping.
|
|
168
|
+
msg = complaint.message('notice', dict(body), self.ks.did, _did(p.peer), mid='complaint-%s' % secrets.token_hex(4))
|
|
169
|
+
self.wire_client.send(p.peer, msg, stored=True)
|
|
170
|
+
p.notice_at, p.thid = self.clock(), msg['id']
|
|
171
|
+
self._persist(p)
|