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,279 @@
|
|
|
1
|
+
"""Unified peering capacity, rate-limiting, and Cashu ecash settlement.
|
|
2
|
+
|
|
3
|
+
Enforces bilateral resource quotas and Cashu NUT-18 / NUT-24 ecash settlement
|
|
4
|
+
across all four protocol boundaries:
|
|
5
|
+
- Nostr relays & paid filters
|
|
6
|
+
- ATProto XRPC & Firehose sync
|
|
7
|
+
- ActivityPub S2S federation & media delivery
|
|
8
|
+
- Secure Scuttlebutt MuxRPC & Blob sync
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import base64
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
import json
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
17
|
+
from typing import Any, Mapping, Optional, Tuple
|
|
18
|
+
|
|
19
|
+
from fleetmesh_core.accounting import cashu
|
|
20
|
+
from fleetmesh_core.conformance.errors import FleetmeshError
|
|
21
|
+
from node.protocols.atproto.metering import TokenBucket
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GatewaySettlementError(FleetmeshError):
|
|
25
|
+
"""Raised on settlement or quota verification failures."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class CrossProtocolPeer:
|
|
30
|
+
"""A tracked peer communicating across any supported wire protocol."""
|
|
31
|
+
protocol: str
|
|
32
|
+
peer_id: str
|
|
33
|
+
bucket: TokenBucket
|
|
34
|
+
balance_sat: int = 0
|
|
35
|
+
total_consumed_tokens: int = 0
|
|
36
|
+
total_settled_sat: int = 0
|
|
37
|
+
penalty_score: float = 0.0
|
|
38
|
+
created_at: int = field(default_factory=lambda: int(time.time()))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class PaymentChallenge:
|
|
43
|
+
"""Standardized payment challenge issued when a peer exceeds quota."""
|
|
44
|
+
protocol: str
|
|
45
|
+
peer_id: str
|
|
46
|
+
required_sat: int
|
|
47
|
+
mint_url: str
|
|
48
|
+
memo: str
|
|
49
|
+
|
|
50
|
+
def to_http_402(self) -> tuple[int, Mapping[str, str], bytes]:
|
|
51
|
+
"""Format as HTTP 402 Payment Required with Cashu challenge header & body."""
|
|
52
|
+
headers = {
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
"WWW-Authenticate": f'Cashu mint="{self.mint_url}", amount={self.required_sat}, unit="sat"',
|
|
55
|
+
}
|
|
56
|
+
body = json.dumps({
|
|
57
|
+
"error": "PaymentRequired",
|
|
58
|
+
"message": f"Peer quota exceeded on {self.protocol}. Deposit Cashu ecash to continue.",
|
|
59
|
+
"mint": self.mint_url,
|
|
60
|
+
"amount_sat": self.required_sat,
|
|
61
|
+
"unit": "sat",
|
|
62
|
+
"memo": self.memo,
|
|
63
|
+
}, indent=2).encode('utf-8')
|
|
64
|
+
return 402, headers, body
|
|
65
|
+
|
|
66
|
+
def to_nostr_notice(self) -> str:
|
|
67
|
+
"""Format as Nostr NOTICE message content."""
|
|
68
|
+
return (
|
|
69
|
+
f"restricted: quota exceeded. Pay {self.required_sat} sats via Cashu to {self.mint_url} "
|
|
70
|
+
f"with memo '{self.memo}'"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def to_muxrpc_error(self) -> str:
|
|
74
|
+
"""Format as SSB MuxRPC error message."""
|
|
75
|
+
return (
|
|
76
|
+
f"PaymentRequired: quota exceeded for @{self.peer_id}. "
|
|
77
|
+
f"Deposit {self.required_sat} sats at {self.mint_url}"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class SettlementReceipt:
|
|
83
|
+
"""Proof of completed ecash settlement credit."""
|
|
84
|
+
protocol: str
|
|
85
|
+
peer_id: str
|
|
86
|
+
credited_sat: int
|
|
87
|
+
new_balance_sat: int
|
|
88
|
+
proof_count: int
|
|
89
|
+
timestamp: int = field(default_factory=lambda: int(time.time()))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class CrossProtocolSettlementGateway:
|
|
93
|
+
"""Unified resource accounting, rate limiting, and ecash settlement gateway."""
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
default_capacity: float = 30.0,
|
|
98
|
+
default_refill_per_sec: float = 0.5,
|
|
99
|
+
price_per_topup_sat: int = 21,
|
|
100
|
+
tokens_per_topup: int = 100,
|
|
101
|
+
mint_url: str = "https://mint.example.org",
|
|
102
|
+
max_penalty_score: float = 10.0,
|
|
103
|
+
):
|
|
104
|
+
self.default_capacity = default_capacity
|
|
105
|
+
self.default_refill_per_sec = default_refill_per_sec
|
|
106
|
+
self.price_per_topup_sat = price_per_topup_sat
|
|
107
|
+
self.tokens_per_topup = tokens_per_topup
|
|
108
|
+
self.mint_url = mint_url.rstrip('/')
|
|
109
|
+
self.max_penalty_score = max_penalty_score
|
|
110
|
+
|
|
111
|
+
self._peers: dict[tuple[str, str], CrossProtocolPeer] = {}
|
|
112
|
+
self._spent_secrets: set[str] = set()
|
|
113
|
+
self._total_challenges_issued = 0
|
|
114
|
+
self._total_settled_sat = 0
|
|
115
|
+
self._lock = threading.RLock()
|
|
116
|
+
|
|
117
|
+
def _peer_key(self, protocol: str, peer_id: str) -> tuple[str, str]:
|
|
118
|
+
return (protocol.lower().strip(), peer_id.strip())
|
|
119
|
+
|
|
120
|
+
def get_or_create_peer(
|
|
121
|
+
self,
|
|
122
|
+
protocol: str,
|
|
123
|
+
peer_id: str,
|
|
124
|
+
capacity: Optional[float] = None,
|
|
125
|
+
refill_rate: Optional[float] = None,
|
|
126
|
+
) -> CrossProtocolPeer:
|
|
127
|
+
"""Fetch or initialize a tracked peer record."""
|
|
128
|
+
key = self._peer_key(protocol, peer_id)
|
|
129
|
+
with self._lock:
|
|
130
|
+
if key not in self._peers:
|
|
131
|
+
cap = capacity if capacity is not None else self.default_capacity
|
|
132
|
+
refill = refill_rate if refill_rate is not None else self.default_refill_per_sec
|
|
133
|
+
bucket = TokenBucket(capacity=cap, refill_rate=refill)
|
|
134
|
+
self._peers[key] = CrossProtocolPeer(
|
|
135
|
+
protocol=key[0],
|
|
136
|
+
peer_id=key[1],
|
|
137
|
+
bucket=bucket,
|
|
138
|
+
)
|
|
139
|
+
return self._peers[key]
|
|
140
|
+
|
|
141
|
+
def record_penalty(self, protocol: str, peer_id: str, penalty: float = 1.0) -> float:
|
|
142
|
+
"""Increase penalty score for bad behavior (e.g. invalid signatures, flooding)."""
|
|
143
|
+
with self._lock:
|
|
144
|
+
peer = self.get_or_create_peer(protocol, peer_id)
|
|
145
|
+
peer.penalty_score += penalty
|
|
146
|
+
# Also drain tokens proportional to penalty
|
|
147
|
+
peer.bucket.tokens = max(0.0, peer.bucket.tokens - penalty * 2.0)
|
|
148
|
+
return peer.penalty_score
|
|
149
|
+
|
|
150
|
+
def check_access(
|
|
151
|
+
self,
|
|
152
|
+
protocol: str,
|
|
153
|
+
peer_id: str,
|
|
154
|
+
cost: int = 1,
|
|
155
|
+
) -> Tuple[bool, Optional[PaymentChallenge]]:
|
|
156
|
+
"""Authorize a request or issue a payment challenge.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
(True, None) if permitted under current token bucket or satoshi balance.
|
|
160
|
+
(False, PaymentChallenge) if dry and requires payment.
|
|
161
|
+
"""
|
|
162
|
+
with self._lock:
|
|
163
|
+
peer = self.get_or_create_peer(protocol, peer_id)
|
|
164
|
+
|
|
165
|
+
# 1. Circuit breaker: excessive penalties reject without immediate topup
|
|
166
|
+
if peer.penalty_score >= self.max_penalty_score:
|
|
167
|
+
challenge = PaymentChallenge(
|
|
168
|
+
protocol=protocol,
|
|
169
|
+
peer_id=peer_id,
|
|
170
|
+
required_sat=self.price_per_topup_sat * 5,
|
|
171
|
+
mint_url=self.mint_url,
|
|
172
|
+
memo=f"penalty-recovery-{peer_id[:16]}",
|
|
173
|
+
)
|
|
174
|
+
self._total_challenges_issued += 1
|
|
175
|
+
return False, challenge
|
|
176
|
+
|
|
177
|
+
# 2. Try consuming from rate-limiter token bucket
|
|
178
|
+
if peer.bucket.consume(float(cost)):
|
|
179
|
+
peer.total_consumed_tokens += cost
|
|
180
|
+
return True, None
|
|
181
|
+
|
|
182
|
+
# 3. If bucket dry, attempt to convert prepaid satoshi balance to tokens
|
|
183
|
+
if peer.balance_sat >= self.price_per_topup_sat:
|
|
184
|
+
peer.balance_sat -= self.price_per_topup_sat
|
|
185
|
+
peer.bucket.tokens += self.tokens_per_topup
|
|
186
|
+
peer.bucket.consume(float(cost))
|
|
187
|
+
peer.total_consumed_tokens += cost
|
|
188
|
+
return True, None
|
|
189
|
+
|
|
190
|
+
# 4. Quota exceeded: Issue payment challenge
|
|
191
|
+
self._total_challenges_issued += 1
|
|
192
|
+
challenge = PaymentChallenge(
|
|
193
|
+
protocol=protocol,
|
|
194
|
+
peer_id=peer_id,
|
|
195
|
+
required_sat=self.price_per_topup_sat,
|
|
196
|
+
mint_url=self.mint_url,
|
|
197
|
+
memo=f"quota-{protocol}-{peer_id[:16]}",
|
|
198
|
+
)
|
|
199
|
+
return False, challenge
|
|
200
|
+
|
|
201
|
+
def settle_cashu(self, protocol: str, peer_id: str, token_str: str) -> SettlementReceipt:
|
|
202
|
+
"""Parse, validate, and credit Cashu ecash payment (V3 cashuA or V4 cashuB)."""
|
|
203
|
+
raw_token = token_str.strip()
|
|
204
|
+
proofs: list[Mapping[str, Any]] = []
|
|
205
|
+
|
|
206
|
+
if raw_token.startswith(cashu.TOKEN_V4_PREFIX):
|
|
207
|
+
try:
|
|
208
|
+
tok_v4 = cashu.decode_token_v4(raw_token)
|
|
209
|
+
proofs = list(tok_v4.get('proofs', []))
|
|
210
|
+
except Exception as e:
|
|
211
|
+
raise GatewaySettlementError(f"Invalid Cashu V4 token: {e}") from e
|
|
212
|
+
elif raw_token.startswith('cashuA'):
|
|
213
|
+
try:
|
|
214
|
+
payload = raw_token[6:]
|
|
215
|
+
# Pad base64 if needed
|
|
216
|
+
payload += '=' * (-len(payload) % 4)
|
|
217
|
+
data = json.loads(base64.urlsafe_b64decode(payload).decode('utf-8'))
|
|
218
|
+
token_entries = data.get('token', [])
|
|
219
|
+
for entry in token_entries:
|
|
220
|
+
proofs.extend(entry.get('proofs', []))
|
|
221
|
+
except Exception as e:
|
|
222
|
+
raise GatewaySettlementError(f"Invalid Cashu V3 token: {e}") from e
|
|
223
|
+
else:
|
|
224
|
+
raise GatewaySettlementError("Unrecognized Cashu token prefix (expected cashuA or cashuB)")
|
|
225
|
+
|
|
226
|
+
proof_count = len(proofs)
|
|
227
|
+
sat_amount = sum(int(p.get('amount', 0)) for p in proofs)
|
|
228
|
+
|
|
229
|
+
if sat_amount <= 0 or proof_count == 0:
|
|
230
|
+
raise GatewaySettlementError("Token contains 0 sats or empty proofs")
|
|
231
|
+
|
|
232
|
+
with self._lock:
|
|
233
|
+
# Double-spend prevention across tokens
|
|
234
|
+
for p in proofs:
|
|
235
|
+
sec = p.get('secret')
|
|
236
|
+
if sec and sec in self._spent_secrets:
|
|
237
|
+
raise GatewaySettlementError(f"Double-spend attempt: proof secret already redeemed ({sec})")
|
|
238
|
+
|
|
239
|
+
for p in proofs:
|
|
240
|
+
sec = p.get('secret')
|
|
241
|
+
if sec:
|
|
242
|
+
self._spent_secrets.add(sec)
|
|
243
|
+
|
|
244
|
+
peer = self.get_or_create_peer(protocol, peer_id)
|
|
245
|
+
|
|
246
|
+
# Credit balance
|
|
247
|
+
peer.balance_sat += sat_amount
|
|
248
|
+
peer.total_settled_sat += sat_amount
|
|
249
|
+
self._total_settled_sat += sat_amount
|
|
250
|
+
|
|
251
|
+
# Refill tokens proportional to satoshis
|
|
252
|
+
granted_tokens = (sat_amount // self.price_per_topup_sat) * self.tokens_per_topup
|
|
253
|
+
if granted_tokens > 0:
|
|
254
|
+
peer.bucket.tokens = min(peer.bucket.capacity * 2, peer.bucket.tokens + granted_tokens)
|
|
255
|
+
|
|
256
|
+
# Decay penalty score on successful payment
|
|
257
|
+
peer.penalty_score = max(0.0, peer.penalty_score - 2.0)
|
|
258
|
+
|
|
259
|
+
return SettlementReceipt(
|
|
260
|
+
protocol=protocol,
|
|
261
|
+
peer_id=peer_id,
|
|
262
|
+
credited_sat=sat_amount,
|
|
263
|
+
new_balance_sat=peer.balance_sat,
|
|
264
|
+
proof_count=proof_count,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def stats(self) -> Mapping[str, Any]:
|
|
268
|
+
"""Aggregate cross-protocol accounting statistics."""
|
|
269
|
+
with self._lock:
|
|
270
|
+
by_proto: dict[str, int] = {}
|
|
271
|
+
for (proto, _), _ in self._peers.items():
|
|
272
|
+
by_proto[proto] = by_proto.get(proto, 0) + 1
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
"total_peers": len(self._peers),
|
|
276
|
+
"peers_by_protocol": by_proto,
|
|
277
|
+
"total_challenges_issued": self._total_challenges_issued,
|
|
278
|
+
"total_settled_sat": self._total_settled_sat,
|
|
279
|
+
}
|