Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ All notable changes to `citrate-labs-sdk` are documented here. This project adhe
bytes (duplicate object keys are refused), and sends the same bytes.
`_send_transaction` accepts an already-serialised JSON payload.
- The key-share guard recognises byte values and their JSON renderings.
- The key-share guard also accepts whole-number floats and signed values as
byte values, matches the Buffer JSON shape whatever other keys are present,
and treats uppercase `X`/`Y` like `x`/`y`. It is a safety net against
accidental inclusion; the SDK never places key shares in metadata itself.

### Tests

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ print(gw.chat_completions(
Runnable examples live in `examples/` (`basic_usage.py`, `encrypted_inference.py`,
`marketplace_demo.py`). Verify it's up: `get_chain_id()` returning `40204` confirms the RPC.

`deploy_model` never places key shares in deployment metadata; its key-share check on
caller-supplied metadata is a safety net against accidental inclusion, not a guarantee
against deliberately re-encoded data.

> Security: the client warns/fails on a remote plaintext `http://` RPC (keys and signed
> transactions would go out in cleartext). Loopback `http://` is always allowed; pass
> `allow_insecure_http=True` for a trusted TLS-less internal host.
Expand Down
24 changes: 16 additions & 8 deletions citrate_sdk/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,13 @@ def _share_x(x: Any) -> bool:


def _is_byte_int(v: Any) -> bool:
return isinstance(v, int) and not isinstance(v, bool) and 0 <= v <= 255
"""A byte value as it may appear in JSON: a whole number in -128..255
(signed or unsigned), as an int or a whole-number float."""
if isinstance(v, bool):
return False
if isinstance(v, float):
return v.is_integer() and -128 <= v <= 255
return isinstance(v, int) and -128 <= v <= 255


def _bytes_like_len(y: Any) -> int:
Expand All @@ -77,7 +83,7 @@ def _bytes_like_len(y: Any) -> int:
if isinstance(y, (list, tuple)):
return len(y) if all(_is_byte_int(v) for v in y) else 0
if isinstance(y, dict):
if set(y) == {"type", "data"} and y.get("type") == "Buffer":
if y.get("type") == "Buffer" and "data" in y:
return _bytes_like_len(list(y["data"])) if isinstance(y.get("data"), list) else 0
n = len(y)
if n and all(isinstance(k, str) for k in y) and set(y) == {str(i) for i in range(n)}:
Expand Down Expand Up @@ -125,12 +131,14 @@ def _looks_like_share(d: dict[Any, Any]) -> bool:
"""A raw Shamir share ({x in 1..255, y of share length as hex or bytes})
or a holder-wrapped share record ({holder_public_key/holderPublicKey,
envelope}). Short or coordinate-like values are not treated as shares."""
y = d.get("y")
if "x" in d and _share_x(d["x"]):
if _bytes_like_len(y) >= _MIN_SHARE_BYTES:
return True
if isinstance(y, str) and _share_y_like(y):
return True
xs = [d[k] for k in ("x", "X") if k in d]
ys = [d[k] for k in ("y", "Y") if k in d]
if any(_share_x(x) for x in xs):
for y in ys:
if _bytes_like_len(y) >= _MIN_SHARE_BYTES:
return True
if isinstance(y, str) and _share_y_like(y):
return True
return "envelope" in d and ("holder_public_key" in d or "holderPublicKey" in d)


Expand Down
296 changes: 295 additions & 1 deletion tests/fixtures/share_guard_vectors.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"_comment": "Shared share-guard test vectors. The SAME file is committed to citrate-sdk-js and citrate-sdk-python (tests/fixtures/share_guard_vectors.json); each SDK's guard must refuse every refuse:true entry and accept every refuse:false entry. Keep the two copies byte-identical. raw_payloads are serialised payload texts for each SDK's payload guard (strict parse, then guard).",
"version": 2,
"version": 3,
"vectors": [
{
"name": "int x, 64-hex y",
Expand Down Expand Up @@ -752,6 +752,300 @@
2
]
}
},
{
"name": "y as whole-number float list",
"refuse": true,
"meta": {
"a": {
"x": 1,
"y": [
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0,
171.0
]
}
}
},
{
"name": "JSON string with float byte list",
"refuse": true,
"meta": {
"blob": "{\"x\": 1, \"y\": [1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2, 1.71e2]}"
}
},
{
"name": "Buffer shape with an extra key",
"refuse": true,
"meta": {
"a": {
"x": 1,
"y": {
"type": "Buffer",
"data": [
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171
],
"extra": 1
}
}
}
},
{
"name": "y as signed byte list",
"refuse": true,
"meta": {
"a": {
"x": 1,
"y": [
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85,
-85
]
}
}
},
{
"name": "uppercase X and Y",
"refuse": true,
"meta": {
"a": {
"X": 1,
"Y": "abababababababababababababababababababababababababababababababab"
}
}
},
{
"name": "uppercase X, lowercase y",
"refuse": true,
"meta": {
"a": {
"X": "3",
"y": "abababababababababababababababababababababababababababababababab"
}
}
},
{
"name": "list with values below -128",
"refuse": false,
"meta": {
"x": 1,
"y": [
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129,
-129
]
}
},
{
"name": "list with fractional floats",
"refuse": false,
"meta": {
"x": 1,
"y": [
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5,
171.5
]
}
},
{
"name": "uppercase X and Y coordinates",
"refuse": false,
"meta": {
"X": 1,
"Y": "10"
}
},
{
"name": "Buffer shape with non-Buffer type",
"refuse": false,
"meta": {
"x": 1,
"y": {
"type": "Other",
"data": [
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171,
171
]
}
}
}
],
"raw_payloads": [
Expand Down
Loading
Loading