zerostyl-sdk 0.1.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.
@@ -0,0 +1,21 @@
1
+ /target
2
+ target/
3
+ Cargo.lock
4
+ **/*.rs.bk
5
+ *.pdb
6
+ .DS_Store
7
+ .claude/worktrees/
8
+ .zerostyl_cache/
9
+ proof.bin
10
+ public_inputs.json
11
+
12
+ # Node / pnpm
13
+ node_modules/
14
+ packages/**/dist/
15
+ pnpm-debug.log
16
+ .pnpm-store/
17
+
18
+ # Python
19
+ packages/**/.venv/
20
+ __pycache__/
21
+ *.egg-info/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Zakaria Chaikhi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.4
2
+ Name: zerostyl-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the ZeroStyl zk toolkit on Arbitrum Stylus: ABI schema types and typed-bindings codegen
5
+ Project-URL: Repository, https://github.com/kazai777/zerostyl
6
+ Author-email: kazai777 <kazai777.dev@gmail.com>
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Keywords: arbitrum,halo2,privacy,stylus,zero-knowledge,zk-snarks
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Security :: Cryptography
18
+ Requires-Python: >=3.10
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=8; extra == 'dev'
21
+ Requires-Dist: ruff>=0.5; extra == 'dev'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # zerostyl-sdk (Python)
25
+
26
+ Python SDK for the [ZeroStyl](https://github.com/kazai777/zerostyl) zk toolkit on
27
+ Arbitrum Stylus. Parses the exporter's `abi.json` documents into typed dataclasses and
28
+ generates typed Python bindings — the same scope as `@zerostyl/sdk-ts`, in Python.
29
+
30
+ Pure Python, zero runtime dependencies, Python ≥ 3.10.
31
+
32
+ ## Install (from the repo)
33
+
34
+ ```bash
35
+ cd packages/sdk-py
36
+ python -m pip install ".[dev]"
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ```python
42
+ from pathlib import Path
43
+ from zerostyl_sdk import parse_abi_schema, generate_bindings
44
+
45
+ abi = parse_abi_schema(Path("abi.json").read_text())
46
+ print(abi.circuit.name, abi.circuit.num_public_inputs)
47
+
48
+ bindings = generate_bindings(abi) # a Python module as a string
49
+ Path("deposit_bindings.py").write_text(bindings)
50
+ ```
51
+
52
+ CLI (equivalent of the TS SDK's `zerostyl-sdk generate`):
53
+
54
+ ```bash
55
+ # after `pip install` (see above), either form works:
56
+ zerostyl-sdk-py generate --abi ../../examples/zk_private_demo/abi.json --out bindings.py
57
+ python -m zerostyl_sdk generate --abi ../../examples/zk_private_demo/abi.json
58
+ ```
59
+
60
+ Generated output for the demo circuit:
61
+
62
+ ```python
63
+ DEPOSIT_CIRCUIT: Final = {
64
+ "name": "deposit",
65
+ ...
66
+ }
67
+
68
+ @dataclass(frozen=True)
69
+ class DepositWitness:
70
+ collateral: str
71
+ collateral_nonce: str
72
+ threshold: str
73
+
74
+ @dataclass(frozen=True)
75
+ class DepositPublicInputs:
76
+ collateral_commitment: str
77
+ ```
78
+
79
+ Type mapping: `u64`/`u128` → `int`, `bool` → `bool`, `fp`/`bytes32`/`address` → `str`
80
+ (`0x`-prefixed hex), arrays → `tuple[..., ...]`.
81
+
82
+ ## Tests
83
+
84
+ ```bash
85
+ python -m pytest
86
+ ruff check src tests
87
+ ```
88
+
89
+ The snapshot test locks the generated bindings for
90
+ `examples/zk_private_demo/abi.json`; regenerate with
91
+ `REGEN_SDK_PY_SNAPSHOTS=1 python -m pytest`.
92
+
93
+ ## Scope and future work
94
+
95
+ Codegen only — proof generation from Python (bindings to the Rust prover) and on-chain
96
+ submission helpers are future work, mirroring the TypeScript SDK's roadmap. The package
97
+ is not yet published to PyPI.
98
+
99
+ ## License
100
+
101
+ MIT — see the repository's [LICENSE](../../LICENSE).
@@ -0,0 +1,78 @@
1
+ # zerostyl-sdk (Python)
2
+
3
+ Python SDK for the [ZeroStyl](https://github.com/kazai777/zerostyl) zk toolkit on
4
+ Arbitrum Stylus. Parses the exporter's `abi.json` documents into typed dataclasses and
5
+ generates typed Python bindings — the same scope as `@zerostyl/sdk-ts`, in Python.
6
+
7
+ Pure Python, zero runtime dependencies, Python ≥ 3.10.
8
+
9
+ ## Install (from the repo)
10
+
11
+ ```bash
12
+ cd packages/sdk-py
13
+ python -m pip install ".[dev]"
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```python
19
+ from pathlib import Path
20
+ from zerostyl_sdk import parse_abi_schema, generate_bindings
21
+
22
+ abi = parse_abi_schema(Path("abi.json").read_text())
23
+ print(abi.circuit.name, abi.circuit.num_public_inputs)
24
+
25
+ bindings = generate_bindings(abi) # a Python module as a string
26
+ Path("deposit_bindings.py").write_text(bindings)
27
+ ```
28
+
29
+ CLI (equivalent of the TS SDK's `zerostyl-sdk generate`):
30
+
31
+ ```bash
32
+ # after `pip install` (see above), either form works:
33
+ zerostyl-sdk-py generate --abi ../../examples/zk_private_demo/abi.json --out bindings.py
34
+ python -m zerostyl_sdk generate --abi ../../examples/zk_private_demo/abi.json
35
+ ```
36
+
37
+ Generated output for the demo circuit:
38
+
39
+ ```python
40
+ DEPOSIT_CIRCUIT: Final = {
41
+ "name": "deposit",
42
+ ...
43
+ }
44
+
45
+ @dataclass(frozen=True)
46
+ class DepositWitness:
47
+ collateral: str
48
+ collateral_nonce: str
49
+ threshold: str
50
+
51
+ @dataclass(frozen=True)
52
+ class DepositPublicInputs:
53
+ collateral_commitment: str
54
+ ```
55
+
56
+ Type mapping: `u64`/`u128` → `int`, `bool` → `bool`, `fp`/`bytes32`/`address` → `str`
57
+ (`0x`-prefixed hex), arrays → `tuple[..., ...]`.
58
+
59
+ ## Tests
60
+
61
+ ```bash
62
+ python -m pytest
63
+ ruff check src tests
64
+ ```
65
+
66
+ The snapshot test locks the generated bindings for
67
+ `examples/zk_private_demo/abi.json`; regenerate with
68
+ `REGEN_SDK_PY_SNAPSHOTS=1 python -m pytest`.
69
+
70
+ ## Scope and future work
71
+
72
+ Codegen only — proof generation from Python (bindings to the Rust prover) and on-chain
73
+ submission helpers are future work, mirroring the TypeScript SDK's roadmap. The package
74
+ is not yet published to PyPI.
75
+
76
+ ## License
77
+
78
+ MIT — see the repository's [LICENSE](../../LICENSE).
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "zerostyl-sdk"
7
+ version = "0.1.0"
8
+ description = "Python SDK for the ZeroStyl zk toolkit on Arbitrum Stylus: ABI schema types and typed-bindings codegen"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "kazai777", email = "kazai777.dev@gmail.com" }]
12
+ requires-python = ">=3.10"
13
+ dependencies = []
14
+ keywords = ["arbitrum", "stylus", "zero-knowledge", "privacy", "zk-snarks", "halo2"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: Security :: Cryptography",
24
+ ]
25
+
26
+ [project.urls]
27
+ Repository = "https://github.com/kazai777/zerostyl"
28
+
29
+ [project.scripts]
30
+ zerostyl-sdk-py = "zerostyl_sdk.cli:main"
31
+
32
+ [project.optional-dependencies]
33
+ dev = ["pytest>=8", "ruff>=0.5"]
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/zerostyl_sdk"]
37
+
38
+ [tool.ruff]
39
+ line-length = 100
40
+ target-version = "py310"
41
+
42
+ [tool.ruff.lint]
43
+ # Pinned rule set so local runs and CI agree regardless of user-level config.
44
+ select = ["E4", "E7", "E9", "F", "I", "UP", "B"]
45
+
46
+ [tool.pytest.ini_options]
47
+ testpaths = ["tests"]
@@ -0,0 +1,40 @@
1
+ """Python SDK for the ZeroStyl zk toolkit on Arbitrum Stylus.
2
+
3
+ Parses the exporter's ``abi.json`` documents into typed dataclasses and
4
+ generates typed Python bindings (mirroring ``@zerostyl/sdk-ts``).
5
+ """
6
+
7
+ from .codegen.generator import generate_bindings
8
+ from .codegen.type_mapping import field_type_to_py
9
+ from .types import (
10
+ ABI_VERSION,
11
+ AbiSchema,
12
+ CircuitMetadata,
13
+ FieldType,
14
+ OnChainBinding,
15
+ ProofMetadata,
16
+ PublicInputField,
17
+ PublicInputsSchema,
18
+ WitnessField,
19
+ WitnessSchema,
20
+ parse_abi_schema,
21
+ )
22
+
23
+ __version__ = "0.1.0"
24
+
25
+ __all__ = [
26
+ "ABI_VERSION",
27
+ "AbiSchema",
28
+ "CircuitMetadata",
29
+ "FieldType",
30
+ "OnChainBinding",
31
+ "ProofMetadata",
32
+ "PublicInputField",
33
+ "PublicInputsSchema",
34
+ "WitnessField",
35
+ "WitnessSchema",
36
+ "__version__",
37
+ "field_type_to_py",
38
+ "generate_bindings",
39
+ "parse_abi_schema",
40
+ ]
@@ -0,0 +1,5 @@
1
+ """Entry point for ``python -m zerostyl_sdk``."""
2
+
3
+ from .cli import main
4
+
5
+ raise SystemExit(main())
@@ -0,0 +1,53 @@
1
+ """Command-line interface: ``zerostyl-sdk-py generate --abi <file> [--out <file>]``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from .codegen.generator import generate_bindings
10
+ from .types import parse_abi_schema
11
+
12
+
13
+ def build_parser() -> argparse.ArgumentParser:
14
+ parser = argparse.ArgumentParser(
15
+ prog="zerostyl-sdk-py",
16
+ description="Generate typed Python bindings from a ZeroStyl abi.json",
17
+ )
18
+ subparsers = parser.add_subparsers(dest="command", required=True)
19
+
20
+ generate = subparsers.add_parser("generate", help="generate bindings from an abi.json")
21
+ generate.add_argument("--abi", "-a", required=True, help="path to the abi.json file")
22
+ generate.add_argument(
23
+ "--out", "-o", help="output .py file (prints to stdout when omitted)"
24
+ )
25
+ return parser
26
+
27
+
28
+ def main(argv: list[str] | None = None) -> int:
29
+ args = build_parser().parse_args(argv)
30
+ if args.command != "generate": # pragma: no cover — argparse enforces this
31
+ return 2
32
+
33
+ abi_path = Path(args.abi)
34
+ try:
35
+ abi = parse_abi_schema(abi_path.read_text(encoding="utf-8"))
36
+ except OSError as exc:
37
+ print(f"error: cannot read {abi_path}: {exc}", file=sys.stderr)
38
+ return 1
39
+ except ValueError as exc:
40
+ print(f"error: {exc}", file=sys.stderr)
41
+ return 1
42
+
43
+ bindings = generate_bindings(abi)
44
+ if args.out:
45
+ Path(args.out).write_text(bindings, encoding="utf-8")
46
+ print(f"wrote {args.out}", file=sys.stderr)
47
+ else:
48
+ print(bindings, end="")
49
+ return 0
50
+
51
+
52
+ if __name__ == "__main__": # pragma: no cover
53
+ raise SystemExit(main())
@@ -0,0 +1,4 @@
1
+ from .generator import generate_bindings
2
+ from .type_mapping import field_type_to_py
3
+
4
+ __all__ = ["field_type_to_py", "generate_bindings"]
@@ -0,0 +1,75 @@
1
+ """Generate Python bindings from an ABI schema.
2
+
3
+ The generated module mirrors the TypeScript SDK's output: a circuit-metadata
4
+ constant plus frozen dataclasses for the witness and the public inputs.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import keyword
10
+
11
+ from ..types import AbiSchema, PublicInputField, WitnessField
12
+ from .type_mapping import field_type_to_py
13
+
14
+ HEADER = "# auto-generated by zerostyl-sdk (python) — do not edit"
15
+
16
+
17
+ def _safe_ident(name: str) -> str:
18
+ """Make an ABI field name a valid, non-keyword Python identifier.
19
+
20
+ ABI names are Rust identifiers, so the only collision is a Python keyword
21
+ (e.g. a field literally named ``from``): append an underscore, matching
22
+ PEP 8's convention for keyword-clashing names.
23
+ """
24
+ return f"{name}_" if keyword.iskeyword(name) else name
25
+
26
+
27
+ def generate_bindings(abi: AbiSchema) -> str:
28
+ """Emit a Python module with typed bindings for one circuit."""
29
+ pascal = _pascal_case(abi.circuit.name)
30
+ upper = abi.circuit.name.upper()
31
+ sections = [
32
+ HEADER,
33
+ "",
34
+ "from dataclasses import dataclass",
35
+ "from typing import Final",
36
+ "",
37
+ _emit_circuit_const(upper, abi),
38
+ "",
39
+ _emit_dataclass(f"{pascal}Witness", abi.witness.fields),
40
+ "",
41
+ _emit_dataclass(f"{pascal}PublicInputs", abi.public_inputs.fields),
42
+ "",
43
+ ]
44
+ return "\n".join(sections)
45
+
46
+
47
+ def _emit_circuit_const(upper: str, abi: AbiSchema) -> str:
48
+ c = abi.circuit
49
+ return "\n".join(
50
+ [
51
+ f"{upper}_CIRCUIT: Final = {{",
52
+ f' "name": {c.name!r},',
53
+ f' "version": {c.version!r},',
54
+ f' "default_k": {c.default_k},',
55
+ f' "num_public_inputs": {c.num_public_inputs},',
56
+ f' "num_private_witnesses": {c.num_private_witnesses},',
57
+ "}",
58
+ ]
59
+ )
60
+
61
+
62
+ def _emit_dataclass(
63
+ name: str, fields: tuple[WitnessField, ...] | tuple[PublicInputField, ...]
64
+ ) -> str:
65
+ lines = ["@dataclass(frozen=True)", f"class {name}:"]
66
+ if not fields:
67
+ lines.append(" pass")
68
+ else:
69
+ for f in fields:
70
+ lines.append(f" {_safe_ident(f.name)}: {field_type_to_py(f.kind)}")
71
+ return "\n".join(lines)
72
+
73
+
74
+ def _pascal_case(name: str) -> str:
75
+ return "".join(part.capitalize() for part in name.split("_") if part)
@@ -0,0 +1,23 @@
1
+ """Mapping from ABI field types to Python type annotations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..types import FieldType
6
+
7
+ _SCALAR_TO_PY = {
8
+ "u64": "int",
9
+ "u128": "int",
10
+ "bool": "bool",
11
+ # Field elements, hashes, and addresses travel as 0x-prefixed hex strings.
12
+ "fp": "str",
13
+ "bytes32": "str",
14
+ "address": "str",
15
+ }
16
+
17
+
18
+ def field_type_to_py(field_type: FieldType) -> str:
19
+ """Return the Python annotation for an ABI field type."""
20
+ if field_type.is_array:
21
+ assert field_type.kind is not None # guaranteed by FieldType validation
22
+ return f"tuple[{field_type_to_py(field_type.kind)}, ...]"
23
+ return _SCALAR_TO_PY[field_type.type]
@@ -0,0 +1,217 @@
1
+ """Typed mirror of the ZeroStyl ABI schema (``abi.json``).
2
+
3
+ The dataclasses here map 1:1 onto the Rust ``zerostyl_circuits::abi::AbiSchema``
4
+ (and the TypeScript ``@zerostyl/sdk-ts`` types): circuit metadata, private
5
+ witness fields, public inputs, and proof metadata.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from dataclasses import dataclass, field
12
+ from typing import Any
13
+
14
+ ABI_VERSION = 1
15
+
16
+ _SCALAR_TAGS = frozenset({"u64", "u128", "bool", "bytes32", "address", "fp"})
17
+ _PROVING_SYSTEMS = frozenset(
18
+ {"halo2_ipa", "halo2_kzg_groth16_wrap", "halo2_kzg", "stark_fri"}
19
+ )
20
+ _VISIBILITIES = frozenset({"private", "public"})
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class FieldType:
25
+ """A field's wire type: a scalar tag, or ``array`` with ``kind``/``len``."""
26
+
27
+ type: str
28
+ kind: FieldType | None = None
29
+ len: int | None = None
30
+
31
+ def __post_init__(self) -> None:
32
+ if self.type == "array":
33
+ if self.kind is None or self.len is None:
34
+ raise ValueError("array field type requires `kind` and `len`")
35
+ elif self.type in _SCALAR_TAGS:
36
+ if self.kind is not None or self.len is not None:
37
+ raise ValueError(f"scalar field type `{self.type}` takes no `kind`/`len`")
38
+ else:
39
+ raise ValueError(f"unknown field type tag `{self.type}`")
40
+
41
+ @property
42
+ def is_array(self) -> bool:
43
+ return self.type == "array"
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class WitnessField:
48
+ name: str
49
+ kind: FieldType
50
+ visibility: str
51
+ description: str | None = None
52
+
53
+ def __post_init__(self) -> None:
54
+ if self.visibility not in _VISIBILITIES:
55
+ raise ValueError(f"unknown visibility `{self.visibility}`")
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class WitnessSchema:
60
+ fields: tuple[WitnessField, ...] = field(default_factory=tuple)
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class PublicInputField:
65
+ name: str
66
+ kind: FieldType
67
+ description: str | None = None
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class PublicInputsSchema:
72
+ fields: tuple[PublicInputField, ...] = field(default_factory=tuple)
73
+
74
+
75
+ @dataclass(frozen=True)
76
+ class CircuitMetadata:
77
+ name: str
78
+ version: str
79
+ description: str
80
+ default_k: int
81
+ num_public_inputs: int
82
+ num_private_witnesses: int
83
+
84
+
85
+ @dataclass(frozen=True)
86
+ class ProofMetadata:
87
+ format_version: int
88
+ proving_system: str
89
+ approx_size_bytes: int | None = None
90
+
91
+ def __post_init__(self) -> None:
92
+ if self.proving_system not in _PROVING_SYSTEMS:
93
+ raise ValueError(f"unknown proving system `{self.proving_system}`")
94
+
95
+
96
+ @dataclass(frozen=True)
97
+ class OnChainBinding:
98
+ chain_id: int
99
+ contract_address: str
100
+
101
+
102
+ @dataclass(frozen=True)
103
+ class AbiSchema:
104
+ abi_version: int
105
+ circuit: CircuitMetadata
106
+ witness: WitnessSchema
107
+ public_inputs: PublicInputsSchema
108
+ proof: ProofMetadata
109
+ on_chain: OnChainBinding | None = None
110
+
111
+
112
+ def _parse_field_type(data: Any) -> FieldType:
113
+ if not isinstance(data, dict):
114
+ raise ValueError(f"field type must be an object, got {type(data).__name__}")
115
+ tag = data.get("type")
116
+ if tag == "array":
117
+ if "kind" not in data or "len" not in data:
118
+ raise ValueError("array field type requires `kind` and `len`")
119
+ return FieldType(type="array", kind=_parse_field_type(data["kind"]), len=int(data["len"]))
120
+ if not isinstance(tag, str):
121
+ raise ValueError("field type object is missing its `type` tag")
122
+ return FieldType(type=tag)
123
+
124
+
125
+ def _require(data: dict[str, Any], key: str, context: str) -> Any:
126
+ if key not in data:
127
+ raise ValueError(f"missing `{key}` in {context}")
128
+ return data[key]
129
+
130
+
131
+ def parse_abi_schema(json_str: str) -> AbiSchema:
132
+ """Parse and validate an ``abi.json`` document.
133
+
134
+ Raises ``ValueError`` on malformed JSON, unknown enum values, an
135
+ unsupported ``abi_version``, or count mismatches between the circuit
136
+ metadata and the field lists.
137
+ """
138
+ try:
139
+ data = json.loads(json_str)
140
+ except json.JSONDecodeError as exc:
141
+ raise ValueError(f"abi.json is not valid JSON: {exc}") from exc
142
+ if not isinstance(data, dict):
143
+ raise ValueError("abi.json must be a JSON object")
144
+
145
+ abi_version = int(_require(data, "abi_version", "abi.json"))
146
+ if abi_version != ABI_VERSION:
147
+ raise ValueError(
148
+ f"unsupported abi_version {abi_version} (this SDK supports {ABI_VERSION})"
149
+ )
150
+
151
+ circuit_data = _require(data, "circuit", "abi.json")
152
+ circuit = CircuitMetadata(
153
+ name=str(_require(circuit_data, "name", "circuit")),
154
+ version=str(_require(circuit_data, "version", "circuit")),
155
+ description=str(_require(circuit_data, "description", "circuit")),
156
+ default_k=int(_require(circuit_data, "default_k", "circuit")),
157
+ num_public_inputs=int(_require(circuit_data, "num_public_inputs", "circuit")),
158
+ num_private_witnesses=int(_require(circuit_data, "num_private_witnesses", "circuit")),
159
+ )
160
+
161
+ witness = WitnessSchema(
162
+ fields=tuple(
163
+ WitnessField(
164
+ name=str(_require(f, "name", "witness field")),
165
+ kind=_parse_field_type(_require(f, "kind", "witness field")),
166
+ visibility=str(_require(f, "visibility", "witness field")),
167
+ description=f.get("description"),
168
+ )
169
+ for f in _require(data, "witness", "abi.json")["fields"]
170
+ )
171
+ )
172
+
173
+ public_inputs = PublicInputsSchema(
174
+ fields=tuple(
175
+ PublicInputField(
176
+ name=str(_require(f, "name", "public input field")),
177
+ kind=_parse_field_type(_require(f, "kind", "public input field")),
178
+ description=f.get("description"),
179
+ )
180
+ for f in _require(data, "public_inputs", "abi.json")["fields"]
181
+ )
182
+ )
183
+
184
+ proof_data = _require(data, "proof", "abi.json")
185
+ proof = ProofMetadata(
186
+ format_version=int(_require(proof_data, "format_version", "proof")),
187
+ proving_system=str(_require(proof_data, "proving_system", "proof")),
188
+ approx_size_bytes=proof_data.get("approx_size_bytes"),
189
+ )
190
+
191
+ on_chain = None
192
+ if data.get("on_chain") is not None:
193
+ oc = data["on_chain"]
194
+ on_chain = OnChainBinding(
195
+ chain_id=int(_require(oc, "chain_id", "on_chain")),
196
+ contract_address=str(_require(oc, "contract_address", "on_chain")),
197
+ )
198
+
199
+ if circuit.num_public_inputs != len(public_inputs.fields):
200
+ raise ValueError(
201
+ f"circuit.num_public_inputs ({circuit.num_public_inputs}) does not match "
202
+ f"public_inputs.fields length ({len(public_inputs.fields)})"
203
+ )
204
+ if circuit.num_private_witnesses != len(witness.fields):
205
+ raise ValueError(
206
+ f"circuit.num_private_witnesses ({circuit.num_private_witnesses}) does not match "
207
+ f"witness.fields length ({len(witness.fields)})"
208
+ )
209
+
210
+ return AbiSchema(
211
+ abi_version=abi_version,
212
+ circuit=circuit,
213
+ witness=witness,
214
+ public_inputs=public_inputs,
215
+ proof=proof,
216
+ on_chain=on_chain,
217
+ )
@@ -0,0 +1,22 @@
1
+ # auto-generated by zerostyl-sdk (python) — do not edit
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Final
5
+
6
+ DEPOSIT_CIRCUIT: Final = {
7
+ "name": 'deposit',
8
+ "version": '1.0.0',
9
+ "default_k": 10,
10
+ "num_public_inputs": 1,
11
+ "num_private_witnesses": 3,
12
+ }
13
+
14
+ @dataclass(frozen=True)
15
+ class DepositWitness:
16
+ collateral: int
17
+ collateral_nonce: str
18
+ threshold: int
19
+
20
+ @dataclass(frozen=True)
21
+ class DepositPublicInputs:
22
+ collateral_commitment: str
@@ -0,0 +1,47 @@
1
+ import json
2
+ import subprocess
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ from zerostyl_sdk.cli import main
7
+
8
+ DEMO_ABI = (
9
+ Path(__file__).resolve().parents[3] / "examples" / "zk_private_demo" / "abi.json"
10
+ )
11
+
12
+
13
+ def test_generate_to_stdout(capsys):
14
+ assert main(["generate", "--abi", str(DEMO_ABI)]) == 0
15
+ out = capsys.readouterr().out
16
+ assert "class DepositWitness:" in out
17
+
18
+
19
+ def test_generate_to_file(tmp_path):
20
+ out_file = tmp_path / "bindings.py"
21
+ assert main(["generate", "--abi", str(DEMO_ABI), "--out", str(out_file)]) == 0
22
+ content = out_file.read_text(encoding="utf-8")
23
+ compile(content, str(out_file), "exec")
24
+ assert "DEPOSIT_CIRCUIT" in content
25
+
26
+
27
+ def test_missing_abi_file_fails(tmp_path, capsys):
28
+ assert main(["generate", "--abi", str(tmp_path / "nope.json")]) == 1
29
+ assert "cannot read" in capsys.readouterr().err
30
+
31
+
32
+ def test_invalid_abi_fails(tmp_path, capsys):
33
+ bad = tmp_path / "bad.json"
34
+ bad.write_text(json.dumps({"abi_version": 1}), encoding="utf-8")
35
+ assert main(["generate", "--abi", str(bad)]) == 1
36
+ assert "error:" in capsys.readouterr().err
37
+
38
+
39
+ def test_module_entry_point_runs():
40
+ result = subprocess.run(
41
+ [sys.executable, "-m", "zerostyl_sdk", "generate", "--abi", str(DEMO_ABI)],
42
+ capture_output=True,
43
+ text=True,
44
+ check=False,
45
+ )
46
+ assert result.returncode == 0
47
+ assert "class DepositWitness:" in result.stdout
@@ -0,0 +1,97 @@
1
+ import json
2
+
3
+ from zerostyl_sdk import generate_bindings, parse_abi_schema
4
+ from zerostyl_sdk.codegen.generator import HEADER
5
+
6
+
7
+ def abi(witness_fields=None, public_fields=None, name="deposit"):
8
+ witness_fields = witness_fields if witness_fields is not None else [
9
+ {"name": "amount", "kind": {"type": "fp"}, "visibility": "private"}
10
+ ]
11
+ public_fields = public_fields if public_fields is not None else [
12
+ {"name": "amount_commitment", "kind": {"type": "fp"}}
13
+ ]
14
+ return parse_abi_schema(
15
+ json.dumps(
16
+ {
17
+ "abi_version": 1,
18
+ "circuit": {
19
+ "name": name,
20
+ "version": "1.0.0",
21
+ "description": "test",
22
+ "default_k": 10,
23
+ "num_public_inputs": len(public_fields),
24
+ "num_private_witnesses": len(witness_fields),
25
+ },
26
+ "witness": {"fields": witness_fields},
27
+ "public_inputs": {"fields": public_fields},
28
+ "proof": {"format_version": 1, "proving_system": "halo2_kzg"},
29
+ }
30
+ )
31
+ )
32
+
33
+
34
+ def test_generated_module_is_valid_python():
35
+ code = generate_bindings(abi())
36
+ compile(code, "<generated>", "exec")
37
+
38
+
39
+ def test_header_and_const_present():
40
+ code = generate_bindings(abi())
41
+ assert code.startswith(HEADER)
42
+ assert 'DEPOSIT_CIRCUIT: Final = {' in code
43
+ assert '"name": \'deposit\'' in code or "\"name\": 'deposit'" in code
44
+ assert '"default_k": 10' in code
45
+
46
+
47
+ def test_dataclasses_have_typed_fields():
48
+ code = generate_bindings(abi())
49
+ assert "@dataclass(frozen=True)" in code
50
+ assert "class DepositWitness:" in code
51
+ assert " amount: str" in code
52
+ assert "class DepositPublicInputs:" in code
53
+ assert " amount_commitment: str" in code
54
+
55
+
56
+ def test_array_fields_map_to_tuples():
57
+ code = generate_bindings(
58
+ abi(
59
+ witness_fields=[
60
+ {
61
+ "name": "siblings",
62
+ "kind": {"type": "array", "kind": {"type": "fp"}, "len": 32},
63
+ "visibility": "private",
64
+ }
65
+ ]
66
+ )
67
+ )
68
+ assert " siblings: tuple[str, ...]" in code
69
+
70
+
71
+ def test_empty_schemas_generate_pass_dataclasses():
72
+ code = generate_bindings(abi(witness_fields=[], public_fields=[]))
73
+ compile(code, "<generated>", "exec")
74
+ assert "class DepositWitness:\n pass" in code
75
+ assert "class DepositPublicInputs:\n pass" in code
76
+
77
+
78
+ def test_multi_segment_name_pascal_cased():
79
+ code = generate_bindings(abi(name="private_vote_tally"))
80
+ assert "class PrivateVoteTallyWitness:" in code
81
+ assert "PRIVATE_VOTE_TALLY_CIRCUIT: Final = {" in code
82
+
83
+
84
+ def test_python_keyword_field_names_are_sanitized():
85
+ # `from` is a valid Rust identifier but a Python keyword; the generated
86
+ # dataclass must stay importable.
87
+ code = generate_bindings(
88
+ abi(
89
+ witness_fields=[
90
+ {"name": "from", "kind": {"type": "address"}, "visibility": "private"},
91
+ {"name": "amount", "kind": {"type": "u64"}, "visibility": "private"},
92
+ ]
93
+ )
94
+ )
95
+ compile(code, "<generated>", "exec")
96
+ assert " from_: str" in code
97
+ assert " amount: int" in code
@@ -0,0 +1,32 @@
1
+ """Locks the generated bindings for the demo circuit against a committed
2
+ snapshot. Set ``REGEN_SDK_PY_SNAPSHOTS=1`` to overwrite the snapshot after an
3
+ intentional generator change."""
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+ from zerostyl_sdk import generate_bindings, parse_abi_schema
9
+
10
+ DEMO_ABI = (
11
+ Path(__file__).resolve().parents[3] / "examples" / "zk_private_demo" / "abi.json"
12
+ )
13
+ SNAPSHOT = Path(__file__).parent / "snapshots" / "zk_private_demo_snap.py"
14
+
15
+
16
+ def test_demo_bindings_match_snapshot():
17
+ abi = parse_abi_schema(DEMO_ABI.read_text(encoding="utf-8"))
18
+ generated = generate_bindings(abi)
19
+ compile(generated, "<generated>", "exec")
20
+
21
+ if os.environ.get("REGEN_SDK_PY_SNAPSHOTS"):
22
+ SNAPSHOT.parent.mkdir(parents=True, exist_ok=True)
23
+ SNAPSHOT.write_text(generated, encoding="utf-8")
24
+ return
25
+
26
+ assert SNAPSHOT.exists(), (
27
+ f"snapshot {SNAPSHOT} missing — run REGEN_SDK_PY_SNAPSHOTS=1 python -m pytest"
28
+ )
29
+ on_disk = SNAPSHOT.read_text(encoding="utf-8").replace("\r\n", "\n")
30
+ assert on_disk == generated.replace("\r\n", "\n"), (
31
+ "snapshot out of sync — run REGEN_SDK_PY_SNAPSHOTS=1 python -m pytest"
32
+ )
@@ -0,0 +1,29 @@
1
+ import pytest
2
+
3
+ from zerostyl_sdk import FieldType, field_type_to_py
4
+
5
+
6
+ @pytest.mark.parametrize(
7
+ ("tag", "expected"),
8
+ [
9
+ ("u64", "int"),
10
+ ("u128", "int"),
11
+ ("bool", "bool"),
12
+ ("fp", "str"),
13
+ ("bytes32", "str"),
14
+ ("address", "str"),
15
+ ],
16
+ )
17
+ def test_scalar_mappings(tag, expected):
18
+ assert field_type_to_py(FieldType(type=tag)) == expected
19
+
20
+
21
+ def test_array_mapping():
22
+ t = FieldType(type="array", kind=FieldType(type="fp"), len=32)
23
+ assert field_type_to_py(t) == "tuple[str, ...]"
24
+
25
+
26
+ def test_nested_array_mapping():
27
+ inner = FieldType(type="array", kind=FieldType(type="u64"), len=4)
28
+ outer = FieldType(type="array", kind=inner, len=2)
29
+ assert field_type_to_py(outer) == "tuple[tuple[int, ...], ...]"
@@ -0,0 +1,131 @@
1
+ import json
2
+
3
+ import pytest
4
+
5
+ from zerostyl_sdk import ABI_VERSION, FieldType, parse_abi_schema
6
+
7
+
8
+ def sample_abi() -> dict:
9
+ return {
10
+ "abi_version": ABI_VERSION,
11
+ "circuit": {
12
+ "name": "deposit",
13
+ "version": "1.0.0",
14
+ "description": "test circuit",
15
+ "default_k": 10,
16
+ "num_public_inputs": 1,
17
+ "num_private_witnesses": 2,
18
+ },
19
+ "witness": {
20
+ "fields": [
21
+ {"name": "amount", "kind": {"type": "fp"}, "visibility": "private"},
22
+ {"name": "amount_nonce", "kind": {"type": "fp"}, "visibility": "private"},
23
+ ]
24
+ },
25
+ "public_inputs": {
26
+ "fields": [{"name": "amount_commitment", "kind": {"type": "fp"}}]
27
+ },
28
+ "proof": {"format_version": 1, "proving_system": "halo2_kzg"},
29
+ }
30
+
31
+
32
+ def test_parses_valid_schema():
33
+ abi = parse_abi_schema(json.dumps(sample_abi()))
34
+ assert abi.abi_version == ABI_VERSION
35
+ assert abi.circuit.name == "deposit"
36
+ assert abi.witness.fields[0].name == "amount"
37
+ assert abi.public_inputs.fields[0].kind.type == "fp"
38
+ assert abi.proof.proving_system == "halo2_kzg"
39
+ assert abi.on_chain is None
40
+
41
+
42
+ def test_parses_nested_array_field_type():
43
+ data = sample_abi()
44
+ data["witness"]["fields"][0]["kind"] = {
45
+ "type": "array",
46
+ "kind": {"type": "fp"},
47
+ "len": 32,
48
+ }
49
+ abi = parse_abi_schema(json.dumps(data))
50
+ kind = abi.witness.fields[0].kind
51
+ assert kind.is_array
52
+ assert kind.len == 32
53
+ assert kind.kind == FieldType(type="fp")
54
+
55
+
56
+ def test_parses_on_chain_binding():
57
+ data = sample_abi()
58
+ data["on_chain"] = {"chain_id": 421614, "contract_address": "0x" + "aa" * 20}
59
+ abi = parse_abi_schema(json.dumps(data))
60
+ assert abi.on_chain is not None
61
+ assert abi.on_chain.chain_id == 421614
62
+
63
+
64
+ @pytest.mark.parametrize(
65
+ "system", ["halo2_ipa", "halo2_kzg_groth16_wrap", "halo2_kzg", "stark_fri"]
66
+ )
67
+ def test_accepts_all_proving_systems(system):
68
+ data = sample_abi()
69
+ data["proof"]["proving_system"] = system
70
+ assert parse_abi_schema(json.dumps(data)).proof.proving_system == system
71
+
72
+
73
+ def test_rejects_unknown_proving_system():
74
+ data = sample_abi()
75
+ data["proof"]["proving_system"] = "groth16"
76
+ with pytest.raises(ValueError, match="unknown proving system"):
77
+ parse_abi_schema(json.dumps(data))
78
+
79
+
80
+ def test_rejects_wrong_abi_version():
81
+ data = sample_abi()
82
+ data["abi_version"] = 99
83
+ with pytest.raises(ValueError, match="unsupported abi_version"):
84
+ parse_abi_schema(json.dumps(data))
85
+
86
+
87
+ def test_rejects_public_input_count_mismatch():
88
+ data = sample_abi()
89
+ data["circuit"]["num_public_inputs"] = 5
90
+ with pytest.raises(ValueError, match="num_public_inputs"):
91
+ parse_abi_schema(json.dumps(data))
92
+
93
+
94
+ def test_rejects_witness_count_mismatch():
95
+ data = sample_abi()
96
+ data["circuit"]["num_private_witnesses"] = 5
97
+ with pytest.raises(ValueError, match="num_private_witnesses"):
98
+ parse_abi_schema(json.dumps(data))
99
+
100
+
101
+ def test_rejects_malformed_json():
102
+ with pytest.raises(ValueError, match="not valid JSON"):
103
+ parse_abi_schema("{not json")
104
+
105
+
106
+ def test_array_field_missing_kind_or_len_raises_value_error():
107
+ data = sample_abi()
108
+ data["witness"]["fields"][0]["kind"] = {"type": "array", "kind": {"type": "fp"}} # no len
109
+ with pytest.raises(ValueError, match="requires `kind` and `len`"):
110
+ parse_abi_schema(json.dumps(data))
111
+
112
+
113
+ def test_rejects_unknown_field_type_tag():
114
+ data = sample_abi()
115
+ data["witness"]["fields"][0]["kind"] = {"type": "u256"}
116
+ with pytest.raises(ValueError, match="unknown field type tag"):
117
+ parse_abi_schema(json.dumps(data))
118
+
119
+
120
+ def test_rejects_unknown_visibility():
121
+ data = sample_abi()
122
+ data["witness"]["fields"][0]["visibility"] = "hidden"
123
+ with pytest.raises(ValueError, match="unknown visibility"):
124
+ parse_abi_schema(json.dumps(data))
125
+
126
+
127
+ def test_field_type_validates_array_shape():
128
+ with pytest.raises(ValueError, match="requires `kind` and `len`"):
129
+ FieldType(type="array")
130
+ with pytest.raises(ValueError, match="takes no"):
131
+ FieldType(type="fp", len=3)