Terp Network Docs

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 CosmwasmAuthenticatorV1 interface and ConfirmExecution slow path) depends on zk-wasm for proof verification; the Authenticate / ConfirmExecution dual-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 ChainUID zk-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:

  1. 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.
  2. Binary format stability: verifying keys have a specific binary format (10-byte footer: VM version, public instance count, vk_params length, vk length in LE). The format must be versioned and stable across upgrades.
  3. Cache coherence: verifying keys have different access patterns and sizes than contract WASM. They require separate filesystem caching (vks/ vs modules/) and LRU eviction with pinned keys for hot circuits.
  4. 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.
  5. 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_proof host function. Contracts call deps.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_proofs library
  • 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_params length (little-endian u32)
    • Bytes 6–9: vk length (little-endian u32)
  • The constant K (circuit degree) needs no dedicated byte — it is recoverable from the deserialized Params object

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 CodeInfo with both wasm_checksum and vk_checksum fields
  • The Go-side wasm_engine.go interface adds GetCircuit(code wasmvm.Checksum) (wasmvm.CircuitBinary, error) alongside GetCode

Phase 3: CACHE (node-level)

  • At node startup or first use, the filesystem cache writes compiled WASM to modules/ and verifying keys to vks/
  • 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_cache HashMap for permanently resident keys (operator-configurable)
  • Each LoadedVerifyingKey holds deserialized Params<vesta::Affine> and VerifyingKey<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 SingleStrategy verifier
  • Runs plonk verification against the vesta/pallas curve
  • Public instances must be pasta_curves::Fp values
  • 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:

CallbackZK IntegrationPath
AuthenticateBLS12-381 aggregate signature onlyFast path (no ZK)
ConfirmExecutionHalo2/Plonk proof + nullifier verificationSlow path (ZK + BLS)
OnAuthenticatorAddedValidates operator PoPs, stores WavsOperatorSetNo 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

ComponentChange
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 toolingbuild-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_cache is 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-local produces 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_checksum invalidation 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 by build_and_write()
  • Integration: upload a contract with MsgStoreCodeWithCircuit, verify both wasm_checksum and vk_checksum are stored in CodeInfo
  • Integration: call deps.api.verify_halo2_proof() from a test contract with a valid proof and matching VK — verify it returns true
  • Integration: call deps.api.verify_halo2_proof() with an invalid proof — verify it returns false
  • 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_cache after LRU eviction of unpinned keys
  • Cache: verify that vks/ and modules/ directories are populated independently at node startup
  • Mode switching: verify that just deps-zk-local compiles 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 MsgStoreCodeWithCircuit be governance-gated (like MsgStoreCode in 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/

On this page