Introduction to Zero Knowledge

What is Zero Knowledge?

Zero Knowledge refers to a cryptographic concept where one party (the prover) can prove to another party (the verifier) that they know a value or that a statement is true, without revealing any information beyond the validity of the statement itself.

This seemingly paradoxical capability has profound implications for privacy, security, and trust in digital systems.

The Three Properties

A zero-knowledge proof must satisfy three fundamental properties:

  1. Completeness: If the statement is true, an honest verifier will be convinced by an honest prover.

  2. Soundness: If the statement is false, no cheating prover can convince the honest verifier that it is true (except with negligible probability).

  3. Zero-Knowledge: If the statement is true, the verifier learns nothing other than the fact that the statement is true.

A Simple Analogy: The Cave

Imagine a circular cave with a single entrance and a magic door blocking the path inside. The door can only be opened with a secret word.

        Entrance
           |
           v
    +------+------+
    |             |
    |    [DOOR]   |
    |             |
    +------+------+
           ^
           |
        Exit

Alice claims she knows the secret word. Bob wants to verify this without learning the word itself.

  1. Bob waits outside while Alice enters the cave
  2. Alice chooses a path (left or right) randomly
  3. Bob enters and calls out which path Alice should exit from
  4. If Alice knows the secret word, she can always comply by opening the door if needed
  5. If Alice doesn't know the word, she can only succeed by chance (50%)
  6. Repeating this multiple times makes cheating statistically impossible

This demonstrates zero knowledge: Bob becomes convinced Alice knows the secret, but never learns what it is.

Why Does Zero Knowledge Matter?

Zero-knowledge proofs enable:

  • Privacy: Prove you have sufficient funds without revealing your balance
  • Authentication: Prove your identity without transmitting passwords
  • Scalability: Verify computations without re-executing them
  • Compliance: Prove regulatory compliance without exposing sensitive data

Zero Knowledge on Kusama

The Kusama network provides an ideal environment for building and deploying zero-knowledge applications:

  • Low-Cost Verification: BN254 pairing precompiles cost 88x less than Ethereum (~$0.017 vs $10+)
  • Rust-Optimized: PolkaVM enables efficient Rust-based cryptographic operations
  • Asset Hub Integration: Native support for private asset transfers on Kusama's system parachain
  • Developer-Friendly: EVM-compatible tooling with Substrate interoperability

This wiki focuses on practical ZK development for the Kusama ecosystem, including:

  • Building shielded pools for private transfers
  • Implementing ZK identity and credentials
  • Deploying privacy-preserving DeFi applications
  • Leveraging Kusama's precompiles for efficient proof verification

History and Development

YearMilestone
1985Goldwasser, Micali, and Rackoff introduce ZK proofs
1986Fiat-Shamir heuristic creates non-interactive proofs
2012Groth-Sahai proofs enable practical applications
2016Zcash launches with zk-SNARKs
2020szk-Rollups scale Ethereum
2025+ZK applications deploy on Kusama Asset Hub

Getting Started

This wiki covers the fundamental concepts, mathematical foundations, and practical applications of zero-knowledge technology, with a focus on building for the Kusama network. Whether you're a developer, researcher, or curious learner, you'll find resources to deepen your understanding.

About This Wiki

Much of the technical content, code examples, and practical implementations in this wiki are derived from the Kusama Shield privacy project. Kusama Shield is building a Shielded pool on Kusama Asset Hub, and their work has been instrumental in demonstrating the feasibility of zero-knowledge applications on Kusama.

Key contributions from Kusama Shield:

  • PoseidonPolkaVM: Rust-optimized Poseidon hashing for PolkaVM
  • Shielded pool contract architecture
  • Circom circuit implementations
  • Client-side proof generation with Groth16 and Halo2
  • Gas benchmarks and feasibility analysis

Visit kusamashield.codeberg.page to learn more about their privacy-preserving transfer system.

Quick Navigation

Community

Join the Kusama ZK community on Matrix:

  • ZK Bounty Matrix Group - Discuss the Zero Knowledge and Advanced Cryptography bounty program, RFPs, and funding opportunities
  • Kusama Privacy Chat - General discussion about privacy technology and shielded pool development on Kusama

Next: Core Concepts

Core Concepts

This section covers the fundamental building blocks of zero-knowledge systems.

Overview

Zero-knowledge cryptography combines several key concepts:

  • Mathematical Foundations: Number theory, elliptic curves, and polynomial commitments
  • Proof Systems: Methods for constructing and verifying proofs
  • Cryptographic Primitives: Hash functions, commitments, and encryption

Key Terminology

TermDefinition
ProverThe party demonstrating knowledge of a secret
VerifierThe party being convinced of the proof
WitnessThe secret information the prover knows
StatementThe claim being proven
CircuitA computational representation of the statement

Trust Models

Trusted Setup

Some proof systems require a one-time setup ceremony that generates public parameters. If the setup is compromised, the security guarantees may be weakened.

Transparent Setup

Other systems (like STARKs) require no trusted setup, relying only on publicly verifiable randomness.

Computational Assumptions

Zero-knowledge proofs rely on various hardness assumptions:

  • Discrete Logarithm Problem
  • Knowledge of Exponent Assumption
  • Random Oracle Model

Next: Zero-Knowledge Proofs

Zero-Knowledge Proofs

Formal Definition

A zero-knowledge proof is an interactive protocol between a prover P and verifier V where:

  • P convinces V that a statement x ∈ L is true (where L is some language)
  • V learns nothing beyond the truth of the statement

Types of ZK Proofs

Proof of Knowledge

Demonstrates that the prover knows a witness w such that (x, w) satisfies some relation R.

Proof of Membership

Demonstrates that x belongs to a language L without revealing which witness was used.

Common Proof Systems

Σ-Protocols (Sigma Protocols)

Three-round public-coin honest-verifier zero-knowledge proofs:

  1. Commitment: Prover sends a commitment
  2. Challenge: Verifier sends a random challenge
  3. Response: Prover responds based on the challenge

Bulletproofs

Short non-interactive zero-knowledge proofs without trusted setup. Commonly used for range proofs in cryptocurrencies.

zk-SNARKs

Zero-Knowledge Succinct Non-Interactive Arguments of Knowledge

  • Succinct: Proofs are small and fast to verify
  • Non-Interactive: Single message from prover to verifier
  • Arguments: Computational soundness (vs. statistical)

zk-STARKs

Zero-Knowledge Scalable Transparent Arguments of Knowledge

  • Scalable: Proving time grows quasi-linearly
  • Transparent: No trusted setup required
  • Post-Quantum: Based on hash functions

Comparison

PropertySNARKsSTARKsBulletproofs
Proof Size~288 bytes~200 KB~600 bytes
Verification Time~10ms~100ms~seconds
Trusted SetupRequiredNoneNone
Post-QuantumNoYesNo

Next: Interactive vs Non-Interactive

Interactive vs Non-Interactive Proofs

Interactive Proofs

In an interactive proof, the prover and verifier exchange multiple messages:

Prover                    Verifier
  |                          |
  | ---- Commitment -------> |
  |                          |
  | <---- Challenge -------- |
  |                          |
  | ------ Response -------> |
  |                          |
  |                    (Verify)

Characteristics

  • Multiple rounds of communication
  • Verifier contributes randomness during the protocol
  • Cannot be verified by third parties without re-running the protocol

Example: Schnorr Protocol

Proves knowledge of discrete log:

  1. Prover commits: r ← random, send R = g^r
  2. Verifier challenges: send c ← random
  3. Prover responds: s = r + c·x (where x is the secret)
  4. Verifier checks: g^s = R · y^c (where y = g^x)

Non-Interactive Proofs

In a non-interactive proof, the prover generates a single proof that anyone can verify:

Prover                    Public
  |                          |
  | ---- Proof π --------->  |
  |                          |
  |                    (Anyone verifies)

The Fiat-Shamir Transform

Converts interactive Σ-protocols to non-interactive using a hash function:

  1. Instead of verifier sending random challenge c
  2. Prover computes c = H(commitment, statement)
  3. Proof = (commitment, response)

Characteristics

  • Single message from prover
  • Publicly verifiable
  • Can be stored and verified later
  • Essential for blockchain applications

Security Considerations

The Fiat-Shamir transform requires:

  • Random Oracle Model: Hash function behaves like random oracle
  • Transcript Binding: Challenge must include all relevant context

When to Use Each

Use CaseType
Real-time authenticationInteractive
Blockchain transactionsNon-interactive
Private computationNon-interactive
Identification protocolsInteractive

Next: SNARKs and STARKs

SNARKs and STARKs

zk-SNARKs

Zero-Knowledge Succinct Non-Interactive Argument of Knowledge

How SNARKs Work

  1. Circuit Representation: Express computation as an arithmetic circuit
  2. R1CS Constraint System: Convert to Rank-1 Constraint System
  3. QAP Transformation: Encode as Quadratic Arithmetic Program
  4. Polynomial Commitment: Commit to polynomials representing the witness
  5. Proof Generation: Create succinct proof using cryptographic primitives
ConstructionKey Features
Groth16Smallest proofs, requires trusted setup per circuit
PLONKUniversal trusted setup, flexible circuits
MarlinUniversal setup, efficient proving
Halo2No trusted setup, recursive proofs

Trusted Setup Ceremonies

Many SNARKs require a Multi-Party Computation (MPC) ceremony:

  • Multiple participants generate randomness
  • Each participant's contribution can be verified
  • At least one honest participant ensures security
  • "Toxic waste" must be destroyed after ceremony

zk-STARKs

Zero-Knowledge Scalable Transparent Argument of Knowledge

How STARKs Work

  1. Execution Trace: Represent computation as a trace of states
  2. Polynomial Interpolation: Encode trace as polynomials
  3. FRI Protocol: Prove low-degree of polynomials
  4. Merkle Proofs: Provide cryptographic commitments

Advantages over SNARKs

  • No Trusted Setup: Uses only hash functions
  • Post-Quantum Security: Based on symmetric cryptography
  • Scalability: Proving time scales well with computation size
  • Transparency: All parameters publicly verifiable

Trade-offs

  • Larger proof sizes (~200 KB vs ~288 bytes)
  • Higher verification costs
  • Newer technology with less battle-testing

Comparison Table

FeatureSNARKsSTARKs
Proof SizeSmall (~288 bytes)Large (~200 KB)
VerificationFast (~10ms)Slower (~100ms)
Proving TimeFastFast
Trusted SetupOften requiredNever required
Quantum ResistanceNoYes
MaturityHighGrowing

Emerging Hybrids

New constructions combine benefits of both:

  • BooSTARKs: STARKs with SNARK-like proof sizes
  • Binius: Binary field arithmetic for efficiency

Next: Applications

Zero Knowledge on Kusama

This section covers building zero-knowledge applications on the Kusama and Polkadot networks, with practical examples and deployment guides.

Why Kusama for ZK Applications?

Kusama provides a unique environment for zero-knowledge development:

  • PolkaVM: Efficient RISC-V based smart contracts with low gas costs
  • Asset Hub: Native support for private asset transfers
  • Cross-Chain Interoperability: Opt into all Polkadot infrastructure from Kusama, access countless permissionless integrations.
  • Lower Stakes: Cheap, fast and unstoppable transactions - ideal for experimentation

Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│                    Kusama Network                            │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │  PolkaVM    │  │ Asset Hub   │  │  Other Parachains   │  │
│  │  Contracts  │  │  (ZK Pool)  │  │  (via XCM)          │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
│         │                │                      │            │
│         └────────────────┼──────────────────────┘            │
│                          │                                   │
│              ┌───────────▼───────────┐                       │
│              │   ZK Verifier         │                       │
│              │   (Groth16/PLONK)     │                       │
│              └───────────────────────┘                       │
└─────────────────────────────────────────────────────────────┘
                           ▲
                           │
              ┌────────────┴────────────┐
              │   Off-chain Provers     │
              │   - Circom circuits     │
              │   - snarkJS proving     │
              │   - Merkle tree mgmt    │
              └─────────────────────────┘

Key Components

1. PolkaVM Contracts

PolkaVM is a RISC-V based virtual machine for Substrate:

  • Gas efficient: Lower costs than EVM for ZK verification
  • Rust native: Write contracts in Rust
  • ZK-friendly: Optimized for cryptographic operations

2. Poseidon Hash Function

Poseidon is the preferred hash function for ZK circuits:

  • SNARK-friendly: Designed for arithmetic circuits
  • BN254 curve: Compatible with Ethereum and Kusama
  • Gas efficient: Cheaper than Keccak/SHA in circuits

3. LeanIMT (Incremental Merkle Tree)

Efficient sparse Merkle tree for privacy pools:

  • Dynamic depth: Grows with insertions
  • Gas optimized: Minimal storage updates
  • zk-kit compatible: Standard implementation

Example Applications

ApplicationDescriptionComponents
Shielded PoolPrivate asset transfersCircom + PolkaVM + LeanIMT
ZK IdentityAnonymous credentialsPoseidon + Groth16
Private DEXHidden order booksSNARKs + Asset Hub
ZK BridgeCross-chain privacyXCM + Verifiers

Development Stack

┌─────────────────────────────────────────┐
│           Frontend (TypeScript)          │
│   - snarkJS for proof generation        │
│   - polkadot.js for chain interaction   │
└─────────────────────────────────────────┘
                    │
┌───────────────────▼───────────────────┐
│           ZK Circuits (Circom)         │
│   - Circuit design                    │
│   - Witness generation                │
│   - Proof creation                    │
└───────────────────────────────────────┘
                    │
┌───────────────────▼───────────────────┐
│        Smart Contracts (Rust/Solidity) │
│   - PolkaVM for verification          │
│   - Asset Hub for token management    │
└───────────────────────────────────────┘
                    │
┌───────────────────▼───────────────────┐
│           Kusama Network               │
│   - Paseo testnet                     │
│   - Asset Hub parachain               │
└───────────────────────────────────────┘

Testnet Information

Paseo Asset Hub

Paseo testnet is a stable relaychain testnet with multiple parachains connected to it. Making it the ideal place to test your applications.

ParameterValue
Chain ID420420422
RPC Endpointhttps://testnet-passet-hub-eth-rpc.polkadot.io
Block Explorerhttps://blockscout-passet-hub.parity-testnet.parity.io/
Faucethttps://faucet.polkadot.io/?parachain=1111
TokenPAS (Paseo)
websitehttps://paseo.site/

Getting Started

  1. Set up development environment

    • Install Rust and PolkaVM toolchain
    • Install Node.js and snarkJS
    • Install Circom compiler
  2. Write your first circuit

    • Start with simple arithmetic circuits
    • Generate test witnesses
    • Create proofs locally
  3. Deploy verifier contract

    • Use snarkJS to generate Solidity verifier
    • Deploy to Paseo Asset Hub
    • Test with generated proofs
  4. Build full application

    • Add Merkle tree for state
    • Implement deposit/withdraw logic
    • Create frontend for users

Next Steps


Previous: Blockchain and Cryptocurrencies | Next: PolkaVM Smart Contracts

Why Kusama is Good for ZK Applications

Kusama provides a uniquely favorable environment for zero-knowledge applications. This page covers the technical and economic advantages of building ZK dApps on Kusama Asset Hub.

Executive Summary

MetricKusama Asset HubEthereum (20 gwei)Advantage
Groth16 Verification3,990 gas ($0.017)202,216 gas ($10.11)594x cheaper
Poseidon Hash (Rust)2,706 gas ($0.012)32,800 gas ($1.64)137x cheaper
Merkle Path (depth 20)37,956 gas ($0.16)605,000 gas ($30.25)189x cheaper
Shielded Pool Deposit~45,000 gas ($0.19)~1,088,000 gas ($54.40)286x cheaper
Shielded Pool Withdraw~7,000 gas ($0.03)~301,000 gas ($15.05)502x cheaper

Costs at KSM $4.30, ETH $2,500. Data from zk-benchmarks study.

Technical Advantages

1. BN254 Precompiles at 88x Lower Cost

Kusama's pallet-revive implements all standard Ethereum precompiles (0x01-0x09) with dramatically reduced costs:

PrecompileKusama GasEthereum GasRatio
ecRecover9913,0000.33x
ecAdd (BN254)9911506.6x*
ecMul (BN254)9916,0000.17x
ecPairing (4 pairs)2,062181,0000.011x

*Note: ecAdd hits gas floor on Kusama but actual compute cost is low.

The ecPairing precompile is the dominant cost in Groth16 verification. At 2,062 gas on Kusama vs 181,000 on Ethereum, ZK proof verification is 88x cheaper in gas terms.

2. Rust-Compiled PVM for Performance

Kusama supports a hybrid architecture:

┌─────────────────────────────────────┐
│  Solidity Contract (High-level)     │
│  - Pool logic, state management     │
│  - Calls Rust for crypto ops        │
└──────────────┬──────────────────────┘
               │
┌──────────────▼──────────────────────┐
│  Rust PVM Contract (Performance)    │
│  - Poseidon hashing                 │
│  - Merkle tree operations           │
│  - 17.7x cheaper than Solidity      │
└─────────────────────────────────────┘

Why Rust is 28x faster:

  • Solidity → RISC-V translation suffers overhead for 256-bit arithmetic
  • Rust uses native u64 limbs with u128 intermediates
  • Montgomery multiplication maps 1:1 to RISC-V instructions
  • Full compiler optimization across the tight inner loops

3. Three-Dimensional Gas Model

pallet-revive uses a multi-dimensional resource metering system:

DimensionWhat it MeasuresZK Relevance
ref_timeComputation time (picoseconds)Primary constraint for ZK ops
proof_sizeState proof size for validatorsMinor for ZK compute
storage_depositLong-term storage (refundable)Merkle tree growth

The gas number reported to EVM tooling is a scaled composite:

gas = max(ref_time, proof_size) / GasScale

With GasScale = 100,000, compute-heavy ZK operations benefit from the efficient ref_time pricing.

4. Block Budget Allows Complex Operations

MetricValue
Block ref_time limit1.44 × 10¹² picoseconds (1.44 seconds)
Max Rust Poseidon hashes per block39
Max Merkle depth per transaction32+
Shielded pool deposit block usage~55%
Shielded pool withdraw block usage~5%

A complete shielded pool deposit (commitment hash + Merkle insertion depth 20) fits comfortably in a single block.

Economic Advantages

1. Sub-Dollar Privacy

The full cost of using a shielded pool:

OperationGasKSM CostUSD Cost
Deposit~45,0000.045$0.19
Withdrawal~7,0000.007$0.03
Full cycle~52,0000.052$0.22

Compare to Ethereum at 20 gwei:

  • Deposit: $54.40
  • Withdrawal: $15.05
  • Full cycle: $69.45

Kusama is 316x cheaper for a complete privacy transaction.

2. Viable Denominations

At sub-dollar costs, small denominations become economically viable:

DenominationGas Cost %Ethereum Gas Cost %
1 KSM ($4.30)5%N/A (prohibitive)
10 KSM ($43)0.5%N/A
100 KSM ($430)0.05%~13%

This enables privacy for small transactions that would be uneconomical on Ethereum.

3. Treasury Bootstrapping

Kusama treasury holds ~704K KSM. Treasury-funded privacy operations could bootstrap anonymity sets:

  • 100 deposits of 10 KSM = 1,000 KSM principal (recoverable) + $19 gas
  • Creates immediate anonymity set for early adopters
  • Treasury proposals can fund relayer infrastructure

Comparison with Other ZK-Friendly Chains

ChainZK Verification CostHash CostPrivacy Viability
Kusama Asset Hub$0.017$0.012High (if adoption)
Ethereum$10.11$1.64Medium (high cost)
Polygon$0.50$0.08Low (small anonymity sets)
BNB Chain$0.30$0.05Medium
AztecCustomCustomHigh (but L2-only)

Real-World Benchmarks

From the zk-benchmarks study (Phase 1):

Groth16 Verification

Contract: 0x1619C4416B8D55BA041cFfB6a80447796AeAe141
Valid proof verification: 3,990 gas
Invalid proof (reverts): ~3,990 gas
Deploy verifier: 915,115 gas

Poseidon Hashing

ImplementationSingle Hash10 HashesMerkle Depth 20
Solidity (resolc)47,851 gasIMPOSSIBLEIMPOSSIBLE
Rust PVM2,706 gas17,420 gas37,956 gas
Improvement17.7x

Precompile Costs

ecPairing (2 pairs):  1,511 gas
ecPairing (4 pairs):  2,062 gas
ecPairing (6 pairs):  2,613 gas (estimated)

Per-pair cost: ~276 gas (vs Ethereum's 34,000 gas)
Ratio: 123x cheaper per pair

Architecture Recommendations

Do ✅

  • Use Rust for crypto: Poseidon, Merkle trees, field arithmetic
  • Use Solidity for logic: Pool flow, state management, access control
  • Call precompiles directly: BN254 pairing, ecMul, ecAdd
  • Batch operations: Multiple hashes in one transaction
  • Use LeanIMT: Gas-efficient Merkle tree structure

Don't ❌

  • Pure Solidity crypto: Hits block weight limit
  • Trust gas numbers alone: Check ref_time for true cost
  • Assume Ethereum parity: Some precompiles unavailable (e.g., EIP-4844)
  • Ignore storage deposits: Merkle tree growth requires upfront capital

Known Limitations

LimitationImpactWorkaround
Solidity 256-bit arithmetic28x slower than RustUse Rust PVM for field ops
Gas abstractionCan mislead on true costCheck ref_time in errors
PointEvaluation (0x0A)Not availableUse Groth16, not KZG-based
Block ref_time limitMax ~39 hashes/txBatch carefully, use Rust
Compiler maturityresolc still experimentalTest thoroughly, use Rust

Getting Started

  1. Set up development environment

    # Install Rust and PolkaVM toolchain
    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    cargo install polkatool
    
    # Install Foundry for Solidity
    curl -L https://foundry.paradigm.xyz | bash
    foundryup
    
  2. Study the benchmarks

  3. Build your first contract

    • Start with PoseidonPolkaVM example
    • Test on Paseo testnet
  4. Deploy and measure

    • Use cast estimate for gas estimation
    • Check transaction receipts for actual ref_time

Resources


Previous: Kusama ZK Introduction | Next: PolkaVM Smart Contracts

PolkaVM Smart Contracts

PolkaVM is a RISC-V based virtual machine for Substrate blockchains, offering gas-efficient smart contract execution. This guide covers building ZK-friendly contracts on PolkaVM.

What is PolkaVM?

PolkaVM is an alternative to the EVM designed for the Polkadot/Kusama ecosystem:

  • RISC-V based: Standard instruction set, easy to target from many languages
  • Gas efficient: Lower costs for cryptographic operations
  • Rust native: First-class Rust support with no_std
  • ZK optimized: Better performance for verification circuits

Development Setup

Online IDE (No Setup Required)

RevX.dev - Browser-based PolkaVM IDE:

  • Write, compile, and deploy Rust contracts directly in your browser
  • Pre-configured toolchain - no local installation needed
  • Built-in deployment to Paseo testnet
  • Perfect for learning and rapid prototyping

Recommended for beginners: Start with RevX.dev to experiment, then set up local tooling for production.

AI-Assisted Development

Claude Code AI Guide to PolkaVM - Comprehensive AI prompts and patterns:

  • Contract templates and boilerplate generation
  • Debugging assistance prompts
  • Gas optimization suggestions
  • ABI encoding/decoding helpers
  • Common patterns and best practices

Local Setup (Prerequisites)

# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Install PolkaVM toolchain
cargo install polkatool

# Install required target
rustup target add riscv64emac-unknown-none-polkavm

Project Structure

my-polkavm-contract/
├── Cargo.toml
├── build.sh
├── src/
│   ├── lib.rs
│   └── zk.rs
└── ts/
    ├── main.ts
    └── package.json

Example: Poseidon Hash Contract

This contract implements the Poseidon hash function on PolkaVM.

Rust Implementation (src/lib.rs)

#![allow(unused)]
#![no_main]
#![no_std]
fn main() {
extern crate alloc;

use alloc::vec::Vec;
use simplealloc::SimpleAlloc;

mod zk;
use zk::{poseidon, Fr};

#[global_allocator]
pub static mut GLOBAL: SimpleAlloc<{ 1024 * 10 }> = SimpleAlloc::new();

use uapi::{input, HostFn, HostFnImpl as api, ReturnFlags};
use ethabi::{decode, ParamType, Token};

// Function selector: hash(uint256[2])
const HASH_SELECTOR: [u8; 4] = [0x56, 0x15, 0x58, 0xfe];

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    unsafe {
        core::arch::asm!("unimp");
        core::hint::unreachable_unchecked();
    }
}

#[no_mangle]
#[polkavm_derive::polkavm_export]
pub extern "C" fn deploy() {}

#[no_mangle]
#[polkavm_derive::polkavm_export]
pub extern "C" fn call() {
    input!(selector: &[u8; 4]);
    
    match selector {
        &HASH_SELECTOR => {
            input!(buffer: &[u8; 4 + 32 + 32]);
            
            // Decode ABI input
            let param_types = &[ParamType::FixedArray(
                Box::new(ParamType::Uint(256)), 2
            )];
            let decode_result = decode(param_types, &buffer[4..]).unwrap();

            if let Token::FixedArray(array_tokens) = &decode_result[0] {
                if let (Token::Uint(a), Token::Uint(b)) = (&array_tokens[0], &array_tokens[1]) {
                    // Convert to field elements
                    let mut a_bytes = [0u8; 32];
                    let mut b_bytes = [0u8; 32];
                    a.to_big_endian(&mut a_bytes);
                    b.to_big_endian(&mut b_bytes);

                    let fr_a = Fr::from_limbs(bytes_to_limbs(&a_bytes));
                    let fr_b = Fr::from_limbs(&bytes_to_limbs(&b_bytes));

                    // Compute Poseidon hash
                    let hash_result = poseidon(&[fr_a, fr_b]);

                    // Return result
                    let result_limbs = hash_result.to_limbs();
                    let result_bytes = limbs_to_bytes(&result_limbs);
                    api::return_value(ReturnFlags::empty(), &result_bytes);
                }
            }
        }
        _ => panic!("Unknown function"),
    }
}

fn bytes_to_limbs(bytes: &[u8; 32]) -> [u64; 4] {
    let mut limbs = [0u64; 4];
    for i in 0..4 {
        let start_idx = (3 - i) * 8;
        limbs[i] = u64::from_be_bytes([
            bytes[start_idx], bytes[start_idx+1],
            bytes[start_idx+2], bytes[start_idx+3],
            bytes[start_idx+4], bytes[start_idx+5],
            bytes[start_idx+6], bytes[start_idx+7]
        ]);
    }
    limbs
}

fn limbs_to_bytes(limbs: &[u64; 4]) -> [u8; 32] {
    let mut result = [0u8; 32];
    for i in 0..4 {
        let limb_bytes = limbs[3-i].to_be_bytes();
        for j in 0..8 {
            result[i*8 + j] = limb_bytes[j];
        }
    }
    result
}
}

Poseidon Implementation (src/zk.rs)

Key components of the Poseidon hash:

#![allow(unused)]
fn main() {
// BN254 field element in Montgomery form
pub struct Fr([u64; 4]);

// Constants for BN254 curve
const MODULUS: [u64; 4] = [
    0x43e1f593f0000001,
    0x2833e84879b97091,
    0xb85045b68181585d,
    0x30644e72e131a029,
];

// S-box: x^5 for BN254
fn sbox(x: &Fr) -> Fr {
    let x2 = x.mul(&x);
    let x4 = x2.mul(&x2);
    x4.mul(x)
}

// Poseidon parameters: t=3, 8 full rounds, 57 partial rounds
const T: usize = 3;
const NROUNDS_F: usize = 8;
const NROUNDS_P: usize = 57;

pub fn poseidon(inputs: &[Fr; 2]) -> Fr {
    let mut state = [Fr::zero(), inputs[0], inputs[1]];
    
    // Apply rounds (constants and MDS matrix omitted for brevity)
    // ...
    
    state[0]
}
}

Build Script (build.sh)

#!/bin/bash
set -e

# Build for PolkaVM target
cargo build --release --target riscv64emac-unknown-none-polkavm

# Extract the contract binary
cp target/riscv64emac-unknown-none-polkavm/release/erc20 .

# Generate TypeScript bindings
cd ts
yarn install

Deployment

1. Compile and Build

./build.sh

2. Deploy to Testnet

cd ts/
export AH_PRIV_KEY="your_private_key"
yarn erc20

Output:

contract address is:  0xD670137a3CfAaF08B5395CDaaEaFf617672896D2

3. Call the Contract

Using cast (Foundry):

cast call <CONTRACT_ADDRESS> \
  "hash(uint256[2]):(uint256)" \
  "[123,456]" \
  --rpc-url https://testnet-passet-hub-eth-rpc.polkadot.io

Expected output:

10422317022970317265083564129873630166...

Solidity Interface

Call the deployed contract from Solidity:

pragma solidity ^0.8.30;

interface IPoseidonT3 {
    function hash(uint256[2] memory input) external pure returns (uint256);
}

contract MyContract {
    function computeHash(uint256 a, uint256 b) 
        external view returns (uint256) 
    {
        return IPoseidonT3(POSEIDON_ADDRESS).hash([a, b]);
    }
}

Example: ERC20 Token Contract

Beyond cryptographic primitives, PolkaVM can implement full ERC20 tokens in Rust. The polkavm-erc20-in-rust repository provides a complete example.

Key Features

  • Full ERC20 Implementation: transfer, approve, transferFrom, balanceOf, allowance
  • Solidity Compatible: Works with existing Solidity tools and interfaces
  • Rust Performance: Optimized with SimpleAlloc and LTO
  • TypeScript Deployment: Complete deployment and testing scripts

Project Structure

polkavm-erc20-in-rust/
├── src/
│   └── erc20.rs        # Main contract implementation
├── ts/
│   ├── main.ts         # Deployment script
│   └── erc20.ts        # Interaction helpers
├── build.sh            # Build script
└── Cargo.toml          # Dependencies

Function Selectors

#![allow(unused)]
fn main() {
const NAME_SELECTOR: [u8; 4] = [0x06, 0xfd, 0xde, 0x03];      // name()
const SYMBOL_SELECTOR: [u8; 4] = [0x95, 0xd8, 0x9b, 0x41];     // symbol()
const DECIMALS_SELECTOR: [u8; 4] = [0x31, 0x3c, 0xe5, 0x67];   // decimals()
const TOTAL_SUPPLY_SELECTOR: [u8; 4] = [0x18, 0x16, 0x0d, 0xdd]; // totalSupply()
const BALANCE_OF_SELECTOR: [u8; 4] = [0x70, 0xa0, 0x82, 0x31]; // balanceOf(address)
const TRANSFER_SELECTOR: [u8; 4] = [0xa9, 0x05, 0x9c, 0xbb];   // transfer(address,uint256)
const APPROVE_SELECTOR: [u8; 4] = [0x09, 0x5e, 0xa7, 0xb3];    // approve(address,uint256)
const TRANSFER_FROM_SELECTOR: [u8; 4] = [0x23, 0xb8, 0x72, 0xdd]; // transferFrom(address,address,uint256)
}

Storage Pattern

#![allow(unused)]
fn main() {
// Balance storage key: 7-byte prefix + 20-byte address
pub fn get_balance_key(sender: &[u8; 20]) -> [u8; 27] {
    let mut key = [0u8; 27];
    key[0..7].copy_from_slice(BALANCE);
    key[7..27].copy_from_slice(sender);
    key
}

// Allowance storage key: 9-byte prefix + 20-byte owner + 20-byte spender
pub fn get_allowance_key(owner: &[u8; 20], spender: &[u8; 20]) -> [u8; 49] {
    let mut key = [0u8; 49];
    key[0..9].copy_from_slice(ALLOWANCE);
    key[9..29].copy_from_slice(owner);
    key[29..49].copy_from_slice(spender);
    key
}
}

Build and Deploy

# Build the contract
./build.sh

# Deploy via TypeScript
cd ts
mv env.example .env  # Add your private key
yarn install
yarn erc20

Example Output

contract address is:  0xAA00B7111CB9dd5074Db32E1B7917dD01ceb39dE
Name:  name
Symbol:  symbol
Decimals:  18
Total Supply:  1234000000000000000000
Transfer successful: 1233 tokens sent

→ View Full ERC20 Example

Gas Optimization Tips

  1. Use Poseidon over Keccak: 5-10x cheaper in circuits
  2. Batch operations: Multiple hashes in one call
  3. Minimize storage: Use memory for intermediate calculations
  4. LeanIMT: Efficient Merkle tree updates

Common Issues

Memory Allocation

PolkaVM has limited memory. Use SimpleAlloc:

#![allow(unused)]
fn main() {
#[global_allocator]
pub static mut GLOBAL: SimpleAlloc<{ 1024 * 10 }> = SimpleAlloc::new();
}

ABI Decoding

Ensure proper ABI encoding for inputs:

#![allow(unused)]
fn main() {
// Input format: [selector, encoded_params...]
// hash(uint256[2]): selector + offset + [a, b]
}

Field Arithmetic

Always validate field elements are in range:

#![allow(unused)]
fn main() {
require(value < SNARK_SCALAR_FIELD, "Value out of range");
}

Testing

Local Testing

# Test with known inputs
cast call <ADDRESS> "hash(uint256[2]):(uint256)" "[0,0]"
# Expected: 552037174761081804855249776048373...

cast call <ADDRESS> "hash(uint256[2]):(uint256)" "[1,2]"
# Expected: 324046091733193931345823963991450...

Integration Tests

// ts/main.ts
import { Contract } from 'ethers';

const contract = new Contract(address, abi, provider);
const hash = await contract.hash([123, 456]);
console.log('Hash:', hash.toString());

Resources

Development Tools

Libraries & Examples

Testnet Resources


Previous: Kusama ZK Introduction | Next: Poseidon Hash

Poseidon Hash on Kusama

Poseidon is a cryptographic hash function designed for zero-knowledge proof systems. This guide covers implementing and using Poseidon on Kusama/Polkadot networks.

Why Poseidon?

Hash FunctionConstraints (BN254)Gas CostZK-Friendly
Poseidon~240Low
Keccak-256~25,000High
SHA-256~20,000High

Poseidon is 100x more efficient in ZK circuits than traditional hashes.

Security: $1M Ethereum Foundation Bounty

The Ethereum Foundation has placed a $1 million bounty on breaking Poseidon through the Poseidon Initiative.

The Challenge

  • Reward: $1,000,000 USD
  • Task: Find a collision or preimage attack against Poseidon
  • Status: UNCLAIMED - No one has successfully broken Poseidon
  • Significance: Demonstrates confidence in Poseidon's security for critical infrastructure

This bounty reinforces Poseidon's position as the trusted hash function for:

  • ZK proof systems (Groth16, PLONK, Halo2)
  • Privacy protocols (Kusama Shield, Aztec)
  • Layer 2 rollups (StarkNet, Polygon zkEVM)
  • Kusama privacy applications

Mathematical Background

BN254 Curve

Poseidon on Kusama uses the BN254 (alt_bn128) curve:

  • Field: Prime field 𝔽p where p = 21888242871839275222246405745257275088696311157297823662689037894645226208583
  • Scalar Field: 𝔽r where r = 21888242871839275222246405745257275088548364400416034343698204186575808495617

Poseidon Parameters

For t=3 (2 inputs + 1 output):

ParameterValue
State Size (t)3
Full Rounds (R_F)8
Partial Rounds (R_P)57
S-Boxx⁵
MDS Matrix3×3 Cauchy matrix

Round Structure

┌─────────────────────────────────────────────────┐
│              Poseidon Permutation                │
├─────────────────────────────────────────────────┤
│  Round 0-3:  Full Round (S-Box all states)      │
│  Round 4-60: Partial Round (S-Box first state)  │
│  Round 61-64: Full Round (S-Box all states)     │
└─────────────────────────────────────────────────┘

Each round:

  1. Add round constants
  2. Apply S-Box (x⁵)
  3. Multiply by MDS matrix

Implementation in Rust (PolkaVM)

Field Element Structure

#![allow(unused)]
fn main() {
/// BN254 scalar field element in Montgomery form
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Fr([u64; 4]);

const MODULUS: [u64; 4] = [
    0x43e1f593f0000001,
    0x2833e84879b97091,
    0xb85045b68181585d,
    0x30644e72e131a029,
];

/// -MODULUS^(-1) mod 2^64
const INV: u64 = 0xc2e1f593efffffff;
}

Montgomery Multiplication

#![allow(unused)]
fn main() {
/// CIOS Montgomery multiplication: computes a*b*R^(-1) mod p
fn mont_mul(a: &[u64; 4], b: &[u64; 4]) -> Fr {
    let mut t = [0u64; 6];

    for i in 0..4 {
        let mut c: u64 = 0;
        for j in 0..4 {
            let uv = (t[j] as u128) + (a[j] as u128) * (b[i] as u128) + (c as u128);
            t[j] = uv as u64;
            c = (uv >> 64) as u64;
        }
        // ... reduction steps
    }
    Fr(result)
}
}

S-Box Implementation

#![allow(unused)]
fn main() {
fn sbox(x: &Fr) -> Fr {
    let x2 = x.mul(&x);      // x²
    let x4 = x2.mul(&x2);    // x⁴
    x4.mul(x)                // x⁵
}
}

Full Poseidon Function

#![allow(unused)]
fn main() {
const T: usize = 3;
const NROUNDS_F: usize = 8;
const NROUNDS_P: usize = 57;

pub fn poseidon(inputs: &[Fr; 2]) -> Fr {
    let total_rounds = NROUNDS_F + NROUNDS_P;
    let mut state = [Fr::zero(), inputs[0], inputs[1]];

    for r in 0..total_rounds {
        // Add round constants
        for i in 0..T {
            state[i] = state[i].add(&ROUND_CONSTANTS[r * T + i]);
        }

        // Apply S-Box
        if r < NROUNDS_F / 2 || r >= total_rounds - NROUNDS_F / 2 {
            // Full round: S-Box all states
            for i in 0..T {
                state[i] = sbox(&state[i]);
            }
        } else {
            // Partial round: S-Box first state only
            state[0] = sbox(&state[0]);
        }

        // MDS matrix multiplication
        let mut new_state = [Fr::zero(); T];
        for i in 0..T {
            for j in 0..T {
                new_state[i] = new_state[i].add(&state[j].mul(&MDS[i][j]));
            }
        }
        state = new_state;
    }

    state[0]  // Return first state as output
}
}

Solidity Integration

Interface

interface IPoseidon {
    function hash(uint256[2] memory inputs) external pure returns (uint256);
}

Usage in Contracts

contract MyZKContract {
    IPoseidon public constant POSEIDON = 
        IPoseidon(0x1d165f6fE5A30422E0E2140e91C8A9B800380637);

    function computeCommitment(uint256 value, uint256 secret) 
        external pure returns (uint256) 
    {
        return POSEIDON.hash([value, secret]);
    }

    function computeNullifier(uint256 nullifier, uint256 secret) 
        external pure returns (uint256) 
    {
        return POSEIDON.hash([nullifier, secret]);
    }
}

Circom Integration

Using circomlib

pragma circom 2.0.0;

include "circomlib/poseidon.circom";

template PoseidonExample() {
    signal input a;
    signal input b;
    signal output out;

    component poseidon = Poseidon(2);  // 2 inputs
    poseidon.inputs[0] <== a;
    poseidon.inputs[1] <== b;
    out <== poseidon.out;
}

component main {public [a, b]} = PoseidonExample();

Custom Poseidon in Circom

pragma circom 2.0.0;

include "poseidon_bn254.circom";

template MyHash() {
    signal input in[2];
    signal output out;

    component poseidon = Poseidon(2);
    
    for (let i = 0; i < 2; i++) {
        poseidon.inputs[i] <== in[i];
    }
    
    out <== poseidon.out;
}

Test Vectors

Verify your implementation against known values:

InputExpected Output
[0, 0]5520371747610818048552497760483731695918538905235353263918705622436011791040
[1, 2]3240460917331939313458239639914504962640333851982787431747809795119847651274
[123, 456]10422317022970317265083564129867363010880980031113186224756990573079674352133

Testing with cast

# Deploy contract first, then test:
cast call <CONTRACT_ADDRESS> \
  "hash(uint256[2]):(uint256)" \
  "[0,0]" \
  --rpc-url https://testnet-passet-hub-eth-rpc.polkadot.io

Applications

1. Commitment Scheme

#![allow(unused)]
fn main() {
// Commitment = Poseidon(value, secret)
let commitment = poseidon(&[value, secret]);
}

2. Nullifier Hash

#![allow(unused)]
fn main() {
// Prevents double-spending in privacy pools
let nullifier_hash = poseidon(&[nullifier, secret]);
}

3. Merkle Tree

#![allow(unused)]
fn main() {
// Internal node hash
let node_hash = poseidon(&[left_child, right_child]);
}

4. UTXO Commitment

#![allow(unused)]
fn main() {
// Full commitment for shielded pools
// Poseidon(value, asset, Poseidon(nullifier, secret))
let inner = poseidon(&[nullifier, secret]);
let commitment = poseidon(&[value, poseidon(&[asset, inner])]);
}

Gas Costs on Kusama

OperationPolkaVM GasEVM Gas
Poseidon hash~2,000~15,000
Keccak-256~50,000~87,000
Storage write~20,000~20,000

PolkaVM is 7.5x cheaper for Poseidon operations!

Security Considerations

Input Validation

Always validate inputs are in the valid field range:

require(input < SNARK_SCALAR_FIELD, "Input out of range");
#![allow(unused)]
fn main() {
const SNARK_SCALAR_FIELD: u256 = u256::from_str_radix(
    "21888242871839275222246405745257275088548364400416034343698204186575808495617",
    10
).unwrap();
}

Non-Zero Check

require(commitment != 0, "Invalid commitment");

Domain Separation

Use different prefixes for different use cases:

#![allow(unused)]
fn main() {
let tx_commitment = poseidon(&[TX_DOMAIN, value, secret]);
let identity_commitment = poseidon(&[ID_DOMAIN, value, secret]);
}

Resources


Previous: PolkaVM Smart Contracts | Next: Asset Hub Integration

EVM Precompiles Guide

Precompiles are native runtime functions exposed at fixed addresses, providing optimized implementations of cryptographic and utility operations. This guide covers using precompiles in ZK applications on Kusama Asset Hub.

What are Precompiles?

Precompiles run at the runtime level rather than as on-chain PVM contracts. When your contract calls a precompile address, the PVM detects it and executes optimized native code directly.

┌─────────────┐
│   Your      │
│  Contract   │
│  (Solidity) │
└──────┬──────┘
       │ Call 0x08 (ecPairing)
       ▼
┌─────────────────────────────────┐
│  PVM Runtime                    │
│  ┌─────────────────────────┐    │
│  │  Precompile 0x08        │    │
│  │  (Native BN254 pairing) │    │
│  └─────────────────────────┘    │
└─────────────────────────────────┘

Available Precompiles on Kusama

NameAddressKusama GasEthereum GasUse Case
ecRecover0x019913,000Signature verification
SHA-2560x0299160 + 12/wordGeneral hashing
RIPEMD-1600x03991600 + 120/wordBitcoin-style hashing
Identity0x0499115 + 3/wordData copying
ModExp0x05991VariableRSA, modular arithmetic
ecAdd (BN254)0x06991150BN254 point addition
ecMul (BN254)0x079916,000BN254 scalar multiplication
ecPairing0x081,511+113,000+ZK proof verification
Blake2f0x099914Blake2 compression
P256Verify0x100CallableN/AWebAuthn/passkey

Not Available

PrecompileAddressStatus
PointEvaluation (EIP-4844)0x0A❌ Not enabled
BLS12-381 (EIP-2537)0x0B-0x13❌ Not enabled

BN254 Precompiles for ZK

ecPairing (0x08) - The Most Important for ZK

The pairing check precompile is the core of Groth16 verification.

Gas Cost:

Base (floor):     991 gas
Per pair:        ~276 gas
Formula:    991 + 276 × pairs

Benchmark Results:

PairsKusama GasEthereum GasRatio
21,511113,00075x cheaper
42,062181,00088x cheaper
6~2,613~249,00095x cheaper

Solidity Interface:

interface IBn254Pairing {
    /// @param input Encoded pairing points
    /// @return result 1 if pairing check passes, 0 otherwise
    function pairingCheck(bytes calldata input) 
        external view returns (bool result);
}

// Input format: [(a_x, a_y, b_x1, b_x2, b_y1, b_y2), ...]
// Each tuple is 192 bytes (6 × 32 bytes)

Example Usage (from Groth16 Verifier):

function verifyProof(
    uint256[2] calldata _pA,
    uint256[2][2] calldata _pB,
    uint256[2] calldata _pC,
    uint256[3] calldata _pubSignals
) public view returns (bool) {
    assembly {
        // Prepare pairing input (6 pairs for Groth16)
        let mIn := mload(0x40)
        
        // -A (negated for verification)
        mstore(mIn, calldataload(_pA))
        mstore(add(mIn, 32), mod(sub(q, calldataload(add(_pA, 32))), q))
        
        // B
        mstore(add(mIn, 64), calldataload(_pB))
        mstore(add(mIn, 96), calldataload(add(_pB, 32)))
        mstore(add(mIn, 128), calldataload(add(_pB, 64)))
        mstore(add(mIn, 160), calldataload(add(_pB, 96)))
        
        // ... (alpha, beta, gamma, delta points)
        
        // Call precompile 0x08
        let success := staticcall(
            sub(gas(), 2000), 
            0x08, 
            mIn, 
            768,  // 6 pairs × 128 bytes
            mIn, 
            0x20
        )
        
        let isValid := and(success, mload(mIn))
        mstore(0, isValid)
        return(0, 0x20)
    }
}

ecMul (0x07) - BN254 Scalar Multiplication

Used in Groth16 for linear combination of verification key points.

Gas Cost: 991 gas (hits floor, actual compute is cheap)

Solidity Interface:

interface IBn254Mul {
    /// @param input Encoded point (x, y) and scalar (s)
    /// @return result Scaled point (x', y')
    function ecMul(bytes calldata input) 
        external view returns (uint256 x, uint256 y);
}

// Input: 96 bytes = [x (32), y (32), s (32)]
// Output: 64 bytes = [x' (32), y' (32)]

Example:

function multiplyPoint(uint256 x, uint256 y, uint256 scalar) 
    public view returns (uint256 rx, uint256 ry) 
{
    bytes memory input = abi.encodePacked(x, y, scalar);
    (bool success, bytes memory result) = 
        address(0x07).staticcall(input);
    require(success, "ecMul failed");
    return abi.decode(result, (uint256, uint256));
}

ecAdd (0x06) - BN254 Point Addition

Gas Cost: 991 gas (gas floor)

Solidity Interface:

interface IBn254Add {
    /// @param input Encoded two points
    /// @return result Sum of points
    function ecAdd(bytes calldata input) 
        external view returns (uint256 x, uint256 y);
}

// Input: 128 bytes = [x1 (32), y1 (32), x2 (32), y2 (32)]
// Output: 64 bytes = [x' (32), y' (32)]

Hash Precompiles

SHA-256 (0x02)

Gas Cost: 991 gas flat (vs Ethereum's 60 + 12/word)

interface ISha256 {
    function sha256(bytes calldata input) 
        external view returns (bytes32 hash);
}

// Usage
bytes memory data = "Hello World";
(bool success, bytes memory result) = 
    address(0x02).staticcall(data);
bytes32 hash = abi.decode(result, (bytes32));

Blake2f (0x09)

Blake2 compression function F, used in some ZK constructions.

Gas Cost: 991 gas flat

interface IBlake2f {
    /// @param input 213 bytes: rounds(4), h[8], m[16], t(2), f(1)
    function blake2f(bytes calldata input) 
        external view returns (bytes32[8] memory h);
}

System Precompile (0x0000000000000000000000000000000000000900)

Polkadot-specific precompile for runtime integration.

Key Functions for ZK

interface ISystem {
    // BLAKE2 hashing (Polkadot native)
    function hashBlake256(bytes memory input) 
        external pure returns (bytes32);
    
    function hashBlake128(bytes memory input) 
        external pure returns (bytes32);
    
    // Sr25519 verification (Polkadot signatures)
    function sr25519Verify(
        uint8[64] calldata signature,
        bytes calldata message,
        bytes32 publicKey
    ) external view returns (bool);
    
    // Weight tracking
    function weightLeft() 
        external view returns (uint64 refTime, uint64 proofSize);
    
    // ECDSA utilities
    function ecdsaToEthAddress(uint8[33] calldata publicKey) 
        external view returns (bytes20);
}

Usage Example

contract ZKVerifier {
    ISystem constant SYSTEM = ISystem(
        0x0000000000000000000000000000000000000900
    );
    
    function verifyWithWeightCheck(
        uint256[2] calldata proofA,
        // ... other proof data
    ) external {
        // Check remaining weight before expensive operation
        (uint64 refTime, ) = SYSTEM.weightLeft();
        require(refTime > 1e12, "Insufficient weight");
        
        // Use Blake2 for non-ZK hashing (cheaper than Poseidon)
        bytes32 dataHash = SYSTEM.hashBlake256(
            abi.encodePacked(msg.sender, block.number)
        );
        
        // Verify Groth16 proof
        _verifyGroth16(proofA, /* ... */);
    }
}

Storage Precompile (0x0000000000000000000000000000000000000901)

Low-level storage access for advanced patterns.

interface IStorage {
    // Full value read/write
    function get(bytes32 key) external view returns (bytes memory);
    function set(bytes32 key, bytes memory value) external;
    
    // Partial reads (optimize gas for large values)
    function get_range(
        bytes32 key, 
        uint32 offset, 
        uint32 length
    ) external view returns (bytes memory);
    
    function get_prefix(bytes32 key, uint32 max_length) 
        external view returns (bytes memory);
    
    // Inspection
    function has_key(bytes32 key) external view returns (bool);
    function length(bytes32 key) external view returns (uint32);
    
    // Cleanup
    function remove(bytes32 key) external;
}

Use Case: Efficient Merkle Tree Storage

contract MerkleTree {
    IStorage constant STORAGE = IStorage(
        0x0000000000000000000000000000000000000901
    );
    
    // Store leaf at sequential key
    function insertLeaf(uint256 leaf, uint256 index) internal {
        bytes32 key = keccak256(abi.encodePacked("leaf", index));
        STORAGE.set(key, abi.encode(leaf));
    }
    
    // Read specific leaf
    function getLeaf(uint256 index) 
        public view returns (uint256) 
    {
        bytes32 key = keccak256(abi.encodePacked("leaf", index));
        bytes memory data = STORAGE.get(key);
        return abi.decode(data, (uint256));
    }
    
    // Check existence without reading
    function leafExists(uint256 index) public view returns (bool) {
        bytes32 key = keccak256(abi.encodePacked("leaf", index));
        return STORAGE.has_key(key);
    }
}

ERC20 Precompile (Asset-Specific Addresses)

Each Assets pallet token has a mapped ERC20 precompile address.

Address Calculation

ERC20 Address = 0x[assetId (8 hex)] + [24 zeros] + [prefix (8 hex)]

Example Asset IDs:

AssetIDERC20 Address
USDT19840x000007C000000000000000000000000001200000
USDC13370x0000053900000000000000000000000001200000
DOT1000x0000006400000000000000000000000001200000

Usage in ZK Applications

interface IERC20 {
    function transfer(address to, uint256 amount) 
        external returns (bool);
    function balanceOf(address account) 
        external view returns (uint256);
    function approve(address spender, uint256 amount) 
        external returns (bool);
}

contract ShieldedPool {
    IERC20 constant USDT = IERC20(
        0x000007C000000000000000000000000001200000
    );
    
    function deposit(uint256 amount, uint256 commitment) external {
        // Transfer USDT from user to pool
        require(
            USDT.transferFrom(msg.sender, address(this), amount),
            "Transfer failed"
        );
        
        // Insert commitment into Merkle tree
        _insertCommitment(commitment);
    }
    
    function withdraw(
        uint256 amount,
        address recipient,
        // ... proof data
    ) external {
        // Verify proof
        _verifyProof(/* ... */);
        
        // Transfer USDT to recipient
        require(
            USDT.transfer(recipient, amount),
            "Withdrawal failed"
        );
    }
}

P256Verify (0x100) - WebAuthn Support

EIP-7212 precompile for WebAuthn/passkey verification.

interface IP256Verify {
    /// @param messageHash Hash of signed message
    /// @param r Signature component
    /// @param s Signature component  
    /// @param x Public key x-coordinate
    /// @param y Public key y-coordinate
    /// @return success true if signature valid
    function verify(
        bytes32 messageHash,
        uint256 r,
        uint256 s,
        uint256 x,
        uint256 y
    ) external view returns (bool success);
}

// Usage for passkey-based ZK proof submission
contract PasskeyVerifier {
    function verifyPasskeyProof(
        bytes32 messageHash,
        uint256[4] calldata signature,
        uint256[2] calldata publicKey,
        // ... ZK proof
    ) external view {
        bool valid = IP256Verify(0x100).verify(
            messageHash,
            signature[0],
            signature[1],
            publicKey[0],
            publicKey[1]
        );
        require(valid, "Invalid passkey signature");
        
        // Proceed with ZK verification
    }
}

Gas Optimization Strategies

1. Batch Precompile Calls

// ❌ Expensive: Multiple separate calls
for (uint i = 0; i < hashes.length; i++) {
    hash[i] = ISha256(0x02).sha256(data[i]);
}

// ✅ Better: Batch in single call where possible
bytes memory batched = abi.encodePacked(data[0], data[1], /* ... */);
bytes32 combined = ISha256(0x02).sha256(batched);

2. Use Rust for Heavy Crypto

// ❌ Solidity Poseidon (hits block limit)
for (uint i = 0; i < 20; i++) {
    node = IPoseidon(POSEIDON_ADDR).hash([left, right]);
} // REVERTS - exceeds block ref_time

// ✅ Rust PVM (fits easily)
for (uint i = 0; i < 20; i++) {
    node = IRustPoseidon(RUST_POSEIDON).hash([left, right]);
} // SUCCESS - 37,956 gas total

3. Check Weight Before Expensive Ops

function expensiveOperation() external {
    (uint64 refTime, ) = ISystem(SYSTEM_ADDR).weightLeft();
    
    // Need ~1T ref_time for 20 Poseidon hashes
    require(refTime > 1e12, "Insufficient weight");
    
    // Proceed with operation
    _computeMerklePath();
}

Testing Precompiles

Using Foundry

// test/PrecompileTest.t.sol
pragma solidity ^0.8.28;

import "forge-std/Test.sol";

contract PrecompileTest is Test {
    function testEcPairing() public {
        // Test vectors from EIP-197
        bytes memory input = hex"..."; // 192 bytes per pair
        (bool success, bytes memory result) = 
            address(0x08).staticcall(input);
        
        assertTrue(success);
        assertEq(abi.decode(result, (uint256)), 1);
    }
    
    function testSha256() public {
        bytes memory input = "test";
        (bool success, bytes memory result) = 
            address(0x02).staticcall(input);
        
        bytes32 expected = 0x9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08;
        assertEq(abi.decode(result, (bytes32)), expected);
    }
}

Using cast

# Test SHA-256
cast call 0x0000000000000000000000000000000000000002 \
  "$(cast calldata 'sha256(bytes)' $(cast to-bytes "test"))" \
  --rpc-url https://testnet-passet-hub-eth-rpc.polkadot.io

# Test ecPairing
cast call 0x0000000000000000000000000000000000000008 \
  "<pairing-input>" \
  --rpc-url https://testnet-passet-hub-eth-rpc.polkadot.io

Common Issues

"UnsupportedPrecompileAddress"

Some precompiles are not enabled in the runtime:

  • PointEvaluation (0x0A) - EIP-4844 not available
  • BLS12-381 (0x0B-0x13) - Not implemented

Solution: Use Groth16/BN254 instead of KZG/BLS-based schemes.

Gas Floor Confusion

Simple precompiles (0x01-0x05) all show 991 gas, but actual compute cost varies.

Solution: Check ref_time in transaction errors for true cost.

ecPairing Scaling

Gas increases linearly with pairs, but not at Ethereum rates.

Kusama: 991 + 276 × pairs
Ethereum: 45,000 + 34,000 × pairs

Resources


Previous: Poseidon Hash | Next: Asset Hub Integration

Asset Hub Integration

Asset Hub is the primary parachain for token management on Kusama/Polkadot. This guide covers integrating ZK applications with Asset Hub.

What is Asset Hub?

Asset Hub (formerly Statemint/Statemine) is a system parachain that provides:

  • Fungible Tokens: Create and manage ERC20-like assets
  • NFTs: Non-fungible token support
  • Low Fees: Cheaper transactions than the relay chain
  • EVM Compatibility: Smart contract support via Frontier

Network Configuration

ParameterValue
Network NamePaseo Asset Hub
Chain ID420420422
RPC URLhttps://testnet-passet-hub-eth-rpc.polkadot.io
Block Explorerhttps://blockscout-passet-hub.parity-testnet.parity.io/
Faucethttps://faucet.polkadot.io/?parachain=1111
Native TokenPAS

Kusama Mainnet

ParameterValue
Network NameKusama Asset Hub
Chain ID(TBD)
RPC URLhttps://kusama-asset-hub-rpc.polkadot.io
Block Explorerhttps://blockscout.kusama.network/
Native TokenKSM

Getting PAS Tokens

1. Create a Wallet

cast wallet new

Output:

Successfully created new keypair.
Address:     0x7E68B2bf528c96e9b9D140211391d4e5FBce033e
Private key: 0xf07706918ef3fac8d5c1856010f470fecf15dca5b30a1ad1e5f8b3c022d8e997

2. Fund from Faucet

Visit: https://faucet.polkadot.io/?parachain=1111

Enter your address and request test tokens.

3. Check Balance

cast balance 0x7E68B2bf528c96e9b9D140211391d4e5FBce033e \
  --rpc-url https://testnet-passet-hub-eth-rpc.polkadot.io

Deploying Contracts

Using Remix

  1. Visit Remix for Polkadot
  2. Connect to Paseo Asset Hub network
  3. Write/paste your contract
  4. Deploy using your wallet

Using Foundry

# Create project
forge init my-zk-project
cd my-zk-project

# Add dependencies
forge install OpenZeppelin/openzeppelin-contracts

# Set RPC
export ETH_RPC_URL=https://testnet-passet-hub-eth-rpc.polkadot.io
export PRIVATE_KEY=your_private_key

# Deploy
forge create src/MyContract.sol:MyContract \
  --rpc-url $ETH_RPC_URL \
  --private-key $PRIVATE_KEY

Using Hardhat

// hardhat.config.js
module.exports = {
  networks: {
    paseo: {
      url: "https://testnet-passet-hub-eth-rpc.polkadot.io",
      accounts: [process.env.PRIVATE_KEY]
    }
  }
};

// Deploy script
async function main() {
  const Contract = await ethers.getContractFactory("MyContract");
  const contract = await Contract.deploy();
  await contract.waitForDeployment();
  console.log("Deployed to:", await contract.getAddress());
}

Example: ZK Verifier Deployment

1. Generate Verifier with snarkJS

# Export Solidity verifier
snarkjs zkey export verifierkey circuit_final.zkey verifier.sol

# Or export Groth16 verifier
snarkjs zkey export solidityverifier circuit_final.zkey verifier.sol

2. Deploy Verifier

# Deploy to Asset Hub
forge create src/verifier.sol:Groth16Verifier \
  --rpc-url https://testnet-passet-hub-eth-rpc.polkadot.io \
  --private-key $PRIVATE_KEY

3. Deploy Application Contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import "./verifier.sol";

contract PolkadotDemo {
    IGroth16Verifier public immutable verifier;

    constructor() {
        verifier = IGroth16Verifier(0x00D124b363e7F278aEFF220398254EdE169D307c);
    }

    event DidSomething(bool myresult);

    function dosomething(
        uint256[2] calldata _pA,
        uint256[2][2] calldata _pB,
        uint256[2] calldata _pC,
        uint256[3] calldata _pubSignals
    ) external {
        require(
            verifier.verifyProof(_pA, _pB, _pC, _pubSignals),
            "Invalid proof"
        );
        emit DidSomething(true);
    }
}

Deploy:

forge create src/main.sol:PolkadotDemo \
  --rpc-url https://testnet-passet-hub-eth-rpc.polkadot.io \
  --private-key $PRIVATE_KEY

Testing Contracts

Using cast

# Call view function
cast call <CONTRACT_ADDRESS> \
  "myFunction(uint256):(uint256)" \
  "123" \
  --rpc-url https://testnet-passet-hub-eth-rpc.polkadot.io

# Send transaction
cast send <CONTRACT_ADDRESS> \
  "myFunction(uint256)" \
  "123" \
  --rpc-url https://testnet-passet-hub-eth-rpc.polkadot.io \
  --private-key $PRIVATE_KEY

Using Foundry Tests

// test/MyContract.t.sol
pragma solidity ^0.8.28;

import "forge-std/Test.sol";
import "../src/MyContract.sol";

contract MyContractTest is Test {
    MyContract public contract;

    function setUp() public {
        contract = new MyContract();
    }

    function testMyFunction() public {
        uint256 result = contract.myFunction(123);
        assertEq(result, 456);
    }
}

Run tests:

forge test

Asset Hub Specific Features

ERC20 Token Deployment

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyToken is ERC20 {
    constructor() ERC20("My Token", "MTK") {
        _mint(msg.sender, 1000000 * 10 ** decimals());
    }
}

Cross-Chain Messages (XCM)

Asset Hub supports XCM for cross-parachain communication:

// Example: Send tokens to another parachain
function sendToParachain(
    uint32 paraId,
    address recipient,
    uint256 amount
) external {
    // XCM integration would go here
    // This is a simplified example
}

Gas Costs

OperationGas CostPAS Cost (approx)
Transfer21,0000.001
ERC20 Transfer65,0000.003
Contract Deployment500,000+0.02+
ZK Verification100,000-300,0000.005-0.015

Best Practices

1. Use Testnet First

Always test on Paseo before deploying to mainnet.

2. Verify Contracts

Use Blockscout for source verification:

forge verify-contract \
  <CONTRACT_ADDRESS> \
  src/MyContract.sol:MyContract \
  --chain-id 420420422 \
  --etherscan-api-key <KEY>

3. Monitor Gas

# Check gas price
cast gas-price --rpc-url https://testnet-passet-hub-eth-rpc.polkadot.io

4. Handle Reverts

try contract.myFunction(value) returns (uint256 result) {
    // Success
} catch Error(string memory reason) {
    // Handle revert message
} catch (bytes memory) {
    // Handle low-level error
}

Troubleshooting

"Insufficient funds"

Ensure your wallet has PAS tokens for gas.

"Contract creation failed"

Check:

  • Constructor arguments are correct
  • Gas limit is sufficient
  • No revert in constructor

"Transaction reverted"

Use cast logs to see events and debug:

cast logs --rpc-url https://testnet-passet-hub-eth-rpc.polkadot.io \
  --from-block latest

Resources


Previous: Poseidon Hash | Next: Shielded Pools

Building Shielded Pools on Kusama

Shielded pools enable private asset transfers using zero-knowledge proofs. This guide walks through building a complete shielded pool on Kusama Asset Hub.

Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│                    Shielded Pool                             │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  Merkle Tree (LeanIMT)                                │   │
│  │  - Tracks all commitments                             │   │
│  │  - Enables membership proofs                          │   │
│  └──────────────────────────────────────────────────────┘   │
│                                                              │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │  Deposit    │  │  Withdraw   │  │  Transfer (UTXO)    │  │
│  │  - Add      │  │  - Spend    │  │  - Change output    │  │
│  │  commitment │  │  nullifier  │  │  commitment         │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
└─────────────────────────────────────────────────────────────┘
                           ▲
                           │
              ┌────────────┴────────────┐
              │   ZK Circuit (Circom)   │
              │   - Prove ownership     │
              │   - Valid merkle path   │
              │   - No double spend     │
              └─────────────────────────┘

Key Concepts

Commitments

A commitment hides the value and recipient:

commitment = Poseidon(value, asset, Poseidon(nullifier, secret))

Nullifiers

Prevent double-spending:

nullifierHash = Poseidon(nullifier, secret)

Merkle Tree

LeanIMT (Incremental Binary Merkle Tree):

  • Efficient insertion: O(log n)
  • Dynamic depth
  • Gas optimized

Circuit Implementation

withdraw.circom

pragma circom 2.0.0;

include "circomlib/poseidon.circom";
include "circomlib/merkle_tree.circom";

template Withdraw(treeDepth) {
    // Public inputs
    signal input root;              // Merkle root
    signal input withdrawnValue;    // Amount to withdraw
    signal input context;           // Replay protection (recipient, asset)
    
    // Private inputs
    signal input nullifier;
    signal input secret;
    signal input merkleProofPath[treeDepth];
    signal input merkleProofIndices[treeDepth];
    signal input newCommitmentSecret;
    
    // Outputs
    signal output newCommitmentHash;
    signal output existingNullifierHash;
    
    // Compute nullifier hash
    component nullifierHash = Poseidon(2);
    nullifierHash.inputs[0] <== nullifier;
    nullifierHash.inputs[1] <== secret;
    existingNullifierHash <== nullifierHash.out;
    
    // Verify Merkle membership
    component merkleCheck = MerkleTreeCheck(treeDepth);
    merkleCheck.f <== existingNullifierHash;
    merkleCheck.root <== root;
    for (var i = 0; i < treeDepth; i++) {
        merkleCheck.siblings[i] <== merkleProofPath[i];
        merkleCheck.index[i] <== merkleProofIndices[i];
    }
    
    // Create new commitment for change
    component newCommitment = Poseidon(2);
    newCommitment.inputs[0] <== withdrawnValue;
    newCommitment.inputs[1] <== newCommitmentSecret;
    newCommitmentHash <== newCommitment.out;
    
    // Context binding (prevent front-running)
    // context = keccak256(recipient, asset) mod p
    // This is computed off-chain and verified as public input
}

component main {public [root, withdrawnValue, context]} = Withdraw(20);

Circuit Constraints

  1. Merkle Proof: Proves commitment exists in tree
  2. Nullifier Hash: Unique per spend, prevents double-spend
  3. New Commitment: UTXO change for remaining value
  4. Context Binding: Binds proof to specific recipient/asset

Smart Contract Implementation

The complete shielded pool contract implementation is available in the Kusama Shield Shielded Pool Contract.

The contract implements:

  • Deposit functionality with Merkle tree insertion
  • Withdraw functionality with Groth16 proof verification
  • Nullifier tracking to prevent double-spending
  • LeanIMT (Incremental Merkle Tree) for efficient state management

Off-Chain Components

Commitment Generation (TypeScript)

import { poseidon } from 'circomlibjs';

function generateCommitment(
    value: bigint,
    asset: bigint,
    nullifier: bigint,
    secret: bigint
): bigint {
    const inner = poseidon([nullifier, secret]);
    const commitment = poseidon([value, poseidon([asset, inner])]);
    return commitment;
}

function generateNullifierHash(
    nullifier: bigint,
    secret: bigint
): bigint {
    return poseidon([nullifier, secret]);
}

Proof Generation

import { groth16 } from 'snarkjs';

async function generateWithdrawProof(
    nullifier: string,
    secret: string,
    merkleProof: string[],
    merkleIndices: number[],
    root: string,
    withdrawnValue: string,
    recipient: string,
    asset: string
) {
    // Compute context
    const context = computeContext(recipient, asset);
    
    // Generate witness
    const witness = await circuit.calculateWitness({
        nullifier,
        secret,
        merkleProofPath: merkleProof,
        merkleProofIndices: merkleIndices,
        root,
        withdrawnValue,
        context,
        newCommitmentSecret: generateRandomSecret()
    });
    
    // Generate proof
    const { proof, publicSignals } = await groth16.prove(
        'circuit_final.zkey',
        witness
    );
    
    return { proof, publicSignals };
}

function computeContext(recipient: string, asset: string): string {
    const hash = ethers.keccak256(
        ethers.solidityPacked(['address', 'address'], [recipient, asset])
    );
    return (BigInt(hash) % SNARK_SCALAR_FIELD).toString();
}

Merkle Tree Management

import { LeanIMT } from '@zk-kit/lean-imt';
import { poseidon } from 'circomlibjs';

class PoolTree {
    private tree: LeanIMT;
    
    constructor() {
        this.tree = new LeanIMT((a, b) => poseidon([a, b]));
    }
    
    insert(commitment: bigint) {
        this.tree.insert(commitment);
    }
    
    generateProof(commitment: bigint) {
        const { root, siblings, index } = this.tree.generateProof(commitment);
        return {
            root: root.toString(),
            siblings: siblings.map(s => s.toString()),
            indices: index.toString(2).padStart(20, '0').split('').map(Number)
        };
    }
    
    getRoot(): bigint {
        return this.tree.root;
    }
}

Deployment Steps

1. Deploy Verifier

# Generate verifier from circuit
snarkjs zkey export solidityverifier circuit_final.zkey verifier.sol

# Deploy to Asset Hub
forge create src/verifier.sol:Groth16Verifier \
  --rpc-url https://testnet-passet-hub-eth-rpc.polkadot.io \
  --private-key $PRIVATE_KEY

2. Deploy Pool Contract

# Deploy with verifier address
forge create src/shielded_pool.sol:FixedIlop \
  --constructor-args <VERIFIER_ADDRESS> \
  --rpc-url https://testnet-passet-hub-eth-rpc.polkadot.io \
  --private-key $PRIVATE_KEY

3. Fund Pool

// Deposit initial liquidity
const tx = await contract.deposit(
    TOKEN_ADDRESS,  // or address(0) for native
    ethers.parseEther('1000'),
    initialCommitment
);
await tx.wait();

Usage Flow

Deposit

// 1. Generate commitment
const commitment = generateCommitment(value, asset, nullifier, secret);

// 2. Call deposit
await contract.deposit(asset, amount, commitment);

// 3. Track commitment in local tree
poolTree.insert(commitment);

Withdraw

// 1. Find commitment in tree
const { root, siblings, indices } = poolTree.generateProof(commitment);

// 2. Generate proof
const { proof, publicSignals } = await generateWithdrawProof(
    nullifier, secret, siblings, indices, root, amount, recipient, asset
);

// 3. Call withdraw
await contract.withdraw(
    proof.a, proof.b, proof.c, publicSignals,
    asset, recipient
);

Security Considerations

1. Nullifier Management

  • Never reuse nullifiers
  • Store nullifiers securely
  • Use cryptographically random secrets

2. Merkle Root Validity

  • Accept historical roots (for UX)
  • Set expiration if needed
  • Monitor for invalid proofs

3. Front-Running Protection

  • Context binding prevents recipient changes
  • Consider commit-reveal for large withdrawals
  • Use private RPC endpoints

4. Escrow Safety

  • Reentrancy guards
  • Checks-effects-interactions pattern
  • Emergency withdrawal mechanism

Gas Optimization

ComponentGas CostOptimization
Deposit~150,000Batch inserts
Withdraw~300,000Efficient circuits
Merkle Insert~50,000LeanIMT structure

Testing

describe('ShieldedPool', () => {
    it('should deposit and withdraw privately', async () => {
        // Deposit
        const commitment = generateCommitment(...);
        await pool.deposit(asset, amount, commitment);
        
        // Generate proof
        const proof = await generateWithdrawProof(...);
        
        // Withdraw
        await pool.withdraw(..., recipient, asset);
        
        // Verify balance changed
        expect(await ethers.provider.getBalance(recipient))
            .to.equal(initialBalance + amount);
    });
});

Resources


Previous: Asset Hub Integration | Next: Circom Guide

Client-Side Proof Generation

This guide covers generating zero-knowledge proofs in the browser using Groth16 and Halo2, based on the Kusama Shield Interface implementation.

Overview

Client-side proof generation enables users to create ZK proofs locally without trusting a server. The Kusama Shield Interface demonstrates two approaches:

  1. Groth16 with snarkjs - Browser-based proving using WebAssembly
  2. Halo2 with Rust/WASM - High-performance proving using wasm-bindgen
┌─────────────────────────────────────────────────────────────┐
│                    User's Browser                            │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  Frontend (React/Vite)                                │   │
│  │    │                                                   │   │
│  │    ▼                                                   │   │
│  │  Proof Worker (Web Worker)                            │   │
│  │    │                                                   │   │
│  │    ├─ snarkjs (Groth16)                               │   │
│  │    │   - Load .wasm circuit                           │   │
│  │    │   - Load .zkey proving key                       │   │
│  │    │   - Generate witness                             │   │
│  │    │   - Create Groth16 proof                         │   │
│  │    │                                                   │   │
│  │    └─ Rust WASM (Halo2)                               │   │
│  │        - Load Halo2 params                            │   │
│  │        - Generate proving key                         │   │
│  │        - Create Halo2 proof                           │   │
│  └──────────────────────────────────────────────────────┘   │
│                           │                                   │
│                           ▼                                   │
│              Submit to Smart Contract                         │
└─────────────────────────────────────────────────────────────┘

Groth16 with snarkjs

Setup

# Install snarkjs
npm install snarkjs

# Install circomlib for Poseidon
npm install circomlibjs

Basic Proof Generation

import * as snarkjs from 'snarkjs';

// Generate commitment proof
export async function generateCommitment(
    secret: string,
    asset: string,
    amount: string,
    leafIndex: string,
    siblings: string[]
) {
    const inputs = {
        secret,
        asset,
        amount,
        leafIndex,
        siblings,
        recipient: "0" // Public parameter
    };

    // Generate proof with full proving (witness + proof)
    const { proof, publicSignals } = await snarkjs.groth16.fullProve(
        inputs,
        "asset.wasm",      // Circuit WASM file
        "asset_0001.zkey"  // Proving key
    );

    // Export Solidity call data
    const calldata = await snarkjs.groth16.exportSolidityCallData(
        proof,
        publicSignals
    );

    return {
        proof,
        publicSignals,
        calldata: JSON.parse("[" + calldata + "]")
    };
}

Format for Ethereum/PVM

function toEthHex(input: string): string {
    return "0x" + BigInt(input).toString(16);
}

// Format proof for Solidity contract
const formattedProof = [
    // pi_a
    [toEthHex(proof.pi_a[0]), toEthHex(proof.pi_a[1])],
    // pi_b (flattened 2x2 matrix)
    [
        [toEthHex(proof.pi_b[0][0]), toEthHex(proof.pi_b[0][1])],
        [toEthHex(proof.pi_b[1][0]), toEthHex(proof.pi_b[1][1])]
    ],
    // pi_c
    [toEthHex(proof.pi_c[0]), toEthHex(proof.pi_c[1])],
    // public signals
    publicSignals.map(signal => toEthHex(signal))
];

Withdrawal Proof

export async function generateWithdrawProof({
    secret,
    asset,
    amount,
    recipient,
    leafIndex,
    siblings,
}: {
    secret: string;
    asset: string;
    amount: string;
    recipient: string;
    leafIndex: string;
    siblings: string[];
}) {
    if (siblings.length !== 256) {
        throw new Error("Siblings array must have exactly 256 elements");
    }

    const input = {
        secret,
        asset,
        amount,
        recipient,
        leafIndex,
        siblings,
    };

    const { proof, publicSignals } = await snarkjs.groth16.fullProve(
        input,
        "main.wasm",
        "main_0000.zkey"
    );

    const calldata = await snarkjs.groth16.exportSolidityCallData(
        proof,
        publicSignals
    );

    return {
        proof: formattedProof,
        calldata: JSON.parse("[" + calldata + "]"),
        publicSignals,
    };
}

Merkle Tree Proof Generation

import { LeanIMT } from '@zk-kit/lean-imt';
import { poseidon } from 'circomlibjs';

class ProofGenerator {
    private tree: LeanIMT;

    constructor() {
        // Initialize LeanIMT with Poseidon hash
        this.tree = new LeanIMT((a, b) => poseidon([a, b]));
    }

    // Insert commitment into tree
    insertCommitment(commitment: bigint) {
        this.tree.insert(commitment);
    }

    // Generate Merkle proof for withdrawal
    async generateMerkleProof(commitment: bigint) {
        const { root, siblings, index } = this.tree.generateProof(commitment);

        return {
            root: root.toString(),
            siblings: siblings.map(s => s.toString()),
            indices: index.toString(2).padStart(20, '0').split('').map(Number),
            leafIndex: index.toString()
        };
    }

    // Generate full withdrawal proof
    async generateWithdrawalProof(
        nullifier: bigint,
        secret: bigint,
        commitment: bigint,
        amount: bigint,
        recipient: string,
        asset: string
    ) {
        // Get Merkle proof
        const merkleProof = await this.generateMerkleProof(commitment);

        // Compute context (replay protection)
        const context = this.computeContext(recipient, asset);

        // Generate ZK proof
        const zkProof = await generateWithdrawProof({
            secret: secret.toString(),
            asset: asset,
            amount: amount.toString(),
            recipient: context,
            leafIndex: merkleProof.leafIndex,
            siblings: merkleProof.siblings,
        });

        return {
            merkleProof,
            zkProof,
            publicSignals: zkProof.publicSignals
        };
    }

    private computeContext(recipient: string, asset: string): string {
        const hash = ethers.keccak256(
            ethers.solidityPacked(['address', 'address'], [recipient, asset])
        );
        return (BigInt(hash) % SNARK_SCALAR_FIELD).toString();
    }
}

Halo2 with Rust/WASM

Project Structure

Interface/
├── src/
│   ├── lib.rs          # WASM bindings
│   ├── zk.rs           # Halo2 circuit implementation
│   └── proof-worker.js # Web Worker for proving
├── public/
│   └── pkg/            # Compiled WASM package
└── package.json

Rust Circuit Implementation

#![allow(unused)]
fn main() {
// src/zk.rs
use halo2_proofs::{
    circuit::{Layouter, SimpleFloorPlanner, Value},
    plonk::{Advice, Column, ConstraintSystem, Error, Circuit},
    poly::kzg::commitment::ParamsKZG,
    halo2curves::bn256::{Bn256, Fr as Fp, G1Affine},
};
use halo2_poseidon::poseidon::{
    primitives::{ConstantLength, Hash},
    Hash as PoseidonHash,
};

// Poseidon specification for BN254
#[derive(Debug, Clone, Copy)]
pub struct Posbn254<const WIDTH: usize, const RATE: usize>;

impl<const WIDTH: usize, const RATE: usize> Spec<Fp, WIDTH, RATE> 
    for Posbn254<WIDTH, RATE> 
{
    fn full_rounds() -> usize { 8 }
    fn partial_rounds() -> usize { 56 }
    fn sbox(val: Fp) -> Fp { val.pow_vartime(&[5]) }
    fn secure_mds() -> usize { 0 }
    fn constants() -> (Vec<[Fp; WIDTH]>, Mds<Fp, WIDTH>, Mds<Fp, WIDTH>) {
        generate_constants::<_, Self, WIDTH, RATE>()
    }
}

// Hash circuit
#[derive(Clone, Copy, Default)]
pub struct HashCircuit<S, const WIDTH: usize, const RATE: usize, const L: usize>
where
    S: Spec<Fp, WIDTH, RATE> + Clone + Copy,
{
    message: Value<[Fp; L]>,
    _spec: PhantomData<S>,
}

impl<S, const WIDTH: usize, const RATE: usize, const L: usize> Circuit<Fp>
    for HashCircuit<S, WIDTH, RATE, L>
where
    S: Spec<Fp, WIDTH, RATE> + Copy + Clone,
{
    type Config = MyConfig<WIDTH, RATE, L>;
    type FloorPlanner = SimpleFloorPlanner;

    fn without_witnesses(&self) -> Self {
        Self {
            message: Value::unknown(),
            _spec: PhantomData,
        }
    }

    fn configure(meta: &mut ConstraintSystem<Fp>) -> Self::Config {
        // Configure Poseidon chip
        let state = (0..WIDTH).map(|_| meta.advice_column()).collect::<Vec<_>>();
        let expected = meta.instance_column();
        meta.enable_equality(expected);
        
        MyConfig {
            input: state[..RATE].try_into().unwrap(),
            expected,
            poseidon_config: Pow5Chip::configure::<S>(
                meta,
                state.try_into().unwrap(),
                partial_sbox,
                rc_a.try_into().unwrap(),
                rc_b.try_into().unwrap(),
            ),
        }
    }

    fn synthesize(
        &self,
        config: Self::Config,
        mut layouter: impl Layouter<Fp>,
    ) -> Result<(), Error> {
        // Assign message and compute hash
        let chip = Pow5Chip::construct(config.poseidon_config.clone());
        
        let message = layouter.assign_region(
            || "load message",
            |mut region| {
                // Assign input values
                // ...
            },
        )?;

        let hasher = Hash::<_, _, S, ConstantLength<L>, WIDTH, RATE>::init(
            chip,
            layouter.namespace(|| "init"),
        )?;
        
        let output = hasher.hash(
            layouter.namespace(|| "hash"), 
            message
        )?;

        // Constrain output to public instance
        layouter.constrain_instance(output.cell(), config.expected, 0)
    }
}
}

WASM Bindings

#![allow(unused)]
fn main() {
// src/lib.rs
use wasm_bindgen::prelude::*;
use halo2_proofs::halo2curves::bn256::Fr as Fp;
use halo2_proofs::poly::kzg::commitment::ParamsKZG;
use std::io::BufReader;

#[wasm_bindgen]
pub fn generate_commitment(secret: &str) -> Result<String, JsError> {
    // Convert secret to field element
    let m2 = Fp::from_str_vartime(secret)
        .ok_or_else(|| JsError::new("Invalid secret - must be numeric"))?;

    // Create message
    let msg = [m2, m2];

    // Compute Poseidon hash
    let output = Hash::<_, Posbn254<3, 2>, ConstantLength<2>, 3, 2>::init()
        .hash(msg);
    
    Ok(format!("{:?}", output))
}

#[wasm_bindgen]
pub async fn generate_proof_data(
    secret: &str,
    parambytes: JsValue
) -> Result<String, JsError> {
    // Parse secret
    let m2 = Fp::from_str_vartime(secret)
        .ok_or_else(|| JsError::new("Invalid secret"))?;
    
    let msg = [m2, m2];
    
    // Create circuit
    const K: u32 = 8;
    const L: usize = 2;
    const WIDTH: usize = 3;
    const RATE: usize = 2;
    
    let circuit = HashCircuit::<Posbn254<WIDTH, RATE>, WIDTH, RATE, L>::new(msg);
    
    // Load params from bytes
    let params_vec = Uint8Array::new(&parambytes).to_vec();
    let params = ParamsKZG::<Bn256>::read(
        &mut BufReader::new(&params_vec[..])
    )?;
    
    // Generate proving and verifying keys
    let (pk, vk) = generate_keys(&params, &circuit)?;
    
    // Compute public output
    let output = PoseidonHash::<_, Posbn254<WIDTH, RATE>, ConstantLength<L>, WIDTH, RATE>::init()
        .hash(msg);
    
    // Generate proof
    let proof = generate_proof2(
        &params, 
        &pk, 
        circuit, 
        vec![vec![output]]
    )?;
    
    Ok(proof) // Hex-encoded proof
}
}

Web Worker Integration

// src/proof-worker.js
import { initThreadPool, generate_proof_data } from '../pkg/generate_zk_wasm';

// Initialize worker with thread pool
async function init() {
    const threads = navigator.hardwareConcurrency;
    console.log(`Initializing ${threads} threads`);
    
    await initThreadPool(threads);
    
    return {
        generate_proof_data
    };
}

const worker = { init };
export default worker;

React Integration

// src/components/ProofGenerator.tsx
import { useEffect, useState } from 'react';
import Comlink from 'comlink';

interface ProofWorker {
    init: () => Promise<{
        generate_proof_data: (
            secret: string, 
            params: Uint8Array
        ) => Promise<string>;
    }>;
}

export function ProofGenerator() {
    const [worker, setWorker] = useState<any>(null);
    const [generating, setGenerating] = useState(false);
    const [proof, setProof] = useState<string | null>(null);

    useEffect(() => {
        // Initialize worker on mount
        async function initWorker() {
            const proofWorker = new Worker(
                new URL('../src/proof-worker.js', import.meta.url)
            );
            const wrapped = Comlink.wrap<ProofWorker>(proofWorker);
            const api = await wrapped.init();
            setWorker(api);
        }
        
        initWorker();
    }, []);

    const handleGenerateProof = async (secret: string) => {
        if (!worker) return;
        
        setGenerating(true);
        try {
            // Fetch Halo2 params
            const response = await fetch(
                'https://trusted-setup-halo2kzg.s3.eu-central-1.amazonaws.com/hermez-raw-11'
            );
            const paramsBuffer = await response.arrayBuffer();
            const params = new Uint8Array(paramsBuffer);
            
            // Generate proof in worker
            const proofResult = await worker.generate_proof_data(
                secret,
                params
            );
            
            setProof(proofResult);
        } catch (error) {
            console.error('Proof generation failed:', error);
        } finally {
            setGenerating(false);
        }
    };

    return (
        <div>
            <button 
                onClick={() => handleGenerateProof('12345')}
                disabled={generating || !worker}
            >
                {generating ? 'Generating...' : 'Generate Proof'}
            </button>
            {proof && <div>Proof: {proof}</div>}
        </div>
    );
}

Performance Optimization

Multi-threaded Proving

// Use wasm-bindgen-rayon for multi-threading
import { initThreadPool } from './pkg';

// Initialize with hardware concurrency
await initThreadPool(navigator.hardwareConcurrency);

// Proving now uses all available cores
const proof = await generate_proof_data(secret, params);

Memory Management

#![allow(unused)]
fn main() {
// src/lib.rs - Check and grow WASM memory
#[wasm_bindgen]
pub fn generate_proof_data(secret: &str, parambytes: JsValue) 
    -> Result<String, JsError> 
{
    let memory = wasm_bindgen::memory()
        .dyn_into::<WebAssembly::Memory>()
        .unwrap();
    
    // Check current memory allocation
    let current_pages = memory.grow(0);
    if current_pages < 600 {  // If less than ~28MB
        memory.grow(64);  // Grow by 64 pages (4MB)
    }
    
    // Generate proof with panic recovery
    let proof = match std::panic::catch_unwind(|| {
        generate_proof2(&params, &pk, circuit, public_inputs)
    }) {
        Ok(Ok(p)) => p,
        Ok(Err(e)) => return Err(JsError::new(&format!("Proof error: {}", e))),
        Err(_) => return Err(JsError::new("Proof generation panicked")),
    };
    
    Ok(proof)
}
}

Witness Caching

// Cache witness for repeated proofs
class WitnessCache {
    private cache = new Map<string, any>();

    async getOrCompute(
        inputs: any,
        circuitPath: string,
        zkeyPath: string
    ) {
        const key = JSON.stringify(inputs);
        
        if (this.cache.has(key)) {
            return this.cache.get(key);
        }
        
        const witness = await snarkjs.witness_calculator(
            await fetch(circuitPath).then(r => r.arrayBuffer())
        ).calculateWitness(inputs);
        
        this.cache.set(key, witness);
        return witness;
    }
}

Submitting Proofs to Contract

Using ethers.js

import { ethers } from 'ethers';

async function submitWithdrawal(
    provider: ethers.Provider,
    signer: ethers.Signer,
    poolAddress: string,
    proof: any
) {
    const pool = new ethers.Contract(
        poolAddress,
        SHIELDED_POOL_ABI,
        signer
    );

    // Format proof for contract
    const formattedProof = {
        a: [proof.proof[0][0], proof.proof[0][1]],
        b: [
            [proof.proof[1][0][0], proof.proof[1][0][1]],
            [proof.proof[1][1][0], proof.proof[1][1][1]]
        ],
        c: [proof.proof[2][0], proof.proof[2][1]],
    };

    // Submit withdrawal transaction
    const tx = await pool.withdraw(
        formattedProof.a,
        formattedProof.b,
        formattedProof.c,
        proof.publicSignals,
        assetAddress,
        recipientAddress
    );

    await tx.wait();
    console.log('Withdrawal submitted:', tx.hash);
}

Using polkadot.js

import { ApiPromise } from '@polkadot/api';

async function submitToPvm(
    api: ApiPromise,
    signer: any,
    contractAddress: string,
    proof: any
) {
    // Encode proof data
    const callData = encodeProofData(proof);

    // Submit via contracts.call extrinsic
    const tx = api.tx.contracts.call(
        contractAddress,
        0, // value
        1000000000000, // gasLimit
        null, // storageDepositLimit
        callData
    );

    await tx.signAndSend(signer);
}

Debugging

Browser Console Logging

// Enable detailed logging
snarkjs.logger = console.log.bind(console);

// Log proof generation steps
console.log('Starting proof generation...');
console.log('Inputs:', inputs);
console.log('Witness calculated');
console.log('Proof generated:', proof);

Error Handling

try {
    const { proof, publicSignals } = await snarkjs.groth16.fullProve(
        inputs,
        circuitWasm,
        zkey
    );
} catch (error) {
    if (error.message.includes('Constraint not satisfied')) {
        console.error('Circuit constraint failed - check inputs');
    } else if (error.message.includes('Memory limit')) {
        console.error('WASM memory exhausted - reduce circuit size');
    } else {
        console.error('Proof generation failed:', error);
    }
}

Resources


Previous: Shielded Pools | Next: Circom Guide

Circom Circuits Guide

Circom is a domain-specific language for designing arithmetic circuits used in zero-knowledge proofs. This guide covers writing circuits for Kusama ZK applications.

What is Circom?

Circom (Circuit Compiler) is a Rust-based language for:

  • Arithmetic Circuits: Define constraints over finite fields
  • ZK Proof Generation: Compatible with Groth16, PLONK, FFLONK
  • Witness Calculation: Generate proof inputs

Installation

# Install circom (version 2.1.6+)
git clone https://github.com/iden3/circom.git
cd circom
cargo build --release
cargo install --path circom

# Verify installation
circom --version

Basic Circuit Structure

Simple Addition Circuit

pragma circom 2.0.0;

template Adder() {
    // Input signals
    signal input a;
    signal input b;
    
    // Output signal
    signal output out;
    
    // Constraint
    out <== a + b;
}

// Main component with public inputs
component main {public [a, b]} = Adder();

Compile the Circuit

# Generate R1CS constraint system
circom circuit.circom --r1cs --wasm --sym

# Output files:
# - circuit.r1cs  (constraint system)
# - circuit.wasm  (witness calculator)
# - circuit.sym   (symbol table)

Signals and Constraints

Signal Types

// Input signal (private by default)
signal input x;

// Output signal
signal output y;

// Intermediate signal
signal temp;

// Public input (specified at compile time)
component main {public [x]} = MyCircuit();

// Array of signals
signal inputs[3];

// Multi-dimensional arrays
signal matrix[2][2];

Constraint Operators

// Assignment with constraint (<==)
out <== a * b;

// Constraint only (===)
a * b === c;

// Non-linear constraint (must use <==)
out <== a * b + c;

// Linear constraint (can use ===)
a + b === c;

Constraint Rules

Valid:

out <== a * b;           // Non-linear assignment
a + b === c;             // Linear constraint
out <== a + b + c;       // Linear assignment

Invalid:

a * b === c * d;         // Non-linear constraint (not assignment)
out === a * b;           // Must use <== for non-linear

Templates and Components

Template with Parameters

pragma circom 2.0.0;

template Multiplier(n) {
    signal input in[n];
    signal output out;
    
    signal products[n];
    
    for (var i = 0; i < n; i++) {
        products[i] <== in[i] * 2;
    }
    
    out <== products[0];
    for (var i = 1; i < n; i++) {
        out <== out + products[i];
    }
}

component main = Multiplier(5);

Component Instantiation

template Inner() {
    signal input x;
    signal output y;
    y <== x * x;
}

template Outer() {
    signal input a;
    signal output b;
    
    component sq = Inner();
    sq.x <== a;
    b <== sq.y;
}

Using circomlib

Poseidon Hash

pragma circom 2.0.0;

include "circomlib/poseidon.circom";

template PoseidonExample() {
    signal input a;
    signal input b;
    signal output out;
    
    component poseidon = Poseidon(2);  // 2 inputs
    poseidon.inputs[0] <== a;
    poseidon.inputs[1] <== b;
    out <== poseidon.out;
}

component main {public [a, b]} = PoseidonExample();

Merkle Tree Verification

pragma circom 2.0.0;

include "circomlib/merkle_tree.circom";

template MerkleVerifier(depth) {
    signal input leaf;
    signal input root;
    signal input siblings[depth];
    signal input indices[depth];  // 0 or 1 for each level
    
    component verifier = MerkleTreeCheck(depth);
    verifier.f <== leaf;
    verifier.root <== root;
    
    for (var i = 0; i < depth; i++) {
        verifier.siblings[i] <== siblings[i];
        verifier.index[i] <== indices[i];
    }
}

component main {public [root]} = MerkleVerifier(20);

SHA-256 (for non-ZK-friendly hashing)

include "circomlib/sha256.circom";

template ShaExample() {
    signal input data[16];  // 512 bits
    signal output hash[256];
    
    component sha = Sha256(16);
    for (var i = 0; i < 16; i++) {
        sha.data[i] <== data[i];
    }
    
    for (var i = 0; i < 256; i++) {
        hash[i] <== sha.out[i];
    }
}

Advanced Patterns

Bit Decomposition

include "circomlib/mimc.circom";
include "circomlib/bitify.circom";

template BitsExample() {
    signal input value;
    signal output bits[32];
    
    // Decompose to bits
    component decomposer = Num2Bits(32);
    decomposer.in <== value;
    
    for (var i = 0; i < 32; i++) {
        bits[i] <== decomposer.out[i];
    }
}

Less Than Comparison

include "circomlib/comparators.circom";

template LessThanExample() {
    signal input a;
    signal input b;
    signal output lt;  // 1 if a < b, 0 otherwise
    
    component ltcmp = LessThan(256);
    ltcmp.in[0] <== a;
    ltcmp.in[1] <== b;
    lt <== ltcmp.out;
}

Multiplexer (Conditional Selection)

template Multiplexer() {
    signal input condition;  // 0 or 1
    signal input a;
    signal input b;
    signal output out;
    
    // out = condition ? a : b
    out <== condition * a + (1 - condition) * b;
}

Complete Example: Private Withdrawal

pragma circom 2.0.0;

include "circomlib/poseidon.circom";
include "circomlib/merkle_tree.circom";

/**
 * Withdraw circuit for shielded pool
 * 
 * Proves:
 * 1. Knowledge of a leaf in the Merkle tree
 * 2. Correct nullifier hash computation
 * 3. Valid withdrawal amount
 */
template Withdraw(treeDepth) {
    // ─── Public Inputs ─────────────────────────────────────
    signal input root;              // Current Merkle root
    signal input withdrawnValue;    // Amount being withdrawn
    signal input context;           // Replay protection hash
    
    // ─── Private Inputs ────────────────────────────────────
    signal input nullifier;         // Unique spend identifier
    signal input secret;            // User's secret key
    signal input merkleProofPath[treeDepth];
    signal input merkleProofIndices[treeDepth];
    signal input newCommitmentSecret;
    
    // ─── Outputs ───────────────────────────────────────────
    signal output newCommitmentHash;
    signal output existingNullifierHash;
    
    // ─── Compute Nullifier Hash ────────────────────────────
    component nullifierHash = Poseidon(2);
    nullifierHash.inputs[0] <== nullifier;
    nullifierHash.inputs[1] <== secret;
    existingNullifierHash <== nullifierHash.out;
    
    // ─── Verify Merkle Membership ──────────────────────────
    component merkleCheck = MerkleTreeCheck(treeDepth);
    merkleCheck.f <== existingNullifierHash;
    merkleCheck.root <== root;
    
    for (var i = 0; i < treeDepth; i++) {
        merkleCheck.siblings[i] <== merkleProofPath[i];
        merkleCheck.index[i] <== merkleProofIndices[i];
    }
    
    // ─── Validate Withdrawal Amount ────────────────────────
    // Ensure value is positive and in range
    component valueBits = Num2Bits(64);
    valueBits.in <== withdrawnValue;
    
    // ─── Create New Commitment (UTXO Change) ───────────────
    component newCommitment = Poseidon(2);
    newCommitment.inputs[0] <== withdrawnValue;
    newCommitment.inputs[1] <== newCommitmentSecret;
    newCommitmentHash <== newCommitment.out;
    
    // ─── Context Binding ───────────────────────────────────
    // Context is verified in the smart contract:
    // context == keccak256(recipient, asset) mod p
}

component main {public [root, withdrawnValue, context]} = Withdraw(20);

Witness Generation

Generate Witness

# Compile circuit
circom withdraw.circom --r1cs --wasm --sym

# Create input file (input.json)
cat > input.json << EOF
{
    "nullifier": "123456789",
    "secret": "987654321",
    "merkleProofPath": ["...", "..."],
    "merkleProofIndices": [0, 1, 0, ...],
    "root": "111222333",
    "withdrawnValue": "100",
    "context": "444555666",
    "newCommitmentSecret": "555666777"
}
EOF

# Generate witness
node withdraw_js/generate_witness.js \
    withdraw_js/witness.wasm \
    input.json \
    witness.wtns

Trusted Setup

# Start powers of tau ceremony
snarkjs powersoftau new bn128 14 pot14_0000.ptau -v

# Contribute to ceremony
snarkjs powersoftau contribute pot14_0000.ptau pot14_0001.ptau \
    --name="First contribution" -v

# Apply circuit-specific phase
snarkjs powersoftau apply-file pot14_0001.ptau \
    circuit_final.ptau -v

# Generate zkey
snarkjs groth16 setup circuit.r1cs pot14_0001.ptau circuit_0000.zkey

# Contribute to zkey
snarkjs zkey contribute circuit_0000.zkey circuit_0001.zkey \
    -name="Contribution 1" -v

# Export verification key
snarkjs zkey export verificationkey circuit_0001.zkey vkey.json

# Export Solidity verifier
snarkjs zkey export solidityverifier circuit_0001.zkey verifier.sol

Generate Proof

snarkjs groth16 prove circuit_final.zkey witness.wtns proof.json public.json

# Verify proof
snarkjs groth16 verify vkey.json public.json proof.json

Debugging

Enable Verbose Output

circom circuit.circom --r1cs --wasm --sym --verbose

Check Constraints

# Show constraint statistics
circom circuit.circom --r1cs --json

# Analyze with snarkjs
snarkjs r1cs info circuit.r1cs

Common Errors

"Signal not connected":

// Wrong
signal out;

// Right
signal output out;
out <== expression;

"Non-quadratic constraint":

// Wrong
a * b * c === d;

// Right
signal temp;
temp <== a * b;
temp * c === d;

"Negative exponent":

// Wrong - Circom doesn't support division directly
out <== a / b;

// Right - Use modular inverse or redesign

Best Practices

1. Minimize Constraints

// Inefficient
signal temp1, temp2, temp3;
temp1 <== a * b;
temp2 <== temp1 * c;
temp3 <== temp2 * d;
out <== temp3;

// Efficient
out <== a * b * c * d;

2. Use Appropriate Bit Lengths

// For amounts (up to 2^64)
component bits = Num2Bits(64);

// For boolean (0 or 1)
component bool = Num2Bits(1);

3. Validate Inputs

// Ensure boolean is 0 or 1
component boolCheck = IsZero();
boolCheck.in <== condition * (1 - condition);

// Ensure value is in range
component rangeCheck = Num2Bits(64);
rangeCheck.in <== value;

4. Document Public Inputs

// Clearly mark public vs private
component main {public [root, amount]} = MyCircuit();
// root: Merkle root (public)
// amount: Withdrawal amount (public)
// Other inputs are private

Resources

Learning & Tutorials

Libraries & Tools

  • circomlib - Standard circuit library (Poseidon, Merkle trees, etc.)
  • circomlibjs - JavaScript bindings for witness generation

Examples


Previous: Shielded Pools | Next: Deployment Guide

Deployment Guide

This guide covers deploying zero-knowledge applications to Kusama's testnet and mainnet environments.

Prerequisites

Required Tools

# Rust and Cargo
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Node.js (v18+)
nvm install 18

# Foundry (for Solidity)
curl -L https://foundry.paradigm.xyz | bash
foundryup

# snarkJS
npm install -g snarkjs

# Circom
git clone https://github.com/iden3/circom.git
cd circom && cargo build --release
sudo cp target/release/circom /usr/local/bin/

# Ethers.js / Hardhat
npm install -g hardhat @nomicfoundation/hardhat-toolbox

Wallet Setup

# Create wallet with cast
cast wallet new

# Save private key securely
export PRIVATE_KEY="your_private_key_here"

# Fund wallet from faucet
# Visit: https://faucet.polkadot.io/?parachain=1111

Testnet Deployment

Paseo Asset Hub Configuration

ParameterValue
RPC URLhttps://testnet-passet-hub-eth-rpc.polkadot.io
Chain ID420420422
Explorerhttps://blockscout-passet-hub.parity-testnet.parity.io/
Faucethttps://faucet.polkadot.io/?parachain=1111

Step 1: Deploy Circuit Verifier

# Generate verifier from circuit
snarkjs zkey export solidityverifier circuit_final.zkey verifier.sol

# Deploy using Foundry
forge create src/verifier.sol:Groth16Verifier \
  --rpc-url https://testnet-passet-hub-eth-rpc.polkadot.io \
  --private-key $PRIVATE_KEY

# Save the deployed address
export VERIFIER_ADDRESS="0x..."

Step 2: Deploy Application Contract

// src/shielded_pool.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {FixedIlop} from "./FixedIlop.sol";

// Deploy this contract
forge create src/shielded_pool.sol:FixedIlop \
  --constructor-args $VERIFIER_ADDRESS \
  --rpc-url https://testnet-passet-hub-eth-rpc.polkadot.io \
  --private-key $PRIVATE_KEY

export POOL_ADDRESS="0x..."

Step 3: Verify Contract Source

# Get API key from Blockscout
export BLOCKSCOUT_API_KEY="your_api_key"

forge verify-contract \
  $POOL_ADDRESS \
  src/shielded_pool.sol:FixedIlop \
  --chain-id 420420422 \
  --verifier-url https://blockscout-passet-hub.parity-testnet.parity.io/api \
  --etherscan-api-key $BLOCKSCOUT_API_KEY \
  --constructor-args $(cast abi-encode "constructor(address)" $VERIFIER_ADDRESS)

Step 4: Deploy Poseidon Library (if needed)

# For projects using external Poseidon
forge create src/Poseidon.sol:Poseidon \
  --rpc-url https://testnet-passet-hub-eth-rpc.polkadot.io \
  --private-key $PRIVATE_KEY

export POSEIDON_ADDRESS="0x..."

Mainnet Deployment

Kusama Asset Hub

ParameterValue
RPC URLhttps://kusama-asset-hub-rpc.polkadot.io
Chain ID(Check current value)
Explorerhttps://blockscout.kusama.network/

Pre-Mainnet Checklist

  • All contracts tested on testnet
  • Security audit completed
  • Emergency pause mechanism implemented
  • Time-lock for upgrades
  • Monitoring and alerting setup
  • Documentation complete

Deploy to Mainnet

# Update RPC URL
export KUSAMA_RPC_URL="https://kusama-asset-hub-rpc.polkadot.io"

# Deploy with same process as testnet
forge create src/verifier.sol:Groth16Verifier \
  --rpc-url $KUSAMA_RPC_URL \
  --private-key $PRIVATE_KEY

# Record all addresses in deployment log

Hardhat Deployment

Configuration

// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");

module.exports = {
  solidity: "0.8.28",
  networks: {
    paseo: {
      url: "https://testnet-passet-hub-eth-rpc.polkadot.io",
      accounts: [process.env.PRIVATE_KEY],
      chainId: 420420422
    },
    kusama: {
      url: "https://kusama-asset-hub-rpc.polkadot.io",
      accounts: [process.env.PRIVATE_KEY],
      chainId: 420420 // Verify actual chain ID
    }
  },
  etherscan: {
    apiKey: {
      paseo: process.env.BLOCKSCOUT_API_KEY
    },
    customChains: [
      {
        network: "paseo",
        chainId: 420420422,
        urls: {
          apiURL: "https://blockscout-passet-hub.parity-testnet.parity.io/api",
          browserURL: "https://blockscout-passet-hub.parity-testnet.parity.io/"
        }
      }
    ]
  }
};

Deployment Script

// scripts/deploy.js
async function main() {
  const [deployer] = await ethers.getSigners();
  console.log("Deploying with:", deployer.address);

  // Deploy verifier
  const Verifier = await ethers.getContractFactory("Groth16Verifier");
  const verifier = await Verifier.deploy();
  await verifier.waitForDeployment();
  console.log("Verifier:", await verifier.getAddress());

  // Deploy pool
  const Pool = await ethers.getContractFactory("FixedIlop");
  const pool = await Pool.deploy(await verifier.getAddress());
  await pool.waitForDeployment();
  console.log("Pool:", await pool.getAddress());

  // Save addresses
  const fs = require('fs');
  fs.writeFileSync('deployment.json', JSON.stringify({
    network: network.name,
    verifier: await verifier.getAddress(),
    pool: await pool.getAddress(),
    deployer: deployer.address,
    timestamp: new Date().toISOString()
  }, null, 2));
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Run Deployment

npx hardhat run scripts/deploy.js --network paseo

Post-Deployment

Fund the Pool

// scripts/fund-pool.js
async function main() {
  const pool = await ethers.getContractAt("FixedIlop", POOL_ADDRESS);
  
  // Deposit initial liquidity
  const tx = await pool.deposit(
    TOKEN_ADDRESS,  // or address(0) for native
    ethers.parseEther("1000"),
    initialCommitment
  );
  await tx.wait();
  
  console.log("Pool funded!");
}

Register on Block Explorer

  1. Visit Blockscout explorer
  2. Navigate to your contract
  3. Click "Verify & Publish"
  4. Submit source code and settings

Set Up Monitoring

# Monitor contract events
cast logs \
  --rpc-url https://testnet-passet-hub-eth-rpc.polkadot.io \
  --address $POOL_ADDRESS \
  --from-block latest \
  --follow

CI/CD Integration

GitHub Actions Example

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Install Foundry
        uses: foundry-rs/foundry-toolchain@v1
      
      - name: Install Node
        uses: actions/setup-node@v4
        with:
          node-version: 18
      
      - name: Install dependencies
        run: npm ci
      
      - name: Deploy to Paseo
        env:
          PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }}
        run: |
          forge create src/verifier.sol:Groth16Verifier \
            --rpc-url https://testnet-passet-hub-eth-rpc.polkadot.io \
            --private-key $PRIVATE_KEY

Troubleshooting

"Insufficient funds"

# Check balance
cast balance $YOUR_ADDRESS \
  --rpc-url https://testnet-passet-hub-eth-rpc.polkadot.io

# Get funds from faucet
# https://faucet.polkadot.io/?parachain=1111

"Gas estimation failed"

# Try with explicit gas limit
forge create Contract \
  --rpc-url $RPC \
  --private-key $KEY \
  --gas-limit 5000000

"Contract creation failed"

# Check constructor arguments
cast abi-encode "constructor(address)" $VERIFIER_ADDRESS

# Verify contract compiles
forge build

"Transaction reverted"

# Use cast to trace
cast trace <TX_HASH> \
  --rpc-url https://testnet-passet-hub-eth-rpc.polkadot.io

# Check logs
cast logs --rpc-url $RPC --from-block <BLOCK>

Security Best Practices

1. Use Multi-Sig for Production

// Deploy with Gnosis Safe as owner
constructor(address _safeAddress) {
    owner = _safeAddress;
}

2. Implement Emergency Pause

bool public paused = false;

function pause() external onlyOwner {
    paused = true;
}

function unpause() external onlyOwner {
    paused = false;
}

modifier notPaused() {
    require(!paused, "Contract paused");
    _;
}

3. Time-Lock Upgrades

uint256 public constant TIMELOCK = 2 days;
mapping(bytes32 => uint256) public proposals;

function propose(bytes32 id) external onlyOwner {
    proposals[id] = block.timestamp + TIMELOCK;
}

function execute(bytes32 id) external onlyOwner {
    require(proposals[id] != 0, "Not proposed");
    require(block.timestamp >= proposals[id], "Timelock active");
    // Execute upgrade
}

4. Rate Limiting

mapping(address => uint256) public lastWithdraw;
uint256 public constant COOLDOWN = 1 hours;

function withdraw(...) external {
    require(
        block.timestamp >= lastWithdraw[msg.sender] + COOLDOWN,
        "Cooldown active"
    );
    lastWithdraw[msg.sender] = block.timestamp;
    // ...
}

Deployment Checklist

Pre-Deployment

  • Circuit tested with multiple inputs
  • Verifier key generated correctly
  • Contract tests passing
  • Gas optimization complete
  • Security audit (for mainnet)

Deployment

  • Correct network selected
  • Sufficient funds in wallet
  • Contract addresses recorded
  • Source verified on explorer

Post-Deployment

  • Contract interaction tested
  • Events emitting correctly
  • Monitoring configured
  • Documentation updated

Resources


Previous: Circom Guide | Next: Further Reading

Grants and Funding

Building zero-knowledge applications on Kusama can be funded through various grant programs and governance mechanisms. This section covers the two primary funding sources for ZK developers.

Funding Overview

SourceAmountFocusDecision By
OpenGov Treasury10-1000+ KSMGeneral projectsCommunity vote
ZK Bounty1000 - 180 000ZK-specific projectsCommunity curators

Quick Navigation

OpenGov Treasury

The decentralized governance system controlling the Kusama treasury:

  • How it works: Community voting on treasury proposals
  • Tracks: Small (<100 KSM), Medium (100-1000 KSM), Large (>1000 KSM)
  • Voting: Conviction voting with token locks
  • Timeline: 7-28 days depending on track

Best for: General projects, infrastructure, ecosystem development

→ Read the OpenGov Guide

Zero Knowledge and Advanced Cryptography Bounty

Part of the Kusama Vision Program (10M DOT pool):

  • Managed by: Community-trusted curators
  • Independent from: Web3 Foundation and OpenGov
  • Focus: Privacy apps, ZK infrastructure, research, developer tools, advanced cryptography
  • Categories: Runtime, Contracts, Applications, Identity, DeFi, Governance, Quantum-resistant
  • Apply: zk.kusama.vision

Best for: Zero-knowledge and advanced cryptography projects that integrate with Kusama ecosystem

→ Read the Zero Knowledge and Advanced Cryptography Guide

Key Differences

AspectOpenGovZero Knowledge and Advanced Cryptography
Funding SourceKusama TreasuryKusama Vision (10M DOT)
Decision MakersCommunity voteCommunity curators
FocusGeneral ecosystemZK and advanced cryptography
ApplicationSubsquare/Polkassemblyzk.kusama.vision
Timeline7-28 days2-4 weeks review
IndependenceOn-chain governanceIndependent from W3F

Which Should You Apply For?

Choose OpenGov if:

  • Your project has broad ecosystem appeal
  • You want community-driven decision making
  • Your project isn't strictly ZK-focused
  • You prefer transparent on-chain voting

Choose Zero Knowledge and Advanced Cryptography if:

  • Your project is specifically about zero-knowledge proofs or advanced cryptography
  • You want expert curator review (not popularity vote)
  • You're building ZK infrastructure or privacy applications
  • You want to be part of the Kusama Vision program

Can I Apply for Both?

Yes! Different components of a larger project can be funded separately, but you cannot double-fund the same work.

Example:

Privacy Pool Project
├── Zero Knowledge and Advanced Cryptography: Circuit development (ZK-specific)
└── OpenGov: Frontend and marketing (general)

Application Processes

OpenGov Treasury Process

  1. Prepare Proposal

    • Technical details
    • Budget breakdown (in KSM)
    • Timeline with milestones
    • Team information
  2. Submit via OpenGov

    • Use Subsquare or Polkassembly
    • Select appropriate track (Small/Medium/Large Spender)
    • Pay decision deposit
  3. Campaign for Support

    • Share on social media
    • Engage with delegates
    • Answer community questions
  4. Vote and Enact

    • Community votes (7-28 days)
    • Confirmation period
    • Automatic enactment if approved

→ Read the full OpenGov Guide

Zero Knowledge and Advanced Cryptography Bounty Process

  1. Join the Community

    • Visit ZK SPACE
    • Discuss your ideas with other developers
    • Visit the community forum at: https://forum.polkadot.network/
    • Get feedback on your proposal concept
    • Figure out why Kusama and PolkaVM is great for your project
  2. Prepare Proposal

    • Technical approach (ZK proof system, integration point)
    • Privacy guarantees
    • Kusama integration plan
    • Open source contributions
    • Budget (in DOT)
  3. Submit Application

  4. Curator Review

    • Community curators evaluate technical merit
    • 2-4 week review period
    • Independent from Web3 Foundation
  5. Decision and Funding

    • Curators make funding decisions
    • Approved projects receive funding up to 180 000 USD
    • Regular progress updates required

→ Read the full Zero Knowledge and Advanced Cryptography Guide

Writing a Successful Proposal

Key Elements (Both Programs)

# Proposal: [Project Name]

## Summary
[2-3 sentence overview]

## Problem
[What are you solving?]

## Solution
[Your approach]

## Technical Details
[Architecture, tech stack, ZK components]

## Budget
[Itemized breakdown]

## Timeline
[Milestones with dates]

## Team
[Credentials and experience]

## Milestones
[Payment schedule tied to deliverables]

Best Practices

Do:

  • ✅ Be specific about deliverables
  • ✅ Tie payments to verifiable milestones
  • ✅ Engage community before submitting
  • ✅ Include links to previous work
  • ✅ Budget realistically with contingency

Don't:

  • ❌ Make vague claims
  • ❌ Request full payment upfront
  • ❌ Ignore feedback
  • ❌ Underestimate timeline
  • ❌ Forget maintenance costs

Resources

OpenGov Platforms

ZK Bounty Platforms

Documentation

Community

  • Discord: Kusama Official Discord - ZK channels
  • Matrix: #Polkadot-Direction:parity.io, ZK SPACE
  • Forum: Kusama Forum governance and ZK sections

Getting Help

For OpenGov Proposals

  1. Read OpenGov Guide
  2. Share draft in Discord/Matrix
  3. Contact OpenGov delegates for feedback
  4. Learn from previous applicants

For Zero Knowledge and Advanced Cryptography Proposals

  1. Read Zero Knowledge and Advanced Cryptography Guide
  2. Join ZK SPACE community
  3. Discuss ideas with curators and community
  4. Review funded project examples

Previous: Why Kusama for ZK | Next: OpenGov

OpenGov Treasury

Kusama OpenGov is the decentralized governance system that controls the Kusama treasury. The treasury is funded by transaction fees, validator slashes, and governance deposits, enabling the community to directly fund projects through democratic voting.

How OpenGov Works

OpenGov enables anyone to submit proposals for treasury funding:

  1. Submit Proposal: Create a referendum with your funding request
  2. Community Voting: KSM holders vote with conviction (longer locks = more voting power)
  3. Confirmation Period: Proposal must meet approval thresholds
  4. Enactment: Approved funds are transferred from treasury

Key Features

FeatureDescription
DecentralizedNo council - direct community control
Multiple TracksDifferent proposal types have different voting periods
DelegationDelegate voting power to experts in specific areas
Conviction VotingLock tokens longer for increased voting power

Treasury Tracks

Different proposal types follow different tracks with varying voting periods:

TrackPurposeAmount RangeVoting Period
Small SpenderSmall grants< 100 KSM~7 days
Medium SpenderMedium grants100-1000 KSM~14 days
Large SpenderLarge grants> 1000 KSM~28 days
TreasuryGeneral spendingAny~28 days

Track Parameters

Each track has specific parameters:

ParameterSmallMediumLarge
Decision period7 days14 days28 days
Confirmation period1 day3 days7 days
Approval thresholdLowerMediumHigher
Support thresholdLowerMediumHigher

Submitting a Treasury Proposal

Step-by-Step Guide

  1. Prepare Your Proposal

    • Project description and goals
    • Technical approach
    • Budget breakdown
    • Timeline with milestones
    • Team information
    • Expected deliverables
  2. Choose Your Platform

  3. Submit the Referendum

    • Select appropriate track (Small/Medium/Large Spender)
    • Pay the decision deposit (varies by track)
    • Set enactment period
  4. Campaign for Support

    • Share on social media (Twitter, Discord, Matrix)
    • Present in community calls
    • Engage with OpenGov delegates
    • Answer community questions
  5. Monitor Voting

    • Track votes in real-time
    • Respond to concerns
    • Provide additional information as needed
  6. Enactment

    • If approved, funds are transferred automatically
    • Begin work on milestones
    • Submit regular progress updates

Writing a Successful Proposal

Essential Components

1. Executive Summary

## Summary
Build a Tornado Cash-style privacy pool for KSM and Kusama-based assets.

## Problem
Kusama lacks private transfer options. All transactions are publicly visible.

## Solution
Deploy a shielded pool using:
- Groth16 proofs (verified via BN254 precompiles)
- Rust-optimized Poseidon hashing
- LeanIMT for efficient Merkle trees

2. Budget Breakdown

## Budget
- Development (3 months): 150 KSM
- Security Audit: 50 KSM
- Infrastructure (1 year): 20 KSM
- Marketing/Community: 10 KSM
- Contingency (10%): 23 KSM
- **Total: 253 KSM**

3. Timeline

## Timeline
- Month 1: Core contract development
- Month 2: Frontend and integration
- Month 3: Testing and audit
- Month 4: Launch and documentation

4. Team

## Team
- **Developer A**: 5 years Rust, 3 years ZK (GitHub: ...)
- **Developer B**: Smart contract auditor, ex-...
- **Advisor C**: Polkadot ecosystem contributor

5. Milestones

## Milestones
1. Contract deployment and testnet launch (30%)
2. Frontend release and user testing (30%)
3. Security audit completion (20%)
4. Mainnet launch and documentation (20%)

Best Practices

Do:

  • ✅ Be specific about deliverables
  • ✅ Tie payments to verifiable milestones
  • ✅ Engage with the community before submitting
  • ✅ Provide regular progress updates
  • ✅ Budget realistically with contingency
  • ✅ Include links to previous work

Don't:

  • ❌ Make vague or overly ambitious claims
  • ❌ Request full payment upfront
  • ❌ Ignore community feedback
  • ❌ Underestimate development time
  • ❌ Forget about maintenance costs
  • ❌ Copy proposals from other projects

Example Proposal Structure

# Treasury Proposal: Kusama Shielded Pool

## Summary
[2-3 sentence overview]

## Problem
[What problem are you solving?]

## Solution
[Technical approach and why it works]

## Technical Details
- Architecture overview
- Technology stack (Rust, Circom, etc.)
- Security considerations
- Integration points

## Budget
[Itemized breakdown]

## Timeline
[Milestones with dates]

## Team
[Members and credentials]

## Milestones
[Payment schedule tied to deliverables]

## Success Metrics
[How will you measure success?]

## Risks and Mitigation
[Potential challenges and solutions]

## Additional Information
[Any other relevant details]

Post-Funding Responsibilities

Once your proposal is funded:

1. Regular Updates

  • Post progress reports on your proposal thread
  • Update at least monthly
  • Include working demos when possible

2. Meet Deadlines

  • Deliver milestones on time
  • Communicate delays proactively
  • Request amendments if needed

3. Community Engagement

  • Respond to questions promptly
  • Participate in community calls
  • Be transparent about challenges

4. Final Report

  • Submit completion summary
  • Provide links to deliverables
  • Document lessons learned

Voting Mechanics

Conviction Voting

Voting power is multiplied based on lock duration:

Lock PeriodConviction Multiplier
No lock0.1x
1x (7 days)1x
2x (14 days)2x
3x (21 days)3x
4x (28 days)4x
5x (35 days)5x
6x (42 days)6x

Approval and Support

Proposals must meet two thresholds:

  • Approval: Ratio of Aye vs Nay votes
  • Support: Ratio of participating votes vs total eligible

Both thresholds vary by track and time in confirmation period.

Delegation

If you can't vote on every proposal, delegate your voting power:

How Delegation Works

  1. Choose a delegate (expert in specific area)
  2. Set conviction level
  3. Delegate can vote on your behalf
  4. You can override delegated votes

Finding Delegates

  • Check delegate profiles on Subsquare
  • Review their voting history
  • Look for expertise in your area of interest
  • Consider splitting delegation across multiple delegates

Canceling or Killing Referendums

Referendum Canceller

  • For non-malicious errors
  • Refunds deposits to originators
  • Requires Whitelisted Caller origin

Referendum Killer

  • For urgent, malicious cases
  • Slashes deposits
  • Requires higher authority origin

Resources

Governance Platforms

Documentation

Community

  • Matrix: #Polkadot-Direction:parity.io
  • Discord: Polkadot Official Discord - governance channels
  • Forum: Polkadot Forum governance section

Success Stories

Notable Treasury-Funded Projects

ProjectAmountOutcome
Privacy Infrastructure200 KSMLaunched with 10K+ users
Developer Tools75 KSM5K+ monthly active users
Documentation Initiative30 KSM100+ guides published
Security Audit Fund150 KSM20+ projects audited

Getting Help

Need assistance with your proposal?

  1. Community Channels: Ask in Polkadot/Kusama Discord or Matrix
  2. OpenGov Delegates: Reach out for feedback and guidance
  3. Previous Applicants: Learn from successful proposal authors
  4. Technical Fellowship: For technical implementation questions
  5. Treasury Working Group: Some communities have dedicated help channels

Previous: Why Kusama for ZK | Next: Zero Knowledge Bounty

Zero Knowledge and Advanced Cryptography

The Zero Knowledge and Advanced Cryptography bounty is part of the Kusama Vision Program, a 10M DOT initiative dedicated to advancing zero-knowledge technology and advanced cryptography on Kusama. This program is managed by community-trusted curators, independent from the Web3 Foundation and OpenGov treasury.

Program Overview

DetailInformation
Total Funding10M DOT (shared pool)
ProgramKusama Vision
Decision MakersCommunity-trusted curators
GovernanceIndependent from Web3 Foundation
Applicationzk.kusama.vision
StatusOngoing (rolling applications)

Core Philosophy

The Zero Knowledge and Advanced Cryptography program is built on the principle that privacy is non-negotiable and fundamental to a free society. The program aims to:

  • Shift trust from institutions and intermediaries to mathematics and cryptography
  • Enable privacy that is enforced by code, not policy
  • Build resilient systems with no censorship, blocking, or filtering of transactions

Why Kusama?

Kusama provides the ideal environment for ZK development:

  1. Fast-Moving Network: Rapid iteration and early deployment
  2. Real Conditions Testing: Production environment, not just testnets
  3. Open Ecosystem: Contribute reusable components for the entire ecosystem
  4. Resilient Execution: Censorship-resistant transaction processing

Funding Categories

The bounty supports projects across multiple categories:

1. Runtime Integration

Integrate ZK proof verification directly into Kusama parachain runtimes.

Examples:

  • Native ZK verification in runtime
  • Precompile additions for ZK operations
  • Runtime-level privacy primitives

2. Smart Contracts

Build privacy-preserving smart contract execution on PolkaVM and other contract platforms.

Examples:

  • Shielded pool contracts
  • Private DeFi protocols
  • ZK-verifiable contract logic

3. Applications

Real-world privacy applications deployed on Kusama.

Examples:

  • Private payment systems
  • Anonymous credential systems
  • Privacy-preserving identity solutions

4. Optimized ZK Libraries for PolkaVM

Develop RISC-V optimized ZK code for the PolkaVM execution environment.

Examples:

  • Poseidon hash implementations
  • SNARK/STARK verifiers
  • Merkle tree libraries

5. Selective Disclosure & ZK Identity

Build credentials, membership proofs, reputation systems, and Sybil resistance mechanisms.

Examples:

  • ZK identity protocols
  • Credential systems with selective disclosure
  • Proof of personhood without doxxing
  • Reputation systems with privacy

6. Private Governance

Enable privacy-preserving participation in OpenGov and other governance systems.

Examples:

  • Private voting systems
  • Hidden delegation strategies
  • Confidential proposal submissions

7. Private DeFi

Develop privacy-preserving decentralized finance protocols.

Examples:

  • Private DEX with hidden order books
  • Confidential lending protocols
  • Private stablecoins
  • Shielded liquidity pools

8. Quantum-Resistant ZK

Research and implement post-quantum cryptographic proof systems.

Examples:

  • STARK-based constructions
  • Lattice-based ZK proofs
  • Hash-based proof systems

Eligibility Criteria

To qualify for funding, projects must:

  • ✅ Apply zero-knowledge technology in innovative ways
  • Integrate with Kusama ecosystem (runtimes, contracts, or applications)
  • Reduce information leakage while preserving verifiability
  • Contribute open, reusable components to the ecosystem
  • ✅ Work in real production conditions on Kusama

Application Process

Step 1: Join the Community

Before applying, engage with the community:

  • Visit ZK SPACE community hub
  • Discuss your ideas with other developers
  • Get feedback on your proposal concept
  • Connect with potential collaborators

Step 2: Prepare Your Proposal

Your proposal should include:

# Zero Knowledge and Advanced Cryptography Proposal

## Project Summary
[Clear description of what you're building]

## Problem Statement
[What privacy problem are you solving?]

## Technical Approach
- ZK proof system: [Groth16/PLONK/Halo2/STARK]
- Integration point: [Runtime/Contract/Application]
- Privacy guarantees: [What is hidden vs. verified]

## Kusama Integration
[How does this integrate with Kusama ecosystem?]

## Open Source Plan
[What components will be reusable by others?]

## Budget
[Requested amount and breakdown]

## Timeline
[Milestones and delivery dates]

## Team
[Relevant experience and credentials]

Step 3: Submit Application

  1. Visit zk.kusama.vision
  2. Click "APPLY NOW"
  3. Fill out the application form
  4. Submit your proposal

Step 4: Curator Review

Your proposal will be reviewed by community-trusted curators who evaluate:

  • Technical merit and innovation
  • Feasibility of implementation
  • Benefit to Kusama ecosystem
  • Team capability
  • Budget reasonableness

Step 5: Decision and Funding

  • Curators make funding decisions
  • Approved projects receive funding
  • Work begins on milestones
  • Regular progress updates required

Decision-Making Process

Community Curators

Unlike OpenGov treasury proposals, Zero Knowledge and Advanced Cryptography decisions are made by a group of community-trusted curators who:

  • Have expertise in zero-knowledge cryptography
  • Understand the Kusama ecosystem
  • Evaluate technical merit independently
  • Are independent from Web3 Foundation
  • Make decisions based on project quality, not popularity

Evaluation Criteria

CriteriaWeightDescription
Technical Innovation30%Novel use of ZK technology
Kusama Integration25%How well it integrates with ecosystem
Privacy Impact20%Real privacy improvements
Reusability15%Open components for others
Team Capability10%Ability to deliver

Request for Proposals (RFP)

The curators have created a list of Request for Proposals (RFPs) - specific projects and research areas they want to see developed. These RFPs represent high-priority needs for the Kusama ecosystem.

View and Apply for RFPs

codeberg.org/kusama-zk/RFP

Browse the full list of open RFPs and submit proposals for projects that match your expertise.

Why Apply for an RFP?

  • Priority Review: RFP proposals receive expedited curator attention
  • Clear Requirements: Well-defined scope and deliverables
  • Higher Success Rate: Aligned with curator priorities
  • Ecosystem Impact: Address critical Kusama needs

RFP Categories

RFPs cover various areas including:

  • ZK Infrastructure: Provers, verifiers, circuit libraries
  • Privacy Applications: Shielded pools, private DeFi, anonymous credentials
  • PolkaVM Optimization: RISC-V optimized ZK code
  • Identity & Governance: Selective disclosure, private voting
  • Research: Post-quantum ZK, proof aggregation, novel constructions

Responding to an RFP

  1. Review the RFP: Read the full requirements on Codeberg
  2. Assess Fit: Ensure your team has relevant expertise
  3. Prepare Proposal: Address all RFP requirements specifically
  4. Submit Application: Apply via zk.kusama.vision
  5. Reference the RFP: Mention which RFP you're responding to in your application

Open RFP Process

RFPs are updated regularly as priorities evolve and projects are completed:

  • New RFPs: Added based on ecosystem needs and curator input
  • Completed RFPs: Marked as fulfilled when projects ship
  • Community Suggestions: Propose new RFP topics via ZK SPACE

What Makes a Strong Proposal?

Do ✅

  • Demonstrate ZK expertise: Show understanding of proof systems
  • Clear privacy guarantees: Explain what's hidden and what's verified
  • Kusama-native thinking: Design for Kusama's architecture
  • Open source commitment: Plan for community reuse
  • Realistic timeline: Achievable milestones
  • Production focus: Build for real-world use

Don't ❌

  • Vague privacy claims: Be specific about cryptographic guarantees
  • Generic proposals: Tailor to Kusama specifically
  • Closed source: Must contribute to ecosystem
  • Overpromising: Under-promise and over-deliver
  • Ignoring existing work: Build on existing ZK libraries

Reporting Requirements

Once funded, projects must:

  1. Regular Updates: Progress reports every 2-4 weeks
  2. Milestone Delivery: Meet agreed-upon deadlines
  3. Open Source: Release code as agreed
  4. Final Report: Summary of outcomes and learnings
  5. Community Engagement: Present work to community

Resources

Technical Resources

Community

  • Discord: Kusama Discord - ZK channels
  • Matrix: ZK SPACE community
  • Forum: Kusama Forum ZK discussions

FAQ

Q: Is this the same as OpenGov treasury funding?

A: No. The Zero Knowledge and Advanced Cryptography is part of Kusama Vision (10M DOT) and is managed by community curators, independent from OpenGov and Web3 Foundation.

Q: Can I apply for both Zero Knowledge and Advanced Cryptography and OpenGov?

A: Yes, but you cannot double-fund the same work. Different components of a larger project can be funded separately.

Q: Do I need to be part of an organization?

A: No, individuals and teams can both apply.

Q: What proof systems are supported?

A: All major proof systems: Groth16, PLONK, Halo2, STARKs, etc. Choose based on your use case.

Q: Does my project need to be Kusama-exclusive?

A: No, but it must integrate meaningfully with Kusama and contribute reusable components to the ecosystem.

Q: How long does the review process take?

A: Typically 2-4 weeks from application to decision, depending on curator availability and proposal complexity.

Q: Can I reapply if rejected?

A: Yes, you can reapply with an improved proposal addressing feedback from curators.

Q: What happens if I don't complete milestones?

A: Funding is typically tied to milestones. Uncompleted milestones may result in reduced or withheld funding.

Previous: OpenGov | Next: PolkaVM Smart Contracts

Further Reading

Academic Papers

Foundational Papers

  • Goldwasser, Micali, Rackoff (1985) - The Knowledge Complexity of Interactive Proof Systems
  • Fiat, Shamir (1986) - How to Prove Yourself: Practical Solutions to Identification and Signature Problems
  • Schnorr (1991) - Efficient Signature Generation by Smart Cards

Modern Developments

  • Groth (2016) - On the Size of Pairing-based Non-interactive Arguments
  • Ben-Sasson et al. (2018) - Scalable, Transparent, and Post-Quantum Secure Computational Integrity
  • Gabizon, Williamson, Ciobotaru (2019) - PLONK: Permutations over Lagrange-bases for Oecumenical Noninteractive arguments of Knowledge

Books

  • "Real-World Cryptography" by David Wong - Practical cryptography including ZK
  • "Programming Bitcoin" by Jimmy Song - Includes ZK proof implementations
  • "Zero-Knowledge Proofs: A Practical Guide" (forthcoming)

Online Resources

Tutorials

Documentation

Development Tools

Languages and Frameworks

ToolLanguageUse Case
CircomDSLSNARK circuits
CairoRust-likeSTARK programs
NoirRust-likeUniversal ZK
Halo2RustSNARK backend
gnarkGoSNARK library

Testing and Debugging

  • SnarkJS - JavaScript library for SNARKs
  • ZoKrates - High-level ZK language
  • Bellman - ZK proof library

Communities

Conferences

  • ZK Summit - Annual zero-knowledge conference
  • Real World Crypto - Applied cryptography
  • CRYPTO / EUROCRYPT - Academic cryptography
  • DevCon - Ethereum development

Getting Involved

For Developers

  1. Start with tutorials using SnarkJS or Circom
  2. Build simple circuits (age verification, password check)
  3. Contribute to open-source ZK projects
  4. Participate in hackathons

For Researchers

  1. Follow latest papers on IACR ePrint
  2. Join working groups at zkproof.org
  3. Attend academic conferences
  4. Collaborate with industry

For Enthusiasts

  1. Follow ZK podcasts and blogs
  2. Join community discussions
  3. Experiment with ZK applications
  4. Spread awareness about privacy technology

Glossary

TermDefinition
Arithmetic CircuitComputational model using addition/multiplication gates
CommitmentCryptographic primitive to commit to a value
Elliptic CurveMathematical structure used in cryptography
FieldAlgebraic structure with addition, multiplication
Hash FunctionOne-way function mapping arbitrary input to fixed output
Merkle TreeTree structure for efficient membership proofs
Polynomial CommitmentCommit to a polynomial, prove evaluations
Range ProofProve a value lies within a range
WitnessSecret input that satisfies a circuit

Back to: Introduction