ADR-6: zk-wasm — Custom WASM Module for Zero-Knowledge Proof Verification
Extending the CosmWasm VM with native halo2 proof verification, enabling ZK authentication, private transaction validation, and identity verification without revealing credentials
ADR 6: zk-wasm — Custom WASM Module for Zero-Knowledge Proof Verification
Changelog
- 2026-05-12: Initial draft
Status
DRAFT
Dependencies
- ADR-1: Standard Template and Design Guidelines — this ADR follows the format defined in ADR-1
- ADR-4: Sovereign Custody and Authentication Specifications — smart account authentication (specifically the
CosmwasmAuthenticatorV1interface andConfirmExecutionslow path) depends on zk-wasm for proof verification; theAuthenticate/ConfirmExecutiondual-mode pattern in ADR-4 requires on-chain ZK proof validation - ADR-5: HashMerchant — uses zk-TLS extensions for attesting external chain data; HashMerchant's
ChainUIDzk-TLS extension config can produce proofs verified by zk-wasm - Downstream: ADR-4 authentication flows, cw-headstash contract (nullifier verification + private airdrop claims), and any future contract requiring on-chain ZK verification
Abstract
This ADR defines the zk-wasm module: a forked CosmWasm virtual machine extended with native halo2 plonk proof verification. The module introduces a host function deps.api.verify_halo2_proof() that smart contracts call to verify zero-knowledge proofs on-chain, plus a parallel storage layer for verifying keys, a caching system, and binary format conventions. The core use cases are ZK authentication proofs (enabling the dual-mode authentication pattern in ADR-4), private transaction validation (nullifier-based claims without revealing recipient identity), and identity verification without credential disclosure. The MVP targets a single halo2 circuit running in-browser WASM via wasm-pack, with proof submission to a CosmWasm contract for on-chain verification through the 5-phase lifecycle (build, upload, cache, pin, verify).
Context and Problem Statement
Terp Network's smart account system (ADR-4) requires a mechanism for privacy-preserving authentication. The ConfirmExecution slow path must verify that a user holds a valid credential (e.g., membership in a genesis merkle tree, ownership of a private key, or authorization from a threshold committee) without revealing the credential itself. Standard CosmWasm provides no native ZK capability — a contract cannot verify a SNARK proof without shipping the verifier as WASM bytecode, which is prohibitively expensive in gas and binary size.
The broader problem: Cosmos chains have no standard mechanism for on-chain ZK proof verification. Existing approaches either rely on IBC-mediated verification (latency and trust assumptions) or ship entire verifier circuits as contract bytecode (gas costs of 700k–1M per verification). Neither is viable for authentication-grade ZK proofs that must be verified in every block.
The specific forces:
- Gas efficiency: public-input-heavy circuits (254+ instances) cost ~700k–1M gas per verification on naive approaches. This must be reduced to ~120–180k for mainnet viability.
- Binary format stability: verifying keys have a specific binary format (10-byte footer: VM version, public instance count,
vk_paramslength,vklength in LE). The format must be versioned and stable across upgrades. - Cache coherence: verifying keys have different access patterns and sizes than contract WASM. They require separate filesystem caching (
vks/vsmodules/) and LRU eviction with pinned keys for hot circuits. - Fork maintenance: the ZK fork (zk-wasmvm, zk-wasmd, zk-cosmos-sdk, zk-ibc-go) diverges from upstream CosmWasm. Dependency mode switching (
just deps-zk-local/just deps-zk-git) must be handled explicitly. - Curve selection: halo2 uses the pasta curves (vesta/pallas for proof, Fp for public instances). This is a fixed choice for the MVP; generalizing to other curves is a roadmap item.
Decision Drivers
- Gas efficiency: on-chain ZK verification must be affordable for authentication use cases (target: under 200k gas per verification)
- Security: proof verification must be sound — a forged proof must never pass on-chain verification
- Developer ergonomics: circuit authors must be able to define halo2 circuits, generate proving and verifying keys, and upload both in a single transaction
- Cache performance: frequently-used verifying keys must be memory-resident without unbounded memory growth
- Backward compatibility: the ZK fork must coexist with the standard CosmWasm dependency graph (mode switching)
- Composability: contracts must be able to derive public instances from on-chain state (block heights, light client headers, application data)
- Upgrade safety: verifying key binary format changes must be detected at upload time, not at verification time
Considered Options / Alternatives
- Pure CosmWasm verifier contract: ship the halo2 verifier as WASM bytecode inside a CosmWasm contract. Gas cost is 700k–1M per verification. Binary size is large. No host-function acceleration. Simple to deploy but economically infeasible for authentication-grade proofs.
- IBC-mediated verification: relay proofs to a chain with native ZK support (e.g., Ethereum with Groth16 precompile) and relay the result back. Introduces latency (minutes), trust assumptions (the relayer, the verifying chain), and IBC channel overhead. Defeats the purpose of on-chain verification.
- Native module (Go) in terp-core: implement halo2 verification as a Cosmos SDK module in Go. Requires re-implementing halo2 verification in Go (no production-ready library exists). Fragile to upstream halo2 changes. Does not benefit from the Rust halo2 ecosystem.
- Forked CosmWasm VM with host function (selected): extend the existing wasmvm Rust layer with a
verify_halo2_proofhost function. Contracts calldeps.api.verify_halo2_proof(proof_bytes, public_inputs). Verifying keys are stored and cached alongside contract WASM. Leverages the existing Rust halo2 ecosystem. Requires maintaining a fork but avoids gas and latency problems.
Decision Outcome
We adopt the forked CosmWasm VM with host function approach. The zk-wasm module extends wasmvm with native halo2 proof verification, implementing a 5-phase verifying key lifecycle.
Detailed Design
5-Phase Verifying Key Lifecycle
Phase 1: BUILD (off-chain)
- Developer defines a halo2 circuit in Rust using
halo2_proofslibrary - Generates proving key and verifying key via
build_and_write() - Compiles the verifying key into a binary file with a 10-byte footer:
- Byte 0: VM version identifier
- Byte 1: public instance count
- Bytes 2–5:
vk_paramslength (little-endian u32) - Bytes 6–9:
vklength (little-endian u32)
- The constant
K(circuit degree) needs no dedicated byte — it is recoverable from the deserializedParamsobject
Phase 2: UPLOAD (on-chain)
- Uses
MsgStoreCodeWithCircuit, storing both contract WASM and verifying key binary in a single transaction - Keeper validates WASM bytecode, calls
check_vk()on VK bytes - Computes separate SHA-256 checksums for WASM and VK
- Stores
CodeInfowith bothwasm_checksumandvk_checksumfields - The Go-side
wasm_engine.gointerface addsGetCircuit(code wasmvm.Checksum) (wasmvm.CircuitBinary, error)alongsideGetCode
Phase 3: CACHE (node-level)
- At node startup or first use, the filesystem cache writes compiled WASM to
modules/and verifying keys tovks/ - VKs are cached independently because they have different access patterns and sizes
- Cache invalidation follows the same SHA-256 checksum strategy as contract WASM
Phase 4: PIN (operator-configurable)
- Promotes hot verifying keys from disk into memory
- Uses LRU eviction for unpinned keys
- Maintains a
pinned_vk_cacheHashMap for permanently resident keys (operator-configurable) - Each
LoadedVerifyingKeyholds deserializedParams<vesta::Affine>andVerifyingKey<vesta::Affine>— the vesta/pallas curve pair
Phase 5: VERIFY (runtime)
- Contract calls
deps.api.verify_halo2_proof(proof_bytes, public_inputs) - Host function
do_verify_halo2_proof()resolves the cached VK - Deserializes the proof, constructs a
SingleStrategyverifier - Runs plonk verification against the vesta/pallas curve
- Public instances must be
pasta_curves::Fpvalues - Contracts derive instances from on-chain state: block heights, light client headers, application data
Public Input Optimization
The original circuit design produced 254+ public inputs, resulting in ~700k–1M gas per verification. The optimization hashes all public inputs into a single field element inside the circuit:
let public_inputs = [
genesis_root,
nullifier,
commitment,
recipient_commitment,
amount,
token_denom_hash,
merkle_path_hint,
];
let public_hash = poseidon_hash(public_inputs);
layouter.constrain_instance(public_hash.cell(), primary, 0)?;The contract verifies only one public input (the hash of all values). Off-chain verifiers reconstruct and re-hash to validate correctness. This reduces gas to ~120–180k per verification (75–80% reduction).
Smart Account Integration (ADR-4)
The CosmwasmAuthenticatorV1 interface uses zk-wasm in the ConfirmExecution slow path:
| Callback | ZK Integration | Path |
|---|---|---|
Authenticate | BLS12-381 aggregate signature only | Fast path (no ZK) |
ConfirmExecution | Halo2/Plonk proof + nullifier verification | Slow path (ZK + BLS) |
OnAuthenticatorAdded | Validates operator PoPs, stores WavsOperatorSet | No ZK |
In-Browser Proof Generation (MVP)
The norick demo page pattern: a halo2 WASM circuit loaded via wasm-pack build --target web, running proof generation entirely in the browser. The generated proof is submitted to a CosmWasm contract for on-chain verification. This enables:
- ZK authentication proofs: user proves membership in a set without revealing which element
- Private transaction validation: nullifier-based claims that don't reveal the recipient identity
- Identity verification without credential disclosure: prove you hold a valid credential without revealing the credential itself
Dependency Mode Switching
The ZK fork (zk-wasmvm, zk-wasmd, zk-cosmos-sdk, zk-ibc-go) lives on the zk-mvp branch by default. An agent or developer must switch to ZK mode before compiling:
just deps-zk-local— uses local path dependencies (for iteration)just deps-zk-git— uses git dependencies (for CI)
Without mode switching, cargo resolves to upstream CosmWasm, which compiles but has zero ZK capability. Version mismatches are runtime panics.
Affected Systems / Modules
| Component | Change |
|---|---|
zk-wasmvm (Rust) | New host function verify_halo2_proof, VK storage, cache, pinning |
zk-wasmd (Go) | wasm_engine.go adds GetCircuit(), MsgStoreCodeWithCircuit handler |
zk-cosmos-sdk (Go) | CodeInfo extended with vk_checksum field |
zk-ibc-go (Go) | No changes (parallel attestation track per ADR-5) |
cw-headstash (CosmWasm) | Consumes deps.api.verify_halo2_proof() in ConfirmExecution |
| Build tooling | build-wasmvm-alpine.sh uses terpnetwork/zk-alpine-builder:1.88 |
Security Considerations
- Soundness depends on the halo2 plonk protocol and the vesta/pallas curve pair. These are well-studied but not post-quantum secure.
- Binary format footer is checked at upload time via
check_vk(). A malformed VK is rejected before it enters the store. pinned_vk_cacheis unbounded if operators pin too many keys — this is an operator-configurable risk, mitigated by alerting on memory pressure.- Proof verification is deterministic: same (VK, proof, instances) always produces the same boolean result. No consensus divergence risk.
Consequences
Positive
- On-chain ZK proof verification is gas-efficient (~120–180k gas vs ~1M for pure-WASM approach)
- Smart accounts gain privacy-preserving authentication (dual-mode fast/slow path)
- Leverages the existing Rust halo2 ecosystem — no reimplementation in Go
- Public input hashing reduces verification gas by 75–80%
- Composability: any CosmWasm contract can call
deps.api.verify_halo2_proof()without custom integration - In-browser proof generation enables client-side ZK workflows (norick demo pattern)
Negative
- Fork maintenance: the ZK fork (zk-wasmvm, zk-wasmd, zk-cosmos-sdk, zk-ibc-go) diverges from upstream CosmWasm. Every upstream release requires a rebase.
- Dependency mode switching is a footgun: forgetting
just deps-zk-localproduces code that compiles but has no ZK capability. - Vest/pallas curve pair is a fixed MVP choice — generalizing to other curves (BN254, BLS12-381) requires additional host functions or a general-purpose zkVM.
- Verifying key binary format is versioned but not self-describing — a format change requires a coordinated upgrade.
Neutral / Trade-offs
- The 10-byte footer format is simple and efficient, but any change requires a
vk_checksuminvalidation and re-upload of all circuits. - LRU eviction with pinned keys is operator-configurable — different operators may have different memory profiles, but all produce identical verification results.
- The
just deps-zk-*mode switching is a deliberate trade-off: maintaining the fork as a separate mode avoids polluting the standard build, but adds cognitive overhead for developers.
Backwards Compatibility
The zk-wasm module is additive: it introduces new message types (MsgStoreCodeWithCircuit), new store keys (vk_checksum in CodeInfo), and new host functions (verify_halo2_proof). Existing contracts that do not use ZK functionality are unaffected — they never call the host function, and the standard CosmWasm lifecycle (store, instantiate, execute, migrate) remains identical.
The ZK fork requires a coordinated upgrade: terp-core must switch from standard wasmvm to zk-wasmvm. This is a hard-fork event. The upgrade handler adds the vk_checksum field to existing CodeInfo entries (default: empty, meaning no circuit). Contracts uploaded before the upgrade continue to function — they simply have no associated verifying key.
Migration plan: v6 upgrade handler adds the vk_checksum field to the store schema and initializes the vks/ cache directory. No data migration is needed for existing contracts.
Test Cases
- Unit: verify
check_vk()rejects a malformed verifying key binary (wrong footer length, invalid VM version byte) - Unit: verify
check_vk()accepts a well-formed VK binary generated bybuild_and_write() - Integration: upload a contract with
MsgStoreCodeWithCircuit, verify bothwasm_checksumandvk_checksumare stored inCodeInfo - Integration: call
deps.api.verify_halo2_proof()from a test contract with a valid proof and matching VK — verify it returnstrue - Integration: call
deps.api.verify_halo2_proof()with an invalid proof — verify it returnsfalse - Gas benchmark: verify that single-instance public input hashing achieves under 200k gas for verification
- Cache: verify that a pinned VK remains in
pinned_vk_cacheafter LRU eviction of unpinned keys - Cache: verify that
vks/andmodules/directories are populated independently at node startup - Mode switching: verify that
just deps-zk-localcompiles zk-wasmvm with ZK host functions, and that standard mode compiles without them - End-to-end: generate a halo2 proof in-browser via wasm-pack, submit to a CosmWasm contract, verify on-chain
Further Discussions / Open Questions
- Should the verifying key binary format be self-describing (e.g., include a version header) rather than requiring the 10-byte footer?
- What is the maximum practical number of pinned verifying keys before memory pressure becomes a validator operational concern?
- Should
MsgStoreCodeWithCircuitbe governance-gated (likeMsgStoreCodein some chains) or permissionless? - How to handle VK rotation: when a circuit is updated, should the old VK be automatically invalidated, or should both coexist?
- Roadmap: should the next phase target a general-purpose zkVM (e.g., RISC Zero, SP1) or additional curve support (BN254, BLS12-381)?
- Should the in-browser WASM proof generation be standardized as a shared library (e.g.,
terp-zk-wasm-pack) or remain per-circuit? - What disclosure is required when using LM tooling to generate ZK circuits (per ADR-9)?
References
- ADR-1: Standard Template and Design Guidelines
- ADR-4: Sovereign Custody and Authentication Specifications
- ADR-5: HashMerchant — Verifiable Merkle Reflection Market
- ADR-9: LM-Augmented Development and Semantic Tooling
~/abstract/zk-wasmvm/ZK.md— ZK wasmvm architecture documentation~/abstract/scripts/build/build-wasmvm-alpine.sh— Alpine builder for zk-wasmvm~/abstract/terp-rs/scripts/just/skills.just— ZK skill definitions for LLM agents~/abstract/headstash/docs/circuit/smart-contracts.md— Cw-Headstash contract specification with ZK verification- halo2_proofs crate: https://docs.rs/halo2_proofs
- pasta_curves crate: https://docs.rs/pasta_curves
- wasm-pack documentation: https://rustwasm.github.io/wasm-pack/