zk-nullifier

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

ZK Nullifiers

ZK Nullifiers

Overview

概述

Building a ZK Solana program requires:
  • Nullifiers to prevent double spending
  • Proof verification
  • A Merkle tree to store state
  • An indexer to serve Merkle proofs
  • Encrypted state
For non zk applications see this skill to use nullifiers: skills/payments
构建ZK Solana程序需要:
  • 用于防止双重支付的Nullifier(无效化器)
  • 证明验证
  • 用于存储状态的Merkle树
  • 用于提供Merkle证明的索引器
  • 加密状态
对于非ZK应用,请查看此技能以使用nullifiers:skills/payments

Workflow

工作流程

  1. Clarify intent
    • Recommend plan mode, if it's not activated
    • Use
      AskUserQuestion
      to resolve blind spots
    • All questions must be resolved before execution
  2. Identify references and skills
    • Match task to resources below
    • Locate relevant documentation and examples
  3. Write plan file (YAML task format)
    • Use
      AskUserQuestion
      for anything unclear — never guess or assume
    • Identify blockers: permissions, dependencies, unknowns
    • Plan must be complete before execution begins
  4. Execute
    • Use
      Task
      tool with subagents for parallel research
    • Subagents load skills via
      Skill
      tool
    • Track progress with
      TodoWrite
  5. When stuck: spawn subagent with
    Read
    ,
    Glob
    ,
    Grep
    , DeepWiki MCP access and load
    skills/ask-mcp
  1. 明确意图
    • 如果计划模式未激活,建议开启
    • 使用
      AskUserQuestion
      解决盲区
    • 执行前必须解决所有问题
  2. 识别参考资料和技能
    • 将任务与下方的资源匹配
    • 定位相关文档和示例
  3. 编写计划文件(YAML任务格式)
    • 任何不清楚的地方都使用
      AskUserQuestion
      ——绝不猜测或假设
    • 识别障碍:权限、依赖项、未知因素
    • 计划必须完整才能开始执行
  4. 执行
    • 使用
      Task
      工具和子代理进行并行研究
    • 子代理通过
      Skill
      工具加载技能
    • 使用
      TodoWrite
      跟踪进度
  5. 遇到问题时:生成带有
    Read
    Glob
    Grep
    、DeepWiki MCP访问权限的子代理,并加载
    skills/ask-mcp

Nullifiers on Solana

Solana上的Nullifier

A nullifier is a deterministically derived hash to ensure an action can only be performed once. The nullifier cannot be linked to the action or user. For example Zcash uses nullifiers to prevent double spending.
To implement nullifiers we need a data structure that ensures every nullifier is only created once and never deleted. On Solana a straight forward way to implement nullifiers is to create a PDA account with the nullifier as seed.
PDA accounts cannot be closed and permanently lock 890,880 lamports (per nullifier rent-exemption). Compressed PDAs are derived similar to Solana PDAs and cost 15,000 lamports to create (no rent-exemption).
StorageCost per nullifier
PDA890,880 lamports
Compressed PDA15,000 lamports
Nullifier是一种确定性派生的哈希,用于确保某个操作只能执行一次。Nullifier无法与操作或用户关联。例如Zcash使用nullifier来防止双重支付。
要实现nullifier,我们需要一种数据结构来确保每个nullifier只创建一次且永不删除。在Solana上,实现nullifier的一种直接方式是创建以nullifier为种子的PDA账户。
PDA账户无法关闭,并且每个nullifier需要永久锁定890,880个lamports(达到免租金要求)。 压缩PDA的派生方式与Solana PDA类似,创建成本为15,000个lamports(无免租金要求)。
存储方式每个nullifier的成本
PDA890,880 lamports
压缩PDA15,000 lamports

Testing

测试

bash
undefined
bash
undefined

Rust tests

Rust测试

cargo test-sbf -p nullifier
cargo test-sbf -p nullifier

TypeScript tests (requires light test-validator)

TypeScript测试(需要轻量测试验证器)

light test-validator # separate terminal npm run test:ts
undefined
light test-validator # 单独终端 npm run test:ts
undefined

Pattern Overview

模式概述

1. 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
1. 客户端计算nullifier = hash(secret, context)
2. 客户端获取派生地址的有效性证明(证明该地址不存在)
3. 客户端调用create_nullifier并传入nullifier值和证明
4. 程序从nullifier派生地址,通过CPI创建压缩账户
5. 如果地址已存在,轻量系统程序会拒绝CPI调用

Resources

资源

Reference Implementation

参考实现

Account Structure

账户结构

rust
#[derive(Clone, Debug, Default, BorshSerialize, BorshDeserialize, LightDiscriminator)]
pub struct NullifierAccount {}
Empty struct since existence alone proves the nullifier was used.
rust
#[derive(Clone, Debug, Default, BorshSerialize, BorshDeserialize, LightDiscriminator)]
pub struct NullifierAccount {}
空结构体,仅通过账户存在性即可证明nullifier已被使用。

Address Derivation

地址派生

rust
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
);
Address is deterministically derived from:
  • Constant prefix (prevents collisions with other account types)
  • Nullifier value (32 bytes)
  • Address tree pubkey
  • Program ID
rust
pub const NULLIFIER_PREFIX: &[u8] = b"nullifier";

let (address, address_seed) = derive_address(
    &[NULLIFIER_PREFIX, nullifier.as_slice()],  // 种子
    &address_tree_pubkey,                        // 地址树
    &program_id,                                 // 程序ID
);
地址由以下内容确定性派生:
  • 常量前缀(防止与其他账户类型冲突)
  • Nullifier值(32字节)
  • 地址树公钥
  • 程序ID

Instruction Data

指令数据

rust
#[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,
}
rust
#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)]
pub struct NullifierInstructionData {
    pub proof: ValidityProof,           // 证明地址不存在的ZK证明
    pub address_tree_info: PackedAddressTreeInfo,
    pub output_state_tree_index: u8,
    pub system_accounts_offset: u8,
}

Create Nullifiers Function

创建Nullifier函数

rust
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(())
}
rust
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 Entry Point

程序入口点

rust
#[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>,
}
rust
#[program]
pub mod nullifier {
    pub fn create_nullifier<'info>(
        ctx: Context<'_, '_, '_, 'info, CreateNullifierAccounts<'info>>,
        data: NullifierInstructionData,
        nullifiers: Vec<[u8; 32]>,
    ) -> Result<()> {
        // 在此处验证你的ZK证明。将nullifiers作为公开输入。
        // 示例:
        // 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>,
}

Client Implementation (TypeScript)

客户端实现(TypeScript)

typescript
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();
typescript
const NULLIFIER_PREFIX = Buffer.from("nullifier");
const addressTree = new web3.PublicKey(batchAddressTree);

// 为每个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()) };
});

// 获取有效性证明(证明地址不存在)
const proofResult = await rpc.getValidityProofV0([], addressesWithTree);

// 构建剩余账户
const remainingAccounts = new PackedAccounts();
remainingAccounts.addSystemAccountsV2(SystemAccountMetaConfig.new(programId));
const addressMerkleTreeIndex = remainingAccounts.insertOrGet(addressTree);
const outputStateTreeIndex = remainingAccounts.insertOrGet(outputStateTree);

// 构建指令数据
const data = {
    proof: { 0: proofResult.compressedProof },
    addressTreeInfo: {
        addressMerkleTreePubkeyIndex: addressMerkleTreeIndex,
        addressQueuePubkeyIndex: addressMerkleTreeIndex,
        rootIndex: proofResult.rootIndices[0],
    },
    outputStateTreeIndex,
    systemAccountsOffset: systemStart,
};

// 调用程序
const ix = await program.methods
    .createNullifier(data, nullifiers.map((n) => Array.from(n)))
    .accounts({ signer: signer.publicKey })
    .remainingAccounts(remainingAccounts)
    .instruction();

Client Implementation (Rust)

客户端实现(Rust)

rust
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)?;
rust
use light_sdk::address::v2::derive_address;

let address_tree_info = rpc.get_address_tree_v2();

// 派生地址
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();

// 获取有效性证明(空哈希=非包含证明)
let rpc_result = rpc
    .get_validity_proof(vec![], address_with_trees, None)
    .await?
    .value;

// 构建账户
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)?;

SDK references

SDK参考

PackageLink
light-sdk
docs.rs
@lightprotocol/stateless.js
API docs
light-client
docs.rs
@lightprotocol/nullifier-program
npm
light-nullifier-program
crates.io
light-program-test
docs.rs
链接
light-sdk
docs.rs
@lightprotocol/stateless.js
API docs
light-client
docs.rs
@lightprotocol/nullifier-program
npm
light-nullifier-program
crates.io
light-program-test
docs.rs