From a Stylus function to a private circuit
This tutorial takes an ordinary Stylus function, marks the parameters you want to hide with
#[zk_private], and lets the exporter generate the halo2 circuit, its descriptor, an abi.json
schema, and a privacy-transformed contract — in one command. By the end you will understand every
file it produces and the honest on-chain model behind the generated contract.
You'll use: zerostyl-export. Time: ~10 minutes.
1. Annotate the source
Write the function you want to make private and annotate the hidden parameters. Here a deposit
whose collateral is committed with Poseidon, proven in 0..1_000_000, and proven >= threshold:
pub fn deposit(
#[zk_private(
commit = "poseidon",
range = "0..1000000",
constraint = "value >= threshold"
)]
collateral: u64,
threshold: u64,
) -> bool {
let _ = (collateral, threshold);
true
}
The attribute keys map to gadgets: commit = "poseidon" → a Poseidon commitment exposed as a
public input; range = "a..b" → a bounded range proof; constraint = "value >= other" → a
comparison against another parameter. Multiple attributes on one parameter compose as a logical
AND. Every #[zk_private] parameter must carry a commit — it is what ties the private value to a
public input the verifier checks.
2. Transform
zerostyl-export transform --contract contract_source.rs --output-dir generated
transformed circuit 'deposit' -> generated
generated/circuit.rs
generated/descriptor.rs
generated/contract_transformed.rs
generated/abi.json
3. What you get
abi.json — the language-neutral schema
{
"abi_version": 1,
"circuit": {
"name": "deposit",
"default_k": 10,
"num_public_inputs": 1,
"num_private_witnesses": 3
},
"witness": {
"fields": [
{ "name": "collateral", "kind": { "type": "u64" }, "visibility": "private" },
{ "name": "collateral_nonce", "kind": { "type": "fp" }, "visibility": "private" },
{ "name": "threshold", "kind": { "type": "u64" }, "visibility": "private" }
]
},
"public_inputs": {
"fields": [ { "name": "collateral_commitment", "kind": { "type": "fp" } } ]
},
"proof": { "format_version": 1, "proving_system": "halo2_kzg" }
}
Note the exporter added collateral_nonce (the Poseidon randomness) as a witness, and exposed
collateral_commitment as the single public input. This same abi.json is what the TypeScript and
Python SDKs consume — see abi.json → typed bindings.
circuit.rs — the halo2 circuit
A Circuit<Fr> that wires PoseidonCommitmentChip, RangeProofChip, and ComparisonChip. The
value is loaded once and the same cell flows into the commitment, the range check, and the
comparison (copy_advice ties them together), so the range/comparison statements are provably
about the committed value — not a free witness.
descriptor.rs — the CircuitDescriptor
Implements prove / verify / mock_prove / inspect and exposes
pub fn descriptor() -> &'static dyn CircuitDescriptor, so the circuit plugs into the CLI, the
debugger, and the SDK exactly like the built-ins.
contract_transformed.rs — the privacy-safe ABI
Each #[zk_private] parameter becomes a B256 commitment, plus a trailing proof: Bytes:
pub fn deposit(
host: &mut impl DepositHost,
collateral_commitment: B256,
threshold: u64,
proof: Bytes,
) -> bool { /* guards → nullifier registry → standardized event */ }
The generated module ships a DepositHost trait (nullifier storage, clock, event sink, and a
verify_proof hook that fails closed by default), a public_inputs() helper, a per-commitment
derive_nullifier, and the ZeroStylPrivacyTransaction event constants — plus a reference Stylus
embedding behind a feature flag.
The generated contract does not verify the SNARK on-chain: Arbitrum Stylus caps deployable
contracts at 24 KB Brotli-compressed while a halo2 verifier exceeds 90 KB. It implements a
hash-guard flow (proof hash + one-shot nullifier + standardized event) with verify_proof as the
extension point for real verification. See Events & Fingerprinting
and the deployability tutorial.
Next steps
- Prove and verify against the generated circuit → Prove & verify with the Rust SDK.
- Generate typed client bindings from
abi.json→ abi.json → typed bindings. - The full worked example lives in the repo at
examples/zk_private_demo/.