Skip to main content

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.

On-chain verification does not fit Stylus yet

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-unknown target (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)
}
}
Byte order

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 0x08 precompile.
  • 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.