# Niftyzk tutorial 2, The Commit-reveal scheme

By [strawberry](https://paragraph.com/@strawberry-52) · 2025-01-10

---

This tutorial will contain information about the generated code. this is the continuation of the previous tutorial, niftyzk tutorial 1. You should read that one first.

So let’s scaffold a new project using `niftyzk init` we will select a commit-reveal scheme with poseidon hash and add 2 inputs for tamper proofing, address and amount.

    $ niftyzk init
    
    Setting up your current directory
    ? What project do you want to scaffold? Commit-Reveal Scheme
    ? Choose the hashing algorithm to use:  poseidon
    ? Do you wish to add tamperproof public inputs? (E.g: walletaddress):  yes
    ? Enter the name of the public inputs in a comma separated list (no numbers or special characters):  address,amount
    Generating circuits
    Generating javascript
    Done
    Run npm install in your project folder
    

**The circuits directory**

Navigate to the `/circuits/` directory to see the generated code. It should contain 2 files, `circuits.circom` which is the entry point and `commitment_hasher.circom` which contains the hashing implementation.

`circuits.circom`

    pragma circom 2.0.0;
    include "./commitment_hasher.circom";
    
    template CommitmentRevealScheme(){
        // Public inputs
        signal input nullifierHash;
        signal input commitmentHash;
        signal input address;
        signal input amount;
        
        
        // private inputs
        signal input nullifier;
        signal input secret;
        
        // Hidden signals to validate inputs so they can't be tampared with
        signal addressSquare;
        signal amountSquare;
        
        component commitmentHasher = CommitmentHasher();
        
        commitmentHasher.nullifier <== nullifier;
        commitmentHasher.secret <== secret;
    
        // Check if the nullifierHash and commitment are valid
        commitmentHasher.nullifierHash === nullifierHash;
        commitmentHasher.commitment === commitmentHash;
    
        // An extra operation with the public signal to avoid tampering
    
        addressSquare <== address * address;
        amountSquare <== amount * amount;
        
    }
    
    component main {public [nullifierHash,commitmentHash,address,amount]} = CommitmentRevealScheme();
    

Let’s see from top to bottom what’s going on.

First, it’s using circom 2.0.0 and imports the commitment\_hasher.circom template

Next, as you can see on the bottom we have 4 public inputs, these inputs will be exposed to the blockchain when verifying a circuit there. `nullifierHash` is used for nullification inside the contract, you can use this variable to make sure the proof is not used twice. `commitmentHash` is the hash used for the commit-reveal scheme. We prove we know the preimage of this hash with the proof.`address` and `amount` are extra public inputs which are added into the circuit. the creator of the proof can add these inputs and submit the proof to the blockchain. A malicious relayer who acquires the proof is unable to modify these inputs if they are made tamper proof as you will see soon.

The private inputs are `secret` and `nullifier` , both must be kept private. They are used for nullification and the commit reveal scheme.

Hidden signals make sure our extra public inputs are unalterable after the proof is created.We create the commitment hasher and assert that the output includes the public inputs. That’s it. The commitment hasher looks like:

    pragma circom 2.0.0;
        
    include "../node_modules/circomlib/circuits/poseidon.circom";
    
    template CommitmentHasher(){
        signal input nullifier;
        signal input secret;
        
        signal output commitment;
        signal output nullifierHash;
    
            
        component commitmentPoseidon = Poseidon(2);
    
        commitmentPoseidon.inputs[0] <== nullifier;
        commitmentPoseidon.inputs[1] <== secret;
    
        commitment <== commitmentPoseidon.out;
    
        component nullifierPoseidon = Poseidon(1);
    
        nullifierPoseidon.inputs[0] <== nullifier;
    
        nullifierHash <== nullifierPoseidon.out;
    }
    

As you can see it combines the inputs and creates new outputs. the commitmentHash is linked to the nullifier, so it’s verifiable that they are related.You could use the commitmentHash for nullification, or keep it like this, it’s up to you. It was designed like this because if you want to have reusable commitments you can add an extra input `nonce` to the nullifierHash but keep the commitment as is. That way the owner of the secret can create multiple proofs using the same commitment with a slightly different nullification strategy. It’s useful for account abstraction if you want the account to be reusable.

**Javascript code**First we look at the library that was scaffolded and then the tests.

The project depends on `ffjavascript`,`snarkjs`,`circomlib`,`circomlibjs`,`circom_tester`

`lib/index.js` contains the source code for the client side code.

First you will the functions for generating circuit inputs:

    /**
     * @returns {bigint} Returns a random bigint
     */
    export function rbigint() { return utils.leBuff2int(crypto.randomBytes(31)) };
      
    

For the inputs, 31 byte sized bigint is used. The reason for 31 bytes is because if we use random 32 bytes it can be higher than the **snark scalar field**

`21888242871839275222246405745257275088548364400416034343698204186575808495617`

and that would make our circuits fail. So 31 bytes for max input. Next you see the hashing implementation which depends on what was selected and the `computeProof` and `verifyProof` functions that call snarkjs

`test/input.js`

Creating the input for a circuit is very important part of development. This file will be called when the unit tests run and also during hot reload.

    
    import { rbigint, generateCommitmentHash, generateNullifierHash,buildHashImplementation } from "../lib/index.js";
    
    /**
     * This is a test input, generated for the starting circuit.
     * If you update the inputs, you need to update this function to match it.
     */
    export async function getInput(){
        await buildHashImplementation();
        
        const secret = rbigint();
        const nullifier = rbigint();
        
        let address = rbigint();
        let amount = rbigint();
                    
        const commitmentHash = await generateCommitmentHash(nullifier, secret);
        const nullifierHash = await generateNullifierHash(nullifier);
        
        return {secret, nullifier, nullifierHash,commitmentHash,address,amount}
    }
    
    // Assert the output for hotreload by returning the expected output
    // Edit this to fit your circuit
    export async function getOutput() {
        return { out: 0 }
    }
    
            
    

The `getInput()` function defines our circuit input and the `getOutput` function is used for output assertion when running `niftyzk dev`

Let’s see what’s going on. First we build the hash implementation. It’s because all implementations are wasm and need to be built first. The built wasm is cached for reuse.

Then we assign random numbers for inputs, compute the hashes and return them. The return values are a mix of private and public inputs and they are fed to the circuit directly.

The current circuit template doesn’t have output signals, so the `getOutput` function is not implemented. You should return values you expect here after changing your circuit.

**Merkle tree**

Now let’s take a look at a commit-reveal scheme with a merkle tree

`niftyzk init` will can rebuild the current project into a new one.

    Setting up your current directory
    ? What project do you want to scaffold? Commit-Reveal Scheme with Fixed Merkle Tree
    ? Choose the hashing algorithm to use:  poseidon
    ? Do you wish to add tamperproof public inputs? (E.g: walletaddress):  yes
    ? Enter the name of the public inputs in a comma separated list (no numbers or special characters):  address,amount
    

Let’s set up one with poseidon hash and merkle tree

You will see that the `commitment_hasher.circom` file stayed the same but the `circuits.circom` changed and there is a new `merkletree.circom` file.

    include "./commitment_hasher.circom";
    include "./merkletree.circom";
    
    template CommitmentRevealScheme(levels){
        // Public inputs
        signal input nullifierHash;
        signal input commitmentHash;
        signal input address;
        signal input amount;
        
        signal input root;
        signal input pathElements[levels]; // The merkle proof which is fixed size, pathElements contains the hashes
        signal input pathIndices[levels]; // Indices encode if we hash left or right
        // private inputs
        signal input nullifier;
        signal input secret;
        
        // Hidden signals to validate inputs so they can't be tampared with
        signal addressSquare;
        signal amountSquare;
        
    
        component commitmentHasher = CommitmentHasher();
        
        commitmentHasher.nullifier <== nullifier;
        commitmentHasher.secret <== secret;
    
        // Check if the nullifierHash and commitment are valid
        commitmentHasher.nullifierHash === nullifierHash;
        commitmentHasher.commitment === commitmentHash;
    
        // An extra operation with the public signal to avoid tampering
    
        addressSquare <== address * address;
        amountSquare <== amount * amount;
        
    
        // Check if the merkle root contains the commitmentHash!
        component tree = MerkleTreeChecker(levels);
    
        tree.leaf <== commitmentHasher.commitment;
        tree.root <== root;
    
        for (var i = 0; i < levels; i++) {
            tree.pathElements[i] <== pathElements[i];
            tree.pathIndices[i] <== pathIndices[i];
        }
    
    
    }
    
    component main {public [nullifierHash,commitmentHash,root,address,amount]} = CommitmentRevealScheme(20);
    

So quite a few things changed. We got new public and private inputs and new templates.

So first we have the merkle tree `root` as a new public input and `pathElements` and `pathIndices` , these contain the merkle proof. The `levels` variable taken specifies the size of the merkle tree, the default 20 will give a lot of branches to work with.

The new template we use is the `MerkleTreeChecker` and we just assign the signals to it’s inputs. The MerkleTreeChecker will assert correctness. It’s assumed that the commitment is one of the leaves and the merkle proof is used for proving it. The `pathElements` contain the leaves and the `pathIndices` specify if we hash left or right.

Let’s see the other file:

    pragma circom 2.0.0;
    
    include "../node_modules/circomlib/circuits/poseidon.circom";
    
    template HashLeftRight(){
      signal input left;
      signal input right;
      signal output hash;
    
      component poseidonHash = Poseidon(2);
    
      poseidonHash.inputs[0] <== left;
      poseidonHash.inputs[1] <== right;
    
      hash <== poseidonHash.out;
    }
    
    
    
    // if s == 0 returns [in[0], in[1]]
    // if s == 1 returns [in[1], in[0]]
    template DualMux() {
        signal input in[2];
        signal input s;
        signal output out[2];
    
        s * (1 - s) === 0;
        out[0] <== (in[1] - in[0])*s + in[0];
        out[1] <== (in[0] - in[1])*s + in[1];
    }
    
    
    // Verifies that a merkle proof is correct for given root and leaf
    // pathIndices input in an array of 0/1 selectors telling whether 
    // given pathElement is on the left or right side of the merkle path
    template MerkleTreeChecker(levels) {
        signal input leaf;
        signal input root;
        signal input pathElements[levels];
        signal input pathIndices[levels];
    
        component selectors[levels];
        component hashers[levels];
    
        signal levelHashes[levels];
    
       levelHashes[0] <== leaf;
    
        for (var i = 1; i < levels; i++) {
            selectors[i] = DualMux();
            hashers[i] = HashLeftRight();
    
            selectors[i].in[1] <== levelHashes[i - 1];
            selectors[i].in[0] <== pathElements[i];
            selectors[i].s <== pathIndices[i];
    
            hashers[i].left <== selectors[i].out[0];
            hashers[i].right <== selectors[i].out[1];
            
            levelHashes[i] <== hashers[i].hash;
        }
        
        root === levelHashes[levels -1];
    }
    

Okay, so quite a few things going on. Let me explain.

First we have a hasher that hashes leaves from left to right. It’s uses the implementation we selected.

After this the `DualMux` is a Multiplexer that will just swap inputs. It works based on `pathIndices` and aligns the pathElements correctly so we can hash left to right for all cases.

The merkle tree checker takes inputs and computes the merkle `root` by combining `pathElements`

I hope this saves you a lot of time developing your circuits.now lets jump to javascript:

`lib/merkletree.js`

The important changes you will find in a new file, merkletree.js which now contains functions to work with merkle trees.It’s zero extra dependency and everything is implemented from scratch. You can run it in any environment.

You will see new functions to call, `generateMerkleTree`,`generateMerkleProof`

You need to supply an array of leaves which are commitments and call `generateMerkleTree` to obtain the tree. Once you have a commitment you want to create a proof for, you can call `generateMerkleProof`

To use the merkle proof as pathElements and pathIndices you call `encodeForCircuit`

**Important**! Trees with uneven leaves are padded! The padding works by duplicating the last branch. This means that the merkle tree contains duplicate leaves but it never contains zeros. Other padding methods could pad with zeroes, so could use the hash(0) for a leaf, that implementation would be insecure on the blockchain.Every uneven level is padded. If we have 9 leaves on the first level, then the last leaf is duplicated etc.

**Merkle tree commands**

The project gives you a few commands to work with from the CLI to interact with merkle trees manually.There are many use-cases, for example if you want to manage a tree for withdrawing airdrops, you might just manipulate it manually.

`lib/run.js` contains that gives you 3 commands, new, proof, verify, you can access them using npm also.

`npm run new` will create a new merkle tree with a similar output:

    CREATING A NEW MERKLE TREE
    
    Enter how many secrets would you like to generate:
    8
    Generating secrets and hashing commitments. Please wait!
    Done
    Generating merkle tree from commitments!
    Done.Root is 9399890428704023149822996752765634449585627060469866502272705510954136382993 
    Serializing data.
    Writing to file.
    Done
    

Now you will see it created a `private` and a `public` directory.

The public directory contains the merkle tree and commitment hashes, nullifier hashes and the private contains secrets used to compute them.The files are named using the root hashes.

`npm run proof`

The command will ask you for a root hash and a commitment to verify. It will split out a JSON which contains the merkle proof. I do not copy it here due to it’s size.

`npm run verify`

This command will ask you for the merkle root and the proof and verifies the proof.

**You are free to modify the merkle tree tooling to adjust it to work with your circuits**

---

*Originally published on [strawberry](https://paragraph.com/@strawberry-52/niftyzk-tutorial-2-the-commit-reveal-scheme)*
