ZK-SNARKs (Zero-Knowledge Succinct Non-Interactive Arguments of Knowledge) enable one party to prove they know a secret value or have performed a valid computation without revealing the secret itself. For private betting, this allows a user to prove they have placed a valid bet—meeting criteria like sufficient balance or a correct prediction—without exposing their identity, the bet amount, or the specific prediction on-chain. This shifts the paradigm from transparent, public ledger transactions to a model of private state with public verifiability, a core requirement for compliant and user-friendly gambling dApps.
Setting Up a ZK-SNARK Layer for Anonymous Betting
Introduction to ZK-SNARKs for Private Betting
A technical guide to implementing zero-knowledge proofs for creating verifiable yet anonymous betting applications on Ethereum and other blockchains.
Setting up a ZK-SNARK layer involves three core technical components: the circuit, the trusted setup, and the verifier contract. First, you define an arithmetic circuit in a domain-specific language like Circom or ZoKrates. This circuit encodes the rules of your betting game. For a simple coin flip bet, the circuit would take private inputs (the user's secret guess and a random salt) and public inputs (the game outcome), and output 1 only if the user's hashed commitment matches and the guess is correct. The circuit logic is the source of truth for what constitutes a valid proof.
Next, a one-time trusted setup ceremony generates the proving and verification keys for your specific circuit. These keys are critical: the proving key allows users to generate proofs, and the verification key allows the blockchain to check them. While the requirement for trust has been a historical concern, projects like the Perpetual Powers of Tau and semaphore ceremonies provide reusable, contributor-mitigated setups. For production, you would use a secure multi-party computation (MPC) ceremony or a reputable existing setup for common circuits to bootstrap your application.
Finally, you deploy a verifier smart contract to your target chain, such as Ethereum, Polygon, or a zkEVM rollup. This contract, often auto-generated from your circuit compilation, contains a verifyProof function. When a user wants to claim winnings, they submit a ZK-SNARK proof generated off-chain with their private inputs. The contract checks this proof against the public input (e.g., the winning outcome) and the embedded verification key. If valid, it executes the payout, all without learning who submitted the winning bet or what their specific guess was. This is the foundation for anonymous prize redemption.
A practical implementation flow using Circom and snarkjs looks like this: 1) Write your circuit (bet.circom). 2) Compile it to generate R1CS constraints and a WASM calculator. 3) Run the trusted setup phase to generate proving_key.zkey and verification_key.json. 4) Use snarkjs in your client to generate a proof from user inputs. 5) Export the verifier contract and deploy it. 6) Have the client call verifyProof on-chain with the proof and public signals. The entire process ensures the blockchain only sees a proof of valid participation, not the participation details themselves.
Key considerations for developers include circuit complexity gas costs, client-side proof generation, and data availability. Complex circuits increase proving time and the gas cost of verification. Users need to run a proving process in their browser, which for large circuits may require a WebAssembly backend. Furthermore, while the bet is private, the contract must still have access to the essential public outcome (e.g., a verifiable random function result) to check the proof against. Integrating with oracles like Chainlink VRF to provide this tamper-proof public data is a common pattern for ensuring the game's fairness is as verifiable as its privacy.
Prerequisites and Setup
This guide outlines the essential tools and foundational knowledge required to build a private betting application using ZK-SNARKs.
Building a ZK-SNARK layer for anonymous betting requires a solid foundation in both cryptographic concepts and modern development tools. You should be comfortable with zero-knowledge proof fundamentals, understanding the roles of the prover and verifier. Familiarity with elliptic curve cryptography (specifically the BN128 or BLS12-381 curves) is highly recommended, as these are commonly used in zk-SNARK libraries like circom and snarkjs. A working knowledge of Node.js (v18 or later) and npm is essential for managing dependencies and running the toolchain.
The core of your development stack will be Circom 2, a domain-specific language for defining arithmetic circuits, and snarkjs, a JavaScript library for generating and verifying proofs. You will also need a package manager like npm or yarn. Start by installing these globally: npm install -g circom snarkjs. For trustless setup ceremonies, you may need the powersOfTau files, which can be downloaded from the Perpetual Powers of Tau ceremony. This setup provides the structured reference string (SRS) needed for your circuit's proving system.
Your development environment should be configured to compile Circom circuits into R1CS (Rank-1 Constraint Systems) and generate the necessary proving and verification keys. A typical workflow involves writing a .circom file defining your betting logic (e.g., proving age > 21 without revealing the exact age), compiling it with circom circuit.circom --r1cs --wasm --sym, and then using snarkjs to perform the trusted setup and generate keys. Ensure you have sufficient RAM (8GB minimum) for the phase 2 setup, which can be computationally intensive for non-trivial circuits.
Beyond the tooling, you must architect your application's state and logic. The smart contract on-chain (e.g., on Ethereum or a compatible L2) will hold the verification key and contain a function to verify submitted proofs. Off-chain, your client application will use the compiled circuit's WebAssembly (WASM) module and the proving key to generate a proof from private user inputs. Understanding this separation—off-chain proof generation and on-chain verification—is critical for designing a gas-efficient and functional dApp.
Finally, consider the security implications of your setup. The initial trusted setup ceremony (Phase 1 & 2) is a critical point; if compromised, the entire system's security fails. For production, participate in or conduct a multi-party ceremony. Always audit your Circom circuits for logical errors and constraints that could leak information. Test thoroughly with libraries like mocha or jest, simulating various betting scenarios with invalid and valid inputs to ensure the proof system rejects and accepts them correctly.
Core ZK-SNARK Concepts for Betting
A technical guide to implementing a ZK-SNARK layer for privacy-preserving on-chain betting applications.
A ZK-SNARK (Zero-Knowledge Succinct Non-Interactive Argument of Knowledge) layer enables users to prove they have performed a valid action, like placing a bet, without revealing the underlying data. In a betting context, this means a user can prove they have sufficient funds, are within betting limits, and have made a valid prediction, all while keeping their wallet address, exact stake, and chosen outcome private on-chain. This transforms a transparent, pseudonymous transaction into an anonymous proof, decoupling identity from action. The core cryptographic primitive is the arithmetic circuit, which encodes the betting logic (e.g., stake <= balance, outcome ∈ {0,1}) into a format the ZK system can prove.
Setting up this layer involves three main technical phases. First, you define the circuit using a framework like Circom or SnarkJS. This circuit is the program that defines the constraints for a valid bet. For example, a simple circuit would take private inputs (secret bet, secret key) and public inputs (a commitment hash) and prove the secret bet hashes to the public commitment. Second, you perform a trusted setup to generate the proving and verification keys. For production, this often uses Perpetual Powers of Tau ceremonies to minimize trust. Finally, you integrate the generated verifier, typically a Solidity smart contract, into your betting dApp to validate submitted proofs.
Here is a simplified Circom circuit template for a private bet commitment. It proves the prover knows a preimage (bet, salt) that hashes to a publicly declared commitment, without revealing the bet itself.
circompragma circom 2.0.0; include "node_modules/circomlib/circuits/poseidon.circom"; template BetCommitment() { // Private signals (known only to prover) signal input bet; signal input salt; // Public signal (known to everyone/verifier) signal output commitment; component hasher = Poseidon(2); hasher.inputs[0] <== bet; hasher.inputs[1] <== salt; commitment <== hasher.out; } component main = BetCommitment();
After compiling this circuit, you use SnarkJS to generate proofs client-side and a verifier contract for on-chain validation.
The on-chain integration requires a verifier contract. After generating the verification key with SnarkJS (snarkjs zkey export verificationkey), you create a Solidity verifier (snarkjs zkey export solidityverifier). Your main betting contract stores public commitments and only processes a bet if a valid ZK-SNARK proof is provided. The user's client-side workflow is: 1) Generate a random salt, 2) Compute commitment = hash(bet, salt), 3) Send only the commitment to the contract, 4) Later, generate a ZK proof proving knowledge of the correct bet and salt for that commitment, 5) Submit the proof to resolve the bet. The contract never sees the actual bet data.
Key considerations for a production system include circuit complexity (larger circuits are more expensive to prove and verify), trusted setup hygiene, and client-side proof generation performance. Use zk-SNARK-friendly hash functions like Poseidon or MiMC within your circuit instead of Keccak (SHA-3). For Ethereum, verification gas costs can be significant; optimizing the circuit and using a Groth16 proving system (which has a constant-size proof) is standard. Libraries like zk-kit and SnarkJS provide essential tooling for this developer workflow, from circuit design to proof generation.
Essential Tools and Documentation
These tools and references are required to design, implement, and audit a ZK-SNARK layer for anonymous betting systems. Each card focuses on concrete software, libraries, or specifications used in production ZK applications today.
Solidity Verifiers and EVM Integration
On-chain verification is the enforcement layer for anonymous betting. ZK-SNARK verifiers are deployed as Solidity contracts that validate proofs without revealing private inputs.
Key implementation details:
- Groth16 verifiers typically cost ~210k gas per verification
- Public inputs include commitments, bet identifiers, and outcome hashes
- Verification contracts must be immutable and non-upgradable
Best practices:
- Generate verifiers directly from snarkjs or Halo2 tooling
- Separate verification logic from bet settlement logic
- Enforce replay protection using nullifiers or unique bet IDs
Most production systems deploy verifiers on Ethereum L1 or low-cost L2s such as Arbitrum or Optimism, depending on proof frequency.
Step 1: Designing the Betting Circuit in Circom
This guide details the first step in building a zero-knowledge betting system: creating the core arithmetic circuit in Circom that enforces game rules without revealing private inputs.
A ZK-SNARK circuit is a program that defines the constraints for a valid computation. For an anonymous betting application, this circuit must prove that a user knows a valid bet—such as a number within a range and a corresponding secret commitment—without revealing the bet itself. We use Circom (Circuit Compiler) to write this circuit because it compiles to R1CS (Rank-1 Constraint Systems), the standard format for SNARK proofs. The circuit's public inputs (e.g., a public commitment hash) and private inputs (e.g., the secret bet and a random nullifier) are declared as signals.
The core logic involves verifying a cryptographic commitment. A user commits to their bet by hashing it with a secret random value (a salt). The circuit must constrain that the provided private inputs, when hashed, match the public commitment. In Circom, this is done using a template. For example, a BetVerifier template would take private signals bet and salt, and a public signal commitment. It uses a built-in Poseidon hash component (common in ZK for efficiency) to compute hash = Poseidon([bet, salt]) and adds a constraint: hash === commitment.
Beyond the commitment, the circuit must enforce game-specific rules. For a simple number guess between 1 and 10, we add constraints to ensure the private bet signal is within that range: bet >= 1 and bet <= 10. We also include a nullifier to prevent double-spending the same commitment. The circuit outputs a public nullifierHash (e.g., Poseidon([salt])) that will be recorded on-chain. If the same nullifier appears twice, the second transaction is rejected, ensuring each bet is unique.
Here is a simplified code snippet for the circuit's main component:
circomtemplate BetCircuit() { // Signal declarations signal input bet; signal input salt; signal input commitment; signal output nullifierHash; // Constrain bet range (1-10) bet >= 1; bet <= 10; // Verify commitment hash component hash = Poseidon(2); hash.inputs[0] <== bet; hash.inputs[1] <== salt; commitment === hash.out; // Generate nullifier hash to prevent double-spend component nullifier = Poseidon(1); nullifier.inputs[0] <== salt; nullifierHash <== nullifier.out; }
The === and <== operators in Circom create the equality constraints that form the R1CS.
After writing the circuit, you compile it with circom BetCircuit.circom --r1cs --wasm --sym. This generates the R1CS file (the constraint system), a Witness generator (WebAssembly code to compute valid inputs), and debugging symbols. The R1CS is the blueprint for your ZK-SNARK. In the next step, a trusted setup ceremony will generate the proving and verification keys from this R1CS file, enabling users to generate proofs and the blockchain to verify them.
Step 2: Generating and Verifying Proofs with snarkjs
This step details the core cryptographic process: using the compiled circuit to create a zero-knowledge proof and verify it on-chain.
With your circuit compiled and the trusted setup ceremony complete, you can now generate a zero-knowledge proof for a specific instance of your betting logic. This process uses snarkjs to prove you know a valid set of private inputs (like your secret bet choice and the random salt) that satisfy the public circuit constraints, without revealing those inputs. The command snarkjs groth16 prove requires the proving key (circuit_0001.zkey), the witness file (witness.wtns) generated from your private inputs, and outputs a proof file (proof.json) and public signals file (public.json).
The public.json file contains the public signals, which are the outputs of your circuit that must be revealed for verification. For an anonymous betting application, this typically includes the hashed commitment (to check against a previously published value) and the resulting game outcome. The proof.json contains the actual cryptographic proof. You can verify the proof locally using snarkjs groth16 verify with the verification key, the proof, and the public signals to ensure it is valid before submitting an expensive on-chain transaction.
For on-chain verification, you must generate a Solidity verifier contract. Run snarkjs zkey export solidityverifier circuit_0001.zkey verifier.sol. This contract contains a verifyProof function that accepts the proof and public signals as uint256 arrays. The core of your betting smart contract will call this function. A successful verification confirms that the prover knows a secret pre-image to the published commitment and that the game logic was executed correctly, all without revealing the secret bet itself.
Integrating the verifier requires careful data formatting. Use snarkjs generatecall to create the correctly formatted calldata from your proof.json and public.json files. This string can be passed directly to the verifier contract's function. In your main betting contract, the verification call is the gatekeeper for prize distribution. Only a valid proof, which cryptographically links a winning outcome to the original hidden commitment, should trigger a payout, ensuring the system's fairness and anonymity.
Step 3: Building the Verifier Smart Contract
This step implements the on-chain logic that validates zero-knowledge proofs, ensuring bets are placed correctly without revealing private user data.
The verifier smart contract is the on-chain anchor of your ZK-SNARK system. Its sole purpose is to accept a cryptographic proof and public inputs, then execute a verification function. If the proof is valid, the contract executes the associated business logic—in this case, registering a bet. We'll write this contract in Solidity, targeting the EVM. The core of the contract will be a function, placeAnonymousBet(bytes calldata _proof, uint256[] calldata _publicInputs), which calls a pre-compiled verification key.
You cannot write the complex verification algorithm in Solidity directly. Instead, you use a verification key generated during the trusted setup of your ZK circuit (from Step 2). This key is hardcoded into the contract. For development, we use libraries like snarkjs to generate Solidity verifier code. Run snarkjs zkey export solidityverifier circuit_final.zkey Verifier.sol. This command creates a Verifier.sol contract with a verifyProof function, which your main betting contract will inherit from or call.
Your main contract, AnonymousBetting.sol, imports the generated Verifier contract. The placeAnonymousBet function must prepare the proof and public inputs in the exact format the verifier expects. The public inputs typically include commitments like a nullifier hash (to prevent double-spending) and a deposit root (to prove funds are in the pool), but not the user's identity or bet choice. A successful verification triggers the bet placement and emits an event with the nullifier, allowing for private off-chain tracking.
Security is paramount. The contract must enforce that the same nullifier cannot be reused (mapping mapping). It should also verify the proof's validity against the current valid deposit Merkle root. Since the verification key is immutable once deployed, any flaw in the circuit or setup is permanent. Thoroughly test the verifier contract on a testnet using proofs generated from your local circuit before considering a mainnet deployment. Use Foundry or Hardhat for comprehensive unit and fork tests.
ZK-SNARK Trust Assumptions and Trade-offs
Comparison of major ZK-SNARK proving systems for an anonymous betting application, focusing on trust, performance, and implementation complexity.
| Trust & Security Feature | Groth16 | PLONK | Halo2 |
|---|---|---|---|
Trusted Setup Required | |||
Universal Setup (Updatable) | |||
Proof Size | ~200 bytes | ~400 bytes | ~1 KB |
Proving Time | Fastest | Moderate | Moderate to Slow |
Verification Time | < 10 ms | < 50 ms | < 100 ms |
Recursive Proof Support | |||
Main Cryptographic Assumption | Pairing-based | Pairing-based | Inner Product Argument |
EVM Verification Gas Cost | ~450k gas | ~500k gas | ~600k gas |
Step 4: Integrating with a Prediction Market Contract
This step details how to connect a zero-knowledge proof system to a smart contract, enabling users to place anonymous bets on prediction market outcomes.
A ZK-SNARK layer for a prediction market acts as a privacy shield between the user and the on-chain contract. Instead of submitting a bet transaction that reveals your chosen outcome (e.g., voteForCandidateA), you generate a zero-knowledge proof. This proof cryptographically demonstrates that you have a valid, funded commitment for one of the possible outcomes without revealing which one. The core contract only needs to verify the proof's validity and the attached deposit, not the private data it conceals. This decouples the act of betting from the act of revealing your position.
The integration requires two main components: a verifier contract and a prover client. The verifier is a Solidity smart contract with a function, often named placeAnonymousBet(bytes calldata proof), that contains the SNARK verification key. Written in a circuit language like Circom or Noir, the prover client generates proofs based on user inputs: their secret nullifier, the selected outcome, and a Merkle proof of their commitment's inclusion in a deposit tree. Popular libraries like snarkjs or the noir_js package handle the heavy lifting of proof generation in the user's browser or backend.
Here is a simplified flow for a binary market (Yes/No):
- User Prepares Inputs: The user has already secured a note (commitment) in a deposit pool. They decide to bet "Yes."
- Generate Proof: The prover runs the circuit with private inputs (secret, outcome="Yes") and public inputs (market ID, deposit root). It outputs a proof.
- Contract Call: The user calls
placeAnonymousBet(proof)on the verifier contract. - On-Chain Verification: The contract's
verifyProoffunction checks the proof against its embedded verification key. If valid, it logs an event with the proof's public inputs and the user's nullifier hash (to prevent double-spending) but not the outcome.
Critical design considerations include nullifier schemes to prevent double-voting and commitment management. Each betting commitment must include a unique secret nullifier. When the proof is submitted, the hash of this nullifier is stored on-chain. Attempting to submit another proof derived from the same secret will produce the same hash, which the contract will reject. The deposit pool, often managed by a separate contract or a trusted setup, must allow users to generate Merkle proofs for their commitments, which are passed as public inputs to the ZK circuit for verification.
For developers, the primary challenge is ensuring the circuit logic perfectly mirrors the contract's business rules. The circuit must enforce that the prover knows a secret for a valid commitment and that the hidden outcome is one of the market's allowed options. Any mismatch between circuit logic and contract state (like an invalid market ID) will cause verification to fail. Testing requires a full stack: simulating proof generation off-chain and then running those proofs through the on-chain verifier in a local fork using tools like Hardhat or Foundry.
After integration, the prediction market operates with enhanced privacy. The public blockchain record shows only that an anonymous, valid bet was placed, not its direction. This enables applications like private voting, sensitive event forecasting, or institutional participation where revealing trading positions is undesirable. The final step involves building the user-facing application that seamlessly handles proof generation and transaction submission, abstracting the cryptographic complexity from the end-user.
Frequently Asked Questions
Common technical questions and solutions for developers implementing a ZK-SNARK privacy layer for on-chain betting applications.
ZK-SNARKs and ZK-STARKs are both zero-knowledge proof systems, but they have distinct trade-offs for a betting application.
ZK-SNARKs (Succinct Non-Interactive Arguments of Knowledge) require a trusted setup ceremony to generate a common reference string (CRS). They produce very small proofs (e.g., ~200 bytes) and have fast verification times, which is ideal for minimizing on-chain gas costs. This makes them suitable for frequent, low-value bets.
ZK-STARKs (Scalable Transparent Arguments of Knowledge) do not require a trusted setup, offering better trust assumptions. However, their proofs are larger (~45-200 KB) and verification is more computationally intensive, leading to higher gas fees. For a betting dApp where transaction cost is critical, ZK-SNARKs are often the pragmatic choice despite the setup requirement.
Key Takeaway: Choose SNARKs for gas efficiency; choose STARKs if eliminating trusted setup is your highest priority.
Conclusion and Next Steps
You have now implemented a foundational ZK-SNARK layer for an anonymous betting application. This guide covered the core workflow from circuit design to on-chain verification.
The primary goal of using ZK-SNARKs is to decouple the act of placing a bet from the act of revealing its outcome. Your BettingCircuit proves that a user knows a valid bet (e.g., a number and a secret) that hashes to a public commitment, without revealing the bet itself. This allows the frontend to submit only the proof and the public inputs to the verifier contract, preserving user privacy until a later reveal phase. The use of sha256 in the circuit, while common, is computationally expensive in ZK; for production, consider optimized hash functions like MiMC or Poseidon.
For next steps, focus on hardening and extending the system. First, audit your circuits and contracts. Use tools like snarkjs's r1csprint to inspect constraints and consider a formal verification service. Second, implement a secure commit-reveal scheme on-chain. The verifier contract should map commitments to proofs and only allow the original prover to later reveal the plaintext bet, linking it to the verified commitment. Third, integrate a trusted setup ceremony for your final circuit. While we used a local Phase 1 Powers of Tau file for testing, a production application requires a secure multi-party ceremony to generate the proving and verification keys.
To scale this system, explore recursive proofs (proofs of proofs) using a framework like Plonky2 or Halo2 to batch multiple bets into a single on-chain verification, drastically reducing gas costs. Also, consider the user experience: your frontend must securely generate the witness and proof, likely in a WebWorker to avoid blocking the UI. Libraries like SnarkJS in the browser or WebAssembly builds of Rust-based proving systems (e.g., arkworks) are essential here. Finally, monitor the evolving landscape of ZK-EVMs and coprocessors; platforms like zkSync Era or Starknet offer native ZK verification that could simplify your architecture.