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.
- zerostyl_sdk-0.1.0/.gitignore +21 -0
- zerostyl_sdk-0.1.0/LICENSE +21 -0
- zerostyl_sdk-0.1.0/PKG-INFO +101 -0
- zerostyl_sdk-0.1.0/README.md +78 -0
- zerostyl_sdk-0.1.0/pyproject.toml +47 -0
- zerostyl_sdk-0.1.0/src/zerostyl_sdk/__init__.py +40 -0
- zerostyl_sdk-0.1.0/src/zerostyl_sdk/__main__.py +5 -0
- zerostyl_sdk-0.1.0/src/zerostyl_sdk/cli.py +53 -0
- zerostyl_sdk-0.1.0/src/zerostyl_sdk/codegen/__init__.py +4 -0
- zerostyl_sdk-0.1.0/src/zerostyl_sdk/codegen/generator.py +75 -0
- zerostyl_sdk-0.1.0/src/zerostyl_sdk/codegen/type_mapping.py +23 -0
- zerostyl_sdk-0.1.0/src/zerostyl_sdk/types.py +217 -0
- zerostyl_sdk-0.1.0/tests/snapshots/zk_private_demo_snap.py +22 -0
- zerostyl_sdk-0.1.0/tests/test_cli.py +47 -0
- zerostyl_sdk-0.1.0/tests/test_generator.py +97 -0
- zerostyl_sdk-0.1.0/tests/test_snapshot.py +32 -0
- zerostyl_sdk-0.1.0/tests/test_type_mapping.py +29 -0
- zerostyl_sdk-0.1.0/tests/test_types.py +131 -0
|
@@ -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,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,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)
|