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,705 @@
|
|
|
1
|
+
"""Bilateral Transit IOU Ledger & Barter Settlement (Phase 4).
|
|
2
|
+
|
|
3
|
+
Implements Record-Then-Answer persistence for `video-transit/1.0` grants,
|
|
4
|
+
cumulative hash-chained receipts (`transit_bytes_out`), gap-tolerant receipt
|
|
5
|
+
verification, soft debt ceilings (`degraded-audio-only`), and bartered network
|
|
6
|
+
utility credits (Blob Shoring, Verified Compute / DVM, and DIDComm Mediation).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any, Callable, Literal, Mapping, Optional, Sequence, TypedDict
|
|
14
|
+
|
|
15
|
+
from fleetmesh_core.accounting import accounting
|
|
16
|
+
from node.compartment.schema import Column, Index, Table
|
|
17
|
+
from node.storage import SqliteStore
|
|
18
|
+
from fleetmesh_core.wire.video_transit import (
|
|
19
|
+
COUNTER_TRANSIT_BYTES_OUT,
|
|
20
|
+
TransitAckStatus,
|
|
21
|
+
TransitGrantBody,
|
|
22
|
+
TransitMode,
|
|
23
|
+
TransitReceiptAckBody,
|
|
24
|
+
TransitReceiptBody,
|
|
25
|
+
check_grant,
|
|
26
|
+
check_transit_close,
|
|
27
|
+
check_transit_receipt,
|
|
28
|
+
receipt_link,
|
|
29
|
+
register_video_transit_checks,
|
|
30
|
+
sign_transit_grant,
|
|
31
|
+
sign_transit_receipt,
|
|
32
|
+
verify_transit_grant,
|
|
33
|
+
verify_transit_receipt,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
UtilityKind = Literal['blob_shoring', 'verified_compute', 'dvm', 'mediation']
|
|
37
|
+
VALID_UTILITY_KINDS: tuple[str, ...] = ('blob_shoring', 'verified_compute', 'dvm', 'mediation')
|
|
38
|
+
|
|
39
|
+
# Default conversion rates from bartered network utility units to transit bytes
|
|
40
|
+
DEFAULT_UTILITY_BYTES_RATES: Mapping[str, int] = {
|
|
41
|
+
'blob_shoring': 1, # 1 byte shored = 1 byte transit credit
|
|
42
|
+
'verified_compute': 1024, # 1 compute fuel unit = 1 KB transit credit
|
|
43
|
+
'dvm': 1024 * 1024, # 1 DVM job unit = 1 MB transit credit
|
|
44
|
+
'mediation': 4096, # 1 mediated envelope byte/msg unit = 4 KB transit credit
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BarterCreditRow(TypedDict):
|
|
49
|
+
credit_id: str
|
|
50
|
+
debtor: str
|
|
51
|
+
creditor: str
|
|
52
|
+
utility_kind: str
|
|
53
|
+
units: int
|
|
54
|
+
equivalent_transit_bytes: int
|
|
55
|
+
proof_ref: str
|
|
56
|
+
created_at: int
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class DebtSummary(TypedDict):
|
|
60
|
+
debtor: str
|
|
61
|
+
gross_transit_bytes: int
|
|
62
|
+
bartered_bytes: int
|
|
63
|
+
cashu_settled_bytes: int
|
|
64
|
+
unsettled_bytes: int
|
|
65
|
+
max_unsettled_transit_bytes: int
|
|
66
|
+
degraded_audio_only: bool
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class OutboundChainState(TypedDict):
|
|
70
|
+
grant_id: str
|
|
71
|
+
window: int
|
|
72
|
+
seq: int
|
|
73
|
+
prev: str
|
|
74
|
+
heads: list[str]
|
|
75
|
+
sent_at: int
|
|
76
|
+
last_bytes_out: int
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_GRANT_COLS = ('grant_id, stream_id, broadcaster, operator, window_seconds, '
|
|
80
|
+
'receipt_cadence_seconds, max_unsettled_transit_bytes, mode, '
|
|
81
|
+
'reflector_endpoints_json, status, created_at, sig')
|
|
82
|
+
_CHAIN_COLS = 'grant_id, window, seq, prev, heads_json, sent_at, last_bytes_out'
|
|
83
|
+
_CREDIT_COLS = ('credit_id, debtor, creditor, utility_kind, units, '
|
|
84
|
+
'equivalent_transit_bytes, proof_ref, created_at')
|
|
85
|
+
"""The column orders the positional reads below depend on.
|
|
86
|
+
|
|
87
|
+
The selects said `SELECT *` and the rows were read by name through a
|
|
88
|
+
`sqlite3.Row` factory - which is one of the driver leaks the dialect layer
|
|
89
|
+
exists to remove, since a second driver has its own row type. The orders are
|
|
90
|
+
written down once here instead.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TransitLedgerStore(SqliteStore):
|
|
95
|
+
"""Transit grants, receipt chains and bartered utility repayments.
|
|
96
|
+
|
|
97
|
+
`DURABILITY = 'commit'` is declared, not decorative: this store holds
|
|
98
|
+
bilateral hash-chained IOUs, and `record_outbound_receipt` commits the
|
|
99
|
+
chain advance *before* the receipt goes on the wire - Record-Then-Answer.
|
|
100
|
+
The class docstring claimed that durability while the store declared no
|
|
101
|
+
class at all and was not a `BaseStore`, which is the shape of defect R0
|
|
102
|
+
(I15) was: a meter whose state was asserted durable, lost across a
|
|
103
|
+
restart, and cost a peer its standing. Naming it is what lets
|
|
104
|
+
test_durability_boundary hold it to the claim.
|
|
105
|
+
|
|
106
|
+
A `SqliteStore` rather than a hand-rolled connection, so the base owns the
|
|
107
|
+
connection, the lock, the per-store schema version, transaction control and
|
|
108
|
+
the dialect - see docs/storage-backends.md.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
DURABILITY = 'commit'
|
|
112
|
+
|
|
113
|
+
SCHEMA = (
|
|
114
|
+
Table('transit_grants', (
|
|
115
|
+
Column('grant_id', 'TEXT', primary_key=True),
|
|
116
|
+
Column('stream_id', 'TEXT', not_null=True),
|
|
117
|
+
Column('broadcaster', 'TEXT', not_null=True),
|
|
118
|
+
Column('operator', 'TEXT', not_null=True),
|
|
119
|
+
Column('window_seconds', 'INTEGER', not_null=True),
|
|
120
|
+
Column('receipt_cadence_seconds', 'INTEGER', not_null=True),
|
|
121
|
+
Column('max_unsettled_transit_bytes', 'INTEGER', not_null=True),
|
|
122
|
+
Column('mode', 'TEXT', not_null=True),
|
|
123
|
+
Column('reflector_endpoints_json', 'TEXT', not_null=True),
|
|
124
|
+
Column('status', 'TEXT', not_null=True),
|
|
125
|
+
Column('created_at', 'INTEGER', not_null=True),
|
|
126
|
+
Column('sig', 'TEXT', not_null=True),
|
|
127
|
+
)),
|
|
128
|
+
Table('transit_chains_out', (
|
|
129
|
+
Column('grant_id', 'TEXT', not_null=True),
|
|
130
|
+
Column('window', 'INTEGER', not_null=True),
|
|
131
|
+
Column('seq', 'INTEGER', not_null=True),
|
|
132
|
+
Column('prev', 'TEXT', not_null=True),
|
|
133
|
+
Column('heads_json', 'TEXT', not_null=True),
|
|
134
|
+
Column('sent_at', 'INTEGER', not_null=True),
|
|
135
|
+
Column('last_bytes_out', 'INTEGER', not_null=True),
|
|
136
|
+
), primary_key=('grant_id', 'window')),
|
|
137
|
+
Table('transit_receipts_in', (
|
|
138
|
+
Column('grant_id', 'TEXT', not_null=True),
|
|
139
|
+
Column('window', 'INTEGER', not_null=True),
|
|
140
|
+
Column('seq', 'INTEGER', not_null=True),
|
|
141
|
+
Column('head', 'TEXT', not_null=True),
|
|
142
|
+
Column('broadcaster', 'TEXT', not_null=True),
|
|
143
|
+
Column('transit_bytes_out', 'INTEGER', not_null=True),
|
|
144
|
+
Column('body_json', 'TEXT', not_null=True),
|
|
145
|
+
Column('received_at', 'INTEGER', not_null=True),
|
|
146
|
+
), primary_key=('grant_id', 'window', 'seq')),
|
|
147
|
+
Table('transit_barter_credits', (
|
|
148
|
+
Column('credit_id', 'TEXT', primary_key=True),
|
|
149
|
+
Column('debtor', 'TEXT', not_null=True),
|
|
150
|
+
Column('creditor', 'TEXT', not_null=True),
|
|
151
|
+
Column('utility_kind', 'TEXT', not_null=True),
|
|
152
|
+
Column('units', 'INTEGER', not_null=True),
|
|
153
|
+
Column('equivalent_transit_bytes', 'INTEGER', not_null=True),
|
|
154
|
+
Column('proof_ref', 'TEXT', not_null=True),
|
|
155
|
+
Column('created_at', 'INTEGER', not_null=True),
|
|
156
|
+
), indices=(
|
|
157
|
+
Index('idx_transit_barter_debtor', ('debtor',)),
|
|
158
|
+
)),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def __init__(self, db_path: str = ':memory:',
|
|
162
|
+
clock: Optional[Callable[[], int]] = None) -> None:
|
|
163
|
+
register_video_transit_checks()
|
|
164
|
+
# An injected clock, defaulting to wall time. The money path - windows,
|
|
165
|
+
# receipt cadence, debt ceilings - is decided at time boundaries, and a
|
|
166
|
+
# direct time.time() makes those boundaries untestable. The compartment
|
|
167
|
+
# passes its own clock down, so a suite that moves the clock moves this too.
|
|
168
|
+
self._clock: Callable[[], int] = clock or (lambda: int(time.time()))
|
|
169
|
+
self.findings: list[tuple[str, int, str, str]] = []
|
|
170
|
+
self.receipt_gaps: list[tuple[str, int, int, int, int]] = []
|
|
171
|
+
super().__init__(db_path)
|
|
172
|
+
|
|
173
|
+
def close(self) -> None:
|
|
174
|
+
super().close()
|
|
175
|
+
|
|
176
|
+
# ---- Grant Issuance & Storage -------------------------------------------
|
|
177
|
+
|
|
178
|
+
def issue_grant(
|
|
179
|
+
self,
|
|
180
|
+
stream_id: str,
|
|
181
|
+
broadcaster: str,
|
|
182
|
+
operator_pk: str,
|
|
183
|
+
operator_sk: str,
|
|
184
|
+
window_seconds: int = 600,
|
|
185
|
+
receipt_cadence_seconds: int = 60,
|
|
186
|
+
max_unsettled_transit_bytes: int = 500 * 1024 * 1024,
|
|
187
|
+
reflector_endpoints: Sequence[str] = (),
|
|
188
|
+
mode: TransitMode = 'pro-bono',
|
|
189
|
+
now: Optional[int] = None,
|
|
190
|
+
) -> TransitGrantBody:
|
|
191
|
+
"""Create, sign, and durably commit a transit grant before wire emission."""
|
|
192
|
+
now_val = self._clock() if now is None else int(now)
|
|
193
|
+
seed = f"{stream_id}:{broadcaster.lower()}:{operator_pk.lower()}:{now_val}".encode('utf-8')
|
|
194
|
+
grant_id = hashlib.sha256(seed).hexdigest()
|
|
195
|
+
unsigned: TransitGrantBody = {
|
|
196
|
+
'grant_id': grant_id,
|
|
197
|
+
'stream_id': stream_id,
|
|
198
|
+
'broadcaster': broadcaster.lower(),
|
|
199
|
+
'operator': operator_pk.lower(),
|
|
200
|
+
'window_seconds': int(window_seconds),
|
|
201
|
+
'receipt_cadence_seconds': int(receipt_cadence_seconds),
|
|
202
|
+
'max_unsettled_transit_bytes': int(max_unsettled_transit_bytes),
|
|
203
|
+
'reflector_endpoints': list(reflector_endpoints),
|
|
204
|
+
'mode': mode,
|
|
205
|
+
'at': now_val,
|
|
206
|
+
}
|
|
207
|
+
signed = sign_transit_grant(unsigned, operator_sk)
|
|
208
|
+
self.save_grant(signed)
|
|
209
|
+
return signed
|
|
210
|
+
|
|
211
|
+
def save_grant(self, grant: TransitGrantBody, status: str = 'live') -> None:
|
|
212
|
+
"""Persist a verified TransitGrantBody to SQLite."""
|
|
213
|
+
why = check_grant(grant)
|
|
214
|
+
if why is not None:
|
|
215
|
+
raise ValueError(f"Invalid transit grant: {why}")
|
|
216
|
+
operator_pk = grant.get('operator', '')
|
|
217
|
+
if not verify_transit_grant(grant, operator_pk):
|
|
218
|
+
raise ValueError("Transit grant signature verification failed")
|
|
219
|
+
|
|
220
|
+
with self._lock:
|
|
221
|
+
self.execute(
|
|
222
|
+
"""
|
|
223
|
+
INSERT OR REPLACE INTO transit_grants (
|
|
224
|
+
grant_id, stream_id, broadcaster, operator,
|
|
225
|
+
window_seconds, receipt_cadence_seconds, max_unsettled_transit_bytes,
|
|
226
|
+
mode, reflector_endpoints_json, status, created_at, sig
|
|
227
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
228
|
+
""",
|
|
229
|
+
(
|
|
230
|
+
grant['grant_id'],
|
|
231
|
+
grant['stream_id'],
|
|
232
|
+
grant['broadcaster'].lower(),
|
|
233
|
+
grant['operator'].lower(),
|
|
234
|
+
int(grant['window_seconds']),
|
|
235
|
+
int(grant['receipt_cadence_seconds']),
|
|
236
|
+
int(grant['max_unsettled_transit_bytes']),
|
|
237
|
+
grant.get('mode', 'pro-bono'),
|
|
238
|
+
json.dumps(list(grant.get('reflector_endpoints', []))),
|
|
239
|
+
status,
|
|
240
|
+
int(grant.get('at', self._clock())),
|
|
241
|
+
grant['sig'],
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
self.commit()
|
|
245
|
+
|
|
246
|
+
def get_grant(self, grant_id: str) -> Optional[TransitGrantBody]:
|
|
247
|
+
with self._lock:
|
|
248
|
+
row = self._one(
|
|
249
|
+
'SELECT ' + _GRANT_COLS + ' FROM transit_grants WHERE grant_id = ?',
|
|
250
|
+
(grant_id,),
|
|
251
|
+
)
|
|
252
|
+
if row is None:
|
|
253
|
+
return None
|
|
254
|
+
mode_val: TransitMode = 'commercial' if row[7] == 'commercial' else 'pro-bono'
|
|
255
|
+
return {
|
|
256
|
+
'grant_id': str(row[0]),
|
|
257
|
+
'stream_id': str(row[1]),
|
|
258
|
+
'broadcaster': str(row[2]),
|
|
259
|
+
'operator': str(row[3]),
|
|
260
|
+
'window_seconds': int(row[4]),
|
|
261
|
+
'receipt_cadence_seconds': int(row[5]),
|
|
262
|
+
'max_unsettled_transit_bytes': int(row[6]),
|
|
263
|
+
'mode': mode_val,
|
|
264
|
+
'reflector_endpoints': list(json.loads(row[8])),
|
|
265
|
+
'at': int(row[10]),
|
|
266
|
+
'sig': str(row[11]),
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
def close_grant(self, grant_id: str) -> None:
|
|
270
|
+
with self._lock:
|
|
271
|
+
self.execute(
|
|
272
|
+
"UPDATE transit_grants SET status = 'closed' WHERE grant_id = ?",
|
|
273
|
+
(grant_id,),
|
|
274
|
+
)
|
|
275
|
+
self.commit()
|
|
276
|
+
|
|
277
|
+
# ---- Record-Then-Answer Outbound Receipt Chain --------------------------
|
|
278
|
+
|
|
279
|
+
def get_outbound_chain(self, grant_id: str, window: int) -> Optional[OutboundChainState]:
|
|
280
|
+
with self._lock:
|
|
281
|
+
row = self._one(
|
|
282
|
+
'SELECT ' + _CHAIN_COLS + ' FROM transit_chains_out WHERE grant_id = ? AND window = ?',
|
|
283
|
+
(grant_id, int(window)),
|
|
284
|
+
)
|
|
285
|
+
if row is None:
|
|
286
|
+
return None
|
|
287
|
+
return {
|
|
288
|
+
'grant_id': str(row[0]),
|
|
289
|
+
'window': int(row[1]),
|
|
290
|
+
'seq': int(row[2]),
|
|
291
|
+
'prev': str(row[3]),
|
|
292
|
+
'heads': list(json.loads(row[4])),
|
|
293
|
+
'sent_at': int(row[5]),
|
|
294
|
+
'last_bytes_out': int(row[6]),
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
def record_outbound_receipt(
|
|
298
|
+
self,
|
|
299
|
+
grant_id: str,
|
|
300
|
+
stream_id: str,
|
|
301
|
+
window: int,
|
|
302
|
+
transit_bytes_out: int,
|
|
303
|
+
sk_hex: str,
|
|
304
|
+
now: Optional[int] = None,
|
|
305
|
+
) -> tuple[TransitReceiptBody, str]:
|
|
306
|
+
"""Produce, sign, and durably persist the next cumulative TransitReceiptBody
|
|
307
|
+
in the hash chain BEFORE it is emitted on the wire (Record-Then-Answer).
|
|
308
|
+
"""
|
|
309
|
+
now_val = self._clock() if now is None else int(now)
|
|
310
|
+
with self._lock:
|
|
311
|
+
state = self.get_outbound_chain(grant_id, window)
|
|
312
|
+
if state is None:
|
|
313
|
+
seq = 0
|
|
314
|
+
prev = accounting.ZERO
|
|
315
|
+
heads: list[str] = []
|
|
316
|
+
last_bytes = 0
|
|
317
|
+
else:
|
|
318
|
+
seq = state['seq']
|
|
319
|
+
prev = state['prev']
|
|
320
|
+
heads = list(state['heads'])
|
|
321
|
+
last_bytes = state['last_bytes_out']
|
|
322
|
+
|
|
323
|
+
if transit_bytes_out < last_bytes:
|
|
324
|
+
raise ValueError(
|
|
325
|
+
f"Cumulative transit_bytes_out cannot decrease ({transit_bytes_out} < {last_bytes})"
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
unsigned: TransitReceiptBody = {
|
|
329
|
+
'grant': grant_id,
|
|
330
|
+
'stream_id': stream_id,
|
|
331
|
+
'window': int(window),
|
|
332
|
+
'seq': seq,
|
|
333
|
+
'prev': prev,
|
|
334
|
+
'counters': {COUNTER_TRANSIT_BYTES_OUT: int(transit_bytes_out)},
|
|
335
|
+
'at': now_val,
|
|
336
|
+
}
|
|
337
|
+
signed = sign_transit_receipt(unsigned, sk_hex)
|
|
338
|
+
head = receipt_link(signed)
|
|
339
|
+
heads.append(head)
|
|
340
|
+
|
|
341
|
+
# Record-Then-Answer: commit chain advancement to SQLite BEFORE returning
|
|
342
|
+
self.execute(
|
|
343
|
+
"""
|
|
344
|
+
INSERT OR REPLACE INTO transit_chains_out (
|
|
345
|
+
grant_id, window, seq, prev, heads_json, sent_at, last_bytes_out
|
|
346
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
347
|
+
""",
|
|
348
|
+
(
|
|
349
|
+
grant_id,
|
|
350
|
+
int(window),
|
|
351
|
+
seq + 1,
|
|
352
|
+
head,
|
|
353
|
+
json.dumps(heads),
|
|
354
|
+
now_val,
|
|
355
|
+
int(transit_bytes_out),
|
|
356
|
+
),
|
|
357
|
+
)
|
|
358
|
+
self.commit()
|
|
359
|
+
return signed, head
|
|
360
|
+
|
|
361
|
+
# ---- Inbound Receipt Verification & Gap-Tolerant Chain Intake -----------
|
|
362
|
+
|
|
363
|
+
def get_inbound_receipts(self, grant_id: str, window: int) -> list[TransitReceiptBody]:
|
|
364
|
+
with self._lock:
|
|
365
|
+
rows = self.execute(
|
|
366
|
+
'SELECT body_json FROM transit_receipts_in WHERE grant_id = ? AND window = ? ORDER BY seq ASC',
|
|
367
|
+
(grant_id, int(window)),
|
|
368
|
+
).fetchall()
|
|
369
|
+
out: list[TransitReceiptBody] = []
|
|
370
|
+
for r in rows:
|
|
371
|
+
out.append(json.loads(r[0]))
|
|
372
|
+
return out
|
|
373
|
+
|
|
374
|
+
def on_inbound_receipt(
|
|
375
|
+
self,
|
|
376
|
+
signer_pubkey: str,
|
|
377
|
+
body: Mapping[str, Any],
|
|
378
|
+
now: Optional[int] = None,
|
|
379
|
+
) -> TransitReceiptAckBody:
|
|
380
|
+
"""Validate and record an inbound TransitReceiptBody.
|
|
381
|
+
|
|
382
|
+
Enforces:
|
|
383
|
+
- Door shape check (`check_transit_receipt`)
|
|
384
|
+
- Schnorr signature verification (`verify_transit_receipt`)
|
|
385
|
+
- Gap tolerance vs. rewrite detection (matching `budget/receipts.py`)
|
|
386
|
+
- Monotonic cumulative `transit_bytes_out` counters
|
|
387
|
+
- Soft debt ceiling evaluation (`degraded-audio-only` when unsettled debt
|
|
388
|
+
reaches `max_unsettled_transit_bytes`).
|
|
389
|
+
"""
|
|
390
|
+
now_val = self._clock() if now is None else int(now)
|
|
391
|
+
why = check_transit_receipt(body)
|
|
392
|
+
if why is not None:
|
|
393
|
+
seq_raw = body.get('seq') if isinstance(body, Mapping) else 0
|
|
394
|
+
seq_val = seq_raw if isinstance(seq_raw, int) and not isinstance(seq_raw, bool) else 0
|
|
395
|
+
return {
|
|
396
|
+
'grant': str(body.get('grant', accounting.ZERO)),
|
|
397
|
+
'seq': seq_val,
|
|
398
|
+
'head': accounting.ZERO,
|
|
399
|
+
'status': 'divergent',
|
|
400
|
+
'unsettled_bytes': 0,
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
grant_id = str(body['grant'])
|
|
404
|
+
window = int(body['window'])
|
|
405
|
+
seq = int(body['seq'])
|
|
406
|
+
grant = self.get_grant(grant_id)
|
|
407
|
+
if grant is None:
|
|
408
|
+
return {
|
|
409
|
+
'grant': grant_id,
|
|
410
|
+
'seq': seq,
|
|
411
|
+
'head': accounting.ZERO,
|
|
412
|
+
'status': 'divergent',
|
|
413
|
+
'unsettled_bytes': 0,
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
broadcaster = grant['broadcaster']
|
|
417
|
+
with self._lock:
|
|
418
|
+
chain = self.get_inbound_receipts(grant_id, window)
|
|
419
|
+
expected_seq = int(chain[-1]['seq']) + 1 if chain else 0
|
|
420
|
+
expected_prev = receipt_link(chain[-1]) if chain else accounting.ZERO
|
|
421
|
+
problems: list[str] = []
|
|
422
|
+
|
|
423
|
+
if signer_pubkey.strip().lower() != broadcaster.strip().lower():
|
|
424
|
+
problems.append('receipt signer does not match grant broadcaster')
|
|
425
|
+
elif not verify_transit_receipt(body, broadcaster):
|
|
426
|
+
problems.append('no valid signature over receipt chain link')
|
|
427
|
+
|
|
428
|
+
gap = 0
|
|
429
|
+
if seq < expected_seq:
|
|
430
|
+
problems.append(f'seq {seq}, expected {expected_seq} or later (rewrite attempt)')
|
|
431
|
+
elif seq > expected_seq:
|
|
432
|
+
gap = seq - expected_seq
|
|
433
|
+
|
|
434
|
+
if not gap and body.get('prev') != expected_prev:
|
|
435
|
+
problems.append('prev does not chain')
|
|
436
|
+
|
|
437
|
+
new_bytes = int(body['counters'][COUNTER_TRANSIT_BYTES_OUT])
|
|
438
|
+
if chain:
|
|
439
|
+
prev_bytes = int(chain[-1]['counters'].get(COUNTER_TRANSIT_BYTES_OUT, 0))
|
|
440
|
+
if new_bytes < prev_bytes:
|
|
441
|
+
problems.append(f'counter {COUNTER_TRANSIT_BYTES_OUT} went backwards ({new_bytes} < {prev_bytes})')
|
|
442
|
+
|
|
443
|
+
if problems:
|
|
444
|
+
self.findings.append((signer_pubkey.lower(), window, 'S2', 'transit receipt chain: ' + '; '.join(problems)))
|
|
445
|
+
summary = self.debt_summary(broadcaster, grant_id)
|
|
446
|
+
return {
|
|
447
|
+
'grant': grant_id,
|
|
448
|
+
'seq': seq,
|
|
449
|
+
'head': expected_prev,
|
|
450
|
+
'status': 'divergent',
|
|
451
|
+
'unsettled_bytes': summary['unsettled_bytes'],
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if gap > 0:
|
|
455
|
+
self.receipt_gaps.append((signer_pubkey.lower(), window, expected_seq, seq, now_val))
|
|
456
|
+
|
|
457
|
+
head = receipt_link(body)
|
|
458
|
+
self.execute(
|
|
459
|
+
"""
|
|
460
|
+
INSERT OR REPLACE INTO transit_receipts_in (
|
|
461
|
+
grant_id, window, seq, head, broadcaster, transit_bytes_out, body_json, received_at
|
|
462
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
463
|
+
""",
|
|
464
|
+
(
|
|
465
|
+
grant_id,
|
|
466
|
+
window,
|
|
467
|
+
seq,
|
|
468
|
+
head,
|
|
469
|
+
broadcaster.lower(),
|
|
470
|
+
new_bytes,
|
|
471
|
+
json.dumps(dict(body), sort_keys=True),
|
|
472
|
+
now_val,
|
|
473
|
+
),
|
|
474
|
+
)
|
|
475
|
+
self.commit()
|
|
476
|
+
|
|
477
|
+
summary = self.debt_summary(broadcaster, grant_id)
|
|
478
|
+
status: TransitAckStatus = (
|
|
479
|
+
'degraded-audio-only' if summary['degraded_audio_only'] else 'accepted'
|
|
480
|
+
)
|
|
481
|
+
return {
|
|
482
|
+
'grant': grant_id,
|
|
483
|
+
'seq': seq,
|
|
484
|
+
'head': head,
|
|
485
|
+
'status': status,
|
|
486
|
+
'unsettled_bytes': summary['unsettled_bytes'],
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
def on_inbound_close(
|
|
490
|
+
self,
|
|
491
|
+
body: Mapping[str, Any],
|
|
492
|
+
now: Optional[int] = None,
|
|
493
|
+
) -> TransitReceiptAckBody:
|
|
494
|
+
"""Validate a TransitCloseBody and close the grant if final head matches."""
|
|
495
|
+
why = check_transit_close(body)
|
|
496
|
+
if why is not None:
|
|
497
|
+
return {
|
|
498
|
+
'grant': str(body.get('grant', accounting.ZERO)),
|
|
499
|
+
'seq': 0,
|
|
500
|
+
'head': accounting.ZERO,
|
|
501
|
+
'status': 'divergent',
|
|
502
|
+
'unsettled_bytes': 0,
|
|
503
|
+
}
|
|
504
|
+
grant_id = str(body['grant'])
|
|
505
|
+
window = int(body['window'])
|
|
506
|
+
grant = self.get_grant(grant_id)
|
|
507
|
+
with self._lock:
|
|
508
|
+
chain = self.get_inbound_receipts(grant_id, window)
|
|
509
|
+
head = receipt_link(chain[-1]) if chain else accounting.ZERO
|
|
510
|
+
is_final = body.get('final_receipt_head') == head
|
|
511
|
+
if is_final:
|
|
512
|
+
self.close_grant(grant_id)
|
|
513
|
+
broadcaster = grant['broadcaster'] if grant else ''
|
|
514
|
+
summary = self.debt_summary(broadcaster, grant_id) if broadcaster else {
|
|
515
|
+
'unsettled_bytes': 0,
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
'grant': grant_id,
|
|
519
|
+
'seq': len(chain),
|
|
520
|
+
'head': head,
|
|
521
|
+
'status': 'final' if is_final else 'accepted',
|
|
522
|
+
'unsettled_bytes': int(summary['unsettled_bytes']),
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
# ---- Bartered Network Utility & Debt Accounting -------------------------
|
|
526
|
+
|
|
527
|
+
def credit_barter(
|
|
528
|
+
self,
|
|
529
|
+
debtor: str,
|
|
530
|
+
creditor: str,
|
|
531
|
+
utility_kind: UtilityKind,
|
|
532
|
+
units: int,
|
|
533
|
+
proof_ref: str,
|
|
534
|
+
equivalent_transit_bytes: Optional[int] = None,
|
|
535
|
+
now: Optional[int] = None,
|
|
536
|
+
) -> BarterCreditRow:
|
|
537
|
+
"""Credit bartered network utility (Blob Shoring, Verified Compute, DVM,
|
|
538
|
+
or DIDComm Mediation) against a debtor's unsettled transit IOU balance.
|
|
539
|
+
"""
|
|
540
|
+
if utility_kind not in VALID_UTILITY_KINDS:
|
|
541
|
+
raise ValueError(f"Unknown utility_kind: {utility_kind!r}")
|
|
542
|
+
if units <= 0:
|
|
543
|
+
raise ValueError(f"units must be positive, got {units}")
|
|
544
|
+
|
|
545
|
+
now_val = self._clock() if now is None else int(now)
|
|
546
|
+
if equivalent_transit_bytes is None:
|
|
547
|
+
rate = DEFAULT_UTILITY_BYTES_RATES.get(utility_kind, 1)
|
|
548
|
+
equiv_bytes = int(units) * rate
|
|
549
|
+
else:
|
|
550
|
+
equiv_bytes = int(equivalent_transit_bytes)
|
|
551
|
+
|
|
552
|
+
seed = f"{debtor.lower()}:{creditor.lower()}:{utility_kind}:{proof_ref}".encode('utf-8')
|
|
553
|
+
credit_id = hashlib.sha256(seed).hexdigest()
|
|
554
|
+
|
|
555
|
+
row: BarterCreditRow = {
|
|
556
|
+
'credit_id': credit_id,
|
|
557
|
+
'debtor': debtor.lower(),
|
|
558
|
+
'creditor': creditor.lower(),
|
|
559
|
+
'utility_kind': utility_kind,
|
|
560
|
+
'units': int(units),
|
|
561
|
+
'equivalent_transit_bytes': equiv_bytes,
|
|
562
|
+
'proof_ref': proof_ref,
|
|
563
|
+
'created_at': now_val,
|
|
564
|
+
}
|
|
565
|
+
with self._lock:
|
|
566
|
+
self.execute(
|
|
567
|
+
"""
|
|
568
|
+
INSERT OR REPLACE INTO transit_barter_credits (
|
|
569
|
+
credit_id, debtor, creditor, utility_kind,
|
|
570
|
+
units, equivalent_transit_bytes, proof_ref, created_at
|
|
571
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
572
|
+
""",
|
|
573
|
+
(
|
|
574
|
+
row['credit_id'],
|
|
575
|
+
row['debtor'],
|
|
576
|
+
row['creditor'],
|
|
577
|
+
row['utility_kind'],
|
|
578
|
+
row['units'],
|
|
579
|
+
row['equivalent_transit_bytes'],
|
|
580
|
+
row['proof_ref'],
|
|
581
|
+
row['created_at'],
|
|
582
|
+
),
|
|
583
|
+
)
|
|
584
|
+
self.commit()
|
|
585
|
+
return row
|
|
586
|
+
|
|
587
|
+
def list_barter_credits(self, debtor: str) -> list[BarterCreditRow]:
|
|
588
|
+
with self._lock:
|
|
589
|
+
rows = self._all(
|
|
590
|
+
'SELECT ' + _CREDIT_COLS + ' FROM transit_barter_credits '
|
|
591
|
+
'WHERE debtor = ? ORDER BY created_at ASC',
|
|
592
|
+
(debtor.lower(),),
|
|
593
|
+
)
|
|
594
|
+
return [
|
|
595
|
+
{
|
|
596
|
+
'credit_id': str(r[0]),
|
|
597
|
+
'debtor': str(r[1]),
|
|
598
|
+
'creditor': str(r[2]),
|
|
599
|
+
'utility_kind': str(r[3]),
|
|
600
|
+
'units': int(r[4]),
|
|
601
|
+
'equivalent_transit_bytes': int(r[5]),
|
|
602
|
+
'proof_ref': str(r[6]),
|
|
603
|
+
'created_at': int(r[7]),
|
|
604
|
+
}
|
|
605
|
+
for r in rows
|
|
606
|
+
]
|
|
607
|
+
|
|
608
|
+
def gross_transit_bytes(self, debtor: str, grant_id: Optional[str] = None) -> int:
|
|
609
|
+
"""Sum the maximum cumulative `transit_bytes_out` per (grant_id, window) for a debtor."""
|
|
610
|
+
with self._lock:
|
|
611
|
+
if grant_id is not None:
|
|
612
|
+
rows = self.execute(
|
|
613
|
+
"""
|
|
614
|
+
SELECT MAX(transit_bytes_out) AS win_max
|
|
615
|
+
FROM transit_receipts_in
|
|
616
|
+
WHERE broadcaster = ? AND grant_id = ?
|
|
617
|
+
GROUP BY grant_id, window
|
|
618
|
+
""",
|
|
619
|
+
(debtor.lower(), grant_id),
|
|
620
|
+
).fetchall()
|
|
621
|
+
else:
|
|
622
|
+
rows = self.execute(
|
|
623
|
+
"""
|
|
624
|
+
SELECT MAX(transit_bytes_out) AS win_max
|
|
625
|
+
FROM transit_receipts_in
|
|
626
|
+
WHERE broadcaster = ?
|
|
627
|
+
GROUP BY grant_id, window
|
|
628
|
+
""",
|
|
629
|
+
(debtor.lower(),),
|
|
630
|
+
).fetchall()
|
|
631
|
+
return sum(int(r[0] or 0) for r in rows)
|
|
632
|
+
|
|
633
|
+
def total_bartered_bytes(self, debtor: str) -> int:
|
|
634
|
+
with self._lock:
|
|
635
|
+
row = self.execute(
|
|
636
|
+
'SELECT COALESCE(SUM(equivalent_transit_bytes), 0) AS total FROM transit_barter_credits WHERE debtor = ?',
|
|
637
|
+
(debtor.lower(),),
|
|
638
|
+
).fetchone()
|
|
639
|
+
return int(row[0]) if row is not None else 0
|
|
640
|
+
|
|
641
|
+
def debt_summary(self, debtor: str, grant_id: Optional[str] = None) -> DebtSummary:
|
|
642
|
+
"""Calculate gross transit bytes, bartered credits, unsettled debt, and
|
|
643
|
+
whether soft debt degradation (`degraded_audio_only`) is active.
|
|
644
|
+
"""
|
|
645
|
+
gross = self.gross_transit_bytes(debtor, grant_id=grant_id)
|
|
646
|
+
bartered = self.total_bartered_bytes(debtor)
|
|
647
|
+
unsettled = max(0, gross - bartered)
|
|
648
|
+
|
|
649
|
+
ceiling = 0
|
|
650
|
+
if grant_id is not None:
|
|
651
|
+
grant = self.get_grant(grant_id)
|
|
652
|
+
if grant is not None:
|
|
653
|
+
ceiling = int(grant['max_unsettled_transit_bytes'])
|
|
654
|
+
else:
|
|
655
|
+
with self._lock:
|
|
656
|
+
row = self.execute(
|
|
657
|
+
'SELECT COALESCE(MAX(max_unsettled_transit_bytes), 0) AS cap FROM transit_grants WHERE broadcaster = ?',
|
|
658
|
+
(debtor.lower(),),
|
|
659
|
+
).fetchone()
|
|
660
|
+
ceiling = int(row[0]) if row is not None else 0
|
|
661
|
+
|
|
662
|
+
degraded = bool(ceiling > 0 and unsettled >= ceiling)
|
|
663
|
+
return {
|
|
664
|
+
'debtor': debtor.lower(),
|
|
665
|
+
'gross_transit_bytes': gross,
|
|
666
|
+
'bartered_bytes': bartered,
|
|
667
|
+
'cashu_settled_bytes': 0,
|
|
668
|
+
'unsettled_bytes': unsettled,
|
|
669
|
+
'max_unsettled_transit_bytes': ceiling,
|
|
670
|
+
'degraded_audio_only': degraded,
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
def summary(self) -> dict[str, list[Any]]:
|
|
674
|
+
"""Return a summary of all known debtors and barter credit rows."""
|
|
675
|
+
with self._lock:
|
|
676
|
+
rows = self.execute(
|
|
677
|
+
"""
|
|
678
|
+
SELECT DISTINCT broadcaster AS debtor FROM transit_grants
|
|
679
|
+
UNION
|
|
680
|
+
SELECT DISTINCT broadcaster AS debtor FROM transit_receipts_in
|
|
681
|
+
UNION
|
|
682
|
+
SELECT DISTINCT debtor FROM transit_barter_credits
|
|
683
|
+
ORDER BY debtor ASC
|
|
684
|
+
"""
|
|
685
|
+
).fetchall()
|
|
686
|
+
debtors = [str(r[0]) for r in rows if r[0]]
|
|
687
|
+
debts = [self.debt_summary(d) for d in debtors]
|
|
688
|
+
credits: list[BarterCreditRow] = []
|
|
689
|
+
for d in debtors:
|
|
690
|
+
credits.extend(self.list_barter_credits(d))
|
|
691
|
+
return {
|
|
692
|
+
'debts': debts,
|
|
693
|
+
'utility_credits': credits,
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
__all__ = [
|
|
698
|
+
'UtilityKind',
|
|
699
|
+
'VALID_UTILITY_KINDS',
|
|
700
|
+
'DEFAULT_UTILITY_BYTES_RATES',
|
|
701
|
+
'BarterCreditRow',
|
|
702
|
+
'DebtSummary',
|
|
703
|
+
'OutboundChainState',
|
|
704
|
+
'TransitLedgerStore',
|
|
705
|
+
]
|