Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
Free 30-min Web3 Consultation
Book Consultation
Smart Contract Security Audits
View Audit Services
Custom DeFi Protocol Development
Explore DeFi
Full-Stack Web3 dApp Development
View App Services
LABS
Guides

How to Design a ZK-SNARKs Circuit for Private User Behavior Tracking

This guide walks through constructing a zero-knowledge proof circuit to compute aggregate user engagement metrics without revealing individual user data. We'll use Circom and SnarkJS with a practical example.
Chainscore © 2026
introduction
PRIVACY ENGINEERING

How to Design a ZK-SNARKs Circuit for Private User Behavior Tracking

This guide explains how to build a ZK-SNARK circuit that proves a user performed specific on-chain actions without revealing their identity or the details of their transactions.

Private analytics with ZK-SNARKs (Zero-Knowledge Succinct Non-Interactive Argument of Knowledge) allows protocols to verify aggregate user behavior while preserving individual privacy. The core idea is to design a circuit that takes private inputs (like a user's transaction history) and public parameters (like a protocol's rules) and outputs a proof. This proof cryptographically attests that the user's actions satisfy certain conditions—such as making a minimum number of trades or holding a specific NFT—without leaking which transactions were involved or the user's wallet address. This enables use cases like private airdrops, loyalty programs, and credit scoring.

Designing the circuit begins with defining the computational statement you want to prove. For user behavior tracking, this is often a predicate over a set of private transactions. For example, you might want to prove: "I executed at least 5 swaps on Uniswap V3 on the Ethereum mainnet in the last 30 days, with a total volume over $10,000." The circuit's public inputs (the instance) would include the threshold values (5, 30 days, $10000) and a public commitment to the user's identity. The private inputs (the witness) would be the raw, off-chain transaction data.

You must then translate this logical statement into arithmetic constraints, typically using a framework like Circom or SnarkJS. The circuit code doesn't process the transaction data directly; instead, it verifies cryptographic attestations of that data. A common pattern is to have the user provide a Merkle proof that their transaction is included in a publicly available data set (like an on-chain event log), then verify the transaction's properties within the circuit. The circuit would check the Merkle proof validity, confirm the transaction timestamp is within the range, and increment a private counter for valid transactions.

Here is a simplified conceptual outline of the circuit logic in pseudocode:

code
// Public Inputs
public threshold_count = 5;
public merkle_root;

// Private Inputs (Witness)
private tx_data[10]; // Array of transaction details
private merkle_paths[10]; // Merkle proofs for each tx

// Circuit Logic
signal total_valid = 0;
for (let i =0; i < 10; i++) {
    // 1. Verify the tx is committed to in the public Merkle tree
    assert(verifyMerkleProof(merkle_root, tx_data[i].hash, merkle_paths[i]));
    // 2. Verify the transaction meets behavioral criteria (e.g., is a Swap on Uniswap V3)
    assert(tx_data[i].protocol == "Uniswap V3");
    assert(tx_data[i].action == "Swap");
    // 3. If valid, increment the counter
    total_valid += 1;
}
// 4. Output the final assertion
assert(total_valid >= threshold_count);

The actual circuit would require more precise constraints for date ranges and value comparisons.

After compiling the circuit (e.g., with circom), you generate a proving key and a verification key. The user runs the prover with their private witness to generate a ZK proof. They then submit only this proof and the public inputs to the verifier contract on-chain. The contract checks the proof against the verification key and the public thresholds. If valid, the contract can mint a reputation NFT or trigger a reward, all without learning anything about the user's specific transaction history. This decouples proof of behavior from identity.

Key considerations for production include circuit size and cost. Each constraint adds to the proving time and the gas cost for on-chain verification. Using efficient primitives for hashing (Poseidon) and comparisons is critical. Furthermore, the system's trust model depends on the integrity of the data source (the Merkle root). This root must be updated in a trusted manner, often by a decentralized oracle or a protocol that commits its event logs to a data availability layer. Tools like Semaphore and ZK-Kit offer libraries for common privacy-preserving constructs like anonymous authentication, which can be integrated into such analytics circuits.

prerequisites
ZK-SNARK CIRCUIT DESIGN

Prerequisites and Setup

Before building a ZK-SNARK circuit for private user behavior tracking, you need to establish a foundational development environment and understand the core cryptographic primitives involved.

The first prerequisite is a working knowledge of zero-knowledge proof (ZKP) fundamentals. You should understand the roles of the prover and verifier, the concept of a computational statement expressed as an arithmetic circuit, and the basic properties of ZK-SNARKs: succinctness, non-interactivity, and zero-knowledge. Familiarity with finite field arithmetic and elliptic curve cryptography is essential, as these are the mathematical backbones of the proving systems. For a practical introduction, read the Zcash Protocol Specification.

Next, set up your development toolchain. You will need a circuit-writing framework like Circom 2.1.7 or Halo2. This guide uses Circom, a popular domain-specific language for defining arithmetic circuits. Install it via npm (npm install -g circom) and ensure you have Rust and Cargo installed for compiling the associated proving system, snarkjs. You will also need a project structure to organize your circuit files (circuits/), build scripts, and test vectors.

Your circuit will process private user data, so you must define the public and private inputs precisely. For behavior tracking, a public input could be a commitment to a user's action hash, while private inputs would be the raw action data and a secret user identity. Decide on the data structure (e.g., a Merkle tree leaf) and the hash function (e.g., Poseidon, which is ZK-friendly). You'll model constraints to ensure the private inputs generate the correct public commitment without revealing them.

Finally, prepare for the trusted setup phase, a critical one-time ceremony for Groth16, a common SNARK system. You will generate a Proving Key and Verification Key using a Phase 1 Powers of Tau contribution and a Phase 2 circuit-specific setup. While you can use a test setup locally, production applications require a secure multi-party computation (MPC) ceremony. Tools like snarkjs provide commands (snarkjs powersoftau new, snarkjs plonk setup) to manage this process in development.

key-concepts-text
CORE CONCEPTS

How to Design a ZK-SNARKs Circuit for Private User Behavior Tracking

Learn to build a zero-knowledge circuit that proves a user performed specific actions without revealing the underlying data, a foundational technique for privacy-preserving analytics and identity.

A ZK-SNARK circuit for private tracking proves a statement about user behavior while keeping the raw data confidential. The core components are private inputs, public signals, and constraints. Private inputs are the secret data only known to the prover, such as a user's click history or time spent on a page. Public signals are non-sensitive data known to both prover and verifier, like a public commitment hash or a threshold value. Constraints are the mathematical relationships, written in a domain-specific language like Circom, that define the valid computations linking private inputs to public outputs.

Design begins by defining the claim. For example: "A user visited at least 5 pages on a site, spending over 30 seconds on each." You must identify what can be public (e.g., the total count 5 and duration 30) and what must stay private (the specific page IDs and exact timestamps). The public signals become the verification key's expected outputs. In Circom, you declare these with signal input private and signal output public statements. The circuit's trustlessness depends on correctly classifying data; a leak of private inputs into public signals breaks the privacy guarantee.

Constraints encode the logic. For our example, you'd create private signals for each page's visitDuration. A constraint would assert each duration is greater than 30: duration[i] > 30. Another component would sum a private flag for each valid visit and constrain that sum to equal the public output totalValidVisits. This is where hashing is critical: the private page IDs might be hashed into a Merkle tree leaf, and a public root serves as the commitment. The circuit proves the user knows a leaf (the private data) belonging to the committed tree without revealing the leaf itself.

Consider a concrete implementation outline using the Circom library. You'd instantiate a template, perhaps named ValidVisitProof, with private inputs for leaf, pathElements, pathIndices (for the Merkle proof), and an array of durations. Public outputs would be the root and thresholdMet. Inside, constraints would verify the Merkle inclusion, compare each duration to the threshold using the GreaterThan comparator, and aggregate results. The final circuit output is a proof that the secret data satisfies all constraints relative to the public root.

Testing and security are paramount. Use tools like circom and snarkjs to compile the circuit, generate a trusted setup, and create proofs with sample data. Audit constraints for underflows, overflows, and unintended disclosures. A common pitfall is using a constraint like a - b == 0 to assert equality; this can fail for negative numbers. Instead, use IsEqual or IsZero components. The circuit must be deterministic: for the same inputs, it must always produce the same public signals, otherwise verification will fail.

This design pattern enables use cases like private ad conversion tracking, anonymous credential systems, and privacy-preserving loyalty programs. The circuit is deployed as a verifier contract on-chain. Users generate proofs locally from their private data and submit the proof and public signals. The contract verifies the proof, trusting only the cryptography, not the user's data. This shifts the paradigm from "trust us with your data" to "trust the math," enabling compliance with regulations like GDPR while maintaining utility.

circuit-design
CIRCUIT DESIGN

Step 1: Defining the Circuit Logic and Signals

The first step in building a ZK-SNARK for private analytics is to formally define the computational problem your circuit will prove. This involves specifying the private inputs, public parameters, and the logical constraints that represent valid user behavior.

A ZK-SNARK circuit is a set of arithmetic constraints that define a correct computation. For private user behavior tracking, the circuit's logic must encode the rules of valid behavior without revealing the raw data. You start by identifying the signals, which are the circuit's variables. There are three types: private inputs (e.g., a user's secret ID and action history), public inputs (e.g., a commitment to the user's state or a nullifier), and intermediate signals (computed values used within the circuit).

Consider a simple example: proving you performed a specific sequence of actions without revealing which ones. Your private signals could be an array actions[5] where each element is 0 or 1. The public signal could be a commitment C to this array. The circuit logic must prove two things: that you know a preimage of C, and that the sum of actions equals a public threshold (e.g., 3). This creates a constraint system: actions[0] + actions[1] + ... + actions[4] == 3 and hash(actions, salt) == C.

You define these constraints using a domain-specific language like Circom or Zokrates. In Circom, you would declare templates for your logic. For the summation check, you might create a component that iterates and accumulates. For the hash, you would use a pre-built template like Poseidon for efficient ZK-friendly hashing. The key is to ensure every logical condition in your application translates into an arithmetic relationship (equality, inequality, boolean logic) between signals.

Common patterns in behavior-tracking circuits include: range checks (e.g., timestamp within a window), merkle tree inclusion proofs (to prove membership in an anonymous set), and state transition validity (proving a new state is derived correctly from an old one). Each constraint increases the circuit size, directly impacting the cost of proof generation. Therefore, logic must be designed to be as efficient as possible.

Finally, you must rigorously test the circuit logic with various inputs using the framework's testing utilities before proceeding to compilation and setup. A flawed constraint definition is the most common source of security vulnerabilities, as it may allow a prover to generate a valid proof for invalid behavior. The circuit definition is the foundation; everything that follows depends on its correctness and precision.

writing-circom-code
CIRCUIT DESIGN

Step 2: Writing the Circom Template

This section details the process of designing a ZK-SNARK circuit in Circom to prove a user's behavior meets specific criteria without revealing the underlying data.

A Circom template is the core building block of a ZK-SNARK circuit, defining a function that can be instantiated multiple times. For private user behavior tracking, the circuit's primary job is to verify that a user's actions satisfy predefined rules. The circuit takes private inputs (the user's secret data) and public inputs (the rules or constraints to be proven) and outputs a single signal, typically 1, if all constraints are satisfied. This output is what the zk-SNARK proof will attest to, allowing a verifier to trust the statement is true without seeing the private inputs.

Start by defining the template's interface using the signal input and signal output keywords. For a behavior tracking circuit, private inputs might include a user's daily step count or transaction history, while public inputs could be a target threshold. For example, a template to prove a user took more than 10,000 steps in a day would have a private input steps and a public input threshold. The core logic uses constraints to encode the business rule, such as steps > threshold. In Circom, constraints are expressed using the <== operator, which asserts an equality while generating the corresponding Rank-1 Constraint System (R1CS).

Here is a basic Circom template for the step-count example:

circom
template StepGoal() {
    // Private input: the actual user data
    signal input steps;
    // Public input: the goal to compare against
    signal input threshold;
    // Output signal: 1 if the goal is met
    signal output met;

    // Intermediate signal to check if steps > threshold
    signal isGreaterThan;

    // Component to compare two numbers
    component comparator = GreaterThan(32); // Assume 32-bit numbers
    comparator.in[0] <== steps;
    comparator.in[1] <== threshold;
    isGreaterThan <== comparator.out;

    // Constraint: output is 1 if and only if steps > threshold
    met <== isGreaterThan;
}

This circuit uses a GreaterThan component (which must be defined or imported) to perform the comparison without revealing steps.

Designing for real-world behavior tracking often requires more complex logic. You may need to aggregate data over time (e.g., weekly totals), enforce multiple conditions, or verify the correctness of hashed commitments. A common pattern is to have the user commit to their data off-chain using a hash (like MiMC or Poseidon), then provide the preimage and the commitment as private inputs. The circuit can then verify the hash matches, proving the data's integrity before applying behavioral rules. This combines data attestation with logic verification in a single proof.

When writing constraints, remember they must be deterministic and finite-field arithmetic. Circom operates over a prime field, so direct comparisons like > are not native; they require auxiliary components like GreaterThan or LessThan from a library such as circomlib. Avoid non-quadratic constraints or loops with dynamic bounds, as they are not supported. Always test your template with the Circom compiler (circom circuit.circom --r1cs --wasm) to ensure it compiles correctly and produces the expected constraints.

Finally, consider the trusted setup implications. Every unique circuit topology requires its own Phase 1 Powers of Tau ceremony and Phase 2 circuit-specific setup. Therefore, finalize your circuit logic before proceeding. A well-designed template should be modular, reusing components for common operations to keep the constraint count manageable, which directly impacts proof generation time and verification gas costs on-chain.

compiling-testing
IMPLEMENTATION

Step 3: Compiling the Circuit and Generating a Proof

This step transforms your circuit logic into a format the prover can use, and then generates a zero-knowledge proof of the computation.

With your circuit logic defined in a framework like Circom or Halo2, the next step is compilation. This process converts your high-level constraints into a Rank-1 Constraint System (R1CS) or a Plonkish arithmetization. The compiler performs several critical tasks: it flattens the circuit into a set of arithmetic gates, generates the witness (the set of all signals for a given input), and produces the proving and verification keys. For our private behavior tracking circuit, compiling with circom circuit.circom --r1cs --wasm --sym would output the R1CS file (circuit.r1cs), a WebAssembly witness generator (circuit.wasm), and a symbol file for debugging.

The proving key and verification key are generated in a trusted setup ceremony specific to your circuit. The proving key is used by the prover to generate proofs, while the much smaller verification key allows anyone to check a proof's validity. For development, you can use a Powers of Tau ceremony output or a local, insecure setup. In production, a multi-party ceremony like Perpetual Powers of Tau is essential for security. These keys are deterministic based on the circuit's R1CS; the same circuit will always produce the same keys.

To generate a proof, you must first compute a valid witness. This is the set of all signal values—including private inputs, public inputs, and intermediate calculations—that satisfy all the circuit's constraints. Using the compiled WebAssembly module, you provide the private user data (e.g., {action: 5, timestamp: 12345, secret: 9876}) and the public parameters. The module calculates the full witness. A valid witness is the proof that you know a set of private inputs that make the circuit output 1 (true).

Finally, you pass this witness and the proving key to a proving system like Groth16 or PLONK. The prover performs complex cryptographic operations (elliptic curve pairings, polynomial commitments) to generate a ZK-SNARK proof. This proof is a small, constant-sized string (e.g., ~200 bytes for Groth16) that cryptographically attests to the statement: "I know some private user behavior data that, when processed by the circuit logic, results in a valid, non-revealing commitment." The proof contains no information about the private inputs themselves.

The output of this step is two-fold: the proof (proof.json) and the public signals (public.json). For our tracking example, the public signals would include only the output commitment hash and any public thresholds. You can now send this proof and public signals to a verifier. The verifier uses the circuit's verification key and the public signals to check the proof in milliseconds, confirming the truth of the statement without learning the underlying user behavior.

PERFORMANCE ANALYSIS

Circuit Optimization Techniques and Trade-offs

Comparison of common optimization strategies for ZK-SNARK circuits, focusing on their impact on prover time, proof size, and developer complexity.

Optimization TechniqueR1CS (Groth16)Plonkish (Plonk/Halo2)AIR (STARKs)

Prover Time Reduction

~10-30%

~40-70%

~60-90%

Proof Size Impact

~1-3 KB (fixed)

~5-10 KB (flexible)

~40-100 KB (large)

Trusted Setup Required

Custom Gate Support

Recursion Support

Complex (wrapping)

Native (Plonk)

Native (FRI)

Developer Complexity

High

Medium

Low-Medium

Memory Usage (RAM)

High

Medium

Low

Suitable for Large States

onchain-verification
ON-CHAIN VERIFICATION

Step 4: Verifying the Proof on Ethereum

This final step involves deploying a verifier smart contract to the Ethereum mainnet to allow anyone to publicly and trustlessly confirm the validity of a ZK-SNARK proof for private user behavior tracking.

The core of on-chain verification is a verifier smart contract. This contract contains the verification key generated during the trusted setup (Step 2) and a single public function, typically named verifyProof. This function accepts the proof generated by the prover (Step 3) and the public inputs (e.g., a commitment hash of the user's actions). The contract's logic, often implemented using a library like snarkjs or the circom toolkit, performs the elliptic curve pairing checks to cryptographically validate that the proof is correct without revealing the private inputs.

Deploying this contract is a critical and costly one-time operation. The verification key is large and must be stored in the contract's immutable storage, leading to high gas costs. For example, a Groth16 proof verification for a moderately complex circuit can cost over 300,000 gas. Developers must optimize the circuit and the verifier code to minimize these costs. The deployed contract address becomes the authoritative source for proof verification for your specific application.

To verify a proof, an off-chain client (like a dApp frontend) calls the contract's verifyProof function with the required parameters: _proof (the serialized proof data) and _publicSignals (an array of public inputs). The contract executes its verification routine and returns a boolean. A return value of true cryptographically guarantees that the prover knows a valid witness (the private user behavior data) that satisfies all the constraints of the original circuit, and that the public outputs are correctly derived from it.

This mechanism enables powerful applications. A DAO could use it to verify a member voted in a private poll without revealing their choice. A gaming protocol could confirm a player achieved a high score fairly without exposing their strategy. The trustlessness comes from the soundness property of ZK-SNARKs: it's computationally infeasible to generate a valid proof for a false statement. The Ethereum blockchain acts as the immutable, decentralized judge.

For developers, integrating verification involves using the snarkjs JavaScript library to generate the Solidity verifier contract from your .zkey file. The command snarkjs zkey export solidityverifier circuit_final.zkey verifier.sol creates the contract. You then compile and deploy this file using Hardhat or Foundry. Always test verification extensively on a testnet like Goerli or Sepolia before a mainnet deployment due to the irreversible gas expenditure.

Post-deployment, your system's workflow is complete: 1) A user generates a ZK-SNARK proof locally from their private data, 2) They submit only the proof and public signals to the blockchain, and 3) Any observer can query your verifier contract to confirm the proof's validity. This preserves user privacy while providing a cryptographic guarantee of honest behavior, a foundational pattern for private on-chain systems.

ZK-SNARKs CIRCUIT DESIGN

Frequently Asked Questions

Common technical questions and solutions for developers building ZK-SNARK circuits for private user behavior tracking.

Designing a circuit for a sequence of user actions requires careful state management. You must define a state transition function within the circuit constraints. A common pattern is to use a Merkle tree or a hash chain to represent the evolving state.

Key Components:

  • Private Inputs: The new event data (e.g., hashed action type, timestamp).
  • Public Inputs: The previous state root/commitment and the new state root/commitment.
  • Circuit Logic: Constrains that the new state is correctly computed from the old state and the private event. It must also verify the user's authorization (e.g., via a signature) to perform the state update.

Example with a hash chain:

circom
// Pseudo-Circom for a hash chain update
template UserActionTracker() {
    signal input oldCommitment;
    signal input newActionHash;
    signal input userSignatureValid; // 1 if valid
    signal output newCommitment;

    // Constraint: new commitment = hash(oldCommitment, newActionHash)
    component hash = Poseidon(2);
    hash.in[0] <== oldCommitment;
    hash.in[1] <== newActionHash;
    newCommitment <== hash.out;

    // Enforce that the signature check passed
    userSignatureValid * (1 - userSignatureValid) === 0;
    userSignatureValid === 1;
}

The prover demonstrates they know a series of valid actions leading to the final public state, without revealing the actions themselves.

conclusion
IMPLEMENTATION SUMMARY

Conclusion and Next Steps

This guide has walked through designing a ZK-SNARK circuit for private user behavior tracking, from defining constraints to generating proofs.

You have now built a foundational circuit for proving knowledge of a user's behavior—like completing specific on-chain actions—without revealing the underlying data. The core components are the private inputs (user's secret identifier and action data), public inputs (a commitment or nullifier), and the constraints that cryptographically link them. This allows a dApp to verify a user is eligible for a reward or has achieved a milestone based on provable, private criteria. The next step is to integrate this proof system into a full application stack.

For production, you must move beyond the conceptual circom example. First, select a proving system and backend. zk-SNARK libraries like snarkjs with the Groth16 prover are common for Ethereum, while zk-STARKs (via StarkWare's cairo-lang) or PLONK-based systems (like those in halo2) offer different trade-offs in trust setup and proof size. You'll need to handle the trusted setup ceremony for SNARKs or configure a STARK prover/verifier. The verification key and smart contract verifier must be deployed to your target chain.

The user-facing client application must securely generate proofs. This is typically done in a browser using WebAssembly builds of your circuit and proving library, or via a dedicated proof server for complex computations. The flow is: 1) The user's client fetches the necessary public signals (e.g., a Merkle root of eligible users). 2) It uses the private witness data to generate a proof locally. 3) It submits the proof and public signals to the verifier contract. Ensure private keys and witness data never leave the user's device.

Consider these advanced design patterns for robust systems. Use semaphore-style nullifiers to prevent double-spending of anonymous actions. Implement time-locks or action sequences within your circuit logic. For data availability, you may need to post public commitments to a data availability layer like Celestia or EigenDA. Always audit your circuit with tools like picus or ecne and the final contract with dedicated security firms. The circom repository's circuits directory and the zkopru project offer excellent real-world references.

The field of ZK application design is rapidly evolving. To continue learning, explore the documentation for noir for alternative circuit languages, or zksync's zksync-circuits for layer-2 specific examples. Participate in the 0xPARC learning community and review the code for privacy applications like tornado-cash-nova or zk-email. Start by forking a simple template, such as a zk-proof-of-membership demo, and incrementally modify the circuit logic to track a specific, useful behavior for your application.

How to Build a ZK-SNARK Circuit for Private Analytics | ChainScore Guides