A collection of zero-knowledge proof systems that allow a prover to demonstrate their IPv6 address is not on a public blocklist — without revealing the address itself.
Built with Circom 2, SnarkJS, and the Groth16 proving system.
- Overview
- How It Works
- Repository Structure
- Prerequisites
- Phase 1 — Linear Scan Blocklist
- Phase 1.1 — Sparse Merkle Tree Blocklist
- Circuit Design
- Security Considerations
- Submodules
This project explores the practical application of zero-knowledge proofs for network access control. The core idea is:
A user wants to prove to a verifier that their IPv6 address is not on a known blocklist, without revealing what their address actually is.
The blocklist itself is public. The prover's IP address is the private witness. A Groth16 ZK-SNARK proof is generated and given to the verifier. The verifier can confirm the proof is valid without learning the IP address.
The workflow at a high level:
- Circuit compilation — The
.circomcircuit is compiled to R1CS constraints and a WASM witness generator. - Trusted setup — A multi-party Powers of Tau (PoT) ceremony generates the common reference string (CRS). Anyone can contribute entropy.
- Witness generation — The prover feeds their private IP address and the public blocklist into the WASM witness calculator to produce
witness.wtns. - Proof generation —
snarkjs groth16 proveuses the final proving key (.zkey) and the witness to produce a proof. - Verification —
snarkjs groth16 verifychecks the proof against the public verification key and the public inputs (blocklist + output signal). No private data is revealed.
Zero-Knowledge-Proofs/
├── Phase_1/
│ └── zkblocklist/ # Phase 1: linear scan circuit
│ ├── zkpblocklist.circom # Circom circuit source
│ ├── setup1.sh # End-to-end setup/prove/verify script
│ ├── input.json # Prover inputs (blocklist + private IP)
│ ├── build/ # Compiled R1CS and WASM (generated)
│ ├── verification_key.json # Exported verification key
│ ├── zkpblocklist_proof.json
│ └── zkpblocklist_public.json
├── Phase_1.1/
│ ├── Merkle_Tree/
│ │ └── mt.cpp # Standalone C++ SHA-256 Merkle tree (reference)
│ └── zkblocklistv1.1/
│ ├── zkpblocklistv1.1.circom # Phase 1.1 Circom circuit
│ ├── setup1.1.sh # End-to-end setup/prove/verify script
│ └── rustSMTgenerator/ # Rust tool: builds a Sparse Merkle Tree
│ ├── src/main.rs # SMT construction + exclusion proof
│ ├── address.json # The address to prove absent from the SMT
│ ├── smt_data.json # SMT output (root, leaves, proof validity)
│ ├── testrs.sh # Automated test harness for the SMT
│ └── cargo.toml
├── circomlib/ # Circom standard library (submodule)
├── sparse-merkle-tree/ # Rust SMT library (submodule)
└── blake2b-rs/ # Blake2b hasher for Rust (submodule)
Install the following before using either phase:
| Tool | Purpose | Install |
|---|---|---|
| Circom 2 | Compiles .circom circuits to R1CS + WASM |
Build from source with Rust (cargo build --release) |
| Node.js (v16+) | Runs witness generator and SnarkJS | Download from nodejs.org |
| SnarkJS | Trusted setup, proof generation, verification | npm install -g snarkjs |
| Rust + Cargo | Builds the Rust SMT generator (Phase 1.1) | `curl https://sh.rustup.rs |
| Git Bash | Runs the .sh scripts on Windows |
Included with Git for Windows |
grep |
Used by setup scripts to parse JSON | Included in Git Bash |
| Python 3 | Used by the SMT test harness | Download from python.org |
Windows note: All
.shscripts must be run in Git Bash, not PowerShell or CMD. The circom compiler path in the scripts uses Windows-style paths — update them to match your local installation before running.
Before running, open Phase_1/zkblocklist/setup1.sh and update these two variables to match your environment:
WORKDIR="C:/Users/yourname/path/to/Phase_1/zkblocklist"
CIRCOM_COMPILER="C:/path/to/circom.exe"Also update the include path at the top of Phase_1/zkblocklist/zkpblocklist.circom:
include "C:/path/to/circomlib/circuits/comparators.circom";Edit Phase_1/zkblocklist/input.json before generating a proof. The file has two fields:
{
"blocklist": [
[8193, 3512, 34211, 0, 0, 35374, 880, 29492],
[8193, 3512, 34211, 0, 0, 35374, 880, 30000],
...
],
"ipv6Address": [8193, 3512, 34211, 0, 0, 35374, 880, 99999]
}blocklist— A 2D array of 128 entries, each an IPv6 address represented as 8 decimal integers (one per 16-bit group). Pad unused entries with[0,0,0,0,0,0,0,0]. This is the public input.ipv6Address— The prover's IPv6 address as 8 decimal integers. This is the private witness.
Converting an IPv6 address to decimal groups:
An IPv6 address like 2001:0db8:85a3:0000:0000:8a2e:0370:7334 maps to:
[0x2001, 0x0db8, 0x85a3, 0x0000, 0x0000, 0x8a2e, 0x0370, 0x7334]
= [8193, 3512, 34211, 0, 0, 35374, 880, 29492]
Open Git Bash and run:
cd Phase_1/zkblocklist
bash setup1.shThe script presents five options:
| Option | When to use |
|---|---|
1 — Full new setup |
First time ever, or after changing the circuit and wanting to redo everything from scratch including the trusted setup |
2 — Inputs changed only |
You only edited input.json — reuses existing proving key |
3 — Circuit changed |
You modified the .circom file; recompiles and re-runs Groth16 setup using the existing .ptau |
4 — Verify only |
Re-runs verification on existing proof files without regenerating anything |
5 — Exit |
Aborts without doing anything |
Option 1 interactive prompts:
- 4× random entropy strings — Typed input for each of the four Powers of Tau contributions. Use a long random string for each (e.g., keyboard mashing). More entropy = more secure ceremony.
- Username — A name to tag your zkey contribution. Can be any string.
After a successful run the following files are written (or updated):
| File | Description |
|---|---|
pot12_prepared.ptau |
Finalized Powers of Tau file for phase 2 |
zkpblocklist_final.zkey |
Final proving key |
verification_key.json |
Verification key — give this to the verifier |
witness.wtns |
Witness (private; do not share) |
zkpblocklist_proof.json |
The ZK proof — give this to the verifier |
zkpblocklist_public.json |
Public signals (blocklist + onBlocklist output) — give this to the verifier |
build/zkpblocklist.r1cs |
R1CS constraint system (generated once per circuit) |
build/zkpblocklist_js/ |
WASM witness generator (generated once per circuit) |
To verify a proof on the verifier's machine (without needing the private key):
snarkjs groth16 verify verification_key.json zkpblocklist_public.json zkpblocklist_proof.jsonThe output signal onBlocklist in zkpblocklist_public.json is:
1— The address is on the blocklist0— The address is not on the blocklist
- Not resistant to IP spoofing — The prover supplies their own IP address as private input. A dishonest prover can simply claim to have any address they want and generate a valid proof for that false address. There is no mechanism to bind the proof to the prover's actual network identity.
- The blocklist is fixed at compile time at 128 entries. Changing the size requires recompiling the circuit and redoing the trusted setup.
- The trusted setup is local and involves only one contributor per run, which is weaker than a large multi-party ceremony.
Phase 1.1 introduces a Sparse Merkle Tree (SMT) data structure as the basis for the blocklist. Instead of checking membership by comparing every entry, the prover provides a Merkle exclusion proof: a cryptographic proof that their key is absent from the tree.
The Rust program in rustSMTgenerator/ builds the SMT from the hardcoded blocklist, accepts a target IPv6 address from address.json, and outputs the SMT root and leaf data to smt_data.json. The Circom circuit (same core logic as Phase 1 at the current stage) is then used with this data.
The SMT is built using the sparse-merkle-tree crate (v0.6.1) with Blake2b hashing.
Blocklist is hardcoded in src/main.rs:
const IPV6ADDRESSES: [[u16; 8]; BLEN] = [
[8193, 3512, 34211, 0, 0, 35374, 880, 29492],
// ... more entries
];
static BLEN: usize = 7; // update this when adding entriesTo add or remove blocklist entries, update both BLEN and the IPV6ADDRESSES array, then rebuild.
Target address is read from address.json:
[8193, 3512, 34211, 0, 0, 35374, 880, 12345]A plain JSON array of 8 unsigned 16-bit integers.
To run the Rust generator:
cd Phase_1.1/zkblocklistv1.1/rustSMTgenerator
# Edit address.json with the target IPv6 address first
cargo runThe program prints whether the exclusion proof is valid and writes smt_data.json.
smt_data.json structure:
{
"root": [/* 32-byte array */],
"leaves": [
{ "key": [/* 32 bytes */], "value": [/* 32 bytes */] }
],
"Exclusion proof valid": true
}Note: The
"Exclusion proof valid"field insmt_data.jsonis for testing purposes only (used bytestrs.sh). The Circom verifier does not use this field — it only uses the key and value data.
Before running, update the path variables in Phase_1.1/zkblocklistv1.1/setup1.1.sh:
WORKDIR="C:/Users/yourname/path/to/Phase_1.1/zkblocklistv1.1"
CIRCOM_COMPILER="C:/path/to/circom.exe"Also update the include path in Phase_1.1/zkblocklistv1.1/zkpblocklistv1.1.circom:
include "C:/path/to/circomlib/circuits/comparators.circom";The circuit also requires an input JSON named zkpblocklistv1.1_input.json in the working directory. Its format is the same as Phase 1 input.json (blocklist array + ipv6Address).
cd Phase_1.1/zkblocklistv1.1
bash setup1.1.shOptions are the same as Phase 1:
| Option | Action |
|---|---|
1 |
Full new setup (PoT ceremony, compile, prove, verify) |
2 |
Inputs changed only |
3 |
Circuit changed |
4 |
Verify proof only |
5 |
Exit |
testrs.sh is an automated regression harness for the Rust SMT generator. It runs the exclusion proof multiple times with randomly generated addresses and verifies that:
- Addresses not on the blocklist produce a valid exclusion proof (
cargo runexits 0). - Addresses on the blocklist fail the exclusion proof (
cargo runexits non-zero).
cd Phase_1.1/zkblocklistv1.1/rustSMTgenerator
bash testrs.shResults are appended to test_results.log. Requires Python 3 to generate random test addresses.
- The circuit still uses the same linear-scan approach as Phase 1. The SMT exclusion proof is generated in Rust but is not yet wired into the Circom verification constraints — the SMT root is not verified inside the circuit.
- IP spoofing vulnerability from Phase 1 still applies.
Both phases use the same ProveBlocklistExclusion template:
Inputs:
blocklist[128][8] — public: 128 IPv6 addresses, each as 8 uint16 values
ipv6Address[8] — private: the prover's IPv6 address
Output:
onBlocklist — 1 if ipv6Address matches any blocklist entry, 0 otherwise
The circuit works as follows:
- For each of the 128 blocklist entries, compare all 8 groups to the private IP using
IsEqualcomponents from circomlib. - AND the 8 comparisons together to get a per-entry match signal.
- Propagate a running "any match found" signal across all 128 entries using the formula:
sumval[i] = matches[i] + (1 - matches[i]) * sumval[i-1] - Apply
IsZeroto eachsumval[i]and take the cumulative product. If everysumvalis zero (no match ever found), the product is 1 andonBlocklist = 0. OtherwiseonBlocklist = 1.
The proving system is Groth16 over the BN128 curve, compiled with snarkjs powersoftau new bn128 14 (supporting up to
- Trust in the setup: The security of the Groth16 proof relies on the Powers of Tau ceremony being conducted with at least one honest participant. The setup scripts perform a local ceremony with a single contributor. For production use, a public multi-party ceremony (e.g., from the Hermez network or Perpetual Powers of Tau) should be used.
- Private input is self-reported: As noted under Limitations, the prover supplies their own IP address. A malicious prover can generate a valid proof for any IP they choose.
- Verification key integrity: The verifier should independently check that the
verification_key.jsonwas derived from the correct circuit and.ptaufile. The setup script outputs:Give them the code, the finalized ptau file, and the r1cs file so they can check the verification key matches the circuit. - Salt in the SMT: The Rust SMT generator uses a hardcoded salt (
SALT) when deriving keys. This salt is embedded in the source code and is not secret.
This repository uses Git submodules:
| Submodule | Description |
|---|---|
circomlib/ |
Standard Circom component library (comparators, hashes, etc.) |
sparse-merkle-tree/ |
Rust SMT implementation used by Phase 1.1 |
blake2b-rs/ |
Blake2b hasher used by the Rust SMT |
After cloning, initialize them with:
git submodule update --init --recursive