mcpField
Z
zk-nullifier

zk-nullifier

github.com/lightprotocol开发735 次安装20 周前更新
代码仓库

关于 zk-nullifier

为 Solana ZK 程序开发提供 nullifier 实现模式与参考资料,面向需要防止重复操作的开发者。内容涵盖压缩 PDA、证明验证和客户端地址派生。

主要功能

  1. 01介绍以 nullifier 防止重复操作
  2. 02说明 PDA 与压缩 PDA 存储成本
  3. 03提供地址派生及程序实现模式
  4. 04列出 Rust 与 TypeScript 测试命令

适用场景

  1. 01为 Solana 程序实现防重复花费
  2. 02参考压缩 PDA 创建 nullifier
  3. 03测试 Rust 或 TypeScript nullifier 客户端

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/lightprotocol/skills --skill zk-nullifier

安装后在 Claude Code 中输入/zk-nullifier确认已加载。

详细介绍

ZK Nullifiers

概述

构建 ZK Solana 程序需要:

  • Nullifier,以防止双重支付
  • 证明验证
  • 用于存储状态的 Merkle 树
  • 用于提供 Merkle 证明的索引器
  • 加密状态

对于非 ZK 应用,请参阅此技能以使用 nullifier:skills/payments

工作流

  1. 明确意图
    • 如果尚未启用,建议使用计划模式
    • 使用 AskUserQuestion 解决盲点
    • 执行前必须解决所有问题
  2. 确定参考资料和技能
    • 将任务与下方的资源对应
    • 查找相关文档和示例
  3. 编写计划文件(YAML 任务格式)
    • 对于任何不明确之处,使用 AskUserQuestion 提问——绝不猜测或擅自假设
    • 确定阻碍因素:权限、依赖项、未知事项
    • 执行开始前必须完成计划
  4. 执行
    • 使用 Task 工具和子 Agent 并行调研
    • 子 Agent 通过 Skill 工具加载技能
    • 使用 TodoWrite 跟踪进度
  5. 遇到困难时:请求启动一个只读子 Agent,使用 Read、Glob、Grep 和 DeepWiki MCP 访问权限,并加载 skills/ask-mcp。将读取范围限定为技能参考资料、示例仓库和文档。

Solana 上的 Nullifier

Nullifier 是一种确定性派生的哈希,可确保某项操作只能执行一次。Nullifier 无法关联到该操作或用户。例如,Zcash 使用 nullifier 防止双重支付。

要实现 nullifier,我们需要一种数据结构,确保每个 nullifier 只创建一次且永不删除。在 Solana 上,实现 nullifier 的一种直接方式是创建一个以 nullifier 为种子的 PDA 账户。

PDA 账户无法关闭,并会永久锁定 890,880 lamports(每个 nullifier 的免租豁免额)。 压缩 PDA 的派生方式与 Solana PDA 类似,创建成本为 15,000 lamports(无需免租豁免额)。

存储方式每个 nullifier 的成本
PDA890,880 lamports
压缩 PDA15,000 lamports

测试

# Rust tests
cargo test-sbf -p nullifier

# TypeScript tests (requires light test-validator)
light test-validator  # separate terminal
npm run test:ts

模式概览

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

资源

参考实现

源代码:program-examples/zk/nullifier

账户结构
#[derive(Clone, Debug, Default, BorshSerialize, BorshDeserialize, LightDiscriminator)]
pub struct NullifierAccount {}

空结构体,因为仅凭账户存在就能证明该 nullifier 已被使用。

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

地址根据以下内容确定性派生:

  • 固定前缀(防止与其他账户类型冲突)
  • Nullifier 值(32 字节)
  • 地址树公钥
  • 程序 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,
}
创建 Nullifier 函数
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>,
}

客户端实现(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();

客户端实现(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)?;

SDK 参考资料

软件包链接
light-sdkdocs.rs
@lightprotocol/stateless.jsAPI 文档
light-clientdocs.rs
@lightprotocol/nullifier-programnpm
light-nullifier-programcrates.io
light-program-testdocs.rs

安全性

此技能仅提供代码模式和文档参考。

  • 不使用凭据。 此技能无需 API 密钥、私钥或签名密钥。已明确声明 env: []。
  • 子 Agent 的范围。 遇到困难时,此技能会要求启动一个只读子 Agent,并使用 Read、Glob、Grep,将范围限定在技能参考资料、示例仓库和文档中。
  • 安装来源。 使用 Lightprotocol/skills 中的 npx skills add Lightprotocol/skills。
  • 已审计的协议。 Light Protocol 智能合约已通过独立审计。报告发布在 github.com/Lightprotocol/light-protocol/tree/main/audits。

评论

支持 Markdown