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,478 @@
|
|
|
1
|
+
"""Transit Reflector Manager, Capacity Shed Handover & MoQ Ring Buffer (Phase 3).
|
|
2
|
+
|
|
3
|
+
Coordinates:
|
|
4
|
+
- Automated origin-to-supernode handover driven by `planes.peering.capacity`
|
|
5
|
+
shed transitions (`narrow_scope` / `amend_down` vs. `restore_below`).
|
|
6
|
+
- Real-time Media-over-QUIC (MoQ) / FSP datagram reflection on port 30820 with
|
|
7
|
+
a bounded drop-on-congestion ring buffer.
|
|
8
|
+
- Soft debt ceiling degradation (`degraded-audio-only`) when a pro-bono origin
|
|
9
|
+
exceeds `max_unsettled_transit_bytes`.
|
|
10
|
+
- Rhio NATS JetStream cluster fanout (`fleetmesh.video.<stream_id>`).
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import collections
|
|
15
|
+
import dataclasses
|
|
16
|
+
import hashlib
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
19
|
+
from typing import Any, Callable, Deque, Mapping, Optional, Sequence, TypedDict
|
|
20
|
+
|
|
21
|
+
from fleetmesh_core.crypto import crypto
|
|
22
|
+
from fleetmesh_core.wire.cbor import cbor_decode, cbor_encode
|
|
23
|
+
from fleetmesh_core.wire.video_transit import (
|
|
24
|
+
COUNTER_TRANSIT_BYTES_OUT,
|
|
25
|
+
PORT_VIDEO_STREAM,
|
|
26
|
+
TransitCloseBody,
|
|
27
|
+
TransitGrantBody,
|
|
28
|
+
TransitMode,
|
|
29
|
+
TransitReceiptBody,
|
|
30
|
+
TransitSolicitationBody,
|
|
31
|
+
receipt_link,
|
|
32
|
+
)
|
|
33
|
+
from .ledger import TransitLedgerStore
|
|
34
|
+
from .packager import VideoStreamPackager
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MoQFrameDict(TypedDict, total=False):
|
|
38
|
+
stream_id: str
|
|
39
|
+
broadcaster: str
|
|
40
|
+
group_id: int
|
|
41
|
+
object_id: int
|
|
42
|
+
track: str
|
|
43
|
+
is_keyframe: bool
|
|
44
|
+
is_audio_only: bool
|
|
45
|
+
payload: bytes
|
|
46
|
+
sha256: str
|
|
47
|
+
timestamp: int
|
|
48
|
+
sig: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ReflectionResult(TypedDict):
|
|
52
|
+
accepted: bool
|
|
53
|
+
forwarded: bool
|
|
54
|
+
degraded_audio_only: bool
|
|
55
|
+
dropped_reason: str
|
|
56
|
+
bytes_fanned_out: int
|
|
57
|
+
ring_depth: int
|
|
58
|
+
rhio_published: bool
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclasses.dataclass(frozen=True)
|
|
62
|
+
class MoQFrame:
|
|
63
|
+
"""Real-time interactive media frame for FIPS FSP (port 30820) and Iroh UDP streams."""
|
|
64
|
+
stream_id: str
|
|
65
|
+
broadcaster: str
|
|
66
|
+
group_id: int
|
|
67
|
+
object_id: int
|
|
68
|
+
track: str = 'video'
|
|
69
|
+
is_keyframe: bool = False
|
|
70
|
+
is_audio_only: bool = False
|
|
71
|
+
payload: bytes = b''
|
|
72
|
+
sha256: str = ''
|
|
73
|
+
timestamp: int = 0
|
|
74
|
+
sig: str = ''
|
|
75
|
+
|
|
76
|
+
def compute_sha256(self) -> str:
|
|
77
|
+
return hashlib.sha256(self.payload).hexdigest()
|
|
78
|
+
|
|
79
|
+
def signing_digest(self) -> bytes:
|
|
80
|
+
meta = {
|
|
81
|
+
'stream_id': self.stream_id,
|
|
82
|
+
'broadcaster': self.broadcaster.lower(),
|
|
83
|
+
'group_id': int(self.group_id),
|
|
84
|
+
'object_id': int(self.object_id),
|
|
85
|
+
'track': self.track,
|
|
86
|
+
'is_keyframe': bool(self.is_keyframe),
|
|
87
|
+
'is_audio_only': bool(self.is_audio_only),
|
|
88
|
+
'sha256': self.sha256.lower() or self.compute_sha256(),
|
|
89
|
+
'timestamp': int(self.timestamp),
|
|
90
|
+
}
|
|
91
|
+
return hashlib.sha256(crypto.canonical(meta)).digest()
|
|
92
|
+
|
|
93
|
+
def sign(self, sk_hex: str) -> 'MoQFrame':
|
|
94
|
+
digest_hex = self.sha256.lower() or self.compute_sha256()
|
|
95
|
+
with_hash = dataclasses.replace(self, sha256=digest_hex)
|
|
96
|
+
sig_hex = crypto.schnorr_sign(with_hash.signing_digest(), sk_hex)
|
|
97
|
+
return dataclasses.replace(with_hash, sig=sig_hex)
|
|
98
|
+
|
|
99
|
+
def verify_integrity(self) -> bool:
|
|
100
|
+
"""Verify payload SHA-256 and broadcaster BIP-340 Schnorr signature.
|
|
101
|
+
Required security boundary for unauthenticated UDP rungs (IrohLiveEngine).
|
|
102
|
+
"""
|
|
103
|
+
expected_hash = self.compute_sha256()
|
|
104
|
+
if self.sha256 and self.sha256.lower() != expected_hash:
|
|
105
|
+
return False
|
|
106
|
+
if not self.sig or not self.broadcaster or len(self.sig) != 128:
|
|
107
|
+
return False
|
|
108
|
+
try:
|
|
109
|
+
return crypto.schnorr_verify(self.signing_digest(), self.broadcaster.lower(), self.sig)
|
|
110
|
+
except (ValueError, TypeError):
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
def to_cbor(self) -> bytes:
|
|
114
|
+
d: MoQFrameDict = {
|
|
115
|
+
'stream_id': self.stream_id,
|
|
116
|
+
'broadcaster': self.broadcaster.lower(),
|
|
117
|
+
'group_id': int(self.group_id),
|
|
118
|
+
'object_id': int(self.object_id),
|
|
119
|
+
'track': self.track,
|
|
120
|
+
'is_keyframe': bool(self.is_keyframe),
|
|
121
|
+
'is_audio_only': bool(self.is_audio_only),
|
|
122
|
+
'payload': self.payload,
|
|
123
|
+
'sha256': self.sha256.lower() or self.compute_sha256(),
|
|
124
|
+
'timestamp': int(self.timestamp),
|
|
125
|
+
'sig': self.sig.lower(),
|
|
126
|
+
}
|
|
127
|
+
return cbor_encode(d, canonical=True)
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def from_cbor(cls, data: bytes) -> 'MoQFrame':
|
|
131
|
+
decoded, _ = cbor_decode(data)
|
|
132
|
+
if not isinstance(decoded, Mapping):
|
|
133
|
+
raise ValueError('Decoded MoQFrame CBOR is not a mapping')
|
|
134
|
+
payload_raw = decoded.get('payload', b'')
|
|
135
|
+
payload_bytes = bytes(payload_raw) if isinstance(payload_raw, (bytes, bytearray)) else b''
|
|
136
|
+
return cls(
|
|
137
|
+
stream_id=str(decoded.get('stream_id', '')),
|
|
138
|
+
broadcaster=str(decoded.get('broadcaster', '')).lower(),
|
|
139
|
+
group_id=int(decoded.get('group_id', 0)),
|
|
140
|
+
object_id=int(decoded.get('object_id', 0)),
|
|
141
|
+
track=str(decoded.get('track', 'video')),
|
|
142
|
+
is_keyframe=bool(decoded.get('is_keyframe', False)),
|
|
143
|
+
is_audio_only=bool(decoded.get('is_audio_only', False)),
|
|
144
|
+
payload=payload_bytes,
|
|
145
|
+
sha256=str(decoded.get('sha256', '')).lower(),
|
|
146
|
+
timestamp=int(decoded.get('timestamp', 0)),
|
|
147
|
+
sig=str(decoded.get('sig', '')).lower(),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class TransitReflectorManager:
|
|
152
|
+
"""Manages automated capacity shed handovers, MoQ ring-buffer reflection,
|
|
153
|
+
Rhio NATS JetStream cluster fanout, and bilateral transit metering.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
def __init__(
|
|
157
|
+
self,
|
|
158
|
+
ledger: Optional[TransitLedgerStore] = None,
|
|
159
|
+
packager: Optional[VideoStreamPackager] = None,
|
|
160
|
+
ring_capacity: int = 64,
|
|
161
|
+
rhio_client: Optional[Any] = None,
|
|
162
|
+
fips_transport: Optional[Any] = None,
|
|
163
|
+
iroh_engine: Optional[Any] = None,
|
|
164
|
+
clock: Optional[Callable[[], int]] = None,
|
|
165
|
+
strain_ledger: Optional[Any] = None,
|
|
166
|
+
) -> None:
|
|
167
|
+
# An injected clock, defaulting to wall time, so a suite can move time
|
|
168
|
+
# at the boundaries this code decides on. The compartment passes its own.
|
|
169
|
+
self._clock: Callable[[], int] = clock or (lambda: int(time.time()))
|
|
170
|
+
# MoQ frames a transport would not take, per transport. A dropped
|
|
171
|
+
# frame in live video is ordinary and not worth a refusal each, but
|
|
172
|
+
# these were discarded entirely, so a subscriber whose transport was
|
|
173
|
+
# wholly broken looked exactly like one receiving everything.
|
|
174
|
+
self.frames_dropped: dict[str, int] = {}
|
|
175
|
+
self.ledger = ledger or TransitLedgerStore(':memory:')
|
|
176
|
+
self.packager = packager
|
|
177
|
+
self.ring_capacity = max(4, int(ring_capacity))
|
|
178
|
+
self.rhio_client = rhio_client
|
|
179
|
+
self.fips_transport = fips_transport
|
|
180
|
+
self.iroh_engine = iroh_engine
|
|
181
|
+
self.strain_ledger = strain_ledger
|
|
182
|
+
|
|
183
|
+
self._lock = threading.RLock()
|
|
184
|
+
# stream_id -> active TransitGrantBody
|
|
185
|
+
self._active_handovers: dict[str, TransitGrantBody] = {}
|
|
186
|
+
# stream_id -> bounded drop-on-congestion ring buffer of MoQFrames
|
|
187
|
+
self._rings: dict[str, Deque[MoQFrame]] = {}
|
|
188
|
+
# grant_id -> cumulative transit_bytes_out metered by reflector
|
|
189
|
+
self._metered_bytes: dict[str, int] = {}
|
|
190
|
+
|
|
191
|
+
# ---- Handshake Helpers --------------------------------------------------
|
|
192
|
+
|
|
193
|
+
def build_solicitation(
|
|
194
|
+
self,
|
|
195
|
+
stream_id: str,
|
|
196
|
+
broadcaster: str,
|
|
197
|
+
bitrate_kbps: int = 4500,
|
|
198
|
+
max_transit_bytes: int = 2 * 1024 * 1024 * 1024,
|
|
199
|
+
mode: TransitMode = 'pro-bono',
|
|
200
|
+
codecs: Sequence[str] = ('avc1.64001f', 'mp4a.40.2'),
|
|
201
|
+
now: Optional[int] = None,
|
|
202
|
+
) -> TransitSolicitationBody:
|
|
203
|
+
now_val = self._clock() if now is None else int(now)
|
|
204
|
+
return {
|
|
205
|
+
'stream_id': stream_id,
|
|
206
|
+
'broadcaster': broadcaster.lower(),
|
|
207
|
+
'bitrate_kbps': int(bitrate_kbps),
|
|
208
|
+
'max_transit_bytes': int(max_transit_bytes),
|
|
209
|
+
'mode': mode,
|
|
210
|
+
'codecs': list(codecs),
|
|
211
|
+
'at': now_val,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
def accept_solicitation(
|
|
215
|
+
self,
|
|
216
|
+
solicitation: TransitSolicitationBody,
|
|
217
|
+
operator_pk: str,
|
|
218
|
+
operator_sk: str,
|
|
219
|
+
reflector_endpoints: Sequence[str],
|
|
220
|
+
window_seconds: int = 600,
|
|
221
|
+
receipt_cadence_seconds: int = 60,
|
|
222
|
+
max_unsettled_transit_bytes: int = 500 * 1024 * 1024,
|
|
223
|
+
now: Optional[int] = None,
|
|
224
|
+
) -> TransitGrantBody:
|
|
225
|
+
"""Operator supernode accepts a transit solicitation and issues a signed grant."""
|
|
226
|
+
return self.ledger.issue_grant(
|
|
227
|
+
stream_id=solicitation['stream_id'],
|
|
228
|
+
broadcaster=solicitation['broadcaster'],
|
|
229
|
+
operator_pk=operator_pk,
|
|
230
|
+
operator_sk=operator_sk,
|
|
231
|
+
window_seconds=window_seconds,
|
|
232
|
+
receipt_cadence_seconds=receipt_cadence_seconds,
|
|
233
|
+
max_unsettled_transit_bytes=max_unsettled_transit_bytes,
|
|
234
|
+
reflector_endpoints=reflector_endpoints,
|
|
235
|
+
mode=solicitation.get('mode', 'pro-bono'),
|
|
236
|
+
now=now,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# ---- Capacity Shed Ladder Handover Automation ---------------------------
|
|
240
|
+
|
|
241
|
+
def is_handover_active(self, stream_id: str) -> bool:
|
|
242
|
+
"""Return True if `stream_id` currently has an active transit reflector handover."""
|
|
243
|
+
with self._lock:
|
|
244
|
+
return stream_id in self._active_handovers
|
|
245
|
+
|
|
246
|
+
def active_sessions(self) -> dict[str, TransitGrantBody]:
|
|
247
|
+
"""Return a snapshot of active stream_id -> TransitGrantBody handovers."""
|
|
248
|
+
with self._lock:
|
|
249
|
+
return dict(self._active_handovers)
|
|
250
|
+
|
|
251
|
+
def engage_handover(self, stream_id: str, grant: TransitGrantBody) -> list[str]:
|
|
252
|
+
"""Engage transit handover for `stream_id` using `grant`.
|
|
253
|
+
Updates VideoStreamPackager's active reflector endpoints so HTTP requests
|
|
254
|
+
receive 307 Temporary Redirects and manifests advertise the reflector.
|
|
255
|
+
"""
|
|
256
|
+
self.ledger.save_grant(grant, status='live')
|
|
257
|
+
endpoints = list(grant.get('reflector_endpoints', []))
|
|
258
|
+
with self._lock:
|
|
259
|
+
self._active_handovers[stream_id] = grant
|
|
260
|
+
if self.packager is not None:
|
|
261
|
+
try:
|
|
262
|
+
self.packager.update_transit_endpoints(stream_id, endpoints)
|
|
263
|
+
except KeyError:
|
|
264
|
+
pass
|
|
265
|
+
return endpoints
|
|
266
|
+
|
|
267
|
+
def release_handover(
|
|
268
|
+
self,
|
|
269
|
+
stream_id: str,
|
|
270
|
+
window: int,
|
|
271
|
+
sk_hex: str,
|
|
272
|
+
reason: str = 'capacity-restored',
|
|
273
|
+
now: Optional[int] = None,
|
|
274
|
+
) -> Optional[ tuple[TransitReceiptBody, TransitCloseBody]]:
|
|
275
|
+
"""Release transit handover once Capacity pressure drops below `restore_below`.
|
|
276
|
+
Emits a final cumulative TransitReceiptBody and TransitCloseBody, and
|
|
277
|
+
clears redirect endpoints on the origin packager.
|
|
278
|
+
"""
|
|
279
|
+
now_val = self._clock() if now is None else int(now)
|
|
280
|
+
with self._lock:
|
|
281
|
+
grant = self._active_handovers.pop(stream_id, None)
|
|
282
|
+
if grant is None:
|
|
283
|
+
return None
|
|
284
|
+
grant_id = grant['grant_id']
|
|
285
|
+
total_bytes = self._metered_bytes.get(grant_id, 0)
|
|
286
|
+
|
|
287
|
+
receipt, head = self.ledger.record_outbound_receipt(
|
|
288
|
+
grant_id=grant_id,
|
|
289
|
+
stream_id=stream_id,
|
|
290
|
+
window=window,
|
|
291
|
+
transit_bytes_out=total_bytes,
|
|
292
|
+
sk_hex=sk_hex,
|
|
293
|
+
now=now_val,
|
|
294
|
+
)
|
|
295
|
+
close_body: TransitCloseBody = {
|
|
296
|
+
'grant': grant_id,
|
|
297
|
+
'stream_id': stream_id,
|
|
298
|
+
'window': int(window),
|
|
299
|
+
'final_receipt_head': head,
|
|
300
|
+
'counters': {COUNTER_TRANSIT_BYTES_OUT: total_bytes},
|
|
301
|
+
'reason': reason,
|
|
302
|
+
'at': now_val,
|
|
303
|
+
}
|
|
304
|
+
self.ledger.close_grant(grant_id)
|
|
305
|
+
with self._lock:
|
|
306
|
+
if not hasattr(self, '_last_release'):
|
|
307
|
+
self._last_release: dict[str, tuple[TransitReceiptBody, TransitCloseBody]] = {}
|
|
308
|
+
self._last_release[stream_id] = (receipt, close_body)
|
|
309
|
+
if self.packager is not None:
|
|
310
|
+
try:
|
|
311
|
+
self.packager.update_transit_endpoints(stream_id, ())
|
|
312
|
+
except KeyError:
|
|
313
|
+
pass
|
|
314
|
+
return receipt, close_body
|
|
315
|
+
|
|
316
|
+
def evaluate_capacity_handover(
|
|
317
|
+
self,
|
|
318
|
+
capacity_comp: Any,
|
|
319
|
+
stream_id: str,
|
|
320
|
+
standby_grant: TransitGrantBody,
|
|
321
|
+
window: int,
|
|
322
|
+
sk_hex: str,
|
|
323
|
+
now: Optional[int] = None,
|
|
324
|
+
) -> str:
|
|
325
|
+
"""Inspect the node's Capacity compartment shed state (`narrow_scope` /
|
|
326
|
+
`amend_down`) and automatically engage or release transit handover.
|
|
327
|
+
Returns 'engaged', 'released', 'active', or 'idle'.
|
|
328
|
+
"""
|
|
329
|
+
shed_active = bool(
|
|
330
|
+
(hasattr(capacity_comp, 'narrowing') and capacity_comp.narrowing())
|
|
331
|
+
or (hasattr(capacity_comp, 'amending') and capacity_comp.amending())
|
|
332
|
+
)
|
|
333
|
+
with self._lock:
|
|
334
|
+
currently_handed_over = stream_id in self._active_handovers
|
|
335
|
+
|
|
336
|
+
if shed_active and not currently_handed_over:
|
|
337
|
+
self.engage_handover(stream_id, standby_grant)
|
|
338
|
+
return 'engaged'
|
|
339
|
+
elif not shed_active and currently_handed_over:
|
|
340
|
+
self.release_handover(stream_id, window=window, sk_hex=sk_hex, now=now)
|
|
341
|
+
return 'released'
|
|
342
|
+
return 'active' if currently_handed_over else 'idle'
|
|
343
|
+
|
|
344
|
+
# ---- Real-Time MoQ Ring Buffer Reflection & Cluster Fanout --------------
|
|
345
|
+
|
|
346
|
+
def get_ring_frames(self, stream_id: str) -> list[MoQFrame]:
|
|
347
|
+
with self._lock:
|
|
348
|
+
ring = self._rings.get(stream_id)
|
|
349
|
+
return list(ring) if ring is not None else []
|
|
350
|
+
|
|
351
|
+
def reflect_moq_frame(
|
|
352
|
+
self,
|
|
353
|
+
frame: MoQFrame,
|
|
354
|
+
grant_id: Optional[str] = None,
|
|
355
|
+
subscribers: Sequence[str] = (),
|
|
356
|
+
) -> ReflectionResult:
|
|
357
|
+
"""Verify, buffer, and fan out a real-time MoQ frame across FIPS (port 30820),
|
|
358
|
+
Iroh UDP streams, and Rhio NATS JetStream cluster subjects.
|
|
359
|
+
"""
|
|
360
|
+
# 1. Cryptographic integrity check at the ingress door
|
|
361
|
+
if not frame.verify_integrity():
|
|
362
|
+
if self.strain_ledger is not None and hasattr(self.strain_ledger, 'record'):
|
|
363
|
+
self.strain_ledger.record('invalid_moq_frame', peer=frame.broadcaster)
|
|
364
|
+
return {
|
|
365
|
+
'accepted': False,
|
|
366
|
+
'forwarded': False,
|
|
367
|
+
'degraded_audio_only': False,
|
|
368
|
+
'dropped_reason': 'invalid_frame_signature_or_digest',
|
|
369
|
+
'bytes_fanned_out': 0,
|
|
370
|
+
'ring_depth': len(self.get_ring_frames(frame.stream_id)),
|
|
371
|
+
'rhio_published': False,
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
# 2. Check soft debt ceiling (`max_unsettled_transit_bytes`)
|
|
375
|
+
summary = self.ledger.debt_summary(frame.broadcaster, grant_id=grant_id)
|
|
376
|
+
degraded = summary['degraded_audio_only']
|
|
377
|
+
if degraded and not frame.is_audio_only:
|
|
378
|
+
return {
|
|
379
|
+
'accepted': True,
|
|
380
|
+
'forwarded': False,
|
|
381
|
+
'degraded_audio_only': True,
|
|
382
|
+
'dropped_reason': 'soft_debt_ceiling_audio_only_degradation',
|
|
383
|
+
'bytes_fanned_out': 0,
|
|
384
|
+
'ring_depth': len(self.get_ring_frames(frame.stream_id)),
|
|
385
|
+
'rhio_published': False,
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
# 3. Append to bounded drop-on-congestion ring buffer
|
|
389
|
+
with self._lock:
|
|
390
|
+
ring = self._rings.get(frame.stream_id)
|
|
391
|
+
if ring is None:
|
|
392
|
+
ring = collections.deque(maxlen=self.ring_capacity)
|
|
393
|
+
self._rings[frame.stream_id] = ring
|
|
394
|
+
ring.append(frame)
|
|
395
|
+
ring_depth = len(ring)
|
|
396
|
+
|
|
397
|
+
# 4. Meter outbound bytes across subscribers
|
|
398
|
+
fanout_multiplier = max(1, len(subscribers))
|
|
399
|
+
bytes_fanned = len(frame.payload) * fanout_multiplier
|
|
400
|
+
if grant_id is not None:
|
|
401
|
+
with self._lock:
|
|
402
|
+
self._metered_bytes[grant_id] = self._metered_bytes.get(grant_id, 0) + bytes_fanned
|
|
403
|
+
|
|
404
|
+
# 5. Fan out to FIPS native FSP port 30820 and Iroh UDP engine if attached
|
|
405
|
+
cbor_frame = frame.to_cbor()
|
|
406
|
+
for sub_peer in subscribers:
|
|
407
|
+
if self.fips_transport is not None and hasattr(self.fips_transport, 'send_datagram'):
|
|
408
|
+
try:
|
|
409
|
+
self.fips_transport.send_datagram(sub_peer, cbor_frame, port=PORT_VIDEO_STREAM)
|
|
410
|
+
except Exception:
|
|
411
|
+
self.frames_dropped['fips'] = self.frames_dropped.get('fips', 0) + 1
|
|
412
|
+
if self.iroh_engine is not None and hasattr(self.iroh_engine, 'send_stream_chunk'):
|
|
413
|
+
try:
|
|
414
|
+
self.iroh_engine.send_stream_chunk(sub_peer, cbor_frame)
|
|
415
|
+
except Exception:
|
|
416
|
+
self.frames_dropped['iroh'] = self.frames_dropped.get('iroh', 0) + 1
|
|
417
|
+
|
|
418
|
+
# 6. Fan out to Rhio NATS JetStream cluster (`fleetmesh.video.<stream_id>`)
|
|
419
|
+
rhio_published = False
|
|
420
|
+
if self.rhio_client is not None and hasattr(self.rhio_client, 'publish'):
|
|
421
|
+
try:
|
|
422
|
+
res = self.rhio_client.publish(
|
|
423
|
+
f"fleetmesh.video.{frame.stream_id}",
|
|
424
|
+
{
|
|
425
|
+
'stream_id': frame.stream_id,
|
|
426
|
+
'group_id': frame.group_id,
|
|
427
|
+
'object_id': frame.object_id,
|
|
428
|
+
'track': frame.track,
|
|
429
|
+
'is_audio_only': frame.is_audio_only,
|
|
430
|
+
'sha256': frame.sha256,
|
|
431
|
+
'size': len(frame.payload),
|
|
432
|
+
'timestamp': frame.timestamp,
|
|
433
|
+
},
|
|
434
|
+
)
|
|
435
|
+
rhio_published = bool(res.get('ok', True)) if isinstance(res, Mapping) else True
|
|
436
|
+
except Exception:
|
|
437
|
+
rhio_published = False
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
'accepted': True,
|
|
441
|
+
'forwarded': True,
|
|
442
|
+
'degraded_audio_only': degraded,
|
|
443
|
+
'dropped_reason': '',
|
|
444
|
+
'bytes_fanned_out': bytes_fanned,
|
|
445
|
+
'ring_depth': ring_depth,
|
|
446
|
+
'rhio_published': rhio_published,
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
def emit_metered_receipt(
|
|
450
|
+
self,
|
|
451
|
+
grant_id: str,
|
|
452
|
+
stream_id: str,
|
|
453
|
+
window: int,
|
|
454
|
+
sk_hex: str,
|
|
455
|
+
now: Optional[int] = None,
|
|
456
|
+
) -> tuple[TransitReceiptBody, str]:
|
|
457
|
+
"""Produce and persist a signed cumulative TransitReceiptBody for all
|
|
458
|
+
bytes metered under `grant_id` so far.
|
|
459
|
+
"""
|
|
460
|
+
with self._lock:
|
|
461
|
+
total = self._metered_bytes.get(grant_id, 0)
|
|
462
|
+
return self.ledger.record_outbound_receipt(
|
|
463
|
+
grant_id=grant_id,
|
|
464
|
+
stream_id=stream_id,
|
|
465
|
+
window=window,
|
|
466
|
+
transit_bytes_out=total,
|
|
467
|
+
sk_hex=sk_hex,
|
|
468
|
+
now=now,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
__all__ = [
|
|
473
|
+
'MoQFrameDict',
|
|
474
|
+
'ReflectionResult',
|
|
475
|
+
'MoQFrame',
|
|
476
|
+
'TransitReflectorManager',
|
|
477
|
+
'receipt_link',
|
|
478
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Store plane: sync, retention, projected, blob_wire, blob_replication."""
|
|
2
|
+
from . import blob_replication, blob_wire, projected, retention, sync
|
|
3
|
+
from .blob_replication.compartment import Replication
|
|
4
|
+
from .blob_wire.core import BlobWire, BlobWireConfig
|
|
5
|
+
from .projected.compartment import ProjectedDB, ProjectedDBConfig
|
|
6
|
+
from .retention.compartment import Retention, RetentionConfig
|
|
7
|
+
from .sync.core import SyncConfig, SyncPeer
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
'BlobWire',
|
|
11
|
+
'BlobWireConfig',
|
|
12
|
+
'ProjectedDB',
|
|
13
|
+
'ProjectedDBConfig',
|
|
14
|
+
'Replication',
|
|
15
|
+
'Retention',
|
|
16
|
+
'RetentionConfig',
|
|
17
|
+
'SyncConfig',
|
|
18
|
+
'SyncPeer',
|
|
19
|
+
'blob_replication',
|
|
20
|
+
'blob_wire',
|
|
21
|
+
'projected',
|
|
22
|
+
'retention',
|
|
23
|
+
'sync',
|
|
24
|
+
]
|