Skip to content

Commit 4de100a

Browse files
author
tilo-14
committed
feat: add zk-nullifier example with single and batch proof support
- Add nullifier circuit with Poseidon hash for single nullifier creation - Add batch nullifier circuit for creating 4 nullifiers with single proof - Include setup script for circuit compilation and key generation - Add e2e tests for both single and batch nullifier creation - Remove incomplete mixer and shielded-pool examples
1 parent ed0a501 commit 4de100a

13 files changed

Lines changed: 154 additions & 41 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,7 @@ pot
1212
# ZK examples - TypeScript tests not ready
1313
zk/**/*.ts
1414
zk/**/tsconfig.json
15+
16+
# ZK examples - not ready
17+
zk/mixer/
18+
zk/shielded-pool/

zk/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ You can use Light to:
1212

1313
**Full Examples:**
1414

15-
- **[zk-id](./zk/zk-id)** - Identity verification using Groth16 proofs. Issuers create credentials; users prove ownership without revealing the credential.
15+
- **[zk-id](./zk-id)** - Identity verification using Groth16 proofs. Issuers create credentials; users prove ownership without revealing the credential.
16+
- **[shielded-pool](./shielded-pool)** - Privacy-preserving SOL pool (Tornado Nova port). UTXO model with arbitrary amounts, encrypted outputs, and relayer support.
17+
- **[mixer](./mixer)** - Fixed-denomination privacy mixer (Tornado Core port). Deposit/withdraw fixed amounts to break on-chain transaction links.
1618

1719
**Basic Examples:**
1820

zk/zk-merkle-proof/CLAUDE.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# zk-merkle-proof
2+
3+
Proves compressed account existence with Groth16 verification without revealing Merkle path.
4+
5+
## Summary
6+
7+
- Creates compressed accounts with Poseidon-hashed data
8+
- Verifies Groth16 proofs that account exists in state tree
9+
- Merkle path, leaf position, and address stay private
10+
- Only data_hash, discriminator, and root are public inputs
11+
12+
## Instructions
13+
14+
### `create_account`
15+
16+
Creates a compressed account with a data hash.
17+
18+
- **path**: `src/lib.rs:37`
19+
20+
**Instruction data:**
21+
22+
| Field | Type | Description |
23+
|-------|------|-------------|
24+
| `proof` | `ValidityProof` | Light Protocol validity proof |
25+
| `address_tree_info` | `PackedAddressTreeInfo` | Address tree reference |
26+
| `output_state_tree_index` | `u8` | State tree for output |
27+
| `data_hash` | `[u8; 32]` | Hash of account data |
28+
29+
**Accounts:**
30+
31+
| Name | Signer | Writable | Description |
32+
|------|--------|----------|-------------|
33+
| `signer` ||| Transaction fee payer |
34+
35+
**Logic:**
36+
37+
1. Derive address from `[b"data_account", data_hash]`
38+
2. Create `DataAccount` with Poseidon hashing via Light CPI
39+
40+
### `verify_account`
41+
42+
Verifies a Groth16 proof that account exists in state tree.
43+
44+
- **path**: `src/lib.rs:76`
45+
46+
**Instruction data:**
47+
48+
| Field | Type | Description |
49+
|-------|------|-------------|
50+
| `input_root_index` | `u16` | Root index in state tree |
51+
| `zk_proof` | `CompressedProof` | Groth16 proof (a, b, c) |
52+
| `data_hash` | `[u8; 32]` | Expected data hash |
53+
54+
**Accounts:**
55+
56+
| Name | Signer | Writable | Description |
57+
|------|--------|----------|-------------|
58+
| `signer` ||| Transaction fee payer |
59+
| `state_merkle_tree` ||| State tree to read root from |
60+
61+
**Logic:**
62+
63+
1. Read expected root from state Merkle tree at `input_root_index`
64+
2. Hash program ID and tree pubkey to BN254 field size
65+
3. Construct 5 public inputs: `[owner_hashed, merkle_tree_hashed, discriminator, data_hash, expected_root]`
66+
4. Decompress G1/G2 proof points
67+
5. Verify Groth16 proof against verifying key
68+
69+
## Accounts
70+
71+
### `DataAccount`
72+
73+
Stores a data hash using Poseidon hashing.
74+
75+
- **path**: `src/lib.rs:157`
76+
- **derivation**: `[b"data_account", data_hash]`
77+
- **hashing**: Poseidon (via `LightHasher` derive)
78+
79+
## Circuit
80+
81+
5 public inputs:
82+
83+
- `owner_hashed` - program ID hashed to BN254 field
84+
- `merkle_tree_hashed` - state tree pubkey hashed
85+
- `discriminator` - account type discriminator
86+
- `data_hash` - account data hash
87+
- `expectedRoot` - current Merkle root
88+
89+
Private inputs (hidden in proof):
90+
91+
- `leaf_index` - position in tree
92+
- `account_leaf_index` - SDK internal position
93+
- `address` - account address
94+
- `pathElements[26]` - Merkle proof siblings
95+
96+
## Build & Test
97+
98+
```bash
99+
./scripts/setup.sh # Compile circuits, generate zkeys
100+
cargo build-sbf && cargo test-sbf # Rust tests
101+
```

zk/zk-merkle-proof/circuits/README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Merkle Proof Circuit
1+
# Compressed Account Merkle Proof Circuit
22

3-
Zero-knowledge circuit that proves a compressed account exists in a Merkle tree without revealing private account details.
3+
Zero-knowledge circuit that proves ownership of a compressed account in a Merkle tree without revealing the account details.
44

55
## What It Does
66

@@ -41,9 +41,9 @@ cargo test-sbf
4141
Single file `merkle_proof.circom` contains all templates:
4242

4343
```
44-
AccountMerkleProof (main)
44+
CompressedAccountMerkleProof (main)
4545
├── CompressedAccountHash
4646
│ └── Poseidon hash of 6 account fields
47-
└── MerkleProof (Tornado Cash Nova pattern)
48-
└── 26-level binary tree verification using Switcher
47+
└── MerkleProof
48+
└── 26-level binary tree verification
4949
```

zk/zk-merkle-proof/circuits/merkle_proof.circom

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ include "../node_modules/circomlib/circuits/poseidon.circom";
44
include "../node_modules/circomlib/circuits/bitify.circom";
55
include "../node_modules/circomlib/circuits/switcher.circom";
66

7+
// Merkle Proof Verification Template
8+
// Verifies that a leaf is in a Merkle tree with a given root
79
template MerkleProof(levels) {
810
signal input leaf;
911
signal input pathElements[levels];
@@ -48,7 +50,7 @@ template CompressedAccountHash() {
4850
hash <== poseidon.out;
4951
}
5052

51-
template AccountMerkleProof(levels) {
53+
template CompressedCompressedAccountMerkleProof(levels) {
5254
signal input owner_hashed;
5355
signal input merkle_tree_hashed;
5456
signal input discriminator;
@@ -77,4 +79,4 @@ template AccountMerkleProof(levels) {
7779

7880
component main {
7981
public [owner_hashed, merkle_tree_hashed, discriminator, data_hash, expectedRoot]
80-
} = AccountMerkleProof(26);
82+
} = CompressedAccountMerkleProof(26);

zk/zk-merkle-proof/src/lib.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ declare_id!("MPzkYomvQc4VQPwMr6bFduyWRQZVCh5CofgDC4dFqJp");
2323
pub const LIGHT_CPI_SIGNER: CpiSigner =
2424
derive_light_cpi_signer!("MPzkYomvQc4VQPwMr6bFduyWRQZVCh5CofgDC4dFqJp");
2525

26-
pub const DATA_ACCOUNT: &[u8] = b"data_account";
26+
pub const ZK_ACCOUNT: &[u8] = b"zk_account";
2727

2828
pub mod verifying_key;
2929

@@ -52,12 +52,12 @@ pub mod zk_merkle_proof {
5252
.map_err(|_| ProgramError::InvalidAccountData)?;
5353

5454
let (address, address_seed) = derive_address(
55-
&[DATA_ACCOUNT, &data_hash],
55+
&[ZK_ACCOUNT, &data_hash],
5656
&address_tree_pubkey,
5757
&crate::ID,
5858
);
5959

60-
let mut account = LightAccountPoseidon::<DataAccount>::new_init(
60+
let mut account = LightAccountPoseidon::<ZkAccount>::new_init(
6161
&crate::ID,
6262
Some(address),
6363
output_state_tree_index,
@@ -94,7 +94,7 @@ pub mod zk_merkle_proof {
9494
.unwrap();
9595

9696
let mut discriminator = [0u8; 32];
97-
discriminator[24..].copy_from_slice(DataAccount::LIGHT_DISCRIMINATOR_SLICE);
97+
discriminator[24..].copy_from_slice(ZkAccount::LIGHT_DISCRIMINATOR_SLICE);
9898

9999
let public_inputs: [[u8; 32]; 5] = [
100100
owner_hashed,
@@ -155,7 +155,7 @@ pub struct VerifyAccountAccounts<'info> {
155155
}
156156

157157
#[derive(Clone, Debug, Default, BorshSerialize, BorshDeserialize, LightDiscriminator, LightHasher)]
158-
pub struct DataAccount {
158+
pub struct ZkAccount {
159159
pub data_hash: DataHash,
160160
}
161161

zk/zk-merkle-proof/tests/test.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use solana_sdk::{
1616
signature::{Keypair, Signature, Signer},
1717
};
1818
use std::collections::HashMap;
19-
use zk_merkle_proof::DATA_ACCOUNT;
19+
use zk_merkle_proof::ZK_ACCOUNT;
2020

2121
#[link(name = "circuit", kind = "static")]
2222
extern "C" {}
@@ -40,7 +40,7 @@ async fn test_create_and_verify_account() {
4040
let address_tree_info = rpc.get_address_tree_v2();
4141

4242
let (account_address, _) = derive_address(
43-
&[DATA_ACCOUNT, &data_hash],
43+
&[ZK_ACCOUNT, &data_hash],
4444
&address_tree_info.tree,
4545
&zk_merkle_proof::ID,
4646
);

zk/zk-nullifier/build.rs

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,33 @@
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/batch_verification_key.json");
6-
println!("cargo:rerun-if-changed=build/nullifier_js");
7-
println!("cargo:rerun-if-changed=build/batchnullifier_js");
4+
println!("cargo:rerun-if-changed=build/nullifier_1_verification_key.json");
5+
println!("cargo:rerun-if-changed=build/nullifier_4_verification_key.json");
6+
println!("cargo:rerun-if-changed=build/nullifier_1_js");
7+
println!("cargo:rerun-if-changed=build/nullifier_4_js");
88

99
// Single nullifier verifying key
10-
if std::path::Path::new("./build/verification_key.json").exists() {
11-
generate_vk_file("./build/verification_key.json", "./src", "verifying_key.rs")
12-
.expect("Failed to generate verifying_key.rs");
10+
if std::path::Path::new("./build/nullifier_1_verification_key.json").exists() {
11+
generate_vk_file(
12+
"./build/nullifier_1_verification_key.json",
13+
"./src",
14+
"nullifier_1.rs",
15+
)
16+
.expect("Failed to generate nullifier_1.rs");
1317
} else {
14-
println!("cargo:warning=verification_key.json not found. Run './scripts/setup.sh'");
18+
println!("cargo:warning=nullifier_1_verification_key.json not found. Run './scripts/setup.sh'");
1519
}
1620

1721
// Batch nullifier verifying key
18-
if std::path::Path::new("./build/batch_verification_key.json").exists() {
22+
if std::path::Path::new("./build/nullifier_4_verification_key.json").exists() {
1923
generate_vk_file(
20-
"./build/batch_verification_key.json",
24+
"./build/nullifier_4_verification_key.json",
2125
"./src",
22-
"batch_verifying_key.rs",
26+
"nullifier_batch_4.rs",
2327
)
24-
.expect("Failed to generate batch_verifying_key.rs");
28+
.expect("Failed to generate nullifier_batch_4.rs");
2529
} else {
26-
println!("cargo:warning=batch_verification_key.json not found. Run './scripts/setup.sh'");
30+
println!("cargo:warning=nullifier_4_verification_key.json not found. Run './scripts/setup.sh'");
2731
}
2832

2933
// Transpile witness generators for non-Solana targets
@@ -33,8 +37,8 @@ fn main() {
3337
let out_dir = std::env::var("OUT_DIR").unwrap();
3438

3539
// Transpile single nullifier circuit
36-
if std::path::Path::new("./build/nullifier_js").exists() {
37-
rust_witness::transpile::transpile_wasm("./build/nullifier_js".to_string());
40+
if std::path::Path::new("./build/nullifier_1_js").exists() {
41+
rust_witness::transpile::transpile_wasm("./build/nullifier_1_js".to_string());
3842
// Rename libcircuit.a → libcircuit_single.a
3943
let src = format!("{}/libcircuit.a", out_dir);
4044
let dst = format!("{}/libcircuit_single.a", out_dir);
@@ -44,8 +48,8 @@ fn main() {
4448
}
4549

4650
// Transpile batch nullifier circuit
47-
if std::path::Path::new("./build/batchnullifier_js").exists() {
48-
rust_witness::transpile::transpile_wasm("./build/batchnullifier_js".to_string());
51+
if std::path::Path::new("./build/nullifier_4_js").exists() {
52+
rust_witness::transpile::transpile_wasm("./build/nullifier_4_js".to_string());
4953
// Rename libcircuit.a → libcircuit_batch.a
5054
let src = format!("{}/libcircuit.a", out_dir);
5155
let dst = format!("{}/libcircuit_batch.a", out_dir);

zk/zk-nullifier/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ pub const LIGHT_CPI_SIGNER: CpiSigner =
2222

2323
pub const NULLIFIER_PREFIX: &[u8] = b"nullifier";
2424

25-
// Max nullifiers per tx: 1 (single) or 4 (batch)
25+
// Customize nullifiers per tx, e.g. 1 (single) or 4 (batch)
2626
pub const BATCH_SIZE: usize = 4;
2727

2828
pub mod nullifier_1;

zk/zk-nullifier/src/nullifier_1.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ pub const VERIFYINGKEY: Groth16Verifyingkey = Groth16Verifyingkey {
99

1010
vk_gamma_g2: [25u8, 142u8, 147u8, 147u8, 146u8, 13u8, 72u8, 58u8, 114u8, 96u8, 191u8, 183u8, 49u8, 251u8, 93u8, 37u8, 241u8, 170u8, 73u8, 51u8, 53u8, 169u8, 231u8, 18u8, 151u8, 228u8, 133u8, 183u8, 174u8, 243u8, 18u8, 194u8, 24u8, 0u8, 222u8, 239u8, 18u8, 31u8, 30u8, 118u8, 66u8, 106u8, 0u8, 102u8, 94u8, 92u8, 68u8, 121u8, 103u8, 67u8, 34u8, 212u8, 247u8, 94u8, 218u8, 221u8, 70u8, 222u8, 189u8, 92u8, 217u8, 146u8, 246u8, 237u8, 9u8, 6u8, 137u8, 208u8, 88u8, 95u8, 240u8, 117u8, 236u8, 158u8, 153u8, 173u8, 105u8, 12u8, 51u8, 149u8, 188u8, 75u8, 49u8, 51u8, 112u8, 179u8, 142u8, 243u8, 85u8, 172u8, 218u8, 220u8, 209u8, 34u8, 151u8, 91u8, 18u8, 200u8, 94u8, 165u8, 219u8, 140u8, 109u8, 235u8, 74u8, 171u8, 113u8, 128u8, 141u8, 203u8, 64u8, 143u8, 227u8, 209u8, 231u8, 105u8, 12u8, 67u8, 211u8, 123u8, 76u8, 230u8, 204u8, 1u8, 102u8, 250u8, 125u8, 170u8],
1111

12-
vk_delta_g2: [15u8, 50u8, 226u8, 15u8, 25u8, 49u8, 85u8, 92u8, 250u8, 85u8, 137u8, 57u8, 88u8, 252u8, 101u8, 174u8, 216u8, 14u8, 90u8, 41u8, 162u8, 60u8, 209u8, 25u8, 208u8, 32u8, 185u8, 189u8, 104u8, 103u8, 199u8, 36u8, 25u8, 202u8, 32u8, 147u8, 48u8, 19u8, 68u8, 85u8, 122u8, 192u8, 42u8, 156u8, 71u8, 42u8, 101u8, 140u8, 37u8, 171u8, 240u8, 154u8, 222u8, 12u8, 19u8, 83u8, 34u8, 132u8, 233u8, 110u8, 77u8, 73u8, 207u8, 173u8, 40u8, 114u8, 71u8, 135u8, 57u8, 66u8, 210u8, 247u8, 147u8, 215u8, 36u8, 47u8, 39u8, 166u8, 215u8, 139u8, 84u8, 153u8, 43u8, 184u8, 129u8, 125u8, 16u8, 115u8, 125u8, 72u8, 250u8, 11u8, 174u8, 190u8, 61u8, 63u8, 40u8, 7u8, 176u8, 8u8, 193u8, 18u8, 115u8, 34u8, 248u8, 254u8, 223u8, 93u8, 117u8, 235u8, 180u8, 210u8, 184u8, 123u8, 194u8, 100u8, 133u8, 247u8, 9u8, 192u8, 41u8, 74u8, 8u8, 73u8, 170u8, 200u8, 31u8, 61u8],
12+
vk_delta_g2: [5u8, 214u8, 55u8, 57u8, 195u8, 158u8, 141u8, 21u8, 221u8, 107u8, 234u8, 39u8, 157u8, 74u8, 252u8, 20u8, 199u8, 144u8, 101u8, 227u8, 7u8, 157u8, 38u8, 68u8, 167u8, 43u8, 87u8, 5u8, 11u8, 13u8, 171u8, 47u8, 47u8, 37u8, 222u8, 89u8, 11u8, 73u8, 171u8, 198u8, 130u8, 72u8, 158u8, 170u8, 147u8, 14u8, 4u8, 235u8, 56u8, 37u8, 173u8, 75u8, 74u8, 48u8, 155u8, 253u8, 66u8, 199u8, 129u8, 200u8, 141u8, 191u8, 112u8, 0u8, 14u8, 46u8, 100u8, 107u8, 1u8, 37u8, 237u8, 245u8, 61u8, 196u8, 22u8, 101u8, 102u8, 176u8, 178u8, 173u8, 53u8, 152u8, 121u8, 66u8, 157u8, 202u8, 157u8, 46u8, 44u8, 96u8, 119u8, 1u8, 65u8, 118u8, 80u8, 4u8, 27u8, 91u8, 77u8, 70u8, 87u8, 129u8, 236u8, 17u8, 33u8, 7u8, 179u8, 200u8, 171u8, 114u8, 72u8, 92u8, 229u8, 49u8, 225u8, 114u8, 250u8, 40u8, 144u8, 199u8, 198u8, 202u8, 150u8, 55u8, 123u8, 168u8, 195u8, 243u8],
1313

1414
vk_ic: &[
1515
[0u8, 225u8, 39u8, 181u8, 79u8, 250u8, 189u8, 217u8, 124u8, 191u8, 91u8, 4u8, 250u8, 224u8, 250u8, 69u8, 80u8, 29u8, 217u8, 232u8, 11u8, 137u8, 22u8, 144u8, 107u8, 98u8, 206u8, 198u8, 55u8, 106u8, 245u8, 27u8, 24u8, 71u8, 117u8, 104u8, 154u8, 235u8, 39u8, 185u8, 32u8, 148u8, 197u8, 136u8, 176u8, 194u8, 26u8, 117u8, 68u8, 73u8, 196u8, 223u8, 81u8, 254u8, 228u8, 233u8, 7u8, 176u8, 206u8, 222u8, 224u8, 102u8, 198u8, 27u8],

0 commit comments

Comments
 (0)