Skip to content

Commit 41cdcff

Browse files
author
tilo-14
committed
cleanup
1 parent 1db7b62 commit 41cdcff

File tree

5 files changed

+27
-69
lines changed

5 files changed

+27
-69
lines changed

zk/nullifier/README.md

Lines changed: 22 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,37 @@
1-
# Nullifier Registry
1+
# Nullifier for ZK
22

3-
Creates nullifier accounts using compressed addresses. Prevents double-spend by failing if a nullifier already exists.
3+
Program with single instruction to create nullifiers to prevent double-spending.
44

5-
Light uses rent-free PDAs in an address Merkle tree indexed by Helius. No custom indexing required.
5+
Can be added to your custom program without requiring a custom circuit.
6+
7+
* On Solana nullifiers require a data structure that ensures the nullifier is only created once.
8+
* A straight forward way is to derive a PDA with the nullifier as seed for the PDA account.
9+
* Nullifier accounts must remain active, hence lock ~0.001 SOL in rent per nullifier PDA permanently.
10+
* Compressed addresses are rent-free, provide similar functionality and derivation.
611

712
| Storage | Cost per nullifier |
813
|---------|-------------------|
914
| PDA | ~0.001 SOL |
10-
| Compressed PDA | ~0.000005 SOL |
11-
12-
## Requirements
15+
| Compressed PDA | ~0.000015 SOL |
1316

14-
- **Rust** (1.90.0 or later)
15-
- **Node.js** (v22 or later)
16-
- **Solana CLI** (2.3.11 or later)
17-
- **Light CLI**: `npm install -g @lightprotocol/zk-compression-cli`
17+
In detail, a nullifier is a hash derived from your secret and the leaf the transaction is using.
18+
When you use private state (stored in a Merkle tree leaf), you publish the nullifier. The program stores it in a set.
19+
If anyone tries to spend the same leaf again, the nullifier would match one already stored, so the transaction fails.
20+
The nullifier reveals nothing about which leaf was spent.
21+
Different state produces different nullifiers, so observers can't link a nullifier back to its source leaf.
1822

1923
## Flow
20-
21-
1. Client derives nullifier addresses from `[NULLIFIER_PREFIX, nullifier_value]`
22-
2. Client requests validity proof from RPC
23-
3. On-chain: derive addresses, create compressed accounts
24-
4. If any address exists, tx fails
25-
26-
## Program instruction
27-
28-
### `create_nullifier`
29-
30-
Creates nullifier accounts for provided values.
31-
32-
**Parameters:**
33-
- `data: NullifierInstructionData` - validity proof, tree info, indices
34-
- `nullifiers: Vec<[u8; 32]>` - nullifier values to register
35-
36-
**Behavior:**
37-
- Derives address from `[b"nullifier", nullifier_value]`
38-
- Creates compressed account at derived address
39-
- Fails if address already exists (prevents replay)
24+
1. Client computes nullifier values (typically `hash(secret, context)`) and fetches validity proof from RPC for the derived addresses to prove it does not exist.
25+
3. Client calls `create_nullifier` with data, nullifiers and validity proof
26+
4. Program derives addresses, creates compressed accounts via CPI to Light system program
27+
5. If any address exists, Light system program rejects the CPI
4028

4129
## Build and Test
4230

31+
- **Rust** (1.90.0 or later)
32+
- **Node.js** (v22 or later)
33+
- **Solana CLI** (2.3.11 or later)
34+
- **Light CLI**: `npm install -g @lightprotocol/zk-compression-cli`
4335
### Using Makefile
4436

4537
From the parent `zk/` directory:

zk/nullifier/programs/nullifier/src/lib.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,6 @@ pub mod nullifier {
1111
use super::*;
1212

1313
/// Creates nullifier accounts for the provided nullifier values.
14-
///
15-
/// # Arguments
16-
/// * `data` - Nullifier instruction data (proof, tree info, indices)
17-
/// * `namespace` - Naespace for address derivation
18-
/// * `nullifiers` - Vector of nullifier values to create accounts for
1914
pub fn create_nullifier<'info>(
2015
ctx: Context<'_, '_, '_, 'info, CreateNullifierAccounts<'info>>,
2116
data: NullifierInstructionData,
@@ -81,8 +76,7 @@ pub mod nullifier_creation {
8176
///
8277
/// # Arguments
8378
/// * `nullifiers` - Slice of nullifier values to create accounts for
84-
/// * `namespace` - Namespace for address derivation (e.g., your program's verification ID)
85-
/// * `data` - Instruction data containing proof and tree info
79+
/// * `data` - Instruction data with proof and tree info
8680
/// * `remaining_accounts` - Remaining accounts from the instruction context
8781
pub fn create_nullifiers<'info>(
8882
nullifiers: &[[u8; 32]],

zk/nullifier/ts-tests/nullifier.test.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,6 @@ describe("nullifier", () => {
5252
it("should create a nullifier", async () => {
5353
const nullifier = randomBytes32();
5454

55-
console.log("Nullifier:", Buffer.from(nullifier).toString("hex").slice(0, 16) + "...");
56-
5755
const { data, remainingAccounts } = await createNullifierInstructionData(
5856
rpc, PROGRAM_ID, [nullifier]
5957
);
@@ -72,23 +70,20 @@ describe("nullifier", () => {
7270
const sig = await rpc.sendTransaction(tx, [signer]);
7371
await confirmTx(rpc, sig);
7472

75-
console.log("Transaction signature:", sig);
73+
console.log("Tx:", sig);
7674

7775
const slot = await rpc.getSlot();
7876
await rpc.confirmTransactionIndexed(slot);
7977

8078
const accounts = await rpc.getCompressedAccountsByOwner(PROGRAM_ID);
8179
assert.ok(accounts.items.length > 0, "Nullifier account should be created");
82-
console.log("Created nullifier accounts:", accounts.items.length);
8380
});
8481
});
8582

8683
describe("Multiple nullifiers", () => {
8784
it("should create multiple nullifiers in one transaction", async () => {
8885
const nullifiers = [randomBytes32(), randomBytes32()];
8986

90-
console.log("Creating 3 nullifiers in one transaction...");
91-
9287
const { data, remainingAccounts } = await createNullifierInstructionData(
9388
rpc, PROGRAM_ID, nullifiers
9489
);
@@ -107,13 +102,10 @@ describe("nullifier", () => {
107102
const sig = await rpc.sendTransaction(tx, [signer]);
108103
await confirmTx(rpc, sig);
109104

110-
console.log("Transaction signature:", sig);
105+
console.log("Tx:", sig);
111106

112107
const slot = await rpc.getSlot();
113108
await rpc.confirmTransactionIndexed(slot);
114-
115-
const accounts = await rpc.getCompressedAccountsByOwner(PROGRAM_ID);
116-
console.log("Total nullifier accounts:", accounts.items.length);
117109
});
118110
});
119111
});

zk/zk-id/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
A minimal zk id Solana program that uses zero-knowledge proofs for identity verification with compressed accounts.
55
Note this is an example how to verify a zk inclusion proof, not a full zk identity protocol and not production-ready.
6+
67
For examples of zk identity protocols, see:
78
- [Iden3](https://github.com/iden3) - Full decentralized identity protocol with claims, revocation, and recovery
89
- [Semaphore](https://github.com/semaphore-protocol/semaphore) - Privacy-preserving group signaling with nullifiers
@@ -67,7 +68,7 @@ This script will:
6768

6869
## Build and Test
6970

70-
### Using Makefile (recommended)
71+
### Using Makefile
7172

7273
From the parent `zk/` directory:
7374

zk/zk-id/ts-tests/zk-id.test.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -241,17 +241,13 @@ describe("zk-id", () => {
241241

242242
const accounts = await rpc.getCompressedAccountsByOwner(PROGRAM_ID);
243243
assert.ok(accounts.items.length > 0, "Issuer account should be created");
244-
console.log("Issuer account created");
245244
});
246245
});
247246

248247
describe("Credential lifecycle", () => {
249248
it("should generate credential keypair correctly", () => {
250249
const { privateKey, publicKey } = generateCredentialKeypair();
251250

252-
console.log("Credential private key:", Buffer.from(privateKey).toString("hex").slice(0, 16) + "...");
253-
console.log("Credential public key:", Buffer.from(publicKey).toString("hex").slice(0, 16) + "...");
254-
255251
const hash = poseidon([BigInt("0x" + Buffer.from(privateKey).toString("hex"))]);
256252
const computedPublicKey = bigintToBytes32(poseidon.F.toObject(hash));
257253

@@ -260,18 +256,13 @@ describe("zk-id", () => {
260256
Array.from(computedPublicKey),
261257
"Public key should be Poseidon(privateKey)"
262258
);
263-
264-
console.log("Keypair verified: publicKey = Poseidon(privateKey)");
265259
});
266260

267261
it("should compute nullifier correctly", () => {
268262
const { privateKey } = generateCredentialKeypair();
269263
const verificationId = generateFieldElement();
270264
const nullifier = computeNullifier(verificationId, privateKey);
271265

272-
console.log("Verification ID:", Buffer.from(verificationId).toString("hex").slice(0, 16) + "...");
273-
console.log("Nullifier:", Buffer.from(nullifier).toString("hex").slice(0, 16) + "...");
274-
275266
const hash = poseidon([
276267
BigInt("0x" + Buffer.from(verificationId).toString("hex")),
277268
BigInt("0x" + Buffer.from(privateKey).toString("hex")),
@@ -283,15 +274,12 @@ describe("zk-id", () => {
283274
Array.from(computedNullifier),
284275
"Nullifier should be Poseidon(verification_id, privateKey)"
285276
);
286-
287-
console.log("Nullifier verified: nullifier = Poseidon(verification_id, credentialPrivateKey)");
288277
});
289278
});
290279

291280
describe("ZK credential verification", () => {
292281
it("should demonstrate full ZK credential proof flow", async () => {
293282
const { privateKey: credentialPrivateKey, publicKey: credentialPubkey } = generateCredentialKeypair();
294-
console.log("\n=== Step 1: Credential keypair generated ===");
295283

296284
const ownerHashed = hashToBn254Field(PROGRAM_ID.toBytes());
297285
const merkleTreeHashed = hashToBn254Field(
@@ -304,7 +292,6 @@ describe("zk-id", () => {
304292

305293
const verificationId = generateFieldElement();
306294
const nullifier = computeNullifier(verificationId, credentialPrivateKey);
307-
console.log("\n=== Step 2: Nullifier computed ===");
308295

309296
const encryptedDataHash = generateFieldElement();
310297
const address = generateFieldElement();
@@ -329,9 +316,6 @@ describe("zk-id", () => {
329316
}
330317
const expectedRoot = bigintToBytes32(current);
331318

332-
console.log("\n=== Step 3: Public inputs prepared ===");
333-
console.log("\n=== Step 4: Generating ZK proof ===");
334-
335319
const zkProof = await generateCredentialProof({
336320
ownerHashed,
337321
merkleTreeHashed,
@@ -352,9 +336,6 @@ describe("zk-id", () => {
352336
assert.ok(zkProof.a.length === 32, "Proof A should be 32 bytes");
353337
assert.ok(zkProof.b.length === 64, "Proof B should be 64 bytes");
354338
assert.ok(zkProof.c.length === 32, "Proof C should be 32 bytes");
355-
356-
console.log("\n=== ZK Proof generated successfully! ===");
357-
console.log("Proof size: 128 bytes (compressed Groth16)");
358339
});
359340

360341
it("should verify nullifier uniqueness property", () => {
@@ -371,8 +352,6 @@ describe("zk-id", () => {
371352
Array.from(nullifier2),
372353
"Different verification IDs should produce different nullifiers"
373354
);
374-
375-
console.log("Nullifier uniqueness verified");
376355
});
377356
});
378357
});

0 commit comments

Comments
 (0)