Skip to content

Commit c2fada3

Browse files
authored
fix: zk-id Include data_hash in nullifier computation (#31)
* Include data_hash in nullifier computation Update nullifier formula from Poseidon(verification_id, private_key) to Poseidon(verification_id, private_key, data_hash) where data_hash is Poseidon(issuer_hashed, credential_pubkey). This binds the nullifier to the credential's on-chain data commitment, ensuring the nullifier is unique per credential and issuer combination. * fix buildrs
1 parent 61f28f2 commit c2fada3

9 files changed

Lines changed: 156 additions & 166 deletions

File tree

zk/zk-id/CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Zero-knowledge identity verification using Groth16 proofs with compressed accoun
66

77
- Issuers create credentials for users; users prove credential ownership without revealing the credential
88
- Credential keypair: private key = `Sha256(sign("CREDENTIAL"))` truncated to 248 bits; public key = `Poseidon(private_key)`
9-
- Nullifier = `Poseidon(verification_id, credential_private_key)` - prevents double-use per verification context
9+
- 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
1010
- ZK circuit verifies 26-level Merkle proof of credential account inclusion
1111

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

7979
**Circuit flow**:
8080
1. Derive `credential_pubkey = Poseidon(privateKey)` via `Keypair` template
81-
2. Verify `nullifier = Poseidon(verification_id, privateKey)`
82-
3. Compute `data_hash = Poseidon(issuer_hashed, credential_pubkey)`
81+
2. Compute `data_hash = Poseidon(issuer_hashed, credential_pubkey)`
82+
3. Verify `nullifier = Poseidon(verification_id, privateKey, data_hash)`
8383
4. Compute account hash via `CompressedAccountHash` (adds discriminator domain `+36893488147419103232`)
8484
5. Verify 26-level Merkle proof against `expectedRoot`
8585
6. Verify `public_encrypted_data_hash === encrypted_data_hash`

zk/zk-id/Cargo.lock

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

zk/zk-id/Cargo.toml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,32 @@
1+
[package]
2+
name = "zk-id-tests"
3+
version = "0.1.0"
4+
edition = "2021"
5+
publish = false
6+
17
[workspace]
28
members = ["programs/zk-id"]
39
resolver = "2"
410

11+
[dependencies]
12+
zk-id = { path = "programs/zk-id" }
13+
14+
[dev-dependencies]
15+
anchor-lang = "0.31.1"
16+
circom-prover = "0.1"
17+
rust-witness = "0.1"
18+
num-bigint = "0.4"
19+
serde_json = "1.0"
20+
solana-sdk = "2.2"
21+
tokio = "1.40.0"
22+
light-hasher = { version = "5.0.0", features = ["sha256", "keccak", "poseidon"] }
23+
light-compressed-account = { version = "0.7.0", features = ["new-unique"] }
24+
light-merkle-tree-reference = "4.0.0"
25+
light-program-test = { version = "0.17.1", features = ["v2"] }
26+
light-client = { version = "0.17.2", features = ["v2"] }
27+
light-sdk = { version = "0.17.1", features = ["anchor", "poseidon", "merkle-tree", "v2"] }
28+
groth16-solana = { git = "https://github.com/Lightprotocol/groth16-solana", features = ["vk", "circom"], rev = "66c0dc87d0808c4d2aadb53c61435b6edb8ddfd9" }
29+
530
[profile.release]
631
overflow-checks = true
732
lto = "fat"

zk/zk-id/circuits/compressed_account_merkle_proof.circom

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,21 +49,23 @@ template CompressedAccountMerkleProof(levels) {
4949
keypair.privateKey <== credentialPrivateKey;
5050
signal credential_pubkey_commitment <== keypair.publicKey;
5151

52-
// Step 2: Compute and verify nullifier
53-
// Nullifier = Poseidon(verification_id, credentialPrivateKey)
54-
// This ensures each credential can only be used once per verification_id
55-
// without leaking information about the credential itself.
56-
component nullifierHasher = Poseidon(2);
57-
nullifierHasher.inputs[0] <== verification_id;
58-
nullifierHasher.inputs[1] <== credentialPrivateKey;
59-
nullifier === nullifierHasher.out;
60-
61-
// Step 3: Compute the credential data hash (used internally for account hash)
52+
// Step 2: Compute the credential data hash
53+
// data_hash = Poseidon(issuer_hashed, credential_pubkey_commitment)
6254
component data_hasher = Poseidon(2);
6355
data_hasher.inputs[0] <== issuer_hashed;
6456
data_hasher.inputs[1] <== credential_pubkey_commitment;
6557
signal data_hash <== data_hasher.out;
6658

59+
// Step 3: Compute and verify nullifier
60+
// Nullifier = Poseidon(verification_id, credentialPrivateKey, data_hash)
61+
// This ensures each credential can only be used once per verification_id
62+
// and binds the nullifier to the credential's on-chain data commitment.
63+
component nullifierHasher = Poseidon(3);
64+
nullifierHasher.inputs[0] <== verification_id;
65+
nullifierHasher.inputs[1] <== credentialPrivateKey;
66+
nullifierHasher.inputs[2] <== data_hash;
67+
nullifier === nullifierHasher.out;
68+
6769
// Step 4: Compute compressed account hash
6870
component accountHasher = CompressedAccountHash();
6971
accountHasher.owner_hashed <== owner_hashed;
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use groth16_solana::vk_parser::generate_vk_file;
22

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

77
// Generate the verifying key Rust file from the JSON
8-
let vk_json_path = "./build/verification_key.json";
8+
let vk_json_path = "../../build/verification_key.json";
99
let output_dir = "./src";
1010
let output_file = "verifying_key.rs";
1111

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

0 commit comments

Comments
 (0)