npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@prifilabs/zk-toolbox

v1.0.0

Published

Zero-Knowledge Proof Toolbox

Readme

Zero-Knowledge Proof Toolbox

This library explores practical design patterns using zero-knowledge proofs, with a focus on ZK-SNARKs. Rather than diving deep into the mathematical foundations, it adopts a hands-on approach to demonstrate how zero-knowledge techniques can be applied to real-world software scenarios.

It's intended for anyone interested in learning how to use ZK-SNARKs through practical examples. Start by reviewing the examples below to understand each use case, then dive into the source code, especially the ZK circuits written in CIRCOM, to explore how the proofs are constructed.

For a more details about those proofs, check our HandsOnZkProofs website.

I'm Thierry Sans, an Associate Professor at the University of Toronto and co-founder of PriFi Labs. I built this library as a teaching tool to illustrate concrete use cases of zero-knowledge proofs for my students and the broader developer community.

Table of Contents

Install

npm install zk-toolbox

This package includes a post-install script that automatically downloads the zk-SNARK witness files needed to create proofs. Those files were generated using the Perpetual Power of Tau (ppot_0080). If you want to recompile them, use the Makefile from the Github repository (see the Contribute section below).

Security Disclaimer

⚠️ This library involves cryptography and zero-knowledge proofs. It has not been audited and may contain undiscovered vulnerabilities. Do not use it in production, in security-critical systems, or to protect sensitive data without a comprehensive third-party audit. Use at your own risk. We welcome community review and feedback.

Known Bug

When generating a proof, the program does not terminate automatically. This behavior is most likely caused by snarkjs leaving behind a lingering WebAssembly worker.

As a workaround, the examples in this library explicitly call process.exit(0) to ensure the process exits cleanly after proof generation.

Using the Library

This section walks through the various zero-knowledge proof design patterns demonstrated in this library. Each example illustrates a different use case and shows how ZK-SNARKs can be applied in practice.

Proof Of Commitment

In Proof of Commitment, the prover demonstrates knowledge of a secret preimage corresponding to a public hash value, without revealing the secret itself. This proof uses the secret as a private input and a nonce as a public input, producing two public outputs:

  • secretHash: the hash of the secret
  • authHash: a hash binding of the secret and the nonce to prevent replay attacks and/or to tie the proof to a specific action or time frame (i.e context binding)

Step 1: Generate Inputs

import { ProofOfCommitment, randomBigInt32ModP, poseidon } from "@prifilabs/zk-toolbox";

const privateInputs = { secret: randomBigInt32ModP() };
const publicInputs = { nonce: randomBigInt32ModP() };

Step 2: Generate the Proof

const proofOfCommitment = new ProofOfCommitment();
const { proof, publicOutputs } = await proofOfCommitment.generate(privateInputs, publicInputs);

Step 3: (Optional) Verify Output Correctness

This step checks that the public outputs match expected values based on the inputs. It’s not required for verification but useful for debugging and understanding.

const secretHash = poseidon([ privateInputs.secret ]);
console.assert(publicOutputs.secretHash == secretHash);
const authHash = poseidon([ privateInputs.secret, publicInputs.nonce  ]);
console.assert(publicOutputs.authHash == authHash);

Step 4: Verify the Proof

Only the proof, public inputs, and public outputs are required for verification, never the private inputs.

const res = await proofOfCommitment.verify(proof, publicInputs, publicOutputs);
console.assert(res);
process.exit(0);

Proof Of Membership

In Proof of Membership, the prover demonstrates that they know a secret which is part of a larger set, without revealing which specific element it is.

In addition, this proof includes a nullifier scheme designed to prevent double-use of the same secret in multiple proofs. A nullifier is a cryptographic commitment derived from the secret that can be published to signal that the secret has been used, without revealing the secret itself. This mechanism is common in anonymous systems like mixers or anonymous voting, where a prover should only be able to generate a valid proof once per secret, without revealing their identity.

To enable this, we define a commitment as the hash of the tuple (secret, nullifier). This binds the secret to its nullifier in a way that maintains privacy but enforces uniqueness: the same secret cannot be reused without producing the same commitment.

This proof leverages Merkle proofs, a cryptographic technique for efficiently verifying membership of a commitment in a set represented as a Merkle tree, without revealing which commitment it is. In essence, Proof of Membership is a zero-knowledge proof of a Merkle inclusion proof.

The proof inputs and outputs are:

  • Private input:

    • secret: the value whose membership is being proven
  • Public inputs:

    • commitments: a list of commitments in the set that will be the leaves of the Merkle tree
    • nullifier: the public value that wil be tied to secret as the commitment
    • nonce: a contextual binding value
  • Public output:

    • authHash: a hash of the secret, the nullifier and nonce, preventing replay attacks and enabling context binding
import { ProofOfMembership, randomBigInt32ModP, poseidon } from "@prifilabs/zk-toolbox";

let secret, nullifier, commitment;
const commitments = [];
const random = Math.floor(Math.random() * 100);
for (let i =0; i<100; i++){
	const s = randomBigInt32ModP();
	const n = randomBigInt32ModP();
	const c = poseidon([s, n]);
	if (random == i){
		secret = s;
		nullifier = n;
		commitment = c;
	}
	commitments.push(c);
}
const nonce = randomBigInt32ModP();
const privateInputs = { secret };
const publicInputs = { commitments, nullifier, nonce };

const proofOfMembership = new ProofOfMembership();
const { proof, publicOutputs } = await proofOfMembership.generate(privateInputs, publicInputs);

console.assert(publicOutputs.authHash == poseidon([privateInputs.secret, publicInputs.nullifier, publicInputs.nonce]));

const res = await proofOfMembership.verify(proof, publicInputs, publicOutputs);
console.assert(res);
process.exit(0);

Proof Of Exclusion

Proof of Exclusion is the complement of Proof of Membership: it allows a prover to demonstrate that a specific commitment they know is not included in a public set of commitments, without revealing the actual value.

This type of proof is especially useful in privacy-preserving systems, such as cryptocurrency mixers, where users may want to prove they are not associated with certain entries. For instance, mixers are often used to anonymize transactions, but some deposits may originate from illicit sources. A Proof of Exclusion enables users to prove that their withdrawal is not linked to any "tinted" or flagged deposits—without exposing which deposit they actually own.

In systems that combine both Proof of Membership and Proof of Exclusion, users can:

  • Prove they are authorized to act (e.g., by proving inclusion in a deposit set)
  • Simultaneously demonstrate that their deposit is not part of a restricted or flagged set

The proof inputs and outputs are:

  • Private input:

    • secret: the value whose membership is being proven
  • Public inputs:

    • commitments: the list of known "tinted" or flagged commitments
    • nullifier: the public value that wil be tied to secret as the commitment
    • nonce: a contextual binding value
  • Public output:

    • authHash: a hash of the secret, the nullifier and nonce, preventing replay attacks and enabling context binding
import { ProofOfExclusion, randomBigInt32ModP, poseidon } from "@prifilabs/zk-toolbox";

const commitments = [];
for (let i =0; i<100; i++){
	commitments.push(poseidon([randomBigInt32ModP()]));
}
const secret = randomBigInt32ModP();
const nullifier = randomBigInt32ModP();
const nonce = randomBigInt32ModP();
const privateInputs = { secret };
const publicInputs = { commitments, nullifier, nonce };

const proofOfExclusion = new ProofOfExclusion();
const { proof, publicOutputs } = await proofOfExclusion.generate(privateInputs, publicInputs);

console.assert(publicOutputs.authHash == poseidon([privateInputs.secret, publicInputs.nullifier, publicInputs.nonce]));

const res = await proofOfExclusion.verify(proof, publicInputs, publicOutputs);
console.assert(res);
process.exit(0);

Binding Proof of Membership with Proof Of Exclusion

A proof of exclusion can be linked to a proof of membership to enables powerful privacy-preserving statements such as "my commitment is part of a certain collection (inclusion) but it is not among the blacklisted one (exclusion)".

But how to prove to a verifier that a proof of membership and a proof of exclusion take the same commitment value since this value is private (hence not known by the verifier)?

An elegant solution is to bind these two proofs together through the authHash output. Both Proof of Membership and Proof of Exclusion have the same authHash as output:

authHash = Poseidon(secret, nonce)

Therefore, to link two proofs together, the prover should use the same secret, the same nullifier and the same nonce in both proofs. The verifier must check that each proof outputs the same authHash:

authHash_membership == authHash_exclusion

Proof Of Encryption

Proof of Encryption allows the prover to demonstrate that they know the plaintext message and encryption key corresponding to a given ciphertext, without revealing either of them. This is a zero-knowledge proof of correct symmetric encryption.

This construction uses the Chacha20 stream cipher. The proof shows that the ciphertext was correctly derived by encrypting the private plaintext with the private key using ChaCha20, and that the prover knows both values.

This kind of proof can be useful in systems that need to demonstrate possession of an encrypted message or credential without revealing its contents. For example:

  • A user may prove they hold the correct decryption key for an encrypted token
  • A service could verify that a submitted ciphertext corresponds to a known encryption process, without ever seeing the underlying data

The proof inputs and outputs are:

  • Private input:

    • plaintext: the original 128-byte message
    • encryptionKey: the 32-byte encryption key
  • Public inputs:

    • encryptionNonce: the 12-byte encryption nonce
    • zkNonce: a contextual binding value
  • Public output:

    • ciphertext: the 128-byte encrypted message (generated via ChaCha20)
    • authHash: a hash of the encryptionKey and zkNonce, preventing replay attacks and enabling context binding
import { ProofOfEncryption, pad, randomBigInt32ModP, poseidon, uint8ArrayToBigInt } from "@prifilabs/zk-toolbox";

// npm install @noble/ciphers
import { chacha20 } from '@noble/ciphers/chacha';
import { utf8ToBytes } from '@noble/ciphers/utils';
import { randomBytes } from '@noble/ciphers/webcrypto';

const privateInputs = {
	plaintext: pad(utf8ToBytes('The quick brown fox jumps over the lazy dog!')),
	encryptionKey: randomBytes(32),
};
const publicInputs = {
	encryptionNonce: randomBytes(12),
	zkNonce: randomBigInt32ModP(),
};

const proofOfEncryption = new ProofOfEncryption();
const { proof, publicOutputs } = await proofOfEncryption.generate(privateInputs, publicInputs);

const ciphertext = chacha20(privateInputs.encryptionKey, publicInputs.encryptionNonce, privateInputs.plaintext);
console.assert(Buffer.from(publicOutputs.ciphertext).toString('hex') === Buffer.from(ciphertext).toString('hex'));

const authHash = poseidon([
    uint8ArrayToBigInt(privateInputs.encryptionKey.slice(0, privateInputs.encryptionKey.length/2)),
    uint8ArrayToBigInt(privateInputs.encryptionKey.slice(privateInputs.encryptionKey.length/2)),
	publicInputs.zkNonce
]);
console.assert(publicOutputs.authHash === authHash);

const res = await proofOfEncryption.verify(proof, publicInputs, publicOutputs);
console.assert(res);
process.exit(0);

Proof Of Signature

[!NOTE]
The zk-SNARK witness file for Proof of Signature is 800 MB and was not included in the library. If you want to use this feature, you will need to compile it using the Makefile from the Github repository (see the Contribute section below).

Proof of Signature is implemented here primarily as a proof of concept rather than a practical solution. At its core, a digital signature is already a form of zero-knowledge proof: it demonstrates that the signer possesses the private key corresponding to a given public key, without revealing the key itself.

However, integrating signature verification within a zero-knowledge proof can offer practical advantages. Since ZK-SNARKs can be verified efficiently, especially in blockchain environments, it can be useful to bundle signature checks into a broader ZK circuit, rather than verifying them separately off-chain or in on-chain logic.

In this example, we focus on the ECDSA signature scheme using the secp256k1 curve and SHA3 as the hashing function. This is the most widely used scheme in cryptocurrencies like Bitcoin and Ethereum. However, ECDSA is not ZK-friendly: its algebraic structure is poorly suited for efficient proof generation in ZK-SNARK systems. As a result, generating this proof is computationally expensive.

For reference, generating a single proof on a MacBook M2 Pro takes approximately 2 minutes and 30 seconds.

The proof inputs and outputs are:

  • Public inputs:

    • msgHash: the message hash computed using SHA3
    • signature: the ECDSA signature over msgHash
  • Public output:

    • pubkey: the public key recovered from the msgHash and signature
import { ProofOfSignature, getECDSAInputs, randomBigInt32ModP, poseidon } from "@prifilabs/zk-toolbox";

// npm install @noble/secp256k1 @noble/hashes
import * as secp from '@noble/secp256k1';
import { keccak_256 } from '@noble/hashes/sha3.js';
import { utf8ToBytes } from '@noble/hashes/utils';

const privKey = secp.utils.randomPrivateKey();
const message = "Hello World!";
const msgHash = Buffer.from(keccak_256(utf8ToBytes(message))).toString('hex');
const signature = await secp.signAsync(msgHash, privKey);
const privateInputs = {};
const publicInputs = { msgHash, signature };

const proofOfSignature = new ProofOfSignature();
const { proof, publicOutputs } = await proofOfSignature.generate(privateInputs, publicInputs);

const pubkey = publicInputs.signature.recoverPublicKey(Buffer.from(publicInputs.msgHash, 'hex')).toHex();
console.assert(publicOutputs.pubkey === pubkey);
const { s } = getECDSAInputs(publicInputs.msgHash, publicInputs.signature);

const res = await proofOfSignature.verify(proof, publicInputs, publicOutputs);
console.assert(res);
process.exit(0);

Proof Of Location

Proof of Location allows the prover to demonstrate that their current geographic coordinates fall within a specified area, defined by a central point and radius, without revealing their exact location.

This type of zero-knowledge proof is useful in applications where location-based access or eligibility needs to be enforced without compromising user privacy. Examples include geo-fenced voting, location-based access to digital assets, or location-sensitive identity claims.

To preserve privacy while ensuring soundness, the exact coordinates are kept private. A private nonce value is mixed into the location hash to protect against brute-force attacks on the hashed output. This prevents adversaries from reverse-engineering the location by guessing plausible coordinates.

The proof inputs and outputs are:

  • Private input:

    • location: the prover’s exact latitude and longitude coordinates
    • nonce: a random value used to salt the location hash
  • Public inputs:

    • center: the latitude and longitude of the center of the allowed area
    • radius: the radius (in meters) defining the boundary of the area
  • Public output:

    • locationHash: a hash of the location and secret, ensuring the coordinates remain hidden
import { ProofOfLocation, randomBigInt32ModP, poseidon } from "@prifilabs/zk-toolbox";

// npm install random-location
import { randomCirclePoint, distance} from 'random-location';

const TORONTO = {
    latitude: 43.653908,
    longitude: -79.384293,
};
const RADIUS = 50000;
const location = randomCirclePoint(TORONTO, RADIUS);
const nonce = randomBigInt32ModP();
const privateInputs = { location, nonce };
const publicInputs = { center: TORONTO, radius: RADIUS };

const proofOfLocation = new ProofOfLocation();
const { proof, publicOutputs } = await proofOfLocation.generate(privateInputs, publicInputs);

const res = await proofOfLocation.verify(proof, publicInputs, publicOutputs);
console.assert(res);
process.exit(0);

Contribute

All code is on Github.

Install SnarkJs Globally

npm install -g snarkjs@latest

To compile the CIRCOM files:

make clean         // removes zk-data and contracts
make ptau          // download the ptau file (if needed) from the *Perpertual Power of Tau* repo
make proofs        // compiles all CIRCOM files

To run the tests:

npm install
npx test

Credits

Building On the Shoulders of Giants:

AI-Generated Content Notice

Portions of this README were written with the assistance of AI to improve clarity and style. However, no part of the source code was generated by AI, all code was written and reviewed by a human developer.

Contact Us

Join us on PriFi Labs' Discord

License

MIT License