Skip to content

Commit 9fcaee7

Browse files
author
tilo-14
committed
Add ZK examples: mixer, payments, and anonymous airdrop
- zk/mixer: Private token mixing with deposit/withdraw - zk/payments: Private transfers with balance commitments - zk/zk-id: Reorganized under zk/ directory - airdrop-implementations/anonymous-airdrop: ZK-proven claims - Updated CI workflow for ZK examples
1 parent d30d82d commit 9fcaee7

79 files changed

Lines changed: 36051 additions & 40 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/rust-tests.yml‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ jobs:
2929
- counter/native
3030
- counter/pinocchio
3131
- account-comparison
32-
- zk-id
32+
- zk/zk-id
33+
- zk/zk-vote
34+
- zk/payments
3335
- airdrop-implementations/simple-claim/program
3436
include:
3537
- example: basic-operations/native
@@ -51,10 +53,10 @@ jobs:
5153
example: ${{ matrix.example }}
5254
solana-cli-version: ${{ env.SOLANA_CLI_VERSION }}
5355
rust-toolchain: ${{ env.RUST_TOOLCHAIN }}
54-
install-circom: ${{ matrix.example == 'zk-id' }}
56+
install-circom: ${{ startsWith(matrix.example, 'zk/') }}
5557

5658
- name: Setup ZK circuits
57-
if: matrix.example == 'zk-id'
59+
if: startsWith(matrix.example, 'zk/')
5860
working-directory: ${{ matrix.example }}
5961
run: ./scripts/setup.sh
6062

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# Anonymous Airdrop - ZK Claim with Privacy
2+
3+
## Summary
4+
5+
- Anonymous token claims using ZK proofs for eligibility verification
6+
- Claimant identity hidden via Merkle proof + nullifier model (Groth16 proofs)
7+
- Claim amounts and recipients are public; only claimant's eligible address is private
8+
- Double-claim prevention via nullifier-derived compressed account addresses
9+
- Time-lock support for scheduled airdrops
10+
11+
## Architecture
12+
13+
```text
14+
┌─────────────────────────────────────────────────────────────────────────┐
15+
│ Anonymous Airdrop Flow │
16+
├─────────────────────────────────────────────────────────────────────────┤
17+
│ │
18+
│ 1. SETUP │
19+
│ Authority creates AirdropConfig PDA with: │
20+
│ - Eligibility Merkle root (hash of all (address, amount) pairs) │
21+
│ - Token vault with airdrop tokens │
22+
│ - Unlock slot for time-gating │
23+
│ │
24+
│ 2. ELIGIBILITY TREE (off-chain) │
25+
│ Merkle tree of: leaf = Poseidon(eligible_address, amount) │
26+
│ Where: eligible_address = Poseidon(private_key) │
27+
│ │
28+
│ 3. CLAIM │
29+
│ Claimant generates ZK proof proving: │
30+
│ - "I know a private key for an address in the eligibility tree" │
31+
│ - "My nullifier is correctly derived from airdrop_id + private_key" │
32+
│ Submits: proof + nullifier + recipient + amount │
33+
│ Creates: NullifierAccount at nullifier-derived address │
34+
│ Transfers: tokens from vault to recipient │
35+
│ │
36+
└─────────────────────────────────────────────────────────────────────────┘
37+
```
38+
39+
## Privacy Properties
40+
41+
| Property | Value |
42+
|----------|-------|
43+
| Claimant identity | Hidden (ZK proof hides which eligible address is claiming) |
44+
| Claim amount | Public (passed as instruction data) |
45+
| Recipient | Public (tokens transferred to visible address) |
46+
| Double-claim prevention | Nullifier-derived address (cryptographic) |
47+
| Trust model | Trustless (Groth16 proofs, no MPC nodes) |
48+
49+
## Source Structure
50+
51+
```text
52+
anonymous-airdrop/
53+
├── program/src/
54+
│ ├── lib.rs # Program entry, accounts, instructions
55+
│ ├── error.rs # AirdropError variants
56+
│ └── verifying_key.rs # Groth16 verifying key (generated)
57+
├── circuits/
58+
│ ├── airdrop_claim.circom # Main circuit (5 public inputs)
59+
│ └── merkle_proof.circom # 20-level Merkle proof
60+
├── scripts/
61+
│ ├── setup.sh # Circuit compilation + trusted setup
62+
│ └── clean.sh # Remove build artifacts
63+
├── typescript/
64+
│ └── client.ts # Example claim client
65+
└── build.rs # Generates verifying_key.rs from JSON
66+
```
67+
68+
## Accounts
69+
70+
### AirdropConfig (Solana PDA)
71+
72+
Seeds: `[b"airdrop", airdrop_id.to_le_bytes()]`
73+
74+
| Field | Type | Size | Description |
75+
|-------|------|------|-------------|
76+
| `airdrop_id` | `u64` | 8 | Unique airdrop identifier |
77+
| `authority` | `Pubkey` | 32 | Can deactivate airdrop |
78+
| `mint` | `Pubkey` | 32 | Token mint |
79+
| `eligibility_root` | `[u8; 32]` | 32 | Merkle root of (address, amount) pairs |
80+
| `token_vault` | `Pubkey` | 32 | Vault holding airdrop tokens |
81+
| `unlock_slot` | `u64` | 8 | Slot when tokens unlock |
82+
| `is_active` | `bool` | 1 | Whether airdrop is active |
83+
| `bump` | `u8` | 1 | PDA bump |
84+
85+
### NullifierAccount (Compressed)
86+
87+
Address: `derive_address([b"nullifier", nullifier])`
88+
89+
| Field | Type | Description |
90+
|-------|------|-------------|
91+
| `nullifier` | `[u8; 32]` | `Poseidon(airdrop_id, private_key)` |
92+
93+
## Instructions
94+
95+
### initialize_airdrop
96+
97+
Creates a new airdrop configuration.
98+
99+
| Field | Value |
100+
|-------|-------|
101+
| **instruction_data** | `airdrop_id: u64`, `eligibility_root: [u8; 32]`, `unlock_slot: u64` |
102+
| **accounts** | `airdrop_config` (init PDA), `mint`, `token_vault`, `authority` (signer), `system_program` |
103+
104+
### claim
105+
106+
Claims tokens with ZK proof of eligibility.
107+
108+
| Field | Value |
109+
|-------|-------|
110+
| **instruction_data** | `validity_proof`, `address_tree_info`, `output_state_tree_index`, `groth16_proof`, `nullifier`, `amount` |
111+
| **accounts** | `airdrop_config`, `token_vault`, `recipient_token_account`, `payer` (signer), `token_program`, `system_program`, + Light Protocol remaining accounts |
112+
| **constraints** | `is_active`, `current_slot >= unlock_slot`, valid Groth16 proof |
113+
114+
### deactivate_airdrop
115+
116+
Deactivates the airdrop (authority only).
117+
118+
## Circuit: airdrop_claim.circom
119+
120+
### Public Inputs (5)
121+
122+
| # | Signal | Description |
123+
|---|--------|-------------|
124+
| 1 | `eligibilityRoot` | Merkle root of (address, amount) leaves |
125+
| 2 | `nullifier` | `Poseidon(airdropId, privateKey)` |
126+
| 3 | `recipient` | Recipient address (hashed to BN254) |
127+
| 4 | `airdropId` | Airdrop identifier (hashed to BN254) |
128+
| 5 | `amount` | Token amount (as 32-byte BE) |
129+
130+
### Constraints
131+
132+
```text
133+
1. eligibleAddress = Poseidon(privateKey)
134+
2. leaf = Poseidon(eligibleAddress, amount)
135+
3. nullifier = Poseidon(airdropId, privateKey)
136+
4. MerkleProof(leaf, pathElements, leafIndex) == eligibilityRoot
137+
5. recipientSquare = recipient * recipient (binds recipient to proof)
138+
```
139+
140+
## Client-Side Proof Generation
141+
142+
```typescript
143+
// 1. Load private key and get Merkle proof from eligibility data
144+
const eligibleAddress = poseidon([privateKey]);
145+
const leaf = poseidon([eligibleAddress, amount]);
146+
const { pathElements, leafIndex } = getMerkleProof(eligibilityTree, leaf);
147+
148+
// 2. Generate nullifier
149+
const nullifier = poseidon([airdropId, privateKey]);
150+
151+
// 3. Generate ZK proof
152+
const { proof } = await snarkjs.groth16.fullProve({
153+
eligibilityRoot: airdropConfig.eligibilityRoot,
154+
nullifier,
155+
recipient: recipientAddress, // hashed
156+
airdropId, // hashed
157+
amount,
158+
// Private
159+
privateKey,
160+
pathElements,
161+
leafIndex,
162+
}, wasmPath, zkeyPath);
163+
164+
// 4. Submit claim (recipient can be any address - no link to eligible address)
165+
await program.methods.claim(
166+
validityProof,
167+
addressTreeInfo,
168+
outputStateTreeIndex,
169+
compressProof(proof),
170+
nullifier,
171+
amount
172+
).rpc();
173+
```
174+
175+
## Security
176+
177+
| Property | Mechanism |
178+
|----------|-----------|
179+
| Claimant anonymity | ZK proof hides which eligible address is claiming |
180+
| Eligibility | Address exists in eligibility Merkle tree with correct amount |
181+
| Double-claim prevention | Nullifier-derived compressed account address uniqueness |
182+
| Airdrop binding | Nullifier includes `airdropId` |
183+
| Front-running prevention | Recipient bound to proof |
184+
| Time-lock | `current_slot >= unlock_slot` check |
185+
186+
## Errors
187+
188+
| Code | Name | Cause |
189+
|------|------|-------|
190+
| 6000 | `TokensLocked` | `current_slot < unlock_slot` |
191+
| 6001 | `InvalidProof` | Groth16 verification failed |
192+
| 6002 | `AirdropNotActive` | Airdrop has been deactivated |
193+
| 6003 | `InvalidEligibilityRoot` | Root mismatch |
194+
| 6004 | `InvalidNullifier` | Nullifier computation error |
195+
| 6005 | `InvalidAddressTree` | Wrong Light Protocol address tree |
196+
| 6006 | `AccountNotEnoughKeys` | Missing Light Protocol accounts |
197+
198+
## Setup
199+
200+
```bash
201+
# Install circuit dependencies
202+
npm install
203+
204+
# Run trusted setup (generates verification_key.json)
205+
./scripts/setup.sh
206+
207+
# Build Solana program (generates verifying_key.rs)
208+
cargo build-sbf
209+
```
210+
211+
## Dependencies
212+
213+
- Light Protocol SDK (compression, address derivation, CPIs)
214+
- groth16-solana (on-chain proof verification)
215+
- circomlib (Poseidon, Switcher)
216+
- snarkjs (circuit compilation, trusted setup)
217+

0 commit comments

Comments
 (0)