mcpField
Z
zk-nullifier

zk-nullifier

github.com/lightprotocol開発735 回インストール20 週間前に更新
リポジトリ

zk-nullifier について

Solana の ZK プログラムで nullifier を実装する開発者向けに、実装パターンと参考資料をまとめたスキルです。圧縮 PDA、証明検証、クライアント側のアドレス導出を扱います。

主な機能

  1. 01重複操作を防ぐ nullifier の解説
  2. 02PDA と圧縮 PDA の保存コスト比較
  3. 03アドレス導出とプログラム実装例
  4. 04Rust と TypeScript のテストコマンド

活用シーン

  1. 01Solana プログラムで二重支払いを防止
  2. 02圧縮 PDA による nullifier 作成の参照
  3. 03Rust または TypeScript クライアントのテスト

インストール

コマンドをコピーしてターミナルで実行します。

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

インストール後、Claude Code で/zk-nullifierを入力して読み込まれたか確認します。

詳細

ZK Nullifiers

概要

ZK Solana プログラムを構築するには、以下が必要です。

  • 二重支払いを防ぐヌリファイア
  • 証明の検証
  • 状態を保存するマークルツリー
  • マークル証明を提供するインデクサー
  • 暗号化された状態

ZK ではないアプリケーションでヌリファイアを使用する場合は、skills/payments のスキルを参照してください。

ワークフロー

  1. 意図を明確にする
    • プランモードが有効になっていない場合は、有効にすることを推奨します
    • AskUserQuestion を使って不明点を解消します
    • 実行を開始する前に、すべての質問を解決する必要があります
  2. 参考資料とスキルを特定する
    • タスクに合った参考資料を以下のリソースから選びます
    • 関連するドキュメントとサンプルを探します
  3. 計画ファイルを作成する(YAML タスク形式)
    • 不明な点があれば AskUserQuestion を使います。推測や思い込みは避けてください
    • ブロッカーを特定します。権限、依存関係、不明点などです
    • 実行を開始する前に、計画を完成させる必要があります
  4. 実行する
    • Task ツールでサブエージェントを使い、並行して調査します
    • サブエージェントは Skill ツールを使ってスキルを読み込みます
    • TodoWrite で進捗を管理します
  5. 行き詰まった場合:Read、Glob、Grep、DeepWiki MCP アクセスを使用し、skills/ask-mcp を読み込む読み取り専用サブエージェントの起動を依頼します。読み取り対象は、スキルの参考資料、サンプルリポジトリ、ドキュメントに限定してください。

Solana のヌリファイア

ヌリファイアは、あるアクションを一度だけ実行できるようにするために決定論的に導出されたハッシュです。ヌリファイアからアクションやユーザーを特定することはできません。たとえば、Zcash は二重支払いを防ぐためにヌリファイアを使用しています。

ヌリファイアを実装するには、それぞれのヌリファイアが一度だけ作成され、削除されないことを保証するデータ構造が必要です。Solana では、ヌリファイアをシードとして PDA アカウントを作成するのが、ヌリファイアを実装する簡単な方法です。

PDA アカウントは閉じることができず、890,880 lamports が恒久的にロックされます(ヌリファイアごとに必要な最低残高)。 圧縮 PDA は Solana の PDA と同様に導出され、作成コストは 15,000 lamports です(最低残高は不要)。

ストレージヌリファイアあたりのコスト
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 {}

存在することだけでヌリファイアが使用済みであると証明できるため、空の構造体です。

アドレスの導出
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
);

アドレスは以下から決定論的に導出されます。

  • 定数プレフィックス(他のアカウント型との衝突を防止)
  • ヌリファイア値(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,
}
ヌリファイア作成関数
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: [] が明示的に宣言されています。
  • サブエージェントの範囲。 行き詰まった場合、このスキルは、スキルの参考資料、サンプルリポジトリ、ドキュメントに範囲を限定して、Read、Glob、Grep を使用する読み取り専用サブエージェントの起動を求めます。
  • インストール元。 Lightprotocol/skills から npx skills add Lightprotocol/skills を実行します。
  • 監査済みプロトコル。 Light Protocol のスマートコントラクトは、独立した監査を受けています。監査レポートは github.com/Lightprotocol/light-protocol/tree/main/audits で公開されています。

コメント