DEV Community

Cover image for Niftyzk tutorial 2, The Commit-reveal scheme
Peter Horvath
Peter Horvath

Posted on

Niftyzk tutorial 2, The Commit-reveal scheme

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
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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. commitmentHashis 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;
}

Enter fullscreen mode Exit fullscreen mode

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)) };

Enter fullscreen mode Exit fullscreen mode

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 verifyProoffunctions 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 }
}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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);


Enter fullscreen mode Exit fullscreen mode

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];
}

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)