Loading...
Loading...
Use for privacy preserving applications and ZK Solana programs. Prevents double spending using rent-free PDAs.
npx skill4agent add lightprotocol/skills zk-nullifierAskUserQuestionAskUserQuestionTaskSkillTodoWriteReadGlobGrepskills/ask-mcp| Storage | Cost per nullifier |
|---|---|
| PDA | 890,880 lamports |
| Compressed PDA | 15,000 lamports |
# Rust tests
cargo test-sbf -p nullifier
# TypeScript tests (requires light test-validator)
light test-validator # separate terminal
npm run test:ts1. Client computes nullifier = hash(secret, context)
2. Client fetches validity proof for derived address (proves it does not exist)
3. Client calls create_nullifier with nullifier values and proof
4. Program derives address from nullifier, creates compressed account via CPI
5. Light system program rejects CPI if address already exists#[derive(Clone, Debug, Default, BorshSerialize, BorshDeserialize, LightDiscriminator)]
pub struct NullifierAccount {}pub const NULLIFIER_PREFIX: &[u8] = b"nullifier";
let (address, address_seed) = derive_address(
&[NULLIFIER_PREFIX, nullifier.as_slice()], // seeds
&address_tree_pubkey, // address tree
&program_id, // program ID
);#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)]
pub struct NullifierInstructionData {
pub proof: ValidityProof, // ZK proof that addresses don't exist
pub address_tree_info: PackedAddressTreeInfo,
pub output_state_tree_index: u8,
pub system_accounts_offset: u8,
}pub fn create_nullifiers<'info>(
nullifiers: &[[u8; 32]],
data: NullifierInstructionData,
signer: &AccountInfo<'info>,
remaining_accounts: &[AccountInfo<'info>],
) -> Result<()> {
let light_cpi_accounts = CpiAccounts::new(
signer,
&remaining_accounts[data.system_accounts_offset as usize..],
LIGHT_CPI_SIGNER,
);
let address_tree_pubkey = data
.address_tree_info
.get_tree_pubkey(&light_cpi_accounts)
.map_err(|_| ErrorCode::AccountNotEnoughKeys)?;
let mut cpi_builder = LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, data.proof);
let mut new_address_params: Vec<NewAddressParamsAssignedPacked> =
Vec::with_capacity(nullifiers.len());
for (i, nullifier) in nullifiers.iter().enumerate() {
let (address, address_seed) = derive_address(
&[NULLIFIER_PREFIX, nullifier.as_slice()],
&address_tree_pubkey,
&crate::ID,
);
let nullifier_account = LightAccount::<NullifierAccount>::new_init(
&crate::ID,
Some(address),
data.output_state_tree_index,
);
cpi_builder = cpi_builder.with_light_account(nullifier_account)?;
new_address_params.push(
data.address_tree_info
.into_new_address_params_assigned_packed(address_seed, Some(i as u8)),
);
}
cpi_builder
.with_new_addresses(&new_address_params)
.invoke(light_cpi_accounts)?;
Ok(())
}#[program]
pub mod nullifier {
pub fn create_nullifier<'info>(
ctx: Context<'_, '_, '_, 'info, CreateNullifierAccounts<'info>>,
data: NullifierInstructionData,
nullifiers: Vec<[u8; 32]>,
) -> Result<()> {
// Verify your ZK proof here. Use nullifiers as public inputs.
// Example:
// let public_inputs = [...nullifiers, ...your_other_inputs];
// Groth16Verifier::new(...).verify()?;
create_nullifiers(
&nullifiers,
data,
ctx.accounts.signer.as_ref(),
ctx.remaining_accounts,
)
}
}
#[derive(Accounts)]
pub struct CreateNullifierAccounts<'info> {
#[account(mut)]
pub signer: Signer<'info>,
}const NULLIFIER_PREFIX = Buffer.from("nullifier");
const addressTree = new web3.PublicKey(batchAddressTree);
// Derive addresses for each nullifier
const addressesWithTree = nullifiers.map((nullifier) => {
const seed = deriveAddressSeedV2([NULLIFIER_PREFIX, nullifier]);
const address = deriveAddressV2(seed, addressTree, programId);
return { tree: addressTree, queue: addressTree, address: bn(address.toBytes()) };
});
// Get validity proof (proves addresses don't exist)
const proofResult = await rpc.getValidityProofV0([], addressesWithTree);
// Build remaining accounts
const remainingAccounts = new PackedAccounts();
remainingAccounts.addSystemAccountsV2(SystemAccountMetaConfig.new(programId));
const addressMerkleTreeIndex = remainingAccounts.insertOrGet(addressTree);
const outputStateTreeIndex = remainingAccounts.insertOrGet(outputStateTree);
// Build instruction data
const data = {
proof: { 0: proofResult.compressedProof },
addressTreeInfo: {
addressMerkleTreePubkeyIndex: addressMerkleTreeIndex,
addressQueuePubkeyIndex: addressMerkleTreeIndex,
rootIndex: proofResult.rootIndices[0],
},
outputStateTreeIndex,
systemAccountsOffset: systemStart,
};
// Call program
const ix = await program.methods
.createNullifier(data, nullifiers.map((n) => Array.from(n)))
.accounts({ signer: signer.publicKey })
.remainingAccounts(remainingAccounts)
.instruction();use light_sdk::address::v2::derive_address;
let address_tree_info = rpc.get_address_tree_v2();
// Derive addresses
let address_with_trees: Vec<AddressWithTree> = nullifiers
.iter()
.map(|n| {
let (address, _) = derive_address(
&[NULLIFIER_PREFIX, n.as_slice()],
&address_tree_info.tree,
&program_id,
);
AddressWithTree {
address,
tree: address_tree_info.tree,
}
})
.collect();
// Get validity proof (empty hashes = non-inclusion proof)
let rpc_result = rpc
.get_validity_proof(vec![], address_with_trees, None)
.await?
.value;
// Build accounts
let mut remaining_accounts = PackedAccounts::default();
let config = SystemAccountMetaConfig::new(program_id);
remaining_accounts.add_system_accounts_v2(config)?;
let packed_address_tree_accounts = rpc_result
.pack_tree_infos(&mut remaining_accounts)
.address_trees;
let output_state_tree_index = rpc
.get_random_state_tree_info()?
.pack_output_tree_index(&mut remaining_accounts)?;