Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions zk/zk-id/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Zero-knowledge identity verification using Groth16 proofs with compressed accoun

- Issuers create credentials for users; users prove credential ownership without revealing the credential
- Credential keypair: private key = `Sha256(sign("CREDENTIAL"))` truncated to 248 bits; public key = `Poseidon(private_key)`
- Nullifier = `Poseidon(verification_id, credential_private_key)` - prevents double-use per verification context
- Nullifier = `Poseidon(verification_id, credential_private_key, data_hash)` where `data_hash = Poseidon(issuer_hashed, credential_public_key)` - prevents double-use per verification context and binds to credential's on-chain data
- ZK circuit verifies 26-level Merkle proof of credential account inclusion

## [README](README.md)
Expand Down Expand Up @@ -78,8 +78,8 @@ derive_address(&[seed_prefix, identifier], &address_tree_pubkey, &program_id)

**Circuit flow**:
1. Derive `credential_pubkey = Poseidon(privateKey)` via `Keypair` template
2. Verify `nullifier = Poseidon(verification_id, privateKey)`
3. Compute `data_hash = Poseidon(issuer_hashed, credential_pubkey)`
2. Compute `data_hash = Poseidon(issuer_hashed, credential_pubkey)`
3. Verify `nullifier = Poseidon(verification_id, privateKey, data_hash)`
4. Compute account hash via `CompressedAccountHash` (adds discriminator domain `+36893488147419103232`)
5. Verify 26-level Merkle proof against `expectedRoot`
6. Verify `public_encrypted_data_hash === encrypted_data_hash`
Expand Down
21 changes: 21 additions & 0 deletions zk/zk-id/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions zk/zk-id/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,32 @@
[package]
name = "zk-id-tests"
version = "0.1.0"
edition = "2021"
publish = false

[workspace]
members = ["programs/zk-id"]
resolver = "2"

[dependencies]
zk-id = { path = "programs/zk-id" }

[dev-dependencies]
anchor-lang = "0.31.1"
circom-prover = "0.1"
rust-witness = "0.1"
num-bigint = "0.4"
serde_json = "1.0"
solana-sdk = "2.2"
tokio = "1.40.0"
light-hasher = { version = "5.0.0", features = ["sha256", "keccak", "poseidon"] }
light-compressed-account = { version = "0.7.0", features = ["new-unique"] }
light-merkle-tree-reference = "4.0.0"
light-program-test = { version = "0.17.1", features = ["v2"] }
light-client = { version = "0.17.2", features = ["v2"] }
light-sdk = { version = "0.17.1", features = ["anchor", "poseidon", "merkle-tree", "v2"] }
groth16-solana = { git = "https://github.com/Lightprotocol/groth16-solana", features = ["vk", "circom"], rev = "66c0dc87d0808c4d2aadb53c61435b6edb8ddfd9" }

[profile.release]
overflow-checks = true
lto = "fat"
Expand Down
22 changes: 12 additions & 10 deletions zk/zk-id/circuits/compressed_account_merkle_proof.circom
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,23 @@ template CompressedAccountMerkleProof(levels) {
keypair.privateKey <== credentialPrivateKey;
signal credential_pubkey_commitment <== keypair.publicKey;

// Step 2: Compute and verify nullifier
// Nullifier = Poseidon(verification_id, credentialPrivateKey)
// This ensures each credential can only be used once per verification_id
// without leaking information about the credential itself.
component nullifierHasher = Poseidon(2);
nullifierHasher.inputs[0] <== verification_id;
nullifierHasher.inputs[1] <== credentialPrivateKey;
nullifier === nullifierHasher.out;

// Step 3: Compute the credential data hash (used internally for account hash)
// Step 2: Compute the credential data hash
// data_hash = Poseidon(issuer_hashed, credential_pubkey_commitment)
component data_hasher = Poseidon(2);
data_hasher.inputs[0] <== issuer_hashed;
data_hasher.inputs[1] <== credential_pubkey_commitment;
signal data_hash <== data_hasher.out;

// Step 3: Compute and verify nullifier
// Nullifier = Poseidon(verification_id, credentialPrivateKey, data_hash)
// This ensures each credential can only be used once per verification_id
// and binds the nullifier to the credential's on-chain data commitment.
component nullifierHasher = Poseidon(3);
nullifierHasher.inputs[0] <== verification_id;
nullifierHasher.inputs[1] <== credentialPrivateKey;
nullifierHasher.inputs[2] <== data_hash;
nullifier === nullifierHasher.out;

// Step 4: Compute compressed account hash
component accountHasher = CompressedAccountHash();
accountHasher.owner_hashed <== owner_hashed;
Expand Down
8 changes: 4 additions & 4 deletions zk/zk-id/build.rs → zk/zk-id/programs/zk-id/build.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use groth16_solana::vk_parser::generate_vk_file;

fn main() {
println!("cargo:rerun-if-changed=build/verification_key.json");
println!("cargo:rerun-if-changed=build/compressed_account_merkle_proof_js");
println!("cargo:rerun-if-changed=../../build/verification_key.json");
println!("cargo:rerun-if-changed=../../build/compressed_account_merkle_proof_js");

// Generate the verifying key Rust file from the JSON
let vk_json_path = "./build/verification_key.json";
let vk_json_path = "../../build/verification_key.json";
let output_dir = "./src";
let output_file = "verifying_key.rs";

Expand All @@ -21,7 +21,7 @@ fn main() {
// Check the TARGET environment variable since build scripts run on the host
let target = std::env::var("TARGET").unwrap_or_default();
if !target.contains("sbf") && !target.contains("solana") {
let witness_wasm_dir = "./build/compressed_account_merkle_proof_js";
let witness_wasm_dir = "../../build/compressed_account_merkle_proof_js";
if std::path::Path::new(witness_wasm_dir).exists() {
rust_witness::transpile::transpile_wasm(witness_wasm_dir.to_string());
// Successfully transpiled witness generator
Expand Down
Loading