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,433 @@
|
|
|
1
|
+
"""Compute market events: offers (11803), asks (30810), order-book parsing and matching.
|
|
2
|
+
|
|
3
|
+
Separate because the market layer is pure event construction and matching and shares nothing with execution.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any, Mapping, Optional, Sequence
|
|
9
|
+
|
|
10
|
+
from fleetmesh_core.accounting import market
|
|
11
|
+
from fleetmesh_core.storage import store
|
|
12
|
+
from fleetmesh_core.wire.interfaces import NostrEvent
|
|
13
|
+
from node import keystore
|
|
14
|
+
from .common import ComputeError, OFFER_KIND, ASK_KIND, DEFAULT_EXPIRATION
|
|
15
|
+
|
|
16
|
+
# -------------------------------------------------------------------------
|
|
17
|
+
# Kind 11803: Compute Offer
|
|
18
|
+
# -------------------------------------------------------------------------
|
|
19
|
+
def build_offer(
|
|
20
|
+
ks: keystore.Keystore,
|
|
21
|
+
executors: Sequence[Sequence[str]],
|
|
22
|
+
limits: Optional[Sequence[Sequence[str]]] = None,
|
|
23
|
+
runtimes: Optional[Sequence[Sequence[str]]] = None,
|
|
24
|
+
pay: Optional[Sequence[Sequence[str]]] = None,
|
|
25
|
+
expiration: Optional[int] = None,
|
|
26
|
+
now: Optional[int] = None,
|
|
27
|
+
) -> NostrEvent:
|
|
28
|
+
"""Build a signed kind 11803 compute offer event.
|
|
29
|
+
|
|
30
|
+
Content MUST be empty. At least one executor tag is required.
|
|
31
|
+
"""
|
|
32
|
+
now_val = int(now if now is not None else time.time())
|
|
33
|
+
exp_val = int(expiration if expiration is not None else now_val + DEFAULT_EXPIRATION)
|
|
34
|
+
|
|
35
|
+
if not executors:
|
|
36
|
+
raise ComputeError('at least one executor tag is required')
|
|
37
|
+
|
|
38
|
+
tags: list[list[str]] = []
|
|
39
|
+
for ex in executors:
|
|
40
|
+
tags.append(['executor'] + list(ex))
|
|
41
|
+
|
|
42
|
+
for lim in limits or []:
|
|
43
|
+
tags.append(['limit'] + list(lim))
|
|
44
|
+
|
|
45
|
+
for rt in runtimes or []:
|
|
46
|
+
tags.append(['runtime'] + list(rt))
|
|
47
|
+
|
|
48
|
+
for p in pay or []:
|
|
49
|
+
tags.append(['pay'] + list(p))
|
|
50
|
+
|
|
51
|
+
tags.append(['expiration', str(exp_val)])
|
|
52
|
+
|
|
53
|
+
ev = store.make_signed_event(ks.node_sk, now_val, OFFER_KIND, tags, '')
|
|
54
|
+
return ev
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def validate_offer(offer: NostrEvent | Mapping[str, Any]) -> list[str]:
|
|
58
|
+
"""Validate that an offer complies with kind 11803 constraints."""
|
|
59
|
+
problems: list[str] = []
|
|
60
|
+
if offer.get('kind') != OFFER_KIND:
|
|
61
|
+
problems.append('not a kind 11803')
|
|
62
|
+
if offer.get('content') != '':
|
|
63
|
+
problems.append('compute offer content must be empty')
|
|
64
|
+
|
|
65
|
+
tags = offer.get('tags', [])
|
|
66
|
+
has_exec = any(t and t[0] == 'executor' and len(t) >= 2 for t in tags)
|
|
67
|
+
if not has_exec:
|
|
68
|
+
problems.append('missing required executor tag')
|
|
69
|
+
|
|
70
|
+
has_exp = any(t and t[0] == 'expiration' and len(t) == 2 for t in tags)
|
|
71
|
+
if not has_exp:
|
|
72
|
+
problems.append('missing required expiration tag')
|
|
73
|
+
|
|
74
|
+
return problems
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# -------------------------------------------------------------------------
|
|
78
|
+
# Kind 30810: Compute Ask
|
|
79
|
+
# -------------------------------------------------------------------------
|
|
80
|
+
def build_ask(
|
|
81
|
+
buyer_sk: str,
|
|
82
|
+
ask_id: str,
|
|
83
|
+
job_payload: Mapping[str, Any],
|
|
84
|
+
executor: str,
|
|
85
|
+
bid_currency: str,
|
|
86
|
+
bid_price: int,
|
|
87
|
+
deadline: int,
|
|
88
|
+
fuel_max: int,
|
|
89
|
+
verify_mode: str,
|
|
90
|
+
expiration: Optional[int] = None,
|
|
91
|
+
verify_param: Optional[str] = None,
|
|
92
|
+
extra_tags: Optional[Sequence[Sequence[str]]] = None,
|
|
93
|
+
now: Optional[int] = None,
|
|
94
|
+
) -> NostrEvent:
|
|
95
|
+
"""Build a signed kind 30810 compute ask event.
|
|
96
|
+
|
|
97
|
+
Content MUST be empty. Module and input MUST NOT be named in tags to preserve
|
|
98
|
+
job confidentiality on public relays.
|
|
99
|
+
"""
|
|
100
|
+
now_val = int(now if now is not None else time.time())
|
|
101
|
+
exp_val = int(expiration if expiration is not None else now_val + DEFAULT_EXPIRATION)
|
|
102
|
+
|
|
103
|
+
if verify_mode not in market.MODES:
|
|
104
|
+
raise ComputeError('unknown verify mode: %r' % (verify_mode,))
|
|
105
|
+
|
|
106
|
+
commit = market.ask_commitment(job_payload)
|
|
107
|
+
|
|
108
|
+
tags: list[list[str]] = [
|
|
109
|
+
['d', ask_id],
|
|
110
|
+
['commit', commit],
|
|
111
|
+
['executor', executor],
|
|
112
|
+
['bid', bid_currency, str(int(bid_price))],
|
|
113
|
+
['deadline', str(int(deadline))],
|
|
114
|
+
['fuel_max', str(int(fuel_max))],
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
if verify_param:
|
|
118
|
+
tags.append(['verify', verify_mode, verify_param])
|
|
119
|
+
else:
|
|
120
|
+
tags.append(['verify', verify_mode])
|
|
121
|
+
|
|
122
|
+
tags.append(['expiration', str(exp_val)])
|
|
123
|
+
|
|
124
|
+
for extra in extra_tags or []:
|
|
125
|
+
if extra and extra[0] in ('module', 'input'):
|
|
126
|
+
raise ComputeError('ask must not leak module or input in tags')
|
|
127
|
+
tags.append(list(extra))
|
|
128
|
+
|
|
129
|
+
ev = store.make_signed_event(buyer_sk, now_val, ASK_KIND, tags, '')
|
|
130
|
+
return ev
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def verify_ask(ask: NostrEvent | Mapping[str, Any], job_payload: Mapping[str, Any]) -> bool:
|
|
134
|
+
"""Check that a received job payload matches the commitment in the ask."""
|
|
135
|
+
if ask.get('kind') != ASK_KIND:
|
|
136
|
+
return False
|
|
137
|
+
# market.* take a concrete dict; copying a Mapping into one is cheap and
|
|
138
|
+
# keeps the read-only contract this function advertises to callers.
|
|
139
|
+
ask_dict = dict(ask)
|
|
140
|
+
if market.leaks_job(ask_dict):
|
|
141
|
+
return False
|
|
142
|
+
return market.commitment_holds(ask_dict, job_payload)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# -------------------------------------------------------------------------
|
|
146
|
+
# Order-Book Parsing & Matching Engine
|
|
147
|
+
# -------------------------------------------------------------------------
|
|
148
|
+
def parse_offer(offer: NostrEvent | Mapping[str, Any]) -> Mapping[str, Any]:
|
|
149
|
+
"""Extract structured offer parameters from a kind 11803 event."""
|
|
150
|
+
tags = offer.get('tags', [])
|
|
151
|
+
executors = []
|
|
152
|
+
limits: dict[str, int] = {}
|
|
153
|
+
runtimes: list[dict[str, str]] = []
|
|
154
|
+
pay: list[str] = []
|
|
155
|
+
expiration: int = 0
|
|
156
|
+
|
|
157
|
+
for t in tags:
|
|
158
|
+
if not t or not isinstance(t, (list, tuple)):
|
|
159
|
+
continue
|
|
160
|
+
# Each arm binds the tag's positional fields by shape; a tag that is
|
|
161
|
+
# too short for its name simply falls through and is ignored, as the
|
|
162
|
+
# spec allows unknown or malformed tags on an offer.
|
|
163
|
+
match list(t):
|
|
164
|
+
case ['executor', engine, *rest]:
|
|
165
|
+
unit = str(rest[0]) if rest else 'msat_per_mfuel'
|
|
166
|
+
try:
|
|
167
|
+
rate = int(rest[1]) if len(rest) > 1 else 0
|
|
168
|
+
except ValueError:
|
|
169
|
+
rate = 0
|
|
170
|
+
executors.append({'engine': str(engine), 'unit': unit, 'rate': rate})
|
|
171
|
+
case ['limit', key, val, *_]:
|
|
172
|
+
try:
|
|
173
|
+
limits[str(key)] = int(val)
|
|
174
|
+
except ValueError:
|
|
175
|
+
pass
|
|
176
|
+
case ['runtime', engine, version, digest, *_]:
|
|
177
|
+
runtimes.append({
|
|
178
|
+
'engine': str(engine),
|
|
179
|
+
'version': str(version),
|
|
180
|
+
'wasi_profile_digest': str(digest),
|
|
181
|
+
})
|
|
182
|
+
case ['pay', method, *_]:
|
|
183
|
+
pay.append(str(method))
|
|
184
|
+
case ['expiration', raw, *_]:
|
|
185
|
+
try:
|
|
186
|
+
expiration = int(raw)
|
|
187
|
+
except ValueError:
|
|
188
|
+
expiration = 0
|
|
189
|
+
case _:
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
'id': offer.get('id', ''),
|
|
194
|
+
'pubkey': offer.get('pubkey', ''),
|
|
195
|
+
'created_at': offer.get('created_at', 0),
|
|
196
|
+
'executors': executors,
|
|
197
|
+
'limits': limits,
|
|
198
|
+
'runtimes': runtimes,
|
|
199
|
+
'pay': pay,
|
|
200
|
+
'expiration': expiration,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def parse_ask(ask: NostrEvent | Mapping[str, Any]) -> Mapping[str, Any]:
|
|
205
|
+
"""Extract structured ask parameters from a kind 30810 event."""
|
|
206
|
+
tags = ask.get('tags', [])
|
|
207
|
+
ask_id: str = ''
|
|
208
|
+
commit: str = ''
|
|
209
|
+
executor: str = ''
|
|
210
|
+
bid_currency: str = 'msat_per_mfuel'
|
|
211
|
+
bid_price: int = 0
|
|
212
|
+
deadline: int = 0
|
|
213
|
+
fuel_max: int = 0
|
|
214
|
+
verify_mode: str = 'none'
|
|
215
|
+
verify_param: Optional[str] = None
|
|
216
|
+
expiration: int = 0
|
|
217
|
+
runtime_digest: Optional[str] = None
|
|
218
|
+
|
|
219
|
+
for t in tags:
|
|
220
|
+
if not t or not isinstance(t, (list, tuple)):
|
|
221
|
+
continue
|
|
222
|
+
# Shape-matched like parse_offer: a too-short tag falls through.
|
|
223
|
+
match list(t):
|
|
224
|
+
case ['d', raw, *_]:
|
|
225
|
+
ask_id = str(raw)
|
|
226
|
+
case ['commit', raw, *_]:
|
|
227
|
+
commit = str(raw)
|
|
228
|
+
case ['executor', raw, *_]:
|
|
229
|
+
executor = str(raw)
|
|
230
|
+
case ['bid', currency, price, *_]:
|
|
231
|
+
bid_currency = str(currency)
|
|
232
|
+
try:
|
|
233
|
+
bid_price = int(price)
|
|
234
|
+
except ValueError:
|
|
235
|
+
bid_price = 0
|
|
236
|
+
case ['deadline', raw, *_]:
|
|
237
|
+
try:
|
|
238
|
+
deadline = int(raw)
|
|
239
|
+
except ValueError:
|
|
240
|
+
deadline = 0
|
|
241
|
+
case ['fuel_max', raw, *_]:
|
|
242
|
+
try:
|
|
243
|
+
fuel_max = int(raw)
|
|
244
|
+
except ValueError:
|
|
245
|
+
fuel_max = 0
|
|
246
|
+
case ['verify', mode, *rest]:
|
|
247
|
+
verify_mode = str(mode)
|
|
248
|
+
if rest:
|
|
249
|
+
verify_param = str(rest[0])
|
|
250
|
+
case ['runtime', _, _, digest, *_]:
|
|
251
|
+
runtime_digest = str(digest)
|
|
252
|
+
case ['expiration', raw, *_]:
|
|
253
|
+
try:
|
|
254
|
+
expiration = int(raw)
|
|
255
|
+
except ValueError:
|
|
256
|
+
expiration = 0
|
|
257
|
+
case _:
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
'id': ask.get('id', ''),
|
|
262
|
+
'pubkey': ask.get('pubkey', ''),
|
|
263
|
+
'created_at': ask.get('created_at', 0),
|
|
264
|
+
'ask_id': ask_id,
|
|
265
|
+
'commit': commit,
|
|
266
|
+
'executor': executor,
|
|
267
|
+
'bid_currency': bid_currency,
|
|
268
|
+
'bid_price': bid_price,
|
|
269
|
+
'deadline': deadline,
|
|
270
|
+
'fuel_max': fuel_max,
|
|
271
|
+
'verify_mode': verify_mode,
|
|
272
|
+
'verify_param': verify_param,
|
|
273
|
+
'runtime_digest': runtime_digest,
|
|
274
|
+
'expiration': expiration,
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def match_ask_to_offers(
|
|
279
|
+
ask: NostrEvent | Mapping[str, Any],
|
|
280
|
+
offers: Sequence[NostrEvent | Mapping[str, Any]],
|
|
281
|
+
trust_weights: Optional[Mapping[str, float]] = None,
|
|
282
|
+
now: Optional[int] = None,
|
|
283
|
+
) -> list[Mapping[str, Any]]:
|
|
284
|
+
"""Match a compute ask against candidate compute offers in the order book.
|
|
285
|
+
|
|
286
|
+
Returns candidate matches ranked by:
|
|
287
|
+
1. Zero-cost / free offers first (volunteer / spare cycles),
|
|
288
|
+
2. Lowest rate amount (msat_per_mfuel),
|
|
289
|
+
3. Lowest reported CPU load percentage (cpu_load_pct),
|
|
290
|
+
4. Highest trust weight / reputation standing,
|
|
291
|
+
5. Furthest expiration deadline.
|
|
292
|
+
"""
|
|
293
|
+
now_val = int(now if now is not None else time.time())
|
|
294
|
+
ask_p = parse_ask(ask)
|
|
295
|
+
matches: list[Mapping[str, Any]] = []
|
|
296
|
+
|
|
297
|
+
for off in offers:
|
|
298
|
+
off_p = parse_offer(off)
|
|
299
|
+
# 1. Expiration check
|
|
300
|
+
if off_p['expiration'] and off_p['expiration'] < now_val:
|
|
301
|
+
continue
|
|
302
|
+
if ask_p['deadline'] and off_p['expiration'] and off_p['expiration'] < ask_p['deadline']:
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
# 2. Fuel limit and CPU/slot headroom check
|
|
306
|
+
if 'fuel_max' in off_p['limits'] and off_p['limits']['fuel_max'] < ask_p['fuel_max']:
|
|
307
|
+
continue
|
|
308
|
+
if 'free_slots' in off_p['limits'] and int(off_p['limits']['free_slots']) <= 0:
|
|
309
|
+
continue
|
|
310
|
+
if 'cpu_load_pct' in off_p['limits'] and int(off_p['limits']['cpu_load_pct']) >= 85:
|
|
311
|
+
continue
|
|
312
|
+
|
|
313
|
+
# 3. Match executor engine
|
|
314
|
+
matched_exec = None
|
|
315
|
+
for ex in off_p['executors']:
|
|
316
|
+
if ex['engine'] == ask_p['executor'] or (
|
|
317
|
+
ask_p['executor'] == 'wasm' and ex['engine'] in ('wasm', 'wasmtime', 'wasmer')
|
|
318
|
+
) or (
|
|
319
|
+
ask_p['executor'] in ('ffmpeg', 'media-transcode', 'video-transcode', 'transcode')
|
|
320
|
+
and ex['engine'] in ('ffmpeg', 'media-transcode', 'video-transcode', 'transcode')
|
|
321
|
+
):
|
|
322
|
+
matched_exec = ex
|
|
323
|
+
break
|
|
324
|
+
if not matched_exec:
|
|
325
|
+
continue
|
|
326
|
+
|
|
327
|
+
# 4. Price check: offer rate must not exceed ask bid (or offer is free: rate == 0)
|
|
328
|
+
offer_rate = matched_exec['rate']
|
|
329
|
+
if offer_rate > ask_p['bid_price'] and offer_rate != 0:
|
|
330
|
+
continue
|
|
331
|
+
|
|
332
|
+
# 5. Runtime digest check if specified in both
|
|
333
|
+
if ask_p['runtime_digest']:
|
|
334
|
+
has_matching_rt = any(
|
|
335
|
+
rt.get('wasi_profile_digest') == ask_p['runtime_digest']
|
|
336
|
+
for rt in off_p['runtimes']
|
|
337
|
+
)
|
|
338
|
+
if off_p['runtimes'] and not has_matching_rt:
|
|
339
|
+
continue
|
|
340
|
+
|
|
341
|
+
# Price estimation
|
|
342
|
+
fuel = ask_p['fuel_max'] or 1000000
|
|
343
|
+
est_cost_msat = (fuel * offer_rate) // 1000000 if offer_rate > 0 else 0
|
|
344
|
+
|
|
345
|
+
seller_pk = off_p['pubkey']
|
|
346
|
+
seller_trust = float(trust_weights.get(seller_pk, 0.0)) if trust_weights else 0.0
|
|
347
|
+
cpu_load_pct = int(off_p['limits'].get('cpu_load_pct', 0))
|
|
348
|
+
free_slots = int(off_p['limits'].get('free_slots', 1))
|
|
349
|
+
|
|
350
|
+
matches.append({
|
|
351
|
+
'offer_id': off_p['id'],
|
|
352
|
+
'seller_pubkey': seller_pk,
|
|
353
|
+
'executor': matched_exec['engine'],
|
|
354
|
+
'rate_unit': matched_exec['unit'],
|
|
355
|
+
'rate': offer_rate,
|
|
356
|
+
'estimated_cost_msat': est_cost_msat,
|
|
357
|
+
'is_free': (offer_rate == 0),
|
|
358
|
+
'cpu_load_pct': cpu_load_pct,
|
|
359
|
+
'free_slots': free_slots,
|
|
360
|
+
'seller_trust': seller_trust,
|
|
361
|
+
'expiration': off_p['expiration'],
|
|
362
|
+
'offer_event': off,
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
# Sort matches: free offers first (0 vs 1), lowest rate, lowest CPU load, highest trust, furthest expiration
|
|
366
|
+
matches.sort(
|
|
367
|
+
key=lambda m: (
|
|
368
|
+
0 if m['is_free'] else 1,
|
|
369
|
+
m['rate'],
|
|
370
|
+
m['cpu_load_pct'],
|
|
371
|
+
-m['seller_trust'],
|
|
372
|
+
-m['expiration'],
|
|
373
|
+
)
|
|
374
|
+
)
|
|
375
|
+
return matches
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def match_offer_to_asks(
|
|
379
|
+
offer: NostrEvent | Mapping[str, Any],
|
|
380
|
+
asks: Sequence[NostrEvent | Mapping[str, Any]],
|
|
381
|
+
now: Optional[int] = None,
|
|
382
|
+
) -> list[Mapping[str, Any]]:
|
|
383
|
+
"""Match a compute offer against pending compute asks in the order book."""
|
|
384
|
+
now_val = int(now if now is not None else time.time())
|
|
385
|
+
off_p = parse_offer(offer)
|
|
386
|
+
if off_p['expiration'] and off_p['expiration'] < now_val:
|
|
387
|
+
return []
|
|
388
|
+
|
|
389
|
+
matches: list[Mapping[str, Any]] = []
|
|
390
|
+
for ask in asks:
|
|
391
|
+
ask_p = parse_ask(ask)
|
|
392
|
+
if ask_p['expiration'] and ask_p['expiration'] < now_val:
|
|
393
|
+
continue
|
|
394
|
+
if ask_p['deadline'] and off_p['expiration'] and off_p['expiration'] < ask_p['deadline']:
|
|
395
|
+
continue
|
|
396
|
+
if 'fuel_max' in off_p['limits'] and off_p['limits']['fuel_max'] < ask_p['fuel_max']:
|
|
397
|
+
continue
|
|
398
|
+
|
|
399
|
+
matched_exec = None
|
|
400
|
+
for ex in off_p['executors']:
|
|
401
|
+
if ex['engine'] == ask_p['executor'] or (
|
|
402
|
+
ask_p['executor'] == 'wasm' and ex['engine'] in ('wasm', 'wasmtime', 'wasmer')
|
|
403
|
+
) or (
|
|
404
|
+
ask_p['executor'] in ('ffmpeg', 'media-transcode', 'video-transcode', 'transcode')
|
|
405
|
+
and ex['engine'] in ('ffmpeg', 'media-transcode', 'video-transcode', 'transcode')
|
|
406
|
+
):
|
|
407
|
+
matched_exec = ex
|
|
408
|
+
break
|
|
409
|
+
if not matched_exec:
|
|
410
|
+
continue
|
|
411
|
+
|
|
412
|
+
offer_rate = matched_exec['rate']
|
|
413
|
+
if offer_rate > ask_p['bid_price'] and offer_rate != 0:
|
|
414
|
+
continue
|
|
415
|
+
|
|
416
|
+
fuel = ask_p['fuel_max'] or 1000000
|
|
417
|
+
est_cost_msat = (fuel * offer_rate) // 1000000 if offer_rate > 0 else 0
|
|
418
|
+
|
|
419
|
+
matches.append({
|
|
420
|
+
'ask_id': ask_p['ask_id'],
|
|
421
|
+
'buyer_pubkey': ask_p['pubkey'],
|
|
422
|
+
'executor': matched_exec['engine'],
|
|
423
|
+
'bid_price': ask_p['bid_price'],
|
|
424
|
+
'rate': offer_rate,
|
|
425
|
+
'estimated_revenue_msat': est_cost_msat,
|
|
426
|
+
'is_free': (offer_rate == 0),
|
|
427
|
+
'deadline': ask_p['deadline'],
|
|
428
|
+
'ask_event': ask,
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
# Sort asks: highest bid first, nearest deadline
|
|
432
|
+
matches.sort(key=lambda m: (-m['bid_price'], m['deadline']))
|
|
433
|
+
return matches
|