Skip to content

Commit 04dfca9

Browse files
nyc432claude
andcommitted
refactor: Complete API refinement for cleaner, more Pythonic interface (v0.1.0)
Major API improvements: - Add unified prove() function combining execution and proof generation - Remove prepare_input() - prove() now accepts bytes directly - Switch guests to env::read_slice() for efficient byte reading - Make journal and other data accessible as properties (not methods) - Clean up naming: image.id instead of image.image_id - Remove redundant verify_proof() in favor of receipt.verify() - Move serialization helpers to optional pyr0.serialization module Benefits: - Simpler one-step proof generation: prove(image, input_bytes) - More Pythonic property-based access: receipt.journal, image.id - Direct byte handling without wrapper functions - Optional serialization helpers for common patterns - Reduced API surface area while maintaining full functionality Both Merkle ZKP and Ed25519 demos updated and working with new API. This release marks the transition from pre-alpha to alpha status. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent ca7cac3 commit 04dfca9

File tree

15 files changed

+299
-183
lines changed

15 files changed

+299
-183
lines changed

CLAUDE.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
# Claude Development Notes
22

3+
## Essential Development Workflow
4+
5+
**After ANY changes to Rust code (src/*.rs):**
6+
7+
```bash
8+
# 1. Build the wheel
9+
uv tool run maturin build --release
10+
# Maturin will output: "📦 Built wheel for CPython 3.12 to /path/to/PyR0-X.X.X-cp312-cp312-platform.whl"
11+
12+
# 2. Force reinstall (CRITICAL - must use --force-reinstall)
13+
# Option A: Use the * wildcard to match the latest built wheel
14+
uv pip install --force-reinstall target/wheels/PyR0-*.whl
15+
16+
# Option B: Copy the exact filename from maturin's output
17+
# Example: uv pip install --force-reinstall target/wheels/PyR0-0.1.0-cp312-cp312-macosx_11_0_arm64.whl
18+
```
19+
20+
**Why --force-reinstall is required:**
21+
- Without it, uv uses cached versions even after rebuilding
22+
- Changes won't take effect without forcing reinstallation
23+
- This is the #1 cause of "changes not working" issues
24+
325
## Using PyO3 with uv - Key Steps
426

527
### Setup
@@ -84,7 +106,7 @@ uv tool run maturin develop
84106
uv tool run maturin build --release
85107

86108
# Step 2: Force reinstall (IMPORTANT: use --force-reinstall to override cache)
87-
uv pip install --force-reinstall target/wheels/PyR0-0.2.0-cp312-cp312-macosx_11_0_arm64.whl
109+
uv pip install --force-reinstall target/wheels/PyR0-*.whl
88110

89111
# Alternative (slower but automatic):
90112
uv sync --no-editable
@@ -137,7 +159,7 @@ print(dir(pyr0)) # Should show 'serialization' if properly installed
137159
- `pyproject.toml` - the `version = "x.x.x"` line
138160
- `Cargo.toml` - the `version = "x.x.x"` line
139161
- `README.md` - the version badge and any version references
140-
3. Include version in commit message (e.g., "Add feature X (v0.0.4)")
162+
3. Include version in commit message (e.g., "Add feature X (v0.1.0)")
141163
4. Create and push a git tag:
142164
```bash
143165
git tag -a vX.X.X -m "Description of changes"

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[package]
44
name = "pyr0"
5-
version = "0.0.4"
5+
version = "0.1.0"
66
edition = "2021"
77

88
[lib]

README.md

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# PyR0 - Python Interface for RISC Zero zkVM
22

3-
[![Version](https://img.shields.io/badge/version-0.0.4-orange)](https://github.com/garyrob/PyR0/releases)
4-
**⚠️ Experimental Pre-Alpha - Apple Silicon Only**
3+
[![Version](https://img.shields.io/badge/version-0.1.0-orange)](https://github.com/garyrob/PyR0/releases)
4+
**⚠️ Experimental Alpha - Apple Silicon Only**
55

66
Python bindings for [RISC Zero](https://www.risczero.com/) zkVM, enabling zero-knowledge proof generation and verification from Python.
77

8-
> **Note**: This is an experimental pre-alpha release (v0.0.4) currently targeting Apple Silicon (M1/M2/M3) Macs only.
8+
> **Note**: This is an experimental alpha release (v0.1.0) currently targeting Apple Silicon (M1/M2/M3) Macs only.
99
1010
## Overview
1111

@@ -52,20 +52,20 @@ import pyr0
5252
# Load a RISC Zero guest program
5353
with open("guest_program.elf", "rb") as f:
5454
elf_data = f.read()
55-
image = pyr0.load_image(elf_data) # Simplified name
55+
image = pyr0.load_image(elf_data)
5656

57-
# Execute with input
58-
input_data = pyr0.prepare_input(b"your input data")
59-
segments, info = pyr0.execute_with_input(image, input_data)
57+
# One-step proof generation (execution + proof)
58+
input_data = b"your input data" # Direct bytes, no wrapper needed
59+
receipt = pyr0.prove(image, input_data)
6060

61-
# Generate a proof
62-
receipt = pyr0.generate_proof(segments[0]) # Clearer name
63-
64-
# Get the journal (public outputs) from the receipt
65-
journal = receipt.journal_bytes()
61+
# Get the journal (public outputs) as a property
62+
journal = receipt.journal
6663

6764
# Verify the proof
68-
pyr0.verify_proof(receipt) # Consistent naming
65+
receipt.verify()
66+
67+
# Access the image ID
68+
image_id = image.id
6969
```
7070

7171
### Merkle Trees with Poseidon Hash
@@ -99,7 +99,7 @@ Prepare data for RISC Zero guest programs:
9999
```python
100100
from pyr0 import serialization
101101

102-
# Serialize various data types for guest programs
102+
# Optional serialization helpers for guest programs
103103
data = serialization.to_vec_u8(bytes_data) # Vec<u8> with length prefix
104104
data = serialization.to_u32(value) # 32-bit unsigned integer
105105
data = serialization.to_u64(value) # 64-bit unsigned integer
@@ -108,8 +108,9 @@ data = serialization.to_bool(value) # Boolean value
108108
data = serialization.to_bytes32(data) # Fixed [u8; 32] array
109109
data = serialization.to_bytes64(data) # Fixed [u8; 64] array
110110

111-
# Prepare Ed25519 signature verification input
112-
input_data = serialization.ed25519_input_vecs(public_key, signature, message)
111+
# Convenience functions for common patterns
112+
input_data = serialization.ed25519_input(public_key, signature, message)
113+
input_data = serialization.merkle_proof_input(leaf, siblings, indices)
113114
```
114115

115116
### Journal Deserialization
@@ -126,7 +127,7 @@ OutputSchema = CStruct(
126127
)
127128

128129
# Parse journal from receipt
129-
journal = receipt.journal_bytes()
130+
journal = receipt.journal # Access as property
130131
output = OutputSchema.parse(journal)
131132
```
132133

demo/ed25519_demo.py

Lines changed: 18 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,7 @@
1515

1616
# Debug: check what we actually imported
1717
print(f"Imported pyr0 from: {pyr0.__file__}")
18-
print(f"Using new API: {hasattr(pyr0, 'load_image')}")
19-
if not hasattr(pyr0.Image, 'image_id'):
20-
print("\n❌ ERROR: Using outdated pyr0 version without image_id support")
21-
print(f" Currently importing from: {pyr0.__file__}")
22-
if "/src/pyr0/" in pyr0.__file__:
23-
print("\n PROBLEM: You're importing from the source directory (editable install)")
24-
print(" This happens because uv installs projects in editable mode by default.")
25-
print("\n TO FIX: Run 'uv sync --no-editable' to build and install the compiled package")
26-
print(" This will build the Rust extension with the latest features.\n")
27-
else:
28-
print("\n The installed package appears to be outdated.")
29-
print(" TO FIX: Rebuild with 'uv sync --no-editable'\n")
30-
sys.exit(1)
18+
print(f"Using new API: {hasattr(pyr0, 'prove')}")
3119

3220
# Constants
3321
GUEST_DIR = Path(__file__).parent / "ed25519_demo_guest"
@@ -74,7 +62,7 @@
7462
print("✓ ELF loaded into image")
7563

7664
# Get and display the program's unique ID
77-
PROGRAM_ID = image.image_id.hex()
65+
PROGRAM_ID = image.id.hex()
7866
print(f"✓ Program ID: {PROGRAM_ID[:16]}...{PROGRAM_ID[-16:]}")
7967
print(f" (Full ID: {PROGRAM_ID})")
8068

@@ -84,17 +72,18 @@
8472
sig_bytes = bytes.fromhex(VALID_SIG)
8573
msg_bytes = MESSAGE.encode('utf-8')
8674

87-
# Use the new serialization approach - Python handles the format
88-
# This creates three Vec<u8> values as expected by the guest
89-
input_data = pyr0.prepare_input(
90-
serialization.ed25519_input_vecs(pk_bytes, sig_bytes, msg_bytes)
91-
)
75+
# Use the new API - prove() accepts bytes directly
76+
# The serialization helper creates the proper format for the guest
77+
input_data = serialization.ed25519_input(pk_bytes, sig_bytes, msg_bytes)
9278

9379
print(f"Input size: {len(input_data)} bytes")
94-
print("Executing...")
80+
print("Executing and generating proof...")
9581

96-
segments, info = pyr0.execute_with_input(image, input_data)
97-
journal = info.get_journal()
82+
# Use the new unified prove() function
83+
start = time.time()
84+
receipt = pyr0.prove(image, input_data)
85+
proof_time = time.time() - start
86+
journal = receipt.journal
9887

9988
print(f"Journal: {len(journal)} bytes")
10089
print(f"First 10 bytes: {journal[:10]}")
@@ -140,15 +129,11 @@
140129
else:
141130
print(f"Unexpected result: {result}")
142131

143-
# Generate proof
144-
print("\nGenerating proof...")
145-
start = time.time()
146-
receipt = pyr0.generate_proof(segments[0]) # Use new consistent name
147-
proof_time = time.time() - start
148-
print(f"Proof generated in {proof_time:.2f}s")
132+
# Proof was already generated by prove()
133+
print(f"\nProof generated in {proof_time:.2f}s")
149134

150135
# Verify the receipt's program ID matches our expected program
151-
receipt_program_id = receipt.program_id().hex()
136+
receipt_program_id = receipt.program_id.hex()
152137
if receipt_program_id != PROGRAM_ID:
153138
print(f"❌ ERROR: Program ID mismatch!")
154139
print(f" Expected: {PROGRAM_ID}")
@@ -157,19 +142,18 @@
157142
print(f"✓ Program ID verified: {receipt_program_id[:16]}...{receipt_program_id[-16:]}")
158143

159144
# Verify the cryptographic proof
160-
pyr0.verify_proof(receipt) # Use new consistent name
145+
receipt.verify()
161146
print("✓ Proof verified")
162147

163148
# Test with invalid signature
164149
print("\n=== Test 2: Invalid Signature ===")
165150
sig_bytes = bytes.fromhex(INVALID_SIG)
166151

167-
input_data = pyr0.prepare_input(
168-
serialization.ed25519_input_vecs(pk_bytes, sig_bytes, msg_bytes)
169-
)
152+
input_data = serialization.ed25519_input(pk_bytes, sig_bytes, msg_bytes)
170153

154+
# For the second test, we just execute, no need for proof
171155
segments, info = pyr0.execute_with_input(image, input_data)
172-
journal = info.get_journal()
156+
journal = info.journal
173157

174158
print(f"Journal: {len(journal)} bytes")
175159

demo/merkle_proof_guest/src/main.rs

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#![no_std]
66

77
extern crate alloc;
8+
use alloc::vec;
89
use alloc::vec::Vec;
910

1011
use risc0_zkvm::guest::env;
@@ -69,50 +70,50 @@ fn verify_merkle_path(
6970
risc0_zkvm::guest::entry!(main);
7071

7172
fn main() {
72-
// RISC Zero serializes everything through env::read()
73-
// Each byte becomes a u32 in the input stream
73+
// Use env::read_slice() for efficient byte reading
74+
// Python sends raw bytes, no u32 expansion
7475

75-
// Read k_pub (32 bytes, each as u32)
76+
// Read all input at once into a buffer
77+
// We know the exact size: 32*3 + 32*16 + 16 = 96 + 512 + 16 = 624 bytes
78+
let mut input_buffer = vec![0u8; 624];
79+
env::read_slice(&mut input_buffer);
80+
81+
// Create a cursor to read from the buffer
82+
let mut offset = 0;
83+
84+
// Read k_pub (32 bytes)
7685
let mut k_pub = [0u8; 32];
77-
for i in 0..32 {
78-
k_pub[i] = env::read::<u32>() as u8;
79-
}
86+
k_pub.copy_from_slice(&input_buffer[offset..offset+32]);
87+
offset += 32;
8088

81-
// Read r (32 bytes, each as u32)
89+
// Read r (32 bytes)
8290
let mut r = [0u8; 32];
83-
for i in 0..32 {
84-
r[i] = env::read::<u32>() as u8;
85-
}
91+
r.copy_from_slice(&input_buffer[offset..offset+32]);
92+
offset += 32;
8693

87-
// Read e (32 bytes, each as u32)
94+
// Read e (32 bytes)
8895
let mut e = [0u8; 32];
89-
for i in 0..32 {
90-
e[i] = env::read::<u32>() as u8;
91-
}
96+
e.copy_from_slice(&input_buffer[offset..offset+32]);
97+
offset += 32;
9298

93-
// Read path length
94-
let path_len: u32 = env::read();
95-
assert_eq!(path_len, 16, "Merkle path must have 16 levels");
99+
// Read path length (16 for our fixed-depth tree)
100+
// For simplicity, we'll hardcode 16 levels since that's what we always use
101+
let path_len = 16usize;
96102

97-
// Read path siblings (each is 32 bytes)
103+
// Read path siblings (16 * 32 bytes)
98104
let mut path = Vec::new();
99105
for _ in 0..path_len {
100106
let mut sibling = [0u8; 32];
101-
for j in 0..32 {
102-
sibling[j] = env::read::<u32>() as u8;
103-
}
107+
sibling.copy_from_slice(&input_buffer[offset..offset+32]);
108+
offset += 32;
104109
path.push(sibling);
105110
}
106111

107-
// Read indices length
108-
let indices_len: u32 = env::read();
109-
assert_eq!(indices_len, 16, "Must have 16 index bits");
110-
111-
// Read indices (each byte as u32)
112+
// Read indices (16 bytes, one per level)
112113
let mut indices = Vec::new();
113-
for _ in 0..indices_len {
114-
let bit_val: u32 = env::read();
115-
indices.push(bit_val != 0);
114+
for _ in 0..path_len {
115+
indices.push(input_buffer[offset] != 0);
116+
offset += 1;
116117
}
117118

118119
// Step 1: Compute the leaf commitment C = Hash(k_pub || r || e)

0 commit comments

Comments
 (0)