Documentation Index
Fetch the complete documentation index at: https://luminouslabs-cc5545c6-bump-sdks.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
For some use cases, such as sending payments, you might want to prevent your
onchain instruction from being executed more than once.
The nullifier program utility solves this for you.
We also deployed a reference implementation to public networks so you can get started quickly:
| |
|---|
| Program ID | NFLx5WGPrTHHvdRNsidcrNcLxRruMC92E4yv7zhZBoT |
| Networks | Mainnet, Devnet |
| Source code | github.com/Lightprotocol/nullifier-program |
| Example Tx | Solana Explorer |
How It Works
- Derives PDA from
["nullifier", id] seeds (where id is your unique identifier, e.g. a nonce, uuid, hash of signature, etc.)
- Creates an empty rent-free PDA at that address
- If the address exists, the whole transaction fails
- Prepend or append this instruction to your transaction.
Dependencies
[dependencies]
light-nullifier-program = "0.1.2"
light-client = "0.23.0"
npm install @lightprotocol/nullifier-program @lightprotocol/stateless.js@beta
Using the Helper
use light_nullifier_program::sdk::{create_nullifier_ix, PROGRAM_ID};
use light_client::{LightClient, LightClientConfig};
use solana_sdk::{system_instruction, transaction::Transaction};
let mut rpc = LightClient::new(
LightClientConfig::new("https://mainnet.helius-rpc.com/?api-key=...")
).await?;
// Create a unique 32-byte ID
let id: [u8; 32] = /* hash of payment inputs or random */;
// Build nullifier instruction
let nullifier_ix = create_nullifier_ix(&mut rpc, payer.pubkey(), id).await?;
// Combine with your transaction
let transfer_ix = system_instruction::transfer(&payer.pubkey(), &recipient, 1_000_000);
let tx = Transaction::new_signed_with_payer(
&[nullifier_ix, transfer_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
import { createNullifierIx } from "@lightprotocol/nullifier-program";
import { createRpc } from "@lightprotocol/stateless.js";
import { Transaction, SystemProgram, ComputeBudgetProgram } from "@solana/web3.js";
const rpc = createRpc("https://mainnet.helius-rpc.com/?api-key=...");
// Create a unique 32-byte ID (e.g., hash of payment inputs)
const id = new Uint8Array(32);
crypto.getRandomValues(id);
// Build nullifier instruction
const nullifierIx = await createNullifierIx(rpc, payer.publicKey, id);
// Combine with your transaction
const tx = new Transaction().add(
nullifierIx,
// your main instruction
SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: recipient,
lamports: 1_000_000,
})
);
Manually Fetching Proof
use light_nullifier_program::sdk::{fetch_proof, build_instruction};
// Step 1: Fetch proof
let proof_result = fetch_proof(&mut rpc, &id).await?;
// Step 2: Build instruction
let nullifier_ix = build_instruction(payer.pubkey(), id, proof_result);
// Add to transaction
let tx = Transaction::new_signed_with_payer(
&[nullifier_ix, transfer_ix],
Some(&payer.pubkey()),
&[&payer],
recent_blockhash,
);
import { fetchProof, buildInstruction } from "@lightprotocol/nullifier-program";
// Step 1: Fetch proof
const proofResult = await fetchProof(rpc, id);
// Step 2: Build instruction
const nullifierIx = buildInstruction(payer.publicKey, id, proofResult);
// Combine with your transaction
const tx = new Transaction().add(
nullifierIx,
// your main instruction
SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: recipient,
lamports: 1_000_000,
})
);
Check If Nullifier Exists
use light_nullifier_program::sdk::derive_nullifier_address;
let address = derive_nullifier_address(&id);
let account = rpc.get_compressed_account(None, Some(address)).await?;
let exists = account.value.is_some();
import { deriveNullifierAddress } from "@lightprotocol/nullifier-program";
import { bn } from "@lightprotocol/stateless.js";
const address = deriveNullifierAddress(id);
const account = await rpc.getCompressedAccount(bn(address.toBytes()));
const exists = account !== null;
Note that this is a reference implementation. Feel free to fork the program as you see fit.Create rent-free nullifier PDAs to prevent duplicate actions
---
description: Create rent-free nullifier PDAs to prevent duplicate actions
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion, Task, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskOutput, mcp__deepwiki, mcp__zkcompression
---
## Create rent-free nullifier PDAs to prevent duplicate actions
Context:
- Guide: https://zkcompression.com/pda/compressed-pdas/guides/how-to-create-nullifier-pdas
- Skills and resources index: https://zkcompression.com/skill.md
- Dedicated skill: https://github.com/Lightprotocol/skills/tree/main/skills/zk-nullifier
- Rust crates: light-nullifier-program, light-client
- TS packages: @lightprotocol/nullifier-program, @lightprotocol/stateless.js
- Example: https://github.com/Lightprotocol/examples-light-token/blob/main/rust-client/actions/create_nullifier.rs
- Program source: https://github.com/Lightprotocol/nullifier-program/
Key APIs:
- Rust: create_nullifier_ix(), fetch_proof(), build_instruction(), derive_nullifier_address()
- TS: createNullifierIx(), fetchProof(), buildInstruction(), deriveNullifierAddress()
### 1. Index project
- Grep `nullifier|create_nullifier|createNullifierIx|deriveNullifierAddress|NFLx5WGPrTHHvdRNsidcrNcLxRruMC92E4yv7zhZBoT` across src/
- Glob `**/*.rs` and `**/*.ts` for project structure
- Identify: existing transaction building, duplicate prevention logic, payment flow
- Check Cargo.toml or package.json for existing light-* dependencies
- Task subagent (Grep/Read/WebFetch) if project has multiple packages to scan in parallel
### 2. Read references
- WebFetch the guide above — review both Rust and TS code samples
- WebFetch skill.md — check for a dedicated skill and resources matching this task
- TaskCreate one todo per phase below to track progress
### 3. Clarify intention
- AskUserQuestion: Rust or TypeScript?
- AskUserQuestion: what is the goal? (prevent duplicate payments, idempotent instruction execution, other use case)
- AskUserQuestion: do you need the helper (create_nullifier_ix) or manual proof fetching (fetch_proof + build_instruction)?
- Summarize findings and wait for user confirmation before implementing
### 4. Create plan
- Based on steps 1–3, draft an implementation plan: which files to modify, what code to add, dependency changes
- Follow the guide's pattern: create unique 32-byte ID → build nullifier instruction → prepend to transaction
- If checking existence is needed, add derive_nullifier_address + get_compressed_account check
- If anything is unclear or ambiguous, loop back to step 3 (AskUserQuestion)
- Present the plan to the user for approval before proceeding
### 5. Implement
- For Rust: Bash `cargo add light-nullifier-program@0.1 light-client@0.19`
- For TypeScript: Bash `npm install @lightprotocol/nullifier-program @lightprotocol/stateless.js@beta`
- Follow the guide and the approved plan
- Write/Edit to create or modify files
- TaskUpdate to mark each step done
### 6. Verify
- Rust: Bash `cargo check` + `cargo test` if tests exist
- TypeScript: Bash `tsc --noEmit` + run existing test suite if present
- TaskUpdate to mark complete
### Tools
- mcp__zkcompression__SearchLightProtocol("<query>") for API details
- mcp__deepwiki__ask_question("Lightprotocol/light-protocol", "<q>") for architecture
- Task subagent with Grep/Read/WebFetch for parallel lookups
- TaskList to check remaining work