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:
-
Completeness: If the statement is true, an honest verifier will be convinced by an honest prover.
-
Soundness: If the statement is false, no cheating prover can convince the honest verifier that it is true (except with negligible probability).
-
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.
- Bob waits outside while Alice enters the cave
- Alice chooses a path (left or right) randomly
- Bob enters and calls out which path Alice should exit from
- If Alice knows the secret word, she can always comply by opening the door if needed
- If Alice doesn't know the word, she can only succeed by chance (50%)
- 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
| Year | Milestone |
|---|---|
| 1985 | Goldwasser, Micali, and Rackoff introduce ZK proofs |
| 1986 | Fiat-Shamir heuristic creates non-interactive proofs |
| 2012 | Groth-Sahai proofs enable practical applications |
| 2016 | Zcash launches with zk-SNARKs |
| 2020s | zk-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
- New to ZK? Start with Core Concepts
- Building on Kusama? Jump to Kusama ZK Development
- Want to deploy? See the Deployment Guide
- Seeking funding? Check Grants and Funding
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
| Term | Definition |
|---|---|
| Prover | The party demonstrating knowledge of a secret |
| Verifier | The party being convinced of the proof |
| Witness | The secret information the prover knows |
| Statement | The claim being proven |
| Circuit | A 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:
- Commitment: Prover sends a commitment
- Challenge: Verifier sends a random challenge
- 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
| Property | SNARKs | STARKs | Bulletproofs |
|---|---|---|---|
| Proof Size | ~288 bytes | ~200 KB | ~600 bytes |
| Verification Time | ~10ms | ~100ms | ~seconds |
| Trusted Setup | Required | None | None |
| Post-Quantum | No | Yes | No |
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:
- Prover commits: r ← random, send R = g^r
- Verifier challenges: send c ← random
- Prover responds: s = r + c·x (where x is the secret)
- 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:
- Instead of verifier sending random challenge c
- Prover computes c = H(commitment, statement)
- 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 Case | Type |
|---|---|
| Real-time authentication | Interactive |
| Blockchain transactions | Non-interactive |
| Private computation | Non-interactive |
| Identification protocols | Interactive |
Next: SNARKs and STARKs
SNARKs and STARKs
zk-SNARKs
Zero-Knowledge Succinct Non-Interactive Argument of Knowledge
How SNARKs Work
- Circuit Representation: Express computation as an arithmetic circuit
- R1CS Constraint System: Convert to Rank-1 Constraint System
- QAP Transformation: Encode as Quadratic Arithmetic Program
- Polynomial Commitment: Commit to polynomials representing the witness
- Proof Generation: Create succinct proof using cryptographic primitives
Popular SNARK Constructions
| Construction | Key Features |
|---|---|
| Groth16 | Smallest proofs, requires trusted setup per circuit |
| PLONK | Universal trusted setup, flexible circuits |
| Marlin | Universal setup, efficient proving |
| Halo2 | No 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
- Execution Trace: Represent computation as a trace of states
- Polynomial Interpolation: Encode trace as polynomials
- FRI Protocol: Prove low-degree of polynomials
- 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
| Feature | SNARKs | STARKs |
|---|---|---|
| Proof Size | Small (~288 bytes) | Large (~200 KB) |
| Verification | Fast (~10ms) | Slower (~100ms) |
| Proving Time | Fast | Fast |
| Trusted Setup | Often required | Never required |
| Quantum Resistance | No | Yes |
| Maturity | High | Growing |
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
| Application | Description | Components |
|---|---|---|
| Shielded Pool | Private asset transfers | Circom + PolkaVM + LeanIMT |
| ZK Identity | Anonymous credentials | Poseidon + Groth16 |
| Private DEX | Hidden order books | SNARKs + Asset Hub |
| ZK Bridge | Cross-chain privacy | XCM + 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.
| Parameter | Value |
|---|---|
| Chain ID | 420420422 |
| RPC Endpoint | https://testnet-passet-hub-eth-rpc.polkadot.io |
| Block Explorer | https://blockscout-passet-hub.parity-testnet.parity.io/ |
| Faucet | https://faucet.polkadot.io/?parachain=1111 |
| Token | PAS (Paseo) |
| website | https://paseo.site/ |
Getting Started
-
Set up development environment
- Install Rust and PolkaVM toolchain
- Install Node.js and snarkJS
- Install Circom compiler
-
Write your first circuit
- Start with simple arithmetic circuits
- Generate test witnesses
- Create proofs locally
-
Deploy verifier contract
- Use snarkJS to generate Solidity verifier
- Deploy to Paseo Asset Hub
- Test with generated proofs
-
Build full application
- Add Merkle tree for state
- Implement deposit/withdraw logic
- Create frontend for users
Next Steps
- PolkaVM Smart Contracts - Write ZK-friendly contracts
- Poseidon Hash - Implement efficient hashing
- Asset Hub Integration - Deploy on Kusama
- Shielded Pools - Build privacy applications
- Circom Guide - Design circuits
- Deployment - Deploy to testnet
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
| Metric | Kusama Asset Hub | Ethereum (20 gwei) | Advantage |
|---|---|---|---|
| Groth16 Verification | 3,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:
| Precompile | Kusama Gas | Ethereum Gas | Ratio |
|---|---|---|---|
| ecRecover | 991 | 3,000 | 0.33x |
| ecAdd (BN254) | 991 | 150 | 6.6x* |
| ecMul (BN254) | 991 | 6,000 | 0.17x |
| ecPairing (4 pairs) | 2,062 | 181,000 | 0.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
u64limbs withu128intermediates - 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:
| Dimension | What it Measures | ZK Relevance |
|---|---|---|
ref_time | Computation time (picoseconds) | Primary constraint for ZK ops |
proof_size | State proof size for validators | Minor for ZK compute |
storage_deposit | Long-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
| Metric | Value |
|---|---|
| Block ref_time limit | 1.44 × 10¹² picoseconds (1.44 seconds) |
| Max Rust Poseidon hashes per block | 39 |
| Max Merkle depth per transaction | 32+ |
| 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:
| Operation | Gas | KSM Cost | USD Cost |
|---|---|---|---|
| Deposit | ~45,000 | 0.045 | $0.19 |
| Withdrawal | ~7,000 | 0.007 | $0.03 |
| Full cycle | ~52,000 | 0.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:
| Denomination | Gas 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
| Chain | ZK Verification Cost | Hash Cost | Privacy Viability |
|---|---|---|---|
| Kusama Asset Hub | $0.017 | $0.012 | High (if adoption) |
| Ethereum | $10.11 | $1.64 | Medium (high cost) |
| Polygon | $0.50 | $0.08 | Low (small anonymity sets) |
| BNB Chain | $0.30 | $0.05 | Medium |
| Aztec | Custom | Custom | High (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
| Implementation | Single Hash | 10 Hashes | Merkle Depth 20 |
|---|---|---|---|
| Solidity (resolc) | 47,851 gas | IMPOSSIBLE | IMPOSSIBLE |
| Rust PVM | 2,706 gas | 17,420 gas | 37,956 gas |
| Improvement | 17.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
| Limitation | Impact | Workaround |
|---|---|---|
| Solidity 256-bit arithmetic | 28x slower than Rust | Use Rust PVM for field ops |
| Gas abstraction | Can mislead on true cost | Check ref_time in errors |
| PointEvaluation (0x0A) | Not available | Use Groth16, not KZG-based |
| Block ref_time limit | Max ~39 hashes/tx | Batch carefully, use Rust |
| Compiler maturity | resolc still experimental | Test thoroughly, use Rust |
Getting Started
-
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 -
Study the benchmarks
- Review zk-benchmarks data
- Understand ref_time vs gas tradeoffs
-
Build your first contract
- Start with PoseidonPolkaVM example
- Test on Paseo testnet
-
Deploy and measure
- Use
cast estimatefor gas estimation - Check transaction receipts for actual ref_time
- Use
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
SimpleAllocand 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
Gas Optimization Tips
- Use Poseidon over Keccak: 5-10x cheaper in circuits
- Batch operations: Multiple hashes in one call
- Minimize storage: Use memory for intermediate calculations
- 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
- RevX.dev - Browser-based PolkaVM IDE
- Claude Code AI Guide - AI-assisted development prompts
- PolkaVM Documentation
- pallet-revive Docs
Libraries & Examples
- Poseidon Paper
- PoseidonPolkaVM
- ERC20 in Rust - Complete ERC20 implementation
- Rust Contract Template
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 Function | Constraints (BN254) | Gas Cost | ZK-Friendly |
|---|---|---|---|
| Poseidon | ~240 | Low | ✓ |
| Keccak-256 | ~25,000 | High | ✗ |
| SHA-256 | ~20,000 | High | ✗ |
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):
| Parameter | Value |
|---|---|
| State Size (t) | 3 |
| Full Rounds (R_F) | 8 |
| Partial Rounds (R_P) | 57 |
| S-Box | x⁵ |
| MDS Matrix | 3×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:
- Add round constants
- Apply S-Box (x⁵)
- 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:
| Input | Expected 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
| Operation | PolkaVM Gas | EVM 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
| Name | Address | Kusama Gas | Ethereum Gas | Use Case |
|---|---|---|---|---|
| ecRecover | 0x01 | 991 | 3,000 | Signature verification |
| SHA-256 | 0x02 | 991 | 60 + 12/word | General hashing |
| RIPEMD-160 | 0x03 | 991 | 600 + 120/word | Bitcoin-style hashing |
| Identity | 0x04 | 991 | 15 + 3/word | Data copying |
| ModExp | 0x05 | 991 | Variable | RSA, modular arithmetic |
| ecAdd (BN254) | 0x06 | 991 | 150 | BN254 point addition |
| ecMul (BN254) | 0x07 | 991 | 6,000 | BN254 scalar multiplication |
| ecPairing | 0x08 | 1,511+ | 113,000+ | ZK proof verification |
| Blake2f | 0x09 | 991 | 4 | Blake2 compression |
| P256Verify | 0x100 | Callable | N/A | WebAuthn/passkey |
Not Available
| Precompile | Address | Status |
|---|---|---|
| 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:
| Pairs | Kusama Gas | Ethereum Gas | Ratio |
|---|---|---|---|
| 2 | 1,511 | 113,000 | 75x cheaper |
| 4 | 2,062 | 181,000 | 88x cheaper |
| 6 | ~2,613 | ~249,000 | 95x 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:
| Asset | ID | ERC20 Address |
|---|---|---|
| USDT | 1984 | 0x000007C000000000000000000000000001200000 |
| USDC | 1337 | 0x0000053900000000000000000000000001200000 |
| DOT | 100 | 0x0000006400000000000000000000000001200000 |
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
Paseo Testnet (Recommended for Development)
| Parameter | Value |
|---|---|
| Network Name | Paseo Asset Hub |
| Chain ID | 420420422 |
| RPC URL | https://testnet-passet-hub-eth-rpc.polkadot.io |
| Block Explorer | https://blockscout-passet-hub.parity-testnet.parity.io/ |
| Faucet | https://faucet.polkadot.io/?parachain=1111 |
| Native Token | PAS |
Kusama Mainnet
| Parameter | Value |
|---|---|
| Network Name | Kusama Asset Hub |
| Chain ID | (TBD) |
| RPC URL | https://kusama-asset-hub-rpc.polkadot.io |
| Block Explorer | https://blockscout.kusama.network/ |
| Native Token | KSM |
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
- Visit Remix for Polkadot
- Connect to Paseo Asset Hub network
- Write/paste your contract
- 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
| Operation | Gas Cost | PAS Cost (approx) |
|---|---|---|
| Transfer | 21,000 | 0.001 |
| ERC20 Transfer | 65,000 | 0.003 |
| Contract Deployment | 500,000+ | 0.02+ |
| ZK Verification | 100,000-300,000 | 0.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
- Merkle Proof: Proves commitment exists in tree
- Nullifier Hash: Unique per spend, prevents double-spend
- New Commitment: UTXO change for remaining value
- 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
| Component | Gas Cost | Optimization |
|---|---|---|
| Deposit | ~150,000 | Batch inserts |
| Withdraw | ~300,000 | Efficient circuits |
| Merkle Insert | ~50,000 | LeanIMT 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:
- Groth16 with snarkjs - Browser-based proving using WebAssembly
- 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(¶mbytes).to_vec(); let params = ParamsKZG::<Bn256>::read( &mut BufReader::new(¶ms_vec[..]) )?; // Generate proving and verifying keys let (pk, vk) = generate_keys(¶ms, &circuit)?; // Compute public output let output = PoseidonHash::<_, Posbn254<WIDTH, RATE>, ConstantLength<L>, WIDTH, RATE>::init() .hash(msg); // Generate proof let proof = generate_proof2( ¶ms, &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(¶ms, &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
- 0xparc Circom Learning Resource - Interactive tutorials and deep-dive explanations
- Circom Documentation - Official language reference
Libraries & Tools
- circomlib - Standard circuit library (Poseidon, Merkle trees, etc.)
- circomlibjs - JavaScript bindings for witness generation
Examples
- zk-assethub-demo Circuits - Circuit 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
| Parameter | Value |
|---|---|
| RPC URL | https://testnet-passet-hub-eth-rpc.polkadot.io |
| Chain ID | 420420422 |
| Explorer | https://blockscout-passet-hub.parity-testnet.parity.io/ |
| Faucet | https://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
| Parameter | Value |
|---|---|
| RPC URL | https://kusama-asset-hub-rpc.polkadot.io |
| Chain ID | (Check current value) |
| Explorer | https://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
- Visit Blockscout explorer
- Navigate to your contract
- Click "Verify & Publish"
- 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
| Source | Amount | Focus | Decision By |
|---|---|---|---|
| OpenGov Treasury | 10-1000+ KSM | General projects | Community vote |
| ZK Bounty | 1000 - 180 000 | ZK-specific projects | Community 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
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
| Aspect | OpenGov | Zero Knowledge and Advanced Cryptography |
|---|---|---|
| Funding Source | Kusama Treasury | Kusama Vision (10M DOT) |
| Decision Makers | Community vote | Community curators |
| Focus | General ecosystem | ZK and advanced cryptography |
| Application | Subsquare/Polkassembly | zk.kusama.vision |
| Timeline | 7-28 days | 2-4 weeks review |
| Independence | On-chain governance | Independent 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
-
Prepare Proposal
- Technical details
- Budget breakdown (in KSM)
- Timeline with milestones
- Team information
-
Submit via OpenGov
- Use Subsquare or Polkassembly
- Select appropriate track (Small/Medium/Large Spender)
- Pay decision deposit
-
Campaign for Support
- Share on social media
- Engage with delegates
- Answer community questions
-
Vote and Enact
- Community votes (7-28 days)
- Confirmation period
- Automatic enactment if approved
Zero Knowledge and Advanced Cryptography Bounty Process
-
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
-
Prepare Proposal
- Technical approach (ZK proof system, integration point)
- Privacy guarantees
- Kusama integration plan
- Open source contributions
- Budget (in DOT)
-
Submit Application
- Apply at zk.kusama.vision
- Click "APPLY NOW"
- Fill out the application form
-
Curator Review
- Community curators evaluate technical merit
- 2-4 week review period
- Independent from Web3 Foundation
-
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
- Kusama Subsquare - Primary interface
- Polkassembly - Discussions and tracking
- Polkadot.js Apps - Direct submission
ZK Bounty Platforms
- zk.kusama.vision - Apply here
- ZK SPACE - Community discussion hub
Documentation
- OpenGov Guide - Detailed OpenGov information
- Zero Knowledge and Advanced Cryptography Guide - Bounty-specific guidance
- Polkadot Wiki: OpenGov
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
- Read OpenGov Guide
- Share draft in Discord/Matrix
- Contact OpenGov delegates for feedback
- Learn from previous applicants
For Zero Knowledge and Advanced Cryptography Proposals
- Read Zero Knowledge and Advanced Cryptography Guide
- Join ZK SPACE community
- Discuss ideas with curators and community
- 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:
- Submit Proposal: Create a referendum with your funding request
- Community Voting: KSM holders vote with conviction (longer locks = more voting power)
- Confirmation Period: Proposal must meet approval thresholds
- Enactment: Approved funds are transferred from treasury
Key Features
| Feature | Description |
|---|---|
| Decentralized | No council - direct community control |
| Multiple Tracks | Different proposal types have different voting periods |
| Delegation | Delegate voting power to experts in specific areas |
| Conviction Voting | Lock tokens longer for increased voting power |
Treasury Tracks
Different proposal types follow different tracks with varying voting periods:
| Track | Purpose | Amount Range | Voting Period |
|---|---|---|---|
| Small Spender | Small grants | < 100 KSM | ~7 days |
| Medium Spender | Medium grants | 100-1000 KSM | ~14 days |
| Large Spender | Large grants | > 1000 KSM | ~28 days |
| Treasury | General spending | Any | ~28 days |
Track Parameters
Each track has specific parameters:
| Parameter | Small | Medium | Large |
|---|---|---|---|
| Decision period | 7 days | 14 days | 28 days |
| Confirmation period | 1 day | 3 days | 7 days |
| Approval threshold | Lower | Medium | Higher |
| Support threshold | Lower | Medium | Higher |
Submitting a Treasury Proposal
Step-by-Step Guide
-
Prepare Your Proposal
- Project description and goals
- Technical approach
- Budget breakdown
- Timeline with milestones
- Team information
- Expected deliverables
-
Choose Your Platform
- Kusama Subsquare - Primary interface
- Polkassembly - Governance dashboard
- Polkadot.js Apps - Direct submission
-
Submit the Referendum
- Select appropriate track (Small/Medium/Large Spender)
- Pay the decision deposit (varies by track)
- Set enactment period
-
Campaign for Support
- Share on social media (Twitter, Discord, Matrix)
- Present in community calls
- Engage with OpenGov delegates
- Answer community questions
-
Monitor Voting
- Track votes in real-time
- Respond to concerns
- Provide additional information as needed
-
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 Period | Conviction Multiplier |
|---|---|
| No lock | 0.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
- Choose a delegate (expert in specific area)
- Set conviction level
- Delegate can vote on your behalf
- 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
- Kusama Subsquare - Primary OpenGov interface
- Polkassembly - Governance dashboard and discussions
- Polkadot.js Apps - Submit and track proposals
Documentation
Community
- Matrix: #Polkadot-Direction:parity.io
- Discord: Polkadot Official Discord - governance channels
- Forum: Polkadot Forum governance section
Success Stories
Notable Treasury-Funded Projects
| Project | Amount | Outcome |
|---|---|---|
| Privacy Infrastructure | 200 KSM | Launched with 10K+ users |
| Developer Tools | 75 KSM | 5K+ monthly active users |
| Documentation Initiative | 30 KSM | 100+ guides published |
| Security Audit Fund | 150 KSM | 20+ projects audited |
Getting Help
Need assistance with your proposal?
- Community Channels: Ask in Polkadot/Kusama Discord or Matrix
- OpenGov Delegates: Reach out for feedback and guidance
- Previous Applicants: Learn from successful proposal authors
- Technical Fellowship: For technical implementation questions
- 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
| Detail | Information |
|---|---|
| Total Funding | 10M DOT (shared pool) |
| Program | Kusama Vision |
| Decision Makers | Community-trusted curators |
| Governance | Independent from Web3 Foundation |
| Application | zk.kusama.vision |
| Status | Ongoing (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:
- Fast-Moving Network: Rapid iteration and early deployment
- Real Conditions Testing: Production environment, not just testnets
- Open Ecosystem: Contribute reusable components for the entire ecosystem
- 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
- Visit zk.kusama.vision
- Click "APPLY NOW"
- Fill out the application form
- 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
| Criteria | Weight | Description |
|---|---|---|
| Technical Innovation | 30% | Novel use of ZK technology |
| Kusama Integration | 25% | How well it integrates with ecosystem |
| Privacy Impact | 20% | Real privacy improvements |
| Reusability | 15% | Open components for others |
| Team Capability | 10% | 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
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
- Review the RFP: Read the full requirements on Codeberg
- Assess Fit: Ensure your team has relevant expertise
- Prepare Proposal: Address all RFP requirements specifically
- Submit Application: Apply via zk.kusama.vision
- 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:
- Regular Updates: Progress reports every 2-4 weeks
- Milestone Delivery: Meet agreed-upon deadlines
- Open Source: Release code as agreed
- Final Report: Summary of outcomes and learnings
- Community Engagement: Present work to community
Resources
Official Links
- Zero Knowledge and Advanced Cryptography Program - Main website
- Apply Now - Submit your proposal
- ZK SPACE - Community discussion hub
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
- ZK Whiteboard Sessions - Video lecture series
- Zero Knowledge Podcast - Industry news and interviews
- Vitalik's ZK Blog Posts - Technical explanations
Documentation
- zkproof.org - Community standards and resources
- Learn0Knowledge - Educational content
- Electric Coin Co. Blog - Zcash development
Development Tools
Languages and Frameworks
| Tool | Language | Use Case |
|---|---|---|
| Circom | DSL | SNARK circuits |
| Cairo | Rust-like | STARK programs |
| Noir | Rust-like | Universal ZK |
| Halo2 | Rust | SNARK backend |
| gnark | Go | SNARK 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
- Start with tutorials using SnarkJS or Circom
- Build simple circuits (age verification, password check)
- Contribute to open-source ZK projects
- Participate in hackathons
For Researchers
- Follow latest papers on IACR ePrint
- Join working groups at zkproof.org
- Attend academic conferences
- Collaborate with industry
For Enthusiasts
- Follow ZK podcasts and blogs
- Join community discussions
- Experiment with ZK applications
- Spread awareness about privacy technology
Glossary
| Term | Definition |
|---|---|
| Arithmetic Circuit | Computational model using addition/multiplication gates |
| Commitment | Cryptographic primitive to commit to a value |
| Elliptic Curve | Mathematical structure used in cryptography |
| Field | Algebraic structure with addition, multiplication |
| Hash Function | One-way function mapping arbitrary input to fixed output |
| Merkle Tree | Tree structure for efficient membership proofs |
| Polynomial Commitment | Commit to a polynomial, prove evaluations |
| Range Proof | Prove a value lies within a range |
| Witness | Secret input that satisfies a circuit |
Back to: Introduction