Verifier Integration
This guide shows how a Stylus contract calls zerostyl-verifier to check a proof on-chain, using
the state_mask circuit. It also states, plainly, the size constraint that gates real on-chain
deployment.
Arbitrum Stylus caps a deployed contract at 24 KB Brotli-compressed. A full halo2-KZG verifier
is far larger — the state_mask verifier is ~240 KB compressed — so a contract that calls
zerostyl-verifier compiles for wasm32 but cannot be deployed within the budget. The demo
contracts on Arbitrum Sepolia therefore record a proof hash rather than verifying the SNARK. The
integration below is the correct reference path; the routes that would actually fit the budget
(a BN254-precompile verifier or a Groth16 wrap) are discussed at the end. Use
zerostyl-orbit to measure an artifact against a chain.
Prerequisites
- A Stylus development environment
- The
wasm32-unknown-unknowntarget (rustup target add wasm32-unknown-unknown) cargo-stylus(cargo install --force cargo-stylus)
Cargo.toml
The crates are not on crates.io — depend on the repository via git, and replicate the workspace's halo2 patch:
[package]
name = "my-zk-contract"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
stylus-sdk = "0.9.0"
alloy-primitives = "=0.8.20"
alloy-sol-types = "=0.8.20"
zerostyl-verifier = { git = "https://github.com/kazai777/zerostyl", default-features = false, features = ["stylus", "state_mask_vk"] }
[patch.crates-io]
halo2_proofs = { git = "https://github.com/privacy-scaling-explorations/halo2.git", tag = "v0.3.0" }
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
strip = true
The verification call
zerostyl-verifier exposes a byte-oriented entry point that never names a curve type, so it is easy
to call from a contract. For state_mask, the two public inputs are the commitment and the
threshold, each a 32-byte little-endian field representation (exactly the bytes
zerostyl-prove writes to public_inputs.json):
use alloc::vec::Vec;
use stylus_sdk::alloy_primitives::B256;
fn verify_solvency_proof(proof: &[u8], commitment: B256, threshold: B256) -> Result<bool, Vec<u8>> {
// public inputs in circuit order, forwarded verbatim
let public_inputs = [commitment.0, threshold.0];
zerostyl_verifier::verify_state_mask_bytes(proof, &public_inputs)
}
It returns Ok(true) on a valid proof, Ok(false) on an invalid one, and Err(_) if an input is
not a canonical field element.
A contract entry point
A minimal contract that verifies a solvency proof and registers each commitment once:
#![cfg_attr(not(any(test, feature = "export-abi")), no_main)]
#![cfg_attr(not(any(test, feature = "export-abi")), no_std)]
extern crate alloc;
use alloc::vec::Vec;
use stylus_sdk::{alloy_primitives::{Address, B256}, prelude::*};
sol_storage! {
#[entrypoint]
pub struct StateMaskVerifier {
mapping(bytes32 => address) verified_commitments;
}
}
#[public]
impl StateMaskVerifier {
/// `commitment` and `threshold` are little-endian Fr reprs from `public_inputs.json`.
pub fn verify_solvency(
&mut self,
proof: stylus_sdk::abi::Bytes,
commitment: B256,
threshold: B256,
) -> Result<bool, Vec<u8>> {
// Idempotence: a commitment can only be proven once.
if self.verified_commitments.get(commitment) != Address::ZERO {
return Ok(false);
}
// Real halo2-KZG verification against the embedded verifying key.
if !verify_solvency_proof(&proof.0, commitment, threshold)? {
return Ok(false);
}
self.verified_commitments.setter(commitment).set(self.vm().msg_sender());
Ok(true)
}
}
Pass the bytes from public_inputs.json verbatim. Do not use abi.encode(uint256) /
bytes32(uint256(x)) — EVM tooling produces big-endian bytes, which the verifier reads as a
different field element and the proof fails.
The complete, tested version lives in the repository at contracts/state_mask_verifier/.
Making it deployable
Because the verifier exceeds the 24 KB limit, a production on-chain path uses one of:
- BN254 precompiles — a hand-written minimal KZG verifier that offloads the pairing to
Arbitrum's
0x08precompile. - Groth16 wrap — wrap the halo2 proof in a small Groth16 SNARK and verify that over the BN254
precompiles (
ProvingSystem::Halo2KzgGroth16Wrap).
zerostyl-orbit reports which precompiles a chain exposes and whether a given artifact fits its
size budget. See the repository's contracts/CONTRACTS.md for the current on-chain security model
and docs/STARK_FEASIBILITY.md for the proof-system trade-offs.