├── rust-toolchain ├── packages ├── registry │ ├── remappings.txt │ ├── .env.example │ ├── foundry.toml │ ├── .gitignore │ ├── script │ │ └── Registry.s.sol │ ├── README.md │ ├── test │ │ └── Registry.t.sol │ └── src │ │ └── Registry.sol ├── mpc │ ├── generated │ │ └── vk │ ├── Cargo.toml │ └── src │ │ ├── main.rs │ │ ├── cli.rs │ │ ├── api.rs │ │ ├── eth_utils.rs │ │ └── utils.rs ├── zk │ ├── oprf_nullifier │ │ ├── Nargo.toml │ │ ├── Prover.toml │ │ └── src │ │ │ └── main.nr │ └── oprf_commitment │ │ ├── Nargo.toml │ │ ├── Prover.toml │ │ └── src │ │ └── main.nr └── scripts │ └── test_api.py ├── Cargo.toml ├── .gitmodules ├── docs ├── book.toml └── src │ ├── SUMMARY.md │ ├── appendix.md │ ├── voprf-id.md │ ├── voprf-rs.md │ ├── voprf-id-registry.md │ ├── zk-circuits.md │ └── overview.md ├── .env.example ├── Dockerfile ├── .gitignore ├── .dockerignore ├── .github └── workflows │ ├── build-test-fmt.yml │ ├── forge-test.yml │ └── deploy-mdbook.yml ├── LICENSE └── README.md /rust-toolchain: -------------------------------------------------------------------------------- 1 | 1.85.0 -------------------------------------------------------------------------------- /packages/registry/remappings.txt: -------------------------------------------------------------------------------- 1 | forge-std/=lib/forge-std/src/ -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["packages/mpc"] 3 | resolver = "2" -------------------------------------------------------------------------------- /packages/mpc/generated/vk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/privacy-ethereum/vOPRF-ID/HEAD/packages/mpc/generated/vk -------------------------------------------------------------------------------- /packages/registry/.env.example: -------------------------------------------------------------------------------- 1 | # Private key for deployment (without 0x prefix) 2 | PRIVATE_KEY=your_private_key_here -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "packages/registry/lib/forge-std"] 2 | path = packages/registry/lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Magamedrasul Ibragimov"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "vOPRF-ID Docs" 7 | 8 | [output.html] 9 | mathjax-support = true -------------------------------------------------------------------------------- /packages/registry/foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | 6 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 7 | -------------------------------------------------------------------------------- /packages/registry/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [vOPRF-ID](./voprf-id.md) 4 | - [Overview](./overview.md) 5 | - [vOPRF.rs Description](./voprf-rs.md) 6 | - [ZK Circuits Description](./zk-circuits.md) 7 | - [vOPRF-ID Registry](./voprf-id-registry.md) 8 | - [Appendix & Useful Links](./appendix.md) -------------------------------------------------------------------------------- /packages/zk/oprf_nullifier/Nargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oprf_nullifier" 3 | type = "bin" 4 | authors = ["Aditya Bisht"] 5 | 6 | [dependencies] 7 | noir_bigcurve = { tag = "v0.6.0", git = "https://github.com/noir-lang/noir_bigcurve" } 8 | bignum = {tag = "v0.5.4", git = "https://github.com/noir-lang/noir-bignum"} -------------------------------------------------------------------------------- /packages/zk/oprf_commitment/Nargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "oprf_commitment" 3 | type = "bin" 4 | authors = ["Aditya Bisht"] 5 | 6 | [dependencies] 7 | noir_bigcurve = { tag = "v0.6.0", git = "https://github.com/noir-lang/noir_bigcurve" } 8 | bignum = {tag = "v0.5.4", git = "https://github.com/noir-lang/noir-bignum"} -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Ethereum RPC URL (required) 2 | ETH_RPC_URL=https://eth.merkle.io 3 | 4 | # OPRF Registry contract address (required) 5 | REGISTRY_ADDRESS=0xb23b9cC87Cb0Ae816Ce74b3B5b8B87f593Ed5D1D 6 | 7 | # Ethereum private key (required) of OPRF Node 8 | # Format can be with or without 0x prefix 9 | ETH_PRIVATE_KEY=0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM rust:1.85-slim 3 | 4 | # Install build dependencies 5 | RUN apt-get update && apt-get install -y \ 6 | pkg-config \ 7 | libssl-dev \ 8 | nano \ 9 | && rm -rf /var/lib/apt/lists/* 10 | 11 | # Set working directory 12 | WORKDIR /vOPRF-ID 13 | 14 | # Copy project files 15 | COPY . . 16 | 17 | # Build the project using workspace 18 | RUN cargo build --release 19 | 20 | # Expose the port 21 | EXPOSE 8080 22 | 23 | # Default to bash for interactive use 24 | CMD ["/bin/bash"] 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust 2 | target 3 | **/*.rs.bk 4 | Cargo.lock 5 | 6 | # Node.js 7 | node_modules/ 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | .npm 12 | .yarn 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .vscode/ 17 | *.swp 18 | *.swo 19 | .DS_Store 20 | 21 | # Build outputs 22 | dist/ 23 | build/ 24 | out/ 25 | 26 | # Environment variables 27 | .env 28 | .env.local 29 | .env.*.local 30 | 31 | # Debug logs 32 | *.log 33 | 34 | # mdbook 35 | book/ 36 | 37 | private_key.txt 38 | 39 | .venv 40 | 41 | broadcast -------------------------------------------------------------------------------- /packages/zk/oprf_commitment/Prover.toml: -------------------------------------------------------------------------------- 1 | salt = "67890" 2 | user_id = "12345" 3 | user_id_commitment = "0x2ef418c0d71ccca89827b221a86e883a8a01b97282de65ab7ad8c3fe37cfaf1a" 4 | 5 | [oprf_commitment] 6 | is_infinity = "0" 7 | 8 | [oprf_commitment.x] 9 | limbs = ["0x2bb7aac4ce2639b06df602f513367f", "0xe2565ead6749e741bc593e9a1c80cc", "0x4566"] 10 | 11 | [oprf_commitment.y] 12 | limbs = ["0xf3dee58554086a6f6bf20642c3e9a7", "0xa9b2f36e4d80f9619a247d15c81892", "0x5e26"] 13 | 14 | [random_scalar] 15 | limbs = ["0x2", "0x0", "0x0"] 16 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Rust 2 | target 3 | **/*.rs.bk 4 | Cargo.lock 5 | 6 | # Node.js 7 | node_modules/ 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | .npm 12 | .yarn 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .vscode/ 17 | *.swp 18 | *.swo 19 | .DS_Store 20 | 21 | # Build outputs 22 | dist/ 23 | build/ 24 | out/ 25 | 26 | # Environment variables 27 | .env 28 | .env.local 29 | .env.*.local 30 | 31 | # Debug logs 32 | *.log 33 | 34 | # mdbook 35 | book/ 36 | 37 | private_key.txt 38 | 39 | .venv 40 | 41 | broadcast -------------------------------------------------------------------------------- /packages/registry/script/Registry.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import {Script, console2} from "forge-std/Script.sol"; 5 | import {Registry} from "../src/Registry.sol"; 6 | 7 | contract RegistryScript is Script { 8 | function setUp() public {} 9 | 10 | function run() public { 11 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 12 | vm.startBroadcast(deployerPrivateKey); 13 | 14 | Registry registry = new Registry(); 15 | console2.log("Registry deployed to:", address(registry)); 16 | 17 | vm.stopBroadcast(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/mpc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mpc" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | rand = "0.8" 8 | once_cell = "1.19" 9 | thiserror = "2.0.12" 10 | anyhow = "1.0.97" 11 | serde = { version = "1.0.219", features = ["derive"] } 12 | serde_json = "1.0.140" 13 | actix-web = "4.10.2" 14 | k256 = { version = "0.13.4", features = ["serde"] } 15 | num-bigint = { version = "0.4.6", features = ["serde", "rand"] } 16 | num-traits = "0.2" 17 | hex = "0.4" 18 | sha2 = "0.10.8" 19 | uuid = { version = "1.16.0", features = ["v4"] } 20 | clap = { version = "4.5.1", features = ["derive"] } 21 | alloy = { version = "0.12", features = ["full"] } 22 | tokio = { version = "1.35.1", features = ["full"] } 23 | dotenv = "0.15.0" 24 | -------------------------------------------------------------------------------- /.github/workflows/build-test-fmt.yml: -------------------------------------------------------------------------------- 1 | name: Build-Test-Fmt 2 | 3 | on: 4 | push: 5 | branches: main 6 | pull_request: 7 | branches: main 8 | 9 | jobs: 10 | build-test-fmt: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - run: rustup show 16 | 17 | - name: Install rustfmt and clippy 18 | run: | 19 | rustup component add rustfmt 20 | rustup component add clippy 21 | 22 | - uses: Swatinem/rust-cache@v2 23 | 24 | - name: Build 25 | run: cargo build 26 | 27 | - name: Test 28 | run: cargo test 29 | 30 | - name: Fmt 31 | run: cargo fmt -- --check 32 | 33 | - name: Run clippy 34 | run: cargo clippy -- -D warnings -------------------------------------------------------------------------------- /packages/scripts/test_api.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import argparse 3 | 4 | # Parse command line arguments 5 | parser = argparse.ArgumentParser(description='Send proof to API endpoint') 6 | parser.add_argument('--address', default='localhost', help='API server address (default: localhost)') 7 | parser.add_argument('--port', default='8080', help='API server port (default: 8080)') 8 | args = parser.parse_args() 9 | 10 | # Read the proof file 11 | with open("../zk/oprf_commitment/target/proof", 'rb') as f: 12 | proof = f.read() 13 | 14 | # Construct the API URL 15 | api_url = f"http://{args.address}:{args.port}/api/v1/evaluate" 16 | 17 | # Send POST request 18 | response = requests.post( 19 | api_url, 20 | json={"proof": list(proof)} 21 | ) 22 | 23 | print("Response:", response.json()) -------------------------------------------------------------------------------- /docs/src/appendix.md: -------------------------------------------------------------------------------- 1 | # Appendix 2 | 3 | ## Useful Links 4 | 5 | - [Ethresearch discussion](https://ethresear.ch/t/web2-nullifiers-using-voprf/21762) 6 | - [Paper for OPRF](https://eprint.iacr.org/2020/647.pdf) 7 | - [Mishti Network announcement](https://medium.com/holonym/announcing-mishti-network-9330d90e0ead) 8 | - [Mishti Network paper](https://drive.google.com/file/d/1DQFEhTe6D6BrITV2na65hpH-op7G23_t/view) 9 | - [Ethresearch discussion on proof of identity](https://ethresear.ch/t/privacy-preserving-nullifiers-for-proof-of-identity-applications/18551) 10 | - [PQ OPRF](https://eprint.iacr.org/2024/450) 11 | - [Rasul's initial blog post on vOPRF](https://curryrasul.com/blog/web2-nullifiers/) 12 | - [Mishti Network initial Ethresearch](https://ethresear.ch/t/a-threshold-network-for-human-keys-to-solve-privacy-and-custody-issues/20276) -------------------------------------------------------------------------------- /packages/mpc/src/main.rs: -------------------------------------------------------------------------------- 1 | mod api; 2 | mod cli; 3 | mod eth_utils; 4 | mod utils; 5 | 6 | use clap::Parser; 7 | use cli::{Cli, Commands}; 8 | 9 | #[actix_web::main] 10 | async fn main() -> std::io::Result<()> { 11 | let cli = Cli::parse(); 12 | 13 | match cli.command { 14 | Commands::Initialize => { 15 | if let Err(e) = cli::handle_initialize().await { 16 | eprintln!("Initialization failed: {}", e); 17 | std::process::exit(1); 18 | } 19 | } 20 | Commands::Serve => { 21 | if let Err(e) = cli::check_private_key_exists().await { 22 | eprintln!("Error checking node status: {}", e); 23 | std::process::exit(1); 24 | } 25 | api::run_server().await?; 26 | } 27 | } 28 | 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/forge-test.yml: -------------------------------------------------------------------------------- 1 | name: Forge Tests 2 | 3 | on: 4 | push: 5 | branches: main 6 | paths: 7 | - 'packages/registry/**' 8 | pull_request: 9 | branches: main 10 | paths: 11 | - 'packages/registry/**' 12 | workflow_dispatch: 13 | 14 | env: 15 | FOUNDRY_PROFILE: ci 16 | 17 | jobs: 18 | forge-tests: 19 | name: Foundry project 20 | runs-on: ubuntu-latest 21 | defaults: 22 | run: 23 | working-directory: packages/registry 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | submodules: recursive 28 | 29 | - name: Install Foundry 30 | uses: foundry-rs/foundry-toolchain@v1 31 | with: 32 | version: nightly 33 | 34 | - name: Run Forge build 35 | run: | 36 | forge --version 37 | forge build --sizes 38 | id: build 39 | 40 | - name: Run Forge tests 41 | run: | 42 | forge test -vvv 43 | id: test 44 | -------------------------------------------------------------------------------- /.github/workflows/deploy-mdbook.yml: -------------------------------------------------------------------------------- 1 | name: Deploy mdBook 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | concurrency: 14 | group: ${{ github.workflow }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | build-and-deploy: 19 | runs-on: ubuntu-latest 20 | environment: 21 | name: github-pages 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v4 25 | 26 | - name: Setup mdBook 27 | uses: peaceiris/actions-mdbook@v2 28 | with: 29 | mdbook-version: 'latest' 30 | 31 | - name: Build mdBook 32 | run: | 33 | cd docs 34 | mdbook build 35 | 36 | - name: Setup Pages 37 | uses: actions/configure-pages@v5 38 | 39 | - name: Upload artifact 40 | uses: actions/upload-pages-artifact@v3 41 | with: 42 | path: docs/book 43 | 44 | - name: Deploy to GitHub Pages 45 | id: deployment 46 | uses: actions/deploy-pages@v4 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Privacy & Scaling Explorations 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/registry/README.md: -------------------------------------------------------------------------------- 1 | ## Foundry 2 | 3 | **Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** 4 | 5 | Foundry consists of: 6 | 7 | - **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). 8 | - **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. 9 | - **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. 10 | - **Chisel**: Fast, utilitarian, and verbose solidity REPL. 11 | 12 | ## Documentation 13 | 14 | https://book.getfoundry.sh/ 15 | 16 | ## Usage 17 | 18 | ### Build 19 | 20 | ```shell 21 | $ forge build 22 | ``` 23 | 24 | ### Test 25 | 26 | ```shell 27 | $ forge test 28 | ``` 29 | 30 | ### Format 31 | 32 | ```shell 33 | $ forge fmt 34 | ``` 35 | 36 | ### Gas Snapshots 37 | 38 | ```shell 39 | $ forge snapshot 40 | ``` 41 | 42 | ### Anvil 43 | 44 | ```shell 45 | $ anvil 46 | ``` 47 | 48 | ### Deploy 49 | 50 | ```shell 51 | $ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key 52 | ``` 53 | 54 | ### Cast 55 | 56 | ```shell 57 | $ cast 58 | ``` 59 | 60 | ### Help 61 | 62 | ```shell 63 | $ forge --help 64 | $ anvil --help 65 | $ cast --help 66 | ``` 67 | -------------------------------------------------------------------------------- /packages/zk/oprf_nullifier/Prover.toml: -------------------------------------------------------------------------------- 1 | salt = "" 2 | user_id = "" 3 | user_id_commitment = "" 4 | 5 | [[chaum_pedersen_proof]] 6 | 7 | [chaum_pedersen_proof.c] 8 | limbs = ["", "", ""] 9 | 10 | [chaum_pedersen_proof.s] 11 | limbs = ["", "", ""] 12 | 13 | [[chaum_pedersen_proof]] 14 | 15 | [chaum_pedersen_proof.c] 16 | limbs = ["", "", ""] 17 | 18 | [chaum_pedersen_proof.s] 19 | limbs = ["", "", ""] 20 | 21 | [[chaum_pedersen_proof]] 22 | 23 | [chaum_pedersen_proof.c] 24 | limbs = ["", "", ""] 25 | 26 | [chaum_pedersen_proof.s] 27 | limbs = ["", "", ""] 28 | 29 | [[oprf_pubkey]] 30 | is_infinity = "" 31 | 32 | [oprf_pubkey.x] 33 | limbs = ["", "", ""] 34 | 35 | [oprf_pubkey.y] 36 | limbs = ["", "", ""] 37 | 38 | [[oprf_pubkey]] 39 | is_infinity = "" 40 | 41 | [oprf_pubkey.x] 42 | limbs = ["", "", ""] 43 | 44 | [oprf_pubkey.y] 45 | limbs = ["", "", ""] 46 | 47 | [[oprf_pubkey]] 48 | is_infinity = "" 49 | 50 | [oprf_pubkey.x] 51 | limbs = ["", "", ""] 52 | 53 | [oprf_pubkey.y] 54 | limbs = ["", "", ""] 55 | 56 | [[oprf_response]] 57 | is_infinity = "" 58 | 59 | [oprf_response.x] 60 | limbs = ["", "", ""] 61 | 62 | [oprf_response.y] 63 | limbs = ["", "", ""] 64 | 65 | [[oprf_response]] 66 | is_infinity = "" 67 | 68 | [oprf_response.x] 69 | limbs = ["", "", ""] 70 | 71 | [oprf_response.y] 72 | limbs = ["", "", ""] 73 | 74 | [[oprf_response]] 75 | is_infinity = "" 76 | 77 | [oprf_response.x] 78 | limbs = ["", "", ""] 79 | 80 | [oprf_response.y] 81 | limbs = ["", "", ""] 82 | 83 | [r] 84 | limbs = ["", "", ""] 85 | -------------------------------------------------------------------------------- /docs/src/voprf-id.md: -------------------------------------------------------------------------------- 1 | # vOPRF-ID 2 | 3 | The Web2-ID Nullifiers project enables **pseudonymous systems for Web2 identities** using verifiable Oblivious PseudoRandom Functions (vOPRFs). It addresses the lack of **nullifiers** in Web2 IDs, which are essential for anonymous protocols. The project aims to build an infrastructure, like Semaphore, for Web2-ID registration and reuse across applications. 4 | 5 | ## Features and Capabilities 6 | 7 | - **Implements a vOPRF protocol** for private, deterministic randomness generation. 8 | - Uses a **multi-party computation (MPC) network** to enhance security. 9 | - Employs **ZK proofs** to verify Web2 identity without revealing it. 10 | - Aims to create a **global registry** for Web2 identities. 11 | - Generates **nullifiers for Web2 IDs**, crucial for pseudonymous protocols. 12 | - Integrates with **Web2-Web3 bridges** like ZK Email and TLS Notary. 13 | 14 | ## Developer Capabilities 15 | 16 | - Build **pseudonymous systems** for applications like anonymous voting and forums. 17 | - Create **privacy-preserving applications** for anonymous interaction with Web2 services. 18 | - Integrate the vOPRF protocol with existing infrastructure. 19 | 20 | ## Applications 21 | 22 | - **Anonymous Voting** with Web2 identities. 23 | - **Anonymous Airdrops** to users based on Web2 identities (e.g., GitHub). 24 | - **Pseudonymous Forums** with limited accounts and spam prevention. 25 | 26 | ## Key Concepts 27 | 28 | - **Nullifiers:** Prevent double-spending or multiple voting. 29 | - **vOPRF:** Allows private, deterministic randomness generation. 30 | - **MPC:** Enhances security via multi-party computation. 31 | - **ZK Proofs:** Verifies statements without revealing information. -------------------------------------------------------------------------------- /packages/mpc/src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::PathBuf; 3 | 4 | use clap::{Parser, Subcommand}; 5 | use k256::{elliptic_curve::rand_core::OsRng, Scalar}; 6 | 7 | use crate::eth_utils; 8 | 9 | #[derive(Parser)] 10 | #[command(author, version = "0.1.0", about = "vOPRF-ID MPC Node implementation", long_about = None)] 11 | pub struct Cli { 12 | #[command(subcommand)] 13 | pub command: Commands, 14 | } 15 | 16 | #[derive(Subcommand)] 17 | pub enum Commands { 18 | /// Initialize the MPC Node 19 | Initialize, 20 | /// Start the MPC Node 21 | Serve, 22 | } 23 | 24 | pub async fn handle_initialize() -> Result<(), Box> { 25 | let private_key_path = PathBuf::from("./private_key.txt"); 26 | 27 | if private_key_path.exists() { 28 | println!("Private key file already exists"); 29 | return Ok(()); 30 | } 31 | 32 | // Generate a new private key 33 | let private_key = Scalar::generate_vartime(&mut OsRng); 34 | 35 | // Register the node with the Registry contract 36 | if let Err(e) = eth_utils::register_node(&private_key).await { 37 | println!("Failed to register node: {}", e); 38 | return Err(e); 39 | } 40 | 41 | // Save the private key to file 42 | let bytes = private_key.to_bytes(); 43 | fs::write(&private_key_path, bytes.as_slice())?; 44 | 45 | println!( 46 | "Successfully initialized private key at {}", 47 | private_key_path.display() 48 | ); 49 | Ok(()) 50 | } 51 | 52 | pub async fn check_private_key_exists() -> Result<(), Box> { 53 | // Check if node is registered in the Registry contract 54 | let is_registered = eth_utils::check_node_registration().await?; 55 | 56 | if !is_registered { 57 | println!("Node is not registered in the Registry contract."); 58 | println!( 59 | "Please run with 'initialize' command first, or check your Ethereum configuration." 60 | ); 61 | std::process::exit(1); 62 | } 63 | 64 | Ok(()) 65 | } 66 | -------------------------------------------------------------------------------- /packages/registry/test/Registry.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import "../src/Registry.sol"; 6 | 7 | contract RegistryTest is Test { 8 | Registry public registry; 9 | bytes32[2] public key1; 10 | bytes32[2] public key2; 11 | bytes32[2] public key3; 12 | bytes32[2] public key4; 13 | 14 | function setUp() public { 15 | registry = new Registry(); 16 | 17 | // Create test keys 18 | key1 = [bytes32(uint256(1)), bytes32(uint256(2))]; 19 | key2 = [bytes32(uint256(3)), bytes32(uint256(4))]; 20 | key3 = [bytes32(uint256(5)), bytes32(uint256(6))]; 21 | key4 = [bytes32(uint256(7)), bytes32(uint256(8))]; 22 | } 23 | 24 | function testBasicRegistration() public { 25 | uint256 nodeId = registry.register(key1); 26 | 27 | assertTrue(registry.isRegistered(key1)); 28 | assertEq(registry.numRegisteredNodes(), 1); 29 | assertEq(nodeId, 0); 30 | 31 | bytes32[2] memory storedKey = registry.getNodePublicKey(nodeId); 32 | assertEq(storedKey[0], key1[0]); 33 | assertEq(storedKey[1], key1[1]); 34 | } 35 | 36 | function testCannotRegisterDuplicateKey() public { 37 | registry.register(key1); 38 | 39 | vm.expectRevert(Registry.DuplicatePublicKey.selector); 40 | registry.register(key1); 41 | } 42 | 43 | function testMaxNodesLimit() public { 44 | // Register three nodes 45 | uint256 nodeId1 = registry.register(key1); 46 | uint256 nodeId2 = registry.register(key2); 47 | uint256 nodeId3 = registry.register(key3); 48 | 49 | // Verify all three are registered 50 | assertTrue(registry.isRegistered(key1)); 51 | assertTrue(registry.isRegistered(key2)); 52 | assertTrue(registry.isRegistered(key3)); 53 | assertEq(registry.numRegisteredNodes(), 3); 54 | assertEq(nodeId1, 0); 55 | assertEq(nodeId2, 1); 56 | assertEq(nodeId3, 2); 57 | 58 | // Try to register a fourth node 59 | vm.expectRevert(Registry.MaxNodesReached.selector); 60 | registry.register(key4); 61 | } 62 | 63 | function testInvalidNodeId() public { 64 | vm.expectRevert(Registry.InvalidNodeId.selector); 65 | registry.getNodePublicKey(0); 66 | } 67 | 68 | function testEmittedEvents() public { 69 | vm.expectEmit(true, true, true, true); 70 | emit Registry.NodeRegistered(0, key1); 71 | 72 | registry.register(key1); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/registry/src/Registry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | contract Registry { 5 | // Fixed-size array of public keys (x,y coordinates) 6 | bytes32[2][3] public nodePublicKeys; 7 | 8 | // Number of registered nodes (0-3) 9 | uint256 public numRegisteredNodes; 10 | 11 | // Event emitted when a node registers their public key 12 | event NodeRegistered(uint256 indexed nodeId, bytes32[2] publicKey); 13 | 14 | // Error when trying to register more than 3 nodes 15 | error MaxNodesReached(); 16 | // Error when trying to register a duplicate public key 17 | error DuplicatePublicKey(); 18 | // Error when node ID is out of range 19 | error InvalidNodeId(); 20 | 21 | /** 22 | * @notice Register a public key for an OPRF node 23 | * @param publicKey The public key as [x, y] coordinates 24 | * @return nodeId The ID of the registered node (0-2) 25 | */ 26 | function register(bytes32[2] calldata publicKey) external returns (uint256 nodeId) { 27 | // Check if we've reached max nodes 28 | if (numRegisteredNodes >= 3) { 29 | revert MaxNodesReached(); 30 | } 31 | 32 | // Check if this public key is already registered 33 | for (uint256 i = 0; i < numRegisteredNodes; i++) { 34 | if (nodePublicKeys[i][0] == publicKey[0] && nodePublicKeys[i][1] == publicKey[1]) { 35 | revert DuplicatePublicKey(); 36 | } 37 | } 38 | 39 | // Store the public key 40 | nodeId = numRegisteredNodes; 41 | nodePublicKeys[nodeId] = publicKey; 42 | numRegisteredNodes++; 43 | 44 | emit NodeRegistered(nodeId, publicKey); 45 | return nodeId; 46 | } 47 | 48 | /** 49 | * @notice Get the public key for a registered node by ID 50 | * @param nodeId The ID of the node (0-2) 51 | * @return The public key as [x, y] coordinates 52 | */ 53 | function getNodePublicKey(uint256 nodeId) external view returns (bytes32[2] memory) { 54 | if (nodeId >= numRegisteredNodes) { 55 | revert InvalidNodeId(); 56 | } 57 | return nodePublicKeys[nodeId]; 58 | } 59 | 60 | /** 61 | * @notice Check if a public key is registered 62 | * @param publicKey The public key to check 63 | * @return True if the public key is registered 64 | */ 65 | function isRegistered(bytes32[2] calldata publicKey) external view returns (bool) { 66 | for (uint256 i = 0; i < numRegisteredNodes; i++) { 67 | if (nodePublicKeys[i][0] == publicKey[0] && nodePublicKeys[i][1] == publicKey[1]) { 68 | return true; 69 | } 70 | } 71 | return false; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/mpc/src/api.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{ 2 | dev::ConnectionInfo, http::StatusCode, web, App, HttpResponse, HttpServer, ResponseError, 3 | }; 4 | use serde::{Deserialize, Serialize}; 5 | use thiserror::Error; 6 | 7 | use crate::utils::{ 8 | ecpoint_to_projective, parse_public_inputs, projective_to_ecpoint, verify_zk_proof, DleqProof, 9 | ECPoint, KEYS, 10 | }; 11 | 12 | const ADDRESS: &str = "0.0.0.0:8080"; 13 | 14 | #[derive(Debug, Serialize, Deserialize)] 15 | pub struct EvaluateRequest { 16 | pub proof: Vec, 17 | } 18 | 19 | #[derive(Serialize)] 20 | struct EvaluateResponse { 21 | result: ECPoint, 22 | dleq_proof: DleqProof, 23 | } 24 | 25 | #[derive(Error, Debug)] 26 | pub enum Error { 27 | #[error("Invalid point")] 28 | InvalidPoint, 29 | #[error("Invalid proof")] 30 | InvalidProof, 31 | #[error("Internal error: {0}")] 32 | Internal(#[from] anyhow::Error), 33 | } 34 | 35 | impl ResponseError for Error { 36 | fn status_code(&self) -> actix_web::http::StatusCode { 37 | match self { 38 | Error::InvalidPoint => StatusCode::BAD_REQUEST, 39 | Error::InvalidProof => StatusCode::UNAUTHORIZED, 40 | Error::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, 41 | } 42 | } 43 | fn error_response(&self) -> HttpResponse { 44 | HttpResponse::build(self.status_code()).json(serde_json::json!({ 45 | "error": { 46 | "code": format!("{:?}", self), 47 | "message": self.to_string() 48 | } 49 | })) 50 | } 51 | } 52 | 53 | async fn evaluate_handler( 54 | req: web::Json, 55 | conn_info: ConnectionInfo, 56 | ) -> Result { 57 | println!("Got the request from {:?}", conn_info.peer_addr().unwrap()); 58 | 59 | // Extract the proof string before passing to web::block 60 | let proof = req.proof.clone(); 61 | 62 | let (_, point) = parse_public_inputs(&proof)?; 63 | 64 | println!("Parsed the proof"); 65 | 66 | // Convert ECPoint to ProjectivePoint 67 | let commitment2_point = ecpoint_to_projective(&point)?; 68 | 69 | // Run the blocking proof verification in a separate thread pool 70 | let _ = web::block(move || verify_zk_proof(&proof)) 71 | .await 72 | .map_err(|e| { 73 | eprintln!("Blocking operation failed: {:?}", e); 74 | Error::InvalidProof 75 | })?; 76 | 77 | println!("Verified the proof"); 78 | 79 | // Perform scalar multiplication with private key 80 | let result_point = commitment2_point * KEYS.0; 81 | 82 | // Convert result to ECPoint 83 | let result = projective_to_ecpoint(&result_point); 84 | 85 | // Generate DLEQ proof that shows the same private key was used 86 | let dleq_proof = DleqProof::new(&commitment2_point); 87 | 88 | println!("Sending the response"); 89 | 90 | Ok(HttpResponse::Ok().json(EvaluateResponse { result, dleq_proof })) 91 | } 92 | 93 | pub async fn run_server() -> std::io::Result<()> { 94 | println!("Starting server on {}", ADDRESS); 95 | HttpServer::new(|| App::new().route("/api/v1/evaluate", web::post().to(evaluate_handler))) 96 | .bind(ADDRESS)? 97 | .run() 98 | .await 99 | } 100 | -------------------------------------------------------------------------------- /docs/src/voprf-rs.md: -------------------------------------------------------------------------------- 1 | This spec focuses on implementing the networking part for vOPRF protocol. It describes how nodes generate keys, process evaluation requests, and coordinate to provide secure vOPRF functionality. 2 | 3 | # Web2 Nullifiers using vOPRF - Network/MPC Specification 4 | 5 | ## Overview 6 | 7 | This specification describes the networking and MPC components of the vOPRF system. The implementation uses a non-coordinated N-of-N setup with N=3 nodes, where each node operates independently without inter-node communication. 8 | 9 | ## Node Setup 10 | 11 | ### Key Generation 12 | Each node must: 13 | 1. Generate a BabyJubJub keypair: 14 | ``` 15 | private_key = random_scalar() 16 | public_key = private_key * G // G is BabyJubJub base point 17 | ``` 18 | 2. Make their public key available for verification (store it in a public registry, e.g. Ethereum) 19 | 20 | ## API Specification 21 | 22 | ### Endpoint: `/api/v1/evaluate` 23 | 24 | Processes vOPRF evaluation requests and returns the result with a proof of correctness. 25 | 26 | #### Request 27 | - Method: `POST` 28 | - Content-Type: `application/json` 29 | - Body: 30 | ```json 31 | { 32 | "proof": { 33 | // ZK proof that commitment is valid 34 | // (as specified in [OPRF Commitment Circuit](./zk-circuits.md#circuit-1-oprf-commitment-circuit)) 35 | } 36 | } 37 | ``` 38 | 39 | #### Response 40 | - Status: 200 OK 41 | - Content-Type: `application/json` 42 | - Body: 43 | ```json 44 | { 45 | "result": { 46 | "x": "0x...", // hex-encoded x coordinate 47 | "y": "0x..." // hex-encoded y coordinate 48 | }, 49 | "dleq_proof": { 50 | "c": "0x...", // challenge 51 | "s": "0x..." // response 52 | } 53 | } 54 | ``` 55 | 56 | ### Processing Steps 57 | 58 | 1. **Input Validation** 59 | - Verify that commitment point is on BabyJubJub curve 60 | - Verify the accompanying ZK proof 61 | 62 | 2. **OPRF Evaluation** 63 | ``` 64 | result = private_key * commitment2 65 | ``` 66 | 67 | 3. **DLEQ Proof Generation** 68 | Generate Chaum-Pedersen proof to prove that: 69 | ``` 70 | (G, public_key) ~ (commitment2, result) 71 | ``` 72 | where `~` denotes "same discrete logarithm" 73 | 74 | Reference: https://github.com/holonym-foundation/mishti-crypto/blob/main/src/lib.rs#L109 75 | 76 | ## Client Integration 77 | 78 | Clients must: 79 | 1. Generate commitment using Circuit 1 (OPRF Commitment Circuit) 80 | 2. Contact all N nodes independently 81 | 3. Verify DLEQ proofs from all nodes 82 | 4. Sum all responses to get final OPRF result 83 | 5. Generate nullifier using Circuit 2 (Nullifier Generation Circuit) 84 | 85 | ## Error Handling 86 | 87 | Return appropriate HTTP status codes: 88 | - 400 Bad Request: Invalid input format or point not on curve 89 | - 401 Unauthorized: Invalid proof 90 | - 429 Too Many Requests: Rate limit exceeded 91 | - 500 Internal Server Error: Node error 92 | 93 | Error response format: 94 | ```json 95 | { 96 | "error": { 97 | "code": "INVALID_POINT", 98 | "message": "Provided point is not on BabyJubJub curve" 99 | } 100 | } 101 | ``` 102 | ``` 103 | 104 | This specification can be implemented in Rust. Reusing parts of [Holonym Foundation's vOPRF](https://github.com/holonym-foundation/mishti-crypto) might be helpful. -------------------------------------------------------------------------------- /docs/src/voprf-id-registry.md: -------------------------------------------------------------------------------- 1 | # Web2 Nullifiers using vOPRF 2 | 3 | *Recommended to read [Overview](./voprf-id.md) for understanding.* 4 | 5 | ## Abstract 6 | 7 | Idea of having nullifiers for Web2 IDs is very promising. There's an [explanation](./overview.md) of the idea and how we can achieve this. 8 | As it was said - we can build a lot of applications on top of that, and this would require users to go through the same [process](./overview.md#main-protocol) for all apps. But can we do better? YES! 9 | 10 | What we can do - is to create a one big global system that will hold registered identities, and people will be able to reuse them across different apps (of course while preserving nullifiers & anonimity) - kinda like global Semaphore for Web2 Identities. 11 | 12 | In the next section I'll explain how we can do that. 13 | 14 | ## How it works 15 | 16 | In the end of [the overview](./overview.md) explanation I said: \\( \DeclareMathOperator{hash}{hash} \DeclareMathOperator{cm}{commitment} \DeclareMathOperator{hashc}{hashToCurve} \\) 17 | > instead of revealing nullifier we can reveal \\( \hash(\text{nullifier}, \text{AppID}) \\) - where \\( \text{AppID} \\) is a unique identifier of the app, and that's gonna be our real nullifier. 18 | 19 | That's actually the key for building such system. 20 | 21 | We can deploy one registry smart-contract. We'll set \\( \text{AppID} = 0 \\). We also gonna keep \\( \text{pseudonym} = \hash(s * G, \text{ AppID}) \\), where, \\( s \\) is "private key" of OPRF MPC, and \\( G = \hashc(\text{UserID}) \\). 22 | All the identities will be stored in Merkle Tree, and \\( \text{pubkey} = \hash(\text{pseudonym}, \cm_1) \\) will be stored in its leaves. 23 | For those who forgot, \\( \cm_1 = \hash(\text{UserID}, \text{ salt}) \\). 24 | 25 | Now, let's say we registered our github identity in our global system and there's an app that gives airdrop to their contributors (of course we want to claim airdrop anonymously). The airdrop app will need to set their own \\( \text{AppID} \\), different from 0, because 0 is already taken; this can get checked by registry smart-contract. It will also need to create a merkle tree of github usernames that are eligible for airdrop (our github username is in that list). 26 | 27 | **Now, to claim airdrop anonymously we'll need to create zk proof with the following parameters**: 28 | * Public: \\( \text{AppID}, \text{ nullifier}, \text{ registryRoot}, \text{ appRoot} \\) 29 | * Private: \\( \text{UserID, } sG, \text{ salt} \\) 30 | 31 | and the constraints: 32 | $$\text{pseudonym} \longleftarrow \hash(sG, \text{ 0})$$ $$\cm_1 \longleftarrow \hash(\text{UserID}, \text{ salt})$$ $$\text{pubkey} \longleftarrow \hash(\text{pseudonym}, \cm_1)$$ $$\text{registryRoot} = \text{merkleTreeVerify(pubkey)}$$ $$\text{appRoot} = \text{merkleTreeVerify(UserID)}$$ $$\text{nullifier} = \hash(sG, \text{ AppID})$$ 33 | 34 |
35 | 36 | As you can see, the pair \\( (sG, \text{ salt}) \\) will serve as an **action key**, and $sG$ itself will be a **viewing key**. 37 | 38 | ## Additional comments 39 | 40 | While reading this, you might ask the same questions we have right now. For example, how we can prevent users spamming OPRF? It also makes sense to make system even more general and give people an option to specify which OPRF provider they gonna use and of what id-provider they will use (there can be many even for the same id-provider, e.g. bridged by zkEmail, zkTLS & OpenPassport). 41 | While they are important, they are not in the scope of this blog post. 42 | 43 | -------------------------------------------------------------------------------- /packages/mpc/src/eth_utils.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::str::FromStr; 3 | 4 | use alloy::{ 5 | network::EthereumWallet, 6 | primitives::{Address, FixedBytes}, 7 | providers::ProviderBuilder, 8 | signers::local::PrivateKeySigner, 9 | sol, 10 | }; 11 | use dotenv::dotenv; 12 | use k256::{ProjectivePoint, Scalar}; 13 | 14 | use crate::utils::{projective_to_ecpoint, KEYS}; 15 | 16 | sol!( 17 | #[sol(rpc)] 18 | "../registry/src/Registry.sol" 19 | ); 20 | 21 | // Configuration struct to hold Ethereum settings 22 | pub struct EthConfig { 23 | pub eth_rpc_url: String, 24 | pub registry_address: Address, 25 | pub eth_private_key: String, 26 | } 27 | 28 | impl EthConfig { 29 | pub fn new() -> Result> { 30 | dotenv().ok(); 31 | 32 | let eth_rpc_url = env::var("ETH_RPC_URL").expect("ETH_RPC_URL must be set in .env file"); 33 | 34 | let registry_address = Address::from_str( 35 | &env::var("REGISTRY_ADDRESS").expect("REGISTRY_ADDRESS must be set in .env file"), 36 | )?; 37 | 38 | let eth_private_key = 39 | env::var("ETH_PRIVATE_KEY").expect("ETH_PRIVATE_KEY must be set in .env file"); 40 | 41 | Ok(Self { 42 | eth_rpc_url, 43 | registry_address, 44 | eth_private_key, 45 | }) 46 | } 47 | } 48 | 49 | // Convert K256 ProjectivePoint to Registry contract's bytes32[2] format 50 | pub fn point_to_bytes32_array(point: &ProjectivePoint) -> [FixedBytes<32>; 2] { 51 | let ec_point = projective_to_ecpoint(point); 52 | 53 | let x_bytes = hex::decode(&ec_point.x).unwrap(); 54 | let y_bytes = hex::decode(&ec_point.y).unwrap(); 55 | 56 | let mut x_fixed: [u8; 32] = [0; 32]; 57 | let mut y_fixed: [u8; 32] = [0; 32]; 58 | 59 | // Ensure we copy the right bytes (hex strings may have odd length) 60 | x_fixed[(32 - x_bytes.len())..].copy_from_slice(&x_bytes); 61 | y_fixed[(32 - y_bytes.len())..].copy_from_slice(&y_bytes); 62 | 63 | [FixedBytes::from(x_fixed), FixedBytes::from(y_fixed)] 64 | } 65 | 66 | // Register a node's public key in the Registry contract 67 | pub async fn register_node(private_key: &Scalar) -> Result> { 68 | let config = EthConfig::new()?; 69 | let public_key = ProjectivePoint::GENERATOR * *private_key; 70 | let public_key_bytes = point_to_bytes32_array(&public_key); 71 | 72 | let pk_signer: PrivateKeySigner = config.eth_private_key.parse()?; 73 | let wallet = EthereumWallet::new(pk_signer); 74 | let provider = ProviderBuilder::new() 75 | .wallet(wallet) 76 | .on_http(config.eth_rpc_url.parse()?); 77 | let registry = Registry::new(config.registry_address, provider); 78 | 79 | let tx_hash = registry 80 | .register(public_key_bytes) 81 | .send() 82 | .await? 83 | .watch() 84 | .await?; 85 | 86 | println!("Registered node. Tx hash: {}", tx_hash); 87 | 88 | Ok(true) 89 | } 90 | 91 | // Check if the node is registered in the Registry contract 92 | pub async fn check_node_registration() -> Result> { 93 | let config = EthConfig::new()?; 94 | let public_key = KEYS.1; 95 | let public_key_bytes = point_to_bytes32_array(&public_key); 96 | 97 | let provider = ProviderBuilder::new().on_http(config.eth_rpc_url.parse()?); 98 | let registry = Registry::new(config.registry_address, provider); 99 | 100 | let is_registered = registry.isRegistered(public_key_bytes).call().await?._0; 101 | 102 | Ok(is_registered) 103 | } 104 | -------------------------------------------------------------------------------- /packages/zk/oprf_commitment/src/main.nr: -------------------------------------------------------------------------------- 1 | use bignum::bignum::BigNum; 2 | use noir_bigcurve::curves::secp256k1::{Secp256k1, Secp256k1_Fq_Params, Secp256k1Scalar}; 3 | use std::{hash::poseidon2, ops::Add}; 4 | 5 | fn main( 6 | user_id: Field, 7 | salt: Field, 8 | random_scalar: BigNum<3, 256, Secp256k1_Fq_Params>, 9 | user_id_commitment: pub Field, 10 | oprf_commitment: pub Secp256k1, 11 | ) { 12 | // Verify the first commitment is a hash of the user_id and salt 13 | let hashed_user_data = poseidon2::Poseidon2::hash([user_id, salt], 2); 14 | assert(hashed_user_data == user_id_commitment); 15 | 16 | // Convert user_id to bytes for curve hashing 17 | let user_id_bytes: [u8; 32] = user_id.to_le_bytes(); 18 | 19 | // Hash user_id to a curve point 20 | let base_point = Secp256k1::hash_to_curve(user_id_bytes); 21 | 22 | // Convert the random scalar to the appropriate type 23 | let scalar = Secp256k1Scalar::from_bignum(random_scalar); 24 | 25 | // Compute the OPRF value (scalar multiplication) 26 | let computed_oprf = Secp256k1::msm([base_point], [scalar]); 27 | 28 | // Verify the OPRF commitment 29 | assert(computed_oprf == oprf_commitment); 30 | } 31 | 32 | #[test] 33 | fn test_valid_oprf_commitment() { 34 | // Test parameters 35 | let user_id = 12345; 36 | let salt = 67890; 37 | let random_scalar: BigNum<3, 256, Secp256k1_Fq_Params> = BigNum::from(2); 38 | 39 | // // Calculate the expected user_id_commitment 40 | let user_id_commitment = poseidon2::Poseidon2::hash([user_id, salt], 2); 41 | 42 | // // Calculate the expected oprf_commitment 43 | let user_id_bytes: [u8; 32] = user_id.to_le_bytes(); 44 | let base_point = Secp256k1::hash_to_curve(user_id_bytes); 45 | let scalar = Secp256k1Scalar::from_bignum(random_scalar); 46 | let oprf_commitment = Secp256k1::msm([base_point], [scalar]); 47 | 48 | println(random_scalar); 49 | println(oprf_commitment); 50 | 51 | // Test with valid parameters - should pass 52 | main( 53 | user_id, 54 | salt, 55 | random_scalar, 56 | user_id_commitment, 57 | oprf_commitment, 58 | ); 59 | } 60 | 61 | #[test(should_fail)] 62 | fn test_invalid_user_id_commitment() { 63 | // Test parameters 64 | let user_id = 12345; 65 | let salt = 67890; 66 | let random_scalar: BigNum<3, 256, Secp256k1_Fq_Params> = BigNum::from(42); 67 | 68 | // Calculate the expected oprf_commitment 69 | let user_id_bytes: [u8; 32] = user_id.to_le_bytes(); 70 | let base_point = Secp256k1::hash_to_curve(user_id_bytes); 71 | let scalar = Secp256k1Scalar::from_bignum(random_scalar); 72 | let oprf_commitment = Secp256k1::msm([base_point], [scalar]); 73 | 74 | // Create an invalid user_id_commitment (using wrong salt) 75 | let invalid_user_id_commitment = poseidon2::Poseidon2::hash([user_id, salt + 1], 2); 76 | 77 | // This should fail because the user_id_commitment is invalid 78 | main( 79 | user_id, 80 | salt, 81 | random_scalar, 82 | invalid_user_id_commitment, 83 | oprf_commitment, 84 | ); 85 | } 86 | 87 | #[test(should_fail)] 88 | fn test_invalid_oprf_commitment() { 89 | // Test parameters 90 | let user_id = 12345; 91 | let salt = 67890; 92 | let random_scalar: BigNum<3, 256, Secp256k1_Fq_Params> = BigNum::from(42); 93 | 94 | // Calculate the expected user_id_commitment 95 | let user_id_commitment = poseidon2::Poseidon2::hash([user_id, salt], 2); 96 | 97 | // Calculate an invalid oprf_commitment (using wrong scalar) 98 | let user_id_bytes: [u8; 32] = user_id.to_le_bytes(); 99 | let base_point = Secp256k1::hash_to_curve(user_id_bytes); 100 | let invalid_scalar = Secp256k1Scalar::from_bignum(random_scalar.add(BigNum::from(1))); 101 | let invalid_oprf_commitment = Secp256k1::msm([base_point], [invalid_scalar]); 102 | 103 | // This should fail because the oprf_commitment is invalid 104 | main( 105 | user_id, 106 | salt, 107 | random_scalar, 108 | user_id_commitment, 109 | invalid_oprf_commitment, 110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # 🔐 vOPRF-ID 4 | 5 | *A secure nullifiers using verifiable Oblivious Pseudorandom Functions* 6 | 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Tests](https://github.com/privacy-scaling-explorations/voprf-id/actions/workflows/build-test-fmt.yml/badge.svg)](https://github.com/privacy-scaling-explorations/voprf-id/actions/workflows/build-test-fmt.yml) 8 | 9 |
10 | 11 | ## 📝 Overview 12 | 13 | vOPRF-ID is a monorepo containing implementations for secure nullifier generation based on verifiable Oblivious Pseudorandom Functions (vOPRF). 14 | 15 | You can read more about the protocol in our [docs](https://privacy-scaling-explorations.github.io/vOPRF-ID/) 16 | 17 | ## 👨‍💻 How to run 18 | 19 | This monorepo contains implementation of all necessary components: 20 | * [X] [vOPRF MPC Node implementation](./packages/mpc/) 21 | * [X] [Client Side zk Circuits for verifiability](./packages/zk/) 22 | * [X] [Registry smart-contract for OPRF nodes](./packages/registry/) 23 | * [X] [Optional check script for OPRF nodes](./packages/scripts/) 24 | 25 | You can run a vOPRF MPC node either locally or in a docker container. 26 | 27 | ### To run locally 28 | 29 | #### 0. Deploy a registry smart-contract 30 | This step is optional, as you can reuse an already deployed contract, where OPRF nodes can store their public keys. 31 | 32 | If you want to deploy your own contract: 33 | 34 | ```bash 35 | cd packages/registry 36 | ``` 37 | 38 | Then create the .env file, and fill it as described in [.env.example](/packages/registry/.env.example). 39 | 40 | To deploy, run: 41 | 42 | ```bash 43 | forge script script/Registry.s.sol:RegistryScript --rpc-url "" --broadcast --private-key "" -vvvv 44 | ``` 45 | 46 | Get the deployed contract address, as we'll use it to run OPRF nodes. 47 | 48 | #### 1. Initialize phase 49 | 50 | First, you need to create an .env file as described in [.env.example](./.env.example). 51 | For the "REGISTRY_ADDRESS" field - you can use one of the deployed contracts. 52 | You should create the .env file in the project root (as well as run other commands). 53 | 54 | ```bash 55 | cargo run --release -- initialize # This will create an OPRF node private key, store the public key in the registry, and save the private key to a file 56 | ``` 57 | 58 | #### 2. Run the node 59 | 60 | To run the node: 61 | 62 | ```bash 63 | cargo run --release -- serve # This will run a vOPRF node 64 | ``` 65 | 66 | ### Run in a docker container 67 | 68 | ```bash 69 | docker build -t voprf . --no-cache 70 | docker run -it --cap-add=NET_ADMIN --name=party1 -p 8081:8080 voprf 71 | ``` 72 | Even if you run in the Docker - you'll still need to set a Registry contract, and initialize the node. 73 | 74 | --- 75 | 76 | ### ZK part 77 | 78 | ZK gives us verifiability of OPRF output. You can see how it works in a [protocol overview](https://privacy-scaling-explorations.github.io/vOPRF-ID/overview.html). 79 | The project contains two separate zk circuits as it's described in the protocol. 80 | To run the first one you have to fill the [Prover.toml](./packages/zk/oprf_commitment/Prover.toml) file first. 81 | Then, to generate witness, vkey and zk proof: 82 | 83 | ```bash 84 | cd packages/zk/oprf_commitment 85 | nargo execute 86 | bb prove -b ./target/oprf_commitment.json -w ./target/oprf_commitment.gz -o ./target/proof 87 | bb write_vk -b ./target/oprf_commitment.json -o ./target 88 | bb verify -k ./target/vk -p ./target/proof 89 | ``` 90 | 91 | You have to put vkey to the [/generated](./packages/mpc/generated/) dir, so that OPRF nodes can verify the first circuit. 92 | 93 | You can send the generated proof to the vOPRF node using the [testing script](./packages/scripts/test_api.py): 94 | 95 | ```bash 96 | cd packages/scripts 97 | python3 -m venv .venv 98 | source .venv/bin/activate 99 | pip3 install requests 100 | python3 test_api.py --address localhost --port 8080 101 | ``` 102 | 103 | -- 104 | 105 | To generate a final zk proof that contains `nullifier` - you have to fill the inputs in the [Prover.toml](./packages/zk/oprf_nullifier/Prover.toml) file. 106 | And run: 107 | 108 | ```bash 109 | cd packages/zk/oprf_nullifier 110 | nargo execute 111 | bb prove -b ./target/oprf_nullifier.json -w ./target/oprf_nullifier.gz -o ./target/proof 112 | bb write_vk -b ./target/oprf_nullifier.json -o ./target 113 | bb verify -k ./target/vk -p ./target/proof 114 | ``` 115 | 116 | ## 👀 P.S 117 | 118 | Current version slightly differs from the specification, for example - using Secp256k1 instead of BabyJubJub. In subsequent versions, the implementation will be updated and improved for usability (as well as benchmarks). 119 | -------------------------------------------------------------------------------- /docs/src/zk-circuits.md: -------------------------------------------------------------------------------- 1 | This spec focuses on how a user commits to a userID with a random scalar `r` and proves consistency with their existing commitment (which comes from a separate ZK Email/TLSNotary proof). The idea is to ensure that a user cannot arbitrarily switch userIDs while reusing the same randomness. 2 | 3 | # Web2 Nullifiers using vOPRF — ZK Circuit Specification 4 | 5 | ## Overview 6 | 7 | This specification describes two ZK circuits that work together with an existing Auth Proof: 8 | 9 | 1. **OPRF Commitment Circuit**: Proves that a user's commitment to the OPRF server is consistent with their previously verified identity. 10 | 2. **Nullifier Generation Circuit**: Proves that a nullifier is correctly derived from the OPRF response and consistent with the user's identity. 11 | 12 | The Auth Proof (e.g., ZK Email/TLSNotary proof) is an external component that provides `commitment1`, a commitment to the user's identity. 13 | 14 | ## High-Level Flow 15 | 16 | 1. User already has an **Auth Proof** with salted commitment to UserID as a public output: 17 | ``` 18 | commitment1 = hash(UserID, salt) 19 | ``` 20 | 21 | 2. User creates an **OPRF Commitment Proof** to send to the OPRF server: 22 | ``` 23 | commitment2 = r * G where G = hashToCurve(UserID) 24 | ``` 25 | where r is random scalar. This proof ensures `commitment2` is consistent with the same UserID in `commitment1`. 26 | 27 | 3. OPRF replies with: 28 | ``` 29 | oprf_response = s * commitment2 30 | ``` 31 | where `s` is a private key of OPRF node; and also replies with proof of correctness (e.g., a Chaum-Pedersen proof). 32 | 33 | 4. User creates a **Nullifier Generation Proof** that: 34 | - Takes `commitment1` and produces `nullifier` 35 | - Verifies the OPRF response is valid 36 | - Computes `nullifier = r^-1 * oprf_response` 37 | 38 | ## Circuit 1: OPRF Commitment Circuit 39 | 40 | This circuit proves that the commitment sent to the OPRF server is consistent with the user's verified identity. 41 | 42 | ### Public Inputs 43 | 1. `commitment1` 44 | From the Auth Proof, computed as `hash(UserID, salt)`. 45 | 46 | 2. `commitment2` 47 | The commitment to send to the OPRF, computed as `r * G` where `G = hashToCurve(UserID)`. 48 | 49 | ### Private Inputs 50 | 1. `UserID` 51 | The user's identity string (email, TLSNotary-verified name, etc.). 52 | 53 | 2. `salt` 54 | The salt used in the Auth Proof commitment. 55 | 56 | 3. `r` 57 | A random scalar chosen by the user. 58 | 59 | ### Circuit Constraints 60 | 1. **Auth Proof Consistency** 61 | ``` 62 | commitment1 == hash(UserID, salt) 63 | ``` 64 | 65 | 2. **OPRF Commitment Calculation** 66 | ``` 67 | G = hashToCurve(UserID) 68 | commitment2 == r * G 69 | ``` 70 | 71 | ### Pseudocode 72 | ``` 73 | // OPRF Commitment Circuit 74 | 75 | function ProveOPRFCommitment( 76 | // Public inputs 77 | commitment1, 78 | commitment2 79 | ) { 80 | // Private inputs 81 | user_id; 82 | salt; 83 | r; 84 | 85 | // 1. Verify consistency with Auth Proof 86 | computed_commitment1 = Hash(user_id, salt); 87 | Assert(computed_commitment1 == commitment1); 88 | 89 | // 2. Verify OPRF commitment 90 | G = HashToCurve(user_id); 91 | computed_commitment2 = ScalarMul(G, r); 92 | Assert(computed_commitment2 == commitment2); 93 | } 94 | ``` 95 | 96 | ## Circuit 2: Nullifier Generation Circuit 97 | 98 | This circuit proves that the nullifier is correctly derived from the OPRF response and consistent with the user's identity. 99 | 100 | ### Public Inputs 101 | 1. `commitment1` 102 | From the Auth Proof, computed as `hash(UserID, salt)`. 103 | 104 | 2. `nullifier` 105 | The final nullifier value computed as `r^-1 * oprf_response`. 106 | 107 | ### Private Inputs 108 | 1. `UserID` 109 | The user's identity string. 110 | 111 | 2. `salt` 112 | The salt used in the Auth Proof commitment. 113 | 114 | 3. `r` 115 | The random scalar used in the OPRF commitment. 116 | 117 | 4. `oprf_response` 118 | The response from the OPRF server. 119 | 120 | 5. `chaum_pedersen_proof` 121 | Proof of correctness for the OPRF response. 122 | 123 | ### Circuit Constraints 124 | 1. **Auth Proof Consistency** 125 | ``` 126 | commitment1 == hash(UserID, salt) 127 | ``` 128 | 129 | 2. **OPRF Response Verification** 130 | ``` 131 | ChaumPedersenVerify(oprf_response, chaum_pedersen_proof) 132 | ``` 133 | 134 | 3. **Nullifier Calculation** 135 | ``` 136 | nullifier == r^-1 * oprf_response 137 | ``` 138 | 139 | ### Pseudocode 140 | ``` 141 | // Nullifier Generation Circuit 142 | 143 | function ProveNullifier( 144 | // Public inputs 145 | commitment1, 146 | nullifier 147 | ) { 148 | // Private inputs 149 | user_id; 150 | salt; 151 | r; 152 | oprf_response; 153 | chaum_pedersen_proof; 154 | 155 | // 1. Verify consistency with Auth Proof 156 | computed_commitment1 = Hash(user_id, salt); 157 | Assert(computed_commitment1 == commitment1); 158 | 159 | // 2. Verify OPRF response 160 | Assert(ChaumPedersenVerify(oprf_response, chaum_pedersen_proof)); 161 | 162 | // 3. Calculate nullifier 163 | r_inverse = InverseScalar(r); 164 | computed_nullifier = ScalarMul(oprf_response, r_inverse); 165 | Assert(computed_nullifier == nullifier); 166 | } 167 | ``` 168 | 169 | These circuits can be implemented in various ZK proving systems such as Groth16, PLONK, Bulletproofs, or others, depending on the specific requirements of the application. 170 | -------------------------------------------------------------------------------- /packages/zk/oprf_nullifier/src/main.nr: -------------------------------------------------------------------------------- 1 | use bignum::{BigNum, bignum::BigNumTrait}; 2 | use noir_bigcurve::BigCurveTrait; 3 | use noir_bigcurve::curves::secp256k1::{Secp256k1, Secp256k1Fr, Secp256k1Scalar}; 4 | use noir_bigcurve::scalar_field::ScalarFieldTrait; 5 | use std::{hash::{poseidon2, sha256}, ops::Div}; 6 | 7 | fn main( 8 | user_id: Field, 9 | salt: Field, 10 | r: Secp256k1Fr, 11 | user_id_commitment: pub Field, 12 | oprf_response: [Secp256k1; 3], 13 | chaum_pedersen_proof: [DleqProof; 3], 14 | oprf_pubkey: pub [Secp256k1; 3], 15 | ) -> pub Field { 16 | let r_scalar = Secp256k1Scalar::from_bignum(r); 17 | // 1. Verify consistency with Auth Proof 18 | let hashed_user_data = poseidon2::Poseidon2::hash([user_id, salt], 2); 19 | assert(hashed_user_data == user_id_commitment); 20 | 21 | // 2. Calculate oprf_commitment 22 | let user_id_bytes: [u8; 32] = user_id.to_le_bytes(); 23 | let base_point = Secp256k1::hash_to_curve(user_id_bytes); 24 | let oprf_commitment = Secp256k1::msm([base_point], [r_scalar]); 25 | 26 | // 3. Verify consistency with OPRF 27 | for i in 0..3 { 28 | dleq_proof_verification( 29 | Secp256k1::one(), 30 | oprf_commitment, 31 | oprf_pubkey[i], 32 | oprf_response[i], 33 | chaum_pedersen_proof[i], 34 | ); 35 | } 36 | 37 | // 4. Calculate nullifier: Poseidon({r^(-1) * oprf_response}.x) 38 | let one: Secp256k1Fr = BigNum::from(1); 39 | let r_inverse: Secp256k1Fr = one.div(r); 40 | let oprf = oprf_response[0] + oprf_response[1] + oprf_response[2]; 41 | let nullifier = Secp256k1::msm([oprf], [Secp256k1Scalar::from_bignum(r_inverse)]); 42 | let x = Field::from_le_bytes::<32>(nullifier.x.to_le_bytes::<32>()); 43 | let nullifier = poseidon2::Poseidon2::hash([x], 1); 44 | 45 | nullifier 46 | } 47 | 48 | fn sha256_6_points(points: [Secp256k1; 6]) -> [u8; 32] { 49 | let mut data: [u8; 65 * 6] = [0; 65 * 6]; 50 | 51 | // Process each point 52 | for i in 0..6 { 53 | // Start position for this point's data 54 | let offset = i * 65; 55 | 56 | // Add 0x04 marker for uncompressed point 57 | data[offset] = 0x04; 58 | 59 | // Get x and y coordinates as bytes 60 | let x_bytes = points[i].x.to_le_bytes::<32>(); 61 | let y_bytes = points[i].y.to_le_bytes::<32>(); 62 | 63 | // Copy x coordinate bytes 64 | for j in 0..32 { 65 | data[offset + 1 + j] = x_bytes[31 - j]; 66 | } 67 | 68 | // Copy y coordinate bytes 69 | for j in 0..32 { 70 | data[offset + 33 + j] = y_bytes[31 - j]; 71 | } 72 | } 73 | 74 | // Hash the concatenated data 75 | sha256(data) 76 | } 77 | 78 | struct DleqProof { 79 | c: Secp256k1Fr, 80 | s: Secp256k1Fr, 81 | } 82 | 83 | fn dleq_proof_verification( 84 | g: Secp256k1, 85 | h: Secp256k1, 86 | y: Secp256k1, 87 | z: Secp256k1, 88 | proof: DleqProof, 89 | ) { 90 | let s_scalar = Secp256k1Scalar::from_bignum(proof.s); 91 | let c_scalar = Secp256k1Scalar::from_bignum(proof.c); 92 | let a_prime = Secp256k1::msm([g, y], [s_scalar, c_scalar]); 93 | let b_prime = Secp256k1::msm([h, z], [s_scalar, c_scalar]); 94 | 95 | let hash = sha256_6_points([g, h, y, z, a_prime, b_prime]); 96 | 97 | let hash_bignum: Secp256k1Fr = BigNum::from_be_bytes(hash); 98 | assert(hash_bignum == proof.c); 99 | } 100 | 101 | #[test] 102 | fn test_dleq_proof_verification() { 103 | let g = Secp256k1 { 104 | x: BigNum::from_be_bytes::<32>([ 105 | 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0, 0x62, 0x95, 0xce, 0x87, 106 | 0x0b, 0x07, 0x02, 0x9b, 0xfc, 0xdb, 0x2d, 0xce, 0x28, 0xd9, 0x59, 0xf2, 0x81, 0x5b, 107 | 0x16, 0xf8, 0x17, 0x98, 108 | ]), 109 | y: BigNum::from_be_bytes::<32>([ 110 | 0x48, 0x3a, 0xda, 0x77, 0x26, 0xa3, 0xc4, 0x65, 0x5d, 0xa4, 0xfb, 0xfc, 0x0e, 0x11, 111 | 0x08, 0xa8, 0xfd, 0x17, 0xb4, 0x48, 0xa6, 0x85, 0x54, 0x19, 0x9c, 0x47, 0xd0, 0x8f, 112 | 0xfb, 0x10, 0xd4, 0xb8, 113 | ]), 114 | is_infinity: false, 115 | }; 116 | let h = Secp256k1 { 117 | x: BigNum::from_be_bytes::<32>([ 118 | 0x87, 0xdd, 0x0a, 0x2e, 0x88, 0x0b, 0x43, 0x91, 0x6d, 0x11, 0x51, 0x17, 0x97, 0xfc, 119 | 0x96, 0x39, 0xfa, 0x44, 0xeb, 0xec, 0x2e, 0x36, 0xee, 0x7f, 0x71, 0x1d, 0x51, 0x17, 120 | 0x45, 0x50, 0x28, 0x34, 121 | ]), 122 | y: BigNum::from_be_bytes::<32>([ 123 | 0x43, 0xf5, 0x8f, 0x22, 0x1b, 0x1c, 0x62, 0x78, 0x8c, 0x28, 0xbf, 0x8b, 0x11, 0xbb, 124 | 0x27, 0x1f, 0xb1, 0xf4, 0x66, 0xd5, 0xe4, 0xee, 0x56, 0xd1, 0x64, 0x94, 0x14, 0xd1, 125 | 0xca, 0x02, 0x7b, 0xea, 126 | ]), 127 | is_infinity: false, 128 | }; 129 | let y = Secp256k1 { 130 | x: BigNum::from_be_bytes::<32>([ 131 | 0xd2, 0x47, 0xa8, 0x6c, 0x95, 0xbd, 0xfb, 0x73, 0x08, 0x89, 0x30, 0xd1, 0x27, 0x5e, 132 | 0xec, 0x5e, 0x81, 0x9f, 0x4f, 0x54, 0x92, 0x86, 0x6f, 0xde, 0x6a, 0x32, 0xfc, 0xe9, 133 | 0x1f, 0xce, 0x60, 0x6f, 134 | ]), 135 | y: BigNum::from_be_bytes::<32>([ 136 | 0xe0, 0x9f, 0xd5, 0x13, 0xa0, 0xbd, 0x5e, 0x50, 0xfc, 0xf3, 0xc0, 0x57, 0xde, 0x93, 137 | 0x8c, 0x53, 0xe8, 0x7f, 0x33, 0x69, 0x46, 0x79, 0x2f, 0x8b, 0xfd, 0xe2, 0xba, 0x96, 138 | 0x47, 0xff, 0x6b, 0xb1, 139 | ]), 140 | is_infinity: false, 141 | }; 142 | let z = Secp256k1 { 143 | x: BigNum::from_be_bytes::<32>([ 144 | 0x9b, 0x61, 0xb4, 0xc7, 0xb5, 0x1d, 0x4a, 0x9b, 0xc9, 0xa3, 0xd9, 0x94, 0x19, 0x20, 145 | 0x5a, 0x79, 0x03, 0x6e, 0xf2, 0xe8, 0x4a, 0xf6, 0x7b, 0x12, 0xa5, 0x4c, 0x45, 0x7d, 146 | 0xa6, 0x1b, 0x40, 0x31, 147 | ]), 148 | y: BigNum::from_be_bytes::<32>([ 149 | 0xf2, 0xd8, 0xf1, 0xe3, 0x78, 0xc9, 0xdd, 0x88, 0x35, 0x4f, 0x95, 0x8d, 0x7b, 0xd4, 150 | 0x82, 0xb8, 0x25, 0xbf, 0xa8, 0x92, 0x84, 0x6b, 0x63, 0x90, 0x46, 0x3c, 0xc1, 0x4c, 151 | 0x9c, 0x1a, 0x9b, 0xc3, 152 | ]), 153 | is_infinity: false, 154 | }; 155 | let c = [ 156 | 39, 239, 139, 102, 162, 58, 212, 15, 66, 214, 5, 12, 112, 151, 201, 200, 171, 208, 32, 220, 157 | 134, 170, 225, 29, 232, 145, 128, 145, 49, 140, 118, 213, 158 | ]; 159 | let c: Secp256k1Fr = BigNum::from_be_bytes(c); 160 | 161 | let s = [ 162 | 212, 126, 161, 189, 151, 214, 139, 31, 86, 128, 147, 191, 115, 38, 241, 211, 34, 103, 44, 163 | 66, 114, 80, 158, 1, 47, 55, 107, 115, 217, 173, 142, 128, 164 | ]; 165 | let s: Secp256k1Fr = BigNum::from_be_bytes(s); 166 | 167 | dleq_proof_verification(g, h, y, z, DleqProof { c, s }); 168 | } 169 | -------------------------------------------------------------------------------- /docs/src/overview.md: -------------------------------------------------------------------------------- 1 | # Web2 Nullifiers using vOPRF 2 | 3 | ## Abstract 4 | 5 | Recent development of protocols, that allow us to make Web2 data portable & verifiable such as [ZK Email](https://prove.email/) or [TLSNotary](https://tlsnotary.org/) opens new use-cases and opportunities for us. For example, we can make proof of ownership of some x.com username or email address and verify it on-chain with ZK Email. Projects like [OpenPassport](https://www.openpassport.app/), [Anon Aadhaar](https://github.com/anon-aadhaar/anon-aadhaar) (and others) are also the case. 6 | 7 | We can also do more complex things, e.g. forum where holders of @ethereum.org email addresses will be able to post anonymously, using zk proofs of membership. 8 | 9 | Projects like [Semaphore](https://semaphore.pse.dev/) helps us to build pseudonymous[^1] systems with membership proofs for "Web3 identities". 10 | 11 | In Semaphore users have their \\( \text{public_id} = \text{hash(secret, nullifier)} \\), and \\( \text{nullifier} \\) actually serves as an id of user - we still don't know who exactly used the system, but we'll be able to find out if they used it more than once. But the thing is **we don't have any nullifiers** in ZK Email/TLS, etc. - that's why it's not possible to create such systems for Web2 identities out of the box. The solution for that is vOPRF. 12 | 13 | vOPRFs (verifiable Oblivious PseudoRandom Functions) - are protocols that allow a client to generate deterministic random based on their input, while keeping it private. So, there're two parties in the protocol - first one as I said is a client, and second one is a OPRF network (usually [MPC](https://en.wikipedia.org/wiki/Secure_multi-party_computation) is used for that). 14 | 15 | With OPRF we'll be able to generate nullifiers for Web2 ID's': users will just need to ask the MPC to generate it, e.g., based on their email address (without revealing plain text of course). 16 | 17 | We can do many things based on that: 18 | * anonymous votings with ported Web2 identities; 19 | * anonymous airdrops - projects can just list github accounts, that are eligible for airdrop, and users will be able to claim (only once) with proof of github using ZK Email; 20 | * pseudonymous forums - I mentioned it before, but with OPRF we can have pseudonyms and limit user to only one account + it might be easier to track & ban spammers 21 | * ... many more. 22 | 23 | ## Detailed explanation 24 | 25 | ### Main protocol: 26 | 27 | There are three parties involved in protocol: \\( \DeclareMathOperator{cm}{commitment} \DeclareMathOperator{hash}{hash} \DeclareMathOperator{hashc}{hashToCurve} \DeclareMathOperator{chaum}{chaumPedersenVerify} \\) 28 | * **User**, that is trying to do some action with their Web2 identity (e.g. google account) pseudonymously (e.g. anonymously participate in voting). 29 | * **OPRF** Server/Network (will just call OPRF). 30 | * We use MPC, because in the case of having only one node generating nullifiers for users - it'll be able to bruteforce and find out which Web2 identity corresponds given nullifier. Every node has to commit to their identity somehow - e.g., by storing their EC public key on a blockchain. 31 | For simplicity I'll explain the case with one node OPRF first, and in [OPRF-MPC section](#oprf-mpc) I'll explain how we can extend it to multiple nodes. 32 | * **Ethereum** (or any other smart-contract platform) 33 | 34 | * Nodes must be run in a TEE to add additional security 35 | * All of the EC operations are done on BabyJubJub Curve 36 | * For the \\( \hash \\) operation we use Poseidon Hash 37 | 38 | 1. User makes ZK Email/TLS auth proof with salted commitment to UserID (or email, name, etc.) as a public output: 39 | $$\cm_1 = \hash(\text{UserID}, \text{salt})$$ I'll call it just **Auth proof** 40 | 41 | 2. User sends new commitment to their UserID to OPRF: 42 | $$\cm_2 = r * G$$ where \\( G = \hashc(\text{UserID}) \\), and \\( r \\) is random scalar.
43 | We want to prevent users from sending arbitrary requests (because they would be able to attack the system by sending commitments to different user's identities), so user must additionally provide a small zk proof, that checks the relation between the commitments, where: 44 | * Public inputs: \\( \cm_1 \\), \\( \cm_2 \\) 45 | * Private inputs: \\( \text{UserID} \\), \\( \text{salt} \\), \\( r \\) 46 | 47 | and constraints: 48 | $$\cm_1 = \hash(\text{UserID}, \text{salt})$$ $$G = \hashc(\text{UserID})$$ $$\cm_2 = r * G$$ 49 | 50 | *It’s possible to do step 1 and 2 in one circuit, but that would require a lot of changes in i.e. ZK Email/TLS circuit, which is not desirable.* 51 | 52 | 3. OPRF replies with: 53 | $$\text{oprf_response} = s * \cm_2$$ where \\( s \\) is a private key of OPRF node; and also replies with proof of correctness of such multiplication, which is in this case might be a Chaum-Pedersen proof of discrete log equality (check [this blog post](https://muens.io/chaum-pedersen-protocol)) on that. 54 | 55 | 56 | 59 | 60 | 4. User creates zk proof with the following parameters: 61 | * Public outputs: \\( \cm_1, \text{ nullifier} \\) 62 | * Private inputs: \\( r, \text{ UserID}, \text{ salt}, \text{ chaum_pedersen_proof}, \text{ oprf_response} \\) 63 | 64 | and validates that: 65 | $$\cm_1 = \hash(\text{UserID}, \text{ salt})$$ $$G \longleftarrow \hashc(\text{UserID})$$ $$\chaum (\text{oprf_response})$$ $$\text{nullifier} \longleftarrow r^{-1} * \text{ oprf_response}$$ 66 | 67 | ## Nullifiers 68 | 69 | That's it, we have nullifier - and now users can use the system as in Semaphore. 70 | If we go a bit further, it's worth to mention that users shouldn't reveal nullifier, because it's linked with their \\( \text{UserID} \\); and if they use the same \\( \text{UserID} \\) in different apps - it'll be possible to track them. 71 | We can do it a bit differently - instead of revealing nullifier we can reveal \\( \hash(\text{nullifier}, \text{AppID}) \\) - where \\( \text{AppID} \\) is a unique identifier of the app, and that's gonna be our real nullifier. 72 | 73 | ### OPRF MPC 74 | 75 | In the example above we used only one node OPRF, but we can easily extend it to multiple nodes. 76 | There're many ways to do that, I'll explain few: 77 | 1. N of N MPC: 78 | 79 | 1.1. All nodes have their own pair of keys. 80 | 81 | 1.2. Every node does step 3 individually: we get \\( \text{oprf_response}_i = s_i * r * G \\) 82 | 83 | 1.3. On step 4 we verify \\( \text{chaum_pedersen_proof} \\) for every node 84 | 85 | 1.4 We calculate \\( \text{nullifier}_i = \text{oprf_response}_i * r^{-1} \\) 86 | 87 | 1.5 We calculate \\( \sum_{i=1}^N\text{nullifier}_i = s * G \\) 88 | 89 | *Important to mention that we have to verify/calculate everything in the circuit* 90 | 2. M of N MPC using linear combination for Shamir Secret Sharing: 91 | 92 | Similar to N of N MPC, but we need only M shares 93 | 3. Interactive M of N threshold MPC: 94 | 95 | Similar to 2, but nodes calculate one common response and one common Chaum-Pedersen proof. With that - we won’t need to verify separate Chaum-Pedersen proofs in zk circuit, we’ll only need to verify one, though with this scheme we’ll need to care about coordination between MPC nodes. 96 | 4. Using BLS: 97 | 98 | 3.1. Calculate common public key of all OPRF nodes by summing individual public keys 99 | 100 | 3.2. The same as in N of N MPC case 101 | 102 | 3.3., 3.4, 3.5. - The same as in N of N MPC case, **BUT** we can do it outside the circuit 103 | 104 | 3.6. Verify BLS pairing in the circuit 105 | 106 | There are techniques such as DKG protocol and Resharing protocol, 107 | that can be useful for production ready protocol. 108 | With them you can build a permissionless MPC network, where it’ll be possible to change the MPC node set each epoch, while having the same common secret key. 109 | You can read more about them in Nanak’s post on OPRF here. 110 | 111 | *For the alpha release we’ll stick to the N of N MPC setup, where N = 3, without DKG & Resharing.* 112 | 113 | ### Additional info 114 | 115 | The same protocol can be applied to other Web2<->Web3 bridges, such as OpenPassport, TLS Notary, ZK JWT, etc. 116 | 117 | [^1]: Pseudonymous system - a privacy-preserving system, where users' transactions are linked to unique identifiers (pseudonyms), but not their actual identities. 118 | -------------------------------------------------------------------------------- /packages/mpc/src/utils.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use std::fs; 4 | use std::process::Command; 5 | 6 | use crate::api::Error; 7 | use k256::{ 8 | elliptic_curve::{ 9 | rand_core::OsRng, 10 | sec1::{FromEncodedPoint, ToEncodedPoint}, 11 | PrimeField, 12 | }, 13 | AffinePoint, EncodedPoint, FieldBytes, ProjectivePoint, Scalar, 14 | }; 15 | use num_bigint::BigUint; 16 | use num_traits::{FromPrimitive, Num}; 17 | use once_cell::sync::Lazy; 18 | use serde::{Deserialize, Serialize}; 19 | use sha2::{Digest, Sha256}; 20 | use uuid::Uuid; 21 | 22 | const PRIVATE_KEY_FILE: &str = "./private_key.txt"; 23 | 24 | pub static KEYS: Lazy<(Scalar, ProjectivePoint)> = Lazy::new(|| { 25 | let data = fs::read(PRIVATE_KEY_FILE) 26 | .expect("Failed to read private key file. Try running with 'initialize' command first."); 27 | let scalar_bytes = FieldBytes::from_slice(&data); 28 | let private_key = Scalar::from_repr_vartime(*scalar_bytes).expect("invalid scalar bytes"); 29 | let public_key = ProjectivePoint::GENERATOR * private_key; 30 | 31 | (private_key, public_key) 32 | }); 33 | 34 | #[derive(Debug, Serialize, Deserialize)] 35 | pub struct ECPoint { 36 | pub x: String, 37 | pub y: String, 38 | } 39 | 40 | pub fn verify_zk_proof(proof: &[u8]) -> Result<(), Error> { 41 | // Create a unique temporary file for the proof 42 | let temp_proof_path = format!("./packages/mpc/generated/temp_proof_{}", Uuid::new_v4()); 43 | 44 | // Write proof string to the temporary file 45 | fs::write(&temp_proof_path, proof).map_err(|e| { 46 | eprintln!("Failed to write proof to temp file: {}", e); 47 | Error::InvalidProof 48 | })?; 49 | 50 | // Execute the verification command 51 | let output = Command::new("bb") 52 | .arg("verify") 53 | .arg("-k") 54 | .arg("./packages/mpc/generated/vk") 55 | .arg("-p") 56 | .arg(&temp_proof_path) 57 | .output() 58 | .map_err(|e| { 59 | // Clean up temp file before returning error 60 | let _ = fs::remove_file(&temp_proof_path); 61 | eprintln!("Failed to execute verification command: {}", e); 62 | Error::InvalidProof 63 | })?; 64 | 65 | // Clean up temporary file 66 | let _ = fs::remove_file(&temp_proof_path); 67 | 68 | // Check if verification succeeded 69 | if output.status.success() { 70 | Ok(()) 71 | } else { 72 | let error_msg = String::from_utf8_lossy(&output.stderr); 73 | eprintln!("Proof verification failed: {}", error_msg); 74 | Err(Error::InvalidProof) 75 | } 76 | } 77 | 78 | #[derive(Debug, Serialize, Deserialize)] 79 | pub struct DleqProof { 80 | pub c: String, // Challenge 81 | pub s: String, // Response 82 | } 83 | 84 | impl DleqProof { 85 | // Create a DLEQ proof for (private_key, private_key * G) and (private_key, private_key * point) 86 | pub fn new(point: &ProjectivePoint) -> Self { 87 | // Get our private key and points 88 | let private_key = KEYS.0; 89 | let g = ProjectivePoint::GENERATOR; // First base point G 90 | let h = *point; // Second base point (input point) 91 | 92 | // Our public key Y = private_key * G 93 | let y = KEYS.1; 94 | // Z = private_key * H (the result we're returning in the API) 95 | let z = h * private_key; 96 | 97 | // Generate a random nonce for the proof 98 | let k = Scalar::generate_vartime(&mut OsRng); 99 | 100 | // Calculate k*G and k*H 101 | let a = g * k; // A = k*G 102 | let b = h * k; // B = k*H 103 | 104 | // Convert points to affine for serialization 105 | let g_affine = AffinePoint::from(g).to_encoded_point(false); 106 | let h_affine = AffinePoint::from(h).to_encoded_point(false); 107 | let y_affine = AffinePoint::from(y).to_encoded_point(false); 108 | let z_affine = AffinePoint::from(z).to_encoded_point(false); 109 | let a_affine = AffinePoint::from(a).to_encoded_point(false); 110 | let b_affine = AffinePoint::from(b).to_encoded_point(false); 111 | 112 | // Create a secure challenge using SHA-256 hash of all relevant values 113 | let mut hasher = Sha256::new(); 114 | hasher.update(g_affine.as_bytes()); 115 | hasher.update(h_affine.as_bytes()); 116 | hasher.update(y_affine.as_bytes()); 117 | hasher.update(z_affine.as_bytes()); 118 | hasher.update(a_affine.as_bytes()); 119 | hasher.update(b_affine.as_bytes()); 120 | 121 | let hash_result = hasher.finalize(); 122 | 123 | // Convert hash to scalar 124 | let mut scalar_bytes = [0u8; 32]; 125 | scalar_bytes.copy_from_slice(&hash_result[..32]); 126 | 127 | // Create challenge scalar from hash 128 | let c_scalar = Scalar::from_repr_vartime(FieldBytes::from(scalar_bytes)).unwrap(); 129 | 130 | // Calculate response s = k - c * private_key 131 | let s = k - c_scalar * private_key; 132 | 133 | Self { 134 | c: hex::encode(c_scalar.to_bytes()), 135 | s: hex::encode(s.to_bytes()), 136 | } 137 | } 138 | 139 | // Verify the DLEQ proof: checks that (g, y) and (h, z) share the same discrete log 140 | pub fn verify( 141 | &self, 142 | g: &ProjectivePoint, 143 | h: &ProjectivePoint, 144 | y: &ProjectivePoint, 145 | z: &ProjectivePoint, 146 | ) -> bool { 147 | // Parse the challenge and response from hex strings 148 | let c_bytes = match hex::decode(&self.c) { 149 | Ok(bytes) => bytes, 150 | Err(_) => return false, 151 | }; 152 | 153 | let s_bytes = match hex::decode(&self.s) { 154 | Ok(bytes) => bytes, 155 | Err(_) => return false, 156 | }; 157 | 158 | // Convert bytes to field bytes 159 | if c_bytes.len() != 32 || s_bytes.len() != 32 { 160 | return false; 161 | } 162 | 163 | let mut c_arr = [0u8; 32]; 164 | let mut s_arr = [0u8; 32]; 165 | c_arr.copy_from_slice(&c_bytes); 166 | s_arr.copy_from_slice(&s_bytes); 167 | 168 | // Convert to scalars 169 | let c_field_bytes = FieldBytes::from(c_arr); 170 | let s_field_bytes = FieldBytes::from(s_arr); 171 | 172 | let c_scalar = match Scalar::from_repr_vartime(c_field_bytes) { 173 | Some(s) => s, 174 | None => return false, 175 | }; 176 | 177 | let s_scalar = match Scalar::from_repr_vartime(s_field_bytes) { 178 | Some(s) => s, 179 | None => return false, 180 | }; 181 | 182 | // Reconstruct A' = s*G + c*Y 183 | let a_prime = (*g * s_scalar) + (*y * c_scalar); 184 | 185 | // Reconstruct B' = s*H + c*Z 186 | let b_prime = (*h * s_scalar) + (*z * c_scalar); 187 | 188 | // Convert points to affine for serialization 189 | let g_affine = AffinePoint::from(*g).to_encoded_point(false); 190 | let h_affine = AffinePoint::from(*h).to_encoded_point(false); 191 | let y_affine = AffinePoint::from(*y).to_encoded_point(false); 192 | let z_affine = AffinePoint::from(*z).to_encoded_point(false); 193 | let a_prime_affine = AffinePoint::from(a_prime).to_encoded_point(false); 194 | let b_prime_affine = AffinePoint::from(b_prime).to_encoded_point(false); 195 | 196 | // Compute challenge c' = Hash(g, h, y, z, a', b') 197 | let mut hasher = Sha256::new(); 198 | hasher.update(g_affine.as_bytes()); 199 | hasher.update(h_affine.as_bytes()); 200 | hasher.update(y_affine.as_bytes()); 201 | hasher.update(z_affine.as_bytes()); 202 | hasher.update(a_prime_affine.as_bytes()); 203 | hasher.update(b_prime_affine.as_bytes()); 204 | 205 | let hash_result = hasher.finalize(); 206 | 207 | // Convert hash to scalar 208 | let mut scalar_bytes = [0u8; 32]; 209 | scalar_bytes.copy_from_slice(&hash_result[..32]); 210 | 211 | // Create challenge scalar from hash 212 | let c_prime_scalar = match Scalar::from_repr_vartime(FieldBytes::from(scalar_bytes)) { 213 | Some(s) => s, 214 | None => return false, 215 | }; 216 | 217 | // Verify that c == c' 218 | c_scalar == c_prime_scalar 219 | } 220 | } 221 | 222 | // Helper function to convert ECPoint to ProjectivePoint 223 | pub fn ecpoint_to_projective(point: &ECPoint) -> Result { 224 | let x = hex::decode(&point.x).map_err(|_| Error::InvalidPoint)?; 225 | let y = hex::decode(&point.y).map_err(|_| Error::InvalidPoint)?; 226 | 227 | // Combine coordinates into SEC1 encoded point 228 | let mut encoded = Vec::with_capacity(65); 229 | encoded.push(0x04); // Uncompressed point marker 230 | encoded.extend_from_slice(&x); 231 | encoded.extend_from_slice(&y); 232 | 233 | // Convert to curve point and verify it's valid 234 | let encoded_point = EncodedPoint::from_bytes(&encoded).map_err(|_| Error::InvalidPoint)?; 235 | ProjectivePoint::from_encoded_point(&encoded_point) 236 | .into_option() 237 | .ok_or(Error::InvalidPoint) 238 | } 239 | 240 | // Helper function to convert ProjectivePoint to ECPoint 241 | pub fn projective_to_ecpoint(point: &ProjectivePoint) -> ECPoint { 242 | // Convert to affine coordinates 243 | let affine = AffinePoint::from(*point); 244 | let encoded_point = affine.to_encoded_point(false); // false = uncompressed 245 | 246 | // Extract x and y coordinates 247 | let x_bytes = encoded_point.x().unwrap(); 248 | let y_bytes = encoded_point.y().unwrap(); 249 | 250 | // Format as hex strings 251 | ECPoint { 252 | x: hex::encode(x_bytes), 253 | y: hex::encode(y_bytes), 254 | } 255 | } 256 | 257 | pub fn limbs_to_hex(limbs: &[String]) -> String { 258 | let limb0 = BigUint::from_str_radix(limbs[0].trim_start_matches("0x"), 16).unwrap(); 259 | let limb1 = BigUint::from_str_radix(limbs[1].trim_start_matches("0x"), 16).unwrap(); 260 | let limb2 = BigUint::from_str_radix(limbs[2].trim_start_matches("0x"), 16).unwrap(); 261 | 262 | // Define 2^120 as a BigUint 263 | let two = BigUint::from_u32(2).unwrap(); 264 | let shift_120 = two.pow(120); 265 | 266 | // Combine the limbs: limb0 + limb1 * 2^120 + limb2 * 2^240 as described in Noir-BigNum 267 | let result = limb0 + (limb1 * &shift_120.clone()) + (limb2 * shift_120.pow(2)); 268 | 269 | // Convert to hexadecimal string, remove "0x" prefix, and ensure 64 digits (256 bits) 270 | let hex_result = format!("{:064x}", result); 271 | 272 | hex_result 273 | } 274 | 275 | pub fn parse_public_inputs(proof: &[u8]) -> Result<(String, ECPoint), Error> { 276 | // Extract user_id_commitment (bytes 0-31) 277 | let user_id_commitment = format!("0x{}", hex::encode(&proof[0..32])); 278 | 279 | // Extract oprf_commitment limbs (6 chunks of 32 bytes each) 280 | let mut x_limbs = Vec::new(); 281 | let mut y_limbs = Vec::new(); 282 | 283 | // x limbs: bytes 32-63, 64-95, 96-127 (with padding) 284 | for i in (32..128).step_by(32) { 285 | let limb = &proof[i..i + 32]; 286 | let hex = hex::encode(limb); 287 | // Remove leading zeros, but keep at least one digit 288 | let trimmed = hex.trim_start_matches('0'); 289 | let limb_str = if trimmed.is_empty() { 290 | "0".to_string() 291 | } else { 292 | format!("0x{}", trimmed) 293 | }; 294 | x_limbs.push(limb_str); 295 | } 296 | 297 | // y limbs: bytes 128-159, 160-191, 192-223 298 | for i in (128..224).step_by(32) { 299 | let limb = &proof[i..i + 32]; 300 | let hex = hex::encode(limb); 301 | let trimmed = hex.trim_start_matches('0'); 302 | let limb_str = if trimmed.is_empty() { 303 | "0".to_string() 304 | } else { 305 | format!("0x{}", trimmed) 306 | }; 307 | y_limbs.push(limb_str); 308 | } 309 | 310 | let x = limbs_to_hex(&x_limbs); 311 | let y = limbs_to_hex(&y_limbs); 312 | 313 | let ecpoint = ECPoint { x, y }; 314 | 315 | Ok((user_id_commitment, ecpoint)) 316 | } 317 | --------------------------------------------------------------------------------