Skip to main content
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 IDNFLx5WGPrTHHvdRNsidcrNcLxRruMC92E4yv7zhZBoT
NetworksMainnet, Devnet
Source codegithub.com/Lightprotocol/nullifier-program
Example TxSolana Explorer
For the usage example source code, see here: create_nullifier.rs

How It Works

  1. Derives PDA from ["nullifier", id] seeds (where id is your unique identifier, e.g. a nonce, uuid, hash of signature, etc.)
  2. Creates an empty rent-free PDA at that address
  3. If the address exists, the whole transaction fails
  4. Prepend or append this instruction to your transaction.

Dependencies

[dependencies]
light-nullifier-program = "0.1.2"
light-client = "0.19.0"

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,
);

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,
);

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();

Note that this is a reference implementation. Feel free to fork the program as you see fit.
Questions or need support? Telegram | email | Discord