singularity-grid 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- {singularity_grid-0.2.0 → singularity_grid-0.3.0}/.gitignore +3 -0
- {singularity_grid-0.2.0 → singularity_grid-0.3.0}/PKG-INFO +6 -3
- {singularity_grid-0.2.0 → singularity_grid-0.3.0}/README.md +1 -1
- {singularity_grid-0.2.0 → singularity_grid-0.3.0}/examples/openai_compatible.py +1 -1
- {singularity_grid-0.2.0 → singularity_grid-0.3.0}/pyproject.toml +5 -2
- {singularity_grid-0.2.0 → singularity_grid-0.3.0}/singularity_grid/client.py +167 -4
- singularity_grid-0.3.0/singularity_grid/e2e.py +138 -0
- {singularity_grid-0.2.0 → singularity_grid-0.3.0}/.github/workflows/publish.yml +0 -0
- {singularity_grid-0.2.0 → singularity_grid-0.3.0}/LICENSE +0 -0
- {singularity_grid-0.2.0 → singularity_grid-0.3.0}/examples/check_capacity.py +0 -0
- {singularity_grid-0.2.0 → singularity_grid-0.3.0}/examples/quickstart.py +0 -0
- {singularity_grid-0.2.0 → singularity_grid-0.3.0}/singularity_grid/__init__.py +0 -0
- {singularity_grid-0.2.0 → singularity_grid-0.3.0}/singularity_grid/models.py +0 -0
- {singularity_grid-0.2.0 → singularity_grid-0.3.0}/singularity_grid/openai_compat.py +0 -0
- {singularity_grid-0.2.0 → singularity_grid-0.3.0}/singularity_grid/py.typed +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: singularity-grid
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Python SDK for the SGL Network confidential compute grid
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Python SDK for the SGL Network confidential compute grid (end-to-end encrypted + streaming)
|
|
5
5
|
Project-URL: Homepage, https://singularitylayer.xyz
|
|
6
6
|
Project-URL: Repository, https://github.com/SingularityLayer/sgl-network-sdk
|
|
7
7
|
Project-URL: Documentation, https://docs.singularitylayer.xyz/sdk
|
|
@@ -20,8 +20,11 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
20
20
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
22
|
Requires-Python: >=3.9
|
|
23
|
+
Requires-Dist: base58>=2.1
|
|
24
|
+
Requires-Dist: cryptography>=42.0
|
|
23
25
|
Requires-Dist: httpx>=0.24
|
|
24
26
|
Requires-Dist: pydantic>=2.0
|
|
27
|
+
Requires-Dist: pynacl>=1.5
|
|
25
28
|
Provides-Extra: openai
|
|
26
29
|
Requires-Dist: openai>=1.0; extra == 'openai'
|
|
27
30
|
Description-Content-Type: text/markdown
|
|
@@ -52,7 +55,7 @@ The SGL Network orchestrator exposes an OpenAI-compatible `/v1/chat/completions`
|
|
|
52
55
|
from openai import OpenAI
|
|
53
56
|
|
|
54
57
|
client = OpenAI(
|
|
55
|
-
base_url="https://
|
|
58
|
+
base_url="https://grid.x402compute.cc/v1",
|
|
56
59
|
api_key="scg_your_api_key",
|
|
57
60
|
)
|
|
58
61
|
|
|
@@ -24,7 +24,7 @@ The SGL Network orchestrator exposes an OpenAI-compatible `/v1/chat/completions`
|
|
|
24
24
|
from openai import OpenAI
|
|
25
25
|
|
|
26
26
|
client = OpenAI(
|
|
27
|
-
base_url="https://
|
|
27
|
+
base_url="https://grid.x402compute.cc/v1",
|
|
28
28
|
api_key="scg_your_api_key",
|
|
29
29
|
)
|
|
30
30
|
|
|
@@ -10,7 +10,7 @@ beyond swapping the base URL and API key.
|
|
|
10
10
|
from openai import OpenAI
|
|
11
11
|
|
|
12
12
|
client = OpenAI(
|
|
13
|
-
base_url="https://
|
|
13
|
+
base_url="https://grid.x402compute.cc/v1",
|
|
14
14
|
api_key="scg_your_api_key_here",
|
|
15
15
|
)
|
|
16
16
|
|
|
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "singularity-grid"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "Python SDK for the SGL Network confidential compute grid"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Python SDK for the SGL Network confidential compute grid (end-to-end encrypted + streaming)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
requires-python = ">=3.9"
|
|
@@ -28,6 +28,9 @@ classifiers = [
|
|
|
28
28
|
dependencies = [
|
|
29
29
|
"httpx>=0.24",
|
|
30
30
|
"pydantic>=2.0",
|
|
31
|
+
"pynacl>=1.5",
|
|
32
|
+
"cryptography>=42.0",
|
|
33
|
+
"base58>=2.1",
|
|
31
34
|
]
|
|
32
35
|
|
|
33
36
|
[project.optional-dependencies]
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import json as _json
|
|
6
|
+
from typing import Any, Dict, Iterator, List, Optional
|
|
6
7
|
|
|
7
8
|
import httpx
|
|
8
9
|
|
|
10
|
+
from . import e2e
|
|
9
11
|
from .models import (
|
|
10
12
|
AttestationProof,
|
|
11
13
|
CapacityResponse,
|
|
@@ -23,9 +25,7 @@ from .models import (
|
|
|
23
25
|
ProcessorResult,
|
|
24
26
|
)
|
|
25
27
|
|
|
26
|
-
DEFAULT_BASE_URL =
|
|
27
|
-
"https://sgl-network-orchestrator.ivaavimusicproductions.workers.dev"
|
|
28
|
-
)
|
|
28
|
+
DEFAULT_BASE_URL = "https://grid.x402compute.cc"
|
|
29
29
|
|
|
30
30
|
DEFAULT_TIMEOUT = 60.0
|
|
31
31
|
|
|
@@ -93,6 +93,9 @@ class GridClient:
|
|
|
93
93
|
headers: Dict[str, str] = {"Accept": "application/json"}
|
|
94
94
|
if api_key:
|
|
95
95
|
headers["Authorization"] = f"Bearer {api_key}"
|
|
96
|
+
# Grid credit billing reads X-API-Key; send both so reserve + chat
|
|
97
|
+
# resolve the paying wallet (credits mode) for end-to-end requests.
|
|
98
|
+
headers["X-API-Key"] = api_key
|
|
96
99
|
self._client = httpx.Client(
|
|
97
100
|
base_url=self._base_url,
|
|
98
101
|
headers=headers,
|
|
@@ -206,6 +209,166 @@ class GridClient:
|
|
|
206
209
|
data = self._request("GET", f"/grid/jobs/{job_id}/attestation")
|
|
207
210
|
return AttestationProof.model_validate(data)
|
|
208
211
|
|
|
212
|
+
# -- chat (end-to-end encrypted) ---------------------------------------
|
|
213
|
+
|
|
214
|
+
def _reserve(self, model: str) -> Dict[str, Any]:
|
|
215
|
+
"""Reserve a node + learn its X25519 key so we can seal the prompt to it."""
|
|
216
|
+
data = self._request("POST", "/v1/reserve", json={"model": model})
|
|
217
|
+
if not data.get("node_x25519_pubkey"):
|
|
218
|
+
raise SGLAPIError(503, "Reserved node does not support E2E encryption")
|
|
219
|
+
return data
|
|
220
|
+
|
|
221
|
+
def chat_completions(
|
|
222
|
+
self,
|
|
223
|
+
model: str,
|
|
224
|
+
messages: List[Dict[str, str]],
|
|
225
|
+
*,
|
|
226
|
+
temperature: float = 0.7,
|
|
227
|
+
max_tokens: int = 512,
|
|
228
|
+
) -> Dict[str, Any]:
|
|
229
|
+
"""End-to-end encrypted chat completion.
|
|
230
|
+
|
|
231
|
+
The prompt is sealed in this client to the serving node's key and only
|
|
232
|
+
decrypts inside its TEE — the orchestrator only relays ciphertext. Requires
|
|
233
|
+
``api_key`` (credits); x402 pay-per-call isn't signed here (use the wallet
|
|
234
|
+
flow). Returns an OpenAI-style dict with an extra ``attestation`` field.
|
|
235
|
+
"""
|
|
236
|
+
reservation = self._reserve(model)
|
|
237
|
+
resp_sk, resp_pub = e2e.new_response_keypair()
|
|
238
|
+
sealed_ct, eph = e2e.seal_input(
|
|
239
|
+
reservation["node_x25519_pubkey"], resp_pub,
|
|
240
|
+
_json.dumps({"messages": messages, "temperature": temperature, "max_tokens": max_tokens}).encode(),
|
|
241
|
+
)
|
|
242
|
+
body = {
|
|
243
|
+
"reservation_token": reservation["reservation_token"],
|
|
244
|
+
"max_tokens": max_tokens, # cleartext, only used to quote the x402 price
|
|
245
|
+
"enc": {
|
|
246
|
+
"ciphertext": sealed_ct,
|
|
247
|
+
"client_ephemeral_pubkey": eph,
|
|
248
|
+
"client_response_pubkey": resp_pub,
|
|
249
|
+
"algorithm": e2e.ALGO_V2,
|
|
250
|
+
},
|
|
251
|
+
}
|
|
252
|
+
try:
|
|
253
|
+
data = self._request("POST", "/v1/chat/completions", json=body)
|
|
254
|
+
except SGLAPIError as err:
|
|
255
|
+
if err.status_code == 402:
|
|
256
|
+
raise SGLAPIError(402, "Payment required — pass api_key (credits); the Python GridClient does not sign x402 payments.") from err
|
|
257
|
+
raise
|
|
258
|
+
|
|
259
|
+
sealed = data.get("sealed_result")
|
|
260
|
+
if not sealed:
|
|
261
|
+
raise SGLAPIError(500, "No sealed result returned")
|
|
262
|
+
plain = e2e.open_output(resp_sk, resp_pub, sealed["ephemeral_public_key"], sealed["ciphertext"])
|
|
263
|
+
parsed = _json.loads(plain)
|
|
264
|
+
return {
|
|
265
|
+
"id": data.get("id", ""),
|
|
266
|
+
"object": "chat.completion",
|
|
267
|
+
"model": model,
|
|
268
|
+
"choices": [{"index": 0, "message": {"role": "assistant", "content": parsed.get("content", "")}, "finish_reason": "stop"}],
|
|
269
|
+
"usage": data.get("usage") or parsed.get("usage", {}),
|
|
270
|
+
"attestation": {
|
|
271
|
+
"node_id": reservation.get("node_id"),
|
|
272
|
+
"tee_type": reservation.get("tee_type"),
|
|
273
|
+
"verified": bool(reservation.get("attestation_verified", False)),
|
|
274
|
+
},
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
def chat_completion_stream(
|
|
278
|
+
self,
|
|
279
|
+
model: str,
|
|
280
|
+
messages: List[Dict[str, str]],
|
|
281
|
+
*,
|
|
282
|
+
temperature: float = 0.7,
|
|
283
|
+
max_tokens: int = 512,
|
|
284
|
+
) -> Iterator[str]:
|
|
285
|
+
"""Yield decoded text as it streams (end-to-end encrypted). Requires
|
|
286
|
+
``api_key`` (credits). Each chunk is decrypted and its ordering +
|
|
287
|
+
termination verified (a truncated stream raises). If streaming isn't
|
|
288
|
+
enabled server-side, the whole reply is yielded as one chunk."""
|
|
289
|
+
reservation = self._reserve(model)
|
|
290
|
+
resp_sk, resp_pub = e2e.new_response_keypair()
|
|
291
|
+
nonce = e2e.random_nonce_b58()
|
|
292
|
+
sealed_ct, eph = e2e.seal_input(
|
|
293
|
+
reservation["node_x25519_pubkey"], resp_pub,
|
|
294
|
+
_json.dumps({"messages": messages, "temperature": temperature, "max_tokens": max_tokens, "stream": True, "nonce": nonce}).encode(),
|
|
295
|
+
)
|
|
296
|
+
body = {
|
|
297
|
+
"reservation_token": reservation["reservation_token"],
|
|
298
|
+
"stream": True,
|
|
299
|
+
"max_tokens": max_tokens,
|
|
300
|
+
"enc": {
|
|
301
|
+
"ciphertext": sealed_ct,
|
|
302
|
+
"client_ephemeral_pubkey": eph,
|
|
303
|
+
"client_response_pubkey": resp_pub,
|
|
304
|
+
"algorithm": e2e.ALGO_V2,
|
|
305
|
+
},
|
|
306
|
+
}
|
|
307
|
+
with self._client.stream("POST", "/v1/chat/completions", json=body) as resp:
|
|
308
|
+
if resp.status_code != 200:
|
|
309
|
+
resp.read()
|
|
310
|
+
if resp.status_code == 402:
|
|
311
|
+
raise SGLAPIError(402, "Payment required — pass api_key (credits); the Python GridClient does not sign x402 payments.")
|
|
312
|
+
msg = resp.text
|
|
313
|
+
try:
|
|
314
|
+
err = resp.json().get("error")
|
|
315
|
+
msg = err.get("message", msg) if isinstance(err, dict) else (err or msg)
|
|
316
|
+
except Exception:
|
|
317
|
+
pass
|
|
318
|
+
raise SGLAPIError(resp.status_code, str(msg))
|
|
319
|
+
|
|
320
|
+
if "text/event-stream" not in resp.headers.get("content-type", ""):
|
|
321
|
+
resp.read()
|
|
322
|
+
data = _json.loads(resp.text)
|
|
323
|
+
sealed = data.get("sealed_result")
|
|
324
|
+
if not sealed:
|
|
325
|
+
raise SGLAPIError(500, "No sealed result returned")
|
|
326
|
+
plain = e2e.open_output(resp_sk, resp_pub, sealed["ephemeral_public_key"], sealed["ciphertext"])
|
|
327
|
+
content = _json.loads(plain).get("content", "")
|
|
328
|
+
if content:
|
|
329
|
+
yield content
|
|
330
|
+
return
|
|
331
|
+
|
|
332
|
+
expected_seq = 0
|
|
333
|
+
out_key = None
|
|
334
|
+
stream_eph = None
|
|
335
|
+
saw_final = False
|
|
336
|
+
for line in resp.iter_lines():
|
|
337
|
+
line = line.strip()
|
|
338
|
+
if not line:
|
|
339
|
+
continue
|
|
340
|
+
if line.startswith("event: error"):
|
|
341
|
+
raise SGLAPIError(502, "stream aborted by server")
|
|
342
|
+
if line.startswith(":") or not line.startswith("data:"):
|
|
343
|
+
continue
|
|
344
|
+
payload = line[5:].strip()
|
|
345
|
+
if payload == "[DONE]":
|
|
346
|
+
continue
|
|
347
|
+
try:
|
|
348
|
+
chunk = _json.loads(payload)
|
|
349
|
+
except _json.JSONDecodeError as exc:
|
|
350
|
+
raise SGLAPIError(502, "malformed stream chunk") from exc
|
|
351
|
+
seq = chunk.get("seq")
|
|
352
|
+
if seq is None or "ct" not in chunk:
|
|
353
|
+
raise SGLAPIError(502, "invalid stream chunk (missing seq/ciphertext)")
|
|
354
|
+
if seq != expected_seq:
|
|
355
|
+
raise SGLAPIError(502, f"stream out of order (expected {expected_seq}, got {seq})")
|
|
356
|
+
if seq == 0:
|
|
357
|
+
stream_eph = chunk.get("eph")
|
|
358
|
+
if not stream_eph:
|
|
359
|
+
raise SGLAPIError(502, "stream chunk 0 missing ephemeral key")
|
|
360
|
+
out_key = e2e.stream_out_key(resp_sk, stream_eph)
|
|
361
|
+
is_final = chunk.get("final") is True
|
|
362
|
+
text = e2e.open_stream_chunk(out_key, resp_pub, stream_eph, nonce, seq, is_final, chunk["ct"]).decode()
|
|
363
|
+
if text:
|
|
364
|
+
yield text
|
|
365
|
+
expected_seq += 1
|
|
366
|
+
if is_final:
|
|
367
|
+
saw_final = True
|
|
368
|
+
break
|
|
369
|
+
if not saw_final:
|
|
370
|
+
raise SGLAPIError(502, "stream ended before final chunk (truncated)")
|
|
371
|
+
|
|
209
372
|
# -- processor endpoints -----------------------------------------------
|
|
210
373
|
|
|
211
374
|
def deploy_processor(
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
End-to-end encryption for the SGL grid (client side).
|
|
3
|
+
|
|
4
|
+
Must match the node, orchestrator, and browser/TS clients byte-for-byte:
|
|
5
|
+
X25519 ECDH -> HKDF-SHA256 -> XChaCha20-Poly1305 (24-byte nonce), AAD-bound.
|
|
6
|
+
Sealed blob layout: nonce(24) || ciphertext, base58.
|
|
7
|
+
|
|
8
|
+
The orchestrator only ever relays ciphertext — it never sees the prompt or reply.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import base58
|
|
14
|
+
from cryptography.hazmat.primitives import hashes
|
|
15
|
+
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
16
|
+
from nacl import bindings
|
|
17
|
+
|
|
18
|
+
ALGO_V2 = "x25519-xchacha20poly1305-hkdf-v2"
|
|
19
|
+
ALGO_V2_STREAM = "x25519-xchacha20poly1305-hkdf-v2-stream"
|
|
20
|
+
|
|
21
|
+
_SALT = b"sgl-e2e-v2-salt"
|
|
22
|
+
_INFO_INPUT = b"sgl-e2e-v2-input"
|
|
23
|
+
_INFO_OUTPUT = b"sgl-e2e-v2-output"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _hkdf(shared: bytes, info: bytes) -> bytes:
|
|
27
|
+
return HKDF(algorithm=hashes.SHA256(), length=32, salt=_SALT, info=info).derive(shared)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _aad_input(node_b58: str, eph_b58: str, resp_b58: str) -> bytes:
|
|
31
|
+
return f"sgl-aad/v2/input|node={node_b58}|eph={eph_b58}|resp={resp_b58}".encode()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _aad_output(resp_b58: str, eph_b58: str) -> bytes:
|
|
35
|
+
return f"sgl-aad/v2/output|resp={resp_b58}|eph={eph_b58}".encode()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _aad_stream(resp_b58: str, eph_b58: str, nonce_b58: str, seq: int, final: bool) -> bytes:
|
|
39
|
+
f = 1 if final else 0
|
|
40
|
+
return (
|
|
41
|
+
f"sgl-aad/v2/stream|resp={resp_b58}|eph={eph_b58}|nonce={nonce_b58}|seq={seq}|final={f}"
|
|
42
|
+
).encode()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _b58e(b: bytes) -> str:
|
|
46
|
+
return base58.b58encode(b).decode()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _b58d(s: str) -> bytes:
|
|
50
|
+
return base58.b58decode(s)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _gen_keypair() -> tuple[bytes, bytes]:
|
|
54
|
+
sk = bindings.randombytes(32)
|
|
55
|
+
pk = bindings.crypto_scalarmult_base(sk)
|
|
56
|
+
return sk, pk
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def new_response_keypair() -> tuple[bytes, str]:
|
|
60
|
+
"""The caller's response keypair — the node seals its reply to this."""
|
|
61
|
+
sk, pk = _gen_keypair()
|
|
62
|
+
return sk, _b58e(pk)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def random_nonce_b58() -> str:
|
|
66
|
+
"""A per-request nonce bound into every stream chunk's AAD."""
|
|
67
|
+
return _b58e(bindings.randombytes(16))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def seal_input(node_pub_b58: str, resp_pub_b58: str, plaintext: bytes) -> tuple[str, str]:
|
|
71
|
+
"""Seal the prompt to the node's X25519 key. Returns (ciphertext_b58, ephemeral_pub_b58)."""
|
|
72
|
+
node_pub = _b58d(node_pub_b58)
|
|
73
|
+
eph_sk, eph_pk = _gen_keypair()
|
|
74
|
+
eph_b58 = _b58e(eph_pk)
|
|
75
|
+
shared = bindings.crypto_scalarmult(eph_sk, node_pub)
|
|
76
|
+
key = _hkdf(shared, _INFO_INPUT)
|
|
77
|
+
nonce = bindings.randombytes(24)
|
|
78
|
+
aad = _aad_input(node_pub_b58, eph_b58, resp_pub_b58)
|
|
79
|
+
ct = bindings.crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, aad, nonce, key)
|
|
80
|
+
return _b58e(nonce + ct), eph_b58
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def open_output(resp_sk: bytes, resp_pub_b58: str, node_eph_b58: str, ct_b58: str) -> bytes:
|
|
84
|
+
"""Open the node's (non-stream) reply sealed to our response key."""
|
|
85
|
+
shared = bindings.crypto_scalarmult(resp_sk, _b58d(node_eph_b58))
|
|
86
|
+
key = _hkdf(shared, _INFO_OUTPUT)
|
|
87
|
+
aad = _aad_output(resp_pub_b58, node_eph_b58)
|
|
88
|
+
blob = _b58d(ct_b58)
|
|
89
|
+
return bindings.crypto_aead_xchacha20poly1305_ietf_decrypt(blob[24:], aad, blob[:24], key)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def stream_out_key(resp_sk: bytes, node_stream_eph_b58: str) -> bytes:
|
|
93
|
+
"""Derive the stream output key once from the node's stream ephemeral (chunk 0)."""
|
|
94
|
+
shared = bindings.crypto_scalarmult(resp_sk, _b58d(node_stream_eph_b58))
|
|
95
|
+
return _hkdf(shared, _INFO_OUTPUT)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def open_stream_chunk(
|
|
99
|
+
out_key: bytes,
|
|
100
|
+
resp_pub_b58: str,
|
|
101
|
+
eph_b58: str,
|
|
102
|
+
nonce_b58: str,
|
|
103
|
+
seq: int,
|
|
104
|
+
final: bool,
|
|
105
|
+
ct_b58: str,
|
|
106
|
+
) -> bytes:
|
|
107
|
+
"""Open one stream chunk with the precomputed key + nonce/seq/final-bound AAD."""
|
|
108
|
+
aad = _aad_stream(resp_pub_b58, eph_b58, nonce_b58, seq, final)
|
|
109
|
+
blob = _b58d(ct_b58)
|
|
110
|
+
return bindings.crypto_aead_xchacha20poly1305_ietf_decrypt(blob[24:], aad, blob[:24], out_key)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
# Cross-language vector from the node's encryption.rs: proves this module's
|
|
115
|
+
# X25519+HKDF+XChaCha20+AAD+base58 match the Rust/TS/browser clients byte-for-byte.
|
|
116
|
+
import hashlib
|
|
117
|
+
import json
|
|
118
|
+
|
|
119
|
+
node_secret = bytes([0x42] * 32)
|
|
120
|
+
node_x25519_sk = hashlib.sha256(b"sgl-x25519-derive:" + node_secret).digest()
|
|
121
|
+
node_pub_b58 = _b58e(bindings.crypto_scalarmult_base(node_x25519_sk))
|
|
122
|
+
|
|
123
|
+
eph_b58 = "DQFdwcBsqukJEBn9UNfQruaTHKHxHFMVRA2B5qZuFdfB"
|
|
124
|
+
resp_b58 = "2L54SXdEHm5mraF2X2GPid3m4PSkwVehEvhk487mWTx8"
|
|
125
|
+
ct_b58 = ("gbEA6dFFVPxdar6e8QsjPKWj7xHcBo32nAqweQC5arnt4M5LmHhjREKoUTdVZsU6mmkxKu1Xmv"
|
|
126
|
+
"Eo4oG8EUySndq2ytTyzDgyfMjyBSmPE2fqjdPDzKYtdrC2kZbAfCXv227GczHgmtQBqchA5qMB5"
|
|
127
|
+
"ydxgxYnk9V8jb8sifTjHM61iEQkisdwYCqna")
|
|
128
|
+
|
|
129
|
+
shared = bindings.crypto_scalarmult(node_x25519_sk, _b58d(eph_b58))
|
|
130
|
+
key = _hkdf(shared, _INFO_INPUT)
|
|
131
|
+
aad = _aad_input(node_pub_b58, eph_b58, resp_b58)
|
|
132
|
+
blob = _b58d(ct_b58)
|
|
133
|
+
pt = bindings.crypto_aead_xchacha20poly1305_ietf_decrypt(blob[24:], aad, blob[:24], key)
|
|
134
|
+
got = json.loads(pt)
|
|
135
|
+
expected = {"messages": [{"role": "user", "content": "cross-lang v2 test"}],
|
|
136
|
+
"temperature": 0.7, "max_tokens": 512}
|
|
137
|
+
assert got == expected, f"MISMATCH: {got}"
|
|
138
|
+
print("OK: singularity-grid E2E crypto matches the cross-language vector")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|